diff --git a/bootstrap.php b/bootstrap.php index 454d4f83b..6b64da732 100644 --- a/bootstrap.php +++ b/bootstrap.php @@ -108,6 +108,12 @@ function delete_example() { })->name('hello')->conditions(array('name' => '\w+')); +/*** ROUTES WITH OPTIONAL SEGMENTS AND DEFAULT ARGUMENTS ***/ + +Slim::get('/archive/:year(/:month(/:day))', function ( $year, $month = 5, $day = 20 ) { + echo "

The date is: $month/$day/$year

"; +}); + /*** RUN SLIM ***/ Slim::run(); diff --git a/slim/Request.php b/slim/Request.php index 0c8ded292..ea0a3d28b 100644 --- a/slim/Request.php +++ b/slim/Request.php @@ -247,13 +247,14 @@ private function getPutParameters() { * Fetch HTTP request headers * * @author Kris Jordan + * @author Jud Stephenson * @return array */ private function getHttpHeaders() { $httpHeaders = array(); foreach ( array_keys($_SERVER) as $key ) { - if ( substr($key, 0, 5) === 'HTTP_' ) { - $httpHeaders[substr($key, 5)] = $_SERVER[$key]; + if ( (substr($key, 0, 5) === 'HTTP_') || (substr($key, 0, 8) === 'PHP_AUTH') ) { + $httpHeaders[((substr($key, 0, 5) == 'HTTP_') ? substr($key, 5) : substr($key, 4))] = $_SERVER[$key]; } } return $httpHeaders; @@ -268,6 +269,7 @@ private function getHttpHeaders() { public function header( $name ) { return isset($this->headers[$name]) ? $this->headers[$name] : null; } + /** * Check for HTTP request method override diff --git a/slim/Route.php b/slim/Route.php index 090791a76..167e566ee 100644 --- a/slim/Route.php +++ b/slim/Route.php @@ -76,10 +76,119 @@ class Route { * @param mixed $callable Anything that returns TRUE for is_callable() */ public function __construct( $pattern, $callable ) { - $this->pattern = ltrim($pattern, '/'); - $this->callable = $callable; + $this->setPattern(str_replace(')', ')?', ltrim($pattern, '/'))); + $this->setCallable($callable); } + /***** ACCESSORS *****/ + + /** + * Get route pattern + * + * @return string + */ + public function getPattern() { + return $this->pattern; + } + + /** + * Set route pattern + * + * @param string $pattern + * @return void + */ + public function setPattern( $pattern ) { + $this->pattern = $pattern; + } + + /** + * Get route callable + * + * @return mixed + */ + public function getCallable() { + return $this->callable; + } + + /** + * Set route callable + * + * @param mixed $callable + * @return void + */ + public function setCallable($callable) { + $this->callable = $callable; + } + + /** + * Get route conditions + * + * @return array + */ + public function getConditions() { + return $this->conditions; + } + + /** + * Set route conditions + * + * @param array $conditions + * @return void + */ + public function setConditions( $conditions ) { + $this->conditions = (array)$conditions; + } + + /** + * Get route name + * + * @return string|null + */ + public function getName() { + return $this->name; + } + + /** + * Set route name + * + * @param string $name + * @return void + */ + public function setName( $name ) { + $this->name = $name; + $this->getRouter()->cacheNamedRoute($name, $this); + } + + /** + * Get route parameters + * + * @return array + */ + public function getParams() { + return $this->params; + } + + /** + * Get router + * + * @return Router + */ + public function getRouter() { + return $this->router; + } + + /** + * Set router + * + * @param Router $router + * @return void + */ + public function setRouter( Router $router ) { + $this->router = $router; + } + + /***** ROUTE PARSING AND MATCHING *****/ + /** * Matches URI? * @@ -104,26 +213,23 @@ public function __construct( $pattern, $callable ) { public function matches( $resourceUri ) { //Extract URL params - preg_match_all('@:([\w]+)@', $this->pattern, $paramNames, PREG_PATTERN_ORDER); + preg_match_all('@:([\w]+)@', $this->getPattern(), $paramNames, PREG_PATTERN_ORDER); $paramNames = $paramNames[0]; //Convert URL params into regex patterns, construct a regex for this route - $patternAsRegex = preg_replace_callback('@:[\w]+@', array($this, 'convertPatternToRegex'), $this->pattern); - if ( substr($this->pattern, -1) === '/' ) { + $patternAsRegex = preg_replace_callback('@:[\w]+@', array($this, 'convertPatternToRegex'), $this->getPattern()); + if ( substr($this->getPattern(), -1) === '/' ) { $patternAsRegex = $patternAsRegex . '?'; } $patternAsRegex = '@^' . $patternAsRegex . '$@'; //Cache URL params' names and values if this route matches the current HTTP request if ( preg_match($patternAsRegex, $resourceUri, $paramValues) ) { - //If route pattern has trailing slash and the resource URL does not have - //a trailing slash, throw a SlimRequestSlashException - if ( substr($this->pattern, -1) === '/' && substr($resourceUri, -1) !== '/' ) { - throw new SlimRequestSlashException(); - } array_shift($paramValues); foreach ( $paramNames as $index => $value ) { - $this->params[substr($value, 1)] = urldecode($paramValues[$index]); + if ( isset($paramValues[substr($value, 1)]) ) { + $this->params[substr($value, 1)] = urldecode($paramValues[substr($value, 1)]); + } } return true; } else { @@ -141,76 +247,55 @@ public function matches( $resourceUri ) { private function convertPatternToRegex( $matches ) { $key = str_replace(':', '', $matches[0]); if ( array_key_exists($key, $this->conditions) ) { - return '(' . $this->conditions[$key] . ')'; + return '(?P<' . $key . '>' . $this->conditions[$key] . ')'; } else { - return '([a-zA-Z0-9_\-\.\!\~\*\\\'\(\)\:\@\&\=\$\+,%]+)'; + return '(?P<' . $key . '>[a-zA-Z0-9_\-\.\!\~\*\\\'\(\)\:\@\&\=\$\+,%]+)'; } } + /***** HELPERS *****/ + /** - * Return the callable for this Route - * - * @return mixed - */ - public function callable() { - return $this->callable; - } - - /** - * Return the parameter names and values for this Route - * - * @return array - */ - public function params() { - return $this->params; - } - - /** - * Set this route's Router - * - * @param Router The router for this Route - * @return void - */ - public function setRouter( Router $router ) { - $this->router = $router; - } - - /** - * Get the pattern for this Route - * - * @return string - */ - public function pattern() { - return $this->pattern; - } - - /** - * Set this route's name + * Set route name (alias for Route::setName) * * @param string $name The name of the route * @return Route */ - public function name( $name = null ) { - if ( !is_null($name) ) { - $this->name = (string)$name; - $this->router->cacheNamedRoute($name, $this); - } + public function name( $name ) { + $this->setName($name); return $this; } /** - * Set this route's conditions + * Set route conditions (alias for Route::setConditions) * * @param array $conditions An associative array of URL parameter conditions * @return Route */ - public function conditions( $conditions = null ) { + public function conditions( $conditions ) { if ( is_array($conditions) ) { $this->conditions = $conditions; } return $this; } + + /***** DISPATCHING *****/ + + /** + * Dispatch route + * + * @return bool + */ + public function dispatch() { + if ( substr($this->getPattern(), -1) === '/' && substr($this->getRouter()->getRequest()->resource, -1) !== '/' ) { + throw new SlimRequestSlashException(); + } + if ( is_callable($this->getCallable()) ) { + call_user_func_array($this->getCallable(), array_values($this->getParams())); + return true; + } + return false; + } } - ?> \ No newline at end of file diff --git a/slim/Router.php b/slim/Router.php index aff32329b..6268563fd 100644 --- a/slim/Router.php +++ b/slim/Router.php @@ -43,7 +43,7 @@ * @author Josh Lockhart * @since Version 1.0 */ -class Router { +class Router implements Iterator { /** * @var Request @@ -61,10 +61,10 @@ class Router { private $namedRoutes; /** - * @var Route The Route that matches the current HTTP request, or NULL + * @var array Array of routes matching the Request method and URL */ - private $matchedRoute; - + private $matchedRoutes; + /** * @var mixed 404 Not Found callback function if a matching route is not found */ @@ -75,6 +75,11 @@ class Router { */ private $error; + /** + * @var int Iterator position + */ + private $position; + /** * Constructor * @@ -88,7 +93,31 @@ public function __construct( Request $request ) { 'PUT' => array(), 'DELETE' => array() ); + $this->position = 0; + } + + /***** ACCESSORS *****/ + + /** + * Get Request + * + * @return Request + */ + public function getRequest() { + return $this->request; + } + + /** + * Set Request + * + * @param Request + * @return void + */ + public function setRequest( Request $req ) { + $this->request = $req; } + + /***** MAPPING *****/ /** * Map a route to a callback function @@ -102,6 +131,9 @@ public function map( $pattern, $callable, $method ) { $route = new Route($pattern, $callable); $route->setRouter($this); $this->routes[$method][] = $route; + if ( $method === $this->getRequest()->method && $route->matches($this->getRequest()->resource) ) { + $this->matchedRoutes[] = $route; + } return $route; } @@ -132,11 +164,11 @@ public function urlFor( $name, $params = array() ) { if ( !isset($this->namedRoutes[(string)$name]) ) { throw new RuntimeException('Named route not found for name: ' . $name); } - $pattern = $this->namedRoutes[(string)$name]->pattern(); + $pattern = $this->namedRoutes[(string)$name]->getPattern(); foreach ( $params as $key => $value ) { $pattern = str_replace(':' . $key, $value, $pattern); } - return $this->request->root . $pattern; + return $this->getRequest()->root . $pattern; } /** @@ -165,29 +197,54 @@ public function error( $callable = null ) { return $this->error; } + /***** ITERATOR INTERFACE *****/ + /** - * Dispatch request + * Return the current route being dispatched * - * @return bool TRUE if matching route is found and callable, else FALSE - */ - public function dispatch() { - foreach ( $this->routes[$this->request->method] as $route ) { - if ( $route->matches($this->request->resource) ) { - $this->matchedRoute = $route; - try { - $callable = $this->matchedRoute->callable(); - if ( is_callable($callable) ) { - call_user_func_array($callable, array_values($this->matchedRoute->params())); - } - return true; - } catch ( SlimPassException $e ) { - continue; - } - } - } - return false; + * @return Route + */ + public function current() { + return $this->matchedRoutes[$this->position]; } -} + /** + * Reset the current route to the first matching route + * + * @return void + */ + public function rewind() { + $this->position = 0; + } + + /** + * Return the 0-indexed position of the current route + * being dispatched among all matching routes + * + * @return int + */ + public function key() { + return $this->position; + } + + /** + * Return the 0-indexed position of the next route to + * be dispatched among all matching routes + * + * @return int + */ + public function next() { + $this->position = $this->position + 1; + } + /** + * Does a matching route exist at a given 0-indexed position? + * + * @return bool + */ + public function valid() { + return isset($this->matchedRoutes[$this->position]); + } + +} ?> \ No newline at end of file diff --git a/slim/Slim.php b/slim/Slim.php index 60e50dc78..8d3d265be 100644 --- a/slim/Slim.php +++ b/slim/Slim.php @@ -104,6 +104,18 @@ class Slim { * @var array Application settings */ private $settings; + + /** + * @var array Plugin hooks + */ + private $hooks = array( + 'slim.before' => array(), + 'slim.before.router' => array(), + 'slim.before.dispatch' => array(), + 'slim.after.dispatch' => array(), + 'slim.after.router' => array(), + 'slim.after' => array() + ); /** * Constructor @@ -117,8 +129,6 @@ private function __construct() { $this->request = new Request(); $this->response = new Response(); $this->router = new Router( $this->request ); - $this->before = array(); - $this->after = array(); $this->settings = array( 'log' => true, 'log_dir' => './logs', @@ -464,7 +474,7 @@ public static function log( $message, $errno = null ) { * @return void */ public static function before( $callable ) { - self::$app->before[] = $callable; + self::hook('slim.before.router', $callable); } /** @@ -478,24 +488,7 @@ public static function before( $callable ) { * @return void */ public static function after( $callable ) { - self::$app->after[] = $callable; - } - - /** - * Run callbacks - * - * This calls each callable object in the $callables array. This - * is used internally to run the Slim app's BEFORE and AFTER callbacks. - * - * @param array $callables Callable objects - * @return void - */ - private static function runCallables( $callables ) { - foreach ( $callables as $callable ) { - if ( is_callable($callable) ) { - call_user_func($callable); - } - } + self::hook('slim.after.router', $callable); } /***** ACCESSORS *****/ @@ -545,13 +538,13 @@ public static function router() { */ public static function view( $viewClass = null ) { if ( !is_null($viewClass) ) { - $existingData = is_null(self::$app->view) ? array() : self::$app->view->data(); + $existingData = is_null(self::$app->view) ? array() : self::$app->view->getData(); if ( $viewClass instanceOf View ) { self::$app->view = $viewClass; } else { self::$app->view = new $viewClass(); } - self::$app->view->data($existingData); + self::$app->view->appendData($existingData); } return self::$app->view; } @@ -572,12 +565,12 @@ public static function view( $viewClass = null ) { * @return void */ public static function render( $template, $data = array(), $status = null ) { - self::view()->templatesDirectory(self::config('templates_dir')); + self::view()->setTemplatesDirectory(self::config('templates_dir')); if ( !is_null($status) ) { self::response()->status($status); } - self::view()->data($data); - self::view()->render($template); + self::view()->appendData($data); + self::view()->display($template); } /***** HTTP CACHING *****/ @@ -840,6 +833,71 @@ public static function defaultError() { echo self::generateTemplateMarkup('Error', '

A website error has occured. The website administrator has been notified of the issue. Sorry for the temporary inconvenience.

'); } + /***** HOOKS *****/ + + /** + * Invoke or assign hook + * + * @param string $name The hook name + * @param mixed $callable A callable object + * @return void + */ + public static function hook($name, $callable = null) { + if ( !isset(self::$app->hooks[$name]) ) { + self::$app->hooks[$name] = array(); + } + if ( !is_null($callable) ) { + if ( is_callable($callable) ) { + self::$app->hooks[$name][] = $callable; + } + } else { + foreach( self::$app->hooks[$name] as $listener ) { + $listener(self::$app); + } + } + } + + /** + * Get hook listeners + * + * Return an array of registered hooks. If `$name` is a valid + * hook name, only the listeners attached to that hook are returned. + * Else, all listeners are returned as an associative array whose + * keys are hook names and whose values are arrays of listeners. + * + * @param string $name Optional. A hook name. + * @return array|null + */ + public static function getHooks($name = null) { + if ( !is_null($name) ) { + return isset(self::$app->hooks[(string)$name]) ? self::$app->hooks[(string)$name] : null; + } else { + return self::$app->hooks; + } + } + + /** + * Clear hook listeners + * + * Clear all listeners for all hooks. If `$name` is + * a valid hook name, only the listeners attached + * to that hook will be cleared. + * + * @param string $name Optional. A hook name. + * @return void + */ + public static function clearHooks($name = null) { + if ( !is_null($name) ) { + if ( isset(self::$app->hooks[(string)$name]) ) { + self::$app->hooks[(string)$name] = array(); + } + } else { + foreach( self::$app->hooks as $key => $value ) { + self::$app->hooks[$key] = array(); + } + } + } + /***** RUN SLIM *****/ /** @@ -856,12 +914,29 @@ public static function defaultError() { */ public static function run() { try { - self::runCallables(self::$app->before); + self::hook('slim.before'); ob_start(); - if ( !self::router()->dispatch() ) { Slim::notFound(); } + self::hook('slim.before.router'); + $dispatched = false; + foreach( self::router() as $route ) { + try { + Slim::hook('slim.before.dispatch'); + $dispatched = $route->dispatch(); + Slim::hook('slim.after.dispatch'); + if ( $dispatched ) { + break; + } + } catch ( SlimPassException $e ) { + continue; + } + } + if ( !$dispatched ) { + Slim::notFound(); + } self::response()->write(ob_get_clean()); - self::runCallables(self::$app->after); + self::hook('slim.after.router'); self::response()->send(); + self::hook('slim.after'); } catch ( SlimRequestSlashException $e ) { self::redirect(self::request()->root . self::request()->resource . '/', 301); } diff --git a/slim/View.php b/slim/View.php index 0c7c3f367..b54e55044 100644 --- a/slim/View.php +++ b/slim/View.php @@ -33,13 +33,11 @@ /** * Slim View * - * The View is delegated the responsibility of rendering a template. Usually - * you will subclass View and, in the subclass, re-implement the render - * method to use a custom templating engine, such as Smarty, Twig, Markdown, etc. - * - * It is very important that the View *echo* the final template output. DO NOT - * return the output... if you return the output rather than echoing it, the - * Slim Response body will be empty. + * The View is delegated the responsibility of rendering and/or displaying + * a template. It is recommended that you subclass View and re-implement the + * `View::render` method to use a custom templating engine such as + * Smarty, Twig, Markdown, etc. It is very important that `View::render` + * `return` the final template output. Do not `echo` the output. * * @package Slim * @author Josh Lockhart @@ -48,71 +46,124 @@ class View { /** - * @var array Associative array of data available to the template + * @var array Key-value array of data available to the template */ protected $data = array(); /** - * @var string The templates directory + * @var string Absolute or relative path to the templates directory */ protected $templatesDirectory; /** * Constructor + * + * This is empty but may be modified in a subclass if required */ - final public function __construct() {} + public function __construct() {} + + /***** ACCESSORS *****/ /** - * Set and/or get View data + * Get data * - * @param array $data [ key => value, ... ] - * @return array + * @param string $key + * @return array|null */ - final public function data( $data = null ) { - if ( is_array($data) ) { - $this->data = array_merge($this->data, $data); + public function getData( $key = null ) { + if ( !is_null($key) ) { + return isset($this->data[$key]) ? $this->data[$key] : null; + } else { + return $this->data; } - return $this->data; } /** - * Set and/or get templates directory + * Set data + * + * This method is overloaded to accept two different method signatures. + * You may use this to set a specific key with a specfic value, + * or you may use this to set all data to a specific array. + * + * USAGE: * - * @param string $dir The absolute or relative path to the templates directory - * @throws RuntimeException If templates directory does not exist or is not a directory - * @return string|null The templates directory, or NULL if not set + * View::setData('color', 'red'); + * View::setData(array('color' => 'red', 'number' => 1)); + * + * @param string|array + * @param mixed Optional. Only use if first argument is a string. + * @return void */ - final public function templatesDirectory( $dir = null ) { - if ( !is_null($dir) ) { - if ( !is_dir($dir) ) { - throw new RuntimeException('Cannot set View templates directory to: ' . $dir . '. Directory does not exist.'); - } - $this->templatesDirectory = rtrim($dir, '/') . '/'; + public function setData() { + $args = func_get_args(); + if ( count($args) === 1 && is_array($args[0]) ) { + $this->data = $args[0]; + } else if ( count($args) === 2 ) { + $this->data[(string)$args[0]] = $args[1]; + } else { + throw new InvalidArgumentException('Cannot set View data with provided arguments. Usage: `View::setData( $key, $value );` or `View::setData([ key => value, ... ]);`'); } - return $this->templatesDirectory; } /** - * Render template - * - * This method is responsible for rendering a template. The associative - * data array is available for use by the template. The rendered - * template should be echo()'d, NOT returned. + * Append data * - * I strongly recommend that you override this method in a subclass if - * you need more advanced templating (ie. Twig or Smarty). + * @param array $data + * @return void + */ + public function appendData( array $data ) { + $this->data = array_merge($this->data, $data); + } + + /** + * Get templates directory * - * The default View class assumes there is a "templates" directory in the - * same directory as your bootstrap.php file. + * @return string|null + */ + public function getTemplatesDirectory() { + return $this->templatesDirectory; + } + + /** + * Set templates directory * - * @param string $template The template name specified in Slim::render() + * @param string $dir * @return void */ + public function setTemplatesDirectory( $dir ) { + if ( !is_dir($dir) ) { + throw new RuntimeException('Cannot set View templates directory to: ' . $dir . '. Directory does not exist.'); + } + $this->templatesDirectory = rtrim($dir, '/'); + } + + /***** RENDERING *****/ + + /** + * Display template + * + * @return void + */ + public function display( $template ) { + echo $this->render($template); + } + + /** + * Render template + * + * @param string $template Path to template file, relative to templates directory + * @return string Rendered template + */ public function render( $template ) { extract($this->data); - require($this->templatesDirectory() . $template); + $templatePath = $this->getTemplatesDirectory() . '/' . ltrim($template, '/'); + if ( !file_exists($templatePath) ) { + throw new RuntimeException('View cannot render template `' . $templatePath . '`. Template does not exist.'); + } + ob_start(); + require $templatePath; + return ob_get_clean(); } } - ?> \ No newline at end of file diff --git a/tests/RouteTest.php b/tests/RouteTest.php index 8701e785f..d36b71efc 100644 --- a/tests/RouteTest.php +++ b/tests/RouteTest.php @@ -75,7 +75,7 @@ public function testRouteSetsNameAndIsCached() { */ public function testRouteSetsPatternWithoutLeadingSlash() { $route = new Route('/foo/bar', function () {}); - $this->assertEquals('foo/bar', $route->pattern()); + $this->assertEquals('foo/bar', $route->getPattern()); } /** @@ -85,7 +85,7 @@ public function testRouteSetsPatternWithoutLeadingSlash() { public function testRouteSetsCallableAsFunction() { $callable = function () { echo "Foo!"; }; $route = new Route('/foo/bar', $callable); - $this->assertSame($callable, $route->callable()); + $this->assertSame($callable, $route->getCallable()); } /** @@ -94,7 +94,7 @@ public function testRouteSetsCallableAsFunction() { */ public function testRouteSetsCallableAsString() { $route = new Route('/foo/bar', 'testCallable'); - $this->assertEquals('testCallable', $route->callable()); + $this->assertEquals('testCallable', $route->getCallable()); } /** @@ -105,7 +105,7 @@ public function testRouteMatchesAndParamExtracted() { $route = new Route('/hello/:name', function () {}); $result = $route->matches($resource); $this->assertTrue($result); - $this->assertEquals($route->params(), array('name' => 'Josh')); + $this->assertEquals($route->getParams(), array('name' => 'Josh')); } /** @@ -116,7 +116,7 @@ public function testRouteMatchesAndMultipleParamsExtracted() { $route = new Route('/hello/:first/and/:second', function () {}); $result = $route->matches($resource); $this->assertTrue($result); - $this->assertEquals($route->params(), array('first' => 'Josh', 'second' => 'John')); + $this->assertEquals($route->getParams(), array('first' => 'Josh', 'second' => 'John')); } /** @@ -127,7 +127,7 @@ public function testRouteDoesNotMatchAndParamsNotExtracted() { $route = new Route('/hello/:name', function () {}); $result = $route->matches($resource); $this->assertFalse($result); - $this->assertEquals($route->params(), array()); + $this->assertEquals($route->getParams(), array()); } /** @@ -139,7 +139,7 @@ public function testRouteMatchesResourceWithConditions() { $route->conditions(array('first' => '[a-zA-Z]{3,}')); $result = $route->matches($resource); $this->assertTrue($result); - $this->assertEquals($route->params(), array('first' => 'Josh', 'second' => 'John')); + $this->assertEquals($route->getParams(), array('first' => 'Josh', 'second' => 'John')); } /** @@ -151,7 +151,7 @@ public function testRouteDoesNotMatchResourceWithConditions() { $route->conditions(array('first' => '[a-z]{3,}')); $result = $route->matches($resource); $this->assertFalse($result); - $this->assertEquals($route->params(), array()); + $this->assertEquals($route->getParams(), array()); } /* @@ -167,7 +167,7 @@ public function testRouteMatchesResourceWithValidRfc2396PathComponent() { $route = new Route('/rfc2386/:symbols', function () {}); $result = $route->matches($resource); $this->assertTrue($result); - $this->assertEquals($route->params(), array('symbols' => $symbols)); + $this->assertEquals($route->getParams(), array('symbols' => $symbols)); } /* @@ -181,8 +181,46 @@ public function testRouteMatchesResourceWithUnreservedMarks() { $route = new Route('/marks/:marks', function () {}); $result = $route->matches($resource); $this->assertTrue($result); - $this->assertEquals($route->params(), array('marks' => $marks)); + $this->assertEquals($route->getParams(), array('marks' => $marks)); } + + /** + * Route optional parameters + * + * Pre-conditions: + * Route pattern requires :year, optionally accepts :month and :day + * + * Post-conditions: + * All: Year is 2010 + * Case A: Month and day default values are used + * Case B: Month is "05" and day default value is used + * Case C: Month is "05" and day is "13" + */ + public function testRouteOptionalParameters() { + $pattern = 'archive/:year(/:month(/:day))'; + + //Case A + $routeA = new Route($pattern, function () {}); + $resourceA = 'archive/2010'; + $resultA = $routeA->matches($resourceA); + $this->assertTrue($resultA); + $this->assertEquals($routeA->getParams(), array('year' => '2010')); + + //Case B + $routeB = new Route($pattern, function () {}); + $resourceB = 'archive/2010/05'; + $resultB = $routeB->matches($resourceB); + $this->assertTrue($resultB); + $this->assertEquals($routeB->getParams(), array('year' => '2010', 'month' => '05')); + + //Case C + $routeC = new Route($pattern, function () {}); + $resourceC = 'archive/2010/05/13'; + $resultC = $routeC->matches($resourceC); + $this->assertTrue($resultC); + $this->assertEquals($routeC->getParams(), array('year' => '2010', 'month' => '05', 'day' => '13')); + } + } -?> +?> \ No newline at end of file diff --git a/tests/RouterTest.php b/tests/RouterTest.php index 7b85da8e1..764fc04f6 100644 --- a/tests/RouterTest.php +++ b/tests/RouterTest.php @@ -1,4 +1,6 @@ +?> \ No newline at end of file diff --git a/tests/SlimTest.php b/tests/SlimTest.php index dc6383146..0c076ac87 100644 --- a/tests/SlimTest.php +++ b/tests/SlimTest.php @@ -200,8 +200,8 @@ public function testSlimGetRoute(){ Slim::init(); $callable = function () { echo "foo"; }; $route = Slim::get('/foo/bar', $callable); - $this->assertEquals('foo/bar', $route->pattern()); - $this->assertSame($callable, $route->callable()); + $this->assertEquals('foo/bar', $route->getPattern()); + $this->assertSame($callable, $route->getCallable()); } /** @@ -218,8 +218,8 @@ public function testSlimPostRoute(){ Slim::init(); $callable = function () { echo "foo"; }; $route = Slim::post('/foo/bar', $callable); - $this->assertEquals('foo/bar', $route->pattern()); - $this->assertSame($callable, $route->callable()); + $this->assertEquals('foo/bar', $route->getPattern()); + $this->assertSame($callable, $route->getCallable()); } /** @@ -236,8 +236,8 @@ public function testSlimPutRoute(){ Slim::init(); $callable = function () { echo "foo"; }; $route = Slim::put('/foo/bar', $callable); - $this->assertEquals('foo/bar', $route->pattern()); - $this->assertSame($callable, $route->callable()); + $this->assertEquals('foo/bar', $route->getPattern()); + $this->assertSame($callable, $route->getCallable()); } /** @@ -254,8 +254,8 @@ public function testSlimDeleteRoute(){ Slim::init(); $callable = function () { echo "foo"; }; $route = Slim::delete('/foo/bar', $callable); - $this->assertEquals('foo/bar', $route->pattern()); - $this->assertSame($callable, $route->callable()); + $this->assertEquals('foo/bar', $route->getPattern()); + $this->assertSame($callable, $route->getCallable()); } /** @@ -348,9 +348,9 @@ public function testSlimRunsBeforeAndAfterCallbacks() { public function testSlimCopiesViewData(){ $data = array('foo' => 'bar'); Slim::init(); - Slim::view()->data($data); + Slim::view()->setData($data); Slim::view('CustomView'); - $this->assertEquals($data, Slim::view()->data()); + $this->assertEquals($data, Slim::view()->getData()); } /************************************************ @@ -374,7 +374,7 @@ public function testSlimRenderSetsResponseStatusOk(){ Slim::init(); Slim::render('test.php', $data, 404); $this->assertEquals(Slim::response()->status(), 404); - $this->assertEquals($data, Slim::view()->data()); + $this->assertEquals($data, Slim::view()->getData()); $this->assertEquals(Slim::response()->status(), 404); } @@ -866,6 +866,112 @@ public function testSlimOkResponse() { $this->assertEquals(Slim::response()->body(), 'Ok'); } + /************************************************ + * SLIM HOOKS + ************************************************/ + + /** + * Test hook listener + * + * Pre-conditions: + * Slim app initialized; + * Hook name does not exist + * Listeners are callable objects + * + * Post-conditions: + * Hook is created; + * Callable objects are assigned to hook + */ + public function testHookValidListener() { + Slim::init(); + $callable1 = function ($app) {}; + $callable2 = function ($app) {}; + Slim::hook('test.hook.one', $callable1); + Slim::hook('test.hook.one', $callable2); + $hooksByKey = Slim::getHooks('test.hook.one'); + $this->assertArrayHasKey('test.hook.one', Slim::getHooks()); + $this->assertSame($hooksByKey[0], $callable1); + $this->assertSame($hooksByKey[1], $callable2); + } + + /** + * Test hook listener if listener is not callable + * + * Pre-conditions: + * Slim app initialized; + * Hook name does not exist; + * Listener is NOT a callable object + * + * Post-conditions: + * Hook is created; + * Callable object is NOT assigned to hook; + */ + public function testHookInvalidListener() { + Slim::init(); + $callable = 'test'; + Slim::hook('test.hook.one', $callable); + $this->assertEquals(array(), Slim::getHooks('test.hook.one')); + } + + /** + * Test hook invocation + * + * Pre-conditions: + * Slim app initialized; + * Hook name does not exist; + * Listener is a callable object + * + * Post-conditions: + * Hook listener is invoked + */ + public function testHookInvocation() { + $this->expectOutputString('Foo'); + Slim::init(); + Slim::hook('test.hook.one', function ($app) { + echo 'Foo'; + }); + Slim::hook('test.hook.one'); + } + + /** + * Test hook invocation if hook does not exist + * + * Pre-conditions: + * Slim app intialized; + * Hook name does not exist + * + * Post-conditions: + * Hook is created; + * Hook initialized with empty array + */ + public function testHookInvocationIfNotExists() { + Slim::init(); + Slim::hook('test.hook.one'); + $this->assertEquals(array(), Slim::getHooks('test.hook.one')); + } + + /** + * Test clear hooks + * + * Pre-conditions: + * Slim app initialized + * Two hooks exist, each with one listener + * + * Post-conditions: + * Case A: Listeners for 'test.hook.one' are cleared + * Case B: Listeners for all hooks are cleared + */ + public function testHookClear() { + Slim::init(); + Slim::hook('test.hook.one', function () {}); + Slim::hook('test.hook.two', function () {}); + Slim::clearHooks('test.hook.two'); + $this->assertEquals(array(), Slim::getHooks('test.hook.two')); + $this->assertTrue(count(Slim::getHooks('test.hook.one')) === 1); + Slim::clearHooks(); + $this->assertEquals(array(), Slim::getHooks('test.hook.one')); + } + } ?> \ No newline at end of file diff --git a/tests/ViewTest.php b/tests/ViewTest.php index b67dcb1e9..92a313510 100644 --- a/tests/ViewTest.php +++ b/tests/ViewTest.php @@ -1,4 +1,6 @@ assertEquals($this->view->data(), array()); + $this->assertEquals($this->view->getData(), array()); } /** - * Test View data is returned when set + * Test View sets and gets data * * Pre-conditions: - * You instantiate a View object and set its data - * - * Post-conditions: - * The latest View data is returned by the View::data method - */ - public function testViewReturnsDataWhenSet() { - $returnedData = $this->view->data($this->generateTestData()); - $this->assertEquals($this->generateTestData(), $returnedData); - } - - /** - * Test View appends data rather than overwriting data - * - * Pre-conditions: - * You instantiate a View object and call its data method - * multiple times to append multiple sets of data + * View is instantiated + * Case A: Set view data key/value + * Case B: Set view data as array + * Case C: Set view data with one argument that is not an array * * Post-conditions: - * The resultant data array should contain the merged - * data from the multiple View::data calls. + * Case A: Data key/value are set + * Case B: Data is set to array + * Case C: An InvalidArgumentException is thrown */ - public function testViewMergesData(){ - $dataOne = array('a' => 'A'); - $dataTwo = array('b' => 'B'); - $this->view->data($dataOne); - $this->view->data($dataTwo); - $this->assertEquals($this->view->data(), array('a' => 'A', 'b' => 'B')); + public function testViewSetAndGetData() { + //Case A + $this->view->setData('one', 1); + $this->assertEquals($this->view->getData('one'), 1); + + //Case B + $data = array('foo' => 'bar', 'a' => 'A'); + $this->view->setData($data); + $this->assertSame($this->view->getData(), $data); + + //Case C + try { + $this->view->setData('foo'); + $this->fail('Setting View data with non-array single argument did not throw exception'); + } catch ( InvalidArgumentException $e ) {} } - + /** - * Test View does not accept non-Array values + * Test View appends data * * Pre-conditions: - * You instantiate a View object and pass a non-Array value - * into its data method. + * View is instantiated + * Append data to View several times * * Post-conditions: - * The View ignores the invalid data and the View's - * existing data attribute remains unchanged. + * The View data contains all appended data */ - public function testViewDoesNotAcceptNonArrayAsData() { - $this->assertEquals($this->view->data(1), array()); + public function testViewAppendsData(){ + $this->view->appendData(array('a' => 'A')); + $this->view->appendData(array('b' => 'B')); + $this->assertEquals($this->view->getData(), array('a' => 'A', 'b' => 'B')); } /** - * Test View sets templates directory + * Test View templates directory * * Pre-conditions: - * You instantiate a View object and set its templates directory - * to an existing directory. + * View is instantiated + * View templates directory is set to an existing directory * * Post-conditions: * The templates directory is set correctly. */ public function testSetsTemplatesDirectory() { - $templatesDirectory = rtrim(realpath('../templates/'), '/') . '/'; - $this->view->templatesDirectory($templatesDirectory); - $this->assertEquals($templatesDirectory, $this->view->templatesDirectory()); + $templatesDirectory = '../templates'; + $this->view->setTemplatesDirectory($templatesDirectory); + $this->assertEquals($templatesDirectory, $this->view->getTemplatesDirectory()); } /** - * Test View templates directory path may have a trailing slash when set + * Test View templates directory may have a trailing slash when set * * Pre-conditions: - * You instantiate a View object and set its template directory to an - * existing directory path with a trailing slash. + * View is instantiated + * View templates directory is set to an existing directory with a trailing slash * * Post-conditions: - * The View templates directory path contains a trailing slash. + * The View templates directory is set correctly without a trailing slash */ public function testTemplatesDirectoryWithTrailingSlash() { - $templatesDirectory = realpath('../templates/'); - $this->view->templatesDirectory($templatesDirectory); - $this->assertEquals($templatesDirectory . '/', $this->view->templatesDirectory()); + $this->view->setTemplatesDirectory('../templates/'); + $this->assertEquals('../templates', $this->view->getTemplatesDirectory()); } /** - * Test View templates directory path may not have a trailing slash when set + * Test View throws Exception if templates directory does not exist * * Pre-conditions: - * You instantiate a View object and set its template directory to an - * existing directory path without a trailing slash. + * View is instantiated + * View templates directory is set to a non-existent directory * * Post-conditions: - * The View templates directory path contains a trailing slash. + * A RuntimeException is thrown */ - public function testTemplatesDirectoryWithoutTrailingSlash() { - $templatesDirectory = rtrim(realpath('../templates/'), '/'); - $this->view->templatesDirectory($templatesDirectory); - $this->assertEquals($templatesDirectory . '/', $this->view->templatesDirectory()); + public function testExceptionForInvalidTemplatesDirectory() { + $this->setExpectedException('RuntimeException'); + $this->view->setTemplatesDirectory('./foo'); } /** - * Test View throws Exception if templates directory does not exist + * Test View renders template * * Pre-conditions: - * You instantiate a View object and set its template directory - * to a non-existent directory. + * View is instantiated + * View templates directory is set to an existing directory. + * View data is set without errors + * Case A: View renders an existing template + * Case B: View renders a non-existing template * * Post-conditions: - * A RuntimeException is thrown + * Case A: The rendered template is returned as a string + * Case B: A RuntimeException is thrown */ - public function testExceptionForInvalidTemplatesDirectory() { - $this->setExpectedException('RuntimeException'); - $this->view->templatesDirectory('./foo'); + public function testRendersTemplateWithData() { + $this->view->setTemplatesDirectory('./templates'); + $this->view->setData(array('foo' => 'bar')); + + //Case A + $output = $this->view->render('test.php'); + $this->assertEquals($output, 'test output bar'); + + //Case B + try { + $output = $this->view->render('foo.php'); + $this->fail('Rendering non-existent template did not throw exception'); + } catch ( RuntimeException $e ) {} } - + /** - * Test View class renders template + * Test View displays template * * Pre-conditions: - * You instantiate a View object, sets its templates directory to - * an existing directory. You pass data into the View, and render - * an existing template. No errors or exceptions are thrown. + * View is instantiated + * View templates directory is set to an existing directory. + * View data is set without errors + * View is displayed * * Post-conditions: - * The contents of the output buffer match the template. + * The output buffer contains the rendered template */ - public function testRendersTemplateWithData() { - $this->view->templatesDirectory(realpath('./templates')); - ob_start(); - $this->view->data(array('foo' => 'bar')); - $this->view->render('test.php'); - $output = ob_get_clean(); - $this->assertEquals($output, 'test output bar'); + public function testDisplaysTemplateWithData() { + $this->expectOutputString('test output bar'); + $this->view->setTemplatesDirectory('./templates'); + $this->view->setData(array('foo' => 'bar')); + $this->view->display('test.php'); } } - -?> +?> \ No newline at end of file diff --git a/views/MustacheView.php b/views/MustacheView.php index e3015c794..26992df57 100644 --- a/views/MustacheView.php +++ b/views/MustacheView.php @@ -55,13 +55,13 @@ class MustacheView extends View { * * @see View::render() * @param string $template The template name specified in Slim::render() - * @return void + * @return string */ public function render( $template ) { require_once self::$mustacheDirectory . '/Mustache.php'; $m = new Mustache(); - $contents = file_get_contents($this->templatesDirectory() . $template); - echo $m->render($contents, $this->data); + $contents = file_get_contents($this->getTemplatesDirectory() . '/' . ltrim($template, '/')); + return $m->render($contents, $this->data); } } diff --git a/views/SmartyView.php b/views/SmartyView.php index b135d176e..b84087862 100644 --- a/views/SmartyView.php +++ b/views/SmartyView.php @@ -84,7 +84,7 @@ class SmartyView extends View { public function render( $template ) { $instance = self::getInstance(); $instance->assign($this->data); - echo $instance->fetch($template); + return $instance->fetch($template); } /** @@ -100,7 +100,7 @@ public static function getInstance() { } require_once self::$smartyDirectory . '/Smarty.class.php'; self::$smartyInstance = new Smarty(); - self::$smartyInstance->template_dir = is_null(self::$smartyTemplatesDirectory) ? $this->templatesDirectory() : self::$smartyTemplatesDirectory; + self::$smartyInstance->template_dir = is_null(self::$smartyTemplatesDirectory) ? $this->getTemplatesDirectory() : self::$smartyTemplatesDirectory; if ( self::$smartyCompileDirectory ) { self::$smartyInstance->compile_dir = self::$smartyCompileDirectory; } diff --git a/views/TwigView.php b/views/TwigView.php index ee6e2a579..a80c0b903 100644 --- a/views/TwigView.php +++ b/views/TwigView.php @@ -69,7 +69,7 @@ class TwigView extends View { public function render( $template ) { $env = $this->getEnvironment(); $template = $env->loadTemplate($template); - echo $template->render($this->data); + return $template->render($this->data); } /** @@ -81,7 +81,7 @@ private function getEnvironment() { if ( !$this->twigEnvironment ) { require_once self::$twigDirectory . '/Autoloader.php'; Twig_Autoloader::register(); - $loader = new Twig_Loader_Filesystem($this->templatesDirectory()); + $loader = new Twig_Loader_Filesystem($this->getTemplatesDirectory()); $this->twigEnvironment = new Twig_Environment( $loader, self::$twigOptions