Completed
Push — master ( fbd805...97df31 )
by Alex
18s queued 15s
created

ClassFinder::getComposerAutoloader()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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