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