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