diff --git a/.gitignore b/.gitignore index f5da770..848a58c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ .idea composer.lock -vendor/ -demo-project/vendor \ No newline at end of file +vendor/ \ No newline at end of file diff --git a/README.md b/README.md index ed14bf2..28d5844 100644 --- a/README.md +++ b/README.md @@ -51,8 +51,9 @@ If you want a great new feature or experience any issues what-so-ever, please fe - [Middlewares](#middlewares) - [Example](#example) -- [ExceptionHandler](#exceptionhandler) - - [Example](#example-1) +- [ExceptionHandlers](#exceptionhandlers) + - [Handling 404, 403 and other errors](#handling-404-403-and-other-errors) + - [Using custom exception handlers](#using-custom-exception-handlers) - [Urls](#urls) - [Get by name (single route)](#get-by-name-single-route) @@ -397,7 +398,7 @@ Named routes allow the convenient generation of URLs or redirects for specific r ```php SimpleRouter::get('/user/profile', function () { - // + // Your code here })->name('profile'); ``` @@ -626,13 +627,30 @@ class CustomMiddleware implements Middleware { --- -# ExceptionHandler +# ExceptionHandlers ExceptionHandler are classes that handles all exceptions. ExceptionsHandlers must implement the `IExceptionHandler` interface. -## Example +## Handling 404, 403 and other errors -Resource controllers can implement the `IRestController` interface, but is not required. +If you simply want to catch a 404 (page not found) etc. you can use the `Router::error($callback)` static helper method. + +This will add a callback method which is fired whenever an error occurs on all routes. + +The basic example below simply redirect the page to `/not-found` if an `NotFoundHttpException` (404) occurred. +The code should be placed in the file that contains your routes. + +```php +Router::get('/not-found', 'PageController@notFound'); + +Router::error(function(Request $request, \Exception $exception) { + if($exception instanceof NotFoundHttpException && $exception->getCode == 404) { + response()->redirect('/not-found'); + } +}); +``` + +## Using custom exception handlers This is a basic example of an ExceptionHandler implementation (please see "[Easily overwrite route about to be loaded](#easily-overwrite-route-about-to-be-loaded)" for examples on how to change callback). @@ -1072,7 +1090,7 @@ $route = new RouteUrl('/answer/1', function() { }); -$route->setMiddleware(\Demo\Middlewares\AuthMiddleware::class); +$route->addMiddleware(\Demo\Middlewares\AuthMiddleware::class); $route->setNamespace('\Demo\Controllers'); $route->setPrefix('v1'); diff --git a/src/Pecee/CsrfToken.php b/src/Pecee/CsrfToken.php index 6080827..3549243 100644 --- a/src/Pecee/CsrfToken.php +++ b/src/Pecee/CsrfToken.php @@ -3,7 +3,7 @@ namespace Pecee; class CsrfToken { - const CSRF_KEY = 'XSRF-TOKEN'; + const CSRF_KEY = 'CSRF-TOKEN'; protected $token; @@ -60,7 +60,7 @@ class CsrfToken */ public function getToken() { - if ($this->hasToken()) { + if ($this->hasToken() === true) { return $_COOKIE[static::CSRF_KEY]; } diff --git a/src/Pecee/Handlers/CallbackExceptionHandler.php b/src/Pecee/Handlers/CallbackExceptionHandler.php new file mode 100644 index 0000000..435a752 --- /dev/null +++ b/src/Pecee/Handlers/CallbackExceptionHandler.php @@ -0,0 +1,38 @@ +callback = $callback; + } + + /** + * @param Request $request + * @param \Exception $error + * @return Request|null + */ + public function handleError(Request $request, \Exception $error) + { + /* Fire exceptions */ + return call_user_func($this->callback, + $request, + $error + ); + } +} \ No newline at end of file diff --git a/src/Pecee/Http/Input/Input.php b/src/Pecee/Http/Input/Input.php index c3732dd..0bd5ec1 100644 --- a/src/Pecee/Http/Input/Input.php +++ b/src/Pecee/Http/Input/Input.php @@ -108,11 +108,11 @@ class Input $file = InputFile::createFromArray([ 'index' => $key, + 'filename' => $getItem($key), 'error' => $getItem($key, 'error'), 'tmp_name' => $getItem($key, 'tmp_name'), 'type' => $getItem($key, 'type'), 'size' => $getItem($key, 'size'), - 'filename' => $getItem($key, 'name'), ]); if (isset($output[$key])) { diff --git a/src/Pecee/Http/Request.php b/src/Pecee/Http/Request.php index 371e0af..0d69a71 100644 --- a/src/Pecee/Http/Request.php +++ b/src/Pecee/Http/Request.php @@ -8,7 +8,7 @@ use Pecee\SimpleRouter\SimpleRouter; class Request { - protected $data = []; + private $data = []; protected $headers; protected $host; protected $uri; diff --git a/src/Pecee/SimpleRouter/Route/IGroupRoute.php b/src/Pecee/SimpleRouter/Route/IGroupRoute.php index 6892379..ff3a69d 100644 --- a/src/Pecee/SimpleRouter/Route/IGroupRoute.php +++ b/src/Pecee/SimpleRouter/Route/IGroupRoute.php @@ -1,6 +1,8 @@ getMiddlewares()[$i]; - $middleware = $this->loadClass($middleware); + if (is_object($middleware) === false) { + $middleware = $this->loadClass($middleware); + } if (($middleware instanceof IMiddleware) === false) { - throw new HttpException($middleware . ' must be instance of Middleware'); + throw new HttpException($middleware . ' must be inherit the IMiddleware interface'); } $middleware->handle($request); @@ -98,8 +100,10 @@ abstract class LoadableRoute extends Route implements ILoadableRoute { $url = $this->getUrl(); - if ($this->getGroup() !== null && count($this->getGroup()->getDomains()) > 0) { - $url = '//' . $this->getGroup()->getDomains()[0] . $url; + $group = $this->getGroup(); + + if ($group !== null && count($group->getDomains()) > 0) { + $url = '//' . $group->getDomains()[0] . $url; } /* Contains parameters that aren't recognized and will be appended at the end of the url */ diff --git a/src/Pecee/SimpleRouter/Route/Route.php b/src/Pecee/SimpleRouter/Route/Route.php index e212f55..079a4f9 100644 --- a/src/Pecee/SimpleRouter/Route/Route.php +++ b/src/Pecee/SimpleRouter/Route/Route.php @@ -2,6 +2,7 @@ namespace Pecee\SimpleRouter\Route; +use Pecee\Http\Middleware\IMiddleware; use Pecee\Http\Request; use Pecee\SimpleRouter\Exceptions\NotFoundHttpException; @@ -52,7 +53,7 @@ abstract class Route implements IRoute protected function loadClass($name) { if (class_exists($name) === false) { - throw new NotFoundHttpException(sprintf('Class %s does not exist', $name), 404); + throw new NotFoundHttpException(sprintf('Class "%s" does not exist', $name), 404); } return new $name(); @@ -62,39 +63,45 @@ abstract class Route implements IRoute { $callback = $this->getCallback(); - if ($callback !== null && is_callable($callback)) { + if ($callback === null) { + return; + } + + /* Render callback function */ + if (is_callable($callback) === true) { /* When the callback is a function */ call_user_func_array($callback, $this->getParameters()); - } else { + return; - /* When the callback is a method */ - $controller = explode('@', $callback); - - $namespace = $this->getNamespace(); - - $className = ($namespace !== null && $controller[0][0] !== '\\') ? $namespace . '\\' . $controller[0] : $controller[0]; - - $class = $this->loadClass($className); - $method = $controller[1]; - - if (method_exists($class, $method) === false) { - throw new NotFoundHttpException(sprintf('Method %s does not exist in class %s', $method, $className), 404); - } - - $parameters = $this->getParameters(); - - /* Filter parameters with null-value */ - - if ($this->filterEmptyParams === true) { - $parameters = array_filter($parameters, function ($var) { - return ($var !== null); - }); - } - - call_user_func_array([$class, $method], $parameters); } + + /* When the callback is a class + method */ + $controller = explode('@', $callback); + + $namespace = $this->getNamespace(); + + $className = ($namespace !== null && $controller[0][0] !== '\\') ? $namespace . '\\' . $controller[0] : $controller[0]; + + $class = $this->loadClass($className); + $method = $controller[1]; + + if (method_exists($class, $method) === false) { + throw new NotFoundHttpException(sprintf('Method "%s" does not exist in class "%s"', $method, $className), 404); + } + + $parameters = $this->getParameters(); + + /* Filter parameters with null-value */ + + if ($this->filterEmptyParams === true) { + $parameters = array_filter($parameters, function ($var) { + return ($var !== null); + }); + } + + call_user_func_array([$class, $method], $parameters); } protected function parseParameters($route, $url, $parameterRegex = null) @@ -168,7 +175,7 @@ abstract class Route implements IRoute */ public function getIdentifier() { - if (strpos($this->callback, '@') !== false) { + if (is_string($this->callback) === true && strpos($this->callback, '@') !== false) { return $this->callback; } @@ -265,7 +272,7 @@ abstract class Route implements IRoute public function getMethod() { - if (strpos($this->callback, '@') !== false) { + if (is_string($this->callback) === true && strpos($this->callback, '@') !== false) { $tmp = explode('@', $this->callback); return $tmp[1]; @@ -276,7 +283,7 @@ abstract class Route implements IRoute public function getClass() { - if (strpos($this->callback, '@') !== false) { + if (is_string($this->callback) === true && strpos($this->callback, '@') !== false) { $tmp = explode('@', $this->callback); return $tmp[0]; @@ -478,9 +485,10 @@ abstract class Route implements IRoute } /** - * Set middleware class-name + * Add middleware class-name * - * @param string $middleware + * @deprecated This method is deprecated and will be removed in the near future. + * @param IMiddleware|string $middleware * @return static */ public function setMiddleware($middleware) @@ -490,6 +498,19 @@ abstract class Route implements IRoute return $this; } + /** + * Add middleware class-name + * + * @param IMiddleware|string $middleware + * @return static + */ + public function addMiddleware($middleware) + { + $this->middlewares[] = $middleware; + + return $this; + } + /** * Set middlewares array * diff --git a/src/Pecee/SimpleRouter/Route/RouteController.php b/src/Pecee/SimpleRouter/Route/RouteController.php index 6ef22e8..e01854a 100644 --- a/src/Pecee/SimpleRouter/Route/RouteController.php +++ b/src/Pecee/SimpleRouter/Route/RouteController.php @@ -74,8 +74,10 @@ class RouteController extends LoadableRoute implements IControllerRoute $method .= '/'; } - if ($this->getGroup() !== null && count($this->getGroup()->getDomains()) > 0) { - $url .= '//' . $this->getGroup()->getDomains()[0]; + $group = $this->getGroup(); + + if ($group !== null && count($group->getDomains()) > 0) { + $url .= '//' . $group->getDomains()[0]; } $url .= '/' . trim($this->getUrl(), '/') . '/' . strtolower($method) . join('/', $parameters); diff --git a/src/Pecee/SimpleRouter/Route/RouteGroup.php b/src/Pecee/SimpleRouter/Route/RouteGroup.php index d7a4379..e6397b1 100644 --- a/src/Pecee/SimpleRouter/Route/RouteGroup.php +++ b/src/Pecee/SimpleRouter/Route/RouteGroup.php @@ -1,6 +1,8 @@ domains) === 0) { + if ($this->domains === null || count($this->domains) === 0) { return true; } @@ -54,6 +56,19 @@ class RouteGroup extends Route implements IGroupRoute return $this->matchDomain($request); } + /** + * Add exception handler + * + * @param IExceptionHandler|string $handler + * @return static $this + */ + public function addExceptionHandler($handler) + { + $this->exceptionHandlers[] = $handler; + + return $this; + } + /** * Set exception-handlers for group * diff --git a/src/Pecee/SimpleRouter/Router.php b/src/Pecee/SimpleRouter/Router.php index 808e376..01f898e 100644 --- a/src/Pecee/SimpleRouter/Router.php +++ b/src/Pecee/SimpleRouter/Router.php @@ -1,4 +1,5 @@ getCallback() !== null && is_callable($route->getCallback())) { + $this->processingRoute = true; + $route->renderRoute($this->request); + $this->processingRoute = false; - $this->processingRoute = true; - $route->renderRoute($this->request); - $this->processingRoute = false; - - if ($route->matchRoute($url, $this->request) === true) { - - /* Add exception handlers */ - if (count($route->getExceptionHandlers()) > 0) { - $exceptionHandlers += $route->getExceptionHandlers(); - } + if ($route->matchRoute($url, $this->request) === true) { + /* Add exception handlers */ + if (count($route->getExceptionHandlers()) > 0) { + /** @noinspection AdditionOperationOnArraysInspection */ + $exceptionHandlers += $route->getExceptionHandlers(); } + } } @@ -183,7 +182,7 @@ class Router } } - $this->exceptionHandlers = array_unique(array_merge($exceptionHandlers, $this->exceptionHandlers)); + $this->exceptionHandlers = array_merge($exceptionHandlers, $this->exceptionHandlers); } /** @@ -283,7 +282,16 @@ class Router } if ($this->request->getLoadedRoute() === null) { - $this->handleException(new NotFoundHttpException('Route not found: ' . $this->request->getUri(), 404)); + + $rewriteUrl = $this->request->getRewriteUrl(); + + if ($rewriteUrl !== null) { + $message = sprintf('Route not found: "%s" (rewrite from: "%s")', $rewriteUrl, $this->request->getUri()); + } else { + $message = sprintf('Route not found: "%s"', $this->request->getUri()); + } + + $this->handleException(new NotFoundHttpException($message, 404)); } } @@ -297,7 +305,10 @@ class Router for ($i = 0; $i < $max; $i++) { $handler = $this->exceptionHandlers[$i]; - $handler = new $handler(); + + if (is_object($handler) === false) { + $handler = new $handler(); + } if (($handler instanceof IExceptionHandler) === false) { throw new HttpException('Exception handler must implement the IExceptionHandler interface.', 500); @@ -372,7 +383,7 @@ class Router } /* Using @ is most definitely a controller@method or alias@method */ - if (strpos($name, '@') !== false) { + if (is_string($name) === true && strpos($name, '@') !== false) { list($controller, $method) = array_map('strtolower', explode('@', $name)); if ($controller === strtolower($route->getClass()) && $method === strtolower($route->getMethod())) { @@ -381,7 +392,7 @@ class Router } /* Check if callback matches (if it's not a function) */ - if (strpos($name, '@') !== false && strpos($route->getCallback(), '@') !== false && !is_callable($route->getCallback())) { + if (is_string($name) === true && is_string($route->getCallback()) && strpos($name, '@') !== false && strpos($route->getCallback(), '@') !== false && is_callable($route->getCallback()) === false) { /* Check if the entire callback is matching */ if (strpos($route->getCallback(), $name) === 0 || strtolower($route->getCallback()) === strtolower($name)) { @@ -451,7 +462,7 @@ class Router } /* Using @ is most definitely a controller@method or alias@method */ - if (strpos($name, '@') !== false) { + if (is_string($name) === true && strpos($name, '@') !== false) { list($controller, $method) = explode('@', $name); /* Loop through all the routes to see if we can find a match */ @@ -517,6 +528,19 @@ class Router return $this->routes; } + /** + * Set routes + * + * @param array $routes + * @return static $this + */ + public function setRoutes(array $routes) + { + $this->routes = $routes; + + return $this; + } + /** * Get current request * diff --git a/src/Pecee/SimpleRouter/SimpleRouter.php b/src/Pecee/SimpleRouter/SimpleRouter.php index c085032..dd804e5 100644 --- a/src/Pecee/SimpleRouter/SimpleRouter.php +++ b/src/Pecee/SimpleRouter/SimpleRouter.php @@ -7,8 +7,10 @@ * This class is added so calls can be made statically like Router::get() making the code look pretty. * It also adds some extra functionality like default-namespace. */ + namespace Pecee\SimpleRouter; +use Pecee\Handlers\CallbackExceptionHandler; use Pecee\Http\Middleware\BaseCsrfVerifier; use Pecee\Http\Response; use Pecee\SimpleRouter\Exceptions\HttpException; @@ -301,6 +303,28 @@ class SimpleRouter return $route; } + /** + * Add exception callback handler. + * + * @param \Closure $callback + * @return CallbackExceptionHandler $callbackHandler + */ + public static function error(\Closure $callback) + { + $routes = static::router()->getRoutes(); + + $callbackHandler = new CallbackExceptionHandler($callback); + + $group = new RouteGroup(); + $group->addExceptionHandler($callbackHandler); + + array_unshift($routes, $group); + + static::router()->setRoutes($routes); + + return $callbackHandler; + } + /** * Get url for a route by using either name/alias, class or method name. * @@ -355,7 +379,7 @@ class SimpleRouter */ public static function router() { - if(static::$router === null) { + if (static::$router === null) { static::$router = new Router(); } diff --git a/test/RouterCallbackExceptionHandlerTest.php b/test/RouterCallbackExceptionHandlerTest.php new file mode 100644 index 0000000..de21d2a --- /dev/null +++ b/test/RouterCallbackExceptionHandlerTest.php @@ -0,0 +1,27 @@ +setExpectedException(ExceptionHandlerException::class); + + // Match normal route on alias + TestRouter::get('/my-new-url', 'DummyController@method2'); + TestRouter::get('/my-url', 'DummyController@method1'); + + TestRouter::error(function (\Pecee\Http\Request $request, \Exception $exception) { + throw new ExceptionHandlerException(); + }); + + TestRouter::debugNoReset('/404-url', 'get'); + TestRouter::router()->reset(); + } + +} \ No newline at end of file diff --git a/test/RouterRewriteTest.php b/test/RouterRewriteTest.php index 176ad39..e72bce6 100644 --- a/test/RouterRewriteTest.php +++ b/test/RouterRewriteTest.php @@ -1,4 +1,5 @@ setExpectedException(\Pecee\SimpleRouter\Exceptions\NotFoundHttpException::class); + + TestRouter::error(function (\Pecee\Http\Request $request, \Exception $error) { + + if (strtolower($request->getUri()) == '/my/test') { + $request->setRewriteUrl('/another-non-existing'); + + return $request; + } + + }); + + TestRouter::debug('/my/test', 'get'); + } + } \ No newline at end of file diff --git a/test/RouterUrlTest.php b/test/RouterUrlTest.php index 34816fe..93e6108 100644 --- a/test/RouterUrlTest.php +++ b/test/RouterUrlTest.php @@ -8,7 +8,7 @@ require_once 'Helpers/TestRouter.php'; class RouterUrlTest extends PHPUnit_Framework_TestCase { - public function testSimularUrls() + public function testSimilarUrls() { // Match normal route on alias TestRouter::resource('/url11', 'DummyController@method1');