-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathScanner.php
More file actions
235 lines (205 loc) · 6.75 KB
/
Scanner.php
File metadata and controls
235 lines (205 loc) · 6.75 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 6.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\AttributeResolver;
use Cake\Core\Configure;
use Cake\Core\Plugin;
use Cake\Core\PluginConfig;
use Cake\Utility\Fs\Finder;
use EmptyIterator;
use Generator;
use Iterator;
use Throwable;
class Scanner
{
/**
* Maximum file size in bytes (10MB).
*/
protected const int MAX_FILE_SIZE = 10 * 1024 * 1024;
/**
* Cache for base paths with plugin information.
*
* @var array<array{path: string, plugin: string|null}>|null
*/
private ?array $basePaths = null;
/**
* List of files that were scanned.
*
* @var array<string>
*/
private array $scannedFiles = [];
/**
* @param \Cake\AttributeResolver\Parser $parser Attribute parser
* @param array<string> $paths Relative glob patterns to scan (e.g., ['src/**\/*.php'])
* @param array<string> $excludePaths Relative patterns to exclude (e.g., ['vendor/**', 'tests/**'])
* @param string|null $basePath Base directory path (defaults to ROOT + all plugins)
*/
public function __construct(
private Parser $parser,
private array $paths = [],
private array $excludePaths = [],
private ?string $basePath = null,
) {
}
/**
* Scan all configured paths and yield discovered attributes.
*
* Expands relative paths against APP root and all loaded plugin paths.
*
* @return \Generator<\Cake\AttributeResolver\ValueObject\AttributeInfo>
*/
public function scanAll(): Generator
{
$this->scannedFiles = [];
$finder = $this->buildFinder();
foreach ($finder as $file) {
$filePath = $file->getRealPath();
$this->scannedFiles[] = $filePath;
try {
$pluginName = $this->identifyPluginName($filePath);
yield from $this->parser->parseFile($file, $pluginName);
} catch (Throwable) {
// Skip files that fail to parse
continue;
}
}
}
/**
* Get the list of files that were scanned.
*
* @return array<string>
*/
public function getScannedFiles(): array
{
return $this->scannedFiles;
}
/**
* Resolve base paths with plugin information.
*
* @return array<array{path: string, plugin: string|null}>
*/
protected function resolveBasePaths(): array
{
if ($this->basePaths !== null) {
return $this->basePaths;
}
// Use custom basePath or default to ROOT
$basePaths = [
['path' => $this->basePath ?? ROOT, 'plugin' => null],
];
foreach ($this->getLoadedPlugins() as $pluginInfo) {
$basePaths[] = $pluginInfo;
}
$this->basePaths = $basePaths;
return $basePaths;
}
/**
* Get loaded plugins that should be scanned.
*
* Returns a consistent list of all loaded plugins regardless of execution context
* (CLI vs web). This ensures attribute discovery is atomic and cache is consistent.
*
* Excludes only:
* - Unknown plugins (configured but not installed)
* - Debug-only plugins when debug mode is disabled
*
* Includes CLI-only plugins even in web context to maintain cache consistency.
* Also includes plugins loaded dynamically via the Plugin class
* that may not be in the static configuration.
*
* @return array<array{path: string, plugin: string}>
*/
protected function getLoadedPlugins(): array
{
$installedPlugins = PluginConfig::getInstalledPlugins();
$debugMode = Configure::read('debug', false);
$result = [];
// Process plugins from PluginConfig
foreach ($installedPlugins as $pluginName => $config) {
// Skip plugins that shouldn't be included
if (
($config['isUnknown'] ?? false) ||
(($config['onlyDebug'] ?? false) && !$debugMode) ||
!isset($config['path'])
) {
continue;
}
$result[$pluginName] = [
'path' => $config['path'],
'plugin' => $pluginName,
];
}
// Merge dynamically loaded plugins not in PluginConfig
$collection = Plugin::getCollection();
foreach ($collection as $plugin) {
$pluginName = $plugin->getName();
if (!isset($result[$pluginName])) {
$result[$pluginName] = [
'path' => $plugin->getPath(),
'plugin' => $pluginName,
];
}
}
return array_values($result);
}
/**
* Build a Finder instance for scanning files.
*
* @return \Iterator
*/
protected function buildFinder(): Iterator
{
if ($this->paths === []) {
return new EmptyIterator();
}
$basePaths = $this->resolveBasePaths();
$directories = array_map(fn(array $info): string => $info['path'], $basePaths);
$finder = new Finder();
// Add all base directories
foreach ($directories as $dir) {
if (is_dir($dir)) {
$finder->in($dir);
}
}
// Apply relative path patterns
foreach ($this->paths as $pattern) {
$finder->pattern($pattern);
}
// Apply exclusions
foreach ($this->excludePaths as $excludePattern) {
$finder->notPath($excludePattern);
}
// Filter out files larger than 10MB
$finder->filter(function ($file) {
return $file->getSize() <= self::MAX_FILE_SIZE;
});
return $finder->files();
}
/**
* Identify which plugin a file belongs to based on its path.
*
* @param string $filePath Absolute file path
* @return string|null Plugin name or null if file is in APP
*/
private function identifyPluginName(string $filePath): ?string
{
foreach ($this->resolveBasePaths() as $baseInfo) {
if ($baseInfo['plugin'] !== null && str_starts_with($filePath, $baseInfo['path'])) {
return $baseInfo['plugin'];
}
}
return null;
}
}