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