Completed
Pull Request — master (#17)
by Ben
07:21 queued 06:12
created

Resolver::setNamespace()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 7
ccs 5
cts 5
cp 1
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 1
crap 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Benrowe\Fqcn;
6
7
use Benrowe\Fqcn\Value\Psr4Namespace;
8
use Composer\Autoload\ClassLoader;
9
use RecursiveDirectoryIterator;
10
use RecursiveIteratorIterator;
11
use RecursiveRegexIterator;
12
use RegexIterator;
13
14
/**
15
 * Psr4 Resolver
16
 *
17
 * Help resolve a PHP PSR-4 namespace to a directory + resolve language
18
 * constructs (classes, interfaces and traits) that implement the provided
19
 * namespace
20
 *
21
 * Example:
22
 *
23
 * ```php
24
 * $composer = require './vendor/autoload.php';
25
 * $resolver = new Benrowe\Fqcn\Resolver('Namespace\\To\\Search\\For', $composer);
26
 * $resolver->findDirectories() // => list of directories
27
 * $resolver->findConstructs() => lists of all language constructs found under the namespace
28
 * ```
29
 *
30
 * @package Benrowe\Fqcn
31
 */
32
class Resolver
33
{
34
    /**
35
     * Instance of composer, since this will be used to load the ps4 prefixes
36
     *
37
     * @var ClassLoader
38
     */
39
    private $composer;
40
41
    /**
42
     * @var Psr4Namespace
43
     */
44
    private $namespace;
45
46
    /**
47
     * Resolver constructor.
48
     *
49
     * @param Psr4Namespace|string $namespace
50
     * @param ClassLoader $composer
51
     */
52 30
    public function __construct($namespace, ClassLoader $composer)
53
    {
54 30
        $this->setNamespace($namespace);
55 30
        $this->composer = $composer;
56 30
    }
57
58
    /**
59
     * Set the namespace to resolve
60
     *
61
     * @param Psr4Namespace|string $namespace $namespace
62
     */
63 30
    public function setNamespace($namespace)
64
    {
65 30
        if (!($namespace instanceof Psr4Namespace)) {
66 30
            $namespace = new Psr4Namespace($namespace);
67
        }
68 30
        $this->namespace = $namespace;
69 30
    }
70
71
    /**
72
     * Get the current namespace
73
     *
74
     * @return Psr4Namespace
75
     */
76 6
    public function getNamespace(): Psr4Namespace
77
    {
78 6
        return $this->namespace;
79
    }
80
81
    /**
82
     * Find all of the available constructs under a specific namespace
83
     *
84
     * @param  string $instanceOf optional, restrict the classes found to those
85
     *                            that extend from this base
86
     * @return array a list of FQCN's that match
87
     */
88 6
    public function findConstructs(string $instanceOf = null): array
89
    {
90 6
        $availablePaths = $this->findDirectories();
91
92 6
        $constructs = $this->findNamespacedConstuctsInDirectories($availablePaths, $this->namespace);
93
94
        // apply filtering
95 6
        if ($instanceOf !== null) {
96 3
            $constructs = array_values(array_filter($constructs, function ($constructName) use ($instanceOf) {
97 3
                return is_subclass_of($constructName, $instanceOf);
98 3
            }));
99
        }
100
101 6
        return $constructs;
102
    }
103
104
    /**
105
     * Resolve a psr4 based namespace to a list of absolute directory paths
106
     *
107
     * @return array list of directories this namespace is mapped to
108
     * @throws Exception
109
     */
110 15
    public function findDirectories(): array
111
    {
112 15
        $prefixes = $this->composer->getPrefixesPsr4();
113
        // pluck the best namespace from the available
114 15
        $namespacePrefix   = $this->findNamespacePrefix($this->namespace, array_keys($prefixes));
115 15
        if (!$namespacePrefix) {
116 3
            throw new Exception('Could not find registered psr4 prefix that matches '.$this->namespace);
117
        }
118
119 12
        return $this->buildDirectoryList($prefixes[$namespacePrefix->getValue()], $this->namespace, $namespacePrefix);
120
    }
121
122
    /**
123
     * Build a list of absolute paths, for the given namespace, based on the relative $prefix
124
     *
125
     * @param  array  $directories the list of directories (their position relates to $prefix)
126
     * @param  Psr4Namespace $namespace   The base namespace
127
     * @param  Psr4Namespace $prefix      The psr4 namespace related to the list of provided directories
128
     * @return array directory paths for provided namespace
129
     */
130 12
    private function buildDirectoryList(array $directories, Psr4Namespace $namespace, Psr4Namespace $prefix): array
131
    {
132 12
        $discovered = [];
133 12
        foreach ($directories as $path) {
134 12
            $path = (new PathBuilder($path, $prefix))->resolve($namespace);
135
            // convert the rest of the relative path, from the prefix into a directory slug
136 12
            if ($path && is_dir($path)) {
137 10
                $discovered[] =  $path;
138
            }
139
        }
140 12
        return $discovered;
141
    }
142
143
    /**
144
     * Find the best psr4 namespace prefix, based on the supplied namespace, and
145
     * list of provided prefix
146
     *
147
     * @param Psr4Namespace $namespace
148
     * @param array  $namespacePrefixes
149
     * @return Psr4Namespace
150
     */
151 15
    private function findNamespacePrefix(Psr4Namespace $namespace, array $namespacePrefixes)
152
    {
153 15
        $prefixResult = null;
154
155
        // find the best matching prefix!
156 15
        foreach ($namespacePrefixes as $prefix) {
157 15
            $prefix = new Psr4Namespace($prefix);
158 15
            if ($namespace->startsWith($prefix) &&
159 15
                ($prefixResult === null || $prefix->length() > $prefixResult->length())
160
            ) {
161
                // if we have a match, and it's longer than the previous match
162 13
                $prefixResult = $prefix;
163
            }
164
        }
165 15
        return $prefixResult;
166
    }
167
168
    /**
169
     * Retrieve a directory iterator for the supplied path
170
     *
171
     * @param  string $path The directory to iterate
172
     * @return RegexIterator
173
     */
174 6
    private function getDirectoryIterator(string $path): RegexIterator
175
    {
176 6
        $dirIterator = new RecursiveDirectoryIterator($path);
177 6
        $iterator = new RecursiveIteratorIterator($dirIterator);
178 6
        return new RegexIterator($iterator, '/^.+\.php$/i', RecursiveRegexIterator::GET_MATCH);
179
    }
180
181
    /**
182
     * Determine if the construct (class, interface or trait) exists
183
     *
184
     * @param string $constructName
185
     * @return bool
186
     */
187 6
    private function languageConstructExists(string $constructName): bool
188
    {
189
        return
190 6
            $this->checkConstructExists($constructName, false) ||
191 6
            $this->checkConstructExists($constructName);
192
    }
193
194
    /**
195
     * Determine if the construct exists
196
     *
197
     * @param  string $constructName
198
     * @param  bool $autoload trigger the autoloader to be fired, if the construct
199
     *                        doesn't exist
200
     * @return bool
201
     */
202 6
    private function checkConstructExists(string $constructName, bool $autoload = true): bool
203
    {
204
        return
205 6
            class_exists($constructName, $autoload) ||
206 6
            interface_exists($constructName, $autoload) ||
207 6
            trait_exists($constructName, $autoload);
208
    }
209
210
    /**
211
     * Process a list of directories, searching for language constructs (classes,
212
     * interfaces, traits) that exist in them, based on the supplied base
213
     * namespace
214
     *
215
     * @param  array  $directories list of absolute directory paths
216
     * @param  Psr4Namespace $namespace   The namespace these directories are representing
217
     * @return array
218
     */
219 6
    private function findNamespacedConstuctsInDirectories(array $directories, Psr4Namespace $namespace): array
220
    {
221 6
        $constructs = [];
222 6
        foreach ($directories as $path) {
223 6
            $constructs = array_merge($constructs, $this->findNamespacedConstuctsInDirectory($path, $namespace));
224
        }
225
226 6
        sort($constructs);
227
228 6
        return $constructs;
229
    }
230
231
    /**
232
     * Recurisvely scan the supplied directory for language constructs that are
233
     * $namespaced
234
     *
235
     * @param  string $directory The directory to scan
236
     * @param  Psr4Namespace $namespace the namespace that represents this directory
237
     * @return array
238
     */
239 6
    private function findNamespacedConstuctsInDirectory(string $directory, Psr4Namespace $namespace): array
240
    {
241 6
        $constructs = [];
242
243 6
        foreach ($this->getDirectoryIterator($directory) as $file) {
244 6
            $fqcn = $namespace.strtr(substr($file[0], strlen($directory) + 1, -4), '//', '\\');
245 6
            if ($this->languageConstructExists($fqcn)) {
246 6
                $constructs[] = $fqcn;
247
            }
248
        }
249
250 6
        return $constructs;
251
    }
252
}
253