Completed
Pull Request — master (#12)
by Ben
07:48
created

Resolver::findNamespacedConstuctsInDirectories()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

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