diff --git a/application/config/application.php b/application/config/application.php index d91711c2..1aa3abd7 100644 --- a/application/config/application.php +++ b/application/config/application.php @@ -131,7 +131,6 @@ */ 'aliases' => array( - 'Arr' => 'Laravel\\Arr', 'Asset' => 'Laravel\\Asset', 'Auth' => 'Laravel\\Security\\Auth', 'Benchmark' => 'Laravel\\Benchmark', @@ -150,12 +149,9 @@ 'Input' => 'Laravel\\Input', 'IoC' => 'Laravel\\IoC', 'Lang' => 'Laravel\\Lang', - 'Loader' => 'Laravel\\Loader', - 'Messages' => 'Laravel\\Validation\\Messages', - 'Package' => 'Laravel\\Facades\\Package', - 'URI' => 'Laravel\\URI', 'URL' => 'Laravel\\URL', 'Redirect' => 'Laravel\\Redirect', + 'Redis' => 'Laravel\\Redis', 'Request' => 'Laravel\\Request', 'Response' => 'Laravel\\Response', 'Session' => 'Laravel\\Session\\Manager', diff --git a/application/config/cache.php b/application/config/cache.php index c0435988..abd07eec 100644 --- a/application/config/cache.php +++ b/application/config/cache.php @@ -12,7 +12,7 @@ | Caching can be used to increase the performance of your application | by storing commonly accessed data in memory or in a file. | - | Supported Drivers: 'file', 'memcached', 'apc'. + | Supported Drivers: 'file', 'memcached', 'apc', 'redis'. | */ diff --git a/application/config/database.php b/application/config/database.php index d8109118..ebcac4d9 100644 --- a/application/config/database.php +++ b/application/config/database.php @@ -70,4 +70,25 @@ ), + /* + |-------------------------------------------------------------------------- + | Redis Databases + |-------------------------------------------------------------------------- + | + | Redis is an open source, fast, and advanced key-value store. However, it + | provides a richer set of commands than a typical key-value store such as + | APC or memcached. + | + | Here you may specify the hosts and ports for your Redis databases. + | + | For more information regarding Redis, check out: http://redis.io + | + */ + + 'redis' => array( + + 'default' => array('host' => '127.0.0.1', 'port' => 6379), + + ), + ); \ No newline at end of file diff --git a/application/config/session.php b/application/config/session.php index 58e1cf88..abd74fa8 100644 --- a/application/config/session.php +++ b/application/config/session.php @@ -12,7 +12,7 @@ | Since HTTP is stateless, sessions are used to maintain "state" across | multiple requests from the same user of your application. | - | Supported Drivers: 'cookie', 'file', 'database', 'memcached', 'apc'. + | Supported Drivers: 'cookie', 'file', 'database', 'memcached', 'apc', 'redis'. | */ diff --git a/laravel/bootstrap/constants.php b/laravel/bootstrap/constants.php index 475804bf..6b7bdefc 100644 --- a/laravel/bootstrap/constants.php +++ b/laravel/bootstrap/constants.php @@ -5,7 +5,7 @@ define('EXT', '.php'); /** - * Define a function that registers an array of constants if they + * Define a function that registers an array of constants if they haven't * haven't already been registered. This allows the constants to * be changed from their default values when unit testing. */ diff --git a/laravel/bootstrap/errors.php b/laravel/bootstrap/errors.php index 488c8e6a..f61b406d 100644 --- a/laravel/bootstrap/errors.php +++ b/laravel/bootstrap/errors.php @@ -101,7 +101,6 @@ { if ( ! is_null($error = error_get_last())) { - die('here'); extract($error, EXTR_SKIP); $handler(new \ErrorException($message, $type, 0, $file, $line)); diff --git a/laravel/cache/drivers/file.php b/laravel/cache/drivers/file.php index 0be234a5..6bf7c6e0 100644 --- a/laravel/cache/drivers/file.php +++ b/laravel/cache/drivers/file.php @@ -68,7 +68,9 @@ protected function retrieve($key) */ public function put($key, $value, $minutes) { - file_put_contents($this->path.$key, (time() + ($minutes * 60)).serialize($value), LOCK_EX); + $value = (time() + ($minutes * 60)).serialize($value); + + file_put_contents($this->path.$key, $value, LOCK_EX); } /** diff --git a/laravel/cache/drivers/redis.php b/laravel/cache/drivers/redis.php new file mode 100644 index 00000000..cc3c621c --- /dev/null +++ b/laravel/cache/drivers/redis.php @@ -0,0 +1,79 @@ +redis = $redis; + } + + /** + * Determine if an item exists in the cache. + * + * @param string $key + * @return bool + */ + public function has($key) + { + return ( ! is_null($this->redis->get($key))); + } + + /** + * Retrieve an item from the cache driver. + * + * @param string $key + * @return mixed + */ + protected function retrieve($key) + { + if ( ! is_null($cache = $this->redis->get($key))) + { + return unserialize($cache); + } + } + + /** + * Write an item to the cache for a given number of minutes. + * + * + * // Put an item in the cache for 15 minutes + * Cache::put('name', 'Taylor', 15); + * + * + * @param string $key + * @param mixed $value + * @param int $minutes + * @return void + */ + public function put($key, $value, $minutes) + { + $this->redis->set($key, serialize($value)); + + $this->redis->expire($key, $minutes * 60); + } + + /** + * Delete an item from the cache. + * + * @param string $key + * @return void + */ + public function forget($key) + { + $this->redis->del($key); + } + +} \ No newline at end of file diff --git a/laravel/config/container.php b/laravel/config/container.php index 72dbc35e..a0284e0e 100644 --- a/laravel/config/container.php +++ b/laravel/config/container.php @@ -71,11 +71,9 @@ | Laravel Caching Components |-------------------------------------------------------------------------- | - | The following components are used by the wonderfully, simple Laravel - | caching system. Each driver is resolved through the container. - | - | New cache drivers may be added to the framework by simply registering - | them into the container. + | The following components are used by the wonderfully simple Laravel cache + | system. Each driver is resolved through the container, so new drivers may + | be added by simply registering them in the container. | */ @@ -91,6 +89,12 @@ }), + 'laravel.cache.redis' => array('resolver' => function() + { + return new Cache\Drivers\Redis(Redis::db()); + }), + + 'laravel.cache.memcached' => array('resolver' => function($c) { return new Cache\Drivers\Memcached($c->core('cache.memcache.connection'), Config::get('cache.key')); @@ -130,10 +134,6 @@ | from the session driver, as well as examining the payload validitiy | and things like the CSRF token. | - | Like the caching components, each session driver is resolved via the - | container and new drivers may be added by registering them into the - | container. Several session drivers are "driven" by the cache drivers. - | */ 'laravel.session.transporter' => array('resolver' => function($c) @@ -166,6 +166,12 @@ }), + 'laravel.session.redis' => array('resolver' => function($c) + { + return new Session\Drivers\Redis($c->core('cache.redis')); + }), + + 'laravel.session.memcached' => array('resolver' => function($c) { return new Session\Drivers\Memcached($c->core('cache.memcached')); diff --git a/laravel/database/manager.php b/laravel/database/manager.php index f50f3411..a4a12329 100644 --- a/laravel/database/manager.php +++ b/laravel/database/manager.php @@ -17,8 +17,6 @@ class Manager { * * If no database name is specified, the default connection will be returned. * - * Note: Database connections are managed as singletons. - * * * // Get the default database connection for the application * $connection = DB::connection(); diff --git a/laravel/database/query.php b/laravel/database/query.php index 3fedcaf4..c05ca07e 100644 --- a/laravel/database/query.php +++ b/laravel/database/query.php @@ -611,7 +611,9 @@ public function decrement($column, $amount = 1) */ protected function adjust($column, $amount, $operator) { - return $this->update(array($column => Manager::raw($this->grammar->wrap($column).$operator.$amount))); + $value = Manager::raw($this->grammar->wrap($column).$operator.$amount); + + return $this->update(array($column => $value)); } /** diff --git a/laravel/laravel.php b/laravel/laravel.php index 7823d9be..289a173d 100644 --- a/laravel/laravel.php +++ b/laravel/laravel.php @@ -91,9 +91,9 @@ */ Routing\Filter::register(require APP_PATH.'filters'.EXT); -list($uri, $method) = array(Request::uri()->get(), Request::method()); +list($uri, $method, $format) = array(Request::uri()->get(), Request::method(), Request::format()); -$route = IoC::container()->core('routing.router')->route($method, $uri); +$route = IoC::container()->core('routing.router')->route($method, $uri, $format); if ( ! is_null($route)) { diff --git a/laravel/redis.php b/laravel/redis.php index 749bfafb..d0c25d74 100644 --- a/laravel/redis.php +++ b/laravel/redis.php @@ -3,92 +3,87 @@ class Redis { /** - * The name of the Redis connection. + * The address for the Redis host. * * @var string */ - public $name; + protected $host; /** - * The configuration array for the Redis connection. + * The port on which Redis can be accessed on the host. * - * @var array + * @var int */ - public $config = array(); + protected $port; /** * The connection to the Redis database. * * @var resource */ - protected static $connection; + protected $connection; + + /** + * The active Redis database instances. + * + * @var array + */ + protected static $databases = array(); /** * Create a new Redis connection instance. * - * @param string $name - * @param array $config + * @param string $host + * @param string $port * @return void */ - public function __construct($name, $config) + public function __construct($host, $port) { - $this->name = $name; - $this->config = $config; + $this->host = $host; + $this->port = $port; } /** - * Create a new Redis connection instance. + * Get a Redis database connection instance. * - * @param string $connection - * @param array $config + * The given name should correspond to a Redis database in the configuration file. + * + * + * // Get the default Redis database instance + * $redis = Redis::db(); + * + * // Get a specified Redis database instance + * $reids = Redis::db('redis_2'); + * + * + * @param string $name * @return Redis */ - public static function make($name, $config) + public static function db($name = 'default') { - return new static($name, $config); - } - - /** - * Create a new Redis connection instance. - * - * The Redis connection is managed as a singleton, so if the connection has - * already been established, that same connection instance will be returned - * on subsequent requests for the connection. - * - * @param string $connection - * @return Redis - */ - public static function connection() - { - if (is_null(static::$connection)) + if (is_null(static::$databases[$name])) { - static::$connection = static::make($name, Config::get('database.redis'))->connect(); + if (is_null($config = Config::get("database.redis.{$name}"))) + { + throw new \Exception("Redis database [$name] is not defined."); + } + + static::$databases[$name] = new static($config['host'], $config['port']); } - return static::$connection; + return static::$databases[$name]; } /** - * Connect to the Redis database. + * Execute a command against the Redis database. * - * The Redis instance itself will be returned by the method. + * + * // Execute the GET command for the "name" key + * $name = Redis::db()->run('get', array('name')); * - * @return Redis - */ - public function connect() - { - static::$connection = @fsockopen($this->config['host'], $this->config['port'], $error, $message); - - if (static::$connection === false) - { - throw new \Exception("Error establishing Redis connection [{$this->name}]: {$error} - {$message}"); - } - - return $this; - } - - /** - * Execute a command agaisnt the Redis database. + * // Execute the LRANGE command for the "list" key + * $list = Redis::db()->run('lrange', array(0, 5)); + * * * @param string $method * @param array $parameters @@ -96,21 +91,74 @@ public function connect() */ public function run($method, $parameters) { - fwrite(static::$connection, $this->command($method, $parameters)); + fwrite($this->connect(), $this->command($method, (array) $parameters)); - $reply = trim(fgets(static::$connection, 512)); + $ersponse = trim(fgets($this->connection, 512)); + + switch (substr($ersponse, 0, 1)) + { + case '-': + throw new \Exception('Redis error: '.substr(trim($ersponse), 4)); + + case '+': + case ':': + return $this->inline($ersponse); + + case '$': + return $this->bulk($ersponse); + + case '*': + return $this->multibulk($ersponse); + + default: + throw new \Exception("Unknown response from Redis server: ".substr($ersponse, 0, 1)); + } + } + + /** + * Establish the connection to the Redis database. + * + * @return resource + */ + protected function connect() + { + if ( ! is_null($this->connection)) return $this->connection; + + $this->connection = @fsockopen($this->host, $this->port, $error, $message); + + if ($this->connection === false) + { + throw new \Exception("Error making Redis connection: {$error} - {$message}"); + } + + return $this->connection; } /** * Build the Redis command based from a given method and parameters. * + * Redis protocol states that a command should conform to the following format: + * + * * CR LF + * $ CR LF + * CR LF + * ... + * $ CR LF + * CR LF + * + * More information regarding the Redis protocol: http://redis.io/topics/protocol + * * @param string $method * @param array $parameters * @return string */ protected function command($method, $parameters) { - $command = '*'.(count($parameters) + 1).CRLF.'$'.strlen($method).CRLF.strtoupper($method).CRLF; + $command = '*'.(count($parameters) + 1).CRLF; + + $command .= '$'.strlen($method).CRLF; + + $command .= strtoupper($method).CRLF; foreach ($parameters as $parameter) { @@ -120,6 +168,73 @@ protected function command($method, $parameters) return $command; } + /** + * Parse and handle an inline response from the Redis database. + * + * @param string $response + * @return string + */ + protected function inline($response) + { + return substr(trim($response), 1); + } + + /** + * Parse and handle a bulk response from the Redis database. + * + * @param string $head + * @return string + */ + protected function bulk($head) + { + if ($head == '$-1') return; + + list($read, $response, $size) = array(0, '', substr($head, 1)); + + do + { + // Calculate and read the appropriate bytes off of the Redis response. + // We'll read off the response in 1024 byte chunks until the entire + // response has been read from the database. + $block = (($remaining = $size - $read) < 1024) ? $remaining : 1024; + + $response .= fread($this->connection, $block); + + $read += $block; + + } while ($read < $size); + + // The response ends with a trailing CRLF. So, we need to read that off + // of the end of the file stream to get it out of the way of the next + // command that is issued to the database. + fread($this->connection, 2); + + return $response; + } + + /** + * Parse and handle a multi-bulk reply from the Redis database. + * + * @param string $head + * @return array + */ + protected function multibulk($head) + { + if (($count = substr($head, 1)) == '-1') return; + + $response = array(); + + // Iterate through each bulk response in the multi-bulk and parse it out + // using the "bulk" method since a multi-bulk response is just a list of + // plain old bulk responses. + for ($i = 0; $i < $count; $i++) + { + $response[] = $this->bulk(trim(fgets($this->connection, 512))); + } + + return $response; + } + /** * Dynamically make calls to the Redis database. */ @@ -128,6 +243,14 @@ public function __call($method, $parameters) return $this->run($method, $parameters); } + /** + * Dynamically pass static method calls to the Redis instance. + */ + public static function __callStatic($method, $parameters) + { + return static::db()->run($method, $parameters); + } + /** * Close the connection to the Redis database. * @@ -135,7 +258,7 @@ public function __call($method, $parameters) */ public function __destruct() { - fclose(static::$connection); + fclose($this->connection); } } \ No newline at end of file diff --git a/laravel/routing/route.php b/laravel/routing/route.php index 0642d142..d36c9e96 100644 --- a/laravel/routing/route.php +++ b/laravel/routing/route.php @@ -150,15 +150,15 @@ protected function response() { return call_user_func_array($this->callback, $this->parameters); } - // If the route is an array we will return the first value with a - // key of "delegate", or the first instance of a Closure. If the - // value is a string, the route is delegating the responsibility + // If the route is an array, we will return the first value with a + // key of "uses", or the first instance of a Closure. If the value + // is a string, the route is delegating the responsibility for // for handling the request to a controller. elseif (is_array($this->callback)) { $callback = Arr::first($this->callback, function($key, $value) { - return $key == 'delegate' or $value instanceof Closure; + return $key == 'uses' or $value instanceof Closure; }); if ($callback instanceof Closure) diff --git a/laravel/routing/router.php b/laravel/routing/router.php index 8b1c92df..35c3e27f 100644 --- a/laravel/routing/router.php +++ b/laravel/routing/router.php @@ -51,8 +51,8 @@ class Router { * @var array */ protected $patterns = array( - '(:num)' => '[0-9]+', - '(:any)' => '[a-zA-Z0-9\.\-_]+', + '(:num)' => '([0-9]+)', + '(:any)' => '([a-zA-Z0-9\.\-_]+)', ); /** @@ -104,9 +104,10 @@ public function find($name) * * @param string $method * @param string $uri + * @param string $format * @return Route */ - public function route($method, $uri) + public function route($method, $uri, $format) { $routes = $this->loader->load($uri); @@ -122,19 +123,18 @@ public function route($method, $uri) foreach ($routes as $keys => $callback) { - // Formats are appended to the route key as a regular expression. - // It will look something like: "(\.(json|xml|html))?" - $formats = $this->provides($callback); + // We need to make sure that the requested format is provided by the + // route. If it isn't, there is no need to continue evaluating it. + if ( ! in_array($format, $this->provides($callback))) continue; - // Only check routes that have multiple URIs or wildcards since other + // Only check routes having multiple URIs or wildcards since other // routes would have been caught by the check for literal matches. - // We also need to check routes with "provides" clauses. - if ($this->fuzzy($keys) or ! is_null($formats)) + if (strpos($keys, '(') !== false or strpos($keys, ',') !== false) { - if ( ! is_null($route = $this->match($destination, $keys, $callback, $formats))) + if ( ! is_null($route = $this->match($destination, $keys, $callback, $format))) { return Request::$route = $route; - } + } } } @@ -142,18 +142,19 @@ public function route($method, $uri) } /** - * Determine if the route contains elements that forbid literal matches. + * Get the request formats for which the route provides responses. * - * Any route key containing a regular expression, wildcard, or multiple - * URIs cannot be matched using a literal string check, but must be - * checked using regular expressions. - * - * @param string $keys - * @return bool + * @param mixed $callback + * @return array */ - protected function fuzzy($keys) + protected function provides($callback) { - return strpos($keys, '(') !== false or strpos($keys, ',') !== false; + if (is_array($callback) and isset($callback['provides'])) + { + return (is_string($provides = $callback['provides'])) ? explode('|', $provides) : $provides; + } + + return array(); } /** @@ -166,18 +167,19 @@ protected function fuzzy($keys) * @param string $destination * @param array $keys * @param mixed $callback - * @param array $formats + * @param string $format * @return mixed */ - protected function match($destination, $keys, $callback, $formats) + protected function match($destination, $keys, $callback, $format) { - // Append the provided formats to the route as an optional regular expression. - // This should make the route look something like: "user(\.(json|xml|html))?" - $formats = ( ! is_null($formats)) ? '(\.('.implode('|', $formats).'))?' : ''; + // We need to remove the format from the route since formats are + // not specified in the route URI directly, but rather through + // the "provides" keyword on the route array. + $destination = str_replace('.'.$format, '', $destination); foreach (explode(', ', $keys) as $key) { - if (preg_match('#^'.$this->wildcards($key).$formats.'$#', $destination)) + if (preg_match('#^'.$this->wildcards($key).'$#', $destination)) { return new Route($keys, $callback, $this->parameters($destination, $key)); } @@ -241,20 +243,6 @@ protected function controller_key($segments) } } - /** - * Get the request formats for which the route provides responses. - * - * @param mixed $callback - * @return array - */ - protected function provides($callback) - { - if (is_array($callback) and isset($callback['provides'])) - { - return (is_string($provides = $callback['provides'])) ? explode('|', $provides) : $provides; - } - } - /** * Translate route URI wildcards into actual regular expressions. * @@ -272,8 +260,6 @@ protected function wildcards($key) $key .= ($replacements > 0) ? str_repeat(')?', $replacements) : ''; - // After replacing all of the optional wildcards, we can replace all - // of the "regular" wildcards and return the fully regexed string. return str_replace(array_keys($this->patterns), array_values($this->patterns), $key); } @@ -288,6 +274,11 @@ protected function wildcards($key) */ protected function parameters($uri, $route) { + // When gathering the parameters, we need to get the request format out + // of the destination, otherwise it could be passed in as a parameter + // to the route closure or controller, which we don't want. + $uri = str_replace('.'.Request::format(), '', $uri); + list($uri, $route) = array(explode('/', $uri), explode('/', $route)); $count = count($route); diff --git a/laravel/session/drivers/redis.php b/laravel/session/drivers/redis.php new file mode 100644 index 00000000..a45b9a16 --- /dev/null +++ b/laravel/session/drivers/redis.php @@ -0,0 +1,60 @@ +redis = $redis; + } + + /** + * Load a session from storage by a given ID. + * + * If no session is found for the ID, null will be returned. + * + * @param string $id + * @return array + */ + public function load($id) + { + return $this->redis->get($id); + } + + /** + * Save a given session to storage. + * + * @param array $session + * @param array $config + * @param bool $exists + * @return void + */ + public function save($session, $config, $exists) + { + $this->redis->put($session['id'], $session, $config['lifetime']); + } + + /** + * Delete a session from storage by a given ID. + * + * @param string $id + * @return void + */ + public function delete($id) + { + $this->redis->forget($id); + } + +} \ No newline at end of file diff --git a/laravel/uri.php b/laravel/uri.php index 563bc020..ed283269 100644 --- a/laravel/uri.php +++ b/laravel/uri.php @@ -41,9 +41,7 @@ public function get() { if ( ! is_null($this->uri)) return $this->uri; - $uri = parse_url($this->server['REQUEST_URI'], PHP_URL_PATH); - - return $this->uri = $this->format($this->clean($uri)); + return $this->uri = $this->format($this->clean($this->parse($this->server['REQUEST_URI']))); } /** @@ -54,14 +52,24 @@ public function get() */ protected function clean($uri) { - $uri = $this->remove($uri, parse_url(Config::$items['application']['url'], PHP_URL_PATH)); + $uri = $this->remove($uri, $this->parse(Config::$items['application']['url'])); if (($index = '/'.Config::$items['application']['index']) !== '/') { $uri = $this->remove($uri, $index); } - return rtrim($uri, '.'.Request::format($uri)); + return $uri; + } + + /** + * Parse a given string URI using PHP_URL_PATH to remove the domain. + * + * @return string + */ + protected function parse($uri) + { + return parse_url($uri, PHP_URL_PATH); } /** diff --git a/laravel/validation/messages.php b/laravel/validation/messages.php index 4e5c61cb..899ce845 100644 --- a/laravel/validation/messages.php +++ b/laravel/validation/messages.php @@ -37,10 +37,19 @@ public function __construct($messages = array()) */ public function add($key, $message) { - if ( ! isset($this->messages[$key]) or array_search($message, $this->messages[$key]) === false) - { - $this->messages[$key][] = $message; - } + if ($this->unique($key, $message)) $this->messages[$key][] = $message; + } + + /** + * Determine if a key and message combination already exists. + * + * @param string $key + * @param string $message + * @return bool + */ + protected function unique($key, $message) + { + return ! isset($this->messages[$key]) or array_search($message, $this->messages[$key]) === false; } /** diff --git a/laravel/view.php b/laravel/view.php index 6497e49d..4527ade9 100644 --- a/laravel/view.php +++ b/laravel/view.php @@ -188,7 +188,7 @@ public function render() // use the regular path to the view. $view = (strpos($this->path, BLADE_EXT) !== false) ? $this->compile() : $this->path; - try { include $view; } catch (Exception $e) { ob_get_clean(); throw $e; } + try { include $view; } catch (\Exception $e) { ob_get_clean(); throw $e; } return ob_get_clean(); }