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