Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
115 / 115
100.00% covered (success)
100.00%
28 / 28
CRAP
100.00% covered (success)
100.00%
1 / 1
Autoloader
100.00% covered (success)
100.00%
115 / 115
100.00% covered (success)
100.00%
28 / 28
52
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 register
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 unregister
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setNamespace
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 makeRenderedDirectoryPaths
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 addNamespace
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 sortNamespaces
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setNamespaces
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 addNamespaces
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getNamespace
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getNamespaces
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 removeNamespace
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 removeNamespaces
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 setClass
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setClasses
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getClass
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getClasses
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 removeClass
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 removeClasses
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 findClassPath
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
6
 loadClass
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 loadClassFile
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 loadDebug
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
1
 setDebugCollector
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
4
 getDebugCollector
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 renderRealName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 renderFilePath
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 renderDirectoryPath
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
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 */
10namespace Framework\Autoload;
11
12use Framework\Autoload\Debug\AutoloadCollector;
13use Framework\Debug\Collector;
14use JetBrains\PhpStorm\Pure;
15use 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 */
25class 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}