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