vendor/symfony/debug/DebugClassLoader.php line 195

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($file0$i);
  43.             $file substr($file$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($classfalse) || \interface_exists($classfalse) || \trait_exists($classfalse);
  149.         if (null !== $file && $class && '\\' === $class[0]) {
  150.             $class substr($class1);
  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 && === \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($messageE_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 ($len + (\strpos($class'\\') ?: \strpos($class'_'))) {
  188.             $len 0;
  189.             $ns '';
  190.         } else {
  191.             $ns = \str_replace('_''\\', \substr($class0$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".'$parentself::$final[$parent], $class);
  210.             }
  211.         }
  212.         // Detect if the parent is annotated
  213.         foreach ($parentAndOwnInterfaces + \class_uses($classfalse) 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($classfalse) ? 'class' : (interface_exists($classfalse) ? 'interface' 'trait');
  219.                 $verb class_exists($usefalse) || interface_exists($classfalse) ? 'extends' : (interface_exists($usefalse) ? 'implements' 'uses');
  220.                 $deprecations[] = sprintf('The "%s" %s %s "%s" that is deprecated%s.'$class$type$verb$useself::$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".'$useclass_exists($usefalse) ? 'class' : (interface_exists($usefalse) ? '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_SEPARATORstr_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($tail0$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 (=== self::$caseCheck) {
  284.             $real $this->darwinRealpath($real);
  285.         }
  286.         if (=== substr_compare($real$tail, -$tailLen$tailLentrue)
  287.             && !== substr_compare($real$tail, -$tailLen$tailLenfalse)
  288.         ) {
  289.             return [substr($tail, -$tailLen 1), substr($real, -$tailLen 1), substr($real0, -$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 strrpos($real'/');
  298.         $file substr($real$i);
  299.         $real substr($real0$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($k0, ++$i);
  320.                     $dir substr($dir0$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($real2) 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($classfalse);
  355.         if ($parent) {
  356.             foreach (class_implements($parentfalse) 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. }