1<?php
2/**
3 * @see       https://github.com/zendframework/zend-loader for the canonical source repository
4 * @copyright Copyright (c) 2005-2018 Zend Technologies USA Inc. (https://www.zend.com)
5 * @license   https://github.com/zendframework/zend-loader/blob/master/LICENSE.md New BSD License
6 */
7
8namespace Zend\Loader;
9
10// Grab SplAutoloader interface
11require_once __DIR__ . '/SplAutoloader.php';
12
13/**
14 * PSR-0 compliant autoloader
15 *
16 * Allows autoloading both namespaced and vendor-prefixed classes. Class
17 * lookups are performed on the filesystem. If a class file for the referenced
18 * class is not found, a PHP warning will be raised by include().
19 */
20class StandardAutoloader implements SplAutoloader
21{
22    const NS_SEPARATOR     = '\\';
23    const PREFIX_SEPARATOR = '_';
24    const LOAD_NS          = 'namespaces';
25    const LOAD_PREFIX      = 'prefixes';
26    const ACT_AS_FALLBACK  = 'fallback_autoloader';
27    const AUTOREGISTER_ZF  = 'autoregister_zf';
28
29    /**
30     * @var array Namespace/directory pairs to search; ZF library added by default
31     */
32    protected $namespaces = [];
33
34    /**
35     * @var array Prefix/directory pairs to search
36     */
37    protected $prefixes = [];
38
39    /**
40     * @var bool Whether or not the autoloader should also act as a fallback autoloader
41     */
42    protected $fallbackAutoloaderFlag = false;
43
44    /**
45     * Constructor
46     *
47     * @param  null|array|\Traversable $options
48     */
49    public function __construct($options = null)
50    {
51        if (null !== $options) {
52            $this->setOptions($options);
53        }
54    }
55
56    /**
57     * Configure autoloader
58     *
59     * Allows specifying both "namespace" and "prefix" pairs, using the
60     * following structure:
61     * <code>
62     * array(
63     *     'namespaces' => array(
64     *         'Zend'     => '/path/to/Zend/library',
65     *         'Doctrine' => '/path/to/Doctrine/library',
66     *     ),
67     *     'prefixes' => array(
68     *         'Phly_'     => '/path/to/Phly/library',
69     *     ),
70     *     'fallback_autoloader' => true,
71     * )
72     * </code>
73     *
74     * @param  array|\Traversable $options
75     * @throws Exception\InvalidArgumentException
76     * @return StandardAutoloader
77     */
78    public function setOptions($options)
79    {
80        if (! is_array($options) && ! ($options instanceof \Traversable)) {
81            require_once __DIR__ . '/Exception/InvalidArgumentException.php';
82            throw new Exception\InvalidArgumentException('Options must be either an array or Traversable');
83        }
84
85        foreach ($options as $type => $pairs) {
86            switch ($type) {
87                case self::AUTOREGISTER_ZF:
88                    if ($pairs) {
89                        $this->registerNamespace('Zend', dirname(__DIR__));
90                        $this->registerNamespace(
91                            'ZendXml',
92                            dirname(dirname((__DIR__))) . DIRECTORY_SEPARATOR .  'ZendXml'
93                        );
94                    }
95                    break;
96                case self::LOAD_NS:
97                    if (is_array($pairs) || $pairs instanceof \Traversable) {
98                        $this->registerNamespaces($pairs);
99                    }
100                    break;
101                case self::LOAD_PREFIX:
102                    if (is_array($pairs) || $pairs instanceof \Traversable) {
103                        $this->registerPrefixes($pairs);
104                    }
105                    break;
106                case self::ACT_AS_FALLBACK:
107                    $this->setFallbackAutoloader($pairs);
108                    break;
109                default:
110                    // ignore
111            }
112        }
113        return $this;
114    }
115
116    /**
117     * Set flag indicating fallback autoloader status
118     *
119     * @param  bool $flag
120     * @return StandardAutoloader
121     */
122    public function setFallbackAutoloader($flag)
123    {
124        $this->fallbackAutoloaderFlag = (bool) $flag;
125        return $this;
126    }
127
128    /**
129     * Is this autoloader acting as a fallback autoloader?
130     *
131     * @return bool
132     */
133    public function isFallbackAutoloader()
134    {
135        return $this->fallbackAutoloaderFlag;
136    }
137
138    /**
139     * Register a namespace/directory pair
140     *
141     * @param  string $namespace
142     * @param  string $directory
143     * @return StandardAutoloader
144     */
145    public function registerNamespace($namespace, $directory)
146    {
147        $namespace = rtrim($namespace, self::NS_SEPARATOR) . self::NS_SEPARATOR;
148        $this->namespaces[$namespace] = $this->normalizeDirectory($directory);
149        return $this;
150    }
151
152    /**
153     * Register many namespace/directory pairs at once
154     *
155     * @param  array $namespaces
156     * @throws Exception\InvalidArgumentException
157     * @return StandardAutoloader
158     */
159    public function registerNamespaces($namespaces)
160    {
161        if (! is_array($namespaces) && ! $namespaces instanceof \Traversable) {
162            require_once __DIR__ . '/Exception/InvalidArgumentException.php';
163            throw new Exception\InvalidArgumentException('Namespace pairs must be either an array or Traversable');
164        }
165
166        foreach ($namespaces as $namespace => $directory) {
167            $this->registerNamespace($namespace, $directory);
168        }
169        return $this;
170    }
171
172    /**
173     * Register a prefix/directory pair
174     *
175     * @param  string $prefix
176     * @param  string $directory
177     * @return StandardAutoloader
178     */
179    public function registerPrefix($prefix, $directory)
180    {
181        $prefix = rtrim($prefix, self::PREFIX_SEPARATOR). self::PREFIX_SEPARATOR;
182        $this->prefixes[$prefix] = $this->normalizeDirectory($directory);
183        return $this;
184    }
185
186    /**
187     * Register many namespace/directory pairs at once
188     *
189     * @param  array $prefixes
190     * @throws Exception\InvalidArgumentException
191     * @return StandardAutoloader
192     */
193    public function registerPrefixes($prefixes)
194    {
195        if (! is_array($prefixes) && ! $prefixes instanceof \Traversable) {
196            require_once __DIR__ . '/Exception/InvalidArgumentException.php';
197            throw new Exception\InvalidArgumentException('Prefix pairs must be either an array or Traversable');
198        }
199
200        foreach ($prefixes as $prefix => $directory) {
201            $this->registerPrefix($prefix, $directory);
202        }
203        return $this;
204    }
205
206    /**
207     * Defined by Autoloadable; autoload a class
208     *
209     * @param  string $class
210     * @return false|string
211     */
212    public function autoload($class)
213    {
214        $isFallback = $this->isFallbackAutoloader();
215        if (false !== strpos($class, self::NS_SEPARATOR)) {
216            if ($this->loadClass($class, self::LOAD_NS)) {
217                return $class;
218            } elseif ($isFallback) {
219                return $this->loadClass($class, self::ACT_AS_FALLBACK);
220            }
221            return false;
222        }
223        if (false !== strpos($class, self::PREFIX_SEPARATOR)) {
224            if ($this->loadClass($class, self::LOAD_PREFIX)) {
225                return $class;
226            } elseif ($isFallback) {
227                return $this->loadClass($class, self::ACT_AS_FALLBACK);
228            }
229            return false;
230        }
231        if ($isFallback) {
232            return $this->loadClass($class, self::ACT_AS_FALLBACK);
233        }
234        return false;
235    }
236
237    /**
238     * Register the autoloader with spl_autoload
239     *
240     * @return void
241     */
242    public function register()
243    {
244        spl_autoload_register([$this, 'autoload']);
245    }
246
247    /**
248     * Transform the class name to a filename
249     *
250     * @param  string $class
251     * @param  string $directory
252     * @return string
253     */
254    protected function transformClassNameToFilename($class, $directory)
255    {
256        // $class may contain a namespace portion, in  which case we need
257        // to preserve any underscores in that portion.
258        $matches = [];
259        preg_match('/(?P<namespace>.+\\\)?(?P<class>[^\\\]+$)/', $class, $matches);
260
261        $class     = (isset($matches['class'])) ? $matches['class'] : '';
262        $namespace = (isset($matches['namespace'])) ? $matches['namespace'] : '';
263
264        return $directory
265             . str_replace(self::NS_SEPARATOR, '/', $namespace)
266             . str_replace(self::PREFIX_SEPARATOR, '/', $class)
267             . '.php';
268    }
269
270    /**
271     * Load a class, based on its type (namespaced or prefixed)
272     *
273     * @param  string $class
274     * @param  string $type
275     * @return bool|string
276     * @throws Exception\InvalidArgumentException
277     */
278    protected function loadClass($class, $type)
279    {
280        if (! in_array($type, [self::LOAD_NS, self::LOAD_PREFIX, self::ACT_AS_FALLBACK])) {
281            require_once __DIR__ . '/Exception/InvalidArgumentException.php';
282            throw new Exception\InvalidArgumentException();
283        }
284
285        // Fallback autoloading
286        if ($type === self::ACT_AS_FALLBACK) {
287            // create filename
288            $filename     = $this->transformClassNameToFilename($class, '');
289            $resolvedName = stream_resolve_include_path($filename);
290            if ($resolvedName !== false) {
291                return include $resolvedName;
292            }
293            return false;
294        }
295
296        // Namespace and/or prefix autoloading
297        foreach ($this->$type as $leader => $path) {
298            if (0 === strpos($class, $leader)) {
299                // Trim off leader (namespace or prefix)
300                $trimmedClass = substr($class, strlen($leader));
301
302                // create filename
303                $filename = $this->transformClassNameToFilename($trimmedClass, $path);
304                if (file_exists($filename)) {
305                    return include $filename;
306                }
307            }
308        }
309        return false;
310    }
311
312    /**
313     * Normalize the directory to include a trailing directory separator
314     *
315     * @param  string $directory
316     * @return string
317     */
318    protected function normalizeDirectory($directory)
319    {
320        $last = $directory[strlen($directory) - 1];
321        if (in_array($last, ['/', '\\'])) {
322            $directory[strlen($directory) - 1] = DIRECTORY_SEPARATOR;
323            return $directory;
324        }
325        $directory .= DIRECTORY_SEPARATOR;
326        return $directory;
327    }
328}
329