diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/Router.php | 71 | ||||
-rw-r--r-- | src/Router/Middleware/BasePath.php | 24 | ||||
-rw-r--r-- | src/Router/Middleware/ErrorHandler.php | 54 | ||||
-rw-r--r-- | src/Router/Middleware/NotFound.php | 49 | ||||
-rw-r--r-- | src/Router/Route.php | 26 | ||||
-rw-r--r-- | src/Router/Routes/Glob.php | 217 | ||||
-rw-r--r-- | src/Router/Routes/RouteBinding.php | 314 | ||||
-rw-r--r-- | src/Router/Runner.php | 14 | ||||
-rw-r--r-- | src/Router/Runner/Callback.php | 2 | ||||
-rw-r--r-- | src/Router/Runner/Controller.php | 39 | ||||
-rw-r--r-- | src/Router/Runner/PhpScript.php | 27 | ||||
-rw-r--r-- | src/Router/RunnerFactory.php (renamed from src/Router/Runner/RunnerFactory.php) | 10 | ||||
-rw-r--r-- | src/Router/UrlParsing.php | 4 |
13 files changed, 478 insertions, 373 deletions
diff --git a/src/Router.php b/src/Router.php index 6d38678..5cc2108 100644 --- a/src/Router.php +++ b/src/Router.php @@ -2,8 +2,8 @@ namespace Jasny; -use Jasny\Router\Runner\RunnerFactory; -use Jasny\Router\Routes\Glob; +use Jasny\Router\Routes; +use Jasny\Router\RunnerFactory; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ResponseInterface; @@ -14,9 +14,9 @@ class Router { /** * Specific routes - * @var array + * @var Routes */ - protected $routes = []; + protected $routes; /** * Middlewares actions @@ -28,22 +28,23 @@ class Router * Factory of Runner objects * @var RunnerFactory **/ - protected $factory = null; + protected $factory; + /** * Class constructor * - * @param array $routes + * @param Routes $routes */ - public function __construct(array $routes) + public function __construct(Routes $routes) { $this->routes = $routes; } /** - * Get a list of all routes + * Get a all routes * - * @return object + * @return Routes */ public function getRoutes() { @@ -51,7 +52,7 @@ class Router } /** - * Get middlewares + * Get all middlewares * * @return array */ @@ -60,6 +61,7 @@ class Router return $this->middlewares; } + /** * Get factory of Runner objects * @@ -100,7 +102,7 @@ class Router public function add($middleware) { if (!is_callable($middleware)) { - throw new \InvalidArgumentException("Middleware should be a callable"); + throw new \InvalidArgumentException("Middleware should be callable"); } $this->middlewares[] = $middleware; @@ -108,6 +110,7 @@ class Router return $this; } + /** * Run the action for the request * @@ -130,17 +133,16 @@ class Router */ public function __invoke(ServerRequestInterface $request, ResponseInterface $response, $next = null) { - $run = [$this, 'run']; - - #Call to $this->run will be executed last in the chain of middlewares - $next = function(ServerRequestInterface $request, ResponseInterface $response) use ($next, $run) { - return call_user_func($run, $request, $response, $next); - }; + if (empty($this->middlewares)) { + return $this->run($request, $response, $next); + } + + $stack = array_merge([[$this, 'run']], $this->middlewares); - #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); + // Turn the stack into a call chain + foreach ($stack as $handle) { + $next = function(ServerRequestInterface $request, ResponseInterface $response) use ($handle, $next) { + return $handle($request, $response, $next); }; } @@ -157,33 +159,32 @@ class Router */ public function run(ServerRequestInterface $request, ResponseInterface $response, $next = null) { - $glob = new Glob($this->routes); - $route = $glob->getRoute($request); + $route = $this->routes->getRoute($request); + + if (!$route) { + return $this->notFound($request, $response); + } + + $requestWithRoute = $request->withAttribute('route', $route); - if (!$route) return $this->notFound($response); - - $request->withAttribute('route', $route); $factory = $this->getFactory(); $runner = $factory($route); - return $runner($request, $response, $next); + return $runner($requestWithRoute, $response, $next); } /** * Return 'Not Found' response * + * @param ServerRequestInterface $request * @param ResponseInterface $response * @return ResponseInterface */ - protected function notFound(ResponseInterface $response) + protected function notFound(ServerRequestInterface $request, ResponseInterface $response) { - $message = 'Not Found'; - - $body = $response->getBody(); - $body->rewind(); - $body->write($message); + $notFound = $response->withStatus(404); + $notFound->getBody()->write('Not Found'); - return $response->withStatus(404, $message)->withBody($body); + return $notFound; } } - diff --git a/src/Router/Middleware/BasePath.php b/src/Router/Middleware/BasePath.php index a80d029..f356aed 100644 --- a/src/Router/Middleware/BasePath.php +++ b/src/Router/Middleware/BasePath.php @@ -57,13 +57,15 @@ class BasePath $uri = $request->getUri(); $path = $this->normalizePath($uri->getPath()); - if (!$this->hasBasePath($path)) return $this->setError($response); + if (!$this->hasBasePath($path)) { + return $this->notFound($request, $response); + } $noBase = $this->getBaselessPath($path); $noBaseUri = $uri->withPath($noBase); - $request = $request->withUri($noBaseUri)->withAttribute('original_uri', $uri); + $rewrittenRequest = $request->withUri($noBaseUri)->withAttribute('original_uri', $uri); - return call_user_func($next, $request, $response); + return $next($rewrittenRequest, $response); } /** @@ -100,19 +102,17 @@ class BasePath } /** - * Set error response + * Respond with 404 Not Found * - * @param ResponseInterface $response + * @param ServerRequestInterface $request + * @param ResponseInterface $response * @return ResponseInterface */ - protected function setError($response) + protected function notFound(ServerRequestInterface $request, ResponseInterface $response) { - $message = 'Not Found'; - - $body = $response->getBody(); - $body->rewind(); - $body->write($message); + $notFound = $response->withStatus(404); + $notFound->getBody()->write('Not Found'); - return $response->withStatus(404, $message)->withBody($body); + return $notFound; } } diff --git a/src/Router/Middleware/ErrorHandler.php b/src/Router/Middleware/ErrorHandler.php deleted file mode 100644 index 789c455..0000000 --- a/src/Router/Middleware/ErrorHandler.php +++ /dev/null @@ -1,54 +0,0 @@ -<?php - -namespace Jasny\Router\Middleware; - -use Psr\Http\Message\ServerRequestInterface; -use Psr\Http\Message\ResponseInterface; - -/** - * Handle error in following middlewares/app actions - */ -class ErrorHandler -{ - /** - * Run middleware action - * - * @param ServerRequestInterface $request - * @param ResponseInterface $response - * @param callback $next - * @return ResponseInterface - */ - public function __invoke(ServerRequestInterface $request, ResponseInterface $response, $next = null) - { - if ($next && !is_callable($next)) { - throw new \InvalidArgumentException("'next' should be a callback"); - } - - $error = false; - - try { - $response = $next ? call_user_func($next, $request, $response) : $response; - } catch(\Throwable $e) { - $error = true; - } catch(\Exception $e) { #This block can be removed when migrating to PHP7, because Throwable represents both Exception and Error - $error = true; - } - - return $error ? $this->handleError($response) : $response; - } - - /** - * Handle caught error - * - * @param ResponseInterface $response - * @return ResponseInterface - */ - protected function handleError($response) - { - $body = $response->getBody(); - $body->rewind(); - $body->write('Unexpected error'); - - return $response->withStatus(500, 'Internal Server Error')->withBody($body); - } -} diff --git a/src/Router/Middleware/NotFound.php b/src/Router/Middleware/NotFound.php index 97b4a51..2484a43 100644 --- a/src/Router/Middleware/NotFound.php +++ b/src/Router/Middleware/NotFound.php @@ -29,6 +29,7 @@ class NotFound **/ protected $methodNotAllowed = null; + /** * Class constructor * @@ -38,12 +39,22 @@ class NotFound */ public function __construct(Routes $routes, $notFound = 404, $methodNotAllowed = null) { - if (!(is_numeric($notFound) && $notFound >= 100 && $notFound <= 599) && !is_callable($notFound)) { - throw new \InvalidArgumentException("'Not found' parameter should be a code in range 100-599 or a callback"); + if (is_string($notFound) && ctype_digit($notFound)) { + $notFound = (int)$notFound; + } + if (!(is_int($notFound) && $notFound >= 100 && $notFound <= 999) && !is_callable($notFound)) { + throw new \InvalidArgumentException("'notFound' should be valid HTTP status code or a callback"); } - if ($methodNotAllowed && !(is_numeric($methodNotAllowed) && $methodNotAllowed >= 100 && $methodNotAllowed <= 599) && !is_callable($methodNotAllowed)) { - throw new \InvalidArgumentException("'Method not allowed' parameter should be a code in range 100-599 or a callback"); + if (is_string($methodNotAllowed) && ctype_digit($methodNotAllowed)) { + $methodNotAllowed = (int)$methodNotAllowed; + } + if ( + isset($methodNotAllowed) && + !(is_int($methodNotAllowed) && $methodNotAllowed >= 100 && $methodNotAllowed <= 999) && + !is_callable($methodNotAllowed) + ) { + throw new \InvalidArgumentException("'methodNotAllowed' should be valid HTTP status code or a callback"); } $this->routes = $routes; @@ -60,6 +71,7 @@ class NotFound { return $this->routes; } + /** * Run middleware action @@ -69,37 +81,34 @@ class NotFound * @param callback $next * @return ResponseInterface */ - public function __invoke(ServerRequestInterface $request, ResponseInterface $response, $next = null) + public function __invoke(ServerRequestInterface $request, ResponseInterface $response, $next) { - if ($next && !is_callable($next)) { + if (!is_callable($next)) { throw new \InvalidArgumentException("'next' should be a callback"); } - if ($this->getRoutes()->hasRoute($request)) { - return $next ? $next($request, $response) : $response; - } - - $status = $this->methodNotAllowed && $this->getRoutes()->hasRoute($request, false) ? - $this->methodNotAllowed : $this->notFound; + if (!$this->getRoutes()->hasRoute($request)) { + $status = $this->methodNotAllowed && $this->getRoutes()->hasRoute($request, false) ? + $this->methodNotAllowed : $this->notFound; - return is_numeric($status) ? $this->simpleResponse($response, $status) : call_user_func($status, $request, $response); + return is_numeric($status) ? $this->simpleResponse($response, $status) : $status($request, $response); + } + + return $next($request, $response); } /** * Simple response * * @param ResponseInterface $response - * @param int $code + * @param int $code * @return ResponseInterface */ protected function simpleResponse(ResponseInterface $response, $code) { - $message = 'Not Found'; - - $body = $response->getBody(); - $body->rewind(); - $body->write($message); + $notFound = $response->withStatus($code); + $notFound->getBody()->write('Not found'); - return $response->withStatus($code, $message)->withBody($body); + return $notFound; } } diff --git a/src/Router/Route.php b/src/Router/Route.php index be5a052..d2158ea 100644 --- a/src/Router/Route.php +++ b/src/Router/Route.php @@ -10,27 +10,21 @@ class Route extends \stdClass /** * Class constructor * - * @param array $values + * @param array|stdClass $values */ - public function __construct(array $values) - { - foreach ($values as $key => $value) { - $this->$key = $value; - } - } - - /** - * Factory method - * - * @param array|\stdClass $values - * @return Route - */ - public static function create($values) + public function __construct($values) { if ($values instanceof \stdClass) { $values = get_object_vars($values); } - return new static($values); + if (!is_array($values)) { + $type = (is_object($values) ? get_class($values) . ' ' : '') . gettype($values); + throw new \InvalidArgumentException("Route values should be an array, not a $type"); + } + + foreach ($values as $key => $value) { + $this->$key = $value; + } } } diff --git a/src/Router/Routes/Glob.php b/src/Router/Routes/Glob.php index fcece76..a4495ee 100644 --- a/src/Router/Routes/Glob.php +++ b/src/Router/Routes/Glob.php @@ -6,6 +6,7 @@ use ArrayObject; use Jasny\Router\UrlParsing; use Jasny\Router\Routes; use Jasny\Router\Route; +use Jasny\Router\Routes\RouteBinding; use Psr\Http\Message\ServerRequestInterface; /** @@ -14,6 +15,19 @@ use Psr\Http\Message\ServerRequestInterface; class Glob extends ArrayObject implements Routes { use UrlParsing; + use RouteBinding; + + /** + * Class constructor + * + * @param Routes[]|array|\Traversable $input + * @param int $flags + */ + public function __construct($input = [], $flags = 0) + { + $routes = $this->createRoutes($input); + parent::__construct($routes, $flags); + } /** * Create a route from an assisiative array or stdClass object @@ -35,14 +49,7 @@ class Glob extends ArrayObject implements Routes throw new \InvalidArgumentException("Unable to create a Route from value " . var_export($value, true)); } - $route = Route::create($value); - - if (!isset($route)) { - throw new \InvalidArgumentException("Unable to create a Route from " . var_export($value, true) . ": " - . "neither 'controller', 'fn' or 'file' key is defined"); - } - - return $route; + return new Route($value); } /** @@ -157,200 +164,6 @@ class Glob extends ArrayObject implements Routes /** - * Fill out the routes variables based on the url parts. - * - * @param array|\stdClass $vars Route variables - * @param ServerRequestInterface $request - * @param array $parts URL parts - * @return array - */ - protected function bind($vars, ServerRequestInterface $request, array $parts) - { - $values = []; - $type = is_array($vars) && array_keys($vars) === array_keys(array_keys($vars)) ? 'numeric' : 'assoc'; - - foreach ($vars as $key => $var) { - if (!isset($var)) continue; - - if (is_object($var) && !$var instanceof \stdClass) { - $part = array($var); - } elseif (!is_scalar($var)) { - $part = array($this->bind($var, $request, $parts)); - } elseif ($var[0] === '$') { - $options = array_map('trim', explode('|', $var)); - $part = $this->bindVar($type, $request, $parts, $options); - } elseif ($var[0] === '~' && substr($var, -1) === '~') { - $pieces = array_map('trim', explode('~', substr($var, 1, -1))); - $bound = array_filter($this->bind($pieces, $request, $parts)); - $part = array(join('', $bound)); - } else { - $part = array($var); - } - - if ($type === 'assoc') { - $values[$key] = $part[0]; - } else { - $values = array_merge($values, $part); - } - } - - if ($vars instanceof Route) { - $values = Route::create($values); - } elseif (is_object($vars) && $type === 'assoc') { - $values = (object)$values; - } - - return $values; - } - - /** - * Bind variable - * - * @param string $type 'assoc' or 'numeric' - * @param ServerRequestInterface $request - * @param array $parts - * @param array $options - * @return array - */ - protected function bindVar($type, ServerRequestInterface $request, array $parts, array $options) - { - foreach ($options as $option) { - $value = null; - - $bound = - $this->bindVarString($option, $value) || - $this->bindVarSuperGlobal($option, $request, $value) || - $this->bindVarRequestHeader($option, $request, $value) || - $this->bindVarMultipleUrlParts($option, $type, $parts, $value) || - $this->bindVarSingleUrlPart($option, $parts, $value); - - if ($bound && isset($value)) { - return $value; - } - } - - return [null]; - } - - /** - * Bind variable when option is a normal string - * - * @param string $option - * @param mixed $value OUTPUT - * @return boolean - */ - protected function bindVarString($option, &$value) - { - if ($option[0] !== '$') { - $value = [$option]; - return true; - } - - return false; - } - - /** - * Bind variable when option is a super global - * - * @param string $option - * @param mixed $value OUTPUT - * @return boolean - */ - protected function bindVarSuperGlobal($option, ServerRequestInterface $request, &$value) - { - if (preg_match('/^\$_(GET|POST|COOKIE)\[([^\[]*)\]$/i', $option, $matches)) { - list(, $var, $key) = $matches; - - $var = strtolower($var); - $data = null; - - if ($var === 'get') { - $data = $request->getQueryParams(); - } elseif ($var === 'post') { - $data = $request->getParsedBody(); - } elseif ($var === 'cookie') { - $data = $request->getCookieParams(); - } - - $value = isset($data[$key]) ? [$data[$key]] : null; - return true; - } - - return false; - } - - /** - * Bind variable when option is a request header - * - * @param string $option - * @param ServerRequestInterface $request - * @param mixed $value OUTPUT - * @return boolean - */ - protected function bindVarRequestHeader($option, ServerRequestInterface $request, &$value) - { - if (preg_match('/^\$(?:HTTP_)?([A-Z_]+)$/', $option, $matches)) { - $sentence = preg_replace('/[\W_]+/', ' ', $matches[1]); - $name = str_replace(' ', '-', ucwords($sentence)); - - $value = [$request->getHeaderLine($name)]; - return true; - } - - return false; - } - - /** - * Bind variable when option contains multiple URL parts - * - * @param string $option - * @param string $type 'assoc' or 'numeric' - * @param array $parts Url parts - * @param mixed $value OUTPUT - * @return boolean - */ - protected function bindVarMultipleUrlParts($option, $type, array $parts, &$value) - { - if (substr($option, -3) === '...' && ctype_digit(substr($option, 1, -3))) { - $i = (int)substr($option, 1, -3); - - if ($type === 'assoc') { - throw new \InvalidArgumentException("Binding multiple parts using '$option' is only allowed in numeric arrays"); - } else { - $value = array_slice($parts, $i - 1); - } - - return true; - } - - return false; - } - - /** - * Bind variable when option contains a single URL part - * - * @param string $option - * @param array $parts Url parts - * @param mixed $value OUTPUT - * @return boolean - */ - protected function bindVarSingleUrlPart($option, array $parts, &$value) - { - if (ctype_digit(substr($option, 1))) { - $i = (int)substr($option, 1); - $part = array_slice($parts, $i - 1, 1); - - if (!empty($part)) { - $value = $part; - return true; - } - } - - return false; - } - - - /** * Check if a route for the URL exists * * @param ServerRequestInterface $request diff --git a/src/Router/Routes/RouteBinding.php b/src/Router/Routes/RouteBinding.php new file mode 100644 index 0000000..91563c3 --- /dev/null +++ b/src/Router/Routes/RouteBinding.php @@ -0,0 +1,314 @@ +<?php + +namespace Jasny\Router\Routes; + +use Jasny\Router\Route; +use Psr\Http\Message\ServerRequestInterface; + +/** + * Functionality to set route properties based on url parameters + */ +trait RouteBinding +{ + /** + * Fill out the routes variables based on the url parts. + * + * @param array|\stdClass $vars Route variables + * @param ServerRequestInterface $request + * @param array $parts URL parts + * @return array + */ + protected function bind($vars, ServerRequestInterface $request, array $parts) + { + $type = is_array($vars) && array_keys($vars) === array_keys(array_keys($vars)) ? 'numeric' : 'assoc'; + + $values = $this->bindParts($vars, $type, $request, $parts); + + if ($vars instanceof Route) { + $class = get_class($vars); + $values = new $class($values); + } elseif (is_object($vars) && $type === 'assoc') { + $values = (object)$values; + } + + return $values; + } + + + /** + * Fill out the values based on the url parts. + * + * @param array|\stdClass $vars Route variables + * @param string $type + * @param ServerRequestInterface $request + * @param array $parts URL parts + * @return array + */ + protected function bindParts($vars, $type, ServerRequestInterface $request, array $parts) + { + $values = []; + + foreach ($vars as $key => $var) { + $part = null; + + $bound = + $this->bindPartObject($var, $part) || + $this->bindPartArray($var, $request, $parts, $part) || + $this->bindPartVar($var, $type, $request, $parts, $part) || + $this->bindPartConcat($var, $request, $parts, $part) || + $this->bindPartValue($var, $part); + + if (!$bound) continue; + + if ($type === 'assoc') { + $values[$key] = $part[0]; + } else { + $values = array_merge($values, $part); + } + } + + return $values; + } + + /** + * Bind part if it's an object + * + * @param mixed $var + * @param array $part OUTPUT + * @return boolean + */ + protected function bindPartObject($var, &$part) + { + if (!is_object($var) || $var instanceof \stdClass) { + return false; + } + + $part = [$var]; + return true; + } + + /** + * Bind part if it's an array + * + * @param mixed $var + * @param ServerRequestInterface $request + * @param array $parts + * @param array $part OUTPUT + * @return boolean + */ + protected function bindPartArray($var, ServerRequestInterface $request, array $parts, &$part) + { + if (!is_array($var) && !$var instanceof \stdClass) { + return false; + } + + $part = [$this->bind($var, $request, $parts)]; + return true; + } + + /** + * Bind part if it's an variable + * + * @param mixed $var + * @param string $type + * @param ServerRequestInterface $request + * @param array $parts + * @param array $part OUTPUT + * @return boolean + */ + protected function bindPartVar($var, $type, ServerRequestInterface $request, array $parts, &$part) + { + if (!is_string($var) || $var[0] !== '$') { + return false; + } + + $options = array_map('trim', explode('|', $var)); + $part = $this->bindVar($type, $request, $parts, $options); + return true; + } + + /** + * Bind part if it's an concatenation + * + * @param mixed $var + * @param ServerRequestInterface $request + * @param array $parts + * @param array $part OUTPUT + * @return boolean + */ + protected function bindPartConcat($var, ServerRequestInterface $request, array $parts, &$part) + { + if (!is_string($var) || $var[0] !== '~' || substr($var, -1) !== '~') { + return false; + } + + $pieces = array_map('trim', explode('~', substr($var, 1, -1))); + $bound = array_filter($this->bind($pieces, $request, $parts)); + $part = [join('', $bound)]; + + return true; + } + + /** + * Bind part if it's a normal value + * + * @param mixed $var + * @param array $part OUTPUT + * @return boolean + */ + protected function bindPartValue($var, &$part) + { + if (!isset($var)) { + return false; + } + + $part = [$var]; + return true; + } + + /** + * Bind variable + * + * @param string $type 'assoc' or 'numeric' + * @param ServerRequestInterface $request + * @param array $parts + * @param array $options + * @return array + */ + protected function bindVar($type, ServerRequestInterface $request, array $parts, array $options) + { + foreach ($options as $option) { + $value = null; + + $bound = + $this->bindVarString($option, $value) || + $this->bindVarSuperGlobal($option, $request, $value) || + $this->bindVarRequestHeader($option, $request, $value) || + $this->bindVarMultipleUrlParts($option, $type, $parts, $value) || + $this->bindVarSingleUrlPart($option, $parts, $value); + + if ($bound && isset($value)) { + return $value; + } + } + + return [null]; + } + + /** + * Bind variable when option is a normal string + * + * @param string $option + * @param mixed $value OUTPUT + * @return boolean + */ + protected function bindVarString($option, &$value) + { + if ($option[0] !== '$') { + $value = [$option]; + return true; + } + + return false; + } + + /** + * Bind variable when option is a super global + * + * @param string $option + * @param mixed $value OUTPUT + * @return boolean + */ + protected function bindVarSuperGlobal($option, ServerRequestInterface $request, &$value) + { + if (preg_match('/^\$_(GET|POST|COOKIE)\[([^\[]*)\]$/i', $option, $matches)) { + list(, $var, $key) = $matches; + + $var = strtolower($var); + $data = null; + + if ($var === 'get') { + $data = $request->getQueryParams(); + } elseif ($var === 'post') { + $data = $request->getParsedBody(); + } elseif ($var === 'cookie') { + $data = $request->getCookieParams(); + } + + $value = isset($data[$key]) ? [$data[$key]] : null; + return true; + } + + return false; + } + + /** + * Bind variable when option is a request header + * + * @param string $option + * @param ServerRequestInterface $request + * @param mixed $value OUTPUT + * @return boolean + */ + protected function bindVarRequestHeader($option, ServerRequestInterface $request, &$value) + { + if (preg_match('/^\$(?:HTTP_)?([A-Z_]+)$/', $option, $matches)) { + $sentence = preg_replace('/[\W_]+/', ' ', $matches[1]); + $name = str_replace(' ', '-', ucwords($sentence)); + + $value = [$request->getHeaderLine($name)]; + return true; + } + + return false; + } + + /** + * Bind variable when option contains multiple URL parts + * + * @param string $option + * @param string $type 'assoc' or 'numeric' + * @param array $parts Url parts + * @param mixed $value OUTPUT + * @return boolean + */ + protected function bindVarMultipleUrlParts($option, $type, array $parts, &$value) + { + if (substr($option, -3) === '...' && ctype_digit(substr($option, 1, -3))) { + $i = (int)substr($option, 1, -3); + + if ($type === 'assoc') { + throw new \InvalidArgumentException("Binding multiple parts using '$option' is only allowed in numeric arrays"); + } else { + $value = array_slice($parts, $i - 1); + } + + return true; + } + + return false; + } + + /** + * Bind variable when option contains a single URL part + * + * @param string $option + * @param array $parts Url parts + * @param mixed $value OUTPUT + * @return boolean + */ + protected function bindVarSingleUrlPart($option, array $parts, &$value) + { + if (ctype_digit(substr($option, 1))) { + $i = (int)substr($option, 1); + $part = array_slice($parts, $i - 1, 1); + + if (!empty($part)) { + $value = $part; + return true; + } + } + + return false; + } +} diff --git a/src/Router/Runner.php b/src/Router/Runner.php index 87f082e..c671fa9 100644 --- a/src/Router/Runner.php +++ b/src/Router/Runner.php @@ -4,7 +4,6 @@ namespace Jasny\Router; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ResponseInterface; -use Jasny\Router\Route; /** * A runner can be invoked in order to run the action specified in a route @@ -23,20 +22,19 @@ abstract class Runner /** * 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 + * @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); + $newResponse = $this->run($request, $response); if (isset($next)) { - $response = call_user_func($next, $request, $response); + $newResponse = call_user_func($next, $request, $newResponse); } - return $response; + return $newResponse; } } - diff --git a/src/Router/Runner/Callback.php b/src/Router/Runner/Callback.php index 7f4c457..8fa8c0e 100644 --- a/src/Router/Runner/Callback.php +++ b/src/Router/Runner/Callback.php @@ -29,6 +29,6 @@ class Callback extends Runner throw new \RuntimeException("'fn' property of route shoud be a callable"); } - return call_user_func($callback, $request, $response); + return $callback($request, $response); } } diff --git a/src/Router/Runner/Controller.php b/src/Router/Runner/Controller.php index 56cf2b5..caca48f 100644 --- a/src/Router/Runner/Controller.php +++ b/src/Router/Runner/Controller.php @@ -7,13 +7,34 @@ use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ResponseInterface; /** - * Description of Controller - * - * @author arnold + * Run a route using a controller */ class Controller extends Runner { /** + * Get class name from controller name + * + * @param string $name + * @return string + */ + protected function getClass($name) + { + return strstr($name, '-') ? \Jasny\studlycase($name) : $name; + } + + /** + * Instantiate a controller object + * @codeCoverageIgnore + * + * @param string $class + * @return callable|object + */ + protected function instantiate($class) + { + return new $class(); + } + + /** * Route to a controller * * @param ServerRequestInterface $request @@ -23,18 +44,20 @@ class Controller extends Runner public function run(ServerRequestInterface $request, ResponseInterface $response) { $route = $request->getAttribute('route'); - $class = !empty($route->controller) ? $route->controller : null; + $name = !empty($route->controller) ? $route->controller : null; + $class = $this->getClass($name); + 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($route); - + + $controller = $this->instantiate($class); + return $controller($request, $response); } } diff --git a/src/Router/Runner/PhpScript.php b/src/Router/Runner/PhpScript.php index d223bd7..51480c5 100644 --- a/src/Router/Runner/PhpScript.php +++ b/src/Router/Runner/PhpScript.php @@ -10,7 +10,18 @@ use Psr\Http\Message\ResponseInterface; * Route to a PHP script */ class PhpScript extends Runner -{ +{ + /** + * Include a file + * @param type $file + * @param ServerRequestInterface $request + * @param ResponseInterface $response + */ + protected function includeScript($file, ServerRequestInterface $request, ResponseInterface $response) + { + return include $file; + } + /** * Route to a file * @@ -23,16 +34,16 @@ class PhpScript extends Runner $route = $request->getAttribute('route'); $file = !empty($route->file) ? ltrim($route->file, '/') : ''; - if (!file_exists($file)) { - throw new \RuntimeException("Failed to route using '$file': File '$file' doesn't exist."); - } - if ($file[0] === '~' || strpos($file, '..') !== false) { - throw new \RuntimeException("Won't route using '$file': '~', '..' are not allowed in filename."); + throw new \RuntimeException("Won't route to '$file': '~', '..' are not allowed in filename"); } - $result = include $file; + if (!file_exists($file)) { + throw new \RuntimeException("Failed to route using '$file': File doesn't exist"); + } + + $result = $this->includeScript($file, $request, $response); - return $result === true ? $response : $result; + return $result === true || $result === 1 ? $response : $result; } } diff --git a/src/Router/Runner/RunnerFactory.php b/src/Router/RunnerFactory.php index e32d2cc..08ed7b0 100644 --- a/src/Router/Runner/RunnerFactory.php +++ b/src/Router/RunnerFactory.php @@ -1,8 +1,9 @@ <?php -namespace Jasny\Router\Runner; +namespace Jasny\Router; use Jasny\Router\Route; +use Jasny\Router\Runner; /** * Factory of Runner instances @@ -18,11 +19,11 @@ class RunnerFactory public function __invoke(Route $route) { if (isset($route->controller)) { - $class = Controller::class; + $class = Runner\Controller::class; } elseif (isset($route->fn)) { - $class = Callback::class; + $class = Runner\Callback::class; } elseif (isset($route->file)) { - $class = PhpScript::class; + $class = Runner\PhpScript::class; } else { throw new \InvalidArgumentException("Route has neither 'controller', 'fn' or 'file' defined"); } @@ -30,4 +31,3 @@ class RunnerFactory return new $class(); } } - diff --git a/src/Router/UrlParsing.php b/src/Router/UrlParsing.php index 5727085..ee89260 100644 --- a/src/Router/UrlParsing.php +++ b/src/Router/UrlParsing.php @@ -31,10 +31,6 @@ trait UrlParsing $url = rtrim($url, '/'); } - if (substr($url, 0, 2) == '/:') { - $url = substr($url, 2); - } - return $url; } } |