Browse Source

updated routing to fix several issues.

Taylor Otwell 12 years ago
parent
commit
3a92facc76

+ 36 - 0
application/bundles.php

@@ -0,0 +1,36 @@
+<?php
+
+/*
+|--------------------------------------------------------------------------
+| Bundle Configuration
+|--------------------------------------------------------------------------
+|
+| Bundles allow you to conveniently extend and organize your application.
+| Think of bundles as self-contained applications. They can have routes,
+| controllers, models, views, configuration, etc. You can even create
+| your own bundles to share with the Laravel community.
+|
+| This is a list of the bundles installed for your application and tells
+| Laravel the location of the bundle's root directory, as well as the
+| root URI the bundle responds to.
+|
+| For example, if you have an "admin" bundle located in "bundles/admin" 
+| that you want to handle requests with URIs that begin with "admin",
+| simply add it to the array like this:
+|
+|		'admin' => array(
+|			'location' => 'admin',
+|			'handles'  => 'admin',
+|		),
+|
+| Note that the "location" is relative to the "bundles" directory.
+| Now the bundle will be recognized by Laravel and will be able
+| to respond to requests beginning with "admin"!
+|
+| Have a bundle that lives in the root of the bundle directory
+| and doesn't respond to any requests? Just add the bundle
+| name to the array and we'll take care of the rest.
+|
+*/
+
+return array();

+ 1 - 29
application/config/application.php

@@ -97,35 +97,6 @@ return array(
 
 	'timezone' => 'UTC',
 
-	/*
-	|--------------------------------------------------------------------------
-	| Bundle Options
-	|--------------------------------------------------------------------------
-	|
-	| Here you may specify options related to application bundles, such as the
-	| amount of time the bundle manifest is cached. Each option is detailed
-	| below with suggestions for sensible values.
-	|
-	| Cache:
-	|
-	| All bundles have a "bundle.info" file which contains information such
-	| as the name of a bundle and the URIs it responds to. This value is
-	| the number of minutes that bundle info is cached.
-	|
-	| Auto:
-	|
-	| You may wish to auto-start some bundles instead of lazy-loading them.
-	| This is useful for debug bundles as well as bundles that are used
-	| throughout your application. You may specify which bundles should
-	| be auto-loaded in this array.
-	|
-	*/
-
-	'bundle' => array(
-		'cache' => 0,
-		'auto'  => array(),
-	),
-
 	/*
 	|--------------------------------------------------------------------------
 	| Class Aliases
@@ -168,6 +139,7 @@ return array(
 		'Redis'      => 'Laravel\\Redis',
 		'Request'    => 'Laravel\\Request',
 		'Response'   => 'Laravel\\Response',
+		'Route'      => 'Laravel\\Routing\\Route',
 		'Router'     => 'Laravel\\Routing\\Router',
 		'Schema'     => 'Laravel\\Database\\Schema',
 		'Section'    => 'Laravel\\Section',

+ 1 - 1
application/controllers/base.php

@@ -7,7 +7,7 @@ class Base_Controller extends Controller {
 	 *
 	 * @param  string    $method
 	 * @param  array     $parameters
-	 * @return Laravel\Response
+	 * @return Response
 	 */
 	public function __call($method, $parameters)
 	{

+ 29 - 4
application/routes.php

@@ -12,32 +12,57 @@
 |
 | Let's respond to a simple GET request to http://example.com/hello:
 |
-|		Router::register('GET /hello', function()
+|		Route::get('hello', function()
 |		{
 |			return 'Hello World!';
 |		});
 |
 | You can even respond to more than one URI:
 |
-|		Router::register('GET /hello, GET /world', function()
+|		Route::post('hello, world', function()
 |		{
 |			return 'Hello World!';
 |		});
 |
 | It's easy to allow URI wildcards using (:num) or (:any):
 |
-|		Router::register('GET /hello/(:any)', function($name)
+|		Route::put('hello/(:any)', function($name)
 |		{
 |			return "Welcome, $name.";
 |		});
 |
 */
 
-Router::register(array('GET /', 'GET /home'), function()
+Route::get('/, home', function()
 {
 	return View::make('home.index');
 });
 
+/*
+|--------------------------------------------------------------------------
+| Application 404 & 500 Error Handlers
+|--------------------------------------------------------------------------
+|
+| To centralize and simplify 404 handling, Laravel uses an awesome event
+| system to retrieve the response. Feel free to modify this function to
+| your tastes and the needs of your application.
+|
+| Similarly, we use an event to handle the display of 500 level errors
+| within the application. These errors are fired when there is an
+| uncaught exception thrown in the application.
+|
+*/
+
+Event::listen('404', function()
+{
+	return Response::error('404');
+});
+
+Event::listen('500', function()
+{
+	return Response::error('500');
+});
+
 /*
 |--------------------------------------------------------------------------
 | Route Filters

+ 19 - 20
application/start.php

@@ -2,40 +2,39 @@
 
 /*
 |--------------------------------------------------------------------------
-| Auto-Loader PSR-0 Directories
+| Auto-Loader Mappings
 |--------------------------------------------------------------------------
 |
-| The Laravel auto-loader can search directories for files using the PSR-0
-| naming convention. This convention basically organizes classes by using
-| the class namespace to indicate the directory structure.
+| Laravel uses a simple array of class to path mappings to drive the class
+| auto-loader. This simple approach helps avoid the performance problems
+| of searching through directories by convention.
 |
-| So you don't have to manually map all of your models, we've added the
-| models and libraries directories for you. So, you can model away and
-| the auto-loader will take care of the rest.
+| Registering a mapping couldn't be easier. Just pass an array of class
+| to path maps into the "map" function of Autoloader. Then, when you
+| want to use that class, just use it. It's simple!
 |
 */
 
-Autoloader::psr(array(
-	path('app').'models',
-	path('app').'libraries',
+Autoloader::map(array(
+	'Base_Controller' => path('app').'controllers/base.php',
 ));
 
 /*
 |--------------------------------------------------------------------------
-| Auto-Loader Mappings
+| Auto-Loader PSR-0 Directories
 |--------------------------------------------------------------------------
 |
-| Laravel uses a simple array of class to path mappings to drive the class
-| auto-loader. This simple approach helps avoid the performance problems
-| of searching through directories by some kind of convention. It also
-| gives you the freedom to organize your application how you want.
+| The Laravel auto-loader can search directories for files using the PSR-0
+| naming convention. This convention basically organizes classes by using
+| the class namespace to indicate the directory structure.
 |
-| Registering a mapping couldn't be easier. Just pass an array of class
-| to path maps into the "map" function of Autoloader. Then, when you
-| want to use that class, just use it. It's a piece of cake.
+| So you don't have to manually map all of your models, we've added the
+| models and libraries directories for you. So, you can model away and
+| the auto-loader will take care of the rest.
 |
 */
 
-Autoloader::map(array(
-	'Base_Controller' => path('app').'controllers/base.php',
+Autoloader::psr(array(
+	path('app').'models',
+	path('app').'libraries',
 ));

+ 1 - 1
laravel/autoloader.php

@@ -104,7 +104,7 @@ class Autoloader {
 	{
 		// The PSR-0 standard indicates that class namespaces and underscores
 		// shoould be used to indcate the directory tree in which the class
-		// resides, so we'll convert them to directory slashes.
+		// resides, so we'll convert them to slashes.
 		$file = str_replace(array('\\', '_'), '/', $class);
 
 		$directories = $directory ?: static::$psr;

+ 55 - 92
laravel/bundle.php

@@ -33,103 +33,35 @@ class Bundle {
 	public static $routed = array();
 
 	/**
-	 * The cache key for the bundle manifest.
+	 * Register the bundle for the application.
 	 *
-	 * @var string
-	 */
-	const manifest = 'laravel.bundle.manifest';
-
-	/**
-	 * Detect all of the installed bundles from disk.
-	 *
-	 * @param  string  $path
-	 * @return array
-	 */
-	public static function detect($path)
-	{
-		return static::search($path);
-	}
-
-	/**
-	 * Detect all of the installed bundles from disk.
-	 *
-	 * @param  string  $path
-	 * @return array
-	 */
-	protected static function search($path)
-	{
-		$bundles = array();
-
-		$items = new fIterator($path);
-
-		foreach ($items as $item)
-		{
-			// If the item is a directory, we'll search for a bundle.info file.
-			// If one exists, we will add it to the bundle array. We will set
-			// the location automatically since we know it.
-			if ($item->isDir())
-			{
-				$path = $item->getRealPath().DS.'bundle.php';
-
-				// If we found a file, we'll require in the array it contains
-				// and add it to the directory. The info array will contain
-				// basic info like the bundle name and any URIs it may
-				// handle incoming requests for.
-				if (file_exists($path))
-				{
-					$info = require $path;
-
-					$info['location'] = dirname($path).DS;
-
-					$bundles[$info['name']] = $info;
-
-					continue;
-				}
-				// If a bundle.info file doesn't exist within a directory,
-				// we'll recurse into the directory to keep searching in
-				// the bundle directory for nested bundles.
-				else
-				{
-					$recurse = static::detect($item->getRealPath());
-
-					$bundles = array_merge($bundles, $recurse);
-				}
-			}
-		}
-
-		return $bundles;
-	}
-
-	/**
-	 * Register a bundle for the application.
-	 *
-	 * @param  array  $config
+	 * @param  string  $bundle
+	 * @param  array   $config
 	 * @return void
 	 */
-	public static function register($config)
+	public static function register($bundle, $config = array())
 	{
 		$defaults = array('handles' => null, 'auto' => false);
 
-		// If a handles clause has been specified, we will cap it with a trailing
-		// slash so the bundle is not extra greedy with its routes. Otherwise a
-		// bundle that handles "s" would handle all routes beginning with "s".
-		if (isset($config['handles']))
+		// If the given configuration is actually a string, we will assume it is a
+		// location and set the bundle name to match it. This is common for most
+		// bundles who simply live in the root bundle directory.
+		if (is_string($config))
 		{
-			$config['handles'] = str_finish($config['handles'], '/');
+			$bundle = $config;
+
+			$config = array('location' => $bundle);
 		}
 
-		static::$bundles[$config['name']] = array_merge($defaults, $config);
-	}
+		// IF no location is set, we will set the location to match the name of
+		// the bundle. This is for bundles who are installed to the root of
+		// the bundle directory so a location was not set.
+		if ( ! isset($config['location']))
+		{
+			$config['location'] = $bundle;
+		}
 
-	/**
-	 * Disable a bundle for the current request.
-	 *
-	 * @param  string  $bundle
-	 * @return void
-	 */
-	public static function disable($bundle)
-	{
-		unset(static::$bundles[$bundle]);
+		static::$bundles[$bundle] = array_merge($defaults, $config);
 	}
 
 	/**
@@ -151,8 +83,7 @@ class Bundle {
 
 		// Each bundle may have a "start" script which is responsible for preparing
 		// the bundle for use by the application. The start script may register any
-		// classes the bundle uses with the auto-loader, or perhaps will start any
-		// dependent bundles so that they are available.
+		// classes the bundle uses with the auto-loader, etc.
 		if (file_exists($path = static::path($bundle).'start'.EXT))
 		{
 			require $path;
@@ -178,6 +109,11 @@ class Bundle {
 	{
 		$path = static::path($bundle).'routes'.EXT;
 
+		// By setting the bundle property on the router the router knows what
+		// value to replace the (:bundle) place-holder with when the bundle
+		// routes are added, keeping the routes flexible.
+		Routing\Router::$bundle = static::option($bundle, 'handles');
+
 		if ( ! static::routed($bundle) and file_exists($path))
 		{
 			require $path;
@@ -186,6 +122,17 @@ class Bundle {
 		static::$routed[] = $bundle;
 	}
 
+	/**
+	 * Disable a bundle for the current request.
+	 *
+	 * @param  string  $bundle
+	 * @return void
+	 */
+	public static function disable($bundle)
+	{
+		unset(static::$bundles[$bundle]);
+	}
+
 	/**
 	 * Determine which bundle handles the given URI.
 	 *
@@ -200,7 +147,10 @@ class Bundle {
 
 		foreach (static::$bundles as $key => $value)
 		{
-			if (starts_with($uri, $value['handles'])) return $key;
+			if (isset($value['handles']) and starts_with($uri, $value['handles'].'/'))
+			{
+				return $key;
+			}
 		}
 
 		return DEFAULT_BUNDLE;
@@ -217,6 +167,19 @@ class Bundle {
 		return $bundle == DEFAULT_BUNDLE or in_array(strtolower($bundle), static::names());
 	}
 
+	/**
+	 * Get the full path location of a given bundle.
+	*
+	* @param  string  $bundle
+	* @return string
+	 */
+	public static function location($bundle)
+	{
+		$location = array_get(static::$bundles, $bundle.'.location');
+
+		return path('bundle').str_finish($location, DS);
+	}
+
 	/**
 	 * Determine if a given bundle has been started for the request.
 	 *
@@ -277,7 +240,7 @@ class Bundle {
 	 */
 	public static function path($bundle)
 	{
-		return ($bundle == DEFAULT_BUNDLE) ? path('app') : static::$bundles[$bundle]['location'];
+		return ($bundle == DEFAULT_BUNDLE) ? path('app') : static::location($bundle);
 	}
 
 	/**
@@ -401,7 +364,7 @@ class Bundle {
 	 * Get the information for a given bundle.
 	 *
 	 * @param  string  $bundle
-	 * @return array
+	 * @return object
 	 */
 	public static function get($bundle)
 	{

+ 1 - 1
laravel/cache/drivers/apc.php

@@ -39,7 +39,7 @@ class APC extends Driver {
 	 */
 	protected function retrieve($key)
 	{
-		if ( ! is_null($cache = apc_fetch($this->key.$key)))
+		if (($cache = apc_fetch($this->key.$key)) !== false)
 		{
 			return $cache;
 		}

+ 0 - 20
laravel/cli/artisan.php

@@ -10,26 +10,6 @@ use Laravel\Config;
  */
 Bundle::start(DEFAULT_BUNDLE);
 
-/**
- * Set the CLI options on the $_SERVER global array so we can easily
- * retrieve them from the various parts of the CLI code. We can use
- * the Request class to access them conveniently.
- */
-list($arguments, $_SERVER['CLI']) = Command::options($_SERVER['argv']);
-
-$_SERVER['CLI'] = array_change_key_case($_SERVER['CLI'], CASE_UPPER);
-
-/**
- * The Laravel environment may be specified on the CLI using the "env"
- * option, allowing the developer to easily use local configuration
- * files from the CLI since the environment is usually controlled
- * by server environmenet variables.
- */
-if (isset($_SERVER['CLI']['ENV']))
-{
-	$_SERVER['LARAVEL_ENV'] = $_SERVER['CLI']['ENV'];
-}
-
 /**
  * The default database connection may be set by specifying a value
  * for the "database" CLI option. This allows migrations to be run

+ 22 - 86
laravel/cli/tasks/bundle/bundler.php

@@ -55,10 +55,8 @@ class Bundler extends Task {
 
 			$this->download($bundle, $path);
 
-			echo "Bundle [{$bundle['name']}] has been installed!".PHP_EOL;
+			echo "Bundle [{$bundle['name']}] installed!".PHP_EOL;
 		}
-
-		$this->refresh();
 	}
 
 	/**
@@ -83,7 +81,7 @@ class Bundler extends Task {
 			// First we want to retrieve the information for the bundle, such as
 			// where it is currently installed. This will allow us to upgrade
 			// the bundle into it's current installation path.
-			$bundle = Bundle::get($name);
+			$location = Bundle::location($name);
 
 			// If the bundle exists, we will grab the data about the bundle from
 			// the API so we can make the right bundle provider for the bundle,
@@ -98,89 +96,12 @@ class Bundler extends Task {
 			// Once we have the bundle information from the API, we'll simply
 			// recursively delete the bundle and then re-download it using
 			// the correct provider assigned to the bundle.
-			File::rmdir($bundle['location']);
+			File::rmdir($location);
 
-			$this->download($response['bundle'], $bundle['location']);
+			$this->download($response['bundle'], $location);
 
 			echo "Bundle [{$name}] has been upgraded!".PHP_EOL;
 		}
-
-		$this->refresh();
-	}
-
-	/**
-	 * Publish bundle assets to the public directory.
-	 *
-	 * @param  array  $bundles
-	 * @return void
-	 */
-	public function publish($bundles)
-	{
-		if (count($bundles) == 0) $bundles = Bundle::names();
-
-		array_walk($bundles, array(IoC::resolve('bundle.publisher'), 'publish'));
-	}
-
-	/**
-	 * Create a new bundle stub.
-	 *
-	 * @param  array  $arguments
-	 * @return void
-	 */
-	public function make($arguments)
-	{
-		if ( ! isset($arguments[0]))
-		{
-			throw new \Exception("We need to know the bundle name!");
-		}
-
-		// First we'll grab the name from the argument list and make sure a bundle
-		// with that name doesn't already exist. If it does, we'll bomb out and
-		// notify the developer of the problem. Bundle names must be unique
-		// since classes are prefixed with the name.
-		$options['name'] = $name = $arguments[0];
-
-		if (Bundle::exists($name))
-		{
-			throw new \Exception("That bundle already exists!");
-		}
-
-		// The developer may specify a location to which the bundle should be
-		// installed. If a location is not specified, the bundle name will
-		// be used as the default installation location.
-		$location = Request::server('cli.location') ?: $name;
-
-		$location = path('bundle').$location;
-
-		$options['handles'] = Request::server('cli.handles');
-
-		// We'll create the actual PHP that should be inserted into the info
-		// file for the bundle. This contains the bundle's name as well as
-		// any URIs it is setup to handle.
-		$info = '<?php return '.var_export($options, true).';';
-
-		mkdir($location, 0777, true);
-
-		// Finally we can write the file to disk and clear the bundle cache.
-		// We clear the cache so that the new bundle will be recognized
-		// immediately and the developer can start using it.
-		File::put($location.DS.'bundle'.EXT, $info);
-
-		echo "Bundle [{$name}] has been created!".PHP_EOL;
-
-		$this->refresh();
-	}
-
-	/**
-	 * Clear the bundle manifest cache.
-	 *
-	 * @return void
-	 */
-	public function refresh()
-	{
-		Cache::forget(Bundle::manifest);
-
-		echo 'Bundle cache cleared!'.PHP_EOL;
 	}
 
 	/**
@@ -197,7 +118,7 @@ class Bundler extends Task {
 		{
 			// First we'll call the bundle repository to gather the bundle data
 			// array, which contains all of the information needed to install
-			// the bundle into the application.
+			// the bundle into the Laravel application.
 			$response = $this->retrieve($bundle);
 
 			if ($response['status'] == 'not-found')
@@ -207,12 +128,14 @@ class Bundler extends Task {
 
 			// If the bundle was retrieved successfully, we will add it to
 			// our array of bundles, as well as merge all of the bundle's
-			// dependencies into the array of responses so that they are
-			// installed along with the consuming dependency.
+			// dependencies into the array of responses.
 			$bundle = $response['bundle'];
 
 			$responses[] = $bundle;
 
+			// We'll also get the bundle's declared dependenceis so they
+			// can be installed along with the bundle, making it easy
+			// to install a group of bundles.
 			$dependencies = $this->get($bundle['dependencies']);
 
 			$responses = array_merge($responses, $dependencies);
@@ -221,6 +144,19 @@ class Bundler extends Task {
 		return $responses;
 	}
 
+	/**
+	 * Publish bundle assets to the public directory.
+	 *
+	 * @param  array  $bundles
+	 * @return void
+	 */
+	public function publish($bundles)
+	{
+		if (count($bundles) == 0) $bundles = Bundle::names();
+
+		array_walk($bundles, array(IoC::resolve('bundle.publisher'), 'publish'));
+	}
+
 	/**
 	 * Install a bundle using a provider.
 	 *

+ 1 - 1
laravel/cli/tasks/migrate/migrator.php

@@ -115,7 +115,7 @@ class Migrator extends Task {
 		// along with their bundles and names. We will iterate through each
 		// migration and run the "down" method, removing them from the
 		// database as we go.
-		foreach ($migrations as $migration)
+		foreach (array_reverse($migrations) as $migration)
 		{
 			$migration['migration']->down();
 

+ 38 - 21
laravel/core.php

@@ -30,14 +30,6 @@ require path('sys').'autoloader'.EXT;
  */
 spl_autoload_register(array('Laravel\\Autoloader', 'load'));
 
-/**
- * Register all of the core class aliases. These aliases provide a
- * convenient way of working with the Laravel core classes without
- * having to worry about the namespacing. The developer is also
- * free to remove aliases when they extend core classes.
- */
-Autoloader::$aliases = Config::get('application.aliases');
-
 /**
  * Register the Laravel namespace so that the auto-loader loads it
  * according to the PSR-0 naming conventions. This should provide
@@ -46,27 +38,40 @@ Autoloader::$aliases = Config::get('application.aliases');
 Autoloader::namespaces(array('Laravel' => path('sys')));
 
 /**
- * Grab the bundle manifest for the application. This contains an
- * array of all of the installed bundles, plus information about
- * each of them. If it's not cached, we'll detect them and then
- * cache it to save time later.
+ * Set the CLI options on the $_SERVER global array so we can easily
+ * retrieve them from the various parts of the CLI code. We can use
+ * the Request class to access them conveniently.
  */
-$bundles = Cache::remember(Bundle::manifest, function()
+if (defined('STDIN'))
 {
-	return Bundle::detect(path('bundle'));
+	$console = CLI\Command::options($_SERVER['argv']);
+
+	list($arguments, $options) = $console;
 
-}, Config::get('application.bundle.cache'));
+	$options = array_change_key_case($options, CASE_UPPER);
+
+	$_SERVER['CLI'] = $options;
+}
 
 /**
- * Register all of the bundles that are defined in the main bundle
- * manifest. This informs the framework where the bundle lives
- * and which URIs it can respnod to.
+ * The Laravel environment may be specified on the CLI using the env
+ * option, allowing the developer to easily use local configuration
+ * files from the CLI since the environment is usually controlled
+ * by server environmenet variables.
  */
-foreach ($bundles as $bundle)
+if (isset($_SERVER['CLI']['ENV']))
 {
-	Bundle::register($bundle);
+	$_SERVER['LARAVEL_ENV'] = $_SERVER['CLI']['ENV'];
 }
 
+/**
+ * Register all of the core class aliases. These aliases provide a
+ * convenient way of working with the Laravel core classes without
+ * having to worry about the namespacing. The developer is also
+ * free to remove aliases when they extend core classes.
+ */
+Autoloader::$aliases = Config::get('application.aliases');
+
 /**
  * Register the default timezone for the application. This will
  * be the default timezone used by all date functions through
@@ -74,4 +79,16 @@ foreach ($bundles as $bundle)
  */
 $timezone = Config::get('application.timezone');
 
-date_default_timezone_set($timezone);
+date_default_timezone_set($timezone);
+
+/**
+ * Finally we'll grab all of the bundles and register them
+ * with the bundle class. All of the bundles are stored in
+ * an array within the application directory.
+ */
+$bundles = require path('app').'bundles'.EXT;
+
+foreach ($bundles as $bundle => $config)
+{
+	Bundle::register($bundle, $config);
+}

+ 2 - 2
laravel/database/connectors/mysql.php

@@ -20,11 +20,11 @@ class MySQL extends Connector {
 		// Check for any optional MySQL PDO options. These options are not required
 		// to establish a PDO connection; however, may be needed in certain server
 		// or hosting environments used by the developer.
-		foreach (array('port', 'unix_socket') as $key => $value)
+		foreach (array('port', 'unix_socket') as $key)
 		{
 			if (isset($config[$key]))
 			{
-				$dsn .= ";{$key}={$value}";
+				$dsn .= ";{$key}={$config[$key]}";
 			}
 		}
 

+ 13 - 13
laravel/database/schema/grammars/mysql.php

@@ -23,14 +23,14 @@ class MySQL extends Grammar {
 	{
 		$columns = implode(', ', $this->columns($table));
 
-		// First we will generate the base table creation statement. Other than
-		// auto-incrementing keys, no indexes will be created during the first
-		// creation of the table. They will be added in separate commands.
+		// First we will generate the base table creation statement. Other than incrementing
+		// keys, no indexes will be created during the first creation of the table since
+		// they will be added in separate commands.
 		$sql = 'CREATE TABLE '.$this->wrap($table).' ('.$columns.')';
 
-		// MySQL supports various "engines" for database tables. If an engine
-		// was specified by the developer, we will set it after adding the
-		// columns the table creation statement.
+		// MySQL supports various "engines" for database tables. If an engine ws specified
+		// by the developer, we will set it after adding the columns the table creation
+		// statement. Some engines support extra indexes.
 		if ( ! is_null($table->engine))
 		{
 			$sql .= ' ENGINE = '.$table->engine;
@@ -50,9 +50,9 @@ class MySQL extends Grammar {
 	{
 		$columns = $this->columns($table);
 
-		// Once we the array of column definitions, we need to add "add"
-		// to the front of each definition, then we'll concatenate the
-		// definitions using commas like normal and generate the SQL.
+		// Once we the array of column definitions, we need to add "add" to the front
+		// of each definition, then we'll concatenate the definitions using commas
+		// like normal and generate the SQL.
 		$columns = implode(', ', array_map(function($column)
 		{
 			return 'ADD '.$column;
@@ -77,7 +77,7 @@ class MySQL extends Grammar {
 			// Each of the data type's have their own definition creation method,
 			// which is responsible for creating the SQL for the type. This lets
 			// us to keep the syntax easy and fluent, while translating the
-			// types to the types used by the database.
+			// types to the correct types.
 			$sql = $this->wrap($column).' '.$this->type($column);
 
 			$elements = array('nullable', 'defaults', 'incrementer');
@@ -223,9 +223,9 @@ class MySQL extends Grammar {
 	{
 		$columns = array_map(array($this, 'wrap'), $command->columns);
 
-		// Once we the array of column names, we need to add "drop" to the
-		// front of each column, then we'll concatenate the columns using
-		// commas and generate the alter statement SQL.
+		// Once we the array of column names, we need to add "drop" to the front
+		// of each column, then we'll concatenate the columns using commas and
+		// generate the alter statement SQL.
 		$columns = implode(', ', array_map(function($column)
 		{
 			return 'DROP '.$column;

+ 12 - 13
laravel/database/schema/grammars/postgres.php

@@ -16,9 +16,9 @@ class Postgres extends Grammar {
 	{
 		$columns = implode(', ', $this->columns($table));
 
-		// First we will generate the base table creation statement. Other than
-		// auto-incrementing keys, no indexes will be created during the first
-		// creation of the table. They will be added in separate commands.
+		// First we will generate the base table creation statement. Other than incrementing
+		// keys, no indexes will be created during the first creation of the table since
+		// they will be added in separate commands.
 		$sql = 'CREATE TABLE '.$this->wrap($table).' ('.$columns.')';
 
 		return $sql;
@@ -35,9 +35,9 @@ class Postgres extends Grammar {
 	{
 		$columns = $this->columns($table);
 
-		// Once we the array of column definitions, we'll add "add column"
-		// to the front of each definition, then we'll concatenate the
-		// definitions using commas like normal and generate the SQL.
+		// Once we the array of column definitions, we need to add "add" to the front
+		// of each definition, then we'll concatenate the definitions using commas
+		// like normal and generate the SQL.
 		$columns = implode(', ', array_map(function($column)
 		{
 			return 'ADD COLUMN '.$column;
@@ -114,10 +114,9 @@ class Postgres extends Grammar {
 	 */
 	protected function incrementer(Table $table, Fluent $column)
 	{
-		// We don't actually need to specify an "auto_increment" keyword since
-		// we handle the auto-increment definition in the type definition for
-		// integers by changing the type to "serial", which is a convenient
-		// notational short-cut provided by Postgres.
+		// We don't actually need to specify an "auto_increment" keyword since we
+		// handle the auto-increment definition in the type definition for
+		// integers by changing the type to "serial".
 		if ($column->type == 'integer' and $column->increment)
 		{
 			return ' PRIMARY KEY';
@@ -218,9 +217,9 @@ class Postgres extends Grammar {
 	{
 		$columns = array_map(array($this, 'wrap'), $command->columns);
 
-		// Once we the array of column names, we need to add "drop" to the
-		// front of each column, then we'll concatenate the columns using
-		// commas and generate the alter statement SQL.
+		// Once we the array of column names, we need to add "drop" to the front
+		// of each column, then we'll concatenate the columns using commas and
+		// generate the alter statement SQL.
 		$columns = implode(', ', array_map(function($column)
 		{
 			return 'DROP COLUMN '.$column;

+ 15 - 19
laravel/database/schema/grammars/sqlite.php

@@ -16,26 +16,22 @@ class SQLite extends Grammar {
 	{
 		$columns = implode(', ', $this->columns($table));
 
-		// First we will generate the base table creation statement. Other than
-		// auto-incrementing keys, no indexes will be created during the first
-		// creation of the table. They will be added in separate commands.
+		// First we will generate the base table creation statement. Other than incrementing
+		// keys, no indexes will be created during the first creation of the table since
+		// they will be added in separate commands.
 		$sql = 'CREATE TABLE '.$this->wrap($table).' ('.$columns;
 
-		// SQLite does not allow adding a primary key as a command apart from
-		// when the table is initially created, so we'll need to sniff out
-		// any primary keys here and add them to the table.
-		//
-		// Because of this, this class does not have the typical "primary"
-		// method as it would be pointless since the primary keys can't
-		// be set on anything but the table creation statement.
+		// SQLite does not allow adding a primary key as a command apart from the creation
+		// of the table, so we'll need to sniff out any primary keys here and add them to
+		// the table now during this command.
 		$primary = array_first($table->commands, function($key, $value)
 		{
 			return $value->type == 'primary';
 		});
 
-		// If we found primary key in the array of commands, we'll create
-		// the SQL for the key addition and append it to the SQL table
-		// creation statement for the schema table.
+		// If we found primary key in the array of commands, we'll create the SQL for
+		// the key addition and append it to the SQL table creation statement for
+		// the schema table so the index is properly generated.
 		if ( ! is_null($primary))
 		{
 			$columns = $this->columnize($primary->columns);
@@ -57,18 +53,18 @@ class SQLite extends Grammar {
 	{
 		$columns = $this->columns($table);
 
-		// Once we have an array of all of the column definitions, we need to
-		// spin through each one and prepend "ADD COLUMN" to each of them,
-		// which is the syntax used by SQLite when adding columns.
+		// Once we the array of column definitions, we need to add "add" to the front
+		// of each definition, then we'll concatenate the definitions using commas
+		// like normal and generate the SQL.
 		$columns = array_map(function($column)
 		{
 			return 'ADD COLUMN '.$column;
 
 		}, $columns);
 
-		// SQLite only allows one column to be added in an ALTER statement,
-		// so we will create an array of statements and return them all to
-		// the schema manager, which will execute each one.
+		// SQLite only allows one column to be added in an ALTER statement, so we
+		// will create an array of statements and return them all to the schema
+		// manager, which will execute each one separately.
 		foreach ($columns as $column)
 		{
 			$sql[] = 'ALTER TABLE '.$this->wrap($table).' '.$column;

+ 18 - 18
laravel/database/schema/grammars/sqlserver.php

@@ -23,9 +23,9 @@ class SQLServer extends Grammar {
 	{
 		$columns = implode(', ', $this->columns($table));
 
-		// First we will generate the base table creation statement. Other than
-		// auto-incrementing keys, no indexes will be created during the first
-		// creation of the table. They will be added in separate commands.
+		// First we will generate the base table creation statement. Other than incrementing
+		// keys, no indexes will be created during the first creation of the table since
+		// they will be added in separate commands.
 		$sql = 'CREATE TABLE '.$this->wrap($table).' ('.$columns.')';
 
 		return $sql;
@@ -42,9 +42,9 @@ class SQLServer extends Grammar {
 	{
 		$columns = $this->columns($table);
 
-		// Once we the array of column definitions, we need to add "add"
-		// to the front of each definition, then we'll concatenate the
-		// definitions using commas like normal and generate the SQL.
+		// Once we the array of column definitions, we need to add "add" to the front
+		// of each definition, then we'll concatenate the definitions using commas
+		// like normal and generate the SQL.
 		$columns = implode(', ', array_map(function($column)
 		{
 			return 'ADD '.$column;
@@ -166,18 +166,18 @@ class SQLServer extends Grammar {
 	{
 		$columns = $this->columnize($command->columns);
 
-		// SQL Server requires the creation of a full-text "catalog" before
-		// creating a full-text index, so we'll first create the catalog
-		// then add another statement for the index. The catalog will
-		// be updated automatically by the server.
+		$table = $this->wrap($table);
+
+		// SQL Server requires the creation of a full-text "catalog" before creating
+		// a full-text index, so we'll first create the catalog then add another
+		// separate statement for the index.
 		$sql[] = "CREATE FULLTEXT CATALOG {$command->catalog}";
 
-		$create =  "CREATE FULLTEXT INDEX ON ".$this->wrap($table)." ({$columns}) ";
+		$create =  "CREATE FULLTEXT INDEX ON ".$table." ({$columns}) ";
 
-		// Full-text indexes must specify a unique, non-nullable column as
-		// the index "key" and this should have been created manually by
-		// the developer in a separate column addition command, so we
-		// can just specify it in this statement.
+		// Full-text indexes must specify a unique, non-null column as the index
+		// "key" and this should have been created manually by the developer in
+		// a separate column addition command.
 		$sql[] = $create .= "KEY INDEX {$command->key} ON {$command->catalog}";
 
 		return $sql;
@@ -235,9 +235,9 @@ class SQLServer extends Grammar {
 	{
 		$columns = array_map(array($this, 'wrap'), $command->columns);
 
-		// Once we the array of column names, we need to add "drop" to the
-		// front of each column, then we'll concatenate the columns using
-		// commas and generate the alter statement SQL.
+		// Once we the array of column names, we need to add "drop" to the front
+		// of each column, then we'll concatenate the columns using commas and
+		// generate the alter statement SQL.
 		$columns = implode(', ', array_map(function($column)
 		{
 			return 'DROP '.$column;

+ 12 - 5
laravel/error.php

@@ -25,9 +25,15 @@ class Error {
 				  <h3>Stack Trace:</h3>
 				  <pre>".$exception->getTraceAsString()."</pre></html>";
 		}
+
+		// If we're not using detailed error messages, we'll use the event
+		// system to get the response that should be sent to the browser.
+		// Using events gives the developer more freedom.
 		else
 		{
-			Response::error('500')->send();
+			$response = Event::first('500');
+
+			return Response::prepare($response)->send();
 		}
 
 		exit(1);
@@ -48,8 +54,7 @@ class Error {
 
 		// For a PHP error, we'll create an ErrorExcepetion and then feed that
 		// exception to the exception method, which will create a simple view
-		// of the exception details. The ErrorException class is built-in to
-		// PHP for converting native errors.
+		// of the exception details for the developer.
 		$exception = new \ErrorException($error, $code, 0, $file, $line);
 
 		if (in_array($code, Config::get('error.ignore')))
@@ -71,8 +76,10 @@ class Error {
 	{
 		// If a fatal error occured that we have not handled yet, we will
 		// create an ErrorException and feed it to the exception handler,
-		// as it will not have been handled by the error handler.
-		if ( ! is_null($error = error_get_last()))
+		// as it will not yet have been handled.
+		$error = error_get_last();
+
+		if ( ! is_null($error))
 		{
 			extract($error, EXTR_SKIP);
 

+ 20 - 0
laravel/event.php

@@ -40,6 +40,26 @@ class Event {
 		static::$events[$event][] = $callback;
 	}
 
+	/**
+	 * Fire an event and return the first response.
+	 *
+	 * <code>
+	 *		// Fire the "start" event
+	 *		$response = Event::first('start');
+	 *
+	 *		// Fire the "start" event passing an array of parameters
+	 *		$response = Event::first('start', array('Laravel', 'Framework'));
+	 * </code>
+	 *
+	 * @param  string  $event
+	 * @param  array   $parameters
+	 * @return mixed
+	 */
+	public static function first($event, $parameters = array())
+	{
+		return head(static::fire($event, $parameters));
+	}
+
 	/**
 	 * Fire an event so that all listeners are called.
 	 *

+ 16 - 4
laravel/helpers.php

@@ -90,14 +90,14 @@ function array_set(&$array, $key, $value)
 	// This loop allows us to dig down into the array to a dynamic depth by
 	// setting the array value for each level that we dig into. Once there
 	// is one key left, we can fall out of the loop and set the value as
-	// we should be at the proper depth within the array.
+	// we should be at the proper depth.
 	while (count($keys) > 1)
 	{
 		$key = array_shift($keys);
 
 		// If the key doesn't exist at this depth, we will just create an
 		// empty array to hold the next value, allowing us to create the
-		// arrays to hold the final value at the proper depth.
+		// arrays to hold the final value.
 		if ( ! isset($array[$key]) or ! is_array($array[$key]))
 		{
 			$array[$key] = array();
@@ -131,7 +131,7 @@ function array_forget(&$array, $key)
 	// This loop functions very similarly to the loop in the "set" method.
 	// We will iterate over the keys, setting the array value to the new
 	// depth at each iteration. Once there is only one key left, we will
-	// be at the proper depth in the array to "forget" the value.
+	// be at the proper depth in the array.
 	while (count($keys) > 1)
 	{
 		$key = array_shift($keys);
@@ -139,7 +139,7 @@ function array_forget(&$array, $key)
 		// Since this method is supposed to remove a value from the array,
 		// if a value higher up in the chain doesn't exist, there is no
 		// need to keep digging into the array, since it is impossible
-		// for the final value to even exist in the array.
+		// for the final value to even exist.
 		if ( ! isset($array[$key]) or ! is_array($array[$key]))
 		{
 			return;
@@ -339,6 +339,18 @@ function starts_with($haystack, $needle)
 	return strpos($haystack, $needle) === 0;
 }
 
+/**
+ * Determine if a given string ends with a given value.
+ *
+ * @param  string  $haystack
+ * @param  string  $needle
+ * @return bool
+ */
+function ends_with($haystack, $needle)
+{
+	return $needle == substr($haystack, strlen($haystack) - strlen($needle));
+}
+
 /**
  * Determine if a given string contains a given sub-string.
  *

+ 15 - 15
laravel/laravel.php

@@ -132,15 +132,25 @@ Input::$input = $input;
 Bundle::start(DEFAULT_BUNDLE);
 
 /**
- * Start all of the bundles that are specified in the configuration
- * array of auto-loaded bundles. This lets the developer have an
- * easy way to load bundles for every request.
+ * Auto-start any bundles configured to start on every request.
+ * This is especially useful for debug bundles or bundles that
+ * are used throughout the application.
  */
-foreach (Config::get('application.bundle.auto') as $bundle)
+foreach (Bundle::$bundles as $bundle => $config)
 {
-	Bundle::start($bundle);
+	if ($config['auto']) Bundle::start($bundle);
 }
 
+/**
+ * Register the "catch-all" route that handles 404 responses for
+ * routes that can not be matched to any other route within the
+ * application. We'll just raise the 404 event.
+ */
+Routing\Router::register('*', '(:all)', function()
+{
+	return Event::first('404');
+});
+
 /**
  * If the requset URI has too many segments, we will bomb out of
  * the request. This is too avoid potential DDoS attacks against
@@ -162,16 +172,6 @@ if (count(URI::$segments) > 15)
  */
 Request::$route = Routing\Router::route(Request::method(), $uri);
 
-if (is_null(Request::$route))
-{
-	Request::$route = new Routing\Route('GET /404', array(function()
-	{
-		return Response::error('404');
-	}));
-
-	$response = Response::error('404');
-}
-
 $response = Request::$route->call();
 
 /**

+ 7 - 16
laravel/redirect.php

@@ -35,6 +35,11 @@ class Redirect extends Response {
 		return static::to($url, $status, true);
 	}
 
+	public static function to_action($action, $parameters = array())
+	{
+		
+	}
+
 	/**
 	 * Create a redirect response to a named route.
 	 *
@@ -49,25 +54,11 @@ class Redirect extends Response {
 	 * @param  string    $route
 	 * @param  array     $parameters
 	 * @param  int       $status
-	 * @param  bool      $https
-	 * @return Redirect
-	 */
-	public static function to_route($route, $parameters = array(), $status = 302, $https = false)
-	{
-		return static::to(URL::to_route($route, $parameters, $https), $status);
-	}
-
-	/**
-	 * Create a redirect response to a named route using HTTPS.
-	 *
-	 * @param  string    $route
-	 * @param  array     $parameters
-	 * @param  int       $status
 	 * @return Redirect
 	 */
-	public static function to_secure_route($route, $parameters = array(), $status = 302)
+	public static function to_route($route, $parameters = array(), $status = 302)
 	{
-		return static::to_route($route, $parameters, $status, true);
+		return static::to(URL::to_route($route, $parameters), $status);
 	}
 
 	/**

+ 4 - 6
laravel/request.php

@@ -1,13 +1,11 @@
-<?php namespace Laravel;
-
-use Closure;
+<?php namespace Laravel; use Closure;
 
 class Request {
 
 	/**
-	 * The route handling the current request.
+	 * All of the route instances handling the request.
 	 *
-	 * @var Routing\Route
+	 * @var array
 	 */
 	public static $route;
 
@@ -139,7 +137,7 @@ class Request {
 	}
 
 	/**
-	 * Get the route handling the current request.
+	 * Get the main route handling the request.
 	 *
 	 * @return Route
 	 */

+ 42 - 22
laravel/routing/controller.php

@@ -3,6 +3,7 @@
 use Laravel\IoC;
 use Laravel\Str;
 use Laravel\View;
+use Laravel\Event;
 use Laravel\Bundle;
 use Laravel\Request;
 use Laravel\Redirect;
@@ -17,6 +18,13 @@ abstract class Controller {
 	 */
 	public $layout;
 
+	/**
+	 * The bundle the controller belongs to.
+	 *
+	 * @var string
+	 */
+	public $bundle;
+
 	/**
 	 * Indicates if the controller uses RESTful routing.
 	 *
@@ -38,7 +46,7 @@ abstract class Controller {
 	 *		// Call the "show" method on the "user" controller
 	 *		$response = Controller::call('user@show');
 	 *
-	 *		// Call the "profile" method on the "user/admin" controller and pass parameters
+	 *		// Call the "user/admin" controller and pass parameters
 	 *		$response = Controller::call('user.admin@profile', array($username));
 	 * </code>
 	 *
@@ -48,47 +56,52 @@ abstract class Controller {
 	 */
 	public static function call($destination, $parameters = array())
 	{
+		static::references($destination, $parameters);
+
 		list($bundle, $destination) = Bundle::parse($destination);
 
 		// We will always start the bundle, just in case the developer is pointing
 		// a route to another bundle. This allows us to lazy load the bundle and
-		// improve performance since the bundle is not loaded on every request.
+		// improve speed since the bundle is not loaded on every request.
 		Bundle::start($bundle);
 
 		list($controller, $method) = explode('@', $destination);
 
-		list($method, $parameters) = static::backreference($method, $parameters);
-
 		$controller = static::resolve($bundle, $controller);
 
 		// If the controller could not be resolved, we're out of options and
 		// will return the 404 error response. If we found the controller,
 		// we can execute the requested method on the instance.
-		if (is_null($controller)) return Response::error('404');
+		if (is_null($controller))
+		{
+			return Event::first('404');
+		}
 
 		return $controller->execute($method, $parameters);
 	}
 
 	/**
-	 * Replace all back-references on the given method.
+	 * Replace all back-references on the given destination.
 	 *
-	 * @param  string  $method
+	 * @param  string  $destination
 	 * @param  array   $parameters
 	 * @return array
 	 */
-	protected static function backreference($method, $parameters)
+	protected static function references(&$destination, &$parameters)
 	{
 		// Controller delegates may use back-references to the action parameters,
 		// which allows the developer to setup more flexible routes to various
-		// controllers with much less code than usual.
+		// controllers with much less code than would be usual.
 		foreach ($parameters as $key => $value)
 		{
-			$method = str_replace('(:'.($key + 1).')', $value, $method, $count);
+			$search = '(:'.($key + 1).')';
+
+			$destination = str_replace($search, $value, $destination, $count);
 
 			if ($count > 0) unset($parameters[$key]);
 		}
 
-		return array(str_replace('(:1)', 'index', $method), $parameters);
+		return array($destination, $parameters);
 	}
 
 	/**
@@ -100,18 +113,23 @@ abstract class Controller {
 	 */
 	public static function resolve($bundle, $controller)
 	{
-		if ( ! static::load($bundle, $controller)) return;
+		$identifier = Bundle::identifier($bundle, $controller);
 
 		// If the controller is registered in the IoC container, we will resolve
 		// it out of the container. Using constructor injection on controllers
-		// via the container allows more flexible and testable applications.
-		$resolver = 'controller: '.Bundle::identifier($bundle, $controller);
+		// via the container allows more flexible applications.
+		$resolver = 'controller: '.$identifier;
 
 		if (IoC::registered($resolver))
 		{
 			return IoC::resolve($resolver);
 		}
 
+		// If we couldn't resolve the controller out of the IoC container we'll
+		// format the controller name into its proper class name and load it
+		// by convention out of the bundle's controller directory.
+		if ( ! static::load($bundle, $controller)) return;
+
 		$controller = static::format($bundle, $controller);
 
 		$controller = new $controller;
@@ -169,11 +187,12 @@ abstract class Controller {
 	 */
 	public function execute($method, $parameters = array())
 	{
+		$filters = $this->filters('before', $method);
+
 		// Again, as was the case with route closures, if the controller "before"
 		// filters return a response, it will be considered the response to the
-		// request and the controller method will not be used to handle the
-		// request to the application.
-		$response = Filter::run($this->filters('before', $method), array(), true);
+		// request and the controller method will not be used .
+		$response = Filter::run($filters, array(), true);
 
 		if (is_null($response))
 		{
@@ -186,7 +205,7 @@ abstract class Controller {
 
 		// The "after" function on the controller is simply a convenient hook
 		// so the developer can work on the response before it's returned to
-		// the browser. This is useful for setting partials on the layout.
+		// the browser. This is useful for templating, etc.
 		$this->after($response);
 
 		Filter::run($this->filters('after', $method), array($response));
@@ -321,7 +340,7 @@ abstract class Controller {
 	 * Dynamically resolve items from the application IoC container.
 	 *
 	 * <code>
-	 *		// Retrieve an object registered in the container as "mailer"
+	 *		// Retrieve an object registered in the container
 	 *		$mailer = $this->mailer;
 	 *
 	 *		// Equivalent call using the IoC container instance
@@ -330,9 +349,10 @@ abstract class Controller {
 	 */
 	public function __get($key)
 	{
-		if (IoC::registered($key)) return IoC::resolve($key);
-
-		throw new \Exception("Accessing undefined property [$key] on controller.");
+		if (IoC::registered($key))
+		{
+			return IoC::resolve($key);
+		}
 	}
 
 }

+ 146 - 91
laravel/routing/route.php

@@ -2,23 +2,24 @@
 
 use Closure;
 use Laravel\Bundle;
+use Laravel\Request;
 use Laravel\Response;
 
 class Route {
 
 	/**
-	 * The route key, including request method and URI.
+	 * The URI the route response to.
 	 *
 	 * @var string
 	 */
-	public $key;
+	public $uri;
 
 	/**
-	 * The URI the route responds to.
+	 * The request method the route responds to.
 	 *
 	 * @var string
 	 */
-	public $uris;
+	public $method;
 
 	/**
 	 * The bundle in which the route was registered.
@@ -44,65 +45,72 @@ class Route {
 	/**
 	 * Create a new Route instance.
 	 *
-	 * @param  string   $key
+	 * @param  string   $method
+	 * @param  string   $uri
 	 * @param  array    $action
 	 * @param  array    $parameters
 	 * @return void
 	 */
-	public function __construct($key, $action, $parameters = array())
+	public function __construct($method, $uri, $action, $parameters = array())
 	{
-		$this->key = $key;
+		$this->uri = $uri;
+		$this->method = $method;
 		$this->action = $action;
 
-		// Extract each URI from the route key. Since the route key has the request
-		// method, we will extract that from the string. If the URI points to the
-		// root of the application, a single forward slash is returned.
-		$uris = array_get($action, 'handles', array($key));
-
-		$this->uris = array_map(array($this, 'destination'), $uris);
-
 		// Determine the bundle in which the route was registered. We will know
 		// the bundle by using the bundle::handles method, which will return
 		// the bundle assigned to that URI.
-		$this->bundle = Bundle::handles($this->uris[0]);
-
-		$defaults = array_get($action, 'defaults', array());
+		$this->bundle = Bundle::handles($uri);
 
-		$this->parameters = array_merge($parameters, $defaults);
-
-		// Once we have set the parameters and URIs, we'll transpose the route
-		// parameters onto the URIs so that the routes response naturally to
-		// the handles without the wildcards messing them up.
-		foreach ($this->uris as &$uri)
-		{
-			$uri = $this->transpose($uri, $this->parameters);
-		}
+		// We'll set the parameters based on the number of parameters passed
+		// compared to the parameters that were needed. If more parameters
+		// are needed, we'll merge in defaults.
+		$this->parameters($uri, $action, $parameters);
 	}
 
 	/**
-	 * Substitute the parameters in a given URI.
+	 * Set the parameters array to the correct value.
 	 *
 	 * @param  string  $uri
+	 * @param  array   $action
 	 * @param  array   $parameters
-	 * @return string
+	 * @return void
 	 */
-	public static function transpose($uri, $parameters)
+	protected function parameters($uri, $action, $parameters)
 	{
-		// Spin through each route parameter and replace the route wildcard segment
-		// with the corresponding parameter passed to the method. Afterwards, we'll
-		// replace all of the remaining optional URI segments.
-		foreach ((array) $parameters as $parameter)
+		$wildcards = 0;
+
+		$defaults = (array) array_get($action, 'defaults');
+
+		// We need to determine how many of the default paramters should be merged
+		// into the parameter array. First, we will count the number of wildcards
+		// in the route URI and then merge the defaults.
+		foreach (array_keys(Router::patterns()) as $wildcard)
 		{
-			if ( ! is_null($parameter))
-			{
-				$uri = preg_replace('/\(.+?\)/', $parameter, $uri, 1);
-			}
+			$wildcards += substr_count($uri, $wildcard);
 		}
 
-		// If there are any remaining optional place-holders, we'll just replace
-		// them with empty strings since not every optional parameter has to be
-		// in the array of parameters that were passed.
-		return str_replace(array_keys(Router::$optional), '', $uri);		
+		$needed = $wildcards - count($parameters);
+
+		// If there are less parameters than wildcards, we will figure out how
+		// many parameters we need to inject from the array of defaults and
+		// merge them in into the main array for the route.
+		if ($needed > 0)
+		{
+			$defaults = array_slice($defaults, count($defaults) - $needed);
+
+			$parameters = array_merge($parameters, $defaults);
+		}
+
+		// If the final number of parameters doesn't match the count of the
+		// wildcards, we'll pad parameter array with null to cover any of
+		// the default values that were forgotten.
+		if (count($parameters) !== $wildcards)
+		{
+			$parameters = array_pad($parameters, $wildcards, null);
+		}
+
+		$this->parameters = $parameters;
 	}
 
 	/**
@@ -141,19 +149,22 @@ class Route {
 	 */
 	public function response()
 	{
-		// If the action is a string, it is simply pointing the route to a
-		// controller action, and we can just call the action and return
-		// its response. This is the most basic form of route, and is
-		// the simplest to handle.
-		if ( ! is_null($delegate = $this->delegate()))
+		// If the action is a string, it is pointing the route to a controller
+		// action, and we can just call the action and return its response.
+		// We'll just pass the action off to the Controller class.
+		$delegate = $this->delegate();
+
+		if ( ! is_null($delegate))
 		{
 			return Controller::call($delegate, $this->parameters);
 		}
 
-		// If the route does not have a delegate, it should either be a
-		// Closure instance or have a Closure in its action array, so
-		// we will attempt to get the Closure and call it.
-		elseif ( ! is_null($handler = $this->handler()))
+		// If the route does not have a delegate, then it must be a Closure
+		// instance or have a Closure in its action array, so we will try
+		// to locate the Closure and call it directly.
+		$handler = $this->handler();
+
+		if ( ! is_null($handler))
 		{
 			return call_user_func_array($handler, $this->parameters);
 		}
@@ -162,21 +173,23 @@ class Route {
 	/**
 	 * Get the filters that are attached to the route for a given event.
 	 *
-	 * If the route belongs to a bundle, the bundle's global filters are returned too.
-	 *
 	 * @param  string  $event
 	 * @return array
 	 */
 	protected function filters($event)
 	{
-		$filters = array_unique(array($event, Bundle::prefix($this->bundle).$event));
+		$global = Bundle::prefix($this->bundle).$event;
+
+		$filters = array_unique(array($event, $global));
 
-		// Next wee will check to see if there are any filters attached
-		// for the given event. If there are, we'll merge them in with
-		// the global filters for the application event.
+		// Next we will check to see if there are any filters attached to
+		// the route for the given event. If there are, we'll merge them
+		// in with the global filters for the event.
 		if (isset($this->action[$event]))
 		{
-			$filters = array_merge($filters, Filter::parse($this->action[$event]));
+			$assigned = Filter::parse($this->action[$event]);
+
+			$filters = array_merge($filters, $assigned);
 		}
 
 		return array(new Filter_Collection($filters));
@@ -197,8 +210,6 @@ class Route {
 	/**
 	 * Get the anonymous function assigned to handle the route.
 	 *
-	 * If no anonymous function is assigned, null will be returned by the method.
-	 *
 	 * @return Closure
 	 */
 	protected function handler()
@@ -222,72 +233,116 @@ class Route {
 	 */
 	public function is($name)
 	{
-		return is_array($this->action) and array_get($this->action, 'name') === $name;
+		return array_get($this->action, 'name') === $name;
 	}
 
 	/**
-	 * Determine if the route handles a given URI.
+	 * Register a controller with the router.
 	 *
-	 * @param  string  $uri
-	 * @return bool
+	 * @param  string|array  $controller
+	 * @param  string|array  $defaults
+	 * @return void
 	 */
-	public function handles($uri)
+	public static function controller($controllers, $defaults = 'index')
 	{
-		$pattern = ($uri !== '/') ? str_replace('*', '(.*)', $uri).'\z' : '^/$';
+		Router::controller($controllers, $defaults);
+	}
 
-		return ! is_null(array_first($this->uris, function($key, $uri) use ($pattern)
-		{
-			return preg_match('#'.$pattern.'#', $uri);
-		}));
+	/**
+	 * Register a secure controller with the router.
+	 *
+	 * @param  string|array  $controllers
+	 * @param  string|array  $defaults
+	 * @return void
+	 */
+	public static function secure_controller($controllers, $defaults = 'index')
+	{
+		Router::controller($controllers, $defaults, true);
 	}
 
 	/**
-	 * Register a route with the router.
+	 * Register a GET route with the router.
 	 *
-	 * <code>
-	 *		// Register a route with the router
-	 *		Router::register('GET /', function() {return 'Home!';});
+	 * @param  string|array  $route
+	 * @param  mixed         $action
+	 * @return void
+	 */
+	public static function get($route, $action)
+	{
+		Router::register('GET', $route, $action);
+	}
+
+	/**
+	 * Register a POST route with the router.
 	 *
-	 *		// Register a route that handles multiple URIs with the router
-	 *		Router::register(array('GET /', 'GET /home'), function() {return 'Home!';});
-	 * </code>
+	 * @param  string|array  $route
+	 * @param  mixed         $action
+	 * @return void
+	 */
+	public static function post($route, $action)
+	{
+		Router::register('POST', $route, $action);
+	}
+
+	/**
+	 * Register a PUT route with the router.
 	 *
 	 * @param  string|array  $route
 	 * @param  mixed         $action
-	 * @param  bool          $https
 	 * @return void
 	 */
-	public static function to($route, $action)
+	public static function put($route, $action)
 	{
-		Router::register($route, $action);
+		Router::register('PUT', $route, $action);
 	}
 
 	/**
-	 * Register a HTTPS route with the router.
+	 * Register a DELETE route with the router.
 	 *
 	 * @param  string|array  $route
 	 * @param  mixed         $action
 	 * @return void
 	 */
-	public static function secure($route, $action)
+	public static function delete($route, $action)
 	{
-		Router::secure($route, $action);
+		Router::register('DELETE', $route, $action);
 	}
 
 	/**
-	 * Extract the URI string from a route destination.
+	 * Register a route that handles any request method.
 	 *
-	 * <code>
-	 *		// Returns "home/index" as the destination's URI
-	 *		$uri = Route::uri('GET /home/index');
-	 * </code>
+	 * @param  string|array  $route
+	 * @param  mixed         $action
+	 * @return void
+	 */
+	public static function any($route, $action)
+	{
+		Router::register('*', $route, $action);
+	}
+
+	/**
+	 * Register a group of routes that share attributes.
 	 *
-	 * @param  string  $destination
-	 * @return string
+	 * @param  array    $attributes
+	 * @param  Closure  $callback
+	 * @return void
+	 */
+	public static function group($attributes, Closure $callback)
+	{
+		Router::group($attributes, $callback);
+	}
+
+	/**
+	 * Register a HTTPS route with the router.
+	 *
+	 * @param  string        $method
+	 * @param  string|array  $route
+	 * @param  mixed         $action
+	 * @return void
 	 */
-	public static function destination($destination)
+	public static function secure($method, $route, $action)
 	{
-		return trim(substr($destination, strpos($destination, '/')), '/') ?: '/';
+		Router::secure($method, $route, $action);
 	}
 
 }

+ 285 - 199
laravel/routing/router.php

@@ -1,7 +1,26 @@
-<?php namespace Laravel\Routing; use Closure, Laravel\Str, Laravel\Bundle;
+<?php namespace Laravel\Routing;
+
+use Closure;
+use Laravel\Str;
+use Laravel\Bundle;
+use Laravel\Request;
 
 class Router {
 
+	/**
+	 * 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();
+
 	/**
 	 * All of the routes that have been registered.
 	 *
@@ -17,18 +36,23 @@ class Router {
 	public static $fallback = array();
 
 	/**
-	 * All of the route names that have been matched with URIs.
+	 * The current attributes being shared by routes.
+	 */
+	public static $group;
+
+	/**
+	 * The "handes" clause for the bundle currently being routed.
 	 *
-	 * @var array
+	 * @var string
 	 */
-	public static $names = array();
+	public static $bundle;
 
 	/**
-	 * The actions that have been reverse routed.
+	 * The number of URI segments allowed as method arguments.
 	 *
-	 * @var array
+	 * @var int
 	 */
-	public static $uses = array();
+	public static $segments = 5;
 
 	/**
 	 * The wildcard patterns supported by the router.
@@ -38,6 +62,7 @@ class Router {
 	public static $patterns = array(
 		'(:num)' => '([0-9]+)',
 		'(:any)' => '([a-zA-Z0-9\.\-_%]+)',
+		'(:all)' => '(.*)',
 	);
 
 	/**
@@ -48,6 +73,7 @@ class Router {
 	public static $optional = array(
 		'/(:num?)' => '(?:/([0-9]+)',
 		'/(:any?)' => '(?:/([a-zA-Z0-9\.\-_%]+)',
+		'/(:all?)' => '(?:/(.*)',
 	);
 
 	/**
@@ -60,13 +86,40 @@ class Router {
 	/**
 	 * Register a HTTPS route with the router.
 	 *
+	 * @param  string        $method
 	 * @param  string|array  $route
 	 * @param  mixed         $action
 	 * @return void
 	 */
-	public static function secure($route, $action)
+	public static function secure($method, $route, $action)
 	{
-		static::register($route, $action, true);
+		$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;
 	}
 
 	/**
@@ -74,18 +127,18 @@ class Router {
 	 *
 	 * <code>
 	 *		// Register a route with the router
-	 *		Router::register('GET /', function() {return 'Home!';});
+	 *		Router::register('GET' ,'/', function() {return 'Home!';});
 	 *
 	 *		// Register a route that handles multiple URIs with the router
-	 *		Router::register(array('GET /', 'GET /home'), function() {return 'Home!';});
+	 *		Router::register(array('GET', '/', 'GET /home'), function() {return 'Home!';});
 	 * </code>
 	 *
+	 * @param  string        $method
 	 * @param  string|array  $route
 	 * @param  mixed         $action
-	 * @param  bool          $https
 	 * @return void
 	 */
-	public static function register($route, $action, $https = false)
+	public static function register($method, $route, $action)
 	{
 		if (is_string($route)) $route = explode(', ', $route);
 
@@ -94,18 +147,23 @@ class Router {
 			// 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.
-			if (starts_with($uri, '*'))
+			if ($method == '*')
 			{
-				static::universal(substr($uri, 2), $action);
+				foreach (static::$methods as $method)
+				{
+					static::register($method, $route, $action);
+				}
 
 				continue;
 			}
 
+			$uri = str_replace('(:bundle)', static::$bundle, $uri);
+
 			// 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.
-			if (str_contains($uri, ' /('))
+			if ($uri[0] == '(')
 			{
 				$routes =& static::$fallback;
 			}
@@ -114,60 +172,155 @@ class Router {
 				$routes =& static::$routes;
 			}
 
-			// 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 when the route is
-			// executed by the application.
-			if (is_string($action))
+			// 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))
 			{
-				$routes[$uri]['uses'] = $action;
+				$routes[$method][$uri] = $action;
 			}
-			// If the action is not a string, we can just simply cast it as an
-			// array, then we will add all of the URIs to the action array as
-			// the "handes" clause so we can easily check which URIs are
-			// handled by the route instance.
 			else
 			{
-				if ($action instanceof Closure) $action = array($action);
-
-				$routes[$uri] = (array) $action;
+				$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;
 			}
 
-			// If the HTTPS option is not set on the action, we will use the
-			// value given to the method. The "secure" method passes in the
-			// HTTPS value in as a parameter short-cut, just so the dev
-			// doesn't always have to add it to an array.
-			if ( ! isset($routes[$uri]['https']))
+			// 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']))
 			{
-				$routes[$uri]['https'] = $https;
+				$routes[$method][$uri]['https'] = false;
 			}
+		}
+	}
 
-			$routes[$uri]['handles'] = (array) $route;
+	/**
+	 * 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);
 		}
+		// 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;
 	}
 
 	/**
-	 * Register a route for all HTTP verbs.
+	 * Register a secure controller with the router.
 	 *
-	 * @param  string  $route
-	 * @param  mixed   $action
+	 * @param  string|array  $controllers
+	 * @param  string|array  $defaults
 	 * @return void
 	 */
-	protected static function universal($route, $action)
+	public static function secure_controller($controllers, $defaults = 'index')
 	{
-		$count = count(static::$methods);
+		static::controller($controllers, $defaults, true);
+	}
 
-		$routes = array_fill(0, $count, $route);
+	/**
+	 * 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}", '/');
 
-		// When registering a universal route, we'll iterate through all of the
-		// verbs supported by the router and prepend each one of the URIs with
-		// one of the request verbs, then we'll register the routes.
-		for ($i = 0; $i < $count; $i++)
+			// 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
 		{
-			$routes[$i] = static::$methods[$i].' '.$routes[$i];
+			$home = '';
 		}
 
-		static::register($routes, $action);
+		// 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, '/') ?: '/';
+
+		$attributes = array('uses' => "{$identifier}@(:1)", 'defaults' => 'index');
+
+		static::register('*', $pattern, $attributes);
 	}
 
 	/**
@@ -182,7 +335,7 @@ class Router {
 
 		// 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
-		// the bundle that are installed for the application.
+		// the bundles that are installed for the application.
 		if (count(static::$names) == 0)
 		{
 			foreach (Bundle::names() as $bundle)
@@ -193,10 +346,10 @@ class Router {
 
 		// 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
-		// load them very quickly if we need to find them a second time.
-		foreach (static::routes() as $key => $value)
+		// load them very quickly the next time.
+		foreach (static::all() as $key => $value)
 		{
-			if (isset($value['name']) and $value['name'] == $name)
+			if (array_get($value, 'name') === $name)
 			{
 				return static::$names[$name] = array($key => $value);
 			}
@@ -204,35 +357,31 @@ class Router {
 	}
 
 	/**
-	 * Find the route that uses the given action and method.
+	 * Find the route that uses the given action.
 	 *
 	 * @param  string  $action
-	 * @param  string  $method
 	 * @return array
 	 */
-	public static function uses($action, $method = 'GET')
+	public static function uses($action)
 	{
 		// 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.
-		if (isset(static::$uses[$method.$action]))
+		if (isset(static::$uses[$action]))
 		{
-			return static::$uses[$method.$action];
+			return static::$uses[$action];
 		}
 
 		Bundle::routes(Bundle::name($action));
 
-		foreach (static::routes() as $uri => $route)
+		// 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)
 		{
-			// To find the route, we'll simply spin through the routes looking
-			// for a route with a "uses" key matching the action, then we'll
-			// check the request method for a match.
-			if (isset($route['uses']) and $route['uses'] == $action)
+			if (array_get($route, 'uses') == $action)
 			{
-				if (starts_with($uri, $method))
-				{
-					return static::$uses[$method.$action] = array($uri => $route);
-				}
+				return static::$uses[$action] = array($uri => $route);
 			}
 		}
 	}
@@ -246,195 +395,132 @@ class Router {
 	 */
 	public static function route($method, $uri)
 	{
-		// First we will make sure the bundle that handles the given URI has
-		// been started for the current request. Bundles may handle any URI
-		// as long as it begins with the string in the "handles" item of
-		// the bundle's registration array.
 		Bundle::start($bundle = Bundle::handles($uri));
 
-		// All route URIs begin with the request method and have a leading
-		// slash before the URI. We'll put the request method and URI in
-		// that format so we can easily check for literal matches.
-		$destination = $method.' /'.trim($uri, '/');
-
-		if (array_key_exists($destination, static::$routes))
+		// Of course literal route matches are the quickest to find, so we will
+		// check for those first. If the destination key exists in teh routes
+		// array we can just return that route now.
+		if (array_key_exists($uri, static::$routes[$method]))
 		{
-			return new Route($destination, static::$routes[$destination], array());
-		}
+			$action = static::$routes[$method][$uri];
 
-		// If we can't find a literal match, we'll iterate through all of
-		// the registered routes to find a matching route that uses some
-		// regular expressions or wildcards.
-		if ( ! is_null($route = static::match($destination)))
-		{
-			return $route;
+			return new Route($method, $uri, $action);
 		}
 
-		// If the bundle handling the request is not the default bundle,
-		// we want to remove the root "handles" string from the URI so
-		// it will not interfere with searching for a controller.
-		//
-		// If we left it on the URI, the root controller for the bundle
-		// would need to be nested in directories matching the clause.
-		// This will not intefere with the Route::handles method
-		// as the destination is used to set the route's URIs.
-		if ($bundle !== DEFAULT_BUNDLE)
+		// 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)))
 		{
-			$uri = str_replace(Bundle::option($bundle, 'handles'), '', $uri);
-
-			$uri = ltrim($uri, '/');
+			return $route;
 		}
-
-		$segments = Str::segments($uri);
-
-		return static::controller($bundle, $method, $destination, $segments);
 	}
 
 	/**
 	 * Iterate through every route to find a matching route.
 	 *
-	 * @param  string  $destination
+	 * @param  string  $method
+	 * @param  string  $uri
 	 * @return Route
 	 */
-	protected static function match($destination)
+	protected static function match($method, $uri)
 	{
-		foreach (static::routes() as $route => $action)
+		foreach (static::routes($method) as $route => $action)
 		{
-			// We only need to check routes with regular expressions since
-			// all other routes would have been able to be caught by the
-			// check for literal matches we just did.
-			if (strpos($route, '(') !== false)
+			// 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, '('))
 			{
 				$pattern = '#^'.static::wildcards($route).'$#';
 
-				// 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.
-				if (preg_match($pattern, $destination, $parameters))
+				// 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))
 				{
-					return new Route($route, $action, array_slice($parameters, 1));
+					return new Route($method, $route, $action, array_slice($parameters, 1));
 				}
 			}
 		}
 	}
 
 	/**
-	 * Attempt to find a controller for the incoming request.
+	 * Translate route URI wildcards into regular expressions.
 	 *
-	 * @param  string  $bundle
-	 * @param  string  $method
-	 * @param  string  $destination
-	 * @param  array   $segments
-	 * @return Route
+	 * @param  string  $key
+	 * @return string
 	 */
-	protected static function controller($bundle, $method, $destination, $segments)
+	protected static function wildcards($key)
 	{
-		if (count($segments) == 0)
-		{
-			$uri = '/';
-
-			// If the bundle is not the default bundle for the application, we'll
-			// set the root URI as the root URI registered with the bundle in the
-			// bundle configuration file for the application. It's registered in
-			// the bundle configuration using the "handles" clause.
-			if ($bundle !== DEFAULT_BUNDLE)
-			{
-				$uri = '/'.Bundle::get($bundle)->handles;
-			}
-
-			// We'll generate a default "uses" clause for the route action that
-			// points to the default controller and method for the bundle so
-			// that the route will execute the default.
-			$action = array('uses' => Bundle::prefix($bundle).'home@index');
-
-			return new Route($method.' '.$uri, $action);
-		}
+		list($search, $replace) = array_divide(static::$optional);
 
-		$directory = Bundle::path($bundle).'controllers/';
+		// 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);
 
-		if ( ! is_null($key = static::locate($segments, $directory)))
+		if ($count > 0)
 		{
-			// First, we'll extract the controller name, then, since we need
-			// to extract the method and parameters, we will remove the name
-			// of the controller from the URI. Then we can shift the method
-			// off of the array of segments. Any remaining segments are the
-			// parameters for the method.
-			$controller = implode('.', array_slice($segments, 0, $key));
-
-			$segments = array_slice($segments, $key);
-
-			$method = (count($segments) > 0) ? array_shift($segments) : 'index';
-
-			// We need to grab the prefix to the bundle so we can prefix
-			// the route identifier with it. This informs the controller
-			// class out of which bundle the controller instance should
-			// be resolved when it is needed by the app.
-			$prefix = Bundle::prefix($bundle);
-
-			$action = array('uses' => $prefix.$controller.'@'.$method);
-
-			return new Route($destination, $action, $segments);
+			$key .= str_repeat(')?', $count);
 		}
+
+		return strtr($key, static::$patterns);
 	}
 
 	/**
-	 * Locate the URI segment matching a controller name.
+	 * Get all of the routes across all request methods.
 	 *
-	 * @param  array   $segments
-	 * @param  string  $directory
-	 * @return int
+	 * @return array
 	 */
-	protected static function locate($segments, $directory)
+	public static function all()
 	{
-		for ($i = count($segments) - 1; $i >= 0; $i--)
-		{
-			// To find the proper controller, we need to iterate backwards through
-			// the URI segments and take the first file that matches. That file
-			// should be the deepest possible controller matched by the URI.
-			if (file_exists($directory.implode('/', $segments).EXT))
-			{
-				return $i + 1;
-			}
+		$all = array();
 
-			// If a controller did not exist for the segments, we will pop
-			// the last segment off of the array so that on the next run
-			// through the loop we'll check one folder up from the one
-			// we checked on this iteration.
-			array_pop($segments);
+		// 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));
 		}
+
+		return $all;
 	}
 
 	/**
-	 * Translate route URI wildcards into regular expressions.
+	 * Get all of the registered routes, with fallbacks at the end.
 	 *
-	 * @param  string  $key
-	 * @return string
+	 * @param  string  $method
+	 * @return array
 	 */
-	protected static function wildcards($key)
+	public static function routes($method = null)
 	{
-		list($search, $replace) = array_divide(static::$optional);
+		$routes = array_get(static::$routes, $method, array());
 
-		// For optional parameters, first translate the wildcards to their
-		// regex equivalent, sans the ")?" ending. We'll add the endings
-		// back on after we know the replacement count.
-		$key = str_replace($search, $replace, $key, $count);
-
-		if ($count > 0)
-		{
-			$key .= str_repeat(')?', $count);
-		}
-
-		return strtr($key, static::$patterns);
+		return array_merge($routes, array_get(static::$fallback, $method, array()));
 	}
 
 	/**
-	 * Get all of the registered routes, with fallbacks at the end.
+	 * Get all of the wildcard patterns
 	 *
 	 * @return array
 	 */
-	public static function routes()
+	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)
 	{
-		return array_merge(static::$routes, static::$fallback);
+		return implode('/', array_fill(0, $times, $pattern));
 	}
 
 }

+ 54 - 8
laravel/uri.php

@@ -21,7 +21,20 @@ class URI {
 	 *
 	 * @var array
 	 */
-	protected static $attempt = array('PATH_INFO', 'REQUEST_URI', 'PHP_SELF', 'REDIRECT_URL');
+	protected static $attempt = array(
+		'PATH_INFO', 'REQUEST_URI',
+		'PHP_SELF', 'REDIRECT_URL'
+	);
+
+	/**
+	 * Get the full URI including the query string.
+	 *
+	 * @return string
+	 */
+	public static function full()
+	{
+		return static::current().static::query();
+	}
 
 	/**
 	 * Get the URI for the current request.
@@ -39,7 +52,7 @@ class URI {
 
 		// If you ever encounter this error, please inform the nerdy Laravel
 		// dev team with information about your server. We want to support
-		// Laravel an as many server environments as possible!
+		// Laravel an as many servers as we possibly can!
 		if (is_null(static::$uri))
 		{
 			throw new \Exception("Could not detect request URI.");
@@ -81,16 +94,16 @@ class URI {
 	 */
 	protected static function format($uri)
 	{
-		// First we want to remove the application's base URL from the URI
-		// if it is in the string. It is possible for some of the server
-		// variables to include the entire document root.
+		// First we want to remove the application's base URL from the URI if it is
+		// in the string. It is possible for some of the parsed server variables to
+		// include the entire document root in the string.
 		$uri = static::remove_base($uri);
 
 		$index = '/'.Config::get('application.index');
 
-		// Next we'll remove the index file from the URI if it is there
-		// and then finally trim down the URI. If the URI is left with
-		// nothing but spaces, we use a single slash for root.
+		// Next we'll remove the index file from the URI if it is there and then
+		// finally trim down the URI. If the URI is left with spaces, we'll use
+		// a single slash for the root URI.
 		if ($index !== '/')
 		{
 			$uri = static::remove($uri, $index);
@@ -99,6 +112,29 @@ class URI {
 		return trim($uri, '/') ?: '/';
 	}
 
+	/**
+	 * Determine if the current URI matches a given pattern.
+	 *
+	 * @param  string  $pattern
+	 * @return bool
+	 */
+	public static function is($pattern)
+	{
+		// Asterisks are translated into zero-or-more regular expression wildcards
+		// to make it convenient to check if the URI starts with a given pattern
+		// such as "library/*". This is only done when not root.
+		if ($pattern !== '/')
+		{
+			$pattern = str_replace('*', '(.*)', $pattern).'\z';
+		}
+		else
+		{
+			$pattern = '^/$';
+		}
+
+		return preg_match('#'.$pattern.'#', static::current());
+	}
+
 	/**
 	 * Parse the PATH_INFO server variable.
 	 *
@@ -201,4 +237,14 @@ class URI {
 		return (strpos($uri, $value) === 0) ? substr($uri, strlen($value)) : $uri;
 	}
 
+	/**
+	 * Get the query string for the current request.
+	 *
+	 * @return string
+	 */
+	protected static function query()
+	{
+		return (count((array) $_GET) > 0) ? '?'.http_build_query($_GET) : '';
+	}
+
 }

+ 65 - 36
laravel/url.php

@@ -1,4 +1,4 @@
-<?php namespace Laravel; use Laravel\Routing\Route, Laravel\Routing\Router;
+<?php namespace Laravel; use Laravel\Routing\Router, Laravel\Routing\Route;
 
 class URL {
 
@@ -9,6 +9,16 @@ class URL {
 	 */
 	public static $base;
 
+	/**
+	 * Get the full URI including the query string.
+	 *
+	 * @return string
+	 */
+	public static function full()
+	{
+		return static::to(URI::full());
+	}
+
 	/**
 	 * Get the full URL for the current request.
 	 *
@@ -30,9 +40,9 @@ class URL {
 
 		$base = 'http://localhost';
 
-		// If the application URL configuration is set, we will just use
-		// that instead of trying to guess the URL based on the $_SERVER
-		// array's host and script name.
+		// If the application URL configuration is set, we will just use that
+		// instead of trying to guess the URL from the $_SERVER array's host
+		// and script variables as this is more reliable.
 		if (($url = Config::get('application.url')) !== '')
 		{
 			$base = $url;
@@ -43,10 +53,14 @@ class URL {
 
 			// Basically, by removing the basename, we are removing everything after the
 			// and including the front controller from the request URI. Leaving us with
-			// the path in which the framework is installed. From that path, we can
-			// construct the base URL to the application.
-			$path = str_replace(basename($_SERVER['SCRIPT_NAME']), '', $_SERVER['SCRIPT_NAME']);
+			// the path in which the framework is installed.
+			$script = $_SERVER['SCRIPT_NAME'];
 
+			$path = str_replace(basename($script), '', $script);
+
+			// Now that we have the base URL, all we need to do is attach the protocol
+			// and the HTTP_HOST to build the full URL for the application. We also
+			// trim off trailing slashes to clean the URL.
 			$base = rtrim($protocol.$_SERVER['HTTP_HOST'].$path, '/');
 		}
 
@@ -109,41 +123,26 @@ class URL {
 	 *
 	 * @param  string  $action
 	 * @param  array   $parameters
-	 * @param  string  $method
 	 * @return string
 	 */
-	public static function to_action($action, $parameters = array(), $method = 'GET')
+	public static function to_action($action, $parameters = array())
 	{
 		// This allows us to use true reverse routing to controllers, since
 		// URIs may be setup to handle the action that do not follow the
-		// typical Laravel controller URI convention.
-		$route = Router::uses($action, $method);
+		// typical Laravel controller URI conventions.
+		$route = Router::uses($action);
 
 		if ( ! is_null($route))
 		{
-			$uri = static::explicit($route, $action, $parameters);
+			return static::explicit($route, $action, $parameters);
 		}
 		// If no route was found that handled the given action, we'll just
 		// generate the URL using the typical controller routing setup
 		// for URIs and turn SSL to false.
 		else
 		{
-			$uri = static::convention($action, $parameters);
+			return static::convention($action, $parameters);
 		}
-
-		return static::to($uri, $https);
-	}
-
-	/**
-	 * Generate a HTTPS URL to a controller action.
-	 *
-	 * @param  string  $action
-	 * @param  array   $parameters
-	 * @return string
-	 */
-	public static function to_post_action($action, $parameters = array())
-	{
-		return static::to_action($action, $parameters, 'POST');
 	}
 
 	/**
@@ -158,7 +157,7 @@ class URL {
 	{
 		$https = array_get(current($route), 'https', false);
 
-		return Route::transpose(Route::destination(key($route)), $parameters);
+		return static::to(static::transpose(key($route), $parameters), $https);
 	}
 
 	/**
@@ -181,12 +180,16 @@ class URL {
 
 		$https = false;
 
+		$parameters = implode('/', $parameters);
+
 		// We'll replace both dots and @ signs in the URI since both are used
 		// to specify the controller and action, and by convention should be
 		// translated into URI slashes.
-		$uri = $root.str_replace(array('.', '@'), '/', $action);
+		$uri = $root.'/'.str_replace(array('.', '@'), '/', $action);
 
-		return str_finish($uri, '/').implode('/', $parameters);
+		$uri = static::to(str_finish($uri, '/').$parameters);
+
+		return trim($uri, '/');
 	}
 
 	/**
@@ -204,7 +207,7 @@ class URL {
 
 		// Since assets are not served by Laravel, we do not need to come through
 		// the front controller. So, we'll remove the application index specified
-		// in the application configuration from the URL.
+		// in the application config from the generated URL.
 		if (($index = Config::get('application.index')) !== '')
 		{
 			$url = str_replace($index.'/', '', $url);
@@ -236,14 +239,40 @@ class URL {
 			throw new \Exception("Error creating URL for undefined route [$name].");
 		}
 
-		$uri = Route::destination(key($route));
-
 		// To determine whether the URL should be HTTPS or not, we look for the "https"
-		// value on the route action array. The route has control over whether the
-		// URL should be generated with an HTTPS protocol.
+		// value on the route action array. The route has control over whether the URL
+		// should be generated with an HTTPS protocol string or just HTTP.
 		$https = array_get(current($route), 'https', false);
 
-		return static::to(Route::transpose($uri, $parameters), $https);
+		return static::to(static::transpose(key($route), $parameters), $https);
+	}
+
+	/**
+	 * Substitute the parameters in a given URI.
+	 *
+	 * @param  string  $uri
+	 * @param  array   $parameters
+	 * @return string
+	 */
+	public static function transpose($uri, $parameters)
+	{
+		// Spin through each route parameter and replace the route wildcard segment
+		// with the corresponding parameter passed to the method. Afterwards, we'll
+		// replace all of the remaining optional URI segments.
+		foreach ((array) $parameters as $parameter)
+		{
+			if ( ! is_null($parameter))
+			{
+				$uri = preg_replace('/\(.+?\)/', $parameter, $uri, 1);
+			}
+		}
+
+		// If there are any remaining optional place-holders, we'll just replace
+		// them with empty strings since not every optional parameter has to be
+		// in the array of parameters that were passed.
+		$uri = str_replace(array_keys(Router::$optional), '', $uri);
+
+		return trim($uri, '/');
 	}
 
 }

+ 2 - 2
laravel/validator.php

@@ -853,11 +853,11 @@ class Validator {
 
 		// More reader friendly versions of the attribute names may be stored
 		// in the validation language file, allowing a more readable version
-		// of the attribute name to be used in the validation message.
+		// of the attribute name to be used in the message.
 		//
 		// If no language line has been specified for the attribute, all of
 		// the underscores will be removed from the attribute name and that
-		// will be used as the attribtue name in the message.
+		// will be used as the attribtue name.
 		$line = "{$bundle}validation.attributes.{$attribute}";
 
 		$display = Lang::line($line)->get($this->language);

+ 1 - 1
laravel/view.php

@@ -67,7 +67,7 @@ class View implements ArrayAccess {
 		//
 		// This makes error display in the view extremely convenient, since the
 		// developer can always assume they have a message container instance
-		// available to them in the view.
+		// available to them in the view's variables.
 		if ( ! isset($this->data['errors']))
 		{
 			if (Session::started() and Session::has('errors'))

+ 2 - 1
storage/cache/.gitignore

@@ -1 +1,2 @@
-laravel.bundle.manifest
+*
+!.gitignore