router.php 15.1 KB
Newer Older
1 2 3 4 5 6
<?php namespace Laravel\Routing;

use Closure;
use Laravel\Str;
use Laravel\Bundle;
use Laravel\Request;
7

8 9 10
class Router {

	/**
11 12 13 14 15 16 17 18 19 20 21 22 23 24
	 * The route names that have been matched.
	 *
	 * @var array
	 */
	public static $names = array();

	/**
	 * The actions that have been reverse routed.
	 *
	 * @var array
	 */
	public static $uses = array();

	/**
25
	 * All of the routes that have been registered.
Taylor Otwell committed
26
	 *
27
	 * @var array
Taylor Otwell committed
28
	 */
29 30 31 32 33
	public static $routes = array(
		'GET'    => array(),
		'POST'   => array(),
		'PUT'    => array(),
		'DELETE' => array(),
34
		'PATCH'  => array(),
35 36
		'HEAD'   => array(),
	);
Taylor Otwell committed
37 38

	/**
39 40 41 42
	 * All of the "fallback" routes that have been registered.
	 *
	 * @var array
	 */
43 44 45 46 47
	public static $fallback = array(
		'GET'    => array(),
		'POST'   => array(),
		'PUT'    => array(),
		'DELETE' => array(),
48
		'PATCH'  => array(),
49 50
		'HEAD'   => array(),
	);
51 52

	/**
53 54 55 56 57 58
	 * The current attributes being shared by routes.
	 */
	public static $group;

	/**
	 * The "handes" clause for the bundle currently being routed.
59
	 *
60
	 * @var string
61
	 */
62
	public static $bundle;
63 64

	/**
65
	 * The number of URI segments allowed as method arguments.
66
	 *
67
	 * @var int
68
	 */
69
	public static $segments = 5;
70 71

	/**
72 73 74 75
	 * The wildcard patterns supported by the router.
	 *
	 * @var array
	 */
76
	public static $patterns = array(
77
		'(:num)' => '([0-9]+)',
78
		'(:any)' => '([a-zA-Z0-9\.\-_%]+)',
79
		'(:all)' => '(.*)',
80 81 82 83 84 85 86
	);

	/**
	 * The optional wildcard patterns supported by the router.
	 *
	 * @var array
	 */
87
	public static $optional = array(
88
		'/(:num?)' => '(?:/([0-9]+)',
89
		'/(:any?)' => '(?:/([a-zA-Z0-9\.\-_%]+)',
90
		'/(:all?)' => '(?:/(.*)',
91 92 93
	);

	/**
94 95 96 97
	 * An array of HTTP request methods.
	 *
	 * @var array
	 */
98
	public static $methods = array('GET', 'POST', 'PUT', 'DELETE', 'HEAD');
99 100

	/**
101 102
	 * Register a HTTPS route with the router.
	 *
103
	 * @param  string        $method
104 105 106 107
	 * @param  string|array  $route
	 * @param  mixed         $action
	 * @return void
	 */
108
	public static function secure($method, $route, $action)
109
	{
110 111 112 113 114 115 116 117
		$action = static::action($action);

		$action['https'] = true;

		static::register($method, $route, $action);
	}

	/**
118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137
	 * Register many request URIs to a single action.
	 *
	 * <code>
	 *		// Register a group of URIs for an action
	 *		Router::share(array('GET', '/'), array('POST', '/'), 'home@index');
	 * </code>
	 *
	 * @param  array  $routes
	 * @param  mixed  $action
	 * @return void
	 */
	public static function share($routes, $action)
	{
		foreach ($routes as $route)
		{
			static::register($route[0], $route[1], $action);
		}
	}

	/**
138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153
	 * Register a group of routes that share attributes.
	 *
	 * @param  array    $attributes
	 * @param  Closure  $callback
	 * @return void
	 */
	public static function group($attributes, Closure $callback)
	{
		// Route groups allow the developer to specify attributes for a group
		// of routes. To register them, we'll set a static property on the
		// router so that the register method will see them.
		static::$group = $attributes;

		call_user_func($callback);

		// Once the routes have been registered, we want to set the group to
Taylor Otwell committed
154
		// null so the attributes will not be given to any of the routes
155 156
		// that are added after the group is declared.
		static::$group = null;
157 158 159
	}

	/**
160
	 * Register a route with the router.
161
	 *
162 163
	 * <code>
	 *		// Register a route with the router
Pavel committed
164
	 *		Router::register('GET', '/', function() {return 'Home!';});
165 166
	 *
	 *		// Register a route that handles multiple URIs with the router
167
	 *		Router::register(array('GET', '/', 'GET /home'), function() {return 'Home!';});
168 169
	 * </code>
	 *
170
	 * @param  string        $method
171
	 * @param  string|array  $route
172
	 * @param  mixed         $action
173
	 * @return void
174
	 */
175
	public static function register($method, $route, $action)
176
	{
177 178
		if (is_string($route)) $route = explode(', ', $route);

179 180
		// If the developer is registering multiple request methods to handle
		// the URI, we'll spin through each method and register the route
Taylor Otwell committed
181
		// for each of them along with each URI and action.
182 183 184 185 186 187 188 189 190 191
		if (is_array($method))
		{
			foreach ($method as $http)
			{
				static::register($http, $route, $action);
			}

			return;
		}

192 193
		foreach ((array) $route as $uri)
		{
194 195 196
			// If the URI begins with a splat, we'll call the universal method, which
			// will register a route for each of the request methods supported by
			// the router. This is just a notational short-cut.
197
			if ($method == '*')
198
			{
199 200 201 202
				foreach (static::$methods as $method)
				{
					static::register($method, $route, $action);
				}
203 204 205 206

				continue;
			}

207 208
			$uri = str_replace('(:bundle)', static::$bundle, $uri);

209 210 211 212
			// If the URI begins with a wildcard, we want to add this route to the
			// array of "fallback" routes. Fallback routes are always processed
			// last when parsing routes since they are very generic and could
			// overload bundle routes that are registered.
213
			if ($uri[0] == '(')
214 215 216 217 218 219 220 221
			{
				$routes =& static::$fallback;
			}
			else
			{
				$routes =& static::$routes;
			}

222 223 224 225
			// If the action is an array, we can simply add it to the array of
			// routes keyed by the URI. Otherwise, we will need to call into
			// the action method to get a valid action array.
			if (is_array($action))
226
			{
227
				$routes[$method][$uri] = $action;
228 229 230
			}
			else
			{
231 232 233 234 235 236 237 238 239
				$routes[$method][$uri] = static::action($action);
			}
			
			// If a group is being registered, we'll merge all of the group
			// options into the action, giving preference to the action
			// for options that are specified in both.
			if ( ! is_null(static::$group))
			{
				$routes[$method][$uri] += static::$group;
240 241
			}

242 243 244 245
			// If the HTTPS option is not set on the action, we'll use the
			// value given to the method. The secure method passes in the
			// HTTPS value in as a parameter short-cut.
			if ( ! isset($routes[$method][$uri]['https']))
246
			{
247
				$routes[$method][$uri]['https'] = false;
248
			}
249 250
		}
	}
251

252 253 254 255 256 257 258 259 260 261 262 263 264 265
	/**
	 * Convert a route action to a valid action array.
	 *
	 * @param  mixed  $action
	 * @return array
	 */
	protected static function action($action)
	{
		// If the action is a string, it is a pointer to a controller, so we
		// need to add it to the action array as a "uses" clause, which will
		// indicate to the route to call the controller.
		if (is_string($action))
		{
			$action = array('uses' => $action);
266
		}
267 268 269 270 271 272 273 274 275
		// If the action is a Closure, we will manually put it in an array
		// to work around a bug in PHP 5.3.2 which causes Closures cast
		// as arrays to become null. We'll remove this.
		elseif ($action instanceof Closure)
		{
			$action = array($action);
		}

		return (array) $action;
276 277
	}

278
	/**
279
	 * Register a secure controller with the router.
280
	 *
281 282
	 * @param  string|array  $controllers
	 * @param  string|array  $defaults
283 284
	 * @return void
	 */
285
	public static function secure_controller($controllers, $defaults = 'index')
286
	{
287 288
		static::controller($controllers, $defaults, true);
	}
289

290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328
	/**
	 * Register a controller with the router.
	 *
	 * @param  string|array  $controller
	 * @param  string|array  $defaults
	 * @param  bool          $https
	 * @return void
	 */
	public static function controller($controllers, $defaults = 'index', $https = false)
	{
		foreach ((array) $controllers as $identifier)
		{
			list($bundle, $controller) = Bundle::parse($identifier);

			// First we need to replace the dots with slashes in thte controller name
			// so that it is in directory format. The dots allow the developer to use
			// a cleaner syntax when specifying the controller. We will also grab the
			// root URI for the controller's bundle.
			$controller = str_replace('.', '/', $controller);

			$root = Bundle::option($bundle, 'handles');

			// If the controller is a "home" controller, we'll need to also build a
			// index method route for the controller. We'll remove "home" from the
			// route root and setup a route to point to the index method.
			if (ends_with($controller, 'home'))
			{
				static::root($identifier, $controller, $root);
			}

			// The number of method arguments allowed for a controller is set by a
			// "segments" constant on this class which allows for the developer to
			// increase or decrease the limit on method arguments.
			$wildcards = static::repeat('(:any?)', static::$segments);

			// Once we have the path and root URI we can build a simple route for
			// the controller that should handle a conventional controller route
			// setup of controller/method/segment/segment, etc.
			$pattern = trim("{$root}/{$controller}/{$wildcards}", '/');
329

330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358
			// Finally we can build the "uses" clause and the attributes for the
			// controller route and register it with the router with a wildcard
			// method so it is available on every request method.
			$uses = "{$identifier}@(:1)";

			$attributes = compact('uses', 'defaults', 'https');

			static::register('*', $pattern, $attributes);
		}
	}

	/**
	 * Register a route for the root of a controller.
	 *
	 * @param  string  $identifier
	 * @param  string  $controller
	 * @param  string  $root
	 * @return void
	 */
	protected static function root($identifier, $controller, $root)
	{
		// First we need to strip "home" off of the controller name to create the
		// URI needed to match the controller's folder, which should match the
		// root URI we want to point to the index method.
		if ($controller !== 'home')
		{
			$home = dirname($controller);
		}
		else
359
		{
360
			$home = '';
361 362
		}

363 364 365 366 367
		// After we trim the "home" off of the controller name we'll build the
		// pattern needed to map to the controller and then register a route
		// to point the pattern to the controller's index method.
		$pattern = trim($root.'/'.$home, '/') ?: '/';

368
		$attributes = array('uses' => "{$identifier}@index");
369 370

		static::register('*', $pattern, $attributes);
371 372 373
	}

	/**
Taylor Otwell committed
374
	 * Find a route by the route's assigned name.
375 376 377
	 *
	 * @param  string  $name
	 * @return array
378
	 */
379
	public static function find($name)
380
	{
381
		if (isset(static::$names[$name])) return static::$names[$name];
382

383 384
		// If no route names have been found at all, we will assume no reverse
		// routing has been done, and we will load the routes file for all of
385
		// the bundles that are installed for the application.
386
		if (count(static::$names) == 0)
387
		{
Taylor Otwell committed
388
			foreach (Bundle::names() as $bundle)
389
			{
390 391 392 393 394 395
				Bundle::routes($bundle);
			}
		}

		// To find a named route, we will iterate through every route defined
		// for the application. We will cache the routes by name so we can
396
		// load them very quickly the next time.
Taylor Otwell committed
397
		foreach (static::routes() as $method => $routes)
398
		{
Taylor Otwell committed
399
			foreach ($routes as $key => $value)
400
			{
401
				if (isset($value['as']) and $value['as'] === $name)
Taylor Otwell committed
402 403 404
				{
					return static::$names[$name] = array($key => $value);
				}
405 406
			}
		}
Taylor Otwell committed
407
	}
408

Taylor Otwell committed
409
	/**
410
	 * Find the route that uses the given action.
411 412 413 414
	 *
	 * @param  string  $action
	 * @return array
	 */
415
	public static function uses($action)
416 417 418 419
	{
		// If the action has already been reverse routed before, we'll just
		// grab the previously found route to save time. They are cached
		// in a static array on the class.
420
		if (isset(static::$uses[$action]))
421
		{
422
			return static::$uses[$action];
423 424 425 426
		}

		Bundle::routes(Bundle::name($action));

427 428 429
		// To find the route, we'll simply spin through the routes looking
		// for a route with a "uses" key matching the action, and if we
		// find one we cache and return it.
Taylor Otwell committed
430
		foreach (static::routes() as $method => $routes)
431
		{
Taylor Otwell committed
432
			foreach ($routes as $key => $value)
433
			{
Taylor Otwell committed
434 435 436 437
				if (isset($value['uses']) and $value['uses'] === $action)
				{
					return static::$uses[$action] = array($key => $value);
				}
438 439 440 441 442
			}
		}
	}

	/**
Taylor Otwell committed
443
	 * Search the routes for the route matching a method and URI.
444
	 *
445 446
	 * @param  string   $method
	 * @param  string   $uri
447 448
	 * @return Route
	 */
449
	public static function route($method, $uri)
450
	{
451 452
		Bundle::start($bundle = Bundle::handles($uri));

Taylor Otwell committed
453
		$routes = (array) static::method($method);
454

455
		// Of course literal route matches are the quickest to find, so we will
456
		// check for those first. If the destination key exists in the routes
457
		// array we can just return that route now.
458
		if (array_key_exists($uri, $routes))
459
		{
460
			$action = $routes[$uri];
461

462
			return new Route($method, $uri, $action);
463 464
		}

465 466 467 468
		// If we can't find a literal match we'll iterate through all of the
		// registered routes to find a matching route based on the route's
		// regular expressions and wildcards.
		if ( ! is_null($route = static::match($method, $uri)))
469
		{
470
			return $route;
471 472 473 474 475 476
		}
	}

	/**
	 * Iterate through every route to find a matching route.
	 *
477 478
	 * @param  string  $method
	 * @param  string  $uri
479 480
	 * @return Route
	 */
481
	protected static function match($method, $uri)
482
	{
Taylor Otwell committed
483
		foreach (static::method($method) as $route => $action)
484
		{
485 486 487 488
			// We only need to check routes with regular expression since all other
			// would have been able to be matched by the search for literal matches
			// we just did before we started searching.
			if (str_contains($route, '('))
489
			{
Taylor Otwell committed
490 491
				$pattern = '#^'.static::wildcards($route).'$#';

492 493 494 495
				// If we get a match we'll return the route and slice off the first
				// parameter match, as preg_match sets the first array item to the
				// full-text match of the pattern.
				if (preg_match($pattern, $uri, $parameters))
496
				{
497
					return new Route($method, $route, $action, array_slice($parameters, 1));
498
				}
499 500
			}
		}
501 502 503
	}

	/**
504
	 * Translate route URI wildcards into regular expressions.
505
	 *
506 507
	 * @param  string  $key
	 * @return string
508
	 */
509
	protected static function wildcards($key)
510
	{
511
		list($search, $replace) = array_divide(static::$optional);
512

513 514 515 516
		// For optional parameters, first translate the wildcards to their
		// regex equivalent, sans the ")?" ending. We'll add the endings
		// back on when we know the replacement count.
		$key = str_replace($search, $replace, $key, $count);
517

518
		if ($count > 0)
519
		{
520
			$key .= str_repeat(')?', $count);
521
		}
522 523

		return strtr($key, static::$patterns);
524 525 526
	}

	/**
527
	 * Get all of the registered routes, with fallbacks at the end.
528
	 *
Taylor Otwell committed
529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544
	 * @return array
	 */
	public static function routes()
	{
		$routes = static::$routes;

		foreach (static::$methods as $method)
		{
			// It's possible that the routes array may not contain any routes for the
			// method, so we'll seed each request method with an empty array if it
			// doesn't already contain any routes.
			if ( ! isset($routes[$method])) $routes[$method] = array();

			$fallback = array_get(static::$fallback, $method, array());

			// When building the array of routes, we'll merge in all of the fallback
Pavel committed
545
			// routes for each request method individually. This allows us to avoid
Taylor Otwell committed
546 547 548 549 550 551 552 553 554 555
			// collisions when merging the arrays together.
			$routes[$method] = array_merge($routes[$method], $fallback);
		}

		return $routes;
	}

	/**
	 * Grab all of the routes for a given request method.
	 *
556 557
	 * @param  string  $method
	 * @return array
558
	 */
Taylor Otwell committed
559
	public static function method($method)
560
	{
561
		$routes = array_get(static::$routes, $method, array());
Taylor Otwell committed
562

563
		return array_merge($routes, array_get(static::$fallback, $method, array()));
564 565
	}

566
	/**
567
	 * Get all of the wildcard patterns
568 569 570
	 *
	 * @return array
	 */
571 572 573 574 575 576 577 578 579 580 581 582 583
	public static function patterns()
	{
		return array_merge(static::$patterns, static::$optional);
	}

	/**
	 * Get a string repeating a URI pattern any number of times.
	 *
	 * @param  string  $pattern
	 * @param  int     $times
	 * @return string
	 */
	protected static function repeat($pattern, $times)
584
	{
585
		return implode('/', array_fill(0, $times, $pattern));
586 587
	}

588
}