ConsoleCommandList   A
last analyzed

Complexity

Total Complexity 23

Size/Duplication

Total Lines 157
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 23
eloc 51
c 0
b 0
f 0
dl 0
loc 157
rs 10

12 Methods

Rating   Name   Duplication   Size   Complexity  
A offsetUnset() 0 4 2
A cacheFile() 0 4 1
A __construct() 0 12 4
A loadCommands() 0 16 4
A getIterator() 0 3 1
A offsetExists() 0 3 1
A load() 0 18 4
A offsetGet() 0 3 1
A cacheData() 0 12 2
A count() 0 3 1
A offsetSet() 0 5 1
A fromCache() 0 3 1
1
<?php
2
3
/**
4
 * This file is part of web-stack
5
 *
6
 * For the full copyright and license information, please view the LICENSE
7
 * file that was distributed with this source code.
8
 */
9
10
declare(strict_types=1);
11
12
namespace Slick\WebStack\Infrastructure\Console\ConsoleCommandLoader;
13
14
use ArrayAccess;
15
use ArrayIterator;
16
use Countable;
17
use Exception;
18
use IteratorAggregate;
19
use RecursiveDirectoryIterator;
20
use RecursiveIteratorIterator;
21
use RegexIterator;
22
use Slick\FsWatch\Directory;
23
use Slick\WebStack\Infrastructure\Exception\InvalidConsoleLoaderPath;
24
use SplFileInfo;
25
26
/**
27
 * ConsoleCommandList
28
 *
29
 * @package Slick\WebStack\Infrastructure\Console\ConsoleCommandLoader
30
 * @implements IteratorAggregate<string, class-string>
31
 * @implements ArrayAccess<string, class-string>
32
 */
33
final class ConsoleCommandList implements IteratorAggregate, ArrayAccess, Countable
34
{
35
36
    /** @var array<string, class-string>  */
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<string, class-string> at position 4 could not be parsed: Unknown type name 'class-string' at position 4 in array<string, class-string>.
Loading history...
37
    private array $commands = [];
38
39
    private const TMP_FILE_NAME = '/_slick_console_list';
40
41
    private readonly Directory $directory;
42
43
    private bool $fromCache = false;
44
45
    /**
46
     * Creates a ConsoleCommandList
47
     *
48
     * @param Directory|string $directory
49
     */
50
    public function __construct(Directory|string $directory)
51
    {
52
        try {
53
            $this->directory = is_string($directory) ? new Directory($directory) : $directory;
0 ignored issues
show
Bug introduced by
The property directory is declared read-only in Slick\WebStack\Infrastru...ader\ConsoleCommandList.
Loading history...
54
            if ($this->load()) {
55
                return;
56
            }
57
            $this->loadCommands($this->directory->path());
58
        } catch (Exception) {
59
            throw new InvalidConsoleLoaderPath(
60
                'Provided commands path is not valid or is not found. ' .
61
                'Could not create command loader. Please check ' . $directory
0 ignored issues
show
Bug introduced by
Are you sure $directory of type Slick\FsWatch\Directory|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

61
                'Could not create command loader. Please check ' . /** @scrutinizer ignore-type */ $directory
Loading history...
62
            );
63
        }
64
    }
65
66
    /**
67
     * @inheritDoc
68
     * @return ArrayIterator<string, class-string>
69
     */
70
    public function getIterator(): ArrayIterator
71
    {
72
        return new ArrayIterator($this->commands);
73
    }
74
75
    /**
76
     * @inheritDoc
77
     */
78
    public function count(): int
79
    {
80
        return count($this->commands);
81
    }
82
83
    /**
84
     * ConsoleCommandList fromCache
85
     *
86
     * @return bool
87
     */
88
    public function fromCache(): bool
89
    {
90
        return $this->fromCache;
91
    }
92
93
    private function loadCommands(string $path): void
94
    {
95
        $directory = new RecursiveDirectoryIterator($path);
96
        $iterator = new RecursiveIteratorIterator($directory);
97
        $phpFiles = new RegexIterator($iterator, '/.*\.php$/i');
98
99
        /** @var SplFileInfo $phpFile */
100
        foreach ($phpFiles as $phpFile) {
101
            $classFile = new ClassFileDetails($phpFile);
102
            /** @var class-string $className */
103
            $className = $classFile->className();
104
            if ($classFile->isCommand() && $className) {
105
                $this->commands[$classFile->commandName()] = $className;
106
            }
107
        }
108
        $this->cacheData();
109
    }
110
111
    /**
112
     * @inheritDoc
113
     */
114
    public function offsetExists(mixed $offset): bool
115
    {
116
        return isset($this->commands[$offset]);
117
    }
118
119
    /**
120
     * @inheritDoc
121
     */
122
    public function offsetGet(mixed $offset): string
123
    {
124
        return $this->commands[$offset];
125
    }
126
127
    /**
128
     * @param mixed $offset
129
     * @param mixed|class-string $value
0 ignored issues
show
Documentation Bug introduced by
The doc comment mixed|class-string at position 2 could not be parsed: Unknown type name 'class-string' at position 2 in mixed|class-string.
Loading history...
130
     * @return void
131
     */
132
    public function offsetSet(mixed $offset, mixed $value): void
133
    {
134
        /** @var class-string $classString */
135
        $classString = (string)$value;
136
        $this->commands[(string) $offset] = $classString;
137
    }
138
139
    /**
140
     * @inheritDoc
141
     */
142
    public function offsetUnset(mixed $offset): void
143
    {
144
        if ($this->offsetExists($offset)) {
145
            unset($this->commands[$offset]);
146
        }
147
    }
148
149
    private function cacheData(): void
150
    {
151
        $file = $this->cacheFile();
152
        if (is_file($file)) {
153
            unlink($file);
154
        }
155
156
        $data = [
157
            'snapshot' => $this->directory->snapshot(),
158
            'commands' => $this->commands,
159
        ];
160
        file_put_contents($file, serialize($data));
161
    }
162
163
    private function load(): bool
164
    {
165
        $file = $this->cacheFile();
166
        if (!file_exists($file) || !$cacheFile = file_get_contents($file)) {
167
            return false;
168
        }
169
170
        $cachedData = unserialize($cacheFile);
171
172
        /** @var Directory\Snapshot $snapshot */
173
        $snapshot = $cachedData['snapshot'];
174
        if ($this->directory->hasChanged($snapshot)) {
175
            return false;
176
        }
177
178
        $this->commands = $cachedData['commands'];
179
        $this->fromCache = true;
180
        return true;
181
    }
182
183
    /**
184
     * @return string
185
     */
186
    private function cacheFile(): string
187
    {
188
        $names = explode('/', $this->directory->path());
189
        return sys_get_temp_dir() . self::TMP_FILE_NAME . "_" . trim(end($names));
190
    }
191
}
192