ClassHelper::requireClassExists()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 3
c 1
b 0
f 0
dl 0
loc 8
rs 10
cc 2
nc 2
nop 1
1
<?php
2
/**
3
 * @package Application Utils
4
 * @subpackage ClassFinder
5
 * @see \AppUtils\ClassHelper
6
 */
7
8
declare(strict_types=1);
9
10
namespace AppUtils;
11
12
use AppUtils\ClassHelper\ClassLoaderNotFoundException;
13
use AppUtils\ClassHelper\ClassNotExistsException;
14
use AppUtils\ClassHelper\ClassNotImplementsException;
15
use Composer\Autoload\ClassLoader;
16
use Throwable;
17
18
/**
19
 * Helper class to simplify working with dynamic class loading,
20
 * in a static analysis-tool-friendly way. PHPStan and co will
21
 * recognize the correct class types given class strings.
22
 *
23
 * @package Application Utils
24
 * @subpackage ClassFinder
25
 * @author Sebastian Mordziol <[email protected]>
26
 */
27
class ClassHelper
28
{
29
    private static ?ClassLoader $classLoader = null;
30
31
    public const ERROR_CANNOT_RESOLVE_CLASS_NAME = 111001;
32
    public const ERROR_THROWABLE_GIVEN_AS_OBJECT = 111002;
33
34
    /**
35
     * Attempts to detect the name of a class, switching between
36
     * the older class naming scheme with underscores (Long_Class_Name)
37
     * and namespaces.
38
     *
39
     * @param string $legacyName
40
     * @param string $nsPrefix Optional namespace prefix, if the namespace contains
41
     *                         the vendor name, for example (Vendor\PackageName\Folder\Class).
42
     * @return string|null The detected class name, or NULL otherwise.
43
     */
44
    public static function resolveClassName(string $legacyName, string $nsPrefix='') : ?string
45
    {
46
        $names = array(
47
            str_replace('\\', '_', $legacyName),
48
            str_replace('_', '\\', $legacyName),
49
            $nsPrefix.'\\'.str_replace('_', '\\', $legacyName)
50
        );
51
52
        foreach($names as $name) {
53
            if (class_exists($name)) {
54
                return ltrim($name, '\\');
55
            }
56
        }
57
58
        return null;
59
    }
60
61
    /**
62
     * Like {@see ClassHelper::resolveClassName()}, but throws an exception
63
     * if the class can not be found.
64
     *
65
     * @param string $legacyName
66
     * @param string $nsPrefix Optional namespace prefix, if the namespace contains
67
     *                         the vendor name, for example (Vendor\PackageName\Folder\Class).
68
     * @return string
69
     * @throws ClassNotExistsException
70
     */
71
    public static function requireResolvedClass(string $legacyName, string $nsPrefix='') : string
72
    {
73
        $class = self::resolveClassName($legacyName, $nsPrefix);
74
75
        if($class !== null)
76
        {
77
            return $class;
78
        }
79
80
        throw new ClassNotExistsException(
81
            $legacyName,
82
            self::ERROR_CANNOT_RESOLVE_CLASS_NAME
83
        );
84
    }
85
86
    /**
87
     * Throws an exception if the target class can not be found.
88
     *
89
     * @param string $className
90
     * @return void
91
     * @throws ClassNotExistsException
92
     */
93
    public static function requireClassExists(string $className) : void
94
    {
95
        if(class_exists($className))
96
        {
97
            return;
98
        }
99
100
        throw new ClassNotExistsException($className);
101
    }
102
103
    /**
104
     * Requires the target class name to exist, and extend
105
     * or implement the specified class/interface. If it does
106
     * not, an exception is thrown.
107
     *
108
     * @param class-string $targetClass
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
109
     * @param class-string $extendsClass
110
     * @return void
111
     *
112
     * @throws ClassNotImplementsException
113
     * @throws ClassNotExistsException
114
     */
115
    public static function requireClassInstanceOf(string $targetClass, string $extendsClass) : void
116
    {
117
        self::requireClassExists($targetClass);
118
        self::requireClassExists($extendsClass);
119
120
        if(is_a($targetClass, $extendsClass, true))
121
        {
122
            return;
123
        }
124
125
        throw new ClassNotImplementsException($extendsClass, $targetClass);
126
    }
127
128
    /**
129
     * If the target object is not an instance of the target class
130
     * or interface, throws an exception.
131
     *
132
     * NOTE: If an exception is passed as object, a class helper
133
     * exception is thrown with the error code {@see ClassHelper::ERROR_THROWABLE_GIVEN_AS_OBJECT},
134
     * and the original exception as previous exception.
135
     *
136
     * @template ClassInstanceType
137
     * @param class-string<ClassInstanceType> $class
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string<ClassInstanceType> at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string<ClassInstanceType>.
Loading history...
138
     * @param object $object
139
     * @param int $errorCode
140
     * @return ClassInstanceType
141
     *
142
     * @throws ClassNotExistsException
143
     * @throws ClassNotImplementsException
144
     */
145
    public static function requireObjectInstanceOf(string $class, object $object, int $errorCode=0)
146
    {
147
        if($object instanceof Throwable)
148
        {
149
            throw new ClassNotExistsException(
150
                $class,
151
                self::ERROR_THROWABLE_GIVEN_AS_OBJECT,
152
                $object
153
            );
154
        }
155
156
        if(!class_exists($class) && !interface_exists($class) && !trait_exists($class))
157
        {
158
            throw new ClassNotExistsException($class, $errorCode);
159
        }
160
161
        if(is_a($object, $class, true))
162
        {
163
            return $object;
164
        }
165
166
        throw new ClassNotImplementsException($class, $object, $errorCode);
167
    }
168
169
    /**
170
     * Retrieves an instance of the Composer class loader of
171
     * the current project. This assumes the usual structure
172
     * with this library being stored in the `vendor` folder.
173
     *
174
     * NOTE: Also works when working on a local copy of the
175
     * Git package.
176
     *
177
     * @return ClassLoader
178
     * @throws ClassLoaderNotFoundException
179
     */
180
    public static function getClassLoader() : ClassLoader
181
    {
182
        if(isset(self::$classLoader)) {
183
            return self::$classLoader;
184
        }
185
186
        // Paths are either the folder structure when the
187
        // package has been installed as a dependency via
188
        // composer, or a local installation of the git package.
189
        $paths = array(
190
            __DIR__.'/../../../autoload.php',
191
            __DIR__.'/../vendor/autoload.php'
192
        );
193
194
        $autoloadFile = null;
195
196
        foreach($paths as $path)
197
        {
198
            if(file_exists($path)) {
199
                $autoloadFile = $path;
200
            }
201
        }
202
203
        if($autoloadFile === null) {
204
            throw new ClassLoaderNotFoundException($paths);
205
        }
206
207
        $loader = require $autoloadFile;
208
209
        if (!$loader instanceof ClassLoader)
210
        {
211
            throw new ClassLoaderNotFoundException($paths);
212
        }
213
214
        self::$classLoader = $loader;
215
216
        return self::$classLoader;
0 ignored issues
show
Bug Best Practice introduced by
The expression return self::classLoader returns the type null which is incompatible with the type-hinted return Composer\Autoload\ClassLoader.
Loading history...
217
    }
218
219
    /**
220
     * Gets the last part in a class name, e.g.:
221
     *
222
     * - `Class_Name_With_Underscores` -> `Underscores`
223
     * - `Class\With\Namespace` -> `Namespace`
224
     *
225
     * @param class-string|string|object $subject
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string|string|object at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string|string|object.
Loading history...
226
     * @return string
227
     */
228
    public static function getClassTypeName($subject) : string
229
    {
230
        $parts = self::splitClass($subject);
231
        return array_pop($parts);
232
    }
233
234
    /**
235
     * Retrieves the namespace part of a class name, if any.
236
     *
237
     * @param class-string|string|object $subject
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string|string|object at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string|string|object.
Loading history...
238
     * @return string
239
     */
240
    public static function getClassNamespace($subject) : string
241
    {
242
        $parts = self::splitClass($subject);
243
        array_pop($parts);
244
245
        return ltrim(implode('\\', $parts), '\\');
246
    }
247
248
    /**
249
     * @param class-string|string|object $subject
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string|string|object at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string|string|object.
Loading history...
250
     * @return string[]
251
     */
252
    private static function splitClass($subject) : array
253
    {
254
        if(is_object($subject)) {
255
            $class = get_class($subject);
256
        } else {
257
            $class = $subject;
258
        }
259
260
        $class = str_replace('\\', '_', $class);
261
262
        return explode('_', $class);
263
    }
264
}
265