Passed
Pull Request — master (#11)
by Christopher
02:13
created

ClassFinder::isClassInVendor()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
cc 1
eloc 2
c 2
b 1
f 0
nc 1
nop 1
dl 0
loc 4
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
                }, []);
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
     * @return string The vase Vendor Directory.
91
     * @throws ReflectionException
92
     */
93
    private static function getVendorDir(): string {
94
        return empty(self::$vendorDir) ?
95
            self::$vendorDir = dirname((new ReflectionClass(ClassLoader::class))->getFileName(), 2) :
96
            self::$vendorDir ;
97
    }
98
99
    /**
100
     * Checks the state requirements (package and autoloader).
101
     *
102
     * @throws Exception thrown when a combination of components is not available
103
     */
104
    private static function checkState() : void
105
    {
106
        self::initClassMap();
107
        if (null === self::$optimisedClassMap && !class_exists(ClassMapGenerator::class)) {
108
            throw new Exception('Cruxinator/ClassFinder requires either composer/composer' .
109
             ' or an optimised autoloader(`composer dump-autoload -o`)');
110
        }
111
    }
112
113
    /**
114
     * Initializes the optimised class map, if possible.
115
     * @throws ReflectionException
116
     */
117
    private static function initClassMap() :void
118
    {
119
        if (true === self::$classLoaderInit) {
120
            return;
121
        }
122
        self::$classLoaderInit = true;
123
        $autoLoader = self::getComposerAutoloader();
124
        $classMap = $autoLoader->getClassMap();
125
        self::$optimisedClassMap = isset($classMap[__CLASS__]) ? $classMap : null;
126
    }
127
128
    /**
129
     * Gets the Composer Class Loader.
130
     *
131
     * @return ClassLoader
132
     * @throws ReflectionException
133
     */
134
    private static function getComposerAutoloader(): ClassLoader
135
    {
136
137
        /** @noinspection PhpIncludeInspection */
138
        return include self::getVendorDir() . DIRECTORY_SEPARATOR . 'autoload.php';
139
    }
140
141
    /**
142
     * Gets a list of classes defined in the autoloader. get_declared_classes().
143
     *
144
     * @param  string        $namespace     namespace prefix to restrict the list (must be configured psr4 namespace
145
     * @param  callable|null $conditional   callable method of signature `conditional(string $className) : bool` to check to include
146
     * @param  bool          $includeVendor whether classes in the vendor directory should be considered
147
     * @throws Exception
148
     * @return array         the list of classes
149
     */
150
    public static function getClasses(string $namespace = '', callable $conditional = null, bool $includeVendor = true):array
151
    {
152
        $conditional = $conditional ?: 'is_string';
153
        $classes = array_values(array_filter(self::getProjectClasses($namespace), function (string $class) use (
154
            $namespace,
155
            $conditional,
156
            $includeVendor
157
        ) {
158
            return self::strStartsWith($namespace, $class) &&
159
                   ($includeVendor || !self::isClassInVendor($class)) &&
160
                   $conditional($class);
161
        }));
162
163
        return $classes;
164
    }
165
166
    /**
167
     * Gets the Directories associated with a given namespace.
168
     *
169
     * @param string $namespace the namespace (without preceding \
170
     * @return array  a list of directories containing classes for that namespace
171
     * @throws ReflectionException
172
     */
173
    private static function getProjectSearchDirs(string $namespace): array
174
    {
175
        $raw = self::getComposerAutoloader()->getPrefixesPsr4();
176
        return $raw[$namespace];
177
    }
178
179
    /**
180
     * Identify if the class is in the vendor directory.
181
     *
182
     * @param  string              $className the class to test
183
     * @throws ReflectionException
184
     * @return bool                true if in vendor otherwise false
185
     */
186
    private static function isClassInVendor(string $className) : bool
187
    {
188
        $filename = (new ReflectionClass($className))->getFileName();
189
        return self::strStartsWith(self::getVendorDir(), $filename);
190
    }
191
}
192