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 \LogicException('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 68 gc_mem_caches(); 69 70 return $collection; 71 } 72 73 /** 74 * {@inheritdoc} 75 */ 76 public function supports($resource, $type = null) 77 { 78 return \is_string($resource) && 'php' === pathinfo($resource, PATHINFO_EXTENSION) && (!$type || 'annotation' === $type); 79 } 80 81 /** 82 * Returns the full class name for the first class in the file. 83 * 84 * @param string $file A PHP file path 85 * 86 * @return string|false Full class name if found, false otherwise 87 */ 88 protected function findClass($file) 89 { 90 $class = false; 91 $namespace = false; 92 $tokens = token_get_all(file_get_contents($file)); 93 94 if (1 === \count($tokens) && T_INLINE_HTML === $tokens[0][0]) { 95 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)); 96 } 97 98 for ($i = 0; isset($tokens[$i]); ++$i) { 99 $token = $tokens[$i]; 100 101 if (!isset($token[1])) { 102 continue; 103 } 104 105 if (true === $class && T_STRING === $token[0]) { 106 return $namespace.'\\'.$token[1]; 107 } 108 109 if (true === $namespace && T_STRING === $token[0]) { 110 $namespace = $token[1]; 111 while (isset($tokens[++$i][1]) && \in_array($tokens[$i][0], [T_NS_SEPARATOR, T_STRING])) { 112 $namespace .= $tokens[$i][1]; 113 } 114 $token = $tokens[$i]; 115 } 116 117 if (T_CLASS === $token[0]) { 118 // Skip usage of ::class constant and anonymous classes 119 $skipClassToken = false; 120 for ($j = $i - 1; $j > 0; --$j) { 121 if (!isset($tokens[$j][1])) { 122 break; 123 } 124 125 if (T_DOUBLE_COLON === $tokens[$j][0] || T_NEW === $tokens[$j][0]) { 126 $skipClassToken = true; 127 break; 128 } elseif (!\in_array($tokens[$j][0], [T_WHITESPACE, T_DOC_COMMENT, T_COMMENT])) { 129 break; 130 } 131 } 132 133 if (!$skipClassToken) { 134 $class = true; 135 } 136 } 137 138 if (T_NAMESPACE === $token[0]) { 139 $namespace = true; 140 } 141 } 142 143 return false; 144 } 145} 146