diff --git a/CHANGELOG.md b/CHANGELOG.md index 75cde3a..1f9777d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,24 +4,43 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [6.0.0] 2024-11 + +> Note: While this is a major release, there are no breaking changes to the public API. The major version bump is due to the removal of support for PHP 8.0 and below. +> +> This being said, there are some internal changes that may affect you if you have extended the library in any way. Please test thoroughly before upgrading. + +### Added +- Added full support for PHP 8.1 to 8.4. +- Ability to use a PSR-15 middleware as a controller. +- Ability to pass an array of HTTP methods to `Router::map` to create a route that matches multiple methods. + - This method still accepts a string so is not a breaking change. +- Ability to add a custom key to a caching router. + +### Changed +- Fixes and improvements throughout for PHP 8.1 to 8.4. + +### Removed +- Removed support for PHP < 8.1. + ## [5.1.0] 2021-07 -## Added +### Added - Support for named routes within groups (@Fredrik82) ## [5.0.1] 2021-03 -## Added +### Added - Support for `psr/container:2.0` ## [5.0.0] 2021-01 -## Added +### Added - A cached router, a way to have a fully built router cached and resolved from cache on subsequent requests. - Response decorators, a way to manipulate a response object returned from a matched route. - Automatic generation of OPTIONS routes if they have not been defined. -## Changed +### Changed - Minimum PHP requirement bumped to 7.2. - `Router` no longer extends FastRoute `RouteCollecter`. - `Router` constructor no longer accepts optional FastRoute `RouteParser` and `DataGenerator`. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dff53aa..9977e3f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,7 +7,7 @@ We accept contributions via Pull Requests on [Github](https://github.com/thephpl ## Pull Requests -- **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](http://pear.php.net/package/PHP_CodeSniffer). +- **[PSR-12 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-12-extended-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](http://pear.php.net/package/PHP_CodeSniffer). - **Add tests!** - Your patch won't be accepted if it doesn't have tests. diff --git a/LICENSE.md b/LICENSE.md index 9bf34af..5ba4c58 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ # The MIT License (MIT) -Copyright (c) 2020 Phil Bennett +Copyright (c) 2024 Phil Bennett > Permission is hereby granted, free of charge, to any person obtaining a copy > of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 0cb3ff8..b910bc0 100644 --- a/README.md +++ b/README.md @@ -8,13 +8,14 @@ [![Quality Score](https://img.shields.io/scrutinizer/g/thephpleague/route.svg?style=flat-square)](https://scrutinizer-ci.com/g/thephpleague/route) [![Total Downloads](https://img.shields.io/packagist/dt/league/route.svg?style=flat-square)](https://packagist.org/packages/league/route) -This package is compliant with [PSR-1], [PSR-2], [PSR-4], [PSR-7], [PSR-11] and [PSR-15]. If you notice compliance oversights, please send a patch via pull request. +This package is compliant with [PSR-1], [PSR-2], [PSR-4], [PSR-7], [PSR-11], [PSR-12] and [PSR-15]. If you notice compliance oversights, please send a patch via pull request. [PSR-1]: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-1-basic-coding-standard.md [PSR-2]: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md [PSR-4]: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-4-autoloader.md [PSR-7]: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-7-http-message.md [PSR-11]: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-11-container.md +[PSR-12]: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-12-extended-coding-style-guide.md [PSR-15]: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-15-request-handlers.md ## Install diff --git a/docs/6.x/controllers.md b/docs/6.x/controllers.md index 0ca17fd..b0d9461 100644 --- a/docs/6.x/controllers.md +++ b/docs/6.x/controllers.md @@ -264,6 +264,44 @@ $router = new League\Route\Router; $router->map('GET', '/', 'Acme\controller'); ~~~ +### PSR-15 Middleware +~~~php +map('GET', '/', new Acme\SomeController); + +// or you can use lazy loading and the container will resolve the controller, +// any resolved controller that implements RequestHandlerInterface will be treated as a PSR-15 middleware +// and the handle method will be invoked +$router->map('GET', '/', Acme\SomeController::class); +~~~ + ## Dependency Injection Where Route is instantiating the objects for your defined controller, a dependency injection container can be used to resolve those objects. Read more on dependency injection [here](/5.x/dependency-injection/). diff --git a/docs/_data/releases.yml b/docs/_data/releases.yml index a13dfb7..bc1b358 100644 --- a/docs/_data/releases.yml +++ b/docs/_data/releases.yml @@ -2,7 +2,7 @@ version: unstable name: League\Route (dev-5.x) type: Unstable - requires: PHP >= 7.2.0 + requires: PHP >= 8.1.0 release: None support: Own Risk url: /unstable/ @@ -17,12 +17,30 @@ Cached Router (BETA): '/unstable/cached-router/' Dependency Injection: '/unstable/dependency-injection/' - - default: true + default: true + version: 6.x + name: League\Route 6.x + type: Current + requires: PHP >= 8.1.0 + release: 6.0.0 - 2024-11 + support: Ongoing + url: /6.x/ + menu: + Getting Started: '/6.x/' + Basic Usage: '/6.x/usage/' + HTTP: '/6.x/http/' + Routes: '/6.x/routes/' + Controllers: '/6.x/controllers/' + Strategies: '/6.x/strategies/' + Middleware: '/6.x/middleware/' + Cached Router (BETA): '/6.x/cached-router/' + Dependency Injection: '/6.x/dependency-injection/' +- version: 5.x name: League\Route 5.x type: Current requires: PHP >= 7.2.0 - release: 5.1.0 - 2021-07 + release: 5.1.2 - 2021-07 support: Ongoing url: /5.x/ menu: @@ -52,16 +70,3 @@ Strategies: '/4.x/strategies/' Middleware: '/4.x/middleware/' Dependency Injection: '/4.x/dependency-injection/' -- - version: 3.x - name: League\Route 3.x - type: Legacy - requires: PHP >= 5.4.0 - release: 3.1.0 - 2018-07 - support: 2019-02 - url: /3.x/ - menu: - Getting Started: '/3.x/' - Basic Usage: '/3.x/usage/' - Concepts: '/3.x/concepts/' - Strategies: '/3.x/strategies/' diff --git a/docs/unstable/cached-router.md b/docs/unstable/cached-router.md index 1cf9566..ad636cb 100644 --- a/docs/unstable/cached-router.md +++ b/docs/unstable/cached-router.md @@ -55,7 +55,7 @@ $cachedRouter = new League\Route\Cache\Router(function (League\Route\Router $rou }); return $router; -}, $cacheStore); +}, $cacheStore, cacheEnabled: true, cacheKey: 'my-router'); $request = Laminas\Diactoros\ServerRequestFactory::fromGlobals( $_SERVER, $_GET, $_POST, $_COOKIE, $_FILES diff --git a/docs/unstable/controllers.md b/docs/unstable/controllers.md index a1b901e..b0d9461 100644 --- a/docs/unstable/controllers.md +++ b/docs/unstable/controllers.md @@ -9,9 +9,9 @@ sections: --- ## Introduction -Every defined route requires a `callable` to invoke when dispatched, something that could be described as a controller in MVC. By default, Route only imposes that the callable is defined with a specific signature, it is given a request object as the first argument, an associative array of wildcard route arguments as the second argument, and expects a response object to be returned. Read more about this in [HTTP](/4.x/http). +Every defined route requires a `callable` to invoke when dispatched, something that could be described as a controller in MVC. By default, Route only imposes that the callable is defined with a specific signature, it is given a request object as the first argument, an associative array of wildcard route arguments as the second argument, and expects a response object to be returned. Read more about this in [HTTP](/5.x/http). -This behaviour can be changed by creating/using a different strategy, read more about strategies [here](/4.x/strategies). +This behaviour can be changed by creating/using a different strategy, read more about strategies [here](/5.x/strategies). ## Defining Controllers @@ -264,6 +264,44 @@ $router = new League\Route\Router; $router->map('GET', '/', 'Acme\controller'); ~~~ +### PSR-15 Middleware +~~~php +map('GET', '/', new Acme\SomeController); + +// or you can use lazy loading and the container will resolve the controller, +// any resolved controller that implements RequestHandlerInterface will be treated as a PSR-15 middleware +// and the handle method will be invoked +$router->map('GET', '/', Acme\SomeController::class); +~~~ + ## Dependency Injection -Where Route is instantiating the objects for your defined controller, a dependency injection container can be used to resolve those objects. Read more on dependency injection [here](/4.x/dependency-injection/). +Where Route is instantiating the objects for your defined controller, a dependency injection container can be used to resolve those objects. Read more on dependency injection [here](/5.x/dependency-injection/). diff --git a/docs/unstable/dependency-injection.md b/docs/unstable/dependency-injection.md index e5448b1..9aa7d0a 100644 --- a/docs/unstable/dependency-injection.md +++ b/docs/unstable/dependency-injection.md @@ -27,7 +27,7 @@ namespace Acme; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; -use Zend\Diactoros\Response; +use Laminas\Diactoros\Response; class SomeController { diff --git a/docs/unstable/http.md b/docs/unstable/http.md index 76650f0..6cff1fc 100644 --- a/docs/unstable/http.md +++ b/docs/unstable/http.md @@ -12,7 +12,7 @@ HTTP messages form the core of any modern web application. Route is built with t We also make use of [PSR-15](https://www.php-fig.org/psr/psr-15/) request handlers and middleware. -Throughout this documentation, we will be using [zend-diactoros](https://zendframework.github.io/zend-diactoros/) to provide our HTTP messages but any implementation is supported. +Throughout this documentation, we will be using [laminas-diactoros](https://docs.laminas.dev/laminas-diactoros/) to provide our HTTP messages but any implementation is supported. ## The Request @@ -47,7 +47,7 @@ class SomeMiddleware implements MiddlewareInterface } ~~~ -Read more about middleware [here](/4.x/middleware). +Read more about middleware [here](/5.x/middleware). ### Controller Signature @@ -63,7 +63,7 @@ function controller(ServerRequestInterface $request) { } ~~~ -See more about controllers [here](/4.x/controllers). +See more about controllers [here](/5.x/controllers). ### Request Input @@ -73,7 +73,7 @@ Route does not provide any functionality for dealing with globals such as `$_GET Because Route is built around PSR-15, this means that middleware and controllers are handles in a [single pass](https://www.php-fig.org/psr/psr-15/meta/#52-single-pass-lambda) approach. What this means in practice is that all middleware is passed a request object but is expected to build and return its own response or pass off to the next middleware in the stack for that to create one. Any controller that is dispatched via Route is wrapped in a middleware that adheres to this. -Once wrapped, your controller ultimately becomes the last middleware in the stack (this does not mean that it has to be invoked last, see [middleware](/4.x/middleware) for more on this), it just means that it will only be concerned with creating and returning a response object. +Once wrapped, your controller ultimately becomes the last middleware in the stack (this does not mean that it has to be invoked last, see [middleware](/5.x/middleware) for more on this), it just means that it will only be concerned with creating and returning a response object. An example of a controller building a response might look like this. @@ -82,7 +82,7 @@ An example of a controller building a response might look like this. use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; -use Zend\Diactoros\Response; +use Laminas\Diactoros\Response; function controller(ServerRequestInterface $request): ResponseInterface { $response = new Response; diff --git a/docs/unstable/middleware.md b/docs/unstable/middleware.md index 5522c5a..799ed09 100644 --- a/docs/unstable/middleware.md +++ b/docs/unstable/middleware.md @@ -32,7 +32,7 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; -use Zend\Diactoros\Response\RedirectResponse; +use Laminas\Diactoros\Response\RedirectResponse; class AuthMiddleware implements MiddlewareInterface { diff --git a/docs/unstable/routes.md b/docs/unstable/routes.md index 6b7e31c..b17c664 100644 --- a/docs/unstable/routes.md +++ b/docs/unstable/routes.md @@ -111,6 +111,25 @@ GET /admin/acme/route2 GET /admin/acme/route3 ~~~ +### Named Routes + +Named routes helps when you want to retrieve a Route by a human friendly label. + +~~~php +group('/admin', function (\League\Route\RouteGroup $route) { + $route->map('GET', '/acme/route1', 'AcmeController::actionOne')->setName('actionOne'); + $route->map('GET', '/acme/route2', 'AcmeController::actionTwo')->setName('actionTwo'); +}); + +$route = $router->getNamedRoute('actionOne'); +$route->getPath(); // "/admin/acme/route1" +~~~ + ### Conditions As mentioned above, route conditions can be applied to a group and will be matched across all routes contained in that group, specific routes within the group can override this functionality as displayed below. diff --git a/docs/unstable/strategies.md b/docs/unstable/strategies.md index 381a629..853e86a 100644 --- a/docs/unstable/strategies.md +++ b/docs/unstable/strategies.md @@ -93,7 +93,7 @@ $router use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; -use Zend\Diactoros\Response; +use Laminas\Diactoros\Response; function controller(ServerRequestInterface $request, array $args): ResponseInterface { // ... @@ -111,7 +111,7 @@ The application strategy simply allows any `Throwable` to bubble out, you can ca `League\Route\Strategy\JsonStrategy` aims to make building JSON APIs a little easier. It provides a PSR-7 `Psr\Http\Message\ServerRequestInterface` implementation and any route arguments to the controller as with the application strategy, the difference being that you can either build and return a response yourself or return an array or object, and a JSON response will be built for you. -To make use of the JSON strategy, you will need to provide it with a [PSR-17](https://www.php-fig.org/psr/psr-17/) response factory implementation. Some examples of HTTP Factory packages can be found [here](https://github.com/http-interop?utf8=%E2%9C%93&q=http-factory&type=&language=). We will use the `zend-diactoros` factory as an example. +To make use of the JSON strategy, you will need to provide it with a [PSR-17](https://www.php-fig.org/psr/psr-17/) response factory implementation. Some examples of HTTP Factory packages can be found [here](https://github.com/http-interop?utf8=%E2%9C%93&q=http-factory&type=&language=). We will use the `laminas-diactoros` factory as an example. ~~~php setStrategy($strategy); use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; -use Zend\Diactoros\Response; +use Laminas\Diactoros\Response; function responseController(ServerRequestInterface $request, array $args): ResponseInterface { // ... diff --git a/docs/unstable/usage.md b/docs/unstable/usage.md index e0de91e..433bbb0 100644 --- a/docs/unstable/usage.md +++ b/docs/unstable/usage.md @@ -29,7 +29,7 @@ If you use [Laminas Diactoros project][diactoros] you will also need composer require laminas/laminas-httphandlerrunner ~~~ -Optionally, you could also install a PSR-11 dependency injection container, see [Dependency Injection](/4.x/dependency-injection) for more information. +Optionally, you could also install a PSR-11 dependency injection container, see [Dependency Injection](/5.x/dependency-injection) for more information. ~~~ composer require league/container @@ -37,7 +37,7 @@ composer require league/container ## Hello, World! -Now that we have all the packages we need, we can a simple Hello, World! application in one file. +Now that we have all the packages we need, we can make a simple Hello, World! application in one file. ~~~php setStrategy($strategy); @@ -103,7 +103,7 @@ $response = $router->dispatch($request); (new Laminas\HttpHandlerRunner\Emitter\SapiEmitter)->emit($response); ~~~ -The code above will convert your returned array in to a JSON response. +The code above will convert your returned array into a JSON response. ~~~json { diff --git a/phpunit.xml b/phpunit.xml index 629fab6..839fb6c 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -7,15 +7,7 @@ stopOnFailure="false" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd" cacheDirectory=".phpunit.cache" - backupStaticProperties="false" - testdox="false" - displayDetailsOnIncompleteTests="true" - displayDetailsOnSkippedTests="true" - displayDetailsOnTestsThatTriggerDeprecations="true" - displayDetailsOnTestsThatTriggerErrors="true" - displayDetailsOnTestsThatTriggerNotices="true" - displayDetailsOnTestsThatTriggerWarnings="true" -> + backupStaticProperties="false"> diff --git a/src/Route.php b/src/Route.php index b8f4e1d..82ba8b2 100644 --- a/src/Route.php +++ b/src/Route.php @@ -28,11 +28,11 @@ class Route implements public function __construct( protected array|string $method, protected string $path, - callable|string $handler, + callable|string|RequestHandlerInterface $handler, protected ?RouteGroup $group = null, protected array $vars = [] ) { - $this->handler = $handler; + $this->handler = ($handler instanceof RequestHandlerInterface) ? [$handler, 'handle'] : $handler; } public function getCallable(?ContainerInterface $container = null): callable @@ -55,6 +55,10 @@ public function getCallable(?ContainerInterface $container = null): callable $callable = $this->resolve($callable, $container); } + if ($callable instanceof RequestHandlerInterface) { + $callable = [$callable, 'handle']; + } + if (!is_callable($callable)) { throw new RuntimeException('Could not resolve a callable for this route'); } diff --git a/src/RouteCollectionInterface.php b/src/RouteCollectionInterface.php index e41e1d1..1ab7c17 100644 --- a/src/RouteCollectionInterface.php +++ b/src/RouteCollectionInterface.php @@ -4,12 +4,14 @@ namespace League\Route; +use Psr\Http\Server\RequestHandlerInterface; + interface RouteCollectionInterface { public function delete(string $path, string|callable $handler): Route; public function get(string $path, string|callable $handler): Route; public function head(string $path, string|callable $handler): Route; - public function map(string|array $method, string $path, string|callable $handler): Route; + public function map(string|array $method, string $path, string|callable|RequestHandlerInterface $handler): Route; public function options(string $path, string|callable $handler): Route; public function patch(string $path, string|callable $handler): Route; public function post(string $path, string|callable $handler): Route; diff --git a/src/RouteCollectionTrait.php b/src/RouteCollectionTrait.php index 150b6da..9db7a1f 100644 --- a/src/RouteCollectionTrait.php +++ b/src/RouteCollectionTrait.php @@ -5,10 +5,15 @@ namespace League\Route; use League\Route\Http\Request; +use Psr\Http\Server\RequestHandlerInterface; trait RouteCollectionTrait { - abstract public function map(string|array $method, string $path, string|callable $handler): Route; + abstract public function map( + string|array $method, + string $path, + string|callable|RequestHandlerInterface $handler + ): Route; public function delete(string $path, string|callable $handler): Route { diff --git a/src/RouteGroup.php b/src/RouteGroup.php index 266a4cc..12822ed 100644 --- a/src/RouteGroup.php +++ b/src/RouteGroup.php @@ -5,6 +5,7 @@ namespace League\Route; use League\Route\Middleware\{MiddlewareAwareInterface, MiddlewareAwareTrait}; +use Psr\Http\Server\RequestHandlerInterface; use League\Route\Strategy\{StrategyAwareInterface, StrategyAwareTrait}; class RouteGroup implements @@ -42,7 +43,7 @@ public function getPrefix(): string return $this->prefix; } - public function map(string|array $method, string $path, string|callable $handler): Route + public function map(string|array $method, string $path, string|callable|RequestHandlerInterface $handler): Route { $path = ($path === '/') ? $this->prefix : $this->prefix . sprintf('/%s', ltrim($path, '/')); $route = $this->collection->map($method, $path, $handler); diff --git a/src/Router.php b/src/Router.php index 20afb34..cd6458f 100644 --- a/src/Router.php +++ b/src/Router.php @@ -117,8 +117,11 @@ public function handle(ServerRequestInterface $request): ResponseInterface return $this->dispatch($request); } - public function map(string|array $method, string $path, string|callable $handler): Route - { + public function map( + string|array $method, + string $path, + string|callable|RequestHandlerInterface $handler + ): Route { $path = sprintf('/%s', ltrim($path, '/')); $route = new Route($method, $path, $handler); diff --git a/tests/Fixture/MiddlewareController.php b/tests/Fixture/MiddlewareController.php new file mode 100644 index 0000000..6cc27ce --- /dev/null +++ b/tests/Fixture/MiddlewareController.php @@ -0,0 +1,17 @@ +assertEquals('action', $newCallable[1]); } + public function testRouteSetsAndResolvesRequestHandlerCallableAsStringViaContainer(): void + { + $container = $this->createMock(ContainerInterface::class); + + $container + ->expects($this->once()) + ->method('has') + ->with($this->equalTo(MiddlewareController::class)) + ->willReturn(true) + ; + + $container + ->expects($this->once()) + ->method('get') + ->with($this->equalTo(MiddlewareController::class)) + ->willReturn(new MiddlewareController()) + ; + + $callable = 'League\Route\Fixture\MiddlewareController'; + $route = new Route('GET', '/', $callable); + + $newCallable = $route->getCallable($container); + $this->assertIsArray($newCallable); + $this->assertInstanceOf(MiddlewareController::class, $newCallable[0]); + $this->assertEquals('handle', $newCallable[1]); + } + public function testRouteCanSetAndGetAllProperties(): void { $route = new Route('GET', '/something', static function () {