Completed
Push — master ( 7ceda2...05bb6a )
by Ben
30:39 queued 15:37
created

Resolver::langaugeConstructExists()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 10
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 10
rs 8.8571
c 0
b 0
f 0
cc 6
eloc 8
nc 6
nop 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Benrowe\Fqcn;
6
7
use Composer\Autoload\ClassLoader;
8
9
/**
10
 * Resolver
11
 * Resolve a php psr4 namespace to a directory
12
 *
13
 * @package Benrowe\Fqcn
14
 */
15
class Resolver
16
{
17
    /**
18
     * Instance of composer, since this will be used to load the ps4 prefixes
19
     *
20
     * @var ClassLoader
21
     */
22
    private $composer;
23
24
    /**
25
     * Resolver constructor.
26
     *
27
     * @param ClassLoader $composer
28
     */
29
    public function __construct(ClassLoader $composer)
30
    {
31
        $this->composer = $composer;
32
    }
33
34
    /**
35
     * Find all of the avaiable classes under a specific namespace
36
     *
37
     * @param  string $namespace  The namespace to search for
38
     * @param  string $instanceOf optional, restrict the classes found to those
39
     *                            that extend from this base
40
     * @return array a list of FQCN's that match
41
     */
42
    public function findClasses(string $namespace, string $instanceOf = null): array
43
    {
44
        $availablePaths = $this->resolveDirectory($namespace);
45
46
        $classes = [];
47
        foreach ($availablePaths as $path) {
48
            foreach ($this->getDirectoryIterator($path) as $file) {
49
                $fqcn = $namespace.strtr(substr($file[0], strlen($path), -4), '//', '\\');
50
                if ($this->langaugeConstructExists($fqcn)) {
51
                    $classes[] = $fqcn;   
52
                }
53
            }
54
        }
55
56
        sort($classes);
57
58
        if ($instanceOf) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $instanceOf of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
59
            $classes = array_values(array_filter($classes, function ($className) use ($instanceOf) {
60
                return is_subclass_of($className, $instanceOf);
61
            }));
62
        }
63
64
        return $classes;
65
    }
66
67
    /**
68
     * Resolve a psr4 based namespace to an absolute directory
69
     *
70
     * @param string $namespace
71
     * @return array
72
     * @throws Exception
73
     */
74
    public function resolveDirectory(string $namespace): array
75
    {
76
        $namespace = $this->normalise($namespace);
77
78
        $prefixes = $this->composer->getPrefixesPsr4();
79
        $prefix   = $this->findPrefix($namespace, array_keys($prefixes));
80
        if (!$prefix) {
81
            throw new Exception('Could not find registered psr4 prefix that matches '.$namespace);
82
        }
83
84
        $discovered = [];
85
        foreach ($prefixes[$prefix] as $path) {
86
            $path = $this->findAbsolutePathForPsr4($namespace, $prefix, $path);
87
            // convert the rest of the relative path, from the prefix into a directory slug
88
            if ($path && is_dir($path)) {
89
                $discovered[] =  $path;
90
            }
91
        }
92
        return $discovered;
93
    }
94
95
    /**
96
     * Find the best psr4 namespace prefix, based on the supplied namespace, and
97
     * list of provided prefix
98
     *
99
     * @param string $namespace
100
     * @param array  $prefixes
101
     * @return string
102
     */
103
    private function findPrefix(string $namespace, array $prefixes): string
104
    {
105
        $prefixResult = '';
106
107
        // find the best matching prefix!
108
        foreach ($prefixes as $prefix) {
109
            // if we have a match, and it's longer than the previous match
110
            if (substr($namespace, 0, strlen($prefix)) == $prefix &&
111
                strlen($prefix) > strlen($prefixResult)
112
            ) {
113
                $prefixResult = $prefix;
114
            }
115
        }
116
        return $prefixResult;
117
    }
118
119
    /**
120
     * Convert the supplied namespace string into a standard format
121
     * no prefix, ends with trailing slash
122
     *
123
     * Example:
124
     * Psr4\Prefix\
125
     * Something\
126
     *
127
     * @param string $namespace
128
     * @return string
129
     * @throws Exception
130
     */
131
    private function normalise(string $namespace): string
132
    {
133
        $tidy = trim($namespace, '\\');
134
        if (!$tidy) {
135
            throw new Exception('Invalid namespace', 100);
136
        }
137
138
        return $tidy . '\\';
139
    }
140
141
    /**
142
     * Get an absolute path for the provided namespace, based on a existing
143
     * directory and its psr4 prefix
144
     *
145
     * @param string $namespace
146
     * @param string $psr4Prefix the psr4 prefix
147
     * @param string $psr4Path and it's related path
148
     * @return string the absolute directory path the provided namespace, given
149
     *                    the correct prefix and path empty string if path can't
150
     *                    be resolved
151
     */
152
    private function findAbsolutePathForPsr4(string $namespace, string $psr4Prefix, string $psr4Path): string
153
    {
154
        $relFqn = trim(substr($namespace, strlen($psr4Prefix)), '\\/');
155
        $path =
156
            $psr4Path .
157
            DIRECTORY_SEPARATOR .
158
            strtr($relFqn, [
159
                '\\' => DIRECTORY_SEPARATOR,
160
                '//' => DIRECTORY_SEPARATOR
161
            ]);
162
        $path = realpath($path);
163
164
        return $path ?: '';
165
    }
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
    private function getDirectoryIterator(string $path): \RegexIterator
175
    {
176
        $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 $artifactName
185
     * @return bool
186
     */
187
    private function langaugeConstructExists(string $artifactName): bool
188
    {
189
        return
190
            class_exists($artifactName, false) || 
191
            interface_exists($artifactName, false) || 
192
            trait_exists($artifactName, false) ||
193
            class_exists($artifactName) || 
194
            interface_exists($artifactName) || 
195
            trait_exists($artifactName);
196
    }
197
}
198