Resolver::languageConstructExists()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 1
dl 0
loc 6
rs 10
c 0
b 0
f 0
ccs 3
cts 3
cp 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 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 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
    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 ($namespace->startsWith($prefix) &&
159
                ($prefixResult === null || $prefix->length() > $prefixResult->length())
160
            ) {
161
                // if we have a match, and it's longer than the previous match
162 12
                $prefixResult = $prefix;
163
            }
164
        }
165
        return $prefixResult;
166 12
    }
167
168
    /**
169 12
     * Retrieve a directory iterator for the supplied path
170 12
     *
171 12
     * @param  string $path The directory to iterate
172 12
     * @return RegexIterator
173
     */
174 12
    private function getDirectoryIterator(string $path): RegexIterator
175
    {
176 12
        $dirIterator = new RecursiveDirectoryIterator($path);
177
        $iterator = new RecursiveIteratorIterator($dirIterator);
178
        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 6
     */
187
    private function languageConstructExists(string $constructName): bool
188 6
    {
189 6
        return
190 6
            $this->checkConstructExists($constructName, false) ||
191
            $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 6
     *                        doesn't exist
200
     * @return bool
201
     */
202 6
    private function checkConstructExists(string $constructName, bool $autoload = true): bool
203 6
    {
204
        return
205
            class_exists($constructName, $autoload) ||
206
            interface_exists($constructName, $autoload) ||
207
            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 6
     *
215
     * @param  array  $directories list of absolute directory paths
216
     * @param  Psr4Namespace $namespace   The namespace these directories are representing
217 6
     * @return array
218 6
     */
219 6
    private function findNamespacedConstuctsInDirectories(array $directories, Psr4Namespace $namespace): array
220
    {
221
        $constructs = [];
222
        foreach ($directories as $path) {
223
            $constructs = array_merge($constructs, $this->findNamespacedConstuctsInDirectory($path, $namespace));
224
        }
225
226
        sort($constructs);
227
228
        return $constructs;
229
    }
230
231 6
    /**
232
     * Recurisvely scan the supplied directory for language constructs that are
233 6
     * $namespaced
234 6
     *
235 6
     * @param  string $directory The directory to scan
236
     * @param  Psr4Namespace $namespace the namespace that represents this directory
237
     * @return array
238 6
     */
239
    private function findNamespacedConstuctsInDirectory(string $directory, Psr4Namespace $namespace): array
240 6
    {
241
        $constructs = [];
242
243
        foreach ($this->getDirectoryIterator($directory) as $file) {
244
            $fqcn = $namespace.strtr(substr($file[0], strlen($directory) + 1, -4), '//', '\\');
245
            if ($this->languageConstructExists($fqcn)) {
246
                $constructs[] = $fqcn;
247
            }
248
        }
249
250
        return $constructs;
251 6
    }
252
}
253