|
1
|
|
|
<?php |
|
2
|
|
|
|
|
3
|
|
|
namespace Riimu\Kit\ClassLoader; |
|
4
|
|
|
|
|
5
|
|
|
/** |
|
6
|
|
|
* Class loader that supports both PSR-0 and PSR-4 autoloading standards. |
|
7
|
|
|
* |
|
8
|
|
|
* The purpose autoloading classes is to load the class files only as they are |
|
9
|
|
|
* needed. This reduces the overall page overhead, as every file does not need |
|
10
|
|
|
* to be requested on every request. It also makes managing class loading much |
|
11
|
|
|
* simpler. |
|
12
|
|
|
* |
|
13
|
|
|
* The standard practice in autoloading is to place classes in files that are |
|
14
|
|
|
* named according to the class names and placed in a directory hierarchy |
|
15
|
|
|
* according to their namespace. ClassLoader supports two such standard |
|
16
|
|
|
* autoloading practices: PSR-0 and PSR-4. |
|
17
|
|
|
* |
|
18
|
|
|
* Class paths can be provided as base paths, which are appended with the full |
|
19
|
|
|
* class name (as per PSR-0), or as prefix paths that can replace part of the |
|
20
|
|
|
* namespace with a specific directory (as per PSR-4). Depending on which kind |
|
21
|
|
|
* of paths are added, the underscores may or may not be treated as namespace |
|
22
|
|
|
* separators. |
|
23
|
|
|
* |
|
24
|
|
|
* @see http://www.php-fig.org/psr/psr-0/ |
|
25
|
|
|
* @see http://www.php-fig.org/psr/psr-4/ |
|
26
|
|
|
* @author Riikka Kalliomäki <[email protected]> |
|
27
|
|
|
* @copyright Copyright (c) 2014-2017 Riikka Kalliomäki |
|
28
|
|
|
* @license http://opensource.org/licenses/mit-license.php MIT License |
|
29
|
|
|
*/ |
|
30
|
|
|
class ClassLoader |
|
31
|
|
|
{ |
|
32
|
|
|
/** @var array List of PSR-4 compatible paths by namespace */ |
|
33
|
|
|
private $prefixPaths; |
|
34
|
|
|
|
|
35
|
|
|
/** @var array List of PSR-0 compatible paths by namespace */ |
|
36
|
|
|
private $basePaths; |
|
37
|
|
|
|
|
38
|
|
|
/** @var bool Whether to look for classes in include_path or not */ |
|
39
|
|
|
private $useIncludePath; |
|
40
|
|
|
|
|
41
|
|
|
/** @var callable The autoload method used to load classes */ |
|
42
|
|
|
private $loader; |
|
43
|
|
|
|
|
44
|
|
|
/** @var \Riimu\Kit\ClassLoader\ClassFinder Finder used to find class files */ |
|
45
|
|
|
private $finder; |
|
46
|
|
|
|
|
47
|
|
|
/** @var bool Whether loadClass should return values and throw exceptions or not */ |
|
48
|
|
|
protected $verbose; |
|
49
|
|
|
|
|
50
|
|
|
/** |
|
51
|
|
|
* Creates a new ClassLoader instance. |
|
52
|
|
|
*/ |
|
53
|
96 |
|
public function __construct() |
|
54
|
|
|
{ |
|
55
|
96 |
|
$this->prefixPaths = []; |
|
56
|
96 |
|
$this->basePaths = []; |
|
57
|
96 |
|
$this->useIncludePath = false; |
|
58
|
96 |
|
$this->verbose = true; |
|
59
|
96 |
|
$this->loader = [$this, 'loadClass']; |
|
60
|
96 |
|
$this->finder = new ClassFinder(); |
|
61
|
96 |
|
} |
|
62
|
|
|
|
|
63
|
|
|
/** |
|
64
|
|
|
* Registers this instance as a class autoloader. |
|
65
|
|
|
* @return bool True if the registration was successful, false if not |
|
66
|
|
|
*/ |
|
67
|
18 |
|
public function register() |
|
68
|
|
|
{ |
|
69
|
18 |
|
return spl_autoload_register($this->loader); |
|
70
|
|
|
} |
|
71
|
|
|
|
|
72
|
|
|
/** |
|
73
|
|
|
* Unregisters this instance as a class autoloader. |
|
74
|
|
|
* @return bool True if the unregistration was successful, false if not |
|
75
|
|
|
*/ |
|
76
|
24 |
|
public function unregister() |
|
77
|
|
|
{ |
|
78
|
24 |
|
return spl_autoload_unregister($this->loader); |
|
79
|
|
|
} |
|
80
|
|
|
|
|
81
|
|
|
/** |
|
82
|
|
|
* Tells if this instance is currently registered as a class autoloader. |
|
83
|
|
|
* @return bool True if registered, false if not |
|
84
|
|
|
*/ |
|
85
|
9 |
|
public function isRegistered() |
|
86
|
|
|
{ |
|
87
|
9 |
|
return in_array($this->loader, spl_autoload_functions(), true); |
|
88
|
|
|
} |
|
89
|
|
|
|
|
90
|
|
|
/** |
|
91
|
|
|
* Tells whether to use include_path as part of base paths. |
|
92
|
|
|
* |
|
93
|
|
|
* When enabled, the directory paths in include_path are treated as base |
|
94
|
|
|
* paths where to look for classes. This option defaults to false for PSR-4 |
|
95
|
|
|
* compliance. |
|
96
|
|
|
* |
|
97
|
|
|
* @param bool $enabled True to use include_path, false to not use |
|
98
|
|
|
* @return ClassLoader Returns self for call chaining |
|
99
|
|
|
*/ |
|
100
|
3 |
|
public function useIncludePath($enabled = true) |
|
101
|
|
|
{ |
|
102
|
3 |
|
$this->useIncludePath = (bool) $enabled; |
|
103
|
|
|
|
|
104
|
3 |
|
return $this; |
|
105
|
|
|
} |
|
106
|
|
|
|
|
107
|
|
|
/** |
|
108
|
|
|
* Sets whether to return values and throw exceptions from loadClass. |
|
109
|
|
|
* |
|
110
|
|
|
* PSR-4 requires that autoloaders do not return values and do not throw |
|
111
|
|
|
* exceptions from the autoloader. By default, the verbose mode is set to |
|
112
|
|
|
* false for PSR-4 compliance. |
|
113
|
|
|
* |
|
114
|
|
|
* @param bool $enabled True to return values and exceptions, false to not |
|
115
|
|
|
* @return ClassLoader Returns self for call chaining |
|
116
|
|
|
*/ |
|
117
|
24 |
|
public function setVerbose($enabled) |
|
118
|
|
|
{ |
|
119
|
24 |
|
$this->verbose = (bool) $enabled; |
|
120
|
|
|
|
|
121
|
24 |
|
return $this; |
|
122
|
|
|
} |
|
123
|
|
|
|
|
124
|
|
|
/** |
|
125
|
|
|
* Sets list of dot included file extensions to use for finding files. |
|
126
|
|
|
* |
|
127
|
|
|
* If no list of extensions is provided, the extension array defaults to |
|
128
|
|
|
* just '.php'. |
|
129
|
|
|
* |
|
130
|
|
|
* @param string[] $extensions Array of dot included file extensions to use |
|
131
|
|
|
* @return ClassLoader Returns self for call chaining |
|
132
|
|
|
*/ |
|
133
|
3 |
|
public function setFileExtensions(array $extensions) |
|
134
|
|
|
{ |
|
135
|
3 |
|
$this->finder->setFileExtensions($extensions); |
|
136
|
|
|
|
|
137
|
3 |
|
return $this; |
|
138
|
|
|
} |
|
139
|
|
|
|
|
140
|
|
|
/** |
|
141
|
|
|
* Adds a PSR-0 compliant base path for searching classes. |
|
142
|
|
|
* |
|
143
|
|
|
* In PSR-0, the class namespace structure directly reflects the location |
|
144
|
|
|
* in the directory tree. A base path indicates the base directory where to |
|
145
|
|
|
* search for classes. For example, if the class 'Foo\Bar', is defined in |
|
146
|
|
|
* '/usr/lib/Foo/Bar.php', you would simply need to add the directory |
|
147
|
|
|
* '/usr/lib' by calling: |
|
148
|
|
|
* |
|
149
|
|
|
* <code>addBasePath('/usr/lib')</code> |
|
150
|
|
|
* |
|
151
|
|
|
* Additionally, you may specify that the base path applies only to a |
|
152
|
|
|
* specific namespace. You can do this by adding the namespace as the second |
|
153
|
|
|
* parameter. For example, if you would like the path in the previous |
|
154
|
|
|
* example to only apply to the namespace 'Foo', you could do so by calling: |
|
155
|
|
|
* |
|
156
|
|
|
* <code>addBasePath('/usr/lib/', 'Foo')</code> |
|
157
|
|
|
* |
|
158
|
|
|
* Note that as per PSR-0, the underscores in the class name are treated |
|
159
|
|
|
* as namespace separators. Therefore 'Foo_Bar_Baz', would need to reside |
|
160
|
|
|
* in 'Foo/Bar/Baz.php'. Regardless of whether the namespace is indicated |
|
161
|
|
|
* by namespace separators or underscores, the namespace parameter must be |
|
162
|
|
|
* defined using namespace separators, e.g 'Foo\Bar'. |
|
163
|
|
|
* |
|
164
|
|
|
* In addition to providing a single path as a string, you may also provide |
|
165
|
|
|
* an array of paths. It is also possible to provide an associative array |
|
166
|
|
|
* where the keys indicate the namespaces. Each value in the associative |
|
167
|
|
|
* array may also be a string or an array of paths. |
|
168
|
|
|
* |
|
169
|
|
|
* @param string|array $path Single path, array of paths or an associative array |
|
170
|
|
|
* @param string|null $namespace Limit the path only to specific namespace |
|
171
|
|
|
* @return ClassLoader Returns self for call chaining |
|
172
|
|
|
*/ |
|
173
|
63 |
|
public function addBasePath($path, $namespace = null) |
|
174
|
|
|
{ |
|
175
|
63 |
|
$this->addPath($this->basePaths, $path, $namespace); |
|
176
|
|
|
|
|
177
|
63 |
|
return $this; |
|
178
|
|
|
} |
|
179
|
|
|
|
|
180
|
|
|
/** |
|
181
|
|
|
* Returns all known base paths. |
|
182
|
|
|
* |
|
183
|
|
|
* The paths will be returned as an associative array. The key indicates |
|
184
|
|
|
* the namespace and the values are arrays that contain all paths that |
|
185
|
|
|
* apply to that specific namespace. Paths that apply to all namespaces can |
|
186
|
|
|
* be found inside the key '' (i.e. empty string). Note that the array does |
|
187
|
|
|
* not include the paths in include_path even if the use of include_path is |
|
188
|
|
|
* enabled. |
|
189
|
|
|
* |
|
190
|
|
|
* @return array All known base paths |
|
191
|
|
|
*/ |
|
192
|
18 |
|
public function getBasePaths() |
|
193
|
|
|
{ |
|
194
|
18 |
|
return $this->basePaths; |
|
195
|
|
|
} |
|
196
|
|
|
|
|
197
|
|
|
/** |
|
198
|
|
|
* Adds a PSR-4 compliant prefix path for searching classes. |
|
199
|
|
|
* |
|
200
|
|
|
* In PSR-4, it is possible to replace part of namespace with specific |
|
201
|
|
|
* path in the directory tree instead of requiring the entire namespace |
|
202
|
|
|
* structure to be present in the directory tree. For example, if the class |
|
203
|
|
|
* 'Vendor\Library\Class' is located in '/usr/lib/Library/src/Class.php', |
|
204
|
|
|
* You would need to add the path '/usr/lib/Library/src' to the namespace |
|
205
|
|
|
* 'Vendor\Library' by calling: |
|
206
|
|
|
* |
|
207
|
|
|
* <code>addPrefixPath('/usr/lib/Library/src', 'Vendor\Library')</code> |
|
208
|
|
|
* |
|
209
|
|
|
* If the method is called without providing a namespace, then the paths |
|
210
|
|
|
* work similarly to paths added via addBasePath(), except that the |
|
211
|
|
|
* underscores in the file name are not treated as namespace separators. |
|
212
|
|
|
* |
|
213
|
|
|
* Similarly to addBasePath(), the paths may be provided as an array or you |
|
214
|
|
|
* can just provide a single associative array as the parameter. |
|
215
|
|
|
* |
|
216
|
|
|
* @param string|array $path Single path or array of paths |
|
217
|
|
|
* @param string|null $namespace The namespace prefix the given path replaces |
|
218
|
|
|
* @return ClassLoader Returns self for call chaining |
|
219
|
|
|
*/ |
|
220
|
21 |
|
public function addPrefixPath($path, $namespace = null) |
|
221
|
|
|
{ |
|
222
|
21 |
|
$this->addPath($this->prefixPaths, $path, $namespace); |
|
223
|
|
|
|
|
224
|
21 |
|
return $this; |
|
225
|
|
|
} |
|
226
|
|
|
|
|
227
|
|
|
/** |
|
228
|
|
|
* Returns all known prefix paths. |
|
229
|
|
|
* |
|
230
|
|
|
* The paths will be returned as an associative array. The key indicates |
|
231
|
|
|
* the namespace and the values are arrays that contain all paths that |
|
232
|
|
|
* apply to that specific namespace. Paths that apply to all namespaces can |
|
233
|
|
|
* be found inside the key '' (i.e. empty string). |
|
234
|
|
|
* |
|
235
|
|
|
* @return array All known prefix paths |
|
236
|
|
|
*/ |
|
237
|
18 |
|
public function getPrefixPaths() |
|
238
|
|
|
{ |
|
239
|
18 |
|
return $this->prefixPaths; |
|
240
|
|
|
} |
|
241
|
|
|
|
|
242
|
|
|
/** |
|
243
|
|
|
* Adds the paths to the list of paths according to the provided parameters. |
|
244
|
|
|
* @param array $list List of paths to modify |
|
245
|
|
|
* @param string|array $path Single path or array of paths |
|
246
|
|
|
* @param string|null $namespace The namespace definition |
|
247
|
|
|
*/ |
|
248
|
66 |
|
private function addPath(& $list, $path, $namespace) |
|
249
|
|
|
{ |
|
250
|
66 |
|
if ($namespace !== null) { |
|
251
|
21 |
|
$paths = [$namespace => $path]; |
|
252
|
7 |
|
} else { |
|
253
|
51 |
|
$paths = is_array($path) ? $path : ['' => $path]; |
|
254
|
|
|
} |
|
255
|
|
|
|
|
256
|
66 |
|
foreach ($paths as $ns => $directories) { |
|
257
|
66 |
|
$this->addNamespacePaths($list, ltrim($ns, '0..9'), $directories); |
|
258
|
22 |
|
} |
|
259
|
66 |
|
} |
|
260
|
|
|
|
|
261
|
|
|
/** |
|
262
|
|
|
* Canonizes the namespace and adds the paths to that specific namespace. |
|
263
|
|
|
* @param array $list List of paths to modify |
|
264
|
|
|
* @param string $namespace Namespace for the paths |
|
265
|
|
|
* @param string[] $paths List of paths for the namespace |
|
266
|
|
|
*/ |
|
267
|
66 |
|
private function addNamespacePaths(& $list, $namespace, $paths) |
|
268
|
|
|
{ |
|
269
|
66 |
|
$namespace = $namespace === '' ? '' : trim($namespace, '\\') . '\\'; |
|
270
|
|
|
|
|
271
|
66 |
|
if (!isset($list[$namespace])) { |
|
272
|
66 |
|
$list[$namespace] = []; |
|
273
|
22 |
|
} |
|
274
|
|
|
|
|
275
|
66 |
|
if (is_array($paths)) { |
|
276
|
12 |
|
$list[$namespace] = array_merge($list[$namespace], $paths); |
|
277
|
4 |
|
} else { |
|
278
|
60 |
|
$list[$namespace][] = $paths; |
|
279
|
|
|
} |
|
280
|
66 |
|
} |
|
281
|
|
|
|
|
282
|
|
|
/** |
|
283
|
|
|
* Attempts to load the class using known class paths. |
|
284
|
|
|
* |
|
285
|
|
|
* The classes will be searched using the prefix paths, base paths and the |
|
286
|
|
|
* include_path (if enabled) in that order. Other than that, the autoloader |
|
287
|
|
|
* makes no guarantees about the order of the searched paths. |
|
288
|
|
|
* |
|
289
|
|
|
* If verbose mode is enabled, then the method will return true if the class |
|
290
|
|
|
* loading was successful and false if not. Additionally the method will |
|
291
|
|
|
* throw an exception if the class already exists or if the class was not |
|
292
|
|
|
* defined in the file that was included. |
|
293
|
|
|
* |
|
294
|
|
|
* @param string $class Full name of the class to load |
|
295
|
|
|
* @return bool|null True if the class was loaded, false if not |
|
296
|
|
|
* @throws \RuntimeException If the class was not defined in the included file |
|
297
|
|
|
* @throws \InvalidArgumentException If the class already exists |
|
298
|
|
|
*/ |
|
299
|
57 |
|
public function loadClass($class) |
|
300
|
|
|
{ |
|
301
|
57 |
|
if ($this->verbose) { |
|
302
|
51 |
|
return $this->load($class); |
|
303
|
|
|
} |
|
304
|
|
|
|
|
305
|
|
|
try { |
|
306
|
9 |
|
$this->load($class); |
|
307
|
9 |
|
} catch (\Exception $exception) { |
|
308
|
|
|
// Ignore exceptions as per PSR-4 |
|
309
|
|
|
} |
|
310
|
9 |
|
} |
|
311
|
|
|
|
|
312
|
|
|
/** |
|
313
|
|
|
* Actually loads the class without any regard to verbose setting. |
|
314
|
|
|
* @param string $class Full name of the class to load |
|
315
|
|
|
* @return bool True if the class was loaded, false if not |
|
316
|
|
|
* @throws \InvalidArgumentException If the class already exists |
|
317
|
|
|
*/ |
|
318
|
57 |
|
private function load($class) |
|
319
|
|
|
{ |
|
320
|
57 |
|
if ($this->isLoaded($class)) { |
|
321
|
3 |
|
throw new \InvalidArgumentException(sprintf( |
|
322
|
3 |
|
"Error loading class '%s', the class already exists", |
|
323
|
1 |
|
$class |
|
324
|
1 |
|
)); |
|
325
|
|
|
} |
|
326
|
|
|
|
|
327
|
54 |
|
if ($file = $this->findFile($class)) { |
|
328
|
39 |
|
return $this->loadFile($file, $class); |
|
329
|
|
|
} |
|
330
|
|
|
|
|
331
|
24 |
|
return false; |
|
332
|
|
|
} |
|
333
|
|
|
|
|
334
|
|
|
/** |
|
335
|
|
|
* Attempts to find a file for the given class using known paths. |
|
336
|
|
|
* @param string $class Full name of the class |
|
337
|
|
|
* @return string|false Path to the class file or false if not found |
|
338
|
|
|
*/ |
|
339
|
60 |
|
public function findFile($class) |
|
340
|
|
|
{ |
|
341
|
60 |
|
return $this->finder->findFile($class, $this->prefixPaths, $this->basePaths, $this->useIncludePath); |
|
342
|
|
|
} |
|
343
|
|
|
|
|
344
|
|
|
/** |
|
345
|
|
|
* Includes the file and makes sure the class exists. |
|
346
|
|
|
* @param string $file Full path to the file |
|
347
|
|
|
* @param string $class Full name of the class |
|
348
|
|
|
* @return bool Always returns true |
|
349
|
|
|
* @throws \RuntimeException If the class was not defined in the included file |
|
350
|
|
|
*/ |
|
351
|
39 |
|
protected function loadFile($file, $class) |
|
352
|
|
|
{ |
|
353
|
39 |
|
include $file; |
|
354
|
|
|
|
|
355
|
39 |
|
if (!$this->isLoaded($class)) { |
|
356
|
9 |
|
throw new \RuntimeException(vsprintf( |
|
357
|
9 |
|
"Error loading class '%s', the class was not defined in the file '%s'", |
|
358
|
9 |
|
[$class, $file] |
|
359
|
3 |
|
)); |
|
360
|
|
|
} |
|
361
|
|
|
|
|
362
|
30 |
|
return true; |
|
363
|
|
|
} |
|
364
|
|
|
|
|
365
|
|
|
/** |
|
366
|
|
|
* Tells if a class, interface or trait exists with given name. |
|
367
|
|
|
* @param string $class Full name of the class |
|
368
|
|
|
* @return bool True if it exists, false if not |
|
369
|
|
|
*/ |
|
370
|
57 |
|
private function isLoaded($class) |
|
371
|
|
|
{ |
|
372
|
57 |
|
return class_exists($class, false) || |
|
373
|
54 |
|
interface_exists($class, false) || |
|
374
|
57 |
|
trait_exists($class, false); |
|
375
|
|
|
} |
|
376
|
|
|
} |
|
377
|
|
|
|