diff --git a/CHANGELOG.md b/CHANGELOG.md index a4e9b074..f987aadd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,9 +20,32 @@ All notable changes to this project will be documented in this file, in reverse - Nothing. -## 1.0.0 - TBD - -First stable release. +## 1.0.0rc6 - TBD + +Sixth release candidate. + +This release contains backwards compatibility breaks with previous release +candidates. All previous functionality should continue to work, but will +emit `E_USER_DEPRECATED` notices prompting you to update your application. +In particular: + +- The routing middleware has been split into two separate middleware + implementations, one for routing, another for dispatching. This eliminates the + need for the route result observer system, as middleware can now be placed + *between* routing and dispatching — an approach that provides for greater + flexibility with regards to providing route-based functionality. +- As a result of the above, `Zend\Expressive\Application` no longer implements + `Zend\Expressive\Router\RouteResultSubjectInterface`, though it retains the + methods associated (each emits a deprecation notice). +- Configuration for `Zend\Expressive\Container\ApplicationFactory` was modified + to implement the `middleware_pipeline` as a single queue, instead of + segregating it between `pre_routing` and `post_routing`. Each item in the + queue follows the original middleware specification from those keys, with one + addition: a `priority` key can be used to allow you to granularly shape the + execution order of the middleware pipeline. + +A [migration guide](http://zend-expressive.rtfd.org/en/latest/migration/rc-to-v1/) +was written to help developers migrate to RC6 from earlier versions. ### Added @@ -42,14 +65,44 @@ First stable release. a flow/architectural diagram to the "features" chapter. - [#262](https://github.com/zendframework/zend-expressive/pull/262) adds a recipe demonstrating creating classes that can intercept multiple routes. +- [#270](https://github.com/zendframework/zend-expressive/pull/270) adds + new methods to `Zend\Expressive\Application`: + - `dispatchMiddleware()` is new middleware for dispatching the middleware + matched by routing (this functionality was split from `routeMiddleware()`). + - `routeResultObserverMiddleware()` is new middleware for notifying route + result observers, and exists only to aid migration functionality; it is + marked deprecated! + - `pipeDispatchMiddleware()` will pipe the dispatch middleware to the + `Application` instance. + - `pipeRouteResultObserverMiddleware()` will pipe the route result observer + middleware to the `Application` instance; like + `routeResultObserverMiddleware()`, the method only exists for aiding + migration, and is marked deprecated. +- [#270](https://github.com/zendframework/zend-expressive/pull/270) adds + `Zend\Expressive\MarshalMiddlewareTrait`, which is composed by + `Zend\Expressive\Application`; it provides methods for marshaling + middleware based on service names or arrays of services. ### Deprecated -- Nothing. +- [#270](https://github.com/zendframework/zend-expressive/pull/270) deprecates + the following methods in `Zend\Expressive\Application`, all of which will + be removed in version 1.1: + - `attachRouteResultObserver()` + - `detachRouteResultObserver()` + - `notifyRouteResultObservers()` + - `pipeRouteResultObserverMiddleware()` + - `routeResultObserverMiddleware()` ### Removed -- Nothing. +- [#270](https://github.com/zendframework/zend-expressive/pull/270) removes the + `Zend\Expressive\Router\RouteResultSubjectInterface` implementation from + `Zend\Expressive\Application`. +- [#270](https://github.com/zendframework/zend-expressive/pull/270) eliminates + the `pre_routing`/`post_routing` terminology from the `middleware_pipeline`, + in favor of individually specified `priority` values in middleware + specifications. ### Fixed diff --git a/doc/book/application.md b/doc/book/application.md index 0a6b8714..f57e12e3 100644 --- a/doc/book/application.md +++ b/doc/book/application.md @@ -186,21 +186,24 @@ function ($error, ServerRequestInterface $request, ResponseInterface $response, Read the section on [piping vs routing](router/piping.md) for more information. -### Registering routing middleware +### Registering routing and dispatch middleware -Routing is accomplished via dedicated a dedicated middleware method, -`Application::routeMiddleware()`. It is an instance method, and can be -piped/registered with other middleware platforms if desired. +Routing is accomplished via a dedicated middleware method, +`Application::routeMiddleware()`; similarly, dispatching of routed middleware +has a corresponding instance middleware method, `Application::dispatchMiddleware()`. +Each can be piped/registered with other middleware platforms if desired. -Internally, the first time `route()` is called (including via one of the proxy -methods), or, if never called, when `__invoke()` (the exposed application -middleware) is called, the instance will pipe `Application::routeMiddleware` to -the middleware pipeline. You can also pipe it manually if desired: +These methods **MUST** be piped to the application so that the application will +route and dispatch routed middleware. This is done using the following methods: ```php $app->pipeRoutingMiddleware(); +$app->pipeDispatchMiddleware(); ``` +See the section on [piping](router/piping.md) to see how you can register +non-routed middleware and create layered middleware applications. + ## Retrieving dependencies As noted in the intro, the `Application` class has several dependencies. Some of diff --git a/doc/book/container/factories.md b/doc/book/container/factories.md index e2f28e82..dee32539 100644 --- a/doc/book/container/factories.md +++ b/doc/book/container/factories.md @@ -35,22 +35,20 @@ instance. When the `config` service is present, the factory can utilize several keys in order to seed the `Application` instance: -- `middleware_pipeline` can be used to seed pre- and/or post-routing middleware: +- `middleware_pipeline` can be used to seed the middleware pipeline: ```php 'middleware_pipeline' => [ - // An array of middleware to register prior to registration of the - // routing middleware: - 'pre_routing' => [ - ], - // An array of middleware to register after registration of the - // routing middleware: - 'post_routing' => [ - ], + // An array of middleware to register. + [ /* ... */ ], + Zend\Expressive\Container\ApplicationFactory::ROUTING_MIDDLEWARE, + Zend\Expressive\Container\ApplicationFactory::DISPATCH_MIDDLEWARE, + [ /* ... */ ], ], ``` - Each item of each array must be an array itself, with the following structure: + Each item of the array, other than the entries for routing and dispatch + middleware, must be an array itself, with the following structure: ```php [ @@ -59,6 +57,7 @@ order to seed the `Application` instance: // optional: 'path' => '/path/to/match', 'error' => true, + 'priority' => 1, // Integer ], ``` @@ -69,7 +68,16 @@ order to seed the `Application` instance: `error` key is present and boolean `true`, then the middleware will be registered as error middleware. (This is necessary due to the fact that the factory defines a callable wrapper around middleware to enable lazy-loading of - middleware.) + middleware.) The `priority` defaults to 1, and follows the semantics of + [SplPriorityQueue](http://php.net/SplPriorityQueue): higher integer values + indicate higher priority (will execute earlier), while lower/negative integer + values indicate lower priority (will execute last). Default priority is 1; use + granular priority values to specify the order in which middleware should be + piped to the application. + + You *can* specify keys for each middleware specification. These will be + ignored by the factory, but can be useful when merging several configurations + into one for the application. - `routes` is used to define routed middleware. The value must be an array, consisting of arrays defining each middleware: diff --git a/doc/book/cookbook/custom-404-page-handling.md b/doc/book/cookbook/custom-404-page-handling.md index 701a838c..04ea4ade 100644 --- a/doc/book/cookbook/custom-404-page-handling.md +++ b/doc/book/cookbook/custom-404-page-handling.md @@ -86,22 +86,25 @@ From there, you still need to register the middleware. This middleware is not routed, and thus needs to be piped to the application instance. You can do this via either configuration, or manually. -To do this via configuration, add an entry under the `post_routing` key of the -`middleware_pipeline` configuration: +To do this via configuration, add an entry under the `middleware_pipeline` +configuration, after the dispatch middleware: ```php 'middleware_pipeline' => [ - 'pre_routing' => [ - [ - //... + /* ... */ + 'routing' => [ + 'middleware' => [ + Zend\Expressive\Container\ApplicationFactory::ROUTING_MIDDLEWARE, + Zend\Expressive\Helper\UrlHelperMiddleware::class, + Zend\Expressive\Container\ApplicationFactory::DISPATCH_MIDDLEWARE, ], - + 'priority' => 1, ], - 'post_routing' => [ - [ - 'middleware' => 'Application\NotFound', - ], + [ + 'middleware' => 'Application\NotFound', + 'priority' => -1, ], + /* ... */ ], ``` @@ -117,9 +120,7 @@ $app->pipe($container->get('Application\NotFound')); This must be done *after*: -- calling `$app->pipeRoutingMiddleware()`, **OR** -- calling any method that injects routed middleware (`get()`, `post()`, - `route()`, etc.), **OR** +- calling `$app->pipeDispatchMiddleware()`, **OR** - pulling the `Application` instance from the service container (assuming you used the `ApplicationFactory`). diff --git a/doc/book/cookbook/debug-toolbars.md b/doc/book/cookbook/debug-toolbars.md index becc080a..31f08739 100644 --- a/doc/book/cookbook/debug-toolbars.md +++ b/doc/book/cookbook/debug-toolbars.md @@ -74,12 +74,11 @@ return [ ], ], 'middleware_pipeline' => [ - 'pre_routing' => [ - [ - 'middleware' => [ - PhpDebugBarMiddleware::class, - ], + [ + 'middleware' => [ + PhpDebugBarMiddleware::class, ], + 'priority' => 1000, ], ], ]; diff --git a/doc/book/cookbook/route-specific-pipeline.md b/doc/book/cookbook/route-specific-pipeline.md index 739f1519..09cb551c 100644 --- a/doc/book/cookbook/route-specific-pipeline.md +++ b/doc/book/cookbook/route-specific-pipeline.md @@ -160,24 +160,23 @@ When either of these approaches are used, the individual middleware listed This approach is essentially equivalent to creating a factory that returns a middleware pipeline. -## What about pre/post pipeline middleware? +## What about pipeline middleware configuration? -What if you want to do this with pre- or post-pipeline middleware? The answer is -that the syntax is exactly the same! +What if you want to do this with your pipeline middleware configuration? The +answer is that the syntax is exactly the same! ```php return [ 'middleware_pipeline' => [ - 'pre_routing' => [ - [ - 'path' => '/api', - 'middleware' => [ - 'AuthenticationMiddleware', - 'AuthorizationMiddleware', - 'BodyParsingMiddleware', - 'ValidationMiddleware', - ], + 'api' => [ + 'path' => '/api', + 'middleware' => [ + 'AuthenticationMiddleware', + 'AuthorizationMiddleware', + 'BodyParsingMiddleware', + 'ValidationMiddleware', ], + 'priority' => 100, ], ], ]; diff --git a/doc/book/cookbook/setting-locale-depending-routing-parameter.md b/doc/book/cookbook/setting-locale-depending-routing-parameter.md index c1f0ba33..974c78e3 100644 --- a/doc/book/cookbook/setting-locale-depending-routing-parameter.md +++ b/doc/book/cookbook/setting-locale-depending-routing-parameter.md @@ -6,7 +6,7 @@ In this recipe we will concentrate on using a routing parameter. > ## Routing parameters > -> Using the approach in this chapter requires that you add a `/:lang` (or +> Using the approach in this chapter requires that you add a `/:locale` (or > similar) segment to each and every route that can be localized, and, depending > on the router used, may also require additional options for specifying > constraints. If the majority of your routes are localized, this will become @@ -16,9 +16,9 @@ In this recipe we will concentrate on using a routing parameter. ## Setting up the route If you want to set the locale depending on an routing parameter, you first have -to add a language parameter to each route that requires localization. +to add a locale parameter to each route that requires localization. -In this example we use the `lang` parameter, which should consist of two +In this example we use the `locale` parameter, which should consist of two lowercase alphabetical characters: ```php @@ -38,23 +38,23 @@ return [ 'routes' => [ [ 'name' => 'home', - 'path' => '/:lang', + 'path' => '/:locale', 'middleware' => Application\Action\HomePageAction::class, 'allowed_methods' => ['GET'], 'options' => [ 'constraints' => [ - 'lang' => '[a-z]{2}', + 'locale' => '[a-z]{2}', ], ], ], [ 'name' => 'contact', - 'path' => '/:lang/contact', + 'path' => '/:locale/contact', 'middleware' => Application\Action\ContactPageAction::class, 'allowed_methods' => ['GET'], 'options' => [ 'constraints' => [ - 'lang' => '[a-z]{2}', + 'locale' => '[a-z]{2}', ], ], ], @@ -71,13 +71,13 @@ return [ > ```php > [ > 'name' => 'home', -> 'path' => '/{lang}', +> 'path' => '/{locale}', > 'middleware' => Application\Action\HomePageAction::class, > 'allowed_methods' => ['GET'], > 'options' => [ > 'constraints' => [ > 'tokens' => [ -> 'lang' => '[a-z]{2}', +> 'locale' => '[a-z]{2}', > ], > ], > ], @@ -89,7 +89,7 @@ return [ > ```php > [ > 'name' => 'home', -> 'path' => '/{lang:[a-z]{2}}', +> 'path' => '/{locale[a-z]{2}}', > 'middleware' => Application\Action\HomePageAction::class, > 'allowed_methods' => ['GET'], > ] @@ -99,247 +99,56 @@ return [ > simply cut-and-paste them without modification. -## Create a route result observer class for localization +## Create a route result middleware class for localization To make sure that you can setup the locale after the routing has been processed, -you need to implement a localization observer which implements the -`RouteResultObserverInterface`. All classes that implement this interface and -that are attached to the `Zend\Expressive\Application` instance get called -whenever the `RouteResult` has changed. +you need to implement localization middleware that acts on the route result, and +registered in the pipeline immediately following the routing middleware. -Such a `LocalizationObserver` class could look similar to this: +Such a `LocalizationMiddleware` class could look similar to this: ```php namespace Application\I18n; use Locale; use Zend\Expressive\Router\RouteResult; -use Zend\Expressive\Router\RouteResultObserverInterface; -class LocalizationObserver implements RouteResultObserverInterface +class LocalizationMiddleware { - public function update(RouteResult $result) + public function __invoke($request, $response, $next) { - if ($result->isFailure()) { - return; - } - - $matchedParams = $result->getMatchedParams(); - - $lang = isset($matchedParams['lang']) ? $matchedParams['lang'] : 'de_DE'; - Locale::setDefault($matchedParams['lang']); - } -} -``` - -Afterwards you need to configure the `LocalizationObserver` in your -`/config/autoload/dependencies.global.php` file: - -```php -return [ - 'dependencies' => [ - 'invokables' => [ - /* ... */ - - Application\I18n\LocalizationObserver::class => - Application\I18n\LocalizationObserver::class, - ], - - /* ... */ - ] -]; -``` - -## Attach the localization observer to the application - -There are five approaches you can take to attach the `LocalizationObserver` to -the application instance, each with pros and cons: - -### Bootstrap script - -Modify the bootstrap script `/public/index.php` to attach the observer: - -```php -use Application\I18n\LocalizationObserver; - -/* ... */ - -$app = $container->get('Zend\Expressive\Application'); -$app->attachRouteResultObserver( - $container->get(LocalizationObserver::class) -); -$app->run(); -``` - -This is likely the simplest way, but means that there may be a growing -amount of code in that file. - -### Observer factory - -Alternately, in the factory for your observer, have it self-attach to the -application instance: - -```php -// get instance of observer... - -// and now check for the Application: -if ($container->has(Application::class)) { - $container->get(Application::class)->attachRouteResultObserver($observer); -} - -return $observer; -``` - -There are two things to be careful of with this approach: - -- Circular dependencies. If a a dependency of the Application is dependent on - your observer, you'll run into this. -- Late registration. If this is injected as a dependency for another class after - routing has happened, then your observer will never be triggered. - -If you can prevent circular dependencies, and ensure that the factory is invoked -early enough, then this is a great, portable way to accomplish it. - -### Delegator factory - -If you're using zend-servicemanager, you can use a delegator factory on the -Application service to pull and register the observer: - -```php -use Zend\Expressive\Application; -use Zend\ServiceManager\DelegatorFactoryInterface; -use Zend\ServiceManager\ServiceLocatorInterface; - -class ApplicationObserverDelegatorFactory implements DelegatorFactoryInterface -{ - public function createDelegatorForName( - ServiceLocatorInterface $container, - $name, - $requestedName, - $callback - ) { - $application = $callback(); - - if (! $container->has(LocalizationObserver::class)) { - return $application; - } - - $application->attachRouteResultObserver( - $container->get(LocalizationObserver::class) - ); - return $application; - } -} -``` - -Then register it as a delegator factory in `config/autoload/dependencies.global.php`: - -```php -return [ - 'dependencies' => [ - 'delegator_factories' => [ - Zend\Expressive\Application::class => [ - ApplicationObserverDelegatorFactory::class, - ], - ], - /* ... */ - ], -]; -``` - -This approach removes the probability of a circular dependency, and ensures -that the observer is attached as early as possible. - -The problem with this approach, though, is portability. You can do something -similar to this with Pimple: - -```php -$pimple->extend(Application::class, function ($app, $container) { - $app->attachRouteResultObserver($container->get(LocalizationObserver::class)); - return $app; -}); -``` - -and there are ways to accomplish it in Aura.Di as well — but they're all -different, making the approach non-portable. - -### Extend the Application factory - -Alternately, extend the Application factory: - -```php -class MyApplicationFactory extends ApplicationFactory -{ - public function __invoke($container) - { - $app = parent::__invoke($container); - $app->attachRouteResultObserver($container->get(LocalizationObserver::class)); - return $app; - } -} -``` - -Then alter the line in `config/autoload/dependencies.global.php` that registers -the `Application` factory to point at your own factory. - -This approach will work across all container types, and is essentially a -portable way of doing delegator factories. - -### Use middleware - -Alternately, use `pre_routing` middleware to accomplish the task; the middleware -will get both the observer and application as dependencies, and simply register -the observer with the application: - -```php -use Zend\Expressive\Router\RouteResultSubjectInterface; - -class LocalizationObserverMiddleware -{ - private $application; - private $observer; - - public function __construct(LocalizationObserver $observer, RouteResultSubjectInterface $application) - { - $this->observer = $observer; - $this->application = $application; - } - - public function __invoke($request, $response, callable $next) - { - $this->application->attachRouteResultObserver($this->observer); + $locale = $request->getAttribute('locale', 'de_DE'); + Locale::setDefault($locale); return $next($request, $response); } } ``` -The factory would inject the observer and application instances; we leave this -as an exercise to the reader. - -In your `config/autoload/middleware-pipeline.global.php`, you'd do the following: +In your `config/autoload/middleware-pipeline.global.php`, you'd register the +dependency, and inject the middleware into the pipeline following the routing +middleware: ```php return [ 'dependencies' => [ - 'factories' => [ - LocalizationObserverMiddleware::class => LocalizationObserverMiddlewareFactory::class, + 'invokables' => [ + LocalizationMiddleware::class => LocalizationMiddleware::class, /* ... */ ], /* ... */ ], 'middleware_pipeline' => [ - 'pre_routing' => [ - [ 'middleware' => LocalizationObserverMiddleware::class ], - /* ... */ - ], - 'post_routing' => [ - /* ... */ + /* ... */ + [ + 'middleware' => [ + Zend\Expressive\Container\ApplicationFactory::ROUTING_MIDDLEWARE, + Helper\UrlHelperMiddleware::class, + LocalizationMiddleware::class, + Zend\Expressive\Container\ApplicationFactory::DISPATCH_MIDDLEWARE, + ], + 'priority' => 1, ], + /* ... */ ], ]; ``` - -This approach is also portable, but, as you can see, requires more setup (a -middleware class + factory + factory registration + middleware registration). -On the flip side, it's portable between applications, which could be something -to consider if you were to make the functionality into a discrete package. diff --git a/doc/book/cookbook/setting-locale-without-routing-parameter.md b/doc/book/cookbook/setting-locale-without-routing-parameter.md index 28b49ce3..4ceaf06b 100644 --- a/doc/book/cookbook/setting-locale-without-routing-parameter.md +++ b/doc/book/cookbook/setting-locale-without-routing-parameter.md @@ -15,15 +15,15 @@ requiring any changes to existing routes. > with a required routing parameter; this approach is described in the > ["Setting a locale based on a routing parameter" recipe](setting-locale-depending-routing-parameter.md). -## Setup a middleware to extract the language from the URI +## Setup a middleware to extract the locale from the URI -First, we need to setup middleware that extracts the language param directly +First, we need to setup middleware that extracts the locale param directly from the request URI's path. If if doesn't find one, it sets a default. -If it does find one, it uses the language to setup the locale. It also: +If it does find one, it uses the value to setup the locale. It also: -- amends the request with a truncated path (removing the language segment). -- adds the langauge segment as the base path of the `UrlHelper`. +- amends the request with a truncated path (removing the locale segment). +- adds the locale segment as the base path of the `UrlHelper`. ```php namespace Application\I18n; @@ -31,7 +31,7 @@ namespace Application\I18n; use Locale; use Zend\Expressive\Helper\UrlHelper; -class SetLanguageMiddleware +class SetLocaleMiddleware { private $helper; @@ -47,14 +47,14 @@ class SetLanguageMiddleware $path = $uri->getPath(); - if (! preg_match('#^/(?P[a-z]{2})/#', $path, $matches) { + if (! preg_match('#^/(?P[a-z]{2})/#', $path, $matches) { Locale::setDefault('de_DE'); return $next($request, $response); } - $lang = $matches['lang']; - Locale::setDefault($lang); - $this->helper->setBasePath($lang); + $locale = $matches['locale']; + Locale::setDefault($locale); + $this->helper->setBasePath($locale); return $next( $request->withUri( @@ -66,7 +66,7 @@ class SetLanguageMiddleware } ``` -Then you will need a factory for the `SetLanguageMiddleware` to inject the +Then you will need a factory for the `SetLocaleMiddleware` to inject the `UrlHelper` instance. ```php @@ -75,18 +75,18 @@ namespace Application\I18n; use Interop\Container\ContainerInterface; use Zend\Expressive\Helper\UrlHelper; -class SetLanguageMiddlewareFactory +class SetLocaleMiddlewareFactory { public function __invoke(ContainerInterface $container) { - return new SetLanguageMiddleware( + return new SetLocaleMiddleware( $container->get(UrlHelper::class) ); } } ``` -Afterwards, you need to configure the `SetLanguageMiddleware` in your +Afterwards, you need to configure the `SetLocaleMiddleware` in your `/config/autoload/middleware-pipeline.global.php` file so that it is executed on every request. @@ -95,33 +95,40 @@ return [ 'dependencies' => [ /* ... */ 'factories' => [ - Application\I18n\SetLanguageMiddleware::class => - Application\I18n\SetLanguageMiddlewareFactory::class, + Application\I18n\SetLocaleMiddleware::class => + Application\I18n\SetLocaleMiddlewareFactory::class, /* ... */ ], ] 'middleware_pipeline' => [ - 'pre_routing' => [ - [ - 'middleware' => [ - Application\I18n\SetLanguageMiddleware::class, - /* ... */ - ], + [ + 'middleware' => [ + Application\I18n\SetLocaleMiddleware::class, /* ... */ ], + 'priority' => 1000, ], - 'post_routing' => [ - /* ... */ + /* ... */ + + 'routing' => [ + 'middleware' => [ + Zend\Expressive\Container\ApplicationFactory::ROUTING_MIDDLEWARE, + Zend\Expressive\Helper\UrlHelperMiddleware::class, + Zend\Expressive\Container\ApplicationFactory::DISPATCH_MIDDLEWARE, + ], + 'priority' => 1, ], + + /* ... */ ], ]; ``` ## Url generation in the view -Since the `UrlHelper` has the language set as a base path, you don't need +Since the `UrlHelper` has the locale set as a base path, you don't need to worry about generating URLs within your view. Just use the helper to generate a URL and it will do the rest. @@ -136,7 +143,7 @@ generate a URL and it will do the rest. ## Redirecting within your middleware -If you want to add the language parameter when creating URIs within your +If you want to add the locale parameter when creating URIs within your action middleware, you just need to inject the `UrlHelper` into your middleware and use it for URL generation: diff --git a/doc/book/cookbook/using-a-base-path.md b/doc/book/cookbook/using-a-base-path.md index 890ca7c5..05810afa 100644 --- a/doc/book/cookbook/using-a-base-path.md +++ b/doc/book/cookbook/using-a-base-path.md @@ -37,8 +37,8 @@ RewriteRule (.*) ./public/$1 The above step ensures that clients can hit the website. Now we need to ensure that the application can route to middleware! -To do this, we will add pre_routing pipeline middleware to intercept the -request, and rewrite the URL accordingly. +To do this, we will add pipeline middleware to intercept the request, and +rewrite the URL accordingly. At the time of writing, we have two suggestions: @@ -106,9 +106,15 @@ return [ /* ... */ ], 'middleware_pipeline' => [ - 'pre_routing' => [ - [ 'middleware' => [ Blast\BaseUrl\BaseUrlMiddleware::class ] ], - /* ... */ + [ 'middleware' => [ Blast\BaseUrl\BaseUrlMiddleware::class ], 'priority' => 1000 ], + /* ... */ + 'routing' => [ + 'middleware' => [ + Zend\Expressive\Container\ApplicationFactory::ROUTING_MIDDLEWARE, + Zend\Expressive\Helper\UrlHelperMiddleware::class, + Zend\Expressive\Container\ApplicationFactory::DISPATCH_MIDDLEWARE, + ], + 'priority' => 1, ], /* ... */ ], diff --git a/doc/book/cookbook/using-zend-form-view-helpers.md b/doc/book/cookbook/using-zend-form-view-helpers.md index aaf3bd71..bc104952 100644 --- a/doc/book/cookbook/using-zend-form-view-helpers.md +++ b/doc/book/cookbook/using-zend-form-view-helpers.md @@ -20,8 +20,8 @@ You have three options: - Replace the `HelperPluginManager` factory with your own; or - Add a delegator factory to or extend the `HelperPluginManager` service to inject the additional helper configuration; or -- Add pre_routing pipeline middleware that composes the `HelperPluginManager` - and configures it. +- Add pipeline middleware that composes the `HelperPluginManager` and configures + it. ## Replacing the HelperPluginManager factory @@ -161,7 +161,7 @@ $container[HelperPluginManager::class] = $container->extend( ## Pipeline middleware -Another option is to use pre_routing pipeline middleware. This approach will +Another option is to use pipeline middleware. This approach will require that the middleware execute on every request, which introduces (very slight) performance overhead. However, it's a portable method that works regardless of the container implementation you choose. @@ -224,13 +224,17 @@ return [ /* ... */ ], 'middleware_pipeline' => [ - 'pre_routing' => [ - ['middleware' => Your\Application\FormHelpersMiddleware::class], - /* ... */ - ], - 'post_routing' => [ - /* ... */ + ['middleware' => Your\Application\FormHelpersMiddleware::class, 'priority' => 1000], + /* ... */ + 'routing' => [ + 'middleware' => [ + Zend\Expressive\Container\ApplicationFactory::ROUTING_MIDDLEWARE, + Zend\Expressive\Helper\UrlHelperMiddleware::class, + Zend\Expressive\Container\ApplicationFactory::DISPATCH_MIDDLEWARE, + ], + 'priority' => 1, ], + /* ... */ ], ]; ``` diff --git a/doc/book/features.md b/doc/book/features.md index e8dfc8fe..9f39ea76 100644 --- a/doc/book/features.md +++ b/doc/book/features.md @@ -98,8 +98,20 @@ argument to act on. > - Responses are returned back *through* the pipeline, in reverse order of > traversal. -The `Application` allows for "pre routing" middleware, routing middleware (and -the routed middleware it dispatches), and "post routing" middleware. +The `Application` allows arbitrary middleware to be injected, with each being +executed in the order in which they are attached; returning a response from +middleware prevents any middleware attached later from executing. + +You can attach middleware manually, in which case the pipeline is executed in +the order of attachment, or use configuration. When you use configuration, you +will specify a priority integer to dictate the order in which middleware should +be attached. Middleware specifying high integer prioritiess are attached (and +thus executed) earlier, while those specifying lower and/or negative integers +are attached later. The default priority is 1. + +Expressive provides a default implementation of "routing" and "dispatch" +middleware, which you either attach to the middleware pipeline manually, or via +configuration. Routing within Expressive consists of decomposing the request to match it to middleware that can handle that given request. This typically consists of a @@ -109,15 +121,22 @@ combination of matching the requested URI path along with allowed HTTP methods: - map a POST request to the path `/contact/process` to the `HandleContactMiddleware` - etc. +Dispatching is simply the act of calling the middleware mapped by routing. The +two events are modeled as separate middleware to allow you to act on the results +of routing before attempting to dispatch the mapped middleware; this can be +useful for implementing route-based authentication or validation. + The majority of your application will consist of routing rules that map to routed middleware. -"Pre routing" middleware is middleware that you wish to execute for every -request. These might include: +Middleware piped to the application earlier than routing should be middleware +that you wish to execute for every request. These might include: -- authentication +- bootstrapping - parsing of request body parameters - addition of debugging tools +- embedded Expressive applications that you want to match at a given literal + path - etc. Such middleware may decide that a request is invalid, and return a response; @@ -125,19 +144,20 @@ doing so means no further middleware will be executed! This is an important feature of middleware architectures, as it allows you to define application-specific workflows optimized for performance, security, etc. -"Post routing" middleware will execute in one of two conditions: +Middleware piped to the application after the routing and dispatch middleware +will execute in one of two conditions: - routing failed - routed middleware called on the next middleware instead of returning a response. -As such, the largest use case for post routing middleware is for error handling. +As such, the largest use case for such middleware is for error handling. One possibility is for [providing custom 404 handling](cookbook/custom-404-page-handling.md), or handling application-specific error conditions (such as authentication or authorization failures). Another possibility is to provide post-processing on the response before -returning it. However, this is typically better handled via pre-routing -middleware, by capturing the response before returning it: +returning it. However, this is typically better handled via middleware piped +early, by capturing the response before returning it: ```php function ($request, $response, $next) diff --git a/doc/book/helpers/body-parse.md b/doc/book/helpers/body-parse.md index 85c81ec6..78e8a136 100644 --- a/doc/book/helpers/body-parse.md +++ b/doc/book/helpers/body-parse.md @@ -26,7 +26,7 @@ $app->pipe(BodyParamsMiddleware::class); $app->run(); ``` -or as `pre_routing` pipeline middleware: +or as pipeline middleware: ```php // config/autoload/middleware-pipeline.global.php @@ -43,13 +43,17 @@ return [ ], ], 'middleware_pipeline' => [ - 'pre_routing' => [ - [ 'middleware' => Helper\BodyParams\BodyParamsMiddleware::class ], - /* ... */ - ], - 'post_routing' => [ - /* ... */ + [ 'middleware' => Helper\BodyParams\BodyParamsMiddleware::class, 'priority' => 100], + /* ... */ + 'routing' => [ + 'middleware' => [ + Zend\Expressive\Container\ApplicationFactory::ROUTING_MIDDLEWARE, + Helper\UrlHelperMiddleware::class, + Zend\Expressive\Container\ApplicationFactory::DISPATCH_MIDDLEWARE, + ], + 'priority' => 1, ], + /* ... */ ], ]; ``` diff --git a/doc/book/helpers/server-url-helper.md b/doc/book/helpers/server-url-helper.md index ed03a466..e2a2cb8a 100644 --- a/doc/book/helpers/server-url-helper.md +++ b/doc/book/helpers/server-url-helper.md @@ -47,7 +47,8 @@ As such, you will need to: - Register the `ServerUrlHelper` as a service in your container. - Register the `ServerUrlMiddleware` as a service in your container. -- Register the `ServerUrlMiddleware` as pre_routing pipeline middleware. +- Register the `ServerUrlMiddleware` as pipeline middleware, immediately + following the routing middleware. The following examples demonstrate registering the services. @@ -78,20 +79,22 @@ $container->set( ); ``` -To register the `ServerUrlMiddleware` as pre-routing pipeline middleware: +To register the `ServerUrlMiddleware` as pipeline middleware following the +routing middleware: ```php use Zend\Expressive\Helper\ServerUrlMiddleware; -// Do this early, before piping other middleware or routes: +// Programmatically: $app->pipe(ServerUrlMiddleware::class); +$app->pipeRoutingMiddleware(); +$app->pipeDispatchMiddleware(); // Or use configuration: // [ // 'middleware_pipeline' => [ -// 'pre_routing' => [ -// ['middleware' => ServerUrlMiddleware::class], -// ], +// ['middleware' => ServerUrlMiddleware::class, 'priority' => PHP_INT_MAX], +// /* ... */ // ], // ] ``` @@ -110,18 +113,17 @@ return [ ], ], 'middleware_pipeline' => [ - 'pre_routing' => [ - ['middleware' => ServerUrlMiddleware::class], - ], + ['middleware' => ServerUrlMiddleware::class, 'priority' => PHP_INT_MAX], + /* ... */ ], -] +]; ``` > ### Skeleton configures helpers > > If you started your project using the Expressive skeleton package, the > `ServerUrlHelper` and `ServerUrlMiddleware` factories are already registered -> for you, as is the `ServerUrlMiddleware` pre_routing pipeline middleware. +> for you, as is the `ServerUrlMiddleware` pipeline middleware. ## Using the helper in middleware diff --git a/doc/book/helpers/url-helper.md b/doc/book/helpers/url-helper.md index 89cf998a..9373f3ba 100644 --- a/doc/book/helpers/url-helper.md +++ b/doc/book/helpers/url-helper.md @@ -2,9 +2,9 @@ `Zend\Expressive\Helper\UrlHelper` provides the ability to generate a URI path based on a given route defined in the `Zend\Expressive\Router\RouterInterface`. -If registered as a route result observer, and the route being used was also -the one matched during routing, you can provide a subset of routing -parameters, and any not provided will be pulled from those matched. +If injected with a route result, and the route being used was also the one +matched during routing, you can provide a subset of routing parameters, and any +not provided will be pulled from those matched. ## Usage @@ -58,15 +58,16 @@ In order to use the helper, you will need to instantiate it with the current `RouterInterface`. The factory `Zend\Expressive\Helper\UrlHelperFactory` has been provided for this purpose, and can be used trivially with most dependency injection containers implementing container-interop. Additionally, -it is most useful when injected with the current results of routing, and as -such should be registered as a route result observer with the application. The -following steps should be followed to register and configure the helper: +it is most useful when injected with the current results of routing, which +requires registering middleware with the application that can inject the route +result. The following steps should be followed to register and configure the helper: - Register the `UrlHelper` as a service in your container, using the provided factory. - Register the `UrlHelperMiddleware` as a service in your container, using the provided factory. -- Register the `UrlHelperMiddleware` as pre_routing pipeline middleware. +- Register the `UrlHelperMiddleware` as pipeline middleware, immediately + following the routing middleware. ### Registering the helper service @@ -112,22 +113,45 @@ return ['dependencies' => [ ### Registering the pipeline middleware -To register the `UrlHelperMiddleware` as pre-routing pipeline middleware: +To register the `UrlHelperMiddleware` as pipeline middleware following the +routing middleware: ```php use Zend\Expressive\Helper\UrlHelperMiddleware; -// Do this early, before piping other middleware or routes: +// Programmatically: +$app->pipeRoutingMiddleware(); $app->pipe(UrlHelperMiddleware::class); +$app->pipeDispatchMiddleware(); // Or use configuration: // [ // 'middleware_pipeline' => [ -// 'pre_routing' => [ -// ['middleware' => UrlHelperMiddleware::class], +// /* ... */ +// Zend\Expressive\Container\ApplicationFactory::ROUTING_MIDDLEWARE, +// ['middleware' => UrlHelperMiddleware::class], +// Zend\Expressive\Container\ApplicationFactory::DISPATCH_MIDDLEWARE, +// /* ... */ +// ], +// ] +// +// Alternately, create a nested middleware pipeline for the routing, UrlHelper, +// and dispatch middleware: +// [ +// 'middleware_pipeline' => [ +// /* ... */ +// 'routing' => [ +// 'middleware' => [ +// Zend\Expressive\Container\ApplicationFactory::ROUTING_MIDDLEWARE, +// UrlHelperMiddleware::class +// Zend\Expressive\Container\ApplicationFactory::DISPATCH_MIDDLEWARE, +// ], +// 'priority' => 1, // ], +// /* ... */ // ], // ] + ``` The following dependency configuration will work for all three when using the @@ -142,18 +166,38 @@ return [ ], ], 'middleware_pipeline' => [ - 'pre_routing' => [ - ['middleware' => UrlHelperMiddleware::class], + Zend\Expressive\Container\ApplicationFactory::ROUTING_MIDDLEWARE, + ['middleware' => UrlHelperMiddleware::class], + Zend\Expressive\Container\ApplicationFactory::DISPATCH_MIDDLEWARE, + ], +]; + +// OR: +return [ + 'dependencies' => [ + 'factories' => [ + UrlHelper::class => UrlHelperFactory::class, + UrlHelperMiddleware::class => UrlHelperMiddlewareFactory::class, + ], + ], + 'middleware_pipeline' => [ + 'routing' => [ + 'middleware' => [ + Zend\Expressive\Container\ApplicationFactory::ROUTING_MIDDLEWARE, + UrlHelperMiddleware::class, + Zend\Expressive\Container\ApplicationFactory::DISPATCH_MIDDLEWARE, + ], + 'priority' => 1, ], ], -] +]; ``` > #### Skeleton configures helpers > > If you started your project using the Expressive skeleton package, the > `UrlHelper` and `UrlHelperMiddleware` factories are already registered for -> you, as is the `UrlHelperMiddleware` pre_routing pipeline middleware. +> you, as is the `UrlHelperMiddleware` pipeline middleware. ## Using the helper in middleware diff --git a/doc/book/images/architecture.dia b/doc/book/images/architecture.dia index 7c21e11d..e96864a5 100644 Binary files a/doc/book/images/architecture.dia and b/doc/book/images/architecture.dia differ diff --git a/doc/book/images/architecture.png b/doc/book/images/architecture.png index e61ff09d..c3a99bda 100644 Binary files a/doc/book/images/architecture.png and b/doc/book/images/architecture.png differ diff --git a/doc/book/images/architecture.svg b/doc/book/images/architecture.svg index 8f3af7be..183c34a1 100644 --- a/doc/book/images/architecture.svg +++ b/doc/book/images/architecture.svg @@ -1,7 +1,9 @@ - + + + @@ -11,8 +13,7 @@ - - + @@ -25,9 +26,15 @@ - - - + + + + + + + + + diff --git a/doc/book/images/architecture.xcf b/doc/book/images/architecture.xcf index aa471937..93861758 100644 Binary files a/doc/book/images/architecture.xcf and b/doc/book/images/architecture.xcf differ diff --git a/doc/book/migration/bookdown.json b/doc/book/migration/bookdown.json new file mode 100644 index 00000000..3dc87476 --- /dev/null +++ b/doc/book/migration/bookdown.json @@ -0,0 +1,7 @@ +{ + "title": "Migration", + "content": [ + {"From RC5 and Earlier": "rc-to-v1.md"} + ] +} + diff --git a/doc/book/migration/rc-to-v1.md b/doc/book/migration/rc-to-v1.md new file mode 100644 index 00000000..dcc7a416 --- /dev/null +++ b/doc/book/migration/rc-to-v1.md @@ -0,0 +1,491 @@ +# Migration from RC5 or earlier + +RC6 introduced changes to the following: + +- The routing middleware was split into separate middleware, one for routing, + and one for dispatching. +- Due to the above change, we decided to remove auto-registration of routing + middleware. +- The above change also suggested an alternative to the middleware pipeline + configuration that simplifies it. +- Route result observers are deprecated, and no longer triggered for routing + failures. +- Middleware configuration specifications now accept a `priority` key to + guarantee the order of items. If you have defined your middleware pipeline in + multiple files that are then merged, you will need to defined these keys to + ensure order. + +## Routing and Dispatch middleware + +Prior to RC6, the routing middleware: + +- performed routing +- notified route result observers +- created a new request that composed the matched routing parameters as request + attributes, and composed the route result instance itself as a request + attribute. +- marshaled the middleware matched by routing +- dispatched the marshaled middleware + +To provide a better separation of concerns, we split the routing middleware into +two distinct methods: `routingMiddleware()` and `dispatchMiddleware()`. + +`routingMiddleware()` performs the following duties: + +- routing; and +- creating a new request that composes the matched routing parameters as request + attributes, and composes the route result instance itself as a request + attribute. + +`dispatchMiddleware()` performs the following duties: + +- marshaling the middleware specified in the route result; and +- dispatching the marshaled middleware. + +One reason for this split is to allow injecting middleware to operate between +routing and dispatch. As an example, you could have middleware that determines +if a matched route requires an authenticated identity: + +```php +public function __invoke($request, $response, $next) +{ + $result = $request->getAttribute(RouteResult::class); + if (! in_array($result->getMatchedRouteName(), $this->authRequired)) { + return $next($request, $response); + } + + if (! $this->authenticated) { + return $next($request, $response->withStatus(401), 'authentication + required'); + } +} +``` + +The above could then be piped between the routing and dispatch middleware: + +```php +$app->pipeRoutingMiddleware(); +$app->pipe(AuthenticationMiddleware::class); +$app->pipeDispatchMiddleware(); +``` + +Since the routing middleware has been split, we determined we could no longer +automatically pipe the routing middleware; detection would require detecting +both sets of middleware, and ensuring they are in the correct order. +Additionally, since one goal of splitting the middleware is to allow +*substitutions* for these responsibilities, auto-injection could in some cases +be undesired. As a result, we now require you to inject each manually. + +### Impact + +This change will require changes in your application. + +1. If you are using Expressive programmatically (i.e., you are not using + a container and the `Zend\Expressive\Container\ApplicationFactory`), + you are now *required* to call `Application::pipeRoutingMiddleware()`. + Additionally, a new method, `Application::pipeDispatchMiddleware()` exists + for injecting the application with the dispatch middleware, this, too, must + be called. + + This has a fortunate side effect: registering routed middleware no longer + affects the middleware pipeline order. As such, you can register your + pipeline first or last prior to running the application. The only stipulation + is that _unless you register the routing **and** dispatch middleware, your routed + middleware will not be executed!_ As such, the following two lines **must** + be added to your application prior to calling `Application::run()`: + +```php +$app->pipeRoutingMiddleware(); +$app->pipeDispatchMiddleware(); +``` + +2. If you are creating your `Application` instance using a container and the + `Zend\Expressive\Container\ApplicationFactory`, you will need to update your + configuration to list the routing and dispatch middleware. The next section + details the configuration changes necessary. + +## ApplicationFactory configuration changes + +As noted in the document summary, the middleware pipeline configuration was +changed starting in RC6. The changes are done in such a way as to honor +configuration from RC5 and earlier, but using such configuration will now prompt +you to update your application. + +RC5 and earlier defined the default `middleware_pipeline` configuration as follows: + +```php +return [ + 'middleware_pipeline' => [ + // An array of middleware to register prior to registration of the + // routing middleware + 'pre_routing' => [ + //[ + // Required: + // 'middleware' => 'Name or array of names of middleware services and/or callables', + // Optional: + // 'path' => '/path/to/match', + // 'error' => true, + //], + [ + 'middleware' => [ + Helper\ServerUrlMiddleware::class, + Helper\UrlHelperMiddleware::class, + ], + ], + ], + + // An array of middleware to register after registration of the + // routing middleware + 'post_routing' => [ + //[ + // Required: + // 'middleware' => 'Name of middleware service, or a callable', + // Optional: + // 'path' => '/path/to/match', + // 'error' => true, + //], + ], + ], +]; +``` + +The following changes have been made: + +- The concept of `pre_routing` and `post_routing` have been deprecated, and will + be removed starting with the 1.1 version. A single middleware pipeline is now + provided, though *any individual specification can also specify an array of + middleware*. +- **The routing and dispatch middleware must now be added to your configuration + for them to be added to your application.** +- Middleware specifications can now optionally provide a `priority` key, with 1 + being the default. High integer priority indicates earlier execution, while + low/negative integer priority indicates later execution. Items with the same + priority are executed in the order they are registered. Priority is now how + you can indicate the order in which middleware should execute. + +### Impact + +While the configuration from RC5 and earlier will continue to work, it will +raise deprecation notices. As such, you will need to update your configuration +to follow the guidelines created with RC6. + +RC6 and later change the configuration to remove the `pre_routing` and +`post_routing` keys. However, individual items within the array retain the same +format as middleware inside those keys, with the addition of a new key, +`priority`: + +```php +[ + // Required: + 'middleware' => 'Name of middleware service, or a callable', + // Optional: + // 'path' => '/path/to/match', + // 'error' => true, + // 'priority' => 1, // integer +] +``` + +The `priority` key is used to determine the order in which middleware is piped +to the application. Higher integer values are piped earlier, while +lower/negative integer values are piped later; middleware with the same priority +are piped in the order in which they are discovered in the pipeline. The default +priority used is 1. + +Additionally, the routing and dispatch middleware now become items in the array; +they (or equivalent entries for your own implementations) must be present in +your configuration if you want your routed middleware to dispatch! This change +gives you full control over the flow of the pipeline. + +To specify the routing middleware, use the constant +`Zend\Expressive\Container\ApplicationFactory::ROUTING_MIDDLEWARE` in place of +a middleware array; this has the value `EXPRESSIVE_ROUTING_MIDDLEWARE`, if you +do not want to import the class. Similarly, for the dispatch middleware, use the +constant `Zend\Expressive\Container\ApplicationFactory::DISPATCH_MIDDLEWARE` +(value `EXPRESSIVE_DISPATCH_MIDDLEWARE`) to specify the dispatch middleware. + +As such, the default configuration now becomes: + +```php +return [ + 'middleware_pipeline' => [ + // An array of middleware to pipe to the application. + // Each item is of the following structure: + // [ + // // Required: + // 'middleware' => 'Name or array of names of middleware services and/or callables', + // // Optional: + // 'path' => '/path/to/match', + // 'error' => true, + // ], + [ + 'middleware' => [ + Helper\ServerUrlMiddleware::class, + ], + 'priority' => PHP_INT_MAX, + ], + + // The following is an entry for: + // - routing middleware + // - middleware that reacts to the routing results + // - dispatch middleware + [ + 'middleware' => [ + Zend\Expressive\Container\ApplicationFactory::ROUTING_MIDDLEWARE, + Helper\UrlHelperMiddleware::class, + Zend\Expressive\Container\ApplicationFactory::DISPATCH_MIDDLEWARE, + ], + 'priority' => 1, + ] + + // The following is an entry for the dispatch middleware: + + // Place error handling middleware after the routing and dispatch + // middleware, with negative priority. + // [ + // 'middleware' => [ + // ], + // 'priority' => -1000, + // ], + ], +]; +``` + +To update an existing application: + +- Promote all `pre_routing` middleware up a level, and remove the `pre_routing` + key. Provide a `priority` value greater than 1. We recommend having a single + middleware specification with an array of middleware that represents the "pre + routing" middleware. +- Add the entries for `Zend\Expressive\Container\ApplicationFactory::ROUTING_MIDDLEWARE` + and `Zend\Expressive\Container\ApplicationFactory::DISPATCH_MIDDLEWARE` + immediately following any `pre_routing` middleware, and before any + `post_routing` middleware; we recommend grouping it per the above example. +- Promote all `post_routing` middleware up a level, and remove the + `post_routing` key. Provide a `priority` value less than 1 or negative. +- **If you have `middleware_pipeline` specifications in multiple files**, you + will need to specify `priority` keys for all middleware in order to guarantee + order after merging. We recommend having a single middleware specification + with an array of middleware that represents the "post routing" middleware. + +As an example, consider the following application configuration: + +```php +return [ + 'middleware_pipeline' => [ + 'pre_routing' => [ + [ + 'middleware' => [ + Zend\Expressive\Helper\ServerUrlMiddleware::class, + Zend\Expressive\Helper\UrlHelperMiddleware::class, + ], + ], + [ 'middleware' => DebugToolbarMiddleware::class ], + [ + 'middleware' => ApiMiddleware::class, + 'path' => '/api, + ], + ], + + 'post_routing' => [ + ['middleware' => NotFoundMiddleware::class, 'error' => true], + ], + ], +]; +``` + +This would be rewritten to the following to work with RC6 and later: + +```php +return [ + 'middleware_pipeline' => [ + 'always' => [ + 'middleware' => [ + Zend\Expressive\Helper\ServerUrlMiddleware::class, + DebugToolbarMiddleware::class, + ], + 'priority' => PHP_INT_MAX, + ], + 'api' => [ + 'middleware' => ApiMiddleware::class, + 'path' => '/api, + 'priority' => 100, + ], + + 'routing' => [ + 'middleware' => [ + Zend\Expressive\Container\ApplicationFactory::ROUTING_MIDDLEWARE, + Zend\Expressive\Helper\UrlHelperMiddleware::class, + Zend\Expressive\Container\ApplicationFactory::DISPATCH_MIDDLEWARE, + ], + 'priority' => 1, + ], + + 'error' => [ + 'middleware' => [ + NotFoundMiddleware::class, + ], + 'error' => true, + 'priority' => -1000, + ], + ], +] +``` + +Note in the above example the various groupings. By grouping middleware by +priority, you can simplify adding new middleware, particularly if you know it +should execute before routing, or as error middleware, or between routing and +dispatch. + +> #### Keys are ignored +> +> The above example provides keys for each middleware specification. The factory +> will ignore these, but they can be useful for cases when you might want to +> specify configuration in multiple files, and merge specific entries together. +> Be aware, however, that the `middleware` key itself is an indexed array; +> items will be appended based on the order in which configuration files are +> merged. If order of these is important, create separate specifications with +> relevant `priority` values. + +## Route result observer deprecation + +As of RC6, the following changes have occurred with regards to route result +observers: + +- They are deprecated for usage with `Zend\Expressive\Application`, and that + class will not be a route result subject starting in 1.1. You will need to + start migrating to alternative solutions. +- The functionality for notifying observers has been moved from the routing + middleware into a dedicated `Application::routeResultObserverMiddleware()` + method. This middleware must be piped separately to the middleware pipeline + for it to trigger. + +### Impact + +If you are using any route result observers, you will need to ensure your +application notifies them, and you will want to migrate to alternative solutions +to ensure your functionality continues to work. + +To ensure your observers are triggered, you will need to adapt your application, +based on how you create your instance. + +If you are *not* using the `ApplicationFactory`, you will need to pipe the +`routeResultObserverMiddleware` to your application, between the routing and +dispatch middleware: + +```php +$app->pipeRoutingMiddleware(); +$app->pipeRouteResultObserverMiddleware(); +$app->pipeDispatchMiddleware(); +``` + +If you are using the `ApplicationFactory`, you may need to update your +configuration to allow injecting the route result observer middleware. If you +have *not* updated your configuration to remove the `pre_routing` and/or +`post_routing` keys, the middleware *will* be registered for you. If you have, +however, you will need to register it following the routing middleware: + +```php +[ + 'middleware_pipeline' => [ + /* ... */ + 'routing' => [ + 'middleware' => [ + Zend\Expressive\Container\ApplicationFactory::ROUTING_MIDDLEWARE, + Zend\Expressive\Container\ApplicationFactory::ROUTE_RESULT_OBSERVER_MIDDLEWARE, + Zend\Expressive\Container\ApplicationFactory::DISPATCH_MIDDLEWARE, + ], + 'priority' => 1, + ], + /* ... */ + ], +] +``` + +To make your observers forwards-compatible requires two changes: + +- Rewriting your observer as middleware. +- Registering your observer as middleware following the routing middleware. + +If your observer looked like the following: + +```php +use Zend\Expressive\Router\RouteResult; +use Zend\Expressive\Router\RouteResultObserverInterface; + +class MyObserver implements RouteResultObserverInterface +{ + private $logger; + + public function __construct(Logger $logger) + { + $this->logger = $logger; + } + + public function update(RouteResult $result) + { + $this->logger->log($result); + } +} +``` + +You could rewrite it as follows: + +```php +use Zend\Expressive\Router\RouteResult; + +class MyObserver +{ + private $logger; + + public function __construct(Logger $logger) + { + $this->logger = $logger; + } + + public function __invoke($request, $response, $next) + { + $result = $request->getAttribute(RouteResult::class, false); + if (! $result) { + return $next($request, $response); + } + + $this->logger->log($result); + return $next($request, $response); + } +} +``` + +You would then register it following the routing middleware. If you are building +your application programmatically, you would do this as follows: + +```php +$app->pipeRoutingMiddleware(); +$app->pipe(MyObserver::class); +$app->pipeDispatchMiddleware(); +``` + +If you are using the `ApplicationFactory`, alter your configuration: + +```php +[ + 'middleware_pipeline' => [ + /* ... */ + 'routing' => [ + 'middleware' => [ + Zend\Expressive\Container\ApplicationFactory::ROUTING_MIDDLEWARE, + MyObserver::class, + Zend\Expressive\Container\ApplicationFactory::DISPATCH_MIDDLEWARE, + ], + 'priority' => 1, + ], + /* ... */ + ], +] +``` + +## Timeline for migration + +The following features will be removed in version 1.1.0: + +- Support for the `pre_routing` and `post_routing` configuration. +- Support for route result observers. diff --git a/doc/book/router/result-observers.md b/doc/book/router/result-observers.md index 8dd2e1b0..26760564 100644 --- a/doc/book/router/result-observers.md +++ b/doc/book/router/result-observers.md @@ -1,5 +1,12 @@ # Route Result Observers +> ## DEPRECATED! +> +> The route result observers feature existed prior to the stable 1.0 release, +> but was deprecated with 1.0.0RC6. Please do not use this feature; instead, +> you can inject middleware between the routing and dispatch middleware that can +> act on the matched route result. + Occasionally, you may have need of the `RouteResult` within other application code. As a primary example, a URI generator may want this information to allow creating "self" URIs, or to allow presenting a subset of parameters to generate diff --git a/doc/book/usage-examples.md b/doc/book/usage-examples.md index c9fdb9b3..c033534e 100644 --- a/doc/book/usage-examples.md +++ b/doc/book/usage-examples.md @@ -451,26 +451,16 @@ return [ // etc. ], 'middleware_pipeline' => [ - 'pre_routing' => [ - // See specification below - ], - 'post_routing' => [ - // See specification below - ], + // See specification below ], ]; ``` -The key to note is `middleware_pipeline`, which can have two subkeys, -`pre_routing` and `post_routing`. Each accepts an array of middlewares to -register in the pipeline; they will each be `pipe()`'d to the Application in the -order specified. Those specified `pre_routing` will be registered before any -routes, and thus before the routing middleware, while those specified -`post_routing` will be `pipe()`'d afterwards (again, also in the order -specified). +The key to note is `middleware_pipeline`, which is an array of middlewares to +register in the pipeline; each will each be `pipe()`'d to the Application in the +order specified. -Each middleware specified in either `pre_routing` or `post_routing` must be in -the following form: +Each middleware specified must be in the following form: ```php [ @@ -479,11 +469,97 @@ the following form: // optional: 'path' => '/path/to/match', 'error' => true, + 'priority' => 1, // Integer ] ``` -Middleware may be any callable, `Zend\Stratigility\MiddlewareInterface` -implementation, or a service name that resolves to one of the two. +Priority should be an integer, and follows the semantics of +[SplPriorityQueue](http://php.net/SplPriorityQueue): higher numbers indicate +higher priority (top of the queue; executed earliest), while lower numbers +indicated lower priority (bottom of the queue, executed last); *negative values +are low priority*. Items of the same priority are executed in the order in which +they are attached. + +The default priority is 1, and this priority is used by the routing and dispatch +middleware. To indicate that middleware should execute *before* these, use a +priority higher than 1. For error middleware, use a priority less than 1. + +The above specification can be used for all middleware, with one exception: +registration of the *routing* and/or *dispatch* middleware that Expressive +provides. In these cases, use the following constants, which will be caught by +the factory and expanded: + +- `Zend\Expressive\Container\ApplicationFactory::ROUTING_MIDDLEWARE` for the + routing middleware; this should always come before the dispatch middleware. +- `Zend\Expressive\Container\ApplicationFactory::DISPATCH_MIDDLEWARE` for the + dispatch middleware. + +As an example: + +```php +return [ + 'middleware_pipeline' => [ + [ /* ... */ ], + Zend\Expressive\Container\ApplicationFactory::ROUTING_MIDDLEWARE, + Zend\Expressive\Container\ApplicationFactory::DISPATCH_MIDDLEWARE, + [ /* ... */ ], + ], +]; +``` + +> #### Place routing middleware correctly +> +> If you are defining routes *and* defining other middleware for the pipeline, +> you **must** add the routing middleware. When you do so, make sure you put +> it at the appropriate location in the pipeline. +> +> Typically, you will place any middleware you want to execute on all requests +> prior to the routing middleware. This includes utilities for bootstrapping +> the application (such as injection of the `ServerUrlHelper`), +> utilities for injecting common response headers (such as CORS support), etc. +> Make sure these middleware specifications include the `priority` key, and that +> the value of this key is greater than 1. +> +> Place *error* middleware *after* the routing middleware. This is middleware +> that should only execute if routing fails or routed middleware cannot complete +> the response. These specifications should also include the `priority` key, and +> the value of that key for such middleware should be less than 1 or negative. +> +> Use priority to shape the specific workflow you want for your middleware. + +Middleware items may be any callable, `Zend\Stratigility\MiddlewareInterface` +implementation, or a service name that resolves to one of the two. Additionally, +you can specify an array of such values; these will be composed in a single +`Zend\Stratigility\MiddlewarePipe` instance, allowing layering of middleware. +In fact, you can specify the various `ApplicationFactory::*_MIDDLEWARE` +constants in such arrays as well: + +```php +return [ + 'middleware_pipeline' => [ + [ /* ... */ ], + 'routing' => [ + 'middleware' => [ + Zend\Expressive\Container\ApplicationFactory::ROUTING_MIDDLEWARE, + /* ... middleware that introspects routing results ... */ + Zend\Expressive\Container\ApplicationFactory::DISPATCH_MIDDLEWARE, + ], + 'priority' => 1, + ], + [ /* ... */ ], + ], +]; +``` + +> #### Pipeline keys are ignored +> +> Keys in a `middleware_pipeline` specification are ignored. However, they can +> be useful when merging several configurations; if multiple configuration files +> specify the same key, then those entries will be merged. Be aware, however, +> that the `middleware` entry for each, since it is an indexed array, will +> merge arrays by appending; in other words, order will not be guaranteed within +> that array after merging. If order is critical, define a middleware spec with +> `priority` keys. The path, if specified, can only be a literal path to match, and is typically used for segregating middleware applications or applying rules to subsets of an diff --git a/doc/bookdown.json b/doc/bookdown.json index 9ff7395a..85a1d28d 100644 --- a/doc/bookdown.json +++ b/doc/bookdown.json @@ -14,7 +14,8 @@ {"Emitters": "book/emitters.md"}, {"Examples": "book/usage-examples.md"}, "book/cookbook/bookdown.json", - {"Expressive Projects": "book/expressive-projects.md"} + {"Expressive Projects": "book/expressive-projects.md"}, + "book/migration/bookdown.json" ], "target": "./html" } diff --git a/mkdocs.yml b/mkdocs.yml index b641106b..0a3b85ff 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -15,6 +15,7 @@ pages: - { Examples: usage-examples.md } - { Cookbook: [{ 'Prepending a common path to all routes': cookbook/common-prefix-for-routes.md }, { 'Route-specific middleware pipelines': cookbook/route-specific-pipeline.md }, { 'Setting custom 404 page handling': cookbook/custom-404-page-handling.md }, { 'Registering custom view helpers when using zend-view': cookbook/using-custom-view-helpers.md }, { 'Using zend-form view helpers': cookbook/using-zend-form-view-helpers.md }, { 'Using Expressive from a subdirectory': cookbook/using-a-base-path.md }, { 'Building modular applications': cookbook/modular-layout.md }, { 'Setting a locale based on a routing parameter': cookbook/setting-locale-depending-routing-parameter.md }, { 'Setting a locale without a routing parameter': cookbook/setting-locale-without-routing-parameter.md }, { 'Enabling debug toolbars': cookbook/debug-toolbars.md }, { 'Handling multiple routes in a single class': cookbook/using-routed-middleware-class-as-controller.md }] } - { 'Expressive Projects': expressive-projects.md } + - { Migration: [{ 'From RC5 and Earlier': migration/rc-to-v1.md }] } site_name: zend-expressive site_description: 'zend-expressive: PSR-7 Middleware Microframework' repo_url: 'https://github.com/zendframework/zend-expressive' diff --git a/src/Application.php b/src/Application.php index 9353e325..8c67c450 100644 --- a/src/Application.php +++ b/src/Application.php @@ -23,6 +23,8 @@ /** * Middleware application providing routing based on paths and HTTP methods. * + * @todo For 1.1, remove the RouteResultSubjectInterface implementation, and + * all deprecated properties and methods. * @method Router\Route get($path, $middleware, $name = null) * @method Router\Route post($path, $middleware, $name = null) * @method Router\Route put($path, $middleware, $name = null) @@ -31,11 +33,19 @@ */ class Application extends MiddlewarePipe implements Router\RouteResultSubjectInterface { + use MarshalMiddlewareTrait; + /** * @var null|ContainerInterface */ private $container; + /** + * @var bool Flag indicating whether or not the dispatch middleware is + * registered in the middleware pipeline. + */ + private $dispatchMiddlewareIsRegistered = false; + /** * @var EmitterInterface */ @@ -68,6 +78,13 @@ class Application extends MiddlewarePipe implements Router\RouteResultSubjectInt */ private $router; + /** + * @deprecated This property will be removed in v1.1. + * @var bool Flag indicating whether or not the route result observer + * middleware is registered in the middleware pipeline. + */ + private $routeResultObserverMiddlewareIsRegistered = false; + /** * Observers to trigger once we have a route result. * @@ -120,7 +137,6 @@ public function __construct( */ public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $out = null) { - $this->pipeRoutingMiddleware(); $out = $out ?: $this->getFinalHandler($response); return parent::__invoke($request, $response, $out); } @@ -176,6 +192,7 @@ public function any($path, $middleware, $name = null) /** * Attach a route result observer. * + * @deprecated This method will be removed in v1.1. * @param Router\RouteResultObserverInterface $observer */ public function attachRouteResultObserver(Router\RouteResultObserverInterface $observer) @@ -186,6 +203,7 @@ public function attachRouteResultObserver(Router\RouteResultObserverInterface $o /** * Detach a route result observer. * + * @deprecated This method will be removed in v1.1. * @param Router\RouteResultObserverInterface $observer */ public function detachRouteResultObserver(Router\RouteResultObserverInterface $observer) @@ -199,6 +217,7 @@ public function detachRouteResultObserver(Router\RouteResultObserverInterface $o /** * Notify all route result observers with the given route result. * + * @deprecated This method will be removed in v1.1. * @param Router\RouteResult */ public function notifyRouteResultObservers(Router\RouteResult $result) @@ -232,41 +251,47 @@ public function notifyRouteResultObservers(Router\RouteResult $result) * the upshot is that you will not be notified if the service is invalid to * use as middleware until runtime. * - * Additionally, ensures that the route middleware is only ever registered + * Middleware may also be passed as an array; each item in the array must + * resolve to middleware eventually (i.e., callable or service name). + * + * Finally, ensures that the route middleware is only ever registered * once. * - * @param string|callable $path Either a URI path prefix, or middleware. - * @param null|string|callable $middleware Middleware + * @param string|array|callable $path Either a URI path prefix, or middleware. + * @param null|string|array|callable $middleware Middleware * @return self */ public function pipe($path, $middleware = null) { - // Lazy-load middleware from the container when possible - $container = $this->container; - if (null === $middleware && is_string($path) && $container && $container->has($path)) { - $middleware = $this->marshalLazyMiddlewareService($path, $container); - $path = '/'; - } elseif (is_string($middleware) - && ! is_callable($middleware) - && $container - && $container->has($middleware) + if (null === $middleware) { + $middleware = $this->prepareMiddleware($path, $this->container); + $path = '/'; + } + + if (! is_callable($middleware) + && (is_string($middleware) || is_array($middleware)) ) { - $middleware = $this->marshalLazyMiddlewareService($middleware, $container); - } elseif (null === $middleware && is_callable($path)) { - $middleware = $path; - $path = '/'; + $middleware = $this->prepareMiddleware($middleware, $this->container); } if ($middleware === [$this, 'routeMiddleware'] && $this->routeMiddlewareIsRegistered) { return $this; } + if ($middleware === [$this, 'dispatchMiddleware'] && $this->dispatchMiddlewareIsRegistered) { + return $this; + } + parent::pipe($path, $middleware); if ($middleware === [$this, 'routeMiddleware']) { $this->routeMiddlewareIsRegistered = true; } + if ($middleware === [$this, 'dispatchMiddleware']) { + $this->dispatchMiddlewareIsRegistered = true; + } + return $this; } @@ -303,20 +328,15 @@ public function pipe($path, $middleware = null) */ public function pipeErrorHandler($path, $middleware = null) { - // Lazy-load middleware from the container - $container = $this->container; - if (null === $middleware && is_string($path) && $container && $container->has($path)) { - $middleware = $this->marshalLazyErrorMiddlewareService($path, $container); - $path = '/'; - } elseif (is_string($middleware) - && ! is_callable($middleware) - && $container - && $container->has($middleware) + if (null === $middleware) { + $middleware = $this->prepareMiddleware($path, $this->container, $forError = true); + $path = '/'; + } + + if (! is_callable($middleware) + && (is_string($middleware) || is_array($middleware)) ) { - $middleware = $this->marshalLazyErrorMiddlewareService($middleware, $container); - } elseif (null === $middleware && is_callable($path)) { - $middleware = $path; - $path = '/'; + $middleware = $this->prepareMiddleware($middleware, $this->container, $forError = true); } $this->pipe($path, $middleware); @@ -335,11 +355,42 @@ public function pipeRoutingMiddleware() $this->pipe([$this, 'routeMiddleware']); } + /** + * Register the dispatch middleware in the middleware pipeline. + */ + public function pipeDispatchMiddleware() + { + if ($this->dispatchMiddlewareIsRegistered) { + return; + } + $this->pipe([$this, 'dispatchMiddleware']); + } + + /** + * Register the route result observer middleware in the middleware pipeline. + * + * @deprecated This method will be removed in v1.1. + */ + public function pipeRouteResultObserverMiddleware() + { + if ($this->routeResultObserverMiddlewareIsRegistered) { + return; + } + $this->pipe([$this, 'routeResultObserverMiddleware']); + $this->routeResultObserverMiddlewareIsRegistered = true; + } + /** * Middleware that routes the incoming request and delegates to the matched middleware. * - * Uses the router to route the incoming request, dispatching matched - * middleware on a request success condition. + * Uses the router to route the incoming request, injecting the request + * with: + * + * - the route result object (under a key named for the RouteResult class) + * - attributes for each matched routing parameter + * + * On completion, it calls on the next middleware (typically the + * `dispatchMiddleware()`). * * If routing fails, `$next()` is called; if routing fails due to HTTP * method negotiation, the response is set to a 405, injected with an @@ -350,14 +401,10 @@ public function pipeRoutingMiddleware() * @param ResponseInterface $response * @param callable $next * @return ResponseInterface - * @throws Exception\InvalidArgumentException if the route result does not contain middleware - * @throws Exception\InvalidArgumentException if unable to retrieve middleware from the container - * @throws Exception\InvalidArgumentException if unable to resolve middleware to a callable */ public function routeMiddleware(ServerRequestInterface $request, ResponseInterface $response, callable $next) { $result = $this->router->match($request); - $this->notifyRouteResultObservers($result); if ($result->isFailure()) { if ($result->isMethodFailure()) { @@ -374,21 +421,75 @@ public function routeMiddleware(ServerRequestInterface $request, ResponseInterfa $request = $request->withAttribute($param, $value); } - $middleware = $result->getMatchedMiddleware(); + return $next($request, $response); + } + + /** + * Dispatch the middleware matched by routing. + * + * If the request does not have the route result, calls on the next + * middleware. + * + * Next, it checks if the route result has matched middleware; if not, it + * raises an exception. + * + * Finally, it attempts to marshal the middleware, and dispatches it when + * complete, return the response. + * + * @param ServerRequestInterface $request + * @param ResponseInterface $response + * @param callable $next + * @returns ResponseInterface + * @throws Exception\InvalidMiddlewareException if no middleware is present + * to dispatch in the route result. + */ + public function dispatchMiddleware(ServerRequestInterface $request, ResponseInterface $response, callable $next) + { + $routeResult = $request->getAttribute(Router\RouteResult::class, false); + if (! $routeResult) { + return $next($request, $response); + } + + $middleware = $routeResult->getMatchedMiddleware(); if (! $middleware) { throw new Exception\InvalidMiddlewareException(sprintf( 'The route %s does not have a middleware to dispatch', - $result->getMatchedRouteName() + $routeResult->getMatchedRouteName() )); } - if (is_array($middleware) && ! is_callable($middleware)) { - $middlewarePipe = $this->marshalMiddlewarePipe($middleware); - return $middlewarePipe($request, $response, $next); + $middleware = $this->prepareMiddleware($middleware, $this->container); + return $middleware($request, $response, $next); + } + + /** + * Middleware for notifying route result observers. + * + * If the request has a route result, calls notifyRouteResultObservers(). + * + * This middleware should be injected between the routing and dispatch + * middleware when creating your middleware pipeline. + * + * If you are using this, rewrite your observers as middleware that + * pulls the route result from the request instead. + * + * @deprecated This method will be removed in v1.1. + * @param ServerRequestInterface $request + * @param ResponseInterface $response + * @param callable $next + * @returns ResponseInterface + */ + public function routeResultObserverMiddleware( + ServerRequestInterface $request, + ResponseInterface $response, + callable $next + ) { + $result = $request->getAttribute(Router\RouteResult::class, false); + if ($result) { + $this->notifyRouteResultObservers($result); } - $callable = $this->marshalMiddleware($middleware); - return $callable($request, $response, $next); + return $next($request, $response); } /** @@ -432,7 +533,6 @@ public function route($path, $middleware = null, array $methods = null, $name = $this->routes[] = $route; $this->router->addRoute($route); - $this->pipeRoutingMiddleware(); return $route; } @@ -554,154 +654,4 @@ private function checkForDuplicateRoute($path, $methods = null) ); } } - - /** - * Attempts to retrieve middleware from the container, or instantiate it directly. - * - * @param string $middleware - * - * @return callable - * @throws Exception\InvalidMiddlewareException If unable to obtain callable middleware - */ - private function marshalMiddleware($middleware) - { - if (is_callable($middleware)) { - return $middleware; - } - - if (! is_string($middleware)) { - throw new Exception\InvalidMiddlewareException( - 'The middleware specified is not callable' - ); - } - - // try to get the action name from the container (if exists) - $callable = $this->marshalMiddlewareFromContainer($middleware); - - if (is_callable($callable)) { - return $callable; - } - - // try to instantiate the middleware directly, if possible - $callable = $this->marshalInvokableMiddleware($middleware); - - if (is_callable($callable)) { - return $callable; - } - - throw new Exception\InvalidMiddlewareException( - sprintf( - 'Unable to resolve middleware "%s" to a callable', - $middleware - ) - ); - } - - /** - * Marshal a middleware pipe from an array of middleware. - * - * Each item in the array can be one of the following: - * - * - A callable middleware - * - A string service name of middleware to retrieve from the container - * - A string class name of a constructor-less middleware class to - * instantiate - * - * As each middleware is verified, it is piped to the middleware pipe. - * - * @param array $middlewares - * @return MiddlewarePipe - * @throws Exception\InvalidMiddlewareException for any invalid middleware items. - */ - private function marshalMiddlewarePipe(array $middlewares) - { - $middlewarePipe = new MiddlewarePipe(); - - foreach ($middlewares as $middleware) { - $middlewarePipe->pipe( - $this->marshalMiddleware($middleware) - ); - } - - return $middlewarePipe; - } - - /** - * Attempt to retrieve the given middleware from the container. - * - * @param string $middleware - * @return string|callable Returns $middleware intact on failure, and the - * middleware instance on success. - * @throws Exception\InvalidArgumentException if a container exception occurs. - */ - private function marshalMiddlewareFromContainer($middleware) - { - $container = $this->container; - if (! $container || ! $container->has($middleware)) { - return $middleware; - } - - try { - return $container->get($middleware); - } catch (ContainerException $e) { - throw new Exception\InvalidMiddlewareException(sprintf( - 'Unable to retrieve middleware "%s" from the container', - $middleware - ), $e->getCode(), $e); - } - } - - /** - * Attempt to instantiate the given middleware. - * - * @param string $middleware - * @return string|callable Returns $middleware intact on failure, and the - * middleware instance on success. - */ - private function marshalInvokableMiddleware($middleware) - { - if (! class_exists($middleware)) { - return $middleware; - } - - return new $middleware(); - } - - /** - * @param string $middleware - * @param ContainerInterface $container - * @return callable - */ - private function marshalLazyMiddlewareService($middleware, ContainerInterface $container) - { - return function ($request, $response, $next = null) use ($container, $middleware) { - $invokable = $container->get($middleware); - if (! is_callable($invokable)) { - throw new Exception\InvalidMiddlewareException(sprintf( - 'Lazy-loaded middleware "%s" is not invokable', - $middleware - )); - } - return $invokable($request, $response, $next); - }; - } - - /** - * @param string $middleware - * @param ContainerInterface $container - * @return callable - */ - private function marshalLazyErrorMiddlewareService($middleware, ContainerInterface $container) - { - return function ($error, $request, $response, $next) use ($container, $middleware) { - $invokable = $container->get($middleware); - if (! is_callable($invokable)) { - throw new Exception\InvalidMiddlewareException(sprintf( - 'Lazy-loaded middleware "%s" is not invokable', - $middleware - )); - } - return $invokable($error, $request, $response, $next); - }; - } } diff --git a/src/Container/ApplicationFactory.php b/src/Container/ApplicationFactory.php index 04ad8b97..9f92f637 100644 --- a/src/Container/ApplicationFactory.php +++ b/src/Container/ApplicationFactory.php @@ -10,6 +10,7 @@ namespace Zend\Expressive\Container; use Interop\Container\ContainerInterface; +use SplPriorityQueue; use Zend\Diactoros\Response\EmitterInterface; use Zend\Expressive\Application; use Zend\Expressive\Exception; @@ -72,20 +73,17 @@ * * return [ * 'middleware_pipeline' => [ - * // An array of middleware to register prior to registration of the - * // routing middleware: - * 'pre_routing' => [ - * ], - * // An array of middleware to register after registration of the - * // routing middleware: - * 'post_routing' => [ - * ], + * // An array of middleware to register with the pipeline. + * // entries to register prior to routing/dispatching... + * Zend\Expressive\Container\ApplicationFactory::ROUTING_MIDDLEWARE, + * Zend\Expressive\Container\ApplicationFactory::DISPATCH_MIDDLEWARE, + * // entries to register after routing/dispatching... * ], * ]; * * - * Each item in either the `pre_routing` or `post_routing` array must be an - * array with the following specification: + * Each item in the middleware_pipeline array (with the exception of the routing + * and dispatch middleware entries) must be of the following specification: * * * [ @@ -94,6 +92,7 @@ * // optional: * 'path' => '/path/to/match', * 'error' => true, + * 'priority' => 1, // integer * ] * * @@ -103,16 +102,31 @@ * Omitting `error` or setting it to a non-true value is the default, * indicating the middleware is standard middleware. * - * Middleware are pipe()'d to the application instance in the order in which - * they appear. "pre_routing" middleware will execute before the application's - * routing middleware, while "post_routing" middleware will execute afterwards. + * `priority` is used to shape the order in which middleware is piped to the + * application. Values are integers, with high values having higher priority + * (piped earlier), and low/negative values having lower priority (piped last). + * Default priority if none is specified is 1. Middleware with the same + * priority are piped in the order in which they appear. * * Middleware piped may be either callables or service names. If you specify * the middleware's `error` flag as `true`, the middleware will be piped using - * Application::pipeErrorHandler() instead of Application::pipe(). + * `Application::pipeErrorHandler()` instead of `Application::pipe()`. + * + * Additionally, you can specify an array of callables or service names as + * the `middleware` value of a specification. Internally, this will create + * a `Zend\Stratigility\MiddlewarePipe` instance, with the middleware + * specified piped in the order provided. */ class ApplicationFactory { + const DISPATCH_MIDDLEWARE = 'EXPRESSIVE_DISPATCH_MIDDLEWARE'; + const ROUTING_MIDDLEWARE = 'EXPRESSIVE_ROUTING_MIDDLEWARE'; + + /** + * @deprecated This constant will be removed in v1.1. + */ + const ROUTE_RESULT_OBSERVER_MIDDLEWARE = 'EXPRESSIVE_ROUTE_RESULT_OBSERVER_MIDDLEWARE'; + /** * Create and return an Application instance. * @@ -138,29 +152,144 @@ public function __invoke(ContainerInterface $container) $app = new Application($router, $container, $finalHandler, $emitter); - $this->injectPreMiddleware($app, $container); - $this->injectRoutes($app, $container); - $this->injectPostMiddleware($app, $container); + $this->injectRoutesAndPipeline($app, $container); return $app; } /** - * Inject routes from configuration, if any. + * Injects routes and the middleware pipeline into the application. * * @param Application $app * @param ContainerInterface $container */ - private function injectRoutes(Application $app, ContainerInterface $container) + private function injectRoutesAndPipeline(Application $app, ContainerInterface $container) { $config = $container->has('config') ? $container->get('config') : []; + $pipelineCreated = false; + + if (isset($config['middleware_pipeline']) && is_array($config['middleware_pipeline'])) { + $pipelineCreated = $this->injectPipeline($config['middleware_pipeline'], $app); + } + + if (isset($config['routes']) && is_array($config['routes'])) { + $this->injectRoutes($config['routes'], $app); + + if (! $pipelineCreated) { + $app->pipeRoutingMiddleware(); + $app->pipeDispatchMiddleware(); + } + } + } + + /** + * Inject the middleware pipeline + * + * This method injects the middleware pipeline. + * + * If the pre-RC6 pre_/post_routing keys exist, it raises a deprecation + * notice, and then builds the pipeline based on that configuration + * (though it will raise an exception if other keys are *also* present). + * + * Otherwise, it passes the pipeline on to `injectMiddleware()`, + * returning a boolean value based on whether or not any + * middleware was injected. + * + * @deprecated This method will be removed in v1.1. + * @param array $pipeline + * @param Application $app + * @return bool + */ + private function injectPipeline(array $pipeline, Application $app) + { + $deprecatedKeys = $this->getDeprecatedKeys(array_keys($pipeline)); + if (! empty($deprecatedKeys)) { + $this->handleDeprecatedPipeline($deprecatedKeys, $pipeline, $app); + return true; + } + + return $this->injectMiddleware($pipeline, $app); + } + + /** + * Retrieve a list of deprecated keys from the pipeline, if any. + * + * @deprecated This method will be removed in v1.1. + * @param array $pipelineKeys + * @return array + */ + private function getDeprecatedKeys(array $pipelineKeys) + { + return array_intersect(['pre_routing', 'post_routing'], $pipelineKeys); + } - if (! isset($config['routes'])) { - $app->pipeRoutingMiddleware(); - return; + /** + * Handle deprecated pre_/post_routing configuration. + * + * @deprecated This method will be removed in v1.1. + * @param array $deprecatedKeys The list of deprecated keys present in the + * pipeline + * @param array $pipeline + * @param Application $app + * @return void + * @throws ContainerInvalidArgumentException if $pipeline contains more than + * just pre_ and/or post_routing keys. + * @throws ContainerInvalidArgumentException if the pre_routing configuration, + * if present, is not an array + * @throws ContainerInvalidArgumentException if the post_routing configuration, + * if present, is not an array + */ + private function handleDeprecatedPipeline(array $deprecatedKeys, array $pipeline, Application $app) + { + if (count($deprecatedKeys) < count($pipeline)) { + throw new ContainerInvalidArgumentException( + 'middleware_pipeline cannot contain a mix of middleware AND pre_/post_routing keys; ' + . 'please update your configuration to define middleware_pipeline as a single pipeline; ' + . 'see http://zend-expressive.rtfd.org/en/latest/migration/rc-to-v1/' + ); } - foreach ($config['routes'] as $spec) { + trigger_error( + 'pre_routing and post_routing configuration is deprecated; ' + . 'update your configuration to define the middleware_pipeline as a single pipeline; ' + . 'see http://zend-expressive.rtfd.org/en/latest/migration/rc-to-v1/', + E_USER_DEPRECATED + ); + + if (isset($pipeline['pre_routing'])) { + if (! is_array($pipeline['pre_routing'])) { + throw new ContainerInvalidArgumentException(sprintf( + 'Pre-routing middleware collection must be an array; received "%s"', + gettype($pipeline['pre_routing']) + )); + } + $this->injectMiddleware($pipeline['pre_routing'], $app); + } + + $app->pipeRoutingMiddleware(); + $app->pipeRouteResultObserverMiddleware(); + $app->pipeDispatchMiddleware(); + + if (isset($pipeline['post_routing'])) { + if (! is_array($pipeline['post_routing'])) { + throw new ContainerInvalidArgumentException(sprintf( + 'Post-routing middleware collection must be an array; received "%s"', + gettype($pipeline['post_routing']) + )); + } + $this->injectMiddleware($pipeline['post_routing'], $app); + } + } + + /** + * Inject routes from configuration, if any. + * + * @param array $routes Route definitions + * @param Application $app + */ + private function injectRoutes(array $routes, Application $app) + { + foreach ($routes as $spec) { if (! isset($spec['path']) || ! isset($spec['middleware'])) { continue; } @@ -200,93 +329,148 @@ private function injectRoutes(Application $app, ContainerInterface $container) * * @param array $collection * @param Application $app - * @param ContainerInterface $container + * @return bool Flag indicating whether or not any middleware was injected. * @throws Exception\InvalidMiddlewareException for invalid middleware. */ - private function injectMiddleware(array $collection, Application $app, ContainerInterface $container) + private function injectMiddleware(array $collection, Application $app) { - foreach ($collection as $spec) { - if (! array_key_exists('middleware', $spec)) { - continue; - } + // Create a priority queue from the specifications + $queue = array_reduce( + array_map($this->createCollectionMapper($app), $collection), + $this->createPriorityQueueReducer(), + new SplPriorityQueue() + ); + + $injections = count($queue) > 0; + foreach ($queue as $spec) { $path = isset($spec['path']) ? $spec['path'] : '/'; $error = array_key_exists('error', $spec) ? (bool) $spec['error'] : false; $pipe = $error ? 'pipeErrorHandler' : 'pipe'; - if (is_array($spec['middleware'])) { - foreach ($spec['middleware'] as $middleware) { - $app->{$pipe}($path, $middleware); - } - } else { - $app->{$pipe}($path, $spec['middleware']); - } + $app->{$pipe}($path, $spec['middleware']); } + + return $injections; } /** - * Inject middleware to pipe before the routing middleware. + * Create and return the pipeline map callback. * - * Pre-routing middleware is specified as the configuration subkey - * middleware_pipeline.pre_routing. + * The returned callback has the signature: * + * + * function ($item) : callable|string + * + * + * It is suitable for mapping pipeline middleware representing the application + * routing o dispatching middleware to a callable; if the provided item does not + * match either, the item is returned verbatim. + * + * @todo Remove ROUTE_RESULT_OBSERVER_MIDDLEWARE detection for 1.1 * @param Application $app - * @param ContainerInterface $container + * @return callable */ - private function injectPreMiddleware(Application $app, ContainerInterface $container) + private function createPipelineMapper(Application $app) { - if (!$container->has('config')) { - return; - } - - $config = $container->get('config'); - - if (! isset($config['middleware_pipeline']['pre_routing'])) { - return; - } + return function ($item) use ($app) { + if ($item === self::ROUTING_MIDDLEWARE) { + return [$app, 'routeMiddleware']; + } - $middlewareCollection = $config['middleware_pipeline']['pre_routing']; + if ($item === self::DISPATCH_MIDDLEWARE) { + return [$app, 'dispatchMiddleware']; + } - if (! is_array($middlewareCollection)) { - throw new ContainerInvalidArgumentException(sprintf( - 'Pre-routing middleware collection must be an array; received "%s"', - gettype($middlewareCollection) - )); - } + if ($item === self::ROUTE_RESULT_OBSERVER_MIDDLEWARE) { + $r = new \ReflectionProperty($app, 'routeResultObserverMiddlewareIsRegistered'); + $r->setAccessible(true); + $r->setValue($app, true); + return [$app, 'routeResultObserverMiddleware']; + } - $this->injectMiddleware($middlewareCollection, $app, $container); + return $item; + }; } /** - * Inject middleware to pipe after the routing middleware. + * Create the collection mapping function. + * + * Returns a callable with the following signature: + * + * + * function (array|string $item) : array + * + * + * When it encounters one of the self::*_MIDDLEWARE constants, it passes + * the value to the `createPipelineMapper()` callback to create a spec + * that uses the return value as pipeline middleware. * - * Post-routing middleware is specified as the configuration subkey - * middleware_pipeline.post_routing. + * If the 'middleware' value is an array, it uses the `createPipelineMapper()` + * callback as an array mapper in order to ensure the self::*_MIDDLEWARE + * are injected correctly. + * + * If the 'middleware' value is missing, or not viable as middleware, it + * raises an exception, to ensure the pipeline is built correctly. * * @param Application $app - * @param ContainerInterface $container + * @return callable */ - private function injectPostMiddleware(Application $app, ContainerInterface $container) + private function createCollectionMapper(Application $app) { - if (!$container->has('config')) { - return; - } + $pipelineMap = $this->createPipelineMapper($app); + $appMiddlewares = [ + self::ROUTING_MIDDLEWARE, + self::DISPATCH_MIDDLEWARE, + self::ROUTE_RESULT_OBSERVER_MIDDLEWARE + ]; - $config = $container->get('config'); + return function ($item) use ($app, $pipelineMap, $appMiddlewares) { + if (in_array($item, $appMiddlewares, true)) { + return ['middleware' => $pipelineMap($item)]; + } - if (! isset($config['middleware_pipeline']['post_routing'])) { - return; - } + if (! is_array($item) || ! array_key_exists('middleware', $item)) { + throw new ContainerInvalidArgumentException(sprintf( + 'Invalid pipeline specification received; must be an array containing a middleware ' + . 'key, or one of the ApplicationFactory::*_MIDDLEWARE constants; received %s', + (is_object($item) ? get_class($item) : gettype($item)) + )); + } - $middlewareCollection = $config['middleware_pipeline']['post_routing']; + if (! is_callable($item['middleware']) && is_array($item['middleware'])) { + $item['middleware'] = array_map($pipelineMap, $item['middleware']); + } - if (! is_array($middlewareCollection)) { - throw new ContainerInvalidArgumentException(sprintf( - 'Post-routing middleware collection must be an array; received "%s"', - gettype($middlewareCollection) - )); - } + return $item; + }; + } - $this->injectMiddleware($middlewareCollection, $app, $container); + /** + * Create reducer function that will reduce an array to a priority queue. + * + * Creates and returns a function with the signature: + * + * + * function (SplQueue $queue, array $item) : SplQueue + * + * + * The function is useful to reduce an array of pipeline middleware to a + * priority queue. + * + * @return callable + */ + private function createPriorityQueueReducer() + { + // $serial is used to ensure that items of the same priority are enqueued + // in the order in which they are inserted. + $serial = PHP_INT_MAX; + return function ($queue, $item) use (&$serial) { + $priority = isset($item['priority']) && is_int($item['priority']) + ? $item['priority'] + : 1; + $queue->insert($item, [$priority, $serial--]); + return $queue; + }; } } diff --git a/src/MarshalMiddlewareTrait.php b/src/MarshalMiddlewareTrait.php new file mode 100644 index 00000000..64323cca --- /dev/null +++ b/src/MarshalMiddlewareTrait.php @@ -0,0 +1,159 @@ +marshalMiddlewarePipe($middleware, $container, $forError); + } + + if (is_string($middleware) && $container && $container->has($middleware)) { + $method = $forError ? 'marshalLazyErrorMiddlewareService' : 'marshalLazyMiddlewareService'; + return $this->{$method}($middleware, $container); + } + + $callable = $middleware; + if (is_string($middleware)) { + $callable = $this->marshalInvokableMiddleware($middleware); + } + + if (! is_callable($callable)) { + throw new Exception\InvalidMiddlewareException( + sprintf( + 'Unable to resolve middleware "%s" to a callable', + (is_object($middleware) + ? get_class($middleware) . "[Object]" + : gettype($middleware) . '[Scalar]') + ) + ); + } + + return $callable; + } + + /** + * Marshal a middleware pipe from an array of middleware. + * + * Each item in the array can be one of the following: + * + * - A callable middleware + * - A string service name of middleware to retrieve from the container + * - A string class name of a constructor-less middleware class to + * instantiate + * + * As each middleware is verified, it is piped to the middleware pipe. + * + * @param array $middlewares + * @param null|ContainerInterface $container + * @param bool $forError Whether or not the middleware pipe generated is + * intended to be populated with error middleware; defaults to false. + * @return MiddlewarePipe + * @throws Exception\InvalidMiddlewareException for any invalid middleware items. + */ + private function marshalMiddlewarePipe(array $middlewares, ContainerInterface $container = null, $forError = false) + { + $middlewarePipe = new MiddlewarePipe(); + + foreach ($middlewares as $middleware) { + $middlewarePipe->pipe( + $this->prepareMiddleware($middleware, $container, $forError) + ); + } + + return $middlewarePipe; + } + + /** + * Attempt to instantiate the given middleware. + * + * @param string $middleware + * @return string|callable Returns $middleware intact on failure, and the + * middleware instance on success. + */ + private function marshalInvokableMiddleware($middleware) + { + if (! class_exists($middleware)) { + return $middleware; + } + + return new $middleware(); + } + + /** + * @param string $middleware + * @param ContainerInterface $container + * @return callable + */ + private function marshalLazyMiddlewareService($middleware, ContainerInterface $container) + { + return function ($request, $response, $next = null) use ($container, $middleware) { + $invokable = $container->get($middleware); + if (! is_callable($invokable)) { + throw new Exception\InvalidMiddlewareException(sprintf( + 'Lazy-loaded middleware "%s" is not invokable', + $middleware + )); + } + return $invokable($request, $response, $next); + }; + } + + /** + * @param string $middleware + * @param ContainerInterface $container + * @return callable + */ + private function marshalLazyErrorMiddlewareService($middleware, ContainerInterface $container) + { + return function ($error, $request, $response, $next) use ($container, $middleware) { + $invokable = $container->get($middleware); + if (! is_callable($invokable)) { + throw new Exception\InvalidMiddlewareException(sprintf( + 'Lazy-loaded middleware "%s" is not invokable', + $middleware + )); + } + return $invokable($error, $request, $response, $next); + }; + } +} diff --git a/test/ApplicationTest.php b/test/ApplicationTest.php index 7cb31f17..1c073a4d 100644 --- a/test/ApplicationTest.php +++ b/test/ApplicationTest.php @@ -213,7 +213,7 @@ public function testCreatingHttpRouteWithExistingPathAndMethodRaisesException() }); } - public function testRouteMiddlewareIsNotPipedAtInstantation() + public function testRouteAndDispatchMiddlewareAreNotPipedAtInstantation() { $app = $this->getApp(); @@ -224,27 +224,7 @@ public function testRouteMiddlewareIsNotPipedAtInstantation() $this->assertCount(0, $pipeline); } - public function testRouteMiddlewareIsPipedAtFirstCallToRoute() - { - $this->router->addRoute(Argument::type(Route::class))->shouldBeCalled(); - - $app = $this->getApp(); - $app->route('/foo', 'bar'); - - $r = new ReflectionProperty($app, 'pipeline'); - $r->setAccessible(true); - $pipeline = $r->getValue($app); - - $this->assertCount(1, $pipeline); - $route = $pipeline->dequeue(); - $this->assertInstanceOf(StratigilityRoute::class, $route); - $test = $route->handler; - - $routeMiddleware = [$app, 'routeMiddleware']; - $this->assertSame($routeMiddleware, $test); - } - - public function testRouteMiddlewareCanRouteArrayOfMiddlewareAsMiddlewarePipe() + public function testDispatchMiddlewareCanDispatchArrayOfMiddlewareAsMiddlewarePipe() { $middleware = [ function () { @@ -256,14 +236,14 @@ function () { $request = new ServerRequest([], [], '/', 'GET'); $routeResult = RouteResult::fromRouteMatch(__METHOD__, $middleware, []); - $this->router->match($request)->willReturn($routeResult); + $request = $request->withAttribute(RouteResult::class, $routeResult); $container = $this->mockContainerInterface(); $this->injectServiceInContainer($container, 'FooBar', function () { }); $app = new Application($this->router->reveal(), $container->reveal()); - $app->routeMiddleware($request, new Response(), function () { + $app->dispatchMiddleware($request, new Response(), function () { }); } @@ -279,13 +259,13 @@ public function uncallableMiddleware() * @dataProvider uncallableMiddleware * @expectedException \Zend\Expressive\Exception\InvalidMiddlewareException */ - public function testThrowsExceptionWhenRoutingUncallableMiddleware($middleware) + public function testThrowsExceptionWhenDispatchingUncallableMiddleware($middleware) { $request = new ServerRequest([], [], '/', 'GET'); $routeResult = RouteResult::fromRouteMatch(__METHOD__, $middleware, []); - $this->router->match($request)->willReturn($routeResult); + $request = $request->withAttribute(RouteResult::class, $routeResult); - $this->getApp()->routeMiddleware($request, new Response(), function () { + $this->getApp()->dispatchMiddleware($request, new Response(), function () { }); } @@ -309,6 +289,26 @@ public function testCannotPipeRouteMiddlewareMoreThanOnce() $this->assertSame($routeMiddleware, $test); } + public function testCannotPipeDispatchMiddlewareMoreThanOnce() + { + $app = $this->getApp(); + $dispatchMiddleware = [$app, 'dispatchMiddleware']; + + $app->pipe($dispatchMiddleware); + $app->pipe($dispatchMiddleware); + + $r = new ReflectionProperty($app, 'pipeline'); + $r->setAccessible(true); + $pipeline = $r->getValue($app); + + $this->assertCount(1, $pipeline); + $route = $pipeline->dequeue(); + $this->assertInstanceOf(StratigilityRoute::class, $route); + $test = $route->handler; + + $this->assertSame($dispatchMiddleware, $test); + } + public function testCanInjectFinalHandlerViaConstructor() { $finalHandler = function ($req, $res, $err = null) { @@ -432,48 +432,21 @@ public function testCanTriggerPipingOfRouteMiddleware() $this->assertEquals('/', $route->path); } - /** - * @group 64 - */ - public function testInvocationWillPipeRoutingMiddlewareIfNotAlreadyPiped() + public function testCanTriggerPipingOfDispatchMiddleware() { - $request = new Request([], [], 'http://example.com/'); - $response = $this->prophesize(ResponseInterface::class); - - $middleware = function ($req, $res, $next = null) { - return $res; - }; - - $this->router->match($request)->willReturn(RouteResult::fromRouteMatch('foo', 'foo', [])); - - $container = $this->mockContainerInterface(); - $this->injectServiceInContainer($container, 'foo', $middleware); - - $app = new Application($this->router->reveal(), $container->reveal()); - - $pipeline = $this->prophesize(SplQueue::class); - - // Test that the route middleware is enqueued - $pipeline->enqueue(Argument::that(function ($route) use ($app) { - if (! $route instanceof StratigilityRoute) { - return false; - } - - if ($route->path !== '/') { - return false; - } - - return ($route->handler === [$app, 'routeMiddleware']); - }))->shouldBeCalled(); - - // Prevent dequeueing - $pipeline->isEmpty()->willReturn(true); + $app = $this->getApp(); + $app->pipeDispatchMiddleware(); $r = new ReflectionProperty($app, 'pipeline'); $r->setAccessible(true); - $r->setValue($app, $pipeline->reveal()); + $pipeline = $r->getValue($app); + + $this->assertCount(1, $pipeline); - $app($request, $response->reveal(), $middleware); + $route = $pipeline->dequeue(); + $this->assertInstanceOf(StratigilityRoute::class, $route); + $this->assertSame([$app, 'dispatchMiddleware'], $route->handler); + $this->assertEquals('/', $route->path); } /** diff --git a/test/Container/ApplicationFactoryTest.php b/test/Container/ApplicationFactoryTest.php index 194c970a..6ed7bf25 100644 --- a/test/Container/ApplicationFactoryTest.php +++ b/test/Container/ApplicationFactoryTest.php @@ -15,6 +15,7 @@ use Prophecy\Prophecy\ObjectProphecy; use ReflectionFunction; use ReflectionProperty; +use SplQueue; use Zend\Diactoros\Response\EmitterInterface; use Zend\Diactoros\Response\SapiEmitter; use Zend\Expressive\Application; @@ -25,6 +26,7 @@ use Zend\Expressive\Router\FastRouteRouter; use Zend\Expressive\Router\Route; use Zend\Expressive\Router\RouterInterface; +use Zend\Stratigility\MiddlewarePipe; use Zend\Stratigility\Route as StratigilityRoute; use ZendTest\Expressive\ContainerTrait; use ZendTest\Expressive\TestAsset\InvokableMiddleware; @@ -198,6 +200,7 @@ public function testWillUseSaneDefaultsForOptionalServices() /** * @group piping + * @deprecated This test can be removed for 1.1 */ public function testCanPipeMiddlewareProvidedDuringConfigurationPriorToSettingRoutes() { @@ -223,13 +226,20 @@ public function testCanPipeMiddlewareProvidedDuringConfigurationPriorToSettingRo $this->injectServiceInContainer($this->container, 'config', $config); + // @codingStandardsIgnoreStart + set_error_handler(function ($errno, $errmsg) { + $this->assertContains('routing', $errmsg); + }, E_USER_DEPRECATED); + // @codingStandardsIgnoreEnd + $app = $this->factory->__invoke($this->container->reveal()); + restore_error_handler(); $r = new ReflectionProperty($app, 'pipeline'); $r->setAccessible(true); $pipeline = $r->getValue($app); - $this->assertCount(3, $pipeline); + $this->assertCount(5, $pipeline); $route = $pipeline->dequeue(); $this->assertInstanceOf(StratigilityRoute::class, $route); @@ -245,10 +255,21 @@ public function testCanPipeMiddlewareProvidedDuringConfigurationPriorToSettingRo $this->assertInstanceOf(StratigilityRoute::class, $route); $this->assertSame([$app, 'routeMiddleware'], $route->handler); $this->assertEquals('/', $route->path); + + $route = $pipeline->dequeue(); + $this->assertInstanceOf(StratigilityRoute::class, $route); + $this->assertSame([$app, 'routeResultObserverMiddleware'], $route->handler); + $this->assertEquals('/', $route->path); + + $route = $pipeline->dequeue(); + $this->assertInstanceOf(StratigilityRoute::class, $route); + $this->assertSame([$app, 'dispatchMiddleware'], $route->handler); + $this->assertEquals('/', $route->path); } /** * @group piping + * @deprecated This test can be removed for 1.1 */ public function testCanPipeMiddlewareProvidedDuringConfigurationAfterSettingRoutes() { @@ -275,19 +296,36 @@ public function testCanPipeMiddlewareProvidedDuringConfigurationAfterSettingRout $this->injectServiceInContainer($this->container, 'config', $config); + // @codingStandardsIgnoreStart + set_error_handler(function ($errno, $errmsg) { + $this->assertContains('routing', $errmsg); + }, E_USER_DEPRECATED); + // @codingStandardsIgnoreEnd + $app = $this->factory->__invoke($this->container->reveal()); + restore_error_handler(); $r = new ReflectionProperty($app, 'pipeline'); $r->setAccessible(true); $pipeline = $r->getValue($app); - $this->assertCount(3, $pipeline); + $this->assertCount(5, $pipeline); $route = $pipeline->dequeue(); $this->assertInstanceOf(StratigilityRoute::class, $route); $this->assertSame([$app, 'routeMiddleware'], $route->handler); $this->assertEquals('/', $route->path); + $route = $pipeline->dequeue(); + $this->assertInstanceOf(StratigilityRoute::class, $route); + $this->assertSame([$app, 'routeResultObserverMiddleware'], $route->handler); + $this->assertEquals('/', $route->path); + + $route = $pipeline->dequeue(); + $this->assertInstanceOf(StratigilityRoute::class, $route); + $this->assertSame([$app, 'dispatchMiddleware'], $route->handler); + $this->assertEquals('/', $route->path); + $route = $pipeline->dequeue(); $this->assertInstanceOf(StratigilityRoute::class, $route); $this->assertInstanceOf(Closure::class, $route->handler); @@ -312,10 +350,8 @@ public function testPipedMiddlewareAsServiceNamesAreReturnedAsClosuresThatPullFr $config = [ 'middleware_pipeline' => [ - 'post_routing' => [ - [ 'middleware' => 'Middleware' ], - [ 'path' => '/foo', 'middleware' => 'Middleware' ], - ], + [ 'middleware' => 'Middleware' ], + [ 'path' => '/foo', 'middleware' => 'Middleware' ], ], ]; @@ -328,12 +364,7 @@ public function testPipedMiddlewareAsServiceNamesAreReturnedAsClosuresThatPullFr $r->setAccessible(true); $pipeline = $r->getValue($app); - $this->assertCount(3, $pipeline); - - $route = $pipeline->dequeue(); - $this->assertInstanceOf(StratigilityRoute::class, $route); - $this->assertSame([$app, 'routeMiddleware'], $route->handler); - $this->assertEquals('/', $route->path); + $this->assertCount(2, $pipeline); $route = $pipeline->dequeue(); $this->assertInstanceOf(StratigilityRoute::class, $route); @@ -355,27 +386,15 @@ public function testMiddlewareIsNotAddedIfSpecIsInvalid() { $config = [ 'middleware_pipeline' => [ - 'post_routing' => [ - [ 'foo' => 'bar' ], - [ 'path' => '/foo' ], - ], + [ 'foo' => 'bar' ], + [ 'path' => '/foo' ], ], ]; $this->injectServiceInContainer($this->container, 'config', $config); + $this->setExpectedException(ContainerException\InvalidArgumentException::class, 'pipeline'); $app = $this->factory->__invoke($this->container->reveal()); - - $r = new ReflectionProperty($app, 'pipeline'); - $r->setAccessible(true); - $pipeline = $r->getValue($app); - - // only routeMiddleware should be added by default - $this->assertCount(1, $pipeline); - $route = $pipeline->dequeue(); - $this->assertInstanceOf(StratigilityRoute::class, $route); - $this->assertSame([$app, 'routeMiddleware'], $route->handler); - $this->assertEquals('/', $route->path); } public function uncallableMiddleware() @@ -394,7 +413,6 @@ public function uncallableMiddleware() } /** - * @group fail * @group piping * @dataProvider uncallableMiddleware */ @@ -402,21 +420,28 @@ public function testRaisesExceptionForNonCallableNonServiceMiddleware($middlewar { $config = [ 'middleware_pipeline' => [ - 'post_routing' => [ - [ 'middleware' => $middleware ], - [ 'path' => '/foo', 'middleware' => $middleware ], - ], + [ 'middleware' => $middleware ], + [ 'path' => '/foo', 'middleware' => $middleware ], ], ]; $this->injectServiceInContainer($this->container, 'config', $config); - $this->setExpectedException(InvalidArgumentException::class); - $app = $this->factory->__invoke($this->container->reveal()); + try { + $this->factory->__invoke($this->container->reveal()); + $this->fail('No exception raised when fetching non-callable non-service middleware'); + } catch (InvalidMiddlewareException $e) { + // This is acceptable + $this->assertInstanceOf(InvalidMiddlewareException::class, $e); + } catch (InvalidArgumentException $e) { + // This is acceptable + $this->assertInstanceOf(InvalidArgumentException::class, $e); + } } /** * @group piping + * @deprecated This test can be removed for 1.1 */ public function testCanPipePreRoutingMiddlewareAsArray() { @@ -438,11 +463,19 @@ function () { $this->injectServiceInContainer($this->container, 'Hello', function () { }); + // @codingStandardsIgnoreStart + set_error_handler(function ($errno, $errmsg) { + $this->assertContains('routing', $errmsg); + }, E_USER_DEPRECATED); + // @codingStandardsIgnoreEnd + $this->factory->__invoke($this->container->reveal()); + restore_error_handler(); } /** * @group piping + * @deprecated This test can be removed for 1.1 */ public function testCanPipePostRoutingMiddlewareAsArray() { @@ -464,7 +497,14 @@ function () { $this->injectServiceInContainer($this->container, 'Hello', function () { }); + // @codingStandardsIgnoreStart + set_error_handler(function ($errno, $errmsg) { + $this->assertContains('routing', $errmsg); + }, E_USER_DEPRECATED); + // @codingStandardsIgnoreEnd + $this->factory->__invoke($this->container->reveal()); + restore_error_handler(); } /** @@ -474,16 +514,14 @@ public function testRaisesExceptionForPipedMiddlewareServiceNamesNotFoundInConta { $config = [ 'middleware_pipeline' => [ - 'post_routing' => [ - [ 'middleware' => 'Middleware' ], - [ 'path' => '/foo', 'middleware' => 'Middleware' ], - ], + [ 'middleware' => 'Middleware' ], + [ 'path' => '/foo', 'middleware' => 'Middleware' ], ], ]; $this->injectServiceInContainer($this->container, 'config', $config); - $this->setExpectedException(InvalidArgumentException::class); + $this->setExpectedException(InvalidMiddlewareException::class); $app = $this->factory->__invoke($this->container->reveal()); } @@ -496,10 +534,8 @@ public function testRaisesExceptionOnInvocationOfUninvokableServiceSpecifiedMidd $config = [ 'middleware_pipeline' => [ - 'post_routing' => [ - [ 'middleware' => 'Middleware' ], - [ 'path' => '/foo', 'middleware' => 'Middleware' ], - ], + [ 'middleware' => 'Middleware' ], + [ 'path' => '/foo', 'middleware' => 'Middleware' ], ], ]; @@ -512,12 +548,7 @@ public function testRaisesExceptionOnInvocationOfUninvokableServiceSpecifiedMidd $r->setAccessible(true); $pipeline = $r->getValue($app); - $this->assertCount(3, $pipeline); - - $routing = $pipeline->dequeue(); - $this->assertInstanceOf(StratigilityRoute::class, $routing); - $this->assertSame([$app, 'routeMiddleware'], $routing->handler); - $this->assertEquals('/', $routing->path); + $this->assertCount(2, $pipeline); $first = $pipeline->dequeue(); $this->assertInstanceOf(StratigilityRoute::class, $first); @@ -575,9 +606,7 @@ public function testCanMarkPipedMiddlewareServiceAsErrorMiddleware() $config = [ 'middleware_pipeline' => [ - 'post_routing' => [ - [ 'middleware' => 'Middleware', 'error' => true ], - ], + [ 'middleware' => 'Middleware', 'error' => true ], ], ]; @@ -590,12 +619,7 @@ public function testCanMarkPipedMiddlewareServiceAsErrorMiddleware() $r->setAccessible(true); $pipeline = $r->getValue($app); - $this->assertCount(2, $pipeline); - - $route = $pipeline->dequeue(); - $this->assertInstanceOf(StratigilityRoute::class, $route); - $this->assertEquals('/', $route->path); - $this->assertSame([$app, 'routeMiddleware'], $route->handler); + $this->assertCount(1, $pipeline); $route = $pipeline->dequeue(); $this->assertInstanceOf(StratigilityRoute::class, $route); @@ -609,6 +633,7 @@ public function testCanMarkPipedMiddlewareServiceAsErrorMiddleware() /** * @group 64 + * @deprecated This test can be removed for 1.1 */ public function testWillPipeRoutingMiddlewareEvenIfNoRoutesAreRegistered() { @@ -627,13 +652,20 @@ public function testWillPipeRoutingMiddlewareEvenIfNoRoutesAreRegistered() $this->injectServiceInContainer($this->container, 'config', $config); + // @codingStandardsIgnoreStart + set_error_handler(function ($errno, $errmsg) { + $this->assertContains('routing', $errmsg); + }, E_USER_DEPRECATED); + // @codingStandardsIgnoreEnd + $app = $this->factory->__invoke($this->container->reveal()); + restore_error_handler(); $r = new ReflectionProperty($app, 'pipeline'); $r->setAccessible(true); $pipeline = $r->getValue($app); - $this->assertCount(3, $pipeline); + $this->assertCount(5, $pipeline); $route = $pipeline->dequeue(); $this->assertInstanceOf(StratigilityRoute::class, $route); @@ -649,6 +681,16 @@ public function testWillPipeRoutingMiddlewareEvenIfNoRoutesAreRegistered() $this->assertInstanceOf(StratigilityRoute::class, $route); $this->assertSame([$app, 'routeMiddleware'], $route->handler); $this->assertEquals('/', $route->path); + + $route = $pipeline->dequeue(); + $this->assertInstanceOf(StratigilityRoute::class, $route); + $this->assertSame([$app, 'routeResultObserverMiddleware'], $route->handler); + $this->assertEquals('/', $route->path); + + $route = $pipeline->dequeue(); + $this->assertInstanceOf(StratigilityRoute::class, $route); + $this->assertSame([$app, 'dispatchMiddleware'], $route->handler); + $this->assertEquals('/', $route->path); } public function testCanSpecifyRouteNamesViaConfiguration() @@ -753,6 +795,9 @@ public function testExceptionIsRaisedInCaseOfInvalidRouteOptionsConfiguration() $this->factory->__invoke($this->container->reveal()); } + /** + * @deprecated This test can be removed for 1.1 + */ public function testExceptionIsRaisedInCaseOfInvalidPreRoutingMiddlewarePipeline() { $config = [ @@ -767,9 +812,18 @@ public function testExceptionIsRaisedInCaseOfInvalidPreRoutingMiddlewarePipeline ContainerException\InvalidArgumentException::class, 'Pre-routing middleware collection must be an array; received "string"' ); + // @codingStandardsIgnoreStart + set_error_handler(function ($errno, $errmsg) { + $this->assertContains('routing', $errmsg); + }, E_USER_DEPRECATED); + // @codingStandardsIgnoreEnd + $this->factory->__invoke($this->container->reveal()); } + /** + * @deprecated This test can be removed for 1.1 + */ public function testExceptionIsRaisedInCaseOfInvalidPostRoutingMiddlewarePipeline() { $config = [ @@ -784,6 +838,419 @@ public function testExceptionIsRaisedInCaseOfInvalidPostRoutingMiddlewarePipelin ContainerException\InvalidArgumentException::class, 'Post-routing middleware collection must be an array; received "string"' ); + // @codingStandardsIgnoreStart + set_error_handler(function ($errno, $errmsg) { + $this->assertContains('routing', $errmsg); + }, E_USER_DEPRECATED); + // @codingStandardsIgnoreEnd + + $this->factory->__invoke($this->container->reveal()); + } + + public function testWillCreatePipelineBasedOnMiddlewareConfiguration() + { + // @codingStandardsIgnoreStart + $api = function ($request, $response, $next) {}; + // @codingStandardsIgnoreEnd + + $dynamicPath = clone $api; + $noPath = clone $api; + $goodbye = clone $api; + $pipelineFirst = clone $api; + $hello = clone $api; + $pipelineLast = clone $api; + + $this->injectServiceInContainer($this->container, 'DynamicPath', $dynamicPath); + $this->injectServiceInContainer($this->container, 'Goodbye', $goodbye); + $this->injectServiceInContainer($this->container, 'Hello', $hello); + + $pipeline = [ + [ 'path' => '/api', 'middleware' => $api ], + [ 'path' => '/dynamic-path', 'middleware' => 'DynamicPath' ], + ['middleware' => $noPath], + ['middleware' => 'Goodbye'], + ['middleware' => [ + $pipelineFirst, + 'Hello', + $pipelineLast, + ]], + ]; + + $config = ['middleware_pipeline' => $pipeline]; + $this->injectServiceInContainer($this->container, 'config', $config); + + $app = $this->factory->__invoke($this->container->reveal()); + + $this->assertAttributeSame( + false, + 'routeMiddlewareIsRegistered', + $app, + 'Route middleware was registered when it should not have been' + ); + + $this->assertAttributeSame( + false, + 'dispatchMiddlewareIsRegistered', + $app, + 'Dispatch middleware was registered when it should not have been' + ); + + $r = new ReflectionProperty($app, 'pipeline'); + $r->setAccessible(true); + $pipeline = $r->getValue($app); + + $this->assertCount(5, $pipeline, 'Did not get expected pipeline count!'); + + $test = $pipeline->dequeue(); + $this->assertEquals('/api', $test->path); + $this->assertSame($api, $test->handler); + + // Lazy middleware is not marshaled until invocation + $test = $pipeline->dequeue(); + $this->assertEquals('/dynamic-path', $test->path); + $this->assertNotSame($dynamicPath, $test->handler); + $this->assertInstanceOf(Closure::class, $test->handler); + + $test = $pipeline->dequeue(); + $this->assertEquals('/', $test->path); + $this->assertSame($noPath, $test->handler); + + // Lazy middleware is not marshaled until invocation + $test = $pipeline->dequeue(); + $this->assertEquals('/', $test->path); + $this->assertNotSame($goodbye, $test->handler); + $this->assertInstanceOf(Closure::class, $test->handler); + + $test = $pipeline->dequeue(); + $nestedPipeline = $test->handler; + $this->assertInstanceOf(MiddlewarePipe::class, $nestedPipeline); + + $r = new ReflectionProperty($nestedPipeline, 'pipeline'); + $r->setAccessible(true); + $nestedPipeline = $r->getValue($nestedPipeline); + + $test = $nestedPipeline->dequeue(); + $this->assertSame($pipelineFirst, $test->handler); + + // Lazy middleware is not marshaled until invocation + $test = $nestedPipeline->dequeue(); + $this->assertNotSame($hello, $test->handler); + $this->assertInstanceOf(Closure::class, $test->handler); + + $test = $nestedPipeline->dequeue(); + $this->assertSame($pipelineLast, $test->handler); + } + + public function mixedMiddlewarePipelines() + { + // @codingStandardsIgnoreStart + $middleware = function ($request, $response, $next) {}; + $pre = ['middleware' => clone $middleware]; + $post = ['middleware' => clone $middleware]; + $pipelined = ['middleware' => clone $middleware]; + return [ + 'pre_routing' => [['middleware_pipeline' => ['pre_routing' => [$pre], $pipelined]]], + 'post_routing' => [['middleware_pipeline' => ['post_routing' => [$post], $pipelined]]], + 'pre_and_post' => [['middleware_pipeline' => ['pre_routing' => [$pre], 'post_routing' => [$post], $pipelined]]], + ]; + // @codingStandardsIgnoreEnd + } + + /** + * @dataProvider mixedMiddlewarePipelines + */ + public function testRaisesExceptionIfMiddlewarePipelineConfigurationMixesMiddlewareAndPreOrPostRouting($config) + { + $this->injectServiceInContainer($this->container, 'config', $config); + + $this->setExpectedException(InvalidArgumentException::class, 'mix of middleware'); + $this->factory->__invoke($this->container->reveal()); + } + + public function middlewarePipelinesWithPreOrPostRouting() + { + // @codingStandardsIgnoreStart + $middleware = function ($request, $response, $next) {}; + $config = ['middleware' => clone $middleware]; + return [ + 'pre_routing' => [['pre_routing' => [$config]]], + 'post_routing' => [['post_routing' => [$config]]], + ]; + // @codingStandardsIgnoreEnd + } + + /** + * @dataProvider middlewarePipelinesWithPreOrPostRouting + * @deprecated This test can be removed for 1.1 + */ + public function testRaisesDeprecationNoticeForUsageOfPreOrPostRoutingPipelineConfiguration($config) + { + $config = ['middleware_pipeline' => $config]; + $this->injectServiceInContainer($this->container, 'config', $config); + + // @codingStandardsIgnoreStart + $triggered = false; + set_error_handler(function ($errno, $errmsg) use (&$triggered) { + $this->assertContains('routing', $errmsg); + $triggered = true; + }, E_USER_DEPRECATED); + // @codingStandardsIgnoreEnd + $this->factory->__invoke($this->container->reveal()); + restore_error_handler(); + $this->assertTrue($triggered, 'Deprecation notice was not triggered!'); + } + + public function configWithRoutesButNoPipeline() + { + // @codingStandardsIgnoreStart + $middleware = function ($request, $response, $next) {}; + // @codingStandardsIgnoreEnd + + $routes = [ + [ + 'path' => '/', + 'middleware' => clone $middleware, + 'allowed_methods' => [ 'GET' ], + ], + ]; + + return [ + 'no-pipeline-defined' => [['routes' => $routes]], + 'empty-pipeline' => [['middleware_pipeline' => [], 'routes' => $routes]], + 'null-pipeline' => [['middleware_pipeline' => null, 'routes' => $routes]], + ]; + } + + /** + * @dataProvider configWithRoutesButNoPipeline + */ + public function testProvidingRoutesAndNoPipelineImplicitlyRegistersRoutingAndDispatchMiddleware($config) + { + $this->injectServiceInContainer($this->container, 'config', $config); + $app = $this->factory->__invoke($this->container->reveal()); + $this->assertAttributeSame(true, 'routeMiddlewareIsRegistered', $app); + $this->assertAttributeSame(true, 'dispatchMiddlewareIsRegistered', $app); + + $r = new ReflectionProperty($app, 'pipeline'); + $r->setAccessible(true); + $pipeline = $r->getValue($app); + + $this->assertCount(2, $pipeline, 'Did not get expected pipeline count!'); + + $test = $pipeline->dequeue(); + $this->assertEquals('/', $test->path); + $this->assertSame([$app, 'routeMiddleware'], $test->handler); + + $test = $pipeline->dequeue(); + $this->assertEquals('/', $test->path); + $this->assertSame([$app, 'dispatchMiddleware'], $test->handler); + } + + public function testPipelineContainingRoutingMiddlewareConstantPipesRoutingMiddleware() + { + $config = [ + 'middleware_pipeline' => [ + ApplicationFactory::ROUTING_MIDDLEWARE, + ], + ]; + $this->injectServiceInContainer($this->container, 'config', $config); + + $app = $this->factory->__invoke($this->container->reveal()); + $this->assertAttributeSame(true, 'routeMiddlewareIsRegistered', $app); + } + + public function testPipelineContainingDispatchMiddlewareConstantPipesDispatchMiddleware() + { + $config = [ + 'middleware_pipeline' => [ + ApplicationFactory::DISPATCH_MIDDLEWARE, + ], + ]; + $this->injectServiceInContainer($this->container, 'config', $config); + + $app = $this->factory->__invoke($this->container->reveal()); + $this->assertAttributeSame(true, 'dispatchMiddlewareIsRegistered', $app); + } + + /** + * @deprecated This test can be removed for 1.1 + */ + public function testPipelineContainingRouteResultObserverMiddlewareConstantPipesRelatedMiddleware() + { + $config = [ + 'middleware_pipeline' => [ + ApplicationFactory::ROUTE_RESULT_OBSERVER_MIDDLEWARE, + ], + ]; + $this->injectServiceInContainer($this->container, 'config', $config); + + $app = $this->factory->__invoke($this->container->reveal()); + $this->assertAttributeSame(true, 'routeResultObserverMiddlewareIsRegistered', $app); + } + + /** + * @dataProvider middlewarePipelinesWithPreOrPostRouting + * @deprecated This test can be removed for 1.1 + */ + public function testUsageOfDeprecatedConfigurationRegistersRouteResultObserverMiddleware($config) + { + $config = ['middleware_pipeline' => $config]; + $this->injectServiceInContainer($this->container, 'config', $config); + + // @codingStandardsIgnoreStart + set_error_handler(function ($errno, $errmsg) use (&$triggered) { + $this->assertContains('routing', $errmsg); + $triggered = true; + }, E_USER_DEPRECATED); + // @codingStandardsIgnoreEnd + + $app = $this->factory->__invoke($this->container->reveal()); + restore_error_handler(); + + $this->assertAttributeSame(true, 'routeResultObserverMiddlewareIsRegistered', $app); + } + + public function testFactoryHonorsPriorityOrderWhenAttachingMiddleware() + { + // @codingStandardsIgnoreStart + $middleware = function ($request, $response, $next) {}; + // @codingStandardsIgnoreEnd + + $pipeline1 = [ [ 'middleware' => clone $middleware, 'priority' => 1 ] ]; + $pipeline2 = [ [ 'middleware' => clone $middleware, 'priority' => 100 ] ]; + $pipeline3 = [ [ 'middleware' => clone $middleware, 'priority' => -100 ] ]; + + $pipeline = array_merge($pipeline3, $pipeline1, $pipeline2); + $config = [ 'middleware_pipeline' => $pipeline ]; + $this->injectServiceInContainer($this->container, 'config', $config); + + $app = $this->factory->__invoke($this->container->reveal()); + + $r = new ReflectionProperty($app, 'pipeline'); + $r->setAccessible(true); + $pipeline = $r->getValue($app); + + $this->assertSame($pipeline2[0]['middleware'], $pipeline->dequeue()->handler); + $this->assertSame($pipeline1[0]['middleware'], $pipeline->dequeue()->handler); + $this->assertSame($pipeline3[0]['middleware'], $pipeline->dequeue()->handler); + } + + public function testMiddlewareWithoutPriorityIsGivenDefaultPriorityAndRegisteredInOrderReceived() + { + // @codingStandardsIgnoreStart + $middleware = function ($request, $response, $next) {}; + // @codingStandardsIgnoreEnd + + $pipeline1 = [ [ 'middleware' => clone $middleware ] ]; + $pipeline2 = [ [ 'middleware' => clone $middleware ] ]; + $pipeline3 = [ [ 'middleware' => clone $middleware ] ]; + + $pipeline = array_merge($pipeline3, $pipeline1, $pipeline2); + $config = [ 'middleware_pipeline' => $pipeline ]; + $this->injectServiceInContainer($this->container, 'config', $config); + + $app = $this->factory->__invoke($this->container->reveal()); + + $r = new ReflectionProperty($app, 'pipeline'); + $r->setAccessible(true); + $pipeline = $r->getValue($app); + + $this->assertSame($pipeline3[0]['middleware'], $pipeline->dequeue()->handler); + $this->assertSame($pipeline1[0]['middleware'], $pipeline->dequeue()->handler); + $this->assertSame($pipeline2[0]['middleware'], $pipeline->dequeue()->handler); + } + + public function testRoutingAndDispatchMiddlewareUseDefaultPriority() + { + // @codingStandardsIgnoreStart + $middleware = function ($request, $response, $next) {}; + // @codingStandardsIgnoreEnd + + $pipeline = [ + [ 'middleware' => clone $middleware, 'priority' => -100 ], + ApplicationFactory::ROUTING_MIDDLEWARE, + [ 'middleware' => clone $middleware, 'priority' => 1 ], + [ 'middleware' => clone $middleware ], + ApplicationFactory::DISPATCH_MIDDLEWARE, + [ 'middleware' => clone $middleware, 'priority' => 100 ], + ]; + + $config = [ 'middleware_pipeline' => $pipeline ]; + $this->injectServiceInContainer($this->container, 'config', $config); + + $app = $this->factory->__invoke($this->container->reveal()); + + $r = new ReflectionProperty($app, 'pipeline'); + $r->setAccessible(true); + $test = $r->getValue($app); + + $this->assertSame($pipeline[5]['middleware'], $test->dequeue()->handler); + $this->assertSame([ $app, 'routeMiddleware' ], $test->dequeue()->handler); + $this->assertSame($pipeline[2]['middleware'], $test->dequeue()->handler); + $this->assertSame($pipeline[3]['middleware'], $test->dequeue()->handler); + $this->assertSame([ $app, 'dispatchMiddleware' ], $test->dequeue()->handler); + $this->assertSame($pipeline[0]['middleware'], $test->dequeue()->handler); + } + + public function specMiddlewareContainingRoutingAndOrDispatchMiddleware() + { + // @codingStandardsIgnoreStart + return [ + 'routing-only' => [[['middleware' => [ApplicationFactory::ROUTING_MIDDLEWARE]]]], + 'dispatch-only' => [[['middleware' => [ApplicationFactory::DISPATCH_MIDDLEWARE]]]], + 'both-routing-and-dispatch' => [[['middleware' => [ApplicationFactory::ROUTING_MIDDLEWARE, ApplicationFactory::DISPATCH_MIDDLEWARE]]]], + ]; + // @codingStandardsIgnoreEnd + } + + /** + * @dataProvider specMiddlewareContainingRoutingAndOrDispatchMiddleware + */ + public function testRoutingAndDispatchMiddlewareCanBeComposedWithinArrayStandardSpecification($pipeline) + { + $expected = $pipeline[0]['middleware']; + $config = [ 'middleware_pipeline' => $pipeline ]; + $this->injectServiceInContainer($this->container, 'config', $config); + + $app = $this->factory->__invoke($this->container->reveal()); + + $r = new ReflectionProperty($app, 'pipeline'); + $r->setAccessible(true); + $appPipeline = $r->getValue($app); + + $this->assertEquals(1, count($appPipeline)); + + $innerMiddleware = $appPipeline->dequeue()->handler; + $this->assertInstanceOf(MiddlewarePipe::class, $innerMiddleware); + + $r = new ReflectionProperty($innerMiddleware, 'pipeline'); + $r->setAccessible(true); + $innerPipeline = $r->getValue($innerMiddleware); + $this->assertInstanceOf(SplQueue::class, $innerPipeline); + + $this->assertEquals( + count($expected), + $innerPipeline->count(), + sprintf('Expected %d items in pipeline; received %d', count($expected), $innerPipeline->count()) + ); + + foreach ($innerPipeline as $index => $route) { + $innerPipeline[$index] = $route->handler; + } + + foreach ($expected as $type) { + switch ($type) { + case ApplicationFactory::ROUTING_MIDDLEWARE: + $middleware = [$app, 'routeMiddleware']; + break; + case ApplicationFactory::DISPATCH_MIDDLEWARE: + $middleware = [$app, 'dispatchMiddleware']; + break; + default: + $this->fail('Unexpected value in pipeline passed from data provider'); + } + $this->assertContains($middleware, $innerPipeline); + } } } diff --git a/test/RouteMiddlewareTest.php b/test/RouteMiddlewareTest.php index 2972d740..aa784e0e 100644 --- a/test/RouteMiddlewareTest.php +++ b/test/RouteMiddlewareTest.php @@ -89,7 +89,7 @@ public function testGeneralRoutingFailureCallsNextWithSameRequestAndResponse() $this->assertTrue($called); } - public function testRoutingSuccessResolvingToCallableMiddlewareInvokesIt() + public function testRoutingSuccessResolvingToCallableMiddlewareCanBeDispatched() { $request = new ServerRequest(); $response = new Response(); @@ -99,37 +99,39 @@ public function testRoutingSuccessResolvingToCallableMiddlewareInvokesIt() return $finalResponse; }; - $result = RouteResult::fromRouteMatch( + $result = RouteResult::fromRouteMatch( '/foo', $middleware, [] ); $this->router->match($request)->willReturn($result); + $request = $request->withAttribute(RouteResult::class, $result); $next = function ($request, $response) { $this->fail('Should not enter $next'); }; - $app = $this->getApplication(); - $test = $app->routeMiddleware($request, $response, $next); + $app = $this->getApplication(); + $test = $app->dispatchMiddleware($request, $response, $next); $this->assertSame($finalResponse, $test); } - public function testRoutingSuccessWithoutMiddlewareRaisesException() + public function testRoutingSuccessWithoutMiddlewareRaisesExceptionInDispatch() { $request = new ServerRequest(); $response = new Response(); $middleware = (object) []; - $result = RouteResult::fromRouteMatch( + $result = RouteResult::fromRouteMatch( '/foo', false, [] ); $this->router->match($request)->willReturn($result); + $request = $request->withAttribute(RouteResult::class, $result); $next = function ($request, $response) { $this->fail('Should not enter $next'); @@ -137,23 +139,24 @@ public function testRoutingSuccessWithoutMiddlewareRaisesException() $app = $this->getApplication(); $this->setExpectedException(InvalidMiddlewareException::class, 'does not have'); - $app->routeMiddleware($request, $response, $next); + $app->dispatchMiddleware($request, $response, $next); } - public function testRoutingSuccessResolvingToNonCallableNonStringMiddlewareRaisesException() + public function testRoutingSuccessResolvingToNonCallableNonStringMiddlewareRaisesExceptionAtDispatch() { $request = new ServerRequest(); $response = new Response(); $middleware = (object) []; - $result = RouteResult::fromRouteMatch( + $result = RouteResult::fromRouteMatch( '/foo', $middleware, [] ); $this->router->match($request)->willReturn($result); + $request = $request->withAttribute(RouteResult::class, $result); $next = function ($request, $response) { $this->fail('Should not enter $next'); @@ -161,23 +164,24 @@ public function testRoutingSuccessResolvingToNonCallableNonStringMiddlewareRaise $app = $this->getApplication(); $this->setExpectedException(InvalidMiddlewareException::class, 'callable'); - $app->routeMiddleware($request, $response, $next); + $app->dispatchMiddleware($request, $response, $next); } - public function testRoutingSuccessResolvingToUninvokableMiddlewareRaisesException() + public function testRoutingSuccessResolvingToUninvokableMiddlewareRaisesExceptionAtDispatch() { $request = new ServerRequest(); $response = new Response(); $middleware = (object) []; - $result = RouteResult::fromRouteMatch( + $result = RouteResult::fromRouteMatch( '/foo', 'not a class', [] ); $this->router->match($request)->willReturn($result); + $request = $request->withAttribute(RouteResult::class, $result); // No container for this one, to ensure we marshal only a potential object instance. $app = new Application($this->router->reveal()); @@ -187,10 +191,10 @@ public function testRoutingSuccessResolvingToUninvokableMiddlewareRaisesExceptio }; $this->setExpectedException(InvalidMiddlewareException::class, 'callable'); - $app->routeMiddleware($request, $response, $next); + $app->dispatchMiddleware($request, $response, $next); } - public function testRoutingSuccessResolvingToInvokableMiddlewareCallsIt() + public function testRoutingSuccessResolvingToInvokableMiddlewareCallsItAtDispatch() { $request = new ServerRequest(); $response = new Response(); @@ -201,6 +205,7 @@ public function testRoutingSuccessResolvingToInvokableMiddlewareCallsIt() ); $this->router->match($request)->willReturn($result); + $request = $request->withAttribute(RouteResult::class, $result); // No container for this one, to ensure we marshal only a potential object instance. $app = new Application($this->router->reveal()); @@ -209,13 +214,13 @@ public function testRoutingSuccessResolvingToInvokableMiddlewareCallsIt() $this->fail('Should not enter $next'); }; - $test = $app->routeMiddleware($request, $response, $next); + $test = $app->dispatchMiddleware($request, $response, $next); $this->assertInstanceOf(ResponseInterface::class, $test); $this->assertTrue($test->hasHeader('X-Invoked')); $this->assertEquals(__NAMESPACE__ . '\TestAsset\InvokableMiddleware', $test->getHeaderLine('X-Invoked')); } - public function testRoutingSuccessResolvingToContainerMiddlewareCallsIt() + public function testRoutingSuccessResolvingToContainerMiddlewareCallsItAtDispatch() { $request = new ServerRequest(); $response = new Response(); @@ -223,13 +228,14 @@ public function testRoutingSuccessResolvingToContainerMiddlewareCallsIt() return $res->withHeader('X-Middleware', 'Invoked'); }; - $result = RouteResult::fromRouteMatch( + $result = RouteResult::fromRouteMatch( '/foo', 'TestAsset\Middleware', [] ); $this->router->match($request)->willReturn($result); + $request = $request->withAttribute(RouteResult::class, $result); $this->injectServiceInContainer($this->container, 'TestAsset\Middleware', $middleware); @@ -239,38 +245,12 @@ public function testRoutingSuccessResolvingToContainerMiddlewareCallsIt() $this->fail('Should not enter $next'); }; - $test = $app->routeMiddleware($request, $response, $next); + $test = $app->dispatchMiddleware($request, $response, $next); $this->assertInstanceOf(ResponseInterface::class, $test); $this->assertTrue($test->hasHeader('X-Middleware')); $this->assertEquals('Invoked', $test->getHeaderLine('X-Middleware')); } - public function testRoutingSuccessResultingInContainerExceptionReRaisesException() - { - $request = new ServerRequest(); - $response = new Response(); - - $result = RouteResult::fromRouteMatch( - '/foo', - 'TestAsset\Middleware', - [] - ); - - $this->router->match($request)->willReturn($result); - - $this->container->has('TestAsset\Middleware')->willReturn(true); - $this->container->get('TestAsset\Middleware')->willThrow(new TestAsset\ContainerException()); - - $app = $this->getApplication(); - - $next = function ($request, $response) { - $this->fail('Should not enter $next'); - }; - - $this->setExpectedException(InvalidMiddlewareException::class, 'retrieve'); - $app->routeMiddleware($request, $response, $next); - } - /** * Get the router adapters to test */ @@ -364,13 +344,17 @@ public function testRoutingWithSamePathWithoutName($adapter) $request = new ServerRequest([ 'REQUEST_METHOD' => 'GET' ], [], '/foo', 'GET'); $response = new Response(); - $result = $app->routeMiddleware($request, $response, $next); + $result = $app->routeMiddleware($request, $response, function ($request, $response) use ($app, $next) { + return $app->dispatchMiddleware($request, $response, $next); + }); $this->assertEquals('Middleware GET', (string) $result->getBody()); $request = new ServerRequest([ 'REQUEST_METHOD' => 'POST' ], [], '/foo', 'POST'); $response = new Response(); - $result = $app->routeMiddleware($request, $response, $next); + $result = $app->routeMiddleware($request, $response, function ($request, $response) use ($app, $next) { + return $app->dispatchMiddleware($request, $response, $next); + }); $this->assertEquals('Middleware POST', (string) $result->getBody()); } @@ -389,13 +373,17 @@ public function testRoutingWithSamePathWithName($adapter) $request = new ServerRequest([ 'REQUEST_METHOD' => 'GET' ], [], '/foo', 'GET'); $response = new Response(); - $result = $app->routeMiddleware($request, $response, $next); + $result = $app->routeMiddleware($request, $response, function ($request, $response) use ($app, $next) { + return $app->dispatchMiddleware($request, $response, $next); + }); $this->assertEquals('Middleware GET', (string) $result->getBody()); $request = new ServerRequest([ 'REQUEST_METHOD' => 'POST' ], [], '/foo', 'POST'); $response = new Response(); - $result = $app->routeMiddleware($request, $response, $next); + $result = $app->routeMiddleware($request, $response, function ($request, $response) use ($app, $next) { + return $app->dispatchMiddleware($request, $response, $next); + }); $this->assertEquals('Middleware POST', (string) $result->getBody()); } @@ -414,13 +402,17 @@ public function testRoutingWithSamePathWithRouteWithoutName($adapter) $request = new ServerRequest([ 'REQUEST_METHOD' => 'GET' ], [], '/foo', 'GET'); $response = new Response(); - $result = $app->routeMiddleware($request, $response, $next); + $result = $app->routeMiddleware($request, $response, function ($request, $response) use ($app, $next) { + return $app->dispatchMiddleware($request, $response, $next); + }); $this->assertEquals('Middleware GET', (string) $result->getBody()); $request = new ServerRequest([ 'REQUEST_METHOD' => 'POST' ], [], '/foo', 'POST'); $response = new Response(); - $result = $app->routeMiddleware($request, $response, $next); + $result = $app->routeMiddleware($request, $response, function ($request, $response) use ($app, $next) { + return $app->dispatchMiddleware($request, $response, $next); + }); $this->assertEquals('Middleware POST', (string) $result->getBody()); } @@ -438,13 +430,17 @@ public function testRoutingWithSamePathWithRouteWithName($adapter) $request = new ServerRequest([ 'REQUEST_METHOD' => 'GET' ], [], '/foo', 'GET'); $response = new Response(); - $result = $app->routeMiddleware($request, $response, $next); + $result = $app->routeMiddleware($request, $response, function ($request, $response) use ($app, $next) { + return $app->dispatchMiddleware($request, $response, $next); + }); $this->assertEquals('Middleware GET', (string) $result->getBody()); $request = new ServerRequest([ 'REQUEST_METHOD' => 'POST' ], [], '/foo', 'POST'); $response = new Response(); - $result = $app->routeMiddleware($request, $response, $next); + $result = $app->routeMiddleware($request, $response, function ($request, $response) use ($app, $next) { + return $app->dispatchMiddleware($request, $response, $next); + }); $this->assertEquals('Middleware POST', (string) $result->getBody()); } @@ -471,17 +467,23 @@ public function testRoutingWithSamePathWithRouteWithMultipleMethods($adapter) $request = new ServerRequest([ 'REQUEST_METHOD' => 'GET' ], [], '/foo', 'GET'); $response = new Response(); - $result = $app->routeMiddleware($request, $response, $next); + $result = $app->routeMiddleware($request, $response, function ($request, $response) use ($app, $next) { + return $app->dispatchMiddleware($request, $response, $next); + }); $this->assertEquals('Middleware GET, POST', (string) $result->getBody()); $request = new ServerRequest([ 'REQUEST_METHOD' => 'POST' ], [], '/foo', 'POST'); $response = new Response(); - $result = $app->routeMiddleware($request, $response, $next); + $result = $app->routeMiddleware($request, $response, function ($request, $response) use ($app, $next) { + return $app->dispatchMiddleware($request, $response, $next); + }); $this->assertEquals('Middleware GET, POST', (string) $result->getBody()); $request = new ServerRequest([ 'REQUEST_METHOD' => 'DELETE' ], [], '/foo', 'DELETE'); $response = new Response(); - $result = $app->routeMiddleware($request, $response, $next); + $result = $app->routeMiddleware($request, $response, function ($request, $response) use ($app, $next) { + return $app->dispatchMiddleware($request, $response, $next); + }); $this->assertEquals('Middleware DELETE', (string) $result->getBody()); } @@ -514,7 +516,9 @@ public function testMatchWithAllHttpMethods($adapter, $method) $request = new ServerRequest([ 'REQUEST_METHOD' => $method ], [], '/foo', $method); $response = new Response(); - $result = $app->routeMiddleware($request, $response, $next); + $result = $app->routeMiddleware($request, $response, function ($request, $response) use ($app, $next) { + return $app->dispatchMiddleware($request, $response, $next); + }); $this->assertEquals('Middleware', (string) $result->getBody()); } @@ -568,7 +572,9 @@ public function testInjectsRouteResultAsAttribute() $this->router->match($request)->willReturn($result); $app = $this->getApplication(); - $test = $app->routeMiddleware($request, $response, $next); + $test = $app->routeMiddleware($request, $response, function ($request, $response) use ($app, $next) { + return $app->dispatchMiddleware($request, $response, $next); + }); $this->assertSame($response, $test); $this->assertTrue($triggered); } @@ -596,33 +602,18 @@ public function testMiddlewareTriggersObserversWithSuccessfulRouteResult() $app->attachRouteResultObserver($routeResultObserver->reveal()); - $test = $app->routeMiddleware($request, $response, $next); + $test = $app->routeMiddleware($request, $response, function ($request, $response) use ($app, $next) { + return $app->routeResultObserverMiddleware( + $request, + $response, + function ($request, $response) use ($app, $next) { + return $app->dispatchMiddleware($request, $response, $next); + } + ); + }); $this->assertSame($response, $test); } - public function testMiddlewareTriggersObserversWithFailedRouteResult() - { - $request = new ServerRequest(); - $response = new Response(); - $result = RouteResult::fromRouteFailure(['GET', 'POST']); - - $routeResultObserver = $this->prophesize(RouteResultObserverInterface::class); - $routeResultObserver->update($result)->shouldBeCalled(); - $this->router->match($request)->willReturn($result); - - $next = function ($request, $response, $error = false) { - $this->assertEquals(405, $error); - $this->assertEquals(405, $response->getStatusCode()); - return $response; - }; - - $app = $this->getApplication(); - $app->attachRouteResultObserver($routeResultObserver->reveal()); - - $test = $app->routeMiddleware($request, $response, $next); - $this->assertEquals(405, $test->getStatusCode()); - } - public function testCanDetachRouteResultObservers() { $routeResultObserver = $this->prophesize(RouteResultObserverInterface::class); @@ -661,7 +652,15 @@ public function testDetachedRouteResultObserverIsNotTriggered() $app->detachRouteResultObserver($routeResultObserver->reveal()); $this->assertAttributeNotContains($routeResultObserver->reveal(), 'routeResultObservers', $app); - $test = $app->routeMiddleware($request, $response, $next); + $test = $app->routeMiddleware($request, $response, function ($request, $response) use ($app, $next) { + return $app->routeResultObserverMiddleware( + $request, + $response, + function ($request, $response) use ($app, $next) { + return $app->dispatchMiddleware($request, $response, $next); + } + ); + }); $this->assertSame($response, $test); }