Skip to content

Commit 15ba2e8

Browse files
weaverryanfabpot
authored andcommitted
Fluid interface for building routes in PHP
1 parent 00dffe7 commit 15ba2e8

File tree

2 files changed

+665
-0
lines changed

2 files changed

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

0 commit comments

Comments
 (0)