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;
13
14use Symfony\Component\Config\Exception\FileLoaderLoadException;
15use Symfony\Component\Config\Loader\LoaderInterface;
16use Symfony\Component\Config\Resource\ResourceInterface;
17
18/**
19 * Helps add and import routes into a RouteCollection.
20 *
21 * @author Ryan Weaver <ryan@knpuniversity.com>
22 */
23class RouteCollectionBuilder
24{
25    /**
26     * @var Route[]|RouteCollectionBuilder[]
27     */
28    private $routes = array();
29
30    private $loader;
31    private $defaults = array();
32    private $prefix;
33    private $host;
34    private $condition;
35    private $requirements = array();
36    private $options = array();
37    private $schemes;
38    private $methods;
39    private $resources = array();
40
41    /**
42     * @param LoaderInterface $loader
43     */
44    public function __construct(LoaderInterface $loader = null)
45    {
46        $this->loader = $loader;
47    }
48
49    /**
50     * Import an external routing resource and returns the RouteCollectionBuilder.
51     *
52     *  $routes->import('blog.yml', '/blog');
53     *
54     * @param mixed       $resource
55     * @param string|null $prefix
56     * @param string      $type
57     *
58     * @return RouteCollectionBuilder
59     *
60     * @throws FileLoaderLoadException
61     */
62    public function import($resource, $prefix = '/', $type = null)
63    {
64        /** @var RouteCollection $collection */
65        $collection = $this->load($resource, $type);
66
67        // create a builder from the RouteCollection
68        $builder = $this->createBuilder();
69        foreach ($collection->all() as $name => $route) {
70            $builder->addRoute($route, $name);
71        }
72
73        foreach ($collection->getResources() as $resource) {
74            $builder->addResource($resource);
75        }
76
77        // mount into this builder
78        $this->mount($prefix, $builder);
79
80        return $builder;
81    }
82
83    /**
84     * Adds a route and returns it for future modification.
85     *
86     * @param string      $path       The route path
87     * @param string      $controller The route's controller
88     * @param string|null $name       The name to give this route
89     *
90     * @return Route
91     */
92    public function add($path, $controller, $name = null)
93    {
94        $route = new Route($path);
95        $route->setDefault('_controller', $controller);
96        $this->addRoute($route, $name);
97
98        return $route;
99    }
100
101    /**
102     * Returns a RouteCollectionBuilder that can be configured and then added with mount().
103     *
104     * @return RouteCollectionBuilder
105     */
106    public function createBuilder()
107    {
108        return new self($this->loader);
109    }
110
111    /**
112     * Add a RouteCollectionBuilder.
113     *
114     * @param string                 $prefix
115     * @param RouteCollectionBuilder $builder
116     */
117    public function mount($prefix, RouteCollectionBuilder $builder)
118    {
119        $builder->prefix = trim(trim($prefix), '/');
120        $this->routes[] = $builder;
121    }
122
123    /**
124     * Adds a Route object to the builder.
125     *
126     * @param Route       $route
127     * @param string|null $name
128     *
129     * @return $this
130     */
131    public function addRoute(Route $route, $name = null)
132    {
133        if (null === $name) {
134            // used as a flag to know which routes will need a name later
135            $name = '_unnamed_route_'.spl_object_hash($route);
136        }
137
138        $this->routes[$name] = $route;
139
140        return $this;
141    }
142
143    /**
144     * Sets the host on all embedded routes (unless already set).
145     *
146     * @param string $pattern
147     *
148     * @return $this
149     */
150    public function setHost($pattern)
151    {
152        $this->host = $pattern;
153
154        return $this;
155    }
156
157    /**
158     * Sets a condition on all embedded routes (unless already set).
159     *
160     * @param string $condition
161     *
162     * @return $this
163     */
164    public function setCondition($condition)
165    {
166        $this->condition = $condition;
167
168        return $this;
169    }
170
171    /**
172     * Sets a default value that will be added to all embedded routes (unless that
173     * default value is already set).
174     *
175     * @param string $key
176     * @param mixed  $value
177     *
178     * @return $this
179     */
180    public function setDefault($key, $value)
181    {
182        $this->defaults[$key] = $value;
183
184        return $this;
185    }
186
187    /**
188     * Sets a requirement that will be added to all embedded routes (unless that
189     * requirement is already set).
190     *
191     * @param string $key
192     * @param mixed  $regex
193     *
194     * @return $this
195     */
196    public function setRequirement($key, $regex)
197    {
198        $this->requirements[$key] = $regex;
199
200        return $this;
201    }
202
203    /**
204     * Sets an opiton that will be added to all embedded routes (unless that
205     * option is already set).
206     *
207     * @param string $key
208     * @param mixed  $value
209     *
210     * @return $this
211     */
212    public function setOption($key, $value)
213    {
214        $this->options[$key] = $value;
215
216        return $this;
217    }
218
219    /**
220     * Sets the schemes on all embedded routes (unless already set).
221     *
222     * @param array|string $schemes
223     *
224     * @return $this
225     */
226    public function setSchemes($schemes)
227    {
228        $this->schemes = $schemes;
229
230        return $this;
231    }
232
233    /**
234     * Sets the methods on all embedded routes (unless already set).
235     *
236     * @param array|string $methods
237     *
238     * @return $this
239     */
240    public function setMethods($methods)
241    {
242        $this->methods = $methods;
243
244        return $this;
245    }
246
247    /**
248     * Adds a resource for this collection.
249     *
250     * @param ResourceInterface $resource
251     *
252     * @return $this
253     */
254    private function addResource(ResourceInterface $resource)
255    {
256        $this->resources[] = $resource;
257
258        return $this;
259    }
260
261    /**
262     * Creates the final RouteCollection and returns it.
263     *
264     * @return RouteCollection
265     */
266    public function build()
267    {
268        $routeCollection = new RouteCollection();
269
270        foreach ($this->routes as $name => $route) {
271            if ($route instanceof Route) {
272                $route->setDefaults(array_merge($this->defaults, $route->getDefaults()));
273                $route->setOptions(array_merge($this->options, $route->getOptions()));
274
275                foreach ($this->requirements as $key => $val) {
276                    if (!$route->hasRequirement($key)) {
277                        $route->setRequirement($key, $val);
278                    }
279                }
280
281                if (null !== $this->prefix) {
282                    $route->setPath('/'.$this->prefix.$route->getPath());
283                }
284
285                if (!$route->getHost()) {
286                    $route->setHost($this->host);
287                }
288
289                if (!$route->getCondition()) {
290                    $route->setCondition($this->condition);
291                }
292
293                if (!$route->getSchemes()) {
294                    $route->setSchemes($this->schemes);
295                }
296
297                if (!$route->getMethods()) {
298                    $route->setMethods($this->methods);
299                }
300
301                // auto-generate the route name if it's been marked
302                if ('_unnamed_route_' === substr($name, 0, 15)) {
303                    $name = $this->generateRouteName($route);
304                }
305
306                $routeCollection->add($name, $route);
307            } else {
308                /* @var self $route */
309                $subCollection = $route->build();
310                $subCollection->addPrefix($this->prefix);
311
312                $routeCollection->addCollection($subCollection);
313            }
314
315            foreach ($this->resources as $resource) {
316                $routeCollection->addResource($resource);
317            }
318        }
319
320        return $routeCollection;
321    }
322
323    /**
324     * Generates a route name based on details of this route.
325     *
326     * @return string
327     */
328    private function generateRouteName(Route $route)
329    {
330        $methods = implode('_', $route->getMethods()).'_';
331
332        $routeName = $methods.$route->getPath();
333        $routeName = str_replace(array('/', ':', '|', '-'), '_', $routeName);
334        $routeName = preg_replace('/[^a-z0-9A-Z_.]+/', '', $routeName);
335
336        // Collapse consecutive underscores down into a single underscore.
337        $routeName = preg_replace('/_+/', '_', $routeName);
338
339        return $routeName;
340    }
341
342    /**
343     * Finds a loader able to load an imported resource and loads it.
344     *
345     * @param mixed       $resource A resource
346     * @param string|null $type     The resource type or null if unknown
347     *
348     * @return RouteCollection
349     *
350     * @throws FileLoaderLoadException If no loader is found
351     */
352    private function load($resource, $type = null)
353    {
354        if (null === $this->loader) {
355            throw new \BadMethodCallException('Cannot import other routing resources: you must pass a LoaderInterface when constructing RouteCollectionBuilder.');
356        }
357
358        if ($this->loader->supports($resource, $type)) {
359            return $this->loader->load($resource, $type);
360        }
361
362        if (null === $resolver = $this->loader->getResolver()) {
363            throw new FileLoaderLoadException($resource);
364        }
365
366        if (false === $loader = $resolver->resolve($resource, $type)) {
367            throw new FileLoaderLoadException($resource);
368        }
369
370        return $loader->load($resource, $type);
371    }
372}
373