ClassLoader::getBasePaths()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
crap 1
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