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