summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/Router.php150
-rw-r--r--src/Router/Router.php553
-rw-r--r--src/Router/Routes.php10
-rw-r--r--src/Router/Runner.php38
-rw-r--r--src/Router/Runner/Callback.php26
-rw-r--r--src/Router/Runner/Controller.php39
-rw-r--r--src/Router/Runner/PhpScript.php66
-rw-r--r--tests/Router/Routes/GlobTest.php5
-rw-r--r--tests/Router/Runner/CallbackTest.php64
-rw-r--r--tests/Router/Runner/ControllerTest.php140
-rw-r--r--tests/Router/Runner/PhpScriptTest.php94
-rw-r--r--tests/Router/RunnerTest.php79
-rw-r--r--tests/RouterTest.php227
13 files changed, 876 insertions, 615 deletions
diff --git a/src/Router.php b/src/Router.php
new file mode 100644
index 0000000..955e1e1
--- /dev/null
+++ b/src/Router.php
@@ -0,0 +1,150 @@
+<?php
+
+namespace Jasny;
+
+use Jasny\Router\Runner;
+use Jasny\Router\Routes\Glob;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Message\ResponseInterface;
+
+/**
+ * Route pretty URLs to correct controller
+ */
+class Router
+{
+ /**
+ * Specific routes
+ * @var array
+ */
+ protected $routes = [];
+
+ /**
+ * Middlewares actions
+ * @var array
+ **/
+ protected $middlewares = [];
+
+ /**
+ * Class constructor
+ *
+ * @param array $routes
+ */
+ public function __construct(array $routes)
+ {
+ $this->routes = $routes;
+ }
+
+ /**
+ * Get a list of all routes
+ *
+ * @return object
+ */
+ public function getRoutes()
+ {
+ return $this->routes;
+ }
+
+ /**
+ * Get middlewares
+ *
+ * @return array
+ */
+ public function getMiddlewares()
+ {
+ return $this->middlewares;
+ }
+
+ /**
+ * Add middleware call to router
+ *
+ * @param callback $middleware
+ * @return Router $this
+ */
+ public function add($middleware)
+ {
+ if (!is_callable($middleware)) {
+ throw new \InvalidArgumentException("Middleware should be a callable");
+ }
+
+ $this->middlewares[] = $middleware;
+
+ return $this;
+ }
+
+ /**
+ * Run the action for the request
+ *
+ * @param ServerRequestInterface $request
+ * @param ResponseInterface $response
+ * @return ResponseInterface
+ */
+ final public function run(ServerRequestInterface $request, ResponseInterface $response)
+ {
+ return $this->__invoke($request, $response);
+ }
+
+ /**
+ * Run the action for the request (optionally as middleware), previously running middlewares, if any
+ *
+ * @param ServerRequestInterface $request
+ * @param ResponseInterface $response
+ * @param callback $next
+ * @return ResponseInterface
+ */
+ public function __invoke(ServerRequestInterface $request, ResponseInterface $response, $next = null)
+ {
+ $handle = [$this, 'handle'];
+
+ #Call to $this->handle will be executed last in the chain of middlewares
+ $next = function(ServerRequestInterface $request, ResponseInterface $response) use ($next, $handle) {
+ return call_user_func($handle, $request, $response, $next);
+ };
+
+ #Build middlewares call chain, so that the last added was executed in first place
+ foreach ($this->middlewares as $middleware) {
+ $next = function(ServerRequestInterface $request, ResponseInterface $response) use ($next, $middleware) {
+ return $middleware($request, $response, $next);
+ };
+ }
+
+ return $next($request, $response);
+ }
+
+ /**
+ * Run the action
+ *
+ * @param ServerRequestInterface $request
+ * @param ResponseInterface $response
+ * @param callback $next
+ * @return ResponseInterface
+ */
+ protected function handle(ServerRequestInterface $request, ResponseInterface $response, $next = null)
+ {
+ $glob = new Glob($this->routes);
+ $route = $glob->getRoute($request);
+
+ if (!$route) return $this->notFound($response);
+
+ $runner = Runner::create($route);
+
+ return $runner($request, $response, $next);
+ }
+
+ /**
+ * Return 'Not Found' response
+ *
+ * @param ResponseInterface $response
+ * @return ResponseInterface
+ */
+ protected function notFound(ResponseInterface $response)
+ {
+ $message = 'Not Found';
+
+ $body = $response->getBody();
+ $body->rewind();
+ $body->write($message);
+
+ return $response->withStatus(404, $message)->withBody($body);
+ }
+}
+
diff --git a/src/Router/Router.php b/src/Router/Router.php
deleted file mode 100644
index a0b62eb..0000000
--- a/src/Router/Router.php
+++ /dev/null
@@ -1,553 +0,0 @@
-<?php
-
-namespace Jasny;
-
-/**
- * Route pretty URLs to correct controller
- *
- * Wildcards:
- * ? Single character
- * # One or more digits
- * * One or more characters
- * ** Any number of subdirs
- * [abc] Match character 'a', 'b' or 'c'
- * [a-z] Match character 'a' to 'z'
- * {png,gif} Match 'png' or 'gif'
- *
- * Escape characters using URL encode (so %5B for '[')
- */
-class Router
-{
- /**
- * Specific routes
- * @var array
- */
- protected $routes = [];
-
- /**
- * Method for route
- * @var string
- */
- protected $method;
-
- /**
- * Webroot subdir from DOCUMENT_ROOT.
- * @var string
- */
- protected $base;
-
- /**
- * URL to route
- * @var string
- */
- protected $url;
-
- /**
- * Variables from matched route (cached)
- * @var object
- */
- protected $route;
-
-
- /**
- * Class constructor
- *
- * @param array $routes Array with route objects
- */
- public function __construct($routes=null)
- {
- if (isset($routes)) $this->setRoutes($routes);
- }
-
- /**
- * Set the routes
- *
- * @param array $routes Array with route objects
- * @return Router
- */
- public function setRoutes($routes)
- {
- if (is_object($routes)) $routes = get_object_vars($routes);
-
- foreach ($routes as &$route) {
- if ($route instanceof \Closure) $route = (object)['fn' => $route];
- }
-
- $this->routes = $routes;
- $this->route = null;
-
- return $this;
- }
-
- /**
- * Add routes to existing list
- *
- * @param array $routes Array with route objects
- * @param string $root Specify the root dir for routes
- * @return Router
- */
- public function addRoutes($routes, $root=null)
- {
- if (is_object($routes)) $routes = get_object_vars($routes);
-
- foreach ($routes as $path=>$route) {
- if (!empty($root)) $path = $root . $path;
-
- if (isset($this->routes[$path])) {
- trigger_error("Route $path is already defined.", E_USER_WARNING);
- continue;
- }
-
- if ($route instanceof \Closure) $route = (object)['fn' => $route];
-
- $this->routes[$path] = $route;
- }
-
- return $this;
- }
-
- /**
- * Get a list of all routes
- *
- * @return object
- */
- public function getRoutes()
- {
- return $this->routes;
- }
-
-
- /**
- * Set the method to route
- *
- * @param string $method
- * @return Router
- */
- public function setMethod($method)
- {
- $this->method = $method;
- $this->route = null;
-
- return $this;
- }
-
- /**
- * Get the method to route.
- * Defaults to REQUEST_METHOD, which can be overwritten by $_POST['_method'].
- *
- * @return string
- */
- public function getMethod()
- {
- if (!isset($this->method)) $this->method = Request::getMethod();
- return $this->method;
- }
-
-
- /**
- * Set the webroot subdir from DOCUMENT_ROOT.
- *
- * @param string $dir
- * @return Router
- */
- public function setBase($dir)
- {
- $this->base = rtrim($dir, '/');
- $this->route = null;
-
- return $this;
- }
-
- /**
- * Get the webroot subdir from DOCUMENT_ROOT.
- *
- * @return string
- */
- public function getBase()
- {
- return $this->base;
- }
-
- /**
- * Add a base path to the URL if the webroot isn't the same as the webservers document root
- *
- * @param string $url
- * @return string
- */
- public function rebase($url)
- {
- return ($this->getBase() ?: '/') . ltrim($url, '/');
- }
-
-
- /**
- * Set the URL to route
- *
- * @param string $url
- * @return Router
- */
- public function setUrl($url)
- {
- $this->url = $url;
- $this->route = null;
-
- return $this;
- }
-
- /**
- * Get the URL to route.
- * Defaults to REQUEST_URI.
- *
- * @return string
- */
- public function getUrl()
- {
- if (!isset($this->url)) $this->url = urldecode(preg_replace('/\?.*$/', '', $_SERVER['REQUEST_URI']));
- return $this->url;
- }
-
- /**
- * Split the URL and return a part
- *
- * @param int $i Part number, starts at 1
- * @return string
- */
- public function getUrlPart($i)
- {
- $parts = $this->splitUrl($this->getUrl());
- return $parts[$i - 1];
- }
-
-
- /**
- * Check if the router has been used.
- *
- * @return boolean
- */
- public function isUsed()
- {
- return isset($this->route);
- }
-
-
- /**
- * Get a matching route.
- *
- * @return object
- */
- public function getRoute()
- {
- if (isset($this->route)) return $this->route;
-
- $method = $this->getMethod();
- $url = $this->getUrl();
-
- if ($this->getBase()) {
- $url = '/' . preg_replace('~^' . preg_quote(trim($this->getBase(), '/'), '~') . '~', '', ltrim($url, '/'));
- }
-
- $match = $this->findRoute($method, $url);
-
- if ($match) {
- $this->route = $this->bind($this->routes[$match], $this->splitUrl($url));
- $this->route->route = $match;
- } else {
- $this->route = false;
- }
-
- return $this->route;
- }
-
- /**
- * Get a property of the matching route.
- *
- * @param string $prop Property name
- * @return mixed
- */
- public function get($prop)
- {
- $route = $this->getRoute();
- return isset($route->$prop) ? $route->$prop : null;
- }
-
-
- /**
- * Execute the action of the given route.
- *
- * @param object $route
- * @param object $overwrite
- * @return boolean|mixed Whatever the controller returns or true on success
- */
- public function routeTo($route, $overwrite=[])
- {
- if (!is_object($route)) {
- $match = $this->findRoute(null, $route);
- if (!isset($match) || !isset($this->routes[$match])) return false;
- $route = $this->routes[$match];
- }
-
- foreach ($overwrite as $key=>$value) {
- $route->$key = $value;
- }
-
- if (isset($route->controller)) return $this->routeToController($route);
- if (isset($route->fn)) return $this->routeToCallback($route);
- if (isset($route->file)) return $this->routeToFile($route);
-
- $warn = "Failed to route using '{$route->route}': Neither 'controller', 'fn' or 'file' is set";
- trigger_error($warn, E_USER_WARNING);
-
- return false;
- }
-
- /**
- * Route to controller action
- *
- * @param object $route
- * @return mixed|boolean
- */
- protected function routeToController($route)
- {
- $class = $this->getControllerClass($route->controller);
- $method = $this->getActionMethod(isset($route->action) ? $route->action : null);
-
- if (!class_exists($class)) return false;
-
- $controller = new $class($this);
- if (!is_callable([$controller, $method])) return false;
-
- if (isset($route->args)) {
- $args = $route->args;
- } elseif (method_exists($controller, $method)) {
- $args = static::getFunctionArgs($route, new \ReflectionMethod($controller, $method));
- }
-
- $ret = call_user_func_array([$controller, $method], $args);
- return isset($ret) ? $ret : true;
- }
-
- /**
- * Route to a callback function
- *
- * @param object $route
- * @return mixed|boolean
- */
- protected function routeToCallback($route)
- {
- if (!is_callable($route->fn)) {
- trigger_error("Failed to route using '{$route->route}': Invalid callback.", E_USER_WARNING);
- return false;
- }
-
- if (isset($route->args)) {
- $args = $route->args;
- } elseif (is_array($route->fn)) {
- $args = static::getFunctionArgs($route, new \ReflectionMethod($route->fn[0], $route->fn[1]));
- } elseif (function_exists($route->fn)) {
- $args = static::getFunctionArgs($route, new \ReflectionFunction($route->fn));
- }
-
- return call_user_func_array($route->fn, $args);
- }
-
-
- /**
- * Execute the action.
- *
- * @todo Check if route would be available for other HTTP methods to respond with a 405
- *
- * @return mixed Whatever the controller returns
- */
- public function execute()
- {
- $route = $this->getRoute();
- if ($route) $ret = $this->routeTo($route);
-
- $httpCode = 404; // or 405?
-
- if (!isset($ret) || $ret === false) return $this->notFound(null, $httpCode);
- return $ret;
- }
-
-
- /**
- * Redirect to another page
- *
- * @param string $url
- * @param int $httpCode 301 (Moved Permanently), 303 (See Other) or 307 (Temporary Redirect)
- */
- public function redirect($url, $httpCode=303)
- {
- if (ob_get_level() > 1) ob_end_clean();
-
- if ($url[0] === '/' && substr($url, 0, 2) !== '//') $url = $this->rebase($url);
-
- http_response_code((int)$httpCode);
- header("Location: $url");
-
- header('Content-Type: text/html');
- echo 'You are being redirected to <a href="' . $url . '">' . $url . '</a>';
- }
-
- /**
- * Give a 400 Bad Request response
- *
- * @param string $message
- * @param int $httpCode Alternative HTTP status code, eg. 406 (Not Acceptable)
- * @param mixed $.. Additional arguments are passed to action
- */
- public function badRequest($message, $httpCode=400)
- {
- if (!$this->routeTo(400, ['args'=>func_get_args()])) {
- self::outputError($httpCode, $message);
- }
- }
-
- /**
- * Route to 401, otherwise result in a 403 forbidden.
- * Note: While the 401 route is used, we don't respond with a 401 http status code.
- */
- public function requireLogin()
- {
- $this->routeTo(401) || $this->forbidden();
- }
-
- /**
- * Give a 403 Forbidden response and exit
- *
- * @param string $message
- * @param int $httpCode Alternative HTTP status code
- * @param mixed $.. Additional arguments are passed to action
- */
- public function forbidden($message=null, $httpCode=403)
- {
- if (ob_get_level() > 1) ob_end_clean();
-
- if (!$this->routeTo(403, ['args'=>func_get_args()])) {
- if (!isset($message)) $message = "Sorry, you are not allowed to view this page";
- self::outputError($httpCode, $message);
- }
- }
-
- /**
- * Give a 404 Not Found response
- *
- * @param string $message
- * @param int $httpCode Alternative HTTP status code, eg. 410 (Gone)
- * @param mixed $.. Additional arguments are passed to action
- */
- public function notFound($message=null, $httpCode=404)
- {
- if (ob_get_level() > 1) ob_end_clean();
-
- if (!$this->routeTo(404, ['args'=>func_get_args()])) {
- if (!isset($message)) $message = $httpCode === 405 ?
- "Sorry, this action isn't supported" :
- "Sorry, this page does not exist";
-
- self::outputError($httpCode, $message);
- }
- }
-
- /**
- * Give a 5xx Server Error response
- *
- * @param string $message
- * @param int|string $httpCode HTTP status code, eg. "500 Internal Server Error" or 503
- * @param mixed $.. Additional arguments are passed to action
- */
- public function error($message=null, $httpCode=500)
- {
- if (ob_get_level() > 1) ob_end_clean();
-
- if (!$this->routeTo(500, ['args'=>func_get_args()])) {
- if (!isset($message)) $message = "Sorry, an unexpected error occured";
- self::outputError($httpCode, $message);
- }
- }
-
-
- /**
- * Get the arguments for a function from a route using reflection
- *
- * @param object $route
- * @param \ReflectionFunctionAbstract $refl
- * @return array
- */
- protected static function getFunctionArgs($route, \ReflectionFunctionAbstract $refl)
- {
- $args = [];
- $params = $refl->getParameters();
-
- foreach ($params as $param) {
- $key = $param->name;
-
- if (property_exists($route, $key)) {
- $value = $route->{$key};
- } else {
- if (!$param->isOptional()) {
- $fn = $refl instanceof \ReflectionMethod
- ? $refl->getDeclaringClass()->getName() . ':' . $refl->getName()
- : $refl->getName();
-
- trigger_error("Missing argument '$key' for $fn()", E_USER_WARNING);
- }
-
- $value = $param->isDefaultValueAvailable() ? $param->getDefaultValue() : null;
- }
-
- $args[$key] = $value;
- }
-
- return $args;
- }
-
- /**
- * Get the class name of the controller
- *
- * @param string $controller
- * @return string
- */
- protected static function getControllerClass($controller)
- {
- return \Jasny\studlycase($controller) . 'Controller';
- }
-
- /**
- * Get the method name of the action
- *
- * @param string $action
- * @return string
- */
- protected static function getActionMethod($action)
- {
- return \Jasny\camelcase($action) . 'Action';
- }
-
-
- // Proxy methods for Jasny\DB\Request. Allows overloading for customized Request class.
-
- /**
- * Get the output format.
- * Tries 'Content-Type' response header, otherwise uses 'Accept' request header.
- *
- * @param string $as 'short' or 'mime'
- * @return string
- */
- protected static function getOutputFormat($as)
- {
- return Request::getOutputFormat($as);
- }
-
- /**
- * Output an HTTP error
- *
- * @param int $httpCode HTTP status code
- * @param string|object $message
- * @param string $format The output format (auto detect by default)
- */
- protected static function outputError($httpCode, $message, $format=null)
- {
- return Request::outputError($httpCode, $message, $format);
- }
-}
diff --git a/src/Router/Routes.php b/src/Router/Routes.php
index 26d407c..ce4fa1a 100644
--- a/src/Router/Routes.php
+++ b/src/Router/Routes.php
@@ -2,7 +2,7 @@
namespace Jasny\Router;
-use Psr\Http\Message\ServerRequestInterface as ServerRequest;
+use Psr\Http\Message\ServerRequestInterface;
/**
* Collection of routes
@@ -12,16 +12,16 @@ interface Routes
/**
* Check if a route for the request exists
*
- * @param ServerRequest $request
+ * @param ServerRequestInterface $request
* @return boolean
*/
- public function hasRoute(ServerRequest $request);
+ public function hasRoute(ServerRequestInterface $request);
/**
* Get route for the request
*
- * @param ServerRequest $request
+ * @param ServerRequestInterface $request
* @return Route
*/
- public function getRoute(ServerRequest $request);
+ public function getRoute(ServerRequestInterface $request);
}
diff --git a/src/Router/Runner.php b/src/Router/Runner.php
index 40c8112..51300a4 100644
--- a/src/Router/Runner.php
+++ b/src/Router/Runner.php
@@ -2,7 +2,7 @@
namespace Jasny\Router;
-use Psr\Http\Message\RequestInterface;
+use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;
use Jasny\Router\Route;
@@ -25,18 +25,45 @@ abstract class Runner
{
$this->route = $route;
}
+
+ /**
+ * Get runner route
+ *
+ * @return Route
+ */
+ public function getRoute()
+ {
+ return $this->route;
+ }
/**
* Invoke the action specified in the route
*
- * @param RequestInterface $request
+ * @param ServerRequestInterface $request
* @param ResponseInterface $response
- * @param callback $next Callback for if runner is used as middleware
* @return ResponseInterface
*/
- abstract public function __invoke(RequestInterface $request, ResponseInterface $response, $next = null);
+ abstract public function run(ServerRequestInterface $request, ResponseInterface $response);
-
+ /**
+ * Invoke the action specified in the route and call the next method
+ *
+ * @param ServerRequestInterface $request
+ * @param ResponseInterface $response
+ * @param callback $next Callback for if runner is used as middleware
+ * @return ResponseInterface
+ */
+ public function __invoke(ServerRequestInterface $request, ResponseInterface $response, $next = null)
+ {
+ $response = $this->run($request, $response);
+
+ if (isset($next)) {
+ $response = call_user_func($next, $request, $response);
+ }
+
+ return $response;
+ }
+
/**
* Factory method
*
@@ -59,3 +86,4 @@ abstract class Runner
return new $class($route);
}
}
+
diff --git a/src/Router/Runner/Callback.php b/src/Router/Runner/Callback.php
index 031dcb1..34ebc4c 100644
--- a/src/Router/Runner/Callback.php
+++ b/src/Router/Runner/Callback.php
@@ -1,15 +1,33 @@
<?php
-namespace Jasny\Router\Route;
+namespace Jasny\Router\Runner;
-use Jasny\Router\Route;
+use Jasny\Router\Runner;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Message\ResponseInterface;
/**
* Description of Callback
*
* @author arnold
*/
-class Callback extends Route
+class Callback extends Runner
{
- //put your code here
+ /**
+ * Use function to handle request and response
+ *
+ * @param ServerRequestInterface $request
+ * @param ResponseInterface $response
+ * @return ResponseInterface|mixed
+ */
+ public function run(ServerRequestInterface $request, ResponseInterface $response)
+ {
+ $callback = !empty($this->route->fn) ? $this->route->fn : null;
+
+ if (!is_callable($callback)) {
+ throw new \RuntimeException("'fn' property of route shoud be a callable");
+ }
+
+ return call_user_func($callback, $request, $response);
+ }
}
diff --git a/src/Router/Runner/Controller.php b/src/Router/Runner/Controller.php
new file mode 100644
index 0000000..0488be5
--- /dev/null
+++ b/src/Router/Runner/Controller.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace Jasny\Router\Runner;
+
+use Jasny\Router\Runner;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Message\ResponseInterface;
+
+/**
+ * Description of Controller
+ *
+ * @author arnold
+ */
+class Controller extends Runner
+{
+ /**
+ * Route to a controller
+ *
+ * @param ServerRequestInterface $request
+ * @param ResponseInterface $response
+ * @return ResponseInterface|mixed
+ */
+ public function run(ServerRequestInterface $request, ResponseInterface $response)
+ {
+ $class = !empty($this->route->controller) ? $this->route->controller : null;
+
+ if (!class_exists($class)) {
+ throw new \RuntimeException("Can not route to controller '$class': class not exists");
+ }
+
+ if (!method_exists($class, '__invoke')) {
+ throw new \RuntimeException("Can not route to controller '$class': class does not have '__invoke' method");
+ }
+
+ $controller = new $class($this->route);
+
+ return $controller($request, $response);
+ }
+}
diff --git a/src/Router/Runner/PhpScript.php b/src/Router/Runner/PhpScript.php
index 93cac8a..f6595a3 100644
--- a/src/Router/Runner/PhpScript.php
+++ b/src/Router/Runner/PhpScript.php
@@ -1,73 +1,47 @@
<?php
-namespace Jasny\Router\Route;
+namespace Jasny\Router\Runner;
-use Jasny\Router\Route;
-use Psr\Http\Message\ResponseInterface as Response;
+use Jasny\Router\Runner;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Message\ResponseInterface;
/**
* Route to a PHP script
*/
-class PhpScript extends Route
-{
+class PhpScript extends Runner
+{
/**
- * Route key
- * @var string
- */
- protected $key;
-
- /**
- * Script path
- * @var string
- */
- public $file;
-
-
- /**
- * Class constructor
- *
- * @param string $file
- * @param string $values
- */
- public function __construct($key, $file, $values)
- {
- parent::__construct($values);
-
- $this->key = $key;
- $this->file = $file;
- }
-
- /**
- * Return route key
+ * Return route file path
*
* @return string
*/
public function __toString()
{
- echo (string)$this->key;
- }
-
+ return (string)$this->route->file;
+ }
/**
* Route to a file
*
- * @param object $route
- * @return Response|mixed
+ * @param ServerRequestInterface $request
+ * @param ResponseInterface $response
+ * @return ResponseInterface|mixed
*/
- protected function execute()
+ public function run(ServerRequestInterface $request, ResponseInterface $response)
{
- $file = ltrim($this->file, '/');
+ $file = !empty($this->route->file) ? ltrim($this->route->file, '/') : '';
if (!file_exists($file)) {
- trigger_error("Failed to route using '$this': File '$file' doesn't exist.", E_USER_WARNING);
- return false;
+ throw new \RuntimeException("Failed to route using '$file': File '$file' doesn't exist.");
}
- if ($this->file[0] === '~' || strpos($this->file, '..') !== false || strpos($this->file, ':') !== false) {
- trigger_error("Won't route using '$this': '~', '..' and ':' not allowed in filename.", E_USER_WARNING);
- return false;
+ if ($file[0] === '~' || strpos($file, '..') !== false) {
+ throw new \RuntimeException("Won't route using '$file': '~', '..' are not allowed in filename.");
}
- return include $file;
+ $result = include $file;
+
+ return $result === true ? $response : $result;
}
}
diff --git a/tests/Router/Routes/GlobTest.php b/tests/Router/Routes/GlobTest.php
index fef0f64..e1ab551 100644
--- a/tests/Router/Routes/GlobTest.php
+++ b/tests/Router/Routes/GlobTest.php
@@ -107,7 +107,7 @@ class GlobTest extends \PHPUnit_Framework_TestCase
*/
public function testOffsetSet($pattern, $options, $exception)
{
- if ($exception) $this->setExpectedException($exception);
+ if ($exception) $this->expectException($exception);
$glob = new Glob();
$glob->offsetSet($pattern, $options);
@@ -133,6 +133,7 @@ class GlobTest extends \PHPUnit_Framework_TestCase
['/foo/*', ['file' => 'bar'], ''],
['', ['controller' => 'bar'], BadMethodCallException::class],
['foo', 'bar', InvalidArgumentException::class],
+ ['', '', BadMethodCallException::class]
];
}
@@ -325,7 +326,7 @@ class GlobTest extends \PHPUnit_Framework_TestCase
*/
public function testBindVarMultipleUrlParts($uri, $options, $positive, $exception)
{
- if ($exception) $this->setExpectedException(InvalidArgumentException::class);
+ if ($exception) $this->expectException(InvalidArgumentException::class);
$values = [$uri => $options];
$glob = new Glob($values);
diff --git a/tests/Router/Runner/CallbackTest.php b/tests/Router/Runner/CallbackTest.php
new file mode 100644
index 0000000..b4ee4ef
--- /dev/null
+++ b/tests/Router/Runner/CallbackTest.php
@@ -0,0 +1,64 @@
+<?php
+
+use Jasny\Router\Route;
+use Jasny\Router\Runner\Callback;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Message\ResponseInterface;
+
+class CallbackTest extends PHPUnit_Framework_TestCase
+{
+ /**
+ * Test creating Callback runner
+ *
+ * @dataProvider callbackProvider
+ * @param Route $route
+ * @param boolean $positive
+ */
+ public function testCallback($route, $positive)
+ {
+ $runner = new Callback($route);
+ $this->assertEquals($route, $runner->getRoute(), "Route was not set correctly");
+
+ $request = $this->createMock(ServerRequestInterface::class);
+ $response = $this->createMock(ResponseInterface::class);
+
+ if (!$positive) $this->expectException(\RuntimeException::class);
+ $result = $runner->run($request, $response);
+
+ if (!$positive) return;
+
+ $this->assertEquals($request, $result['request'], "Request object was not passed correctly to result");
+ $this->assertEquals($response, $result['response'], "Response object was not passed correctly to result");
+ }
+
+
+ /**
+ * Provide data fpr testing 'create' method
+ */
+ public function callbackProvider()
+ {
+ $callback = function($request, $response) {
+ return ['request' => $request, 'response' => $response];
+ };
+
+ return [
+ [Route::create(['fn' => $callback, 'value' => 'test']), true],
+ [Route::create(['fn' => [$this, 'getCallback'], 'value' => 'test']), true],
+ [Route::create(['controller' => 'TestController', 'value' => 'test']), false],
+ [Route::create(['file' => 'some_file.php', 'value' => 'test']), false],
+ [Route::create(['test' => 'test']), false],
+ ];
+ }
+
+ /**
+ * Testable callback for creating Route
+ *
+ * @param ServerRequestInterface $request
+ * @param ResponseInterface $response
+ * @return array
+ */
+ public function getCallback($request, $response)
+ {
+ return ['request' => $request, 'response' => $response];
+ }
+}
diff --git a/tests/Router/Runner/ControllerTest.php b/tests/Router/Runner/ControllerTest.php
new file mode 100644
index 0000000..1d64622
--- /dev/null
+++ b/tests/Router/Runner/ControllerTest.php
@@ -0,0 +1,140 @@
+<?php
+
+use Jasny\Router\Route;
+use Jasny\Router\Runner\Controller;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Message\ResponseInterface;
+
+class ControllerTest extends PHPUnit_Framework_TestCase
+{
+ /**
+ * Tmp scripts
+ * @var array
+ **/
+ public static $files = [];
+
+ /**
+ * Test creating Controller runner
+ *
+ * @dataProvider phpScriptProvider
+ * @param Route $route
+ * @param boolean $positive
+ */
+ public function testPhpScript($route, $positive)
+ {
+ $runner = new Controller($route);
+ $this->assertEquals($route, $runner->getRoute(), "Route was not set correctly");
+
+ $request = $this->createMock(ServerRequestInterface::class);
+ $response = $this->createMock(ResponseInterface::class);
+
+ if (!$positive) $this->expectException(\RuntimeException::class);
+ $result = $runner->run($request, $response);
+
+ $this->assertEquals($request, $result['request'], "Request object was not passed correctly to result");
+ $this->assertEquals($response, $result['response'], "Response object was not passed correctly to result");
+ }
+
+ /**
+ * Provide data for testing 'create' method
+ */
+ public function phpScriptProvider()
+ {
+ foreach (['noInvoke', 'withInvoke'] as $type) {
+ list($class, $path) = static::createTmpScript($type);
+ static::$files[$type] = compact('class', 'path');
+ }
+
+ return [
+ [Route::create(['test' => 'test']), false],
+ [Route::create(['fn' => 'testFunction', 'value' => 'test']), false],
+ [Route::create(['controller' => 'TestController', 'value' => 'test']), false],
+ [Route::create(['controller' => '', 'value' => 'test']), false],
+ [Route::create(['controller' => static::$files['noInvoke']['class'], 'path' => static::$files['noInvoke']['path']]), false],
+ [Route::create(['controller' => static::$files['withInvoke']['class'], 'path' => static::$files['withInvoke']['path']]), true],
+ ];
+ }
+
+ /**
+ * Delete tmp test scripts
+ */
+ public static function tearDownAfterClass()
+ {
+ foreach (static::$files as $path) {
+ unlink($path['path']);
+ }
+ }
+
+ /**
+ * Create single tmp script file for testing
+ *
+ * @param string $type ('returnTrue', 'returnNotTrue')
+ * @return string $path
+ */
+ public static function createTmpScript($type)
+ {
+ $dir = rtrim(sys_get_temp_dir(), '/');
+
+ do {
+ $name = static::getRandomString() . '-test-script.php';
+ $path = $dir . '/' . $name;
+
+ if (!file_exists($path)) break;
+ } while (true);
+
+ if ($type === 'noInvoke') {
+ $class = 'RunnerTestConrtollerInvalid';
+ $content =
+<<<CONTENT
+<?php
+
+class $class {
+ public \$route = null;
+
+ public function __construct(\$route)
+ {
+ \$this->route = \$route;
+ }
+}
+CONTENT;
+ } else {
+ $class = 'RunnerTestConrtoller';
+ $content =
+<<<CONTENT
+<?php
+
+class $class {
+ public \$route = null;
+
+ public function __construct(\$route)
+ {
+ \$this->route = \$route;
+ }
+
+ public function __invoke(Psr\Http\Message\ServerRequestInterface \$request, Psr\Http\Message\ResponseInterface \$response)
+ {
+ return ['request' => \$request, 'response' => \$response];
+ }
+}
+CONTENT;
+ }
+
+ $bytes = file_put_contents($path, $content);
+ static::assertTrue((int)$bytes > 0);
+
+ require_once $path;
+
+ return [$class, $path];
+ }
+
+ /**
+ * Get random string of given length (no more then length of md5 hash)
+ *
+ * @param int $length
+ * @return string
+ */
+ public static function getRandomString($length = 10)
+ {
+ return substr(md5(microtime(true) * mt_rand()), 0, $length);
+ }
+}
diff --git a/tests/Router/Runner/PhpScriptTest.php b/tests/Router/Runner/PhpScriptTest.php
new file mode 100644
index 0000000..12b9219
--- /dev/null
+++ b/tests/Router/Runner/PhpScriptTest.php
@@ -0,0 +1,94 @@
+<?php
+
+use Jasny\Router\Route;
+use Jasny\Router\Runner\PhpScript;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Message\ResponseInterface;
+
+class PhpScriptTest extends PHPUnit_Framework_TestCase
+{
+ /**
+ * Test creating PhpScript runner
+ *
+ * @dataProvider phpScriptProvider
+ * @param Route $route
+ * @param boolean $positive
+ */
+ public function testPhpScript($route, $positive)
+ {
+ $runner = new PhpScript($route);
+ $this->assertEquals($route, $runner->getRoute(), "Route was not set correctly");
+
+ $request = $this->createMock(ServerRequestInterface::class);
+ $response = $this->createMock(ResponseInterface::class);
+
+ if (!$positive) $this->expectException(\RuntimeException::class);
+ $result = $runner->run($request, $response);
+
+ if (!$positive) return;
+
+ $this->assertEquals($runner->getRoute()->file, (string)$runner);
+
+ if ($route->type === 'returnTrue') {
+ $this->assertEquals($response, $result, "Request object was not returned as result");
+ } else {
+ $this->assertEquals($request, $result['request'], "Request object was not passed correctly to result");
+ $this->assertEquals($response, $result['response'], "Response object was not passed correctly to result");
+ }
+
+ unlink($route->file);
+ }
+
+ /**
+ * Provide data fpr testing 'create' method
+ */
+ public function phpScriptProvider()
+ {
+ return [
+ [Route::create(['test' => 'test']), false],
+ [Route::create(['fn' => 'testFunction', 'value' => 'test']), false],
+ [Route::create(['controller' => 'TestController', 'value' => 'test']), false],
+ [Route::create(['file' => '', 'value' => 'test']), false],
+ [Route::create(['file' => 'some_file.php', 'value' => 'test']), false],
+ [Route::create(['file' => '../' . basename(getcwd()), 'value' => 'test']), false],
+ [Route::create(['file' => $this->createTmpScript('returnTrue'), 'type' => 'returnTrue']), true],
+ [Route::create(['file' => $this->createTmpScript('returnNotTrue'), 'type' => 'returnNotTrue']), true]
+ ];
+ }
+
+ /**
+ * Create single tmp script file for testing
+ *
+ * @param string $type ('returnTrue', 'returnNotTrue')
+ * @return string $path
+ */
+ public function createTmpScript($type)
+ {
+ $dir = rtrim(sys_get_temp_dir(), '/');
+
+ do {
+ $name = $this->getRandomString() . '-test-script.php';
+ $path = $dir . '/' . $name;
+
+ if (!file_exists($path)) break;
+ } while (true);
+
+ $content = $type === 'returnTrue' ? "<?php\n return true;" : "<?php\n return ['request' => \$request, 'response' => \$response];";
+ $bytes = file_put_contents($path, $content);
+
+ $this->assertTrue((int)$bytes > 0);
+
+ return $path;
+ }
+
+ /**
+ * Get random string of given length (no more then length of md5 hash)
+ *
+ * @param int $length
+ * @return string
+ */
+ public function getRandomString($length = 10)
+ {
+ return substr(md5(microtime(true) * mt_rand()), 0, $length);
+ }
+}
diff --git a/tests/Router/RunnerTest.php b/tests/Router/RunnerTest.php
new file mode 100644
index 0000000..60b80b9
--- /dev/null
+++ b/tests/Router/RunnerTest.php
@@ -0,0 +1,79 @@
+<?php
+
+use Jasny\Router\Route;
+use Jasny\Router\Runner;
+use Jasny\Router\Runner\Controller;
+use Jasny\Router\Runner\Callback;
+use Jasny\Router\Runner\PhpScript;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Message\ResponseInterface;
+
+class RunnerTest extends PHPUnit_Framework_TestCase
+{
+ /**
+ * Test creating Runner object using factory method
+ *
+ * @dataProvider createProvider
+ * @param Route $route
+ * @param string $class Runner class to use
+ * @param boolean $positive
+ */
+ public function testCreate($route, $class, $positive)
+ {
+ if (!$positive) $this->expectException(\RuntimeException::class);
+
+ $runner = Runner::create($route);
+
+ if (!$positive) return;
+
+ $this->assertInstanceOf($class, $runner, "Runner object has invalid class");
+ $this->assertEquals($route, $runner->getRoute(), "Route was not set correctly");
+ }
+
+ /**
+ * Provide data fpr testing 'create' method
+ */
+ public function createProvider()
+ {
+ return [
+ [Route::create(['controller' => 'TestController', 'value' => 'test']), Controller::class, true],
+ [Route::create(['fn' => 'testFunction', 'value' => 'test']), Callback::class, true],
+ [Route::create(['file' => 'some_file.php', 'value' => 'test']), PhpScript::class, true],
+ [Route::create(['test' => 'test']), '', false],
+ ];
+ }
+
+ /**
+ * Test runner __invoke method
+ */
+ public function testInvoke()
+ {
+ $runner = $this->getMockBuilder('Jasny\Router\Runner')->disableOriginalConstructor()->getMockForAbstractClass();
+ $queries = [
+ 'request' => $this->createMock(ServerRequestInterface::class),
+ 'response' => $this->createMock(ResponseInterface::class)
+ ];
+
+ #Test that 'run' receives correct arguments inside '__invoke'
+ $runner->method('run')->will($this->returnCallback(function($arg1, $arg2) {
+ return ['request' => $arg1, 'response' => $arg2];
+ }));
+
+ $result = $runner($queries['request'], $queries['response']);
+ $this->assertEquals($result['request'], $queries['request'], "Request was not returned correctly from 'run'");
+ $this->assertEquals($result['response'], $queries['response'], "Response was not returned correctly from 'run'");
+
+ #The same test with calling 'next' callback
+ $result = $runner($queries['request'], $queries['response'], function($request, $prevResponse) use ($queries) {
+ $this->assertEquals($request, $queries['request'], "Request is not correct in 'next'");
+ $this->assertEquals($prevResponse['request'], $queries['request'], "Prev response was not passed correctly to 'next'");
+ $this->assertEquals($prevResponse['response'], $queries['response'], "Prev response was not passed correctly to 'next'");
+
+ return $queries + ['next_called' => true];
+ });
+
+ $this->assertTrue($result['next_called'], "'Next' callback was not called");
+ $this->assertEquals($result['request'], $queries['request'], "Request was not returned correctly from 'run' with 'next'");
+ $this->assertEquals($result['response'], $queries['response'], "Request was not returned correctly from 'run' with 'next'");
+ }
+}
diff --git a/tests/RouterTest.php b/tests/RouterTest.php
new file mode 100644
index 0000000..9b63360
--- /dev/null
+++ b/tests/RouterTest.php
@@ -0,0 +1,227 @@
+<?php
+
+use Jasny\Router;
+use Jasny\Router\Route;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\StreamInterface;
+
+class RouterTest extends PHPUnit_Framework_TestCase
+{
+ /**
+ * Test creating Router
+ */
+ public function testConstruct()
+ {
+ $routes = [
+ '/foo' => ['fn' => 'test_function'],
+ '/foo/bar' => ['controller' => 'TestController']
+ ];
+
+ $router = new Router($routes);
+ $this->assertEquals($routes, $router->getRoutes(), "Routes were not set correctly");
+ }
+
+ /**
+ * Test that on router 'run', method '__invoke' is called
+ */
+ public function testRun()
+ {
+ $router = $this->createMock(Router::class, ['__invoke']);
+ list($request, $response) = $this->getRequests();
+
+ $router->method('__invoke')->will($this->returnCallback(function($arg1, $arg2) {
+ return ['request' => $arg1, 'response' => $arg2];
+ }));
+
+ $result = $router->run($request, $response);
+
+ $this->assertEquals($request, $result['request'], "Request was not processed correctly");
+ $this->assertEquals($response, $result['response'], "Response was not processed correctly");
+ }
+
+ /**
+ * Test '__invoke' method
+ */
+ public function testInvoke()
+ {
+ $routes = [
+ '/foo/bar' => Route::create(['controller' => 'TestController']),
+ '/foo' => Route::create(['fn' => function($arg1, $arg2) {
+ return ['request' => $arg1, 'response' => $arg2];
+ }])
+ ];
+
+ list($request, $response) = $this->getRequests();
+ $router = new Router($routes);
+ $result = $router($request, $response);
+
+ $this->assertEquals($request, $result['request'], "Request was not processed correctly");
+ $this->assertEquals($response, $result['response'], "Response was not processed correctly");
+ }
+
+ /**
+ * Test '__invoke' method with 'next' callback
+ */
+ public function testInvokeNext()
+ {
+ $routes = [
+ '/foo/bar' => Route::create(['controller' => 'TestController']),
+ '/foo' => Route::create(['fn' => function($request, $response) {
+ return $response;
+ }])
+ ];
+
+ list($request, $response) = $this->getRequests();
+ $router = new Router($routes);
+ $result = $router($request, $response, function($arg1, $arg2) {
+ return ['request' => $arg1, 'response' => $arg2];
+ });
+
+ $this->assertEquals($request, $result['request'], "Request was not processed correctly");
+ $this->assertEquals($response, $result['response'], "Response was not processed correctly");
+ }
+
+ /**
+ * Test case when route is not found
+ */
+ public function testNotFound()
+ {
+ $routes = [
+ '/foo/bar' => Route::create(['controller' => 'TestController'])
+ ];
+
+ list($request, $response) = $this->getRequests();
+ $this->expectNotFound($response);
+
+ $router = new Router($routes);
+ $result = $router($request, $response);
+
+ $this->assertEquals(get_class($response), get_class($result), "Returned result is not an instance of 'ServerRequestInterface'");
+ }
+
+ /**
+ * Test adding middleware action
+ *
+ * @dataProvider addProvider
+ * @param mixed $middleware1
+ * @param callable $middleware2
+ * @param boolean $positive
+ */
+ public function testAdd($middleware1, $middleware2, $positive)
+ {
+ $router = new Router([]);
+ $this->assertEquals(0, count($router->getMiddlewares()), "Middlewares array should be empty");
+
+ if (!$positive) $this->expectException(\InvalidArgumentException::class);
+
+ $result = $router->add($middleware1);
+ $this->assertEquals(1, count($router->getMiddlewares()), "There should be only one item in middlewares array");
+ $this->assertEquals($middleware1, reset($router->getMiddlewares()), "Wrong item in middlewares array");
+ $this->assertEquals($router, $result, "'Add' should return '\$this'");
+
+ if (!$middleware2) return;
+
+ $router->add($middleware2);
+ $this->assertEquals(2, count($router->getMiddlewares()), "There should be two items in middlewares array");
+ foreach ($router->getMiddlewares() as $action) {
+ $this->assertTrue($action == $middleware1 || $action == $middleware2, "Wrong item in middlewares array");
+ }
+ }
+
+ /**
+ * Provide data for testing 'add' method
+ */
+ public function addProvider()
+ {
+ return [
+ ['wrong_callback', null, false],
+ [[$this, 'getMiddlewareCalledFirst'], null, true],
+ [[$this, 'getMiddlewareCalledFirst'], [$this, 'getMiddlewareCalledLast'], true]
+ ];
+ }
+
+ /**
+ * Test executing router with middlewares chain (test only execution order)
+ */
+ public function testRunMiddlewares()
+ {
+ $routes = [
+ '/foo' => Route::create(['fn' => function($request, $response) {
+ $response->testMiddlewareCalls[] = 'handle';
+ return $response;
+ }])
+ ];
+
+ list($request, $response) = $this->getRequests();
+ $router = new Router($routes);
+ $router->add([$this, 'getMiddlewareCalledLast'])->add([$this, 'getMiddlewareCalledFirst']);
+
+ $result = $router($request, $response, function($request, $response) {
+ $response->testMiddlewareCalls[] = 'outer';
+ return $response;
+ });
+
+ $this->assertEquals(['first','last','handle','outer'], $response->testMiddlewareCalls, "Actions were executed in wrong order");
+ }
+
+ /**
+ * Get requests for testing
+ *
+ * @return array
+ */
+ public function getRequests()
+ {
+ $request = $this->createMock(ServerRequestInterface::class);
+ $response = $this->createMock(ResponseInterface::class);
+
+ $request->method('getUri')->will($this->returnValue('/foo'));
+ $request->method('getMethod')->will($this->returnValue('GET'));
+
+ return [$request, $response];
+ }
+
+ /**
+ * Get middleware action, that should ba called first in middleware chain
+ *
+ * @param ServerRequestInterface $request
+ * @param ResponseInterface $response
+ * @param callback $next
+ * @return ResponseInterface
+ */
+ public function getMiddlewareCalledFirst(ServerRequestInterface $request, ResponseInterface $response, $next)
+ {
+ $response->testMiddlewareCalls[] = 'first';
+ return $next($request, $response);
+ }
+
+ /**
+ * Get middleware action, that should be called last in middleware chain
+ *
+ * @param ServerRequestInterface $request
+ * @param ResponseInterface $response
+ * @param callback $next
+ * @return ResponseInterface
+ */
+ public function getMiddlewareCalledLast(ServerRequestInterface $request, ResponseInterface $response, $next)
+ {
+ $response->testMiddlewareCalls[] = 'last';
+ return $next($request, $response);
+ }
+
+ /**
+ * Expect 'not found' response
+ *
+ * @param ResponseInterface
+ */
+ public function expectNotFound(ResponseInterface $response)
+ {
+ $stream = $this->createMock(StreamInterface::class);
+ $stream->expects($this->once())->method('rewind');
+ $stream->expects($this->once())->method('write')->with($this->equalTo('Not Found'));
+
+ $response->method('getBody')->will($this->returnValue($stream));
+ $response->expects($this->once())->method('withBody')->with($this->equalTo($stream))->will($this->returnSelf());
+ $response->expects($this->once())->method('withStatus')->with($this->equalTo(404), $this->equalTo('Not Found'))->will($this->returnSelf());
+ }
+}