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
|
|
|
|