vendor/symfony/debug/DebugClassLoader.php line 165

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of the Symfony package.
  4.  *
  5.  * (c) Fabien Potencier <fabien@symfony.com>
  6.  *
  7.  * For the full copyright and license information, please view the LICENSE
  8.  * file that was distributed with this source code.
  9.  */
  10. namespace Symfony\Component\Debug;
  11. /**
  12.  * Autoloader checking if the class is really defined in the file found.
  13.  *
  14.  * The ClassLoader will wrap all registered autoloaders
  15.  * and will throw an exception if a file is found but does
  16.  * not declare the class.
  17.  *
  18.  * @author Fabien Potencier <fabien@symfony.com>
  19.  * @author Christophe Coevoet <stof@notk.org>
  20.  * @author Nicolas Grekas <p@tchwork.com>
  21.  */
  22. class DebugClassLoader
  23. {
  24.     private $classLoader;
  25.     private $isFinder;
  26.     private $loaded = [];
  27.     private static $caseCheck;
  28.     private static $checkedClasses = [];
  29.     private static $final = [];
  30.     private static $finalMethods = [];
  31.     private static $deprecated = [];
  32.     private static $internal = [];
  33.     private static $internalMethods = [];
  34.     private static $darwinCache = ['/' => ['/', []]];
  35.     public function __construct(callable $classLoader)
  36.     {
  37.         $this->classLoader = $classLoader;
  38.         $this->isFinder = \is_array($classLoader) && method_exists($classLoader[0], 'findFile');
  39.         if (!isset(self::$caseCheck)) {
  40.             $file = file_exists(__FILE__) ? __FILE__ : rtrim(realpath('.'), \DIRECTORY_SEPARATOR);
  41.             $i = strrpos($file, \DIRECTORY_SEPARATOR);
  42.             $dir = substr($file, 0, 1 + $i);
  43.             $file = substr($file, 1 + $i);
  44.             $test = strtoupper($file) === $file ? strtolower($file) : strtoupper($file);
  45.             $test = realpath($dir.$test);
  46.             if (false === $test || false === $i) {
  47.                 // filesystem is case sensitive
  48.                 self::$caseCheck = 0;
  49.             } elseif (substr($test, -\strlen($file)) === $file) {
  50.                 // filesystem is case insensitive and realpath() normalizes the case of characters
  51.                 self::$caseCheck = 1;
  52.             } elseif (false !== stripos(PHP_OS, 'darwin')) {
  53.                 // on MacOSX, HFS+ is case insensitive but realpath() doesn't normalize the case of characters
  54.                 self::$caseCheck = 2;
  55.             } else {
  56.                 // filesystem case checks failed, fallback to disabling them
  57.                 self::$caseCheck = 0;
  58.             }
  59.         }
  60.     }
  61.     /**
  62.      * Gets the wrapped class loader.
  63.      *
  64.      * @return callable The wrapped class loader
  65.      */
  66.     public function getClassLoader()
  67.     {
  68.         return $this->classLoader;
  69.     }
  70.     /**
  71.      * Wraps all autoloaders.
  72.      */
  73.     public static function enable()
  74.     {
  75.         // Ensures we don't hit https://bugs.php.net/42098
  76.         class_exists('Symfony\Component\Debug\ErrorHandler');
  77.         class_exists('Psr\Log\LogLevel');
  78.         if (!\is_array($functions = spl_autoload_functions())) {
  79.             return;
  80.         }
  81.         foreach ($functions as $function) {
  82.             spl_autoload_unregister($function);
  83.         }
  84.         foreach ($functions as $function) {
  85.             if (!\is_array($function) || !$function[0] instanceof self) {
  86.                 $function = [new static($function), 'loadClass'];
  87.             }
  88.             spl_autoload_register($function);
  89.         }
  90.     }
  91.     /**
  92.      * Disables the wrapping.
  93.      */
  94.     public static function disable()
  95.     {
  96.         if (!\is_array($functions = spl_autoload_functions())) {
  97.             return;
  98.         }
  99.         foreach ($functions as $function) {
  100.             spl_autoload_unregister($function);
  101.         }
  102.         foreach ($functions as $function) {
  103.             if (\is_array($function) && $function[0] instanceof self) {
  104.                 $function = $function[0]->getClassLoader();
  105.             }
  106.             spl_autoload_register($function);
  107.         }
  108.     }
  109.     /**
  110.      * @return string|null
  111.      */
  112.     public function findFile($class)
  113.     {
  114.         return $this->isFinder ? $this->classLoader[0]->findFile($class) ?: null : null;
  115.     }
  116.     /**
  117.      * Loads the given class or interface.
  118.      *
  119.      * @param string $class The name of the class
  120.      *
  121.      * @throws \RuntimeException
  122.      */
  123.     public function loadClass($class)
  124.     {
  125.         $e = error_reporting(error_reporting() | E_PARSE | E_ERROR | E_CORE_ERROR | E_COMPILE_ERROR);
  126.         try {
  127.             if ($this->isFinder && !isset($this->loaded[$class])) {
  128.                 $this->loaded[$class] = true;
  129.                 if (!$file = $this->classLoader[0]->findFile($class) ?: false) {
  130.                     // no-op
  131.                 } elseif (\function_exists('opcache_is_script_cached') && @opcache_is_script_cached($file)) {
  132.                     require $file;
  133.                     return;
  134.                 } else {
  135.                     require $file;
  136.                 }
  137.             } else {
  138.                 ($this->classLoader)($class);
  139.                 $file = false;
  140.             }
  141.         } finally {
  142.             error_reporting($e);
  143.         }
  144.         $this->checkClass($class, $file);
  145.     }
  146.     private function checkClass($class, $file = null)
  147.     {
  148.         $exists = null === $file || \class_exists($class, false) || \interface_exists($class, false) || \trait_exists($class, false);
  149.         if (null !== $file && $class && '\\' === $class[0]) {
  150.             $class = substr($class, 1);
  151.         }
  152.         if ($exists) {
  153.             if (isset(self::$checkedClasses[$class])) {
  154.                 return;
  155.             }
  156.             self::$checkedClasses[$class] = true;
  157.             $refl = new \ReflectionClass($class);
  158.             if (null === $file && $refl->isInternal()) {
  159.                 return;
  160.             }
  161.             $name = $refl->getName();
  162.             if ($name !== $class && 0 === \strcasecmp($name, $class)) {
  163.                 throw new \RuntimeException(sprintf('Case mismatch between loaded and declared class names: "%s" vs "%s".', $class, $name));
  164.             }
  165.             $deprecations = $this->checkAnnotations($refl, $name);
  166.             foreach ($deprecations as $message) {
  167.                 @trigger_error($message, E_USER_DEPRECATED);
  168.             }
  169.         }
  170.         if (!$file) {
  171.             return;
  172.         }
  173.         if (!$exists) {
  174.             if (false !== strpos($class, '/')) {
  175.                 throw new \RuntimeException(sprintf('Trying to autoload a class with an invalid name "%s". Be careful that the namespace separator is "\" in PHP, not "/".', $class));
  176.             }
  177.             throw new \RuntimeException(sprintf('The autoloader expected class "%s" to be defined in file "%s". The file was found but the class was not in it, the class name or namespace probably has a typo.', $class, $file));
  178.         }
  179.         if (self::$caseCheck && $message = $this->checkCase($refl, $file, $class)) {
  180.             throw new \RuntimeException(sprintf('Case mismatch between class and real file names: "%s" vs "%s" in "%s".', $message[0], $message[1], $message[2]));
  181.         }
  182.     }
  183.     public function checkAnnotations(\ReflectionClass $refl, $class)
  184.     {
  185.         $deprecations = [];
  186.         // Don't trigger deprecations for classes in the same vendor
  187.         if (2 > $len = 1 + (\strpos($class, '\\') ?: \strpos($class, '_'))) {
  188.             $len = 0;
  189.             $ns = '';
  190.         } else {
  191.             $ns = \str_replace('_', '\\', \substr($class, 0, $len));
  192.         }
  193.         // Detect annotations on the class
  194.         if (false !== $doc = $refl->getDocComment()) {
  195.             foreach (['final', 'deprecated', 'internal'] as $annotation) {
  196.                 if (false !== \strpos($doc, $annotation) && preg_match('#\n\s+\* @'.$annotation.'(?:( .+?)\.?)?\r?\n\s+\*(?: @|/$)#s', $doc, $notice)) {
  197.                     self::${$annotation}[$class] = isset($notice[1]) ? preg_replace('#\.?\r?\n( \*)? *(?= |\r?\n|$)#', '', $notice[1]) : '';
  198.                 }
  199.             }
  200.         }
  201.         $parent = \get_parent_class($class);
  202.         $parentAndOwnInterfaces = $this->getOwnInterfaces($class, $parent);
  203.         if ($parent) {
  204.             $parentAndOwnInterfaces[$parent] = $parent;
  205.             if (!isset(self::$checkedClasses[$parent])) {
  206.                 $this->checkClass($parent);
  207.             }
  208.             if (isset(self::$final[$parent])) {
  209.                 $deprecations[] = sprintf('The "%s" class is considered final%s. It may change without further notice as of its next major version. You should not extend it from "%s".', $parent, self::$final[$parent], $class);
  210.             }
  211.         }
  212.         // Detect if the parent is annotated
  213.         foreach ($parentAndOwnInterfaces + \class_uses($class, false) as $use) {
  214.             if (!isset(self::$checkedClasses[$use])) {
  215.                 $this->checkClass($use);
  216.             }
  217.             if (isset(self::$deprecated[$use]) && \strncmp($ns, \str_replace('_', '\\', $use), $len)) {
  218.                 $type = class_exists($class, false) ? 'class' : (interface_exists($class, false) ? 'interface' : 'trait');
  219.                 $verb = class_exists($use, false) || interface_exists($class, false) ? 'extends' : (interface_exists($use, false) ? 'implements' : 'uses');
  220.                 $deprecations[] = sprintf('The "%s" %s %s "%s" that is deprecated%s.', $class, $type, $verb, $use, self::$deprecated[$use]);
  221.             }
  222.             if (isset(self::$internal[$use]) && \strncmp($ns, \str_replace('_', '\\', $use), $len)) {
  223.                 $deprecations[] = sprintf('The "%s" %s is considered internal%s. It may change without further notice. You should not use it from "%s".', $use, class_exists($use, false) ? 'class' : (interface_exists($use, false) ? 'interface' : 'trait'), self::$internal[$use], $class);
  224.             }
  225.         }
  226.         if (\trait_exists($class)) {
  227.             return $deprecations;
  228.         }
  229.         // Inherit @final and @internal annotations for methods
  230.         self::$finalMethods[$class] = [];
  231.         self::$internalMethods[$class] = [];
  232.         foreach ($parentAndOwnInterfaces as $use) {
  233.             foreach (['finalMethods', 'internalMethods'] as $property) {
  234.                 if (isset(self::${$property}[$use])) {
  235.                     self::${$property}[$class] = self::${$property}[$class] ? self::${$property}[$use] + self::${$property}[$class] : self::${$property}[$use];
  236.                 }
  237.             }
  238.         }
  239.         foreach ($refl->getMethods(\ReflectionMethod::IS_PUBLIC | \ReflectionMethod::IS_PROTECTED) as $method) {
  240.             if ($method->class !== $class) {
  241.                 continue;
  242.             }
  243.             if ($parent && isset(self::$finalMethods[$parent][$method->name])) {
  244.                 list($declaringClass, $message) = self::$finalMethods[$parent][$method->name];
  245.                 $deprecations[] = sprintf('The "%s::%s()" method is considered final%s. It may change without further notice as of its next major version. You should not extend it from "%s".', $declaringClass, $method->name, $message, $class);
  246.             }
  247.             if (isset(self::$internalMethods[$class][$method->name])) {
  248.                 list($declaringClass, $message) = self::$internalMethods[$class][$method->name];
  249.                 if (\strncmp($ns, $declaringClass, $len)) {
  250.                     $deprecations[] = sprintf('The "%s::%s()" method is considered internal%s. It may change without further notice. You should not extend it from "%s".', $declaringClass, $method->name, $message, $class);
  251.                 }
  252.             }
  253.             // Detect method annotations
  254.             if (false === $doc = $method->getDocComment()) {
  255.                 continue;
  256.             }
  257.             foreach (['final', 'internal'] as $annotation) {
  258.                 if (false !== \strpos($doc, $annotation) && preg_match('#\n\s+\* @'.$annotation.'(?:( .+?)\.?)?\r?\n\s+\*(?: @|/$)#s', $doc, $notice)) {
  259.                     $message = isset($notice[1]) ? preg_replace('#\.?\r?\n( \*)? *(?= |\r?\n|$)#', '', $notice[1]) : '';
  260.                     self::${$annotation.'Methods'}[$class][$method->name] = [$class, $message];
  261.                 }
  262.             }
  263.         }
  264.         return $deprecations;
  265.     }
  266.     public function checkCase(\ReflectionClass $refl, $file, $class)
  267.     {
  268.         $real = explode('\\', $class.strrchr($file, '.'));
  269.         $tail = explode(\DIRECTORY_SEPARATOR, str_replace('/', \DIRECTORY_SEPARATOR, $file));
  270.         $i = \count($tail) - 1;
  271.         $j = \count($real) - 1;
  272.         while (isset($tail[$i], $real[$j]) && $tail[$i] === $real[$j]) {
  273.             --$i;
  274.             --$j;
  275.         }
  276.         array_splice($tail, 0, $i + 1);
  277.         if (!$tail) {
  278.             return;
  279.         }
  280.         $tail = \DIRECTORY_SEPARATOR.implode(\DIRECTORY_SEPARATOR, $tail);
  281.         $tailLen = \strlen($tail);
  282.         $real = $refl->getFileName();
  283.         if (2 === self::$caseCheck) {
  284.             $real = $this->darwinRealpath($real);
  285.         }
  286.         if (0 === substr_compare($real, $tail, -$tailLen, $tailLen, true)
  287.             && 0 !== substr_compare($real, $tail, -$tailLen, $tailLen, false)
  288.         ) {
  289.             return [substr($tail, -$tailLen + 1), substr($real, -$tailLen + 1), substr($real, 0, -$tailLen + 1)];
  290.         }
  291.     }
  292.     /**
  293.      * `realpath` on MacOSX doesn't normalize the case of characters.
  294.      */
  295.     private function darwinRealpath($real)
  296.     {
  297.         $i = 1 + strrpos($real, '/');
  298.         $file = substr($real, $i);
  299.         $real = substr($real, 0, $i);
  300.         if (isset(self::$darwinCache[$real])) {
  301.             $kDir = $real;
  302.         } else {
  303.             $kDir = strtolower($real);
  304.             if (isset(self::$darwinCache[$kDir])) {
  305.                 $real = self::$darwinCache[$kDir][0];
  306.             } else {
  307.                 $dir = getcwd();
  308.                 chdir($real);
  309.                 $real = getcwd().'/';
  310.                 chdir($dir);
  311.                 $dir = $real;
  312.                 $k = $kDir;
  313.                 $i = \strlen($dir) - 1;
  314.                 while (!isset(self::$darwinCache[$k])) {
  315.                     self::$darwinCache[$k] = [$dir, []];
  316.                     self::$darwinCache[$dir] = &self::$darwinCache[$k];
  317.                     while ('/' !== $dir[--$i]) {
  318.                     }
  319.                     $k = substr($k, 0, ++$i);
  320.                     $dir = substr($dir, 0, $i--);
  321.                 }
  322.             }
  323.         }
  324.         $dirFiles = self::$darwinCache[$kDir][1];
  325.         if (isset($dirFiles[$file])) {
  326.             return $real .= $dirFiles[$file];
  327.         }
  328.         $kFile = strtolower($file);
  329.         if (!isset($dirFiles[$kFile])) {
  330.             foreach (scandir($real, 2) as $f) {
  331.                 if ('.' !== $f[0]) {
  332.                     $dirFiles[$f] = $f;
  333.                     if ($f === $file) {
  334.                         $kFile = $k = $file;
  335.                     } elseif ($f !== $k = strtolower($f)) {
  336.                         $dirFiles[$k] = $f;
  337.                     }
  338.                 }
  339.             }
  340.             self::$darwinCache[$kDir][1] = $dirFiles;
  341.         }
  342.         return $real .= $dirFiles[$kFile];
  343.     }
  344.     /**
  345.      * `class_implements` includes interfaces from the parents so we have to manually exclude them.
  346.      *
  347.      * @param string       $class
  348.      * @param string|false $parent
  349.      *
  350.      * @return string[]
  351.      */
  352.     private function getOwnInterfaces($class, $parent)
  353.     {
  354.         $ownInterfaces = class_implements($class, false);
  355.         if ($parent) {
  356.             foreach (class_implements($parent, false) as $interface) {
  357.                 unset($ownInterfaces[$interface]);
  358.             }
  359.         }
  360.         foreach ($ownInterfaces as $interface) {
  361.             foreach (class_implements($interface) as $interface) {
  362.                 unset($ownInterfaces[$interface]);
  363.             }
  364.         }
  365.         return $ownInterfaces;
  366.     }
  367. }