Subversion Repositories oidplus

Rev

Rev 1407 | Rev 1422 | 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 ?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. }
  573.