Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
86 / 86
100.00% covered (success)
100.00%
7 / 7
CRAP
100.00% covered (success)
100.00%
1 / 1
Locator
100.00% covered (success)
100.00%
86 / 86
100.00% covered (success)
100.00%
7 / 7
42
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getClassName
100.00% covered (success)
100.00%
28 / 28
100.00% covered (success)
100.00%
1 / 1
14
 getNamespacedFilepath
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
8
 ensureExtension
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 findFiles
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
5
 getFiles
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
5
 listFiles
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
7
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 JetBrains\PhpStorm\Pure;
13
14/**
15 * Class Locator.
16 *
17 * The Locator class has methods for finding files and the class FQN using an
18 * Autoloader instance.
19 *
20 * @package autoload
21 */
22class Locator
23{
24    protected Autoloader $autoloader;
25
26    /**
27     * Locator constructor.
28     *
29     * @param Autoloader $autoloader
30     */
31    public function __construct(Autoloader $autoloader)
32    {
33        $this->autoloader = $autoloader;
34    }
35
36    /**
37     * Gets the first Qualified Class Name in a given filename.
38     *
39     * The "class name" can be the name of a class, an interface or a trait.
40     *
41     * @param string $filename
42     *
43     * @see https://www.php.net/manual/en/language.namespaces.rules.php
44     * @see https://www.php.net/manual/en/language.oop5.interfaces.php
45     *
46     * @return string|null The class name or null if not found
47     */
48    public function getClassName(string $filename) : ?string
49    {
50        if ( ! \is_file($filename)) {
51            return null;
52        }
53        $tokens = \token_get_all((string) \file_get_contents($filename));
54        $last = \count($tokens);
55        $namespace = '';
56        $class = '';
57        foreach ($tokens as $current => $token) {
58            if ($token[0] === \T_NAMESPACE) {
59                for ($next = $current + 1; $next < $last; $next++) {
60                    if ($tokens[$next][0] === \T_STRING || $tokens[$next][0] === \T_NAME_QUALIFIED) {
61                        $namespace .= '\\' . $tokens[$next][1];
62                    } elseif ($tokens[$next] === '{' || $tokens[$next] === ';') {
63                        break;
64                    }
65                }
66                continue;
67            }
68            if ($token[0] === \T_RETURN) {
69                return null;
70            }
71            if (\in_array($token[0], [
72                \T_CLASS,
73                \T_ENUM,
74                \T_INTERFACE,
75                \T_TRAIT,
76            ], true)) {
77                for ($next = $current + 1; $next < $last; $next++) {
78                    if ($tokens[$next] === '{') {
79                        $token = $tokens[$current + 2];
80                        $class = $namespace . '\\' . $token[1];
81                        break 2;
82                    }
83                }
84            }
85        }
86        return $class ? \ltrim($class, '\\') : null;
87    }
88
89    /**
90     * Get the first filepath found in all namespaces.
91     *
92     * @param string $file The file name without extension
93     * @param string $extension The file extension
94     *
95     * @return string|null The filepath or null if not found
96     */
97    public function getNamespacedFilepath(string $file, string $extension = '.php') : ?string
98    {
99        if ($extension) {
100            $file = $this->ensureExtension($file, $extension);
101        }
102        $file = \strtr(\ltrim($file, '/'), ['\\' => '/']);
103        $segments = \explode('/', $file);
104        $count = \count($segments) - 1;
105        $file = $segments[$count];
106        unset($segments[$count]);
107        $namespaces = $this->autoloader->getNamespaces();
108        $namespace = '';
109        while ($segments) {
110            $namespace .= $namespace === ''
111                ? \array_shift($segments)
112                : '\\' . \array_shift($segments);
113            if ( ! isset($namespaces[$namespace])) {
114                continue;
115            }
116            foreach ($namespaces[$namespace] as $directory) {
117                $filepath = \rtrim(
118                    $directory . \implode(\DIRECTORY_SEPARATOR, $segments),
119                    \DIRECTORY_SEPARATOR
120                ) . \DIRECTORY_SEPARATOR . $file;
121                if (\is_file($filepath)) {
122                    return $filepath;
123                }
124            }
125            break;
126        }
127        return \is_file($file) ? $file : null;
128    }
129
130    #[Pure]
131    protected function ensureExtension(string $filename, string $extension) : string
132    {
133        if ( ! \str_ends_with($filename, $extension)) {
134            $filename .= $extension;
135        }
136        return $filename;
137    }
138
139    /**
140     * Find namesake files inside namespaced directories.
141     *
142     * @param string $filename The file name
143     * @param string $extension The file extension
144     *
145     * @return array<int,string> An array of filenames found
146     */
147    #[Pure]
148    public function findFiles(string $filename, string $extension = '.php') : array
149    {
150        if ($extension) {
151            $filename = $this->ensureExtension($filename, $extension);
152        }
153        $files = [];
154        foreach ($this->autoloader->getNamespaces() as $directories) {
155            foreach ($directories as $directory) {
156                if (\is_file($directory .= $filename)) {
157                    $files[] = $directory;
158                }
159            }
160        }
161        return $files;
162    }
163
164    /**
165     * Get a list of all files inside namespaced sub directories.
166     *
167     * @param string $subDirectory Sub directory path
168     *
169     * @return array<int,string>
170     */
171    public function getFiles(string $subDirectory) : array
172    {
173        $namespacedFiles = [];
174        foreach ($this->autoloader->getNamespaces() as $directories) {
175            foreach ($directories as $directory) {
176                $files = $this->listFiles($directory . $subDirectory);
177                if ($files) {
178                    $namespacedFiles[] = $files;
179                }
180            }
181        }
182        return $namespacedFiles ? \array_merge(...$namespacedFiles) : [];
183    }
184
185    /**
186     * Get a list of all files inside a directory.
187     *
188     * @param string $directory Absolute directory path
189     *
190     * @return array<int,string>|null Returns an array of filenames or null
191     * if the directory can not be resolved
192     */
193    public function listFiles(string $directory) : ?array
194    {
195        $directory = \realpath($directory);
196        if ($directory === false) {
197            return null;
198        }
199        $directory .= \DIRECTORY_SEPARATOR;
200        $files = [];
201        foreach ((array) \scandir($directory, 0) as $filename) {
202            if ($filename === '.' || $filename === '..') {
203                continue;
204            }
205            $filename = $directory . $filename;
206            if (\is_file($filename)) {
207                $files[] = $filename;
208                continue;
209            }
210            foreach ($this->listFiles($filename) as $subDirectory) {
211                $files[] = $subDirectory;
212            }
213        }
214        return $files;
215    }
216}