Completed
Push — master ( c29642...62b599 )
by Ben
04:47
created

Resolver   A

Complexity

Total Complexity 28

Size/Duplication

Total Lines 246
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 1

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 28
lcom 1
cbo 1
dl 0
loc 246
ccs 70
cts 70
cp 1
rs 10
c 0
b 0
f 0

12 Methods

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