Subversion Repositories oidplus

Rev

Rev 1417 | Rev 1424 | Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | RSS feed

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