router.php 13.6 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
	public static $routes = array();
Taylor Otwell committed
30 31

	/**
32 33 34 35 36 37 38
	 * All of the "fallback" routes that have been registered.
	 *
	 * @var array
	 */
	public static $fallback = array();

	/**
39 40 41 42 43 44
	 * The current attributes being shared by routes.
	 */
	public static $group;

	/**
	 * The "handes" clause for the bundle currently being routed.
45
	 *
46
	 * @var string
47
	 */
48
	public static $bundle;
49 50

	/**
51
	 * The number of URI segments allowed as method arguments.
52
	 *
53
	 * @var int
54
	 */
55
	public static $segments = 5;
56 57

	/**
58 59 60 61
	 * The wildcard patterns supported by the router.
	 *
	 * @var array
	 */
62
	public static $patterns = array(
63
		'(:num)' => '([0-9]+)',
64
		'(:any)' => '([a-zA-Z0-9\.\-_%]+)',
65
		'(:all)' => '(.*)',
66 67 68 69 70 71 72
	);

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

	/**
80 81 82 83 84 85 86
	 * An array of HTTP request methods.
	 *
	 * @var array
	 */
	public static $methods = array('GET', 'POST', 'PUT', 'DELETE');

	/**
87 88
	 * Register a HTTPS route with the router.
	 *
89
	 * @param  string        $method
90 91 92 93
	 * @param  string|array  $route
	 * @param  mixed         $action
	 * @return void
	 */
94
	public static function secure($method, $route, $action)
95
	{
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122
		$action = static::action($action);

		$action['https'] = true;

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

	/**
	 * 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
		// null so the attributes will not be assigned to any of the routes
		// that are added after the group is declared.
		static::$group = null;
123 124 125
	}

	/**
126
	 * Register a route with the router.
127
	 *
128 129
	 * <code>
	 *		// Register a route with the router
130
	 *		Router::register('GET' ,'/', function() {return 'Home!';});
131 132
	 *
	 *		// Register a route that handles multiple URIs with the router
133
	 *		Router::register(array('GET', '/', 'GET /home'), function() {return 'Home!';});
134 135
	 * </code>
	 *
136
	 * @param  string        $method
137
	 * @param  string|array  $route
138
	 * @param  mixed         $action
139
	 * @return void
140
	 */
141
	public static function register($method, $route, $action)
142
	{
143 144
		if (is_string($route)) $route = explode(', ', $route);

145 146
		foreach ((array) $route as $uri)
		{
147 148 149
			// 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.
150
			if ($method == '*')
151
			{
152 153 154 155
				foreach (static::$methods as $method)
				{
					static::register($method, $route, $action);
				}
156 157 158 159

				continue;
			}

160 161
			$uri = str_replace('(:bundle)', static::$bundle, $uri);

162 163 164 165
			// 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.
166
			if ($uri[0] == '(')
167 168 169 170 171 172 173 174
			{
				$routes =& static::$fallback;
			}
			else
			{
				$routes =& static::$routes;
			}

175 176 177 178
			// 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))
179
			{
180
				$routes[$method][$uri] = $action;
181 182 183
			}
			else
			{
184 185 186 187 188 189 190 191 192
				$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;
193 194
			}

195 196 197 198
			// 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']))
199
			{
200
				$routes[$method][$uri]['https'] = false;
201
			}
202 203
		}
	}
204

205 206 207 208 209 210 211 212 213 214 215 216 217 218
	/**
	 * 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);
219
		}
220 221 222 223 224 225 226 227 228
		// 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;
229 230
	}

231
	/**
232
	 * Register a secure controller with the router.
233
	 *
234 235
	 * @param  string|array  $controllers
	 * @param  string|array  $defaults
236 237
	 * @return void
	 */
238
	public static function secure_controller($controllers, $defaults = 'index')
239
	{
240 241
		static::controller($controllers, $defaults, true);
	}
242

243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281
	/**
	 * 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}", '/');
282

283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311
			// 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
312
		{
313
			$home = '';
314 315
		}

316 317 318 319 320
		// 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, '/') ?: '/';

321
		$attributes = array('uses' => "{$identifier}@index");
322 323

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

	/**
Taylor Otwell committed
327
	 * Find a route by the route's assigned name.
328 329 330
	 *
	 * @param  string  $name
	 * @return array
331
	 */
332
	public static function find($name)
333
	{
334
		if (isset(static::$names[$name])) return static::$names[$name];
335

336 337
		// 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
338
		// the bundles that are installed for the application.
339
		if (count(static::$names) == 0)
340
		{
Taylor Otwell committed
341
			foreach (Bundle::names() as $bundle)
342
			{
343 344 345 346 347 348
				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
349 350
		// load them very quickly the next time.
		foreach (static::all() as $key => $value)
351
		{
352
			if (array_get($value, 'name') === $name)
353 354
			{
				return static::$names[$name] = array($key => $value);
355 356
			}
		}
Taylor Otwell committed
357
	}
358

Taylor Otwell committed
359
	/**
360
	 * Find the route that uses the given action.
361 362 363 364
	 *
	 * @param  string  $action
	 * @return array
	 */
365
	public static function uses($action)
366 367 368 369
	{
		// 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.
370
		if (isset(static::$uses[$action]))
371
		{
372
			return static::$uses[$action];
373 374 375 376
		}

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

377 378 379 380
		// 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.
		foreach (static::all() as $uri => $route)
381
		{
382
			if (array_get($route, 'uses') == $action)
383
			{
384
				return static::$uses[$action] = array($uri => $route);
385 386 387 388 389
			}
		}
	}

	/**
Taylor Otwell committed
390
	 * Search the routes for the route matching a method and URI.
391
	 *
392 393
	 * @param  string   $method
	 * @param  string   $uri
394 395
	 * @return Route
	 */
396
	public static function route($method, $uri)
397
	{
398 399
		Bundle::start($bundle = Bundle::handles($uri));

400 401
		$routes = (array) static::routes($method);

402
		// Of course literal route matches are the quickest to find, so we will
403
		// check for those first. If the destination key exists in the routes
404
		// array we can just return that route now.
405
		if (array_key_exists($uri, $routes))
406
		{
407
			$action = $routes[$uri];
408

409
			return new Route($method, $uri, $action);
410 411
		}

412 413 414 415
		// 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)))
416
		{
417
			return $route;
418 419 420 421 422 423
		}
	}

	/**
	 * Iterate through every route to find a matching route.
	 *
424 425
	 * @param  string  $method
	 * @param  string  $uri
426 427
	 * @return Route
	 */
428
	protected static function match($method, $uri)
429
	{
430
		foreach (static::routes($method) as $route => $action)
431
		{
432 433 434 435
			// 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, '('))
436
			{
Taylor Otwell committed
437 438
				$pattern = '#^'.static::wildcards($route).'$#';

439 440 441 442
				// 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))
443
				{
444
					return new Route($method, $route, $action, array_slice($parameters, 1));
445
				}
446 447
			}
		}
448 449 450
	}

	/**
451
	 * Translate route URI wildcards into regular expressions.
452
	 *
453 454
	 * @param  string  $key
	 * @return string
455
	 */
456
	protected static function wildcards($key)
457
	{
458
		list($search, $replace) = array_divide(static::$optional);
459

460 461 462 463
		// 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);
464

465
		if ($count > 0)
466
		{
467
			$key .= str_repeat(')?', $count);
468
		}
469 470

		return strtr($key, static::$patterns);
471 472 473
	}

	/**
474
	 * Get all of the routes across all request methods.
475
	 *
476
	 * @return array
477
	 */
478
	public static function all()
479
	{
480
		$all = array();
481

482 483 484 485 486 487
		// To get all the routes, we'll just loop through each request
		// method supported by the router and merge in each of the
		// arrays into the main array of routes.
		foreach (static::$methods as $method)
		{
			$all = array_merge($all, static::routes($method));
488
		}
489 490

		return $all;
491 492 493
	}

	/**
494
	 * Get all of the registered routes, with fallbacks at the end.
495
	 *
496 497
	 * @param  string  $method
	 * @return array
498
	 */
499
	public static function routes($method = null)
500
	{
501
		$routes = array_get(static::$routes, $method, array());
Taylor Otwell committed
502

503
		return array_merge($routes, array_get(static::$fallback, $method, array()));
504 505
	}

506
	/**
507
	 * Get all of the wildcard patterns
508 509 510
	 *
	 * @return array
	 */
511 512 513 514 515 516 517 518 519 520 521 522 523
	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)
524
	{
525
		return implode('/', array_fill(0, $times, $pattern));
526 527
	}

528
}