ClassFinder::strStartsWith()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 2
dl 0
loc 3
rs 10
1
<?php
2
3
namespace Cruxinator\ClassFinder;
4
5
use Composer\Autoload\ClassLoader;
6
use Composer\Autoload\ClassMapGenerator;
7
use Exception;
8
use ReflectionClass;
9
use ReflectionException;
10
11
/**
12
 * Class ClassFinder.
13
 *
14
 * Functionality similar to get_declared_classes(), with autoload support.
15
 */
16
abstract class ClassFinder
17
{
18
    /**
19
     * @var array|string[]
20
     */
21
    private static $loadedNamespaces = [];
22
    /**
23
     * @var string
24
     */
25
    private static $vendorDir = '';
26
    /**
27
     * @var null|array|string[]
28
     */
29
    private static $optimisedClassMap = null;
30
    /**
31
     * @var bool Indicates if autoloader class map is initialised
32
     */
33
    private static $classLoaderInit = false;
34
35
    /**
36
     * Explicitly loads a namespace before returning declared classes.
37
     *
38
     * @param string $namespace the namespace to load
39
     *
40
     * @throws Exception
41
     *
42
     * @return array|string[] an array with the name of the defined classes
43
     */
44
    private static function getProjectClasses(string $namespace): array
45
    {
46
        if (!in_array($namespace, self::$loadedNamespaces)) {
47
            $map = self::getClassMap($namespace);
48
            array_walk($map, function ($filename, $className, $namespace) {
49
                assert(file_exists($filename), $filename);
50
                self::strStartsWith($namespace, $className) && class_exists($className, true);
51
            }, $namespace);
52
        }
53
54
        return get_declared_classes();
55
    }
56
57
    /**
58
     * Attempts to get an optimised ClassMap failing that attempts to generate one for the namespace.
59
     *
60
     * @param string $namespace the namespace to generate for if necessary
61
     *
62
     * @throws Exception
63
     *
64
     * @return null|array|string[] the class map, keyed by Classname values of files
65
     */
66
    private static function getClassMap(string $namespace): array
67
    {
68
        self::checkState();
69
70
        return null !== (self::$optimisedClassMap) ?
71
            self::$optimisedClassMap :
72
            array_reduce(
73
                self::getProjectSearchDirs($namespace),
74
                function ($map, $dir) {
75
                    // Use composer's ClassMapGenerator to pull the class list out of each project search directory
76
                    return array_merge($map, ClassMapGenerator::createMap($dir));
77
                },
78
                self::getComposerAutoloader()->getClassMap()
79
            );
80
    }
81
82
    /**
83
     * Checks if a string starts with another string.
84
     * Simple Helper for readability.
85
     *
86
     * @param string $needle   the string to check
87
     * @param string $haystack the input string
88
     *
89
     * @return bool true if haystack starts with needle, false otherwise
90
     */
91
    private static function strStartsWith(string $needle, string $haystack): bool
92
    {
93
        return substr($haystack, 0, strlen($needle)) === $needle;
94
    }
95
96
    /**
97
     * Gets the base vendor Directory.
98
     *
99
     * @throws ReflectionException
100
     *
101
     * @return string the vase Vendor Director
102
     */
103
    private static function getVendorDir(): string
104
    {
105
        return empty(self::$vendorDir) ?
106
            self::$vendorDir = dirname((new ReflectionClass(ClassLoader::class))->getFileName(), 2) :
107
            self::$vendorDir;
108
    }
109
110
    /**
111
     * Checks the state requirements (package and autoloader).
112
     *
113
     * @throws Exception thrown when a combination of components is not available
114
     */
115
    private static function checkState(): void
116
    {
117
        self::initClassMap();
118
        if (null === self::$optimisedClassMap && !class_exists(ClassMapGenerator::class)) {
119
            throw new Exception('Cruxinator/ClassFinder requires either composer/composer'.
120
             ' or an optimised autoloader(`composer dump-autoload -o`)');
121
        }
122
    }
123
124
    /**
125
     * Initializes the optimised class map, if possible.
126
     *
127
     * @throws ReflectionException
128
     */
129
    private static function initClassMap(): void
130
    {
131
        if (true === self::$classLoaderInit) {
132
            return;
133
        }
134
        self::$classLoaderInit = true;
135
        $autoLoader = self::getComposerAutoloader();
136
        $classMap = $autoLoader->getClassMap();
137
        self::$optimisedClassMap = isset($classMap[__CLASS__]) ? $classMap : null;
138
    }
139
140
    /**
141
     * Gets the Composer Class Loader.
142
     *
143
     * @throws ReflectionException
144
     *
145
     * @return ClassLoader
146
     */
147
    private static function getComposerAutoloader(): ClassLoader
148
    {
149
        return include self::getVendorDir().DIRECTORY_SEPARATOR.'autoload.php';
150
    }
151
152
    /**
153
     * Gets a list of classes defined in the autoloader. get_declared_classes().
154
     *
155
     * @param string        $namespace     namespace prefix to restrict the list (must be configured psr4 namespace
156
     * @param callable|null $conditional   callable method of signature `conditional(string $className) : bool` to check to include
157
     * @param bool          $includeVendor whether classes in the vendor directory should be considered
158
     *
159
     * @throws Exception
160
     *
161
     * @return array the list of classes
162
     */
163
    public static function getClasses(string $namespace = '', callable $conditional = null, bool $includeVendor = true): array
164
    {
165
        $conditional = $conditional ?: 'is_string';
166
        $classes = array_filter(self::getProjectClasses($namespace), function (string $class) use (
167
            $namespace,
168
            $conditional,
169
            $includeVendor
170
        ) {
171
            return self::strStartsWith($namespace, $class) &&
172
                   ($includeVendor || !self::isClassInVendor($class)) &&
173
                   $conditional($class);
174
        });
175
176
        return $classes;
177
    }
178
179
    /**
180
     * Gets the Directories associated with a given namespace.
181
     *
182
     * @param string $namespace the namespace (without preceding \)
183
     *
184
     * @throws ReflectionException
185
     *
186
     * @return array a list of directories containing classes for that namespace
187
     */
188
    private static function getProjectSearchDirs(string $namespace): array
189
    {
190
        $raw = self::getComposerAutoloader()->getPrefixesPsr4();
191
192
        return self::findCompatibleNamespace($namespace, $raw);
193
    }
194
195
    private static function findCompatibleNamespace(string $namespace, array $psr4): array
196
    {
197
        $namespaceParts = explode('\\', $namespace);
198
        while (!array_key_exists($namespace, $psr4) && count($namespaceParts) !== 0) {
199
            $namespace = implode('\\', $namespaceParts).'\\';
200
            array_pop($namespaceParts);
201
        }
202
203
        return array_key_exists($namespace, $psr4) ? $psr4[$namespace] : array_values($psr4);
204
    }
205
206
    /**
207
     * Identify if the class is in the vendor directory.
208
     *
209
     * @param string $className the class to test
210
     *
211
     * @throws ReflectionException
212
     *
213
     * @return bool true if in vendor otherwise false
214
     */
215
    private static function isClassInVendor(string $className): bool
216
    {
217
        $filename = (new ReflectionClass($className))->getFileName();
218
219
        return self::strStartsWith(self::getVendorDir(), $filename);
220
    }
221
}
222