Skip to content

Commit 54e3d71

Browse files
committed
feature #15778 Fluid interface for building routes in PHP (weaverryan)
This PR was squashed before being merged into the 2.8 branch (closes #15778). Discussion ---------- Fluid interface for building routes in PHP | Q | A | ------------- | --- | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | n/a | License | MIT | Doc PR | not yet... This - along with #15742 - attempts to making adding routes in PHP (via an actual class+method) not only possible, but also useful. The two classes - `Route` and `RouteCollectionBuilder` are based off of Silex's `Controller` and `ControllerCollection`. The `RouteCollectionBuilder` is basically a `RouteCollection` that's able to import other resources. Here are the goals: A) Import routes easily ```php $routes->import('routing.yml'); ``` B) Fluid addition of routes into the collection ```php $routes->add('/admin', 'AppBundle:Admin:index', 'admin_index') ->setMethods(['GET']); ``` C) Ability to create routes with auto-generating names D) Ability to add a "sub-collection" (kind of like an import, without pointing to another file). Included is the ability to set the controller class: ```php $blogRoutes = $routes->createBuilder('/blog') ->setControllerClass('AppBundle\Controller\BlogController'); $blogRoutes->add('/', 'indexAction'); $blogRoutes->add('/{id}', 'editAction'); $routes->addBuilder($blogRoutes); ``` E) The collection options can be set before or after the routes. With `RouteCollection`, if you set something - e.g. a prefix or a default - and THEN add more routes, those options are not passed to those routes. This is by design, but not ideal for building routes (e.g. in the previous code example, the controllerClass would not be applied using the opposite logic, since it's set before adding the routes). Thanks! Commits ------- 15ba2e8 Fluid interface for building routes in PHP
2 parents 238419f + 15ba2e8 commit 54e3d71

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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy