1<?php 2 3/* 4 * This file is part of the Symfony package. 5 * 6 * (c) Fabien Potencier <fabien@symfony.com> 7 * 8 * For the full copyright and license information, please view the LICENSE 9 * file that was distributed with this source code. 10 */ 11 12namespace Symfony\Component\Routing\Loader; 13 14use Symfony\Component\Config\FileLocatorInterface; 15use Symfony\Component\Config\Loader\FileLoader; 16use Symfony\Component\Config\Resource\FileResource; 17use Symfony\Component\Routing\RouteCollection; 18 19/** 20 * AnnotationFileLoader loads routing information from annotations set 21 * on a PHP class and its methods. 22 * 23 * @author Fabien Potencier <fabien@symfony.com> 24 */ 25class AnnotationFileLoader extends FileLoader 26{ 27 protected $loader; 28 29 /** 30 * @throws \RuntimeException 31 */ 32 public function __construct(FileLocatorInterface $locator, AnnotationClassLoader $loader) 33 { 34 if (!\function_exists('token_get_all')) { 35 throw new \RuntimeException('The Tokenizer extension is required for the routing annotation loaders.'); 36 } 37 38 parent::__construct($locator); 39 40 $this->loader = $loader; 41 } 42 43 /** 44 * Loads from annotations from a file. 45 * 46 * @param string $file A PHP file path 47 * @param string|null $type The resource type 48 * 49 * @return RouteCollection|null A RouteCollection instance 50 * 51 * @throws \InvalidArgumentException When the file does not exist or its routes cannot be parsed 52 */ 53 public function load($file, $type = null) 54 { 55 $path = $this->locator->locate($file); 56 57 $collection = new RouteCollection(); 58 if ($class = $this->findClass($path)) { 59 $refl = new \ReflectionClass($class); 60 if ($refl->isAbstract()) { 61 return null; 62 } 63 64 $collection->addResource(new FileResource($path)); 65 $collection->addCollection($this->loader->load($class, $type)); 66 } 67 if (\PHP_VERSION_ID >= 70000) { 68 // PHP 7 memory manager will not release after token_get_all(), see https://bugs.php.net/70098 69 gc_mem_caches(); 70 } 71 72 return $collection; 73 } 74 75 /** 76 * {@inheritdoc} 77 */ 78 public function supports($resource, $type = null) 79 { 80 return \is_string($resource) && 'php' === pathinfo($resource, \PATHINFO_EXTENSION) && (!$type || 'annotation' === $type); 81 } 82 83 /** 84 * Returns the full class name for the first class in the file. 85 * 86 * @param string $file A PHP file path 87 * 88 * @return string|false Full class name if found, false otherwise 89 */ 90 protected function findClass($file) 91 { 92 $class = false; 93 $namespace = false; 94 $tokens = token_get_all(file_get_contents($file)); 95 96 if (1 === \count($tokens) && \T_INLINE_HTML === $tokens[0][0]) { 97 throw new \InvalidArgumentException(sprintf('The file "%s" does not contain PHP code. Did you forgot to add the "<?php" start tag at the beginning of the file?', $file)); 98 } 99 100 $nsTokens = [\T_NS_SEPARATOR => true, \T_STRING => true]; 101 if (\defined('T_NAME_QUALIFIED')) { 102 $nsTokens[T_NAME_QUALIFIED] = true; 103 } 104 105 for ($i = 0; isset($tokens[$i]); ++$i) { 106 $token = $tokens[$i]; 107 108 if (!isset($token[1])) { 109 continue; 110 } 111 112 if (true === $class && \T_STRING === $token[0]) { 113 return $namespace.'\\'.$token[1]; 114 } 115 116 if (true === $namespace && isset($nsTokens[$token[0]])) { 117 $namespace = $token[1]; 118 while (isset($tokens[++$i][1], $nsTokens[$tokens[$i][0]])) { 119 $namespace .= $tokens[$i][1]; 120 } 121 $token = $tokens[$i]; 122 } 123 124 if (\T_CLASS === $token[0]) { 125 // Skip usage of ::class constant and anonymous classes 126 $skipClassToken = false; 127 for ($j = $i - 1; $j > 0; --$j) { 128 if (!isset($tokens[$j][1])) { 129 break; 130 } 131 132 if (\T_DOUBLE_COLON === $tokens[$j][0] || \T_NEW === $tokens[$j][0]) { 133 $skipClassToken = true; 134 break; 135 } elseif (!\in_array($tokens[$j][0], [\T_WHITESPACE, \T_DOC_COMMENT, \T_COMMENT])) { 136 break; 137 } 138 } 139 140 if (!$skipClassToken) { 141 $class = true; 142 } 143 } 144 145 if (\T_NAMESPACE === $token[0]) { 146 $namespace = true; 147 } 148 } 149 150 return false; 151 } 152} 153