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\LoaderLoadException; 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 = []; 29 30 private $loader; 31 private $defaults = []; 32 private $prefix; 33 private $host; 34 private $condition; 35 private $requirements = []; 36 private $options = []; 37 private $schemes; 38 private $methods; 39 private $resources = []; 40 41 public function __construct(LoaderInterface $loader = null) 42 { 43 $this->loader = $loader; 44 } 45 46 /** 47 * Import an external routing resource and returns the RouteCollectionBuilder. 48 * 49 * $routes->import('blog.yml', '/blog'); 50 * 51 * @param mixed $resource 52 * @param string|null $prefix 53 * @param string $type 54 * 55 * @return self 56 * 57 * @throws LoaderLoadException 58 */ 59 public function import($resource, $prefix = '/', $type = null) 60 { 61 /** @var RouteCollection[] $collections */ 62 $collections = $this->load($resource, $type); 63 64 // create a builder from the RouteCollection 65 $builder = $this->createBuilder(); 66 67 foreach ($collections as $collection) { 68 if (null === $collection) { 69 continue; 70 } 71 72 foreach ($collection->all() as $name => $route) { 73 $builder->addRoute($route, $name); 74 } 75 76 foreach ($collection->getResources() as $resource) { 77 $builder->addResource($resource); 78 } 79 } 80 81 // mount into this builder 82 $this->mount($prefix, $builder); 83 84 return $builder; 85 } 86 87 /** 88 * Adds a route and returns it for future modification. 89 * 90 * @param string $path The route path 91 * @param string $controller The route's controller 92 * @param string|null $name The name to give this route 93 * 94 * @return Route 95 */ 96 public function add($path, $controller, $name = null) 97 { 98 $route = new Route($path); 99 $route->setDefault('_controller', $controller); 100 $this->addRoute($route, $name); 101 102 return $route; 103 } 104 105 /** 106 * Returns a RouteCollectionBuilder that can be configured and then added with mount(). 107 * 108 * @return self 109 */ 110 public function createBuilder() 111 { 112 return new self($this->loader); 113 } 114 115 /** 116 * Add a RouteCollectionBuilder. 117 * 118 * @param string $prefix 119 */ 120 public function mount($prefix, self $builder) 121 { 122 $builder->prefix = trim(trim($prefix), '/'); 123 $this->routes[] = $builder; 124 } 125 126 /** 127 * Adds a Route object to the builder. 128 * 129 * @param string|null $name 130 * 131 * @return $this 132 */ 133 public function addRoute(Route $route, $name = null) 134 { 135 if (null === $name) { 136 // used as a flag to know which routes will need a name later 137 $name = '_unnamed_route_'.spl_object_hash($route); 138 } 139 140 $this->routes[$name] = $route; 141 142 return $this; 143 } 144 145 /** 146 * Sets the host on all embedded routes (unless already set). 147 * 148 * @param string $pattern 149 * 150 * @return $this 151 */ 152 public function setHost($pattern) 153 { 154 $this->host = $pattern; 155 156 return $this; 157 } 158 159 /** 160 * Sets a condition on all embedded routes (unless already set). 161 * 162 * @param string $condition 163 * 164 * @return $this 165 */ 166 public function setCondition($condition) 167 { 168 $this->condition = $condition; 169 170 return $this; 171 } 172 173 /** 174 * Sets a default value that will be added to all embedded routes (unless that 175 * default value is already set). 176 * 177 * @param string $key 178 * @param mixed $value 179 * 180 * @return $this 181 */ 182 public function setDefault($key, $value) 183 { 184 $this->defaults[$key] = $value; 185 186 return $this; 187 } 188 189 /** 190 * Sets a requirement that will be added to all embedded routes (unless that 191 * requirement is already set). 192 * 193 * @param string $key 194 * @param mixed $regex 195 * 196 * @return $this 197 */ 198 public function setRequirement($key, $regex) 199 { 200 $this->requirements[$key] = $regex; 201 202 return $this; 203 } 204 205 /** 206 * Sets an option that will be added to all embedded routes (unless that 207 * option is already set). 208 * 209 * @param string $key 210 * @param mixed $value 211 * 212 * @return $this 213 */ 214 public function setOption($key, $value) 215 { 216 $this->options[$key] = $value; 217 218 return $this; 219 } 220 221 /** 222 * Sets the schemes on all embedded routes (unless already set). 223 * 224 * @param array|string $schemes 225 * 226 * @return $this 227 */ 228 public function setSchemes($schemes) 229 { 230 $this->schemes = $schemes; 231 232 return $this; 233 } 234 235 /** 236 * Sets the methods on all embedded routes (unless already set). 237 * 238 * @param array|string $methods 239 * 240 * @return $this 241 */ 242 public function setMethods($methods) 243 { 244 $this->methods = $methods; 245 246 return $this; 247 } 248 249 /** 250 * Adds a resource for this collection. 251 * 252 * @return $this 253 */ 254 private function addResource(ResourceInterface $resource): self 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 if (null !== $this->prefix) { 311 $subCollection->addPrefix($this->prefix); 312 } 313 314 $routeCollection->addCollection($subCollection); 315 } 316 } 317 318 foreach ($this->resources as $resource) { 319 $routeCollection->addResource($resource); 320 } 321 322 return $routeCollection; 323 } 324 325 /** 326 * Generates a route name based on details of this route. 327 */ 328 private function generateRouteName(Route $route): string 329 { 330 $methods = implode('_', $route->getMethods()).'_'; 331 332 $routeName = $methods.$route->getPath(); 333 $routeName = str_replace(['/', ':', '|', '-'], '_', $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 LoaderLoadException If no loader is found 351 */ 352 private function load($resource, string $type = null): array 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 $collections = $this->loader->load($resource, $type); 360 361 return \is_array($collections) ? $collections : [$collections]; 362 } 363 364 if (null === $resolver = $this->loader->getResolver()) { 365 throw new LoaderLoadException($resource, null, null, null, $type); 366 } 367 368 if (false === $loader = $resolver->resolve($resource, $type)) { 369 throw new LoaderLoadException($resource, null, null, null, $type); 370 } 371 372 $collections = $loader->load($resource, $type); 373 374 return \is_array($collections) ? $collections : [$collections]; 375 } 376} 377