Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.
   1  <?php
   2  
   3  /*
   4   * This file is part of Composer.
   5   *
   6   * (c) Nils Adermann <naderman@naderman.de>
   7   *     Jordi Boggiano <j.boggiano@seld.be>
   8   *
   9   * For the full copyright and license information, please view the LICENSE
  10   * file that was distributed with this source code.
  11   */
  12  
  13  namespace Composer\Autoload;
  14  
  15  /**
  16   * ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
  17   *
  18   *     $loader = new \Composer\Autoload\ClassLoader();
  19   *
  20   *     // register classes with namespaces
  21   *     $loader->add('Symfony\Component', __DIR__.'/component');
  22   *     $loader->add('Symfony',           __DIR__.'/framework');
  23   *
  24   *     // activate the autoloader
  25   *     $loader->register();
  26   *
  27   *     // to enable searching the include path (eg. for PEAR packages)
  28   *     $loader->setUseIncludePath(true);
  29   *
  30   * In this example, if you try to use a class in the Symfony\Component
  31   * namespace or one of its children (Symfony\Component\Console for instance),
  32   * the autoloader will first look for the class under the component/
  33   * directory, and it will then fallback to the framework/ directory if not
  34   * found before giving up.
  35   *
  36   * This class is loosely based on the Symfony UniversalClassLoader.
  37   *
  38   * @author Fabien Potencier <fabien@symfony.com>
  39   * @author Jordi Boggiano <j.boggiano@seld.be>
  40   * @see    https://www.php-fig.org/psr/psr-0/
  41   * @see    https://www.php-fig.org/psr/psr-4/
  42   */
  43  class ClassLoader
  44  {
  45      /** @var ?string */
  46      private $vendorDir;
  47  
  48      // PSR-4
  49      /**
  50       * @var array[]
  51       * @psalm-var array<string, array<string, int>>
  52       */
  53      private $prefixLengthsPsr4 = array();
  54      /**
  55       * @var array[]
  56       * @psalm-var array<string, array<int, string>>
  57       */
  58      private $prefixDirsPsr4 = array();
  59      /**
  60       * @var array[]
  61       * @psalm-var array<string, string>
  62       */
  63      private $fallbackDirsPsr4 = array();
  64  
  65      // PSR-0
  66      /**
  67       * @var array[]
  68       * @psalm-var array<string, array<string, string[]>>
  69       */
  70      private $prefixesPsr0 = array();
  71      /**
  72       * @var array[]
  73       * @psalm-var array<string, string>
  74       */
  75      private $fallbackDirsPsr0 = array();
  76  
  77      /** @var bool */
  78      private $useIncludePath = false;
  79  
  80      /**
  81       * @var string[]
  82       * @psalm-var array<string, string>
  83       */
  84      private $classMap = array();
  85  
  86      /** @var bool */
  87      private $classMapAuthoritative = false;
  88  
  89      /**
  90       * @var bool[]
  91       * @psalm-var array<string, bool>
  92       */
  93      private $missingClasses = array();
  94  
  95      /** @var ?string */
  96      private $apcuPrefix;
  97  
  98      /**
  99       * @var self[]
 100       */
 101      private static $registeredLoaders = array();
 102  
 103      /**
 104       * @param ?string $vendorDir
 105       */
 106      public function __construct($vendorDir = null)
 107      {
 108          $this->vendorDir = $vendorDir;
 109      }
 110  
 111      /**
 112       * @return string[]
 113       */
 114      public function getPrefixes()
 115      {
 116          if (!empty($this->prefixesPsr0)) {
 117              return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
 118          }
 119  
 120          return array();
 121      }
 122  
 123      /**
 124       * @return array[]
 125       * @psalm-return array<string, array<int, string>>
 126       */
 127      public function getPrefixesPsr4()
 128      {
 129          return $this->prefixDirsPsr4;
 130      }
 131  
 132      /**
 133       * @return array[]
 134       * @psalm-return array<string, string>
 135       */
 136      public function getFallbackDirs()
 137      {
 138          return $this->fallbackDirsPsr0;
 139      }
 140  
 141      /**
 142       * @return array[]
 143       * @psalm-return array<string, string>
 144       */
 145      public function getFallbackDirsPsr4()
 146      {
 147          return $this->fallbackDirsPsr4;
 148      }
 149  
 150      /**
 151       * @return string[] Array of classname => path
 152       * @psalm-return array<string, string>
 153       */
 154      public function getClassMap()
 155      {
 156          return $this->classMap;
 157      }
 158  
 159      /**
 160       * @param string[] $classMap Class to filename map
 161       * @psalm-param array<string, string> $classMap
 162       *
 163       * @return void
 164       */
 165      public function addClassMap(array $classMap)
 166      {
 167          if ($this->classMap) {
 168              $this->classMap = array_merge($this->classMap, $classMap);
 169          } else {
 170              $this->classMap = $classMap;
 171          }
 172      }
 173  
 174      /**
 175       * Registers a set of PSR-0 directories for a given prefix, either
 176       * appending or prepending to the ones previously set for this prefix.
 177       *
 178       * @param string          $prefix  The prefix
 179       * @param string[]|string $paths   The PSR-0 root directories
 180       * @param bool            $prepend Whether to prepend the directories
 181       *
 182       * @return void
 183       */
 184      public function add($prefix, $paths, $prepend = false)
 185      {
 186          if (!$prefix) {
 187              if ($prepend) {
 188                  $this->fallbackDirsPsr0 = array_merge(
 189                      (array) $paths,
 190                      $this->fallbackDirsPsr0
 191                  );
 192              } else {
 193                  $this->fallbackDirsPsr0 = array_merge(
 194                      $this->fallbackDirsPsr0,
 195                      (array) $paths
 196                  );
 197              }
 198  
 199              return;
 200          }
 201  
 202          $first = $prefix[0];
 203          if (!isset($this->prefixesPsr0[$first][$prefix])) {
 204              $this->prefixesPsr0[$first][$prefix] = (array) $paths;
 205  
 206              return;
 207          }
 208          if ($prepend) {
 209              $this->prefixesPsr0[$first][$prefix] = array_merge(
 210                  (array) $paths,
 211                  $this->prefixesPsr0[$first][$prefix]
 212              );
 213          } else {
 214              $this->prefixesPsr0[$first][$prefix] = array_merge(
 215                  $this->prefixesPsr0[$first][$prefix],
 216                  (array) $paths
 217              );
 218          }
 219      }
 220  
 221      /**
 222       * Registers a set of PSR-4 directories for a given namespace, either
 223       * appending or prepending to the ones previously set for this namespace.
 224       *
 225       * @param string          $prefix  The prefix/namespace, with trailing '\\'
 226       * @param string[]|string $paths   The PSR-4 base directories
 227       * @param bool            $prepend Whether to prepend the directories
 228       *
 229       * @throws \InvalidArgumentException
 230       *
 231       * @return void
 232       */
 233      public function addPsr4($prefix, $paths, $prepend = false)
 234      {
 235          if (!$prefix) {
 236              // Register directories for the root namespace.
 237              if ($prepend) {
 238                  $this->fallbackDirsPsr4 = array_merge(
 239                      (array) $paths,
 240                      $this->fallbackDirsPsr4
 241                  );
 242              } else {
 243                  $this->fallbackDirsPsr4 = array_merge(
 244                      $this->fallbackDirsPsr4,
 245                      (array) $paths
 246                  );
 247              }
 248          } elseif (!isset($this->prefixDirsPsr4[$prefix])) {
 249              // Register directories for a new namespace.
 250              $length = strlen($prefix);
 251              if ('\\' !== $prefix[$length - 1]) {
 252                  throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
 253              }
 254              $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
 255              $this->prefixDirsPsr4[$prefix] = (array) $paths;
 256          } elseif ($prepend) {
 257              // Prepend directories for an already registered namespace.
 258              $this->prefixDirsPsr4[$prefix] = array_merge(
 259                  (array) $paths,
 260                  $this->prefixDirsPsr4[$prefix]
 261              );
 262          } else {
 263              // Append directories for an already registered namespace.
 264              $this->prefixDirsPsr4[$prefix] = array_merge(
 265                  $this->prefixDirsPsr4[$prefix],
 266                  (array) $paths
 267              );
 268          }
 269      }
 270  
 271      /**
 272       * Registers a set of PSR-0 directories for a given prefix,
 273       * replacing any others previously set for this prefix.
 274       *
 275       * @param string          $prefix The prefix
 276       * @param string[]|string $paths  The PSR-0 base directories
 277       *
 278       * @return void
 279       */
 280      public function set($prefix, $paths)
 281      {
 282          if (!$prefix) {
 283              $this->fallbackDirsPsr0 = (array) $paths;
 284          } else {
 285              $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
 286          }
 287      }
 288  
 289      /**
 290       * Registers a set of PSR-4 directories for a given namespace,
 291       * replacing any others previously set for this namespace.
 292       *
 293       * @param string          $prefix The prefix/namespace, with trailing '\\'
 294       * @param string[]|string $paths  The PSR-4 base directories
 295       *
 296       * @throws \InvalidArgumentException
 297       *
 298       * @return void
 299       */
 300      public function setPsr4($prefix, $paths)
 301      {
 302          if (!$prefix) {
 303              $this->fallbackDirsPsr4 = (array) $paths;
 304          } else {
 305              $length = strlen($prefix);
 306              if ('\\' !== $prefix[$length - 1]) {
 307                  throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
 308              }
 309              $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
 310              $this->prefixDirsPsr4[$prefix] = (array) $paths;
 311          }
 312      }
 313  
 314      /**
 315       * Turns on searching the include path for class files.
 316       *
 317       * @param bool $useIncludePath
 318       *
 319       * @return void
 320       */
 321      public function setUseIncludePath($useIncludePath)
 322      {
 323          $this->useIncludePath = $useIncludePath;
 324      }
 325  
 326      /**
 327       * Can be used to check if the autoloader uses the include path to check
 328       * for classes.
 329       *
 330       * @return bool
 331       */
 332      public function getUseIncludePath()
 333      {
 334          return $this->useIncludePath;
 335      }
 336  
 337      /**
 338       * Turns off searching the prefix and fallback directories for classes
 339       * that have not been registered with the class map.
 340       *
 341       * @param bool $classMapAuthoritative
 342       *
 343       * @return void
 344       */
 345      public function setClassMapAuthoritative($classMapAuthoritative)
 346      {
 347          $this->classMapAuthoritative = $classMapAuthoritative;
 348      }
 349  
 350      /**
 351       * Should class lookup fail if not found in the current class map?
 352       *
 353       * @return bool
 354       */
 355      public function isClassMapAuthoritative()
 356      {
 357          return $this->classMapAuthoritative;
 358      }
 359  
 360      /**
 361       * APCu prefix to use to cache found/not-found classes, if the extension is enabled.
 362       *
 363       * @param string|null $apcuPrefix
 364       *
 365       * @return void
 366       */
 367      public function setApcuPrefix($apcuPrefix)
 368      {
 369          $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
 370      }
 371  
 372      /**
 373       * The APCu prefix in use, or null if APCu caching is not enabled.
 374       *
 375       * @return string|null
 376       */
 377      public function getApcuPrefix()
 378      {
 379          return $this->apcuPrefix;
 380      }
 381  
 382      /**
 383       * Registers this instance as an autoloader.
 384       *
 385       * @param bool $prepend Whether to prepend the autoloader or not
 386       *
 387       * @return void
 388       */
 389      public function register($prepend = false)
 390      {
 391          spl_autoload_register(array($this, 'loadClass'), true, $prepend);
 392  
 393          if (null === $this->vendorDir) {
 394              return;
 395          }
 396  
 397          if ($prepend) {
 398              self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
 399          } else {
 400              unset(self::$registeredLoaders[$this->vendorDir]);
 401              self::$registeredLoaders[$this->vendorDir] = $this;
 402          }
 403      }
 404  
 405      /**
 406       * Unregisters this instance as an autoloader.
 407       *
 408       * @return void
 409       */
 410      public function unregister()
 411      {
 412          spl_autoload_unregister(array($this, 'loadClass'));
 413  
 414          if (null !== $this->vendorDir) {
 415              unset(self::$registeredLoaders[$this->vendorDir]);
 416          }
 417      }
 418  
 419      /**
 420       * Loads the given class or interface.
 421       *
 422       * @param  string    $class The name of the class
 423       * @return true|null True if loaded, null otherwise
 424       */
 425      public function loadClass($class)
 426      {
 427          if ($file = $this->findFile($class)) {
 428              includeFile($file);
 429  
 430              return true;
 431          }
 432  
 433          return null;
 434      }
 435  
 436      /**
 437       * Finds the path to the file where the class is defined.
 438       *
 439       * @param string $class The name of the class
 440       *
 441       * @return string|false The path if found, false otherwise
 442       */
 443      public function findFile($class)
 444      {
 445          // class map lookup
 446          if (isset($this->classMap[$class])) {
 447              return $this->classMap[$class];
 448          }
 449          if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
 450              return false;
 451          }
 452          if (null !== $this->apcuPrefix) {
 453              $file = apcu_fetch($this->apcuPrefix.$class, $hit);
 454              if ($hit) {
 455                  return $file;
 456              }
 457          }
 458  
 459          $file = $this->findFileWithExtension($class, '.php');
 460  
 461          // Search for Hack files if we are running on HHVM
 462          if (false === $file && defined('HHVM_VERSION')) {
 463              $file = $this->findFileWithExtension($class, '.hh');
 464          }
 465  
 466          if (null !== $this->apcuPrefix) {
 467              apcu_add($this->apcuPrefix.$class, $file);
 468          }
 469  
 470          if (false === $file) {
 471              // Remember that this class does not exist.
 472              $this->missingClasses[$class] = true;
 473          }
 474  
 475          return $file;
 476      }
 477  
 478      /**
 479       * Returns the currently registered loaders indexed by their corresponding vendor directories.
 480       *
 481       * @return self[]
 482       */
 483      public static function getRegisteredLoaders()
 484      {
 485          return self::$registeredLoaders;
 486      }
 487  
 488      /**
 489       * @param  string       $class
 490       * @param  string       $ext
 491       * @return string|false
 492       */
 493      private function findFileWithExtension($class, $ext)
 494      {
 495          // PSR-4 lookup
 496          $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
 497  
 498          $first = $class[0];
 499          if (isset($this->prefixLengthsPsr4[$first])) {
 500              $subPath = $class;
 501              while (false !== $lastPos = strrpos($subPath, '\\')) {
 502                  $subPath = substr($subPath, 0, $lastPos);
 503                  $search = $subPath . '\\';
 504                  if (isset($this->prefixDirsPsr4[$search])) {
 505                      $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
 506                      foreach ($this->prefixDirsPsr4[$search] as $dir) {
 507                          if (file_exists($file = $dir . $pathEnd)) {
 508                              return $file;
 509                          }
 510                      }
 511                  }
 512              }
 513          }
 514  
 515          // PSR-4 fallback dirs
 516          foreach ($this->fallbackDirsPsr4 as $dir) {
 517              if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
 518                  return $file;
 519              }
 520          }
 521  
 522          // PSR-0 lookup
 523          if (false !== $pos = strrpos($class, '\\')) {
 524              // namespaced class name
 525              $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
 526                  . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
 527          } else {
 528              // PEAR-like class name
 529              $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
 530          }
 531  
 532          if (isset($this->prefixesPsr0[$first])) {
 533              foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
 534                  if (0 === strpos($class, $prefix)) {
 535                      foreach ($dirs as $dir) {
 536                          if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
 537                              return $file;
 538                          }
 539                      }
 540                  }
 541              }
 542          }
 543  
 544          // PSR-0 fallback dirs
 545          foreach ($this->fallbackDirsPsr0 as $dir) {
 546              if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
 547                  return $file;
 548              }
 549          }
 550  
 551          // PSR-0 include paths.
 552          if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
 553              return $file;
 554          }
 555  
 556          return false;
 557      }
 558  }
 559  
 560  /**
 561   * Scope isolated include.
 562   *
 563   * Prevents access to $this/self from included files.
 564   *
 565   * @param  string $file
 566   * @return void
 567   * @private
 568   */
 569  function includeFile($file)
 570  {
 571      include $file;
 572  }