Completed
Push — master ( e1c890...c13eee )
by Ben
25:48 queued 10:51
created

Resolver::getDirectoryIterator()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 6
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 4
nc 1
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
                try {
51
                    // test if the class exists
52
                    new \ReflectionClass($fqcn);
53
                    $classes[] = $fqcn;
54
                } catch (\ReflectionException $e) {
55
                    // could not load the class/interface/trait
56
                }
57
            }
58
        }
59
60
        sort($classes);
61
62
        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...
63
            $classes = array_values(array_filter($classes, function ($className) use ($instanceOf) {
64
                return is_subclass_of($className, $instanceOf);
65
            }));
66
        }
67
68
        return $classes;
69
    }
70
71
    /**
72
     * Resolve a psr4 based namespace to an absolute directory
73
     *
74
     * @param string $namespace
75
     * @return array
76
     * @throws Exception
77
     */
78
    public function resolveDirectory(string $namespace): array
79
    {
80
        $namespace = $this->normalise($namespace);
81
82
        $prefixes = $this->composer->getPrefixesPsr4();
83
        $prefix   = $this->findPrefix($namespace, array_keys($prefixes));
84
        if (!$prefix) {
85
            throw new Exception('Could not find registered psr4 prefix that matches '.$namespace);
86
        }
87
88
        $discovered = [];
89
        foreach ($prefixes[$prefix] as $path) {
90
            $path = $this->findAbsolutePathForPsr4($namespace, $prefix, $path);
91
            // convert the rest of the relative path, from the prefix into a directory slug
92
            if ($path && is_dir($path)) {
93
                $discovered[] =  $path;
94
            }
95
        }
96
        return $discovered;
97
    }
98
99
    /**
100
     * Find the best psr4 namespace prefix, based on the supplied namespace, and
101
     * list of provided prefix
102
     *
103
     * @param string $namespace
104
     * @param array  $prefixes
105
     * @return string
106
     */
107
    private function findPrefix(string $namespace, array $prefixes): string
108
    {
109
        $prefixResult = '';
110
111
        // find the best matching prefix!
112
        foreach ($prefixes as $prefix) {
113
            // if we have a match, and it's longer than the previous match
114
            if (substr($namespace, 0, strlen($prefix)) == $prefix &&
115
                strlen($prefix) > strlen($prefixResult)
116
            ) {
117
                $prefixResult = $prefix;
118
            }
119
        }
120
        return $prefixResult;
121
    }
122
123
    /**
124
     * Convert the supplied namespace string into a standard format
125
     * no prefix, ends with trailing slash
126
     *
127
     * Example:
128
     * Psr4\Prefix\
129
     * Something\
130
     *
131
     * @param string $namespace
132
     * @return string
133
     * @throws Exception
134
     */
135
    private function normalise(string $namespace): string
136
    {
137
        $tidy = trim($namespace, '\\');
138
        if (!$tidy) {
139
            throw new Exception('Invalid namespace', 100);
140
        }
141
142
        return $tidy . '\\';
143
    }
144
145
    /**
146
     * Get an absolute path for the provided namespace, based on a existing
147
     * directory and its psr4 prefix
148
     *
149
     * @param string $namespace
150
     * @param string $psr4Prefix the psr4 prefix
151
     * @param string $psr4Path and it's related path
152
     * @return string the absolute directory path the provided namespace, given
153
     *                    the correct prefix and path empty string if path can't
154
     *                    be resolved
155
     */
156
    private function findAbsolutePathForPsr4(string $namespace, string $psr4Prefix, string $psr4Path): string
157
    {
158
        $relFqn = trim(substr($namespace, strlen($psr4Prefix)), '\\/');
159
        $path =
160
            $psr4Path .
161
            DIRECTORY_SEPARATOR .
162
            strtr($relFqn, [
163
                '\\' => DIRECTORY_SEPARATOR,
164
                '//' => DIRECTORY_SEPARATOR
165
            ]);
166
        $path = realpath($path);
167
168
        return $path ?: '';
169
    }
170
171
172
    /**
173
     * Retrieve a directory iterator for the supplied path
174
     *
175
     * @param  string $path The directory to iterate
176
     * @return \RegexIterator
177
     */
178
    private function getDirectoryIterator(string $path): \RegexIterator
179
    {
180
        $dirIterator = new \RecursiveDirectoryIterator($path);
181
        $iterator = new \RecursiveIteratorIterator($dirIterator);
182
        return new \RegexIterator($iterator, '/^.+\.php$/i', \RecursiveRegexIterator::GET_MATCH);
183
    }
184
}
185