Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
115 / 115 |
|
100.00% |
28 / 28 |
CRAP | |
100.00% |
1 / 1 |
Autoloader | |
100.00% |
115 / 115 |
|
100.00% |
28 / 28 |
52 | |
100.00% |
1 / 1 |
__construct | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
register | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
unregister | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setNamespace | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
makeRenderedDirectoryPaths | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
addNamespace | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
3 | |||
sortNamespaces | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setNamespaces | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
addNamespaces | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
getNamespace | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getNamespaces | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
removeNamespace | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
removeNamespaces | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
setClass | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
setClasses | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
getClass | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getClasses | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
removeClass | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
removeClasses | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
findClassPath | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
6 | |||
loadClass | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
loadClassFile | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 | |||
loadDebug | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
1 | |||
setDebugCollector | |
100.00% |
20 / 20 |
|
100.00% |
1 / 1 |
4 | |||
getDebugCollector | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
renderRealName | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
renderFilePath | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
renderDirectoryPath | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 |
1 | <?php declare(strict_types=1); |
2 | /* |
3 | * This file is part of Aplus Framework Autoload Library. |
4 | * |
5 | * (c) Natan Felles <natanfelles@gmail.com> |
6 | * |
7 | * For the full copyright and license information, please view the LICENSE |
8 | * file that was distributed with this source code. |
9 | */ |
10 | namespace Framework\Autoload; |
11 | |
12 | use Framework\Autoload\Debug\AutoloadCollector; |
13 | use Framework\Debug\Collector; |
14 | use JetBrains\PhpStorm\Pure; |
15 | use RuntimeException; |
16 | |
17 | /** |
18 | * Class Autoloader. |
19 | * |
20 | * The Autoloader class allows to set namespace directories to search for files |
21 | * (PSR4) and set the absolute path of classes without namespaces (PSR0). |
22 | * |
23 | * @package autoload |
24 | */ |
25 | class Autoloader |
26 | { |
27 | /** |
28 | * List of classes to file paths. |
29 | * |
30 | * @var array<string,string> |
31 | */ |
32 | protected array $classes = []; |
33 | /** |
34 | * List of namespaces to directory paths. |
35 | * |
36 | * @var array<string,array<int,string>> |
37 | */ |
38 | protected array $namespaces = []; |
39 | protected AutoloadCollector $debugCollector; |
40 | |
41 | /** |
42 | * Autoloader constructor. |
43 | * |
44 | * @param bool $register Register the Autoloader::loadClass() as autoload |
45 | * implementation |
46 | * @param string $extensions A comma delimited list of file extensions for |
47 | * spl_autoload |
48 | * |
49 | * @see Autoloader::loadClass() |
50 | */ |
51 | public function __construct(bool $register = true, string $extensions = '.php') |
52 | { |
53 | if ($register) { |
54 | $this->register($extensions); |
55 | } |
56 | } |
57 | |
58 | /** |
59 | * Registers the Autoloader::loadClass() as autoload implementation. |
60 | * |
61 | * @param string $extensions A comma delimited list of file extensions for |
62 | * spl_autoload |
63 | * |
64 | * @see Autoloader::loadClass() |
65 | * |
66 | * @return bool True on success or false on failure |
67 | */ |
68 | public function register(string $extensions = '.php') : bool |
69 | { |
70 | \spl_autoload_extensions($extensions); |
71 | // @phpstan-ignore-next-line |
72 | return \spl_autoload_register([$this, 'loadClass'], true, false); |
73 | } |
74 | |
75 | /** |
76 | * Unregisters the Autoloader::loadClass() as autoload implementation. |
77 | * |
78 | * @see Autoloader::loadClass() |
79 | * |
80 | * @return bool True on success or false on failure |
81 | */ |
82 | public function unregister() : bool |
83 | { |
84 | return \spl_autoload_unregister([$this, 'loadClass']); |
85 | } |
86 | |
87 | /** |
88 | * Sets one namespace mapping for a directory path. |
89 | * |
90 | * @param string $namespace Namespace name |
91 | * @param array<string>|string $dir Directory path |
92 | * |
93 | * @return static |
94 | */ |
95 | public function setNamespace(string $namespace, array | string $dir) : static |
96 | { |
97 | $directories = $this->makeRenderedDirectoryPaths((array) $dir); |
98 | if ($directories) { |
99 | $this->namespaces[$this->renderRealName($namespace)] = $directories; |
100 | $this->sortNamespaces(); |
101 | } |
102 | return $this; |
103 | } |
104 | |
105 | /** |
106 | * @param array<string> $directories |
107 | * |
108 | * @return array<int,string> |
109 | */ |
110 | protected function makeRenderedDirectoryPaths(array $directories) : array |
111 | { |
112 | $paths = []; |
113 | foreach ($directories as $directory) { |
114 | $paths[] = $this->renderDirectoryPath($directory); |
115 | } |
116 | return $paths; |
117 | } |
118 | |
119 | /** |
120 | * Adds directory paths to a namespace. |
121 | * |
122 | * @param string $namespace Namespace name |
123 | * @param array<string>|string $dir Directory path |
124 | * |
125 | * @return static |
126 | */ |
127 | public function addNamespace(string $namespace, array | string $dir) : static |
128 | { |
129 | $directories = $this->makeRenderedDirectoryPaths((array) $dir); |
130 | if ($directories) { |
131 | $name = $this->renderRealName($namespace); |
132 | if (isset($this->namespaces[$name])) { |
133 | $directories = [...$this->namespaces[$name], ...$directories]; |
134 | } |
135 | $this->namespaces[$name] = $directories; |
136 | $this->sortNamespaces(); |
137 | } |
138 | return $this; |
139 | } |
140 | |
141 | protected function sortNamespaces() : void |
142 | { |
143 | \krsort($this->namespaces); |
144 | } |
145 | |
146 | /** |
147 | * Sets namespaces mapping for directory paths. |
148 | * |
149 | * @param array<string,array<string>|string> $namespaces Namespace names |
150 | * as keys and directory paths as values |
151 | * |
152 | * @return static |
153 | */ |
154 | public function setNamespaces(array $namespaces) : static |
155 | { |
156 | foreach ($namespaces as $name => $dir) { |
157 | $this->setNamespace($name, $dir); |
158 | } |
159 | $this->sortNamespaces(); |
160 | return $this; |
161 | } |
162 | |
163 | /** |
164 | * Adds directory paths to namespaces. |
165 | * |
166 | * @param array<string,array<string>|string> $namespaces Namespace names |
167 | * as keys and directory paths as values |
168 | * |
169 | * @return static |
170 | */ |
171 | public function addNamespaces(array $namespaces) : static |
172 | { |
173 | foreach ($namespaces as $name => $dir) { |
174 | $this->addNamespace($name, $dir); |
175 | } |
176 | return $this; |
177 | } |
178 | |
179 | /** |
180 | * Gets the directory paths for a given namespace. |
181 | * |
182 | * @param string $name Namespace name |
183 | * |
184 | * @return array<int,string> The namespace directory paths |
185 | */ |
186 | #[Pure] |
187 | public function getNamespace(string $name) : array |
188 | { |
189 | return $this->namespaces[$this->renderRealName($name)] ?? []; |
190 | } |
191 | |
192 | /** |
193 | * Gets all mapped namespaces. |
194 | * |
195 | * @return array<string,array<int,string>> |
196 | */ |
197 | #[Pure] |
198 | public function getNamespaces() : array |
199 | { |
200 | return $this->namespaces; |
201 | } |
202 | |
203 | /** |
204 | * Removes one namespace from the mapping. |
205 | * |
206 | * @param string $name Namespace name |
207 | * |
208 | * @return static |
209 | */ |
210 | public function removeNamespace(string $name) : static |
211 | { |
212 | unset($this->namespaces[$this->renderRealName($name)]); |
213 | return $this; |
214 | } |
215 | |
216 | /** |
217 | * Removes namespaces from the mapping. |
218 | * |
219 | * @param array<string> $names List of namespace names |
220 | * |
221 | * @return static |
222 | */ |
223 | public function removeNamespaces(array $names) : static |
224 | { |
225 | foreach ($names as $name) { |
226 | $this->removeNamespace($name); |
227 | } |
228 | return $this; |
229 | } |
230 | |
231 | /** |
232 | * Sets one class mapping for a file path. |
233 | * |
234 | * @param string $name Fully qualified class name (with namespace) |
235 | * @param string $filepath Class file path |
236 | * |
237 | * @return static |
238 | */ |
239 | public function setClass(string $name, string $filepath) : static |
240 | { |
241 | $this->classes[$this->renderRealName($name)] = $this->renderFilePath($filepath); |
242 | return $this; |
243 | } |
244 | |
245 | /** |
246 | * Sets classes mapping for file paths. |
247 | * |
248 | * @param array<string,string> $classes Associative array with class names |
249 | * as keys and file paths as values |
250 | * |
251 | * @return static |
252 | */ |
253 | public function setClasses(array $classes) : static |
254 | { |
255 | foreach ($classes as $name => $filepath) { |
256 | $this->setClass($name, $filepath); |
257 | } |
258 | return $this; |
259 | } |
260 | |
261 | /** |
262 | * Gets a class file path. |
263 | * |
264 | * @param string $name Fully qualified class name (with namespace) |
265 | * |
266 | * @return string|null The file path or null if class is not mapped |
267 | */ |
268 | #[Pure] |
269 | public function getClass(string $name) : ?string |
270 | { |
271 | return $this->classes[$this->renderRealName($name)] ?? null; |
272 | } |
273 | |
274 | /** |
275 | * Gets all mapped classes. |
276 | * |
277 | * @return array<string,string> An array of class names as keys and |
278 | * file paths as values |
279 | */ |
280 | #[Pure] |
281 | public function getClasses() : array |
282 | { |
283 | return $this->classes; |
284 | } |
285 | |
286 | /** |
287 | * Removes one class from the mapping. |
288 | * |
289 | * @param string $name Fully qualified class name (with namespace) |
290 | * |
291 | * @return static |
292 | */ |
293 | public function removeClass(string $name) : static |
294 | { |
295 | unset($this->classes[$this->renderRealName($name)]); |
296 | return $this; |
297 | } |
298 | |
299 | /** |
300 | * Removes classes from the mapping. |
301 | * |
302 | * @param array<int,string> $names List of class names |
303 | * |
304 | * @return static |
305 | */ |
306 | public function removeClasses(array $names) : static |
307 | { |
308 | foreach ($names as $name) { |
309 | $this->removeClass($name); |
310 | } |
311 | return $this; |
312 | } |
313 | |
314 | /** |
315 | * Finds the file path of a class searching in the class mapping and |
316 | * resolving namespaces. |
317 | * |
318 | * @param string $class Fully qualified class name (with namespace) |
319 | * |
320 | * @return string|null The class file path or null if not found |
321 | */ |
322 | #[Pure] |
323 | public function findClassPath(string $class) : ?string |
324 | { |
325 | $path = $this->getClass($class); |
326 | if ($path) { |
327 | return $path; |
328 | } |
329 | foreach ($this->getNamespaces() as $namespace => $paths) { |
330 | $namespace .= '\\'; |
331 | if (\str_starts_with($class, $namespace)) { |
332 | foreach ($paths as $path) { |
333 | $path .= \strtr( |
334 | \substr($class, \strlen($namespace)), |
335 | ['\\' => \DIRECTORY_SEPARATOR] |
336 | ); |
337 | $path .= '.php'; |
338 | if (\is_file($path)) { |
339 | return $path; |
340 | } |
341 | } |
342 | } |
343 | } |
344 | return null; |
345 | } |
346 | |
347 | /** |
348 | * Loads a class file. |
349 | * |
350 | * @param string $class Fully qualified class name (with namespace) |
351 | * |
352 | * @return bool TRUE if the file is loaded, otherwise FALSE |
353 | */ |
354 | public function loadClass(string $class) : bool |
355 | { |
356 | if (isset($this->debugCollector)) { |
357 | return $this->loadDebug($class); |
358 | } |
359 | return $this->loadClassFile($class); |
360 | } |
361 | |
362 | protected function loadClassFile(string $class) : bool |
363 | { |
364 | $class = $this->findClassPath($class); |
365 | if ($class) { |
366 | // Require $class in an isolated scope - no access to $this |
367 | (static function () use ($class) : void { |
368 | require $class; |
369 | })(); |
370 | return true; |
371 | } |
372 | return false; |
373 | } |
374 | |
375 | protected function loadDebug(string $class) : bool |
376 | { |
377 | $start = \microtime(true); |
378 | $loaded = $this->loadClassFile($class); |
379 | $end = \microtime(true); |
380 | $this->debugCollector->addData([ |
381 | 'start' => $start, |
382 | 'end' => $end, |
383 | 'class' => $class, |
384 | 'file' => $this->findClassPath($class), |
385 | 'loaded' => $loaded, |
386 | ]); |
387 | return $loaded; |
388 | } |
389 | |
390 | public function setDebugCollector(AutoloadCollector $debugCollector = null, string $name = 'default') : static |
391 | { |
392 | if ($debugCollector) { |
393 | $this->debugCollector = $debugCollector; |
394 | return $this; |
395 | } |
396 | $data = []; |
397 | foreach ([Collector::class, AutoloadCollector::class] as $class) { |
398 | $start = \microtime(true); |
399 | $loaded = $this->loadClassFile($class); |
400 | $end = \microtime(true); |
401 | $data[] = [ |
402 | 'start' => $start, |
403 | 'end' => $end, |
404 | 'class' => $class, |
405 | 'file' => $this->findClassPath($class), |
406 | 'loaded' => $loaded, |
407 | ]; |
408 | } |
409 | $this->debugCollector = new AutoloadCollector($name); |
410 | $this->debugCollector->setAutoloader($this); |
411 | foreach ($data as $item) { |
412 | $this->debugCollector->addData($item); |
413 | } |
414 | return $this; |
415 | } |
416 | |
417 | public function getDebugCollector() : ?AutoloadCollector |
418 | { |
419 | return $this->debugCollector ?? null; |
420 | } |
421 | |
422 | /** |
423 | * Renders a class or namespace name without lateral slashes. |
424 | * |
425 | * @param string $name Class or namespace name |
426 | * |
427 | * @return string |
428 | */ |
429 | #[Pure] |
430 | protected function renderRealName(string $name) : string |
431 | { |
432 | return \trim($name, '\\'); |
433 | } |
434 | |
435 | /** |
436 | * Renders the canonicalized absolute pathname for a file path. |
437 | * |
438 | * @param string $path File path |
439 | * |
440 | * @return string |
441 | */ |
442 | protected function renderFilePath(string $path) : string |
443 | { |
444 | $real = \realpath($path); |
445 | if ($real === false || ! \is_file($real)) { |
446 | throw new RuntimeException("Path is not a file: {$path}"); |
447 | } |
448 | return $real; |
449 | } |
450 | |
451 | /** |
452 | * Gets the canonicalized absolute pathname for a directory path. |
453 | * |
454 | * Adds a trailing slash. |
455 | * |
456 | * @param string $path |
457 | * |
458 | * @return string |
459 | */ |
460 | protected function renderDirectoryPath(string $path) : string |
461 | { |
462 | $real = \realpath($path); |
463 | if ($real === false || ! \is_dir($real)) { |
464 | throw new RuntimeException("Path is not a directory: {$path}"); |
465 | } |
466 | return $real . \DIRECTORY_SEPARATOR; |
467 | } |
468 | } |