diff --git a/.docheader b/.docheader index 5f13b6d4..9bba16ae 100644 --- a/.docheader +++ b/.docheader @@ -1,5 +1,7 @@ /** * @see https://github.com/zendframework/zend-expressive for the canonical source repository - * @copyright Copyright (c) %regexp:(20\d{2}-)?20\d{2}% Zend Technologies USA Inc. (http://www.zend.com) + * @copyright Copyright (c) %regexp:(20\d{2}-)?20\d{2}% Zend Technologies USA Inc. (https://www.zend.com) * @license https://github.com/zendframework/zend-expressive/blob/master/LICENSE.md New BSD License */ + +declare(strict_types=1); diff --git a/.travis.yml b/.travis.yml index 3e8ccc45..02802796 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,28 +13,6 @@ env: matrix: include: - - php: 5.6 - env: - - DEPS=lowest - - LEGACY_DEPS="phpunit/phpunit malukenho/docheader" - - php: 5.6 - env: - - DEPS=locked - - LEGACY_DEPS="phpunit/phpunit malukenho/docheader" - - php: 5.6 - env: - - DEPS=latest - - LEGACY_DEPS="phpunit/phpunit malukenho/docheader" - - php: 7 - env: - - DEPS=lowest - - php: 7 - env: - - DEPS=locked - - LEGACY_DEPS="phpunit/phpunit symfony/console symfony/debug symfony/finder" - - php: 7 - env: - - DEPS=latest - php: 7.1 env: - DEPS=lowest @@ -52,6 +30,7 @@ matrix: - php: 7.2 env: - DEPS=locked + - PHPSTAN_CHECK=true - php: 7.2 env: - DEPS=latest @@ -60,8 +39,7 @@ before_install: - if [[ $TEST_COVERAGE != 'true' ]]; then phpenv config-rm xdebug.ini || return 0 ; fi install: - - travis_retry composer install $COMPOSER_ARGS --ignore-platform-reqs - - if [[ $LEGACY_DEPS != '' ]]; then travis_retry composer update $COMPOSER_ARGS --with-dependencies $LEGACY_DEPS ; fi + - travis_retry composer install $COMPOSER_ARGS - if [[ $DEPS == 'latest' ]]; then travis_retry composer update $COMPOSER_ARGS ; fi - if [[ $DEPS == 'lowest' ]]; then travis_retry composer update --prefer-lowest --prefer-stable $COMPOSER_ARGS ; fi - if [[ $TEST_COVERAGE == 'true' ]]; then travis_retry composer require --dev $COMPOSER_ARGS $COVERAGE_DEPS ; fi @@ -73,6 +51,7 @@ script: - if [[ $TEST_COVERAGE == 'true' ]]; then composer test-coverage ; else composer test ; fi - if [[ $CS_CHECK == 'true' ]]; then composer cs-check ; fi - if [[ $CS_CHECK == 'true' ]]; then composer license-check ; fi + - if [[ $PHPSTAN_CHECK == 'true' ]]; then composer phpstan ; fi after_script: - if [[ $TEST_COVERAGE == 'true' ]]; then vendor/bin/php-coveralls -v ; fi diff --git a/CHANGELOG.md b/CHANGELOG.md index 30891f5e..87b7001c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,658 @@ All notable changes to this project will be documented in this file, in reverse chronological order by release. +## 3.0.0rc3 - 2018-03-07 + +### Added + +- Nothing. + +### Changed + +- [#579](https://github.com/zendframework/zend-expressive/pull/579) updates the + version constraint for zend-expressive-router to use 3.0.0rc4 or later. + +- [#579](https://github.com/zendframework/zend-expressive/pull/579) updates the + version constraint for zend-stratigility to use 3.0.0rc1 or later. + +### Deprecated + +- Nothing. + +### Removed + +- [#580](https://github.com/zendframework/zend-expressive/pull/580) removes + zend-diactoros as a requirement; all usages of it within the package are + currently conditional on it being installed, and can be replaced easily with + any other PSR-7 implementation at this time. + +### Fixed + +- Nothing. + +## 3.0.0rc2 - 2018-03-06 + +### Added + +- Nothing. + +### Changed + +- Nothing. + +### Deprecated + +- Nothing. + +### Removed + +- Nothing. + +### Fixed + +- [#578](https://github.com/zendframework/zend-expressive/pull/578) fixes the + version constraint used with zend-stratigility to allow it to update to later + versions when released. + +## 3.0.0rc1 - 2018-02-27 + +### Added + +- Nothing. + +### Changed + +- Nothing. + +### Deprecated + +- Nothing. + +### Removed + +- Nothing. + +### Fixed + +- [#574](https://github.com/zendframework/zend-expressive/pull/574) updates the + classes `Zend\Expressive\Exception\InvalidMiddlewareException` and + `MissingDependencyException` to implement the + [PSR-11](https://www.php-fig.org/psr/psr-11/) `ContainerExceptionInterface`. + +## 3.0.0alpha9 - 2018-02-22 + +### Added + +- [#562](https://github.com/zendframework/zend-expressive/pull/562) adds the + class `Zend\Expressive\Response\ServerRequestErrorResponseGenerator`, and maps + it to the `Zend\Expressive\Container\ServerRequestErrorResponseGeneratorFactory`. + The class generates an error response when an exeption occurs producing a + server request instance, and can be optionally templated. + +### Changed + +- [#568](https://github.com/zendframework/zend-expressive/pull/568) updates the + zendframework/zend-stratigility dependency to require at least 3.0.0alpha4. + +- [#568](https://github.com/zendframework/zend-expressive/pull/568) updates the + `ErrorHandlerFactory` to pull the `Psr\Http\Message\ResponseInterface` + service, which returns a factory capable of returning a response instance, + and passes it to the `Zend\Stratigility\Middleware\ErrorHandler` instance it + creates, as that class changes in 3.0.0alpha4 such that it now expects a + factory instead of an instance. + +- [#562](https://github.com/zendframework/zend-expressive/pull/562) modifies the + `Zend\Expressive\Container\RequestHandlerRunnerFactory` to depend on the + `Zend\Expressive\Response\ServerRequestErrorResponseGenerator` service instead + of the `Zend\Expressive\SERVER_REQUEST_ERROR_RESPONSE_GENERATOR` virtual + service. + +- [#562](https://github.com/zendframework/zend-expressive/pull/562) extracts + most logic from `Zend\Expressive\Middleware\ErrorResponseGenerator` to a new + trait, `Zend\Expressive\Response\ErrorResponseGeneratorTrait`. A trait was + used as the classes consuming it are from different namespaces, and thus + different inheritance trees. The trait is used by both the + `ErrorResponseGenerator` and the new `ServerRequestErrorResponseGenerator`. + +### Deprecated + +- Nothing. + +### Removed + +- [#562](https://github.com/zendframework/zend-expressive/pull/562) removes the + constant `Zend\Expressive\SERVER_REQUEST_ERROR_RESPONSE_GENERATOR`. It was + only used internally previously. + +### Fixed + +- Nothing. + +## 3.0.0alpha8 - 2018-02-21 + +### Added + +- Nothing. + +### Changed + +- [#559](https://github.com/zendframework/zend-expressive/pull/559) reverts the + changes performed for [#556](https://github.com/zendframework/zend-expressive/pull/556) + to the `ApplicationFactory`. It now uses the canonical service name for the + `PathBasedRoutingMiddleware` instead of the `ROUTE_MIDDLEWARE` constant. + +- [#561](https://github.com/zendframework/zend-expressive/pull/561) updates to + zend-expressive-router 3.0.0alpha3. + +- [#561](https://github.com/zendframework/zend-expressive/pull/561) renames + `Zend\Expressive\Container\ResponseFactory` to `Zend\Expressive\Container\ResponseFactoryFactory`, + and the factory now returns a callable that will return a zend-diactoros + `Response` instance, instead of returning the instance itself. Each of the + various services named after zend-expressive-router response constants were + removed in favor of a single `Psr\Http\Message\ResponseInterface` service + resolving to the `ResponseFactoryFactory`. + +- [#561](https://github.com/zendframework/zend-expressive/pull/561) modifies the + `Zend\Expressive\Handler\NotFoundHandler` to compose a response factory + instead of a response prototype. This approach allows it to use the + `Psr\Http\Message\ResponseInterface` service defined per the above note. + +- [#561](https://github.com/zendframework/zend-expressive/pull/561) renames + the `Zend\Expressive\Router\IMPLICIT_HEAD_MIDDLEWARE_STREAM_FACTORY` service + to `Psr\Http\Message\StreamInterface`, as this is what zend-expressive-router + now expects. + +- [#561](https://github.com/zendframework/zend-expressive/pull/561) renames the + `Zend\Expressive\ServerRequestFactory` service to + `Psr\Http\Message\ServerRequestInterface`. The + `Zend\Expressive\SERVER_REQUEST_FACTORY` constant now resolves to the + interface name. + +### Deprecated + +- Nothing. + +### Removed + +- Nothing. + +### Fixed + +- [#558](https://github.com/zendframework/zend-expressive/pull/558) adds a + missing import statement for the PSR-7 `StreamInterface` to the + `StreamFactoryFactory`. + +- [#555](https://github.com/zendframework/zend-expressive/pull/555) adds tests + to better ensure that the entries in the `ConfigProvider` resolve to valid + factories, aliases, etc. + +## 3.0.0alpha7 - 2018-02-14 + +### Added + +- Nothing. + +### Changed + +- [#556](https://github.com/zendframework/zend-expressive/pull/556) modifies the + `ApplicationFactory` such that it now uses the + `Zend\Expressive\ROUTE_MIDDLEWARE` constant in order to retrieve the + `Zend\Expressive\Router\Middleware\PathBasedRoutingMiddleware` instance. + This is done to help smooth upgrades from v2 to v3, as it prevents a manual + step when updating the `config/pipeline.php`, and ensures that the instance + composed in the application is the same instance piped to the application. + +### Deprecated + +- Nothing. + +### Removed + +- Nothing. + +### Fixed + +- Nothing. + +## 3.0.0alpha6 - 2018-02-14 + +### Added + +- [#551](https://github.com/zendframework/zend-expressive/pull/551) and + [#553](https://github.com/zendframework/zend-expressive/pull/553) add + `Zend\Expressive\Container\StreamFactoryFactory`, for producing an callable + capable of producing an empty, writable PSR-7 `StreamInterface` instance using + zend-diactoros. The stream produced is backed by a `php://temp` stream. + +- [#551](https://github.com/zendframework/zend-expressive/pull/551) and + [#554](https://github.com/zendframework/zend-expressive/pull/554) add + the following constants under the `Zend\Expressive` namespace: + + - `DEFAULT_DELEGATE` can be used to refer to the former `DefaultDelegate` FQCN service. + - `IMPLICIT_HEAD_MIDDLEWARE` can be used to refer to the former `Zend\Expressive\Router\Middleware\ImplicitHeadMiddleware` service. + - `IMPLICIT_OPTIONS_MIDDLEWARE` can be used to refer to the former `Zend\Expressive\Router\Middleware\ImplicitOPTIONSMiddleware` service. + - `NOT_FOUND_MIDDLEWARE` can be used to refer to the former `Zend\Expressive\Middleware\NotFoundMiddleware` service. + - `NOT_FOUND_RESPONSE` can be used to refer to the former `Zend\Expressive\Response\NotFoundResponseInterface` service. + - `SERVER_REQUEST_ERROR_RESPONSE_GENERATOR` can be used to refer to the former `Zend\Expressive\ServerRequestErrorResponseGenerator` service. + - `SERVER_REQUEST_FACTORY` can be used to refer to the former `Zend\Expressive\ServerRequestFactory` service. + +### Changed + +- [#551](https://github.com/zendframework/zend-expressive/pull/551) updates + dependencies to pin to zend-expressive-router 3.0.0alpha2 or later. + +- [#551](https://github.com/zendframework/zend-expressive/pull/551) renames + `Zend\Expressive\Middleware\NotFoundMiddleware` to + `Zend\Expressive\Handler\NotFoundHandler`, which allows it to be used as a + PSR-15 request handler, and, when piped or routed to, also as middleware. + The original class name was aliased to the renamed class in the + `ConfigProvider`. + +- [#551](https://github.com/zendframework/zend-expressive/pull/551) modifies the + `ApplicationConfigInjectionDelegator` to raise an exception if the callback + passed to it does not produce an `Application` instance, instead of returning + the instance without changes. This allows developers to understand what they + need to correct in their service configuration. + +- [#551](https://github.com/zendframework/zend-expressive/pull/551) updates + the `ConfigProvider` to add entries for the following zend-expressive-router + constants as follows: + + - `IMPLICIT_HEAD_MIDDLEWARE_RESPONSE` maps to the `ResponseFactory`. + - `IMPLICIT_HEAD_MIDDLEWARE_STREAM_FACTORY` maps to the `StreamFactory`. + - `IMPLICIT_OPTIONS_MIDDLEWARE_RESPONSE` maps to the `ResponseFactory`. + +- [#554](https://github.com/zendframework/zend-expressive/pull/554) updates + the `ConfigProvider` to add entries for the following constants as follows: + + - `IMPLICIT_HEAD_MIDDLEWARE` aliases to the `Zend\Expressive\Router\Middleware\ImplicitHeadMiddleware` service. + - `IMPLICIT_OPTIONS_MIDDLEWARE` aliases to the `Zend\Expressive\Router\Middleware\ImplicitOptionsMiddleware` service. + +### Deprecated + +- Nothing. + +### Removed + +- [#551](https://github.com/zendframework/zend-expressive/pull/551) removes + `Zend\Expressive\Container\RouteMiddlewareFactory`, as zend-expressive-router + now provides a factory for the middleware. + +- [#551](https://github.com/zendframework/zend-expressive/pull/551) removes + `Zend\Expressive\Container\DispatchMiddlewareFactory`, as zend-expressive-router + now provides a factory for the middleware. + +- [#551](https://github.com/zendframework/zend-expressive/pull/551) removes + `Zend\Expressive\Middleware\ImplicitHeadMiddleware`, as it is now provided by + the zend-expressive-router package. + +- [#551](https://github.com/zendframework/zend-expressive/pull/551) removes + `Zend\Expressive\Middleware\ImplicitOptionsMiddleware`, as it is now provided + by the zend-expressive-router package. + +### Fixed + +- Nothing. + +## 3.0.0alpha5 - 2018-02-07 + +### Added + +- Nothing. + +### Changed + +- [#547](https://github.com/zendframework/zend-expressive/pull/547) modifies the + `ConfigProvider`, the `NotFoundMiddlewareFactory`, and the + `RouteMiddlewareFactory` to remove the concept of the _unshared_ + `ResponseInterface` service, as service sharing is not always configurable in + container implementations. To resolve the ability to provide discrete + instances, the `ConfigProvider` defines two new virtual services that each + resolve to the `Zend\Expressive\Container\ResponseFactory`: + + - `Zend\Expressive\Response\NotFoundResponseInterface` + - `Zend\Expressive\Response\RouterResponseInterface` + + The related factories now consume these services in order to receive a + response prototype for the services they produce. + +- [#542](https://github.com/zendframework/zend-expressive/pull/542) modifies the + `composer.json` to no longer suggest the pimple/pimple package, but rather the + zendframework/zend-pimple-config package. + +- [#542](https://github.com/zendframework/zend-expressive/pull/542) modifies the + `composer.json` to no longer suggest the aura/di package, but rather the + zendframework/zend-auradi-config package. + +### Deprecated + +- Nothing. + +### Removed + +- Nothing. + +### Fixed + +- Nothing. + +## 3.0.0alpha4 - 2018-02-07 + +### Added + +- Nothing. + +### Changed + +- Nothing. + +### Deprecated + +- Nothing. + +### Removed + +- Nothing. + +### Fixed + +- [#549](https://github.com/zendframework/zend-expressive/pull/549) modifies how + the `ServerRequestFactoryFactory` returns the + `ServerRequestFactory::fromGlobals()` mechanism, wrapping it in an anonymous + function. This ensures compatibility across all containers. + +- [#550](https://github.com/zendframework/zend-expressive/pull/550) fixes how + the `ConfigProvider` references the `ErrorResponseGenerator`, using the + `Zend\Expressive\Middleware` namespace instead of the + `Zend\Stratigility\Middleware` namespace. + +## 3.0.0alpha3 - 2018-02-06 + +### Added + +- Nothing. + +### Changed + +- [#546](https://github.com/zendframework/zend-expressive/pull/546) merges + `Zend\Expressive\Middleware\NotFoundHandler` into + `Zend\Expressive\Middleware\NotFoundMiddleware`, as well as merges + `Zend\Expressive\Container\NotFoundHandlerFactory` into + `Zend\Expressive\Container\NotFoundMiddlewareFactory`. `NotFoundMiddleware` + now does the work of the former `Zend\Expressive\Delegate\NotFoundDelegate`, + and, as such, accepts the following constructor arguments: + + - PSR-7 `ResponseInterface $responsePrototype` (required) + - `Zend\Expressive\Template\TemplateRendererInterface $renderer` (optional) + - `string $template = self::TEMPLATE_DEFAULT` (optional; defaults to "error::404") + - `string $layout = self::LAYOUT_DEFAULT` (optional; defaults to "layout::default") + +### Deprecated + +- Nothing. + +### Removed + +- [#546](https://github.com/zendframework/zend-expressive/pull/546) removes the + class `Zend\Expressive\Delegate\DefaultDelegate`, as there is no longer a + concept of a default handler invoked by the application. Instead, developers + MUST pipe middleware at the innermost layer of the pipeline guaranteed to + return a response; we recommend using `Zend\Expressive\Middleware\NotFoundMiddleware` + for this purpose. + +### Fixed + +- Nothing. + +## 3.0.0alpha2 - 2018-02-05 + +### Added + +- [#543](https://github.com/zendframework/zend-expressive/pull/543) adds support + for the final PSR-15 interfaces, and explicitly depends on + psr/http-server-middleware. + +- [#543](https://github.com/zendframework/zend-expressive/pull/543) adds a new + class, `Zend\Expressive\MiddlewareContainer`. The class decorates a PSR-11 + `ContainerInterface`, and adds the following behavior: + + - If a class is not in the container, but exists, `has()` will return `true`. + - If a class is not in the container, but exists, `get()` will attempt to + instantiate it, caching the instance locally if it is valid. + - Any instance pulled from the container or directly instantiated is tested. + If it is a PSR-15 `RequestHandlerInterface`, it will decorate it in a + zend-stratigility `RequestHandlerMiddleware` instance. If the instance is + not a PSR-15 `MiddlewareInterface`, the container will raise a + `Zend\Expressive\Exception\InvalidMiddlewareException`. + +- [#543](https://github.com/zendframework/zend-expressive/pull/543) adds a new + class, `Zend\Expressive\MiddlewareFactory`. The class composes a + `MiddlewareContainer`, and exposes the following methods: + + - `callable(callable $middleware) : CallableMiddlewareDecorator` + - `handler(RequestHandlerInterface $handler) : RequestHandlerMiddleware` + - `lazy(string $service) : LazyLoadingMiddleware` + - `prepare($middleware) : MiddlewareInterface`: accepts a string service name, + callable, `RequestHandlerInterface`, `MiddlewareInterface`, or array of such + values, and returns a `MiddlewareInterface`, raising an exception if it + cannot determine what to do. + - `pipeline(...$middleware) : MiddlewarePipe`: passes each argument to + `prepare()`, and the result to `MiddlewarePipe::pipe()`, returning the + pipeline when complete. + +- [#543](https://github.com/zendframework/zend-expressive/pull/543) adds + the following factory classes, each within the `Zend\Expressive\Container` + namespace: + + - `ApplicationPipelineFactory`: creates and returns a + `Zend\Stratigility\MiddlewarePipe` to use as the application middleware + pipeline. + - `DispatchMiddlewareFactory`: creates and returns a `Zend\Expressive\Router\DispatchMiddleware` instance. + - `EmitterFactory`: creates and returns a + `Zend\HttpHandlerRunner\Emitter\EmitterStack` instance composing an + `SapiEmitter` from that same namespace as the only emitter on the stack. + This is used as a dependency for the `Zend\HttpHandlerRunner\RequestHandlerRunner` + service. + - `MiddlewareContainerFactory`: creates and returns a `Zend\Expressive\MiddlewareContainer` + instance decorating the PSR-11 container passed to the factory. + - `MiddlewareFactoryFactory`: creates and returns a `Zend\Expressive\MiddlewareFactory` + instance decorating a `MiddlewareContainer` instance as pulled from the + container. + - `RequestHandlerRunnerFactory`: creates and returns a + `Zend\HttpHandlerRunner\RequestHandlerRunner` instance, using the services + `Zend\Expressive\Application`, `Zend\HttpHandlerRunner\Emitter\EmitterInterface`, + `Zend\Expressive\ServerRequestFactory`, and `Zend\Expressive\ServerRequestErrorResponseGenerator`. + - `RouteMiddlewareFactory`: creates and returns a `Zend\Expressive\Router\PathBasedRoutingMiddleware` instance. + - `ServerRequestFactoryFactory`: creates and returns a `callable` factory for + generating a PSR-7 `ServerRequestInterface` instance; this returned factory is a + dependency for the `Zend\HttpHandlerRunner\RequestHandlerRunner` service. + - `ServerRequestErrorResponseGeneratorFactory`: creates and returns a + `callable` that accepts a PHP `Throwable` in order to generate a PSR-7 + `ResponseInterface` instance; this returned factory is a dependency for the + `Zend\HttpHandlerRunner\RequestHandlerRunner` service, which uses it to + generate a response in the scenario that the `ServerRequestFactory` is + unable to create a request instance. + +- [#543](https://github.com/zendframework/zend-expressive/pull/543) adds + the class `Zend\Expressive\Container\ApplicationConfigInjectionDelegator`. + This class may be used either as a delegator factory on the + `Zend\Expressive\Application` instance, or you may use the two static methods + it defines to inject pipeline middleware and/or routes from configuration: + + - `injectPipelineFromConfig(Application $application, array $config) : void` + - `injectRoutesFromConfig(Application $application, array $config) : void` + + These methods work the same way as the associated `Application` methods from + version 2, accepting the same configuration. + +- [#543](https://github.com/zendframework/zend-expressive/pull/543) adds + `Zend\Expressive\ConfigProvider`, which details the default service mappings. + +### Changed + +- [#543](https://github.com/zendframework/zend-expressive/pull/543) adds + dependencies on each of: + + - zend-stratigility 3.0.0alpha3 + - zend-expressive-router 3.0.0alpha1 + - zend-httphandlerrunner 1.0.0 + + and removes the dependency http-interop/http-server-middleware. + +- [#543](https://github.com/zendframework/zend-expressive/pull/543) renames + `Zend\Expressive\Middleware\NotFoundHandler` to + `Zend\Expressive\Middleware\NotFoundMiddleware`, and its accompanying factory + `Zend\Expressive\Container\NotFoundHandlerFactory` to + `Zend\Expressive\Container\NotFoundMiddlewareFactory`. + +- [#543](https://github.com/zendframework/zend-expressive/pull/543) renames + `Zend\Expressive\Delegate\NotFoundDelegate` to + `Zend\Expressive\Handler\NotFoundHandler`, updating it to implement the PSR-15 + `RequestHandlerInterface`. It also renames the factory + `Zend\Expressive\Container\NotFoundDelegateFactory` to + `Zend\Expressive\Container\NotFoundHandlerFactory`. + +- [#543](https://github.com/zendframework/zend-expressive/pull/543) refactors + `Zend\Expressive\Application` completely. + + The class no longer extends `Zend\Stratigility\MiddlewarePipe`, and instead + implements the PSR-15 `MiddlewareInterface` and `RequestHandlerInterface`. + + It now **requires** the following dependencies via constructor injection, in + the following order: + + - `Zend\Expressive\MiddlewareFactory` + - `Zend\Stratigility\MiddlewarePipe`; this is the pipeline representing the application. + - `Zend\Expressive\Router\PathBasedRoutingMiddleware` + - `Zend\HttpHandlerRunner\RequestHandlerRunner` + + It removes all "getter" methods (as detailed in the "Removed" section of this + release), but retains the following methods, with the changes described below. + Please note: in most cases, these methods accept the same arguments as in the + version 2 series, with the exception of callable double-pass middleware (these + may be decorated manually using `Zend\Stratigility\doublePassMiddleware()`), + and http-interop middleware (no longer supported; rewrite as PSR-15 + middleware). + + - `pipe($middlewareOrPath, $middleware = null) : void` passes its arguments to + the composed `MiddlewareFactory`'s `prepare()` method; if two arguments are + provided, the second is passed to the factory, and the two together are + passed to `Zend\Stratigility\path()` in order to decorate them to work as + middleware. The prepared middleware is then piped to the composed + `MiddlewarePipe` instance. + + As a result of switching to use the `MiddlewareFactory` to prepare + middleware, you may now pipe `RequestHandlerInterface` instances as well. + + - `route(string $path, $middleware, array $methods = null, string $name) : Route` + passes its `$middleware` argument to the `MiddlewareFactory::prepare()` + method, and then all arguments to the composed `PathBasedRoutingMiddleware` + instance's `route()` method. + + As a result of switching to use the `MiddlewareFactory` to prepare + middleware, you may now route to `RequestHandlerInterface` instances as + well. + + - Each of `get`, `post`, `patch`, `put`, `delete`, and `any` now proxy to + `route()` after marshaling the correct `$methods`. + + - `getRoutes() : Route[]` proxies to the composed `PathBasedRoutingMiddleware` + instance. + + - `handle(ServerRequestInterface $request) : ResponseInterface` proxies to the + composed `MiddlewarePipe` instance's `handle()` method. + + - `process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface` + proxies to the composed `MiddlewarePipe` instance's `process()` method. + + - `run() : void` proxies to the composed `RequestHandlerRunner` instance. + Please note that the method no longer accepts any arguments. + +- [#543](https://github.com/zendframework/zend-expressive/pull/543) modifies the + `Zend\Expressive\Container\ApplicationFactory` to reflect the changes to the + `Zend\Expressive\Application` class as detailed above. It pulls the following + services to inject via the constructor: + + - `Zend\Expressive\MiddlewareFactory` + - `Zend\Stratigility\ApplicationPipeline`, which should resolve to a + `MiddlewarePipe` instance; use the + `Zend\Expressive\Container\ApplicationPipelineFactory`. + - `Zend\Expressive\Router\PathBasedRoutingMiddleware` + - `Zend\HttpHandlerRunner\RequestHandlerRunner` + +### Deprecated + +- Nothing. + +### Removed + +- [#543](https://github.com/zendframework/zend-expressive/pull/543) removes + support for http-interop/http-server-middleware. + +- [#543](https://github.com/zendframework/zend-expressive/pull/543) removes the + class `Zend\Expressive\Middleware\RouteMiddleware`. Use the + `PathBasedRoutingMiddleware` or `RouteMiddleware` from zend-expressive-router + instead; the factory `Zend\Expressive\Container\RouteMiddlewareFactory` will + return a `PathBasedRoutingMiddleware` instance for you. + +- [#543](https://github.com/zendframework/zend-expressive/pull/543) removes the + class `Zend\Expressive\Middleware\DispatchMiddleware`. Use the + `DispatchMiddleware` from zend-expressive-router instead; the factory + `Zend\Expressive\Container\DispatchMiddlewareFactory` will return an instance + for you. + +- [#543](https://github.com/zendframework/zend-expressive/pull/543) removes the + class `Zend\Expressive\Emitter\EmitterStack`; use the class + `Zend\HttpHandlerRunner\Emitter\EmitterStack` instead. + +- [#543](https://github.com/zendframework/zend-expressive/pull/543) removes the + following methods from `Zend\Expressive\Application`: + + - `pipeRoutingMiddleware()`: use `pipe(\Zend\Expressive\Router\PathBasedRoutingMiddleware::class)` instead. + - `pipeDispatchMiddleware()`: use `pipe(\Zend\Expressive\Router\DispatchMiddleware::class)` instead. + - `getContainer()` + - `getDefaultDelegate()`: ensure you pipe middleware capable of returning a response at the innermost layer; this can be done by decorating a request handler using `Zend\Stratigility\Middleware\RequestHandlerMiddleware`, using `Zend\Expressive\Middleware\NotFoundMiddleware`, or other approaches. + - `getEmitter()`: use the `Zend\HttpHandlerRunner\Emitter\EmitterInterface` service from the container. + - `injectPipelineFromConfig()`: use the new `ApplicationConfigInjectionDelegator` and/or the static method of the same name it defines. + - `injectRoutesFromConfig()`: use the new `ApplicationConfigInjectionDelegator` and/or the static method of the same name it defines. + +- [#543](https://github.com/zendframework/zend-expressive/pull/543) removes the + class `Zend\Expressive\AppFactory`. + +### Fixed + +- Nothing. + +## 3.0.0alpha1 - 2018-01-22 + +### Added + +- [#529](https://github.com/zendframework/zend-expressive/pull/529) adds support + for http-interop/http-server-middleware (PSR-15 pre-cursor). + +- [#538](https://github.com/zendframework/zend-expressive/pull/538) adds scalar + and return type hints to methods wherever possible. + +### Changed + +- Nothing. + +### Deprecated + +- Nothing. + +### Removed + +- [#529](https://github.com/zendframework/zend-expressive/pull/529) removes + support for PHP versions prior to PHP 7.1. + +- [#529](https://github.com/zendframework/zend-expressive/pull/529) removes + support for http-interop/http-middleware (previous PSR-15 iteration). + +### Fixed + +- Nothing. + ## 2.2.0 - 2018-03-12 ### Added diff --git a/README.md b/README.md index bce397a4..abf31ef6 100644 --- a/README.md +++ b/README.md @@ -73,10 +73,10 @@ can recommend the following implementations: - [zend-servicemanager](https://github.com/zendframework/zend-servicemanager): `composer require zendframework/zend-servicemanager` -- [pimple-container-interop](https://github.com/xtreamwayz/pimple-container-interop): - `composer require xtreamwayz/pimple-container-interop` -- [Aura.Di](https://github.com/auraphp/Aura.Di): - `composer require aura/di` +- [Pimple](https://github.com/silexphp/Pimple) (see [docs](docs/book/features/container/pimple.md) for more details): + `composer require zendframework/zend-pimple-config` +- [Aura.Di](https://github.com/auraphp/Aura.Di) (see [docs](docs/book/features/container/aura-di.md) for more details): + `composer require zendframework/zend-auradi-config` Additionally, you may optionally want to install a template renderer implementation, and/or an error handling integration. These are covered in the diff --git a/bin/expressive-tooling b/bin/expressive-tooling index 7806d796..80f6fbbd 100755 --- a/bin/expressive-tooling +++ b/bin/expressive-tooling @@ -4,10 +4,12 @@ * Script for migrating configuration-driven pipelines/routes to programmatic usage. * * @see https://github.com/zendframework/zend-expressive for the canonical source repository - * @copyright Copyright (c) 2016 Zend Technologies USA Inc. (http://www.zend.com) + * @copyright Copyright (c) 2016-2017 Zend Technologies USA Inc. (https://www.zend.com) * @license https://github.com/zendframework/zend-expressive/blob/master/LICENSE.md New BSD License */ +declare(strict_types=1); + namespace Zend\Expressive; if (false === ($paths = getenv('PATH'))) { diff --git a/composer.json b/composer.json index d5c2e5c5..96b8fe7f 100644 --- a/composer.json +++ b/composer.json @@ -8,6 +8,7 @@ "psr", "psr-7", "psr-11", + "expressive", "zf", "zendframework", "zend-expressive" @@ -21,39 +22,47 @@ "forum": "https://discourse.zendframework.com/c/questions/expressive" }, "require": { - "php": "^5.6 || ^7.0", + "php": "^7.1", "fig/http-message-util": "^1.1.2", - "http-interop/http-middleware": "^0.4.1", "psr/container": "^1.0", "psr/http-message": "^1.0.1", - "zendframework/zend-diactoros": "^1.3.10", - "zendframework/zend-expressive-router": "^2.4.1", - "zendframework/zend-expressive-template": "^1.0.4", - "zendframework/zend-stratigility": "^2.2.0" + "psr/http-server-middleware": "^1.0", + "zendframework/zend-expressive-router": "^3.0.0rc4", + "zendframework/zend-expressive-template": "^2.0.0alpha1", + "zendframework/zend-httphandlerrunner": "^1.0.1", + "zendframework/zend-stratigility": "^3.0.0rc1" }, "require-dev": { - "filp/whoops": "^2.1.6 || ^1.1.10", - "malukenho/docheader": "^0.1.5", + "filp/whoops": "^1.1.10 || ^2.1.13", + "malukenho/docheader": "^0.1.6", "mockery/mockery": "^1.0", - "phpunit/phpunit": "^5.7.23 || ^6.4.3", + "phpstan/phpstan": "^0.9.2", + "phpstan/phpstan-strict-rules": "^0.9", + "phpunit/phpunit": "^7.0.1", "zendframework/zend-coding-standard": "~1.0.0", - "zendframework/zend-expressive-aurarouter": "^2.2", - "zendframework/zend-expressive-fastroute": "^2.2", - "zendframework/zend-expressive-zendrouter": "^2.2", - "zendframework/zend-servicemanager": "^3.3 || ^2.7.8" + "zendframework/zend-diactoros": "^1.7.1", + "zendframework/zend-expressive-aurarouter": "^3.0.0rc3", + "zendframework/zend-expressive-fastroute": "^3.0.0rc4", + "zendframework/zend-expressive-zendrouter": "^3.0.0rc3", + "zendframework/zend-servicemanager": "^2.7.8 || ^3.3" }, "conflict": { - "container-interop/container-interop": "<1.2.0" + "container-interop/container-interop": "<1.2.0", + "zendframework/zend-diactoros": "<1.7.1" }, "suggest": { "filp/whoops": "^2.1 to use the Whoops error handler", + "psr/http-message-implementation": "Please install a psr/http-message-implementation to consume Expressive; e.g., zendframework/zend-diactoros", + "zendframework/zend-auradi-config": "^1.0 to use Aura.Di dependency injection container", "zendframework/zend-expressive-helpers": "^3.0 for its UrlHelper, ServerUrlHelper, and BodyParseMiddleware", - "aura/di": "^3.2 to make use of Aura.Di dependency injection container", - "xtreamwayz/pimple-container-interop": "^1.0 to use Pimple for dependency injection", "zendframework/zend-expressive-tooling": "For migration and development tools; require it with the --dev flag", + "zendframework/zend-pimple-config": "^1.0 to use Pimple for dependency injection container", "zendframework/zend-servicemanager": "^3.3 to use zend-servicemanager for dependency injection" }, "autoload": { + "files": [ + "src/constants.php" + ], "psr-4": { "Zend\\Expressive\\": "src/" } @@ -61,10 +70,7 @@ "autoload-dev": { "psr-4": { "ZendTest\\Expressive\\": "test/" - }, - "files": [ - "test/class_exists.php" - ] + } }, "config": { "sort-packages": true @@ -90,6 +96,7 @@ ], "cs-check": "phpcs", "cs-fix": "phpcbf", + "phpstan": "phpstan analyze -l max -c phpstan.neon ./src", "test": "phpunit --colors=always", "test-coverage": "phpunit --colors=always --coverage-clover clover.xml", "license-check": "docheader check src/ test/" diff --git a/composer.lock b/composer.lock index 8cd6e07f..534ee968 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "43ef3ad68c22086ad9fa309ae57ea601", + "content-hash": "ac2230b7c744135cbf3ac7e2673317b9", "packages": [ { "name": "fig/http-message-util", @@ -56,59 +56,6 @@ ], "time": "2017-02-09T16:10:21+00:00" }, - { - "name": "http-interop/http-middleware", - "version": "0.4.1", - "source": { - "type": "git", - "url": "https://github.com/http-interop/http-middleware.git", - "reference": "9a801fe60e70d5d608b61d56b2dcde29516c81b9" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/http-interop/http-middleware/zipball/9a801fe60e70d5d608b61d56b2dcde29516c81b9", - "reference": "9a801fe60e70d5d608b61d56b2dcde29516c81b9", - "shasum": "" - }, - "require": { - "php": ">=5.3.0", - "psr/http-message": "^1.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Interop\\Http\\ServerMiddleware\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" - } - ], - "description": "Common interface for HTTP server-side middleware", - "keywords": [ - "factory", - "http", - "middleware", - "psr", - "psr-17", - "psr-7", - "request", - "response" - ], - "abandoned": "http-interop/http-server-middleware", - "time": "2017-01-14T15:23:42+00:00" - }, { "name": "psr/container", "version": "1.0.0", @@ -209,97 +156,110 @@ "time": "2016-08-06T14:39:51+00:00" }, { - "name": "webimpress/composer-extra-dependency", - "version": "0.2.2", + "name": "psr/http-server-handler", + "version": "1.0.0", "source": { "type": "git", - "url": "https://github.com/webimpress/composer-extra-dependency.git", - "reference": "31fa56391d30f03b1180c87610cbe22254780ad9" + "url": "https://github.com/php-fig/http-server-handler.git", + "reference": "439d92054dc06097f2406ec074a2627839955a02" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webimpress/composer-extra-dependency/zipball/31fa56391d30f03b1180c87610cbe22254780ad9", - "reference": "31fa56391d30f03b1180c87610cbe22254780ad9", + "url": "https://api.github.com/repos/php-fig/http-server-handler/zipball/439d92054dc06097f2406ec074a2627839955a02", + "reference": "439d92054dc06097f2406ec074a2627839955a02", "shasum": "" }, "require": { - "composer-plugin-api": "^1.1", - "php": "^5.6 || ^7.0" - }, - "require-dev": { - "composer/composer": "^1.5.2", - "mikey179/vfsstream": "^1.6.5", - "phpunit/phpunit": "^5.7.22 || ^6.4.1", - "zendframework/zend-coding-standard": "~1.0.0" + "php": ">=7.0", + "psr/http-message": "^1.0" }, - "type": "composer-plugin", + "type": "library", "extra": { - "class": "Webimpress\\ComposerExtraDependency\\Plugin" + "branch-alias": { + "dev-master": "1.0.x-dev" + } }, "autoload": { "psr-4": { - "Webimpress\\ComposerExtraDependency\\": "src/" + "Psr\\Http\\Server\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-2-Clause" + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } ], - "description": "Composer plugin to require extra dependencies", - "homepage": "https://github.com/webimpress/composer-extra-dependency", + "description": "Common interface for HTTP server-side request handler", "keywords": [ - "composer", - "dependency", - "webimpress" + "handler", + "http", + "http-interop", + "psr", + "psr-15", + "psr-7", + "request", + "response", + "server" ], - "time": "2017-10-17T17:15:14+00:00" + "time": "2018-01-22T17:04:15+00:00" }, { - "name": "webimpress/http-middleware-compatibility", - "version": "0.1.4", + "name": "psr/http-server-middleware", + "version": "1.0.0", "source": { "type": "git", - "url": "https://github.com/webimpress/http-middleware-compatibility.git", - "reference": "8ed1c2c7523dce0035b98bc4f3a73ca9cd1d3717" + "url": "https://github.com/php-fig/http-server-middleware.git", + "reference": "ea17eb1fb2c8df6db919cc578451a8013c6a0ae5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webimpress/http-middleware-compatibility/zipball/8ed1c2c7523dce0035b98bc4f3a73ca9cd1d3717", - "reference": "8ed1c2c7523dce0035b98bc4f3a73ca9cd1d3717", + "url": "https://api.github.com/repos/php-fig/http-server-middleware/zipball/ea17eb1fb2c8df6db919cc578451a8013c6a0ae5", + "reference": "ea17eb1fb2c8df6db919cc578451a8013c6a0ae5", "shasum": "" }, "require": { - "http-interop/http-middleware": "^0.1.1 || ^0.2 || ^0.3 || ^0.4.1 || ^0.5", - "php": "^5.6 || ^7.0", - "webimpress/composer-extra-dependency": "^0.2.2" - }, - "require-dev": { - "phpunit/phpunit": "^5.7.23 || ^6.4.3" + "php": ">=7.0", + "psr/http-message": "^1.0", + "psr/http-server-handler": "^1.0" }, "type": "library", "extra": { - "dependency": [ - "http-interop/http-middleware" - ] + "branch-alias": { + "dev-master": "1.0.x-dev" + } }, "autoload": { - "files": [ - "autoload/http-middleware.php" - ] + "psr-4": { + "Psr\\Http\\Server\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-2-Clause" + "MIT" ], - "description": "Compatibility library for Draft PSR-15 HTTP Middleware", - "homepage": "https://github.com/webimpress/http-middleware-compatibility", + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP server-side middleware", "keywords": [ + "http", + "http-interop", "middleware", + "psr", "psr-15", - "webimpress" + "psr-7", + "request", + "response" ], - "abandoned": "psr/http-server-middleware", - "time": "2017-10-17T17:31:10+00:00" + "time": "2018-01-22T17:08:31+00:00" }, { "name": "zendframework/zend-diactoros", @@ -399,41 +359,43 @@ }, { "name": "zendframework/zend-expressive-router", - "version": "2.4.1", + "version": "3.0.0rc4", "source": { "type": "git", "url": "https://github.com/zendframework/zend-expressive-router.git", - "reference": "e1a00596aa20a29968bdc6ecdf0256c8bfd6e0b5" + "reference": "3b21fb4e1b568bfa8b0ae699b6f0bdd5d5644863" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-expressive-router/zipball/e1a00596aa20a29968bdc6ecdf0256c8bfd6e0b5", - "reference": "e1a00596aa20a29968bdc6ecdf0256c8bfd6e0b5", + "url": "https://api.github.com/repos/zendframework/zend-expressive-router/zipball/3b21fb4e1b568bfa8b0ae699b6f0bdd5d5644863", + "reference": "3b21fb4e1b568bfa8b0ae699b6f0bdd5d5644863", "shasum": "" }, "require": { "fig/http-message-util": "^1.1.2", - "php": "^5.6 || ^7.0", + "php": "^7.1", "psr/container": "^1.0", "psr/http-message": "^1.0.1", - "webimpress/http-middleware-compatibility": "^0.1.1" + "psr/http-server-middleware": "^1.0" }, "require-dev": { - "http-interop/http-middleware": "0.4.1", - "malukenho/docheader": "^0.1.5", - "phpunit/phpunit": "^5.7.23 || ^6.4.3", - "zendframework/zend-coding-standard": "~1.0.0" + "malukenho/docheader": "^0.1.6", + "phpunit/phpunit": "^6.5.5", + "zendframework/zend-coding-standard": "~1.0.0", + "zendframework/zend-diactoros": "^1.7", + "zendframework/zend-stratigility": "^3.0.0rc1" }, "suggest": { - "zendframework/zend-expressive-aurarouter": "^1.0 to use the Aura.Router routing adapter", - "zendframework/zend-expressive-fastroute": "^1.2 to use the FastRoute routing adapter", - "zendframework/zend-expressive-zendrouter": "^1.2 to use the zend-router routing adapter" + "zendframework/zend-expressive-aurarouter": "^3.0 to use the Aura.Router routing adapter", + "zendframework/zend-expressive-fastroute": "^3.0 to use the FastRoute routing adapter", + "zendframework/zend-expressive-zendrouter": "^3.0 to use the zend-router routing adapter" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.4.x-dev", - "dev-develop": "3.0.x-dev" + "dev-master": "2.3.x-dev", + "dev-develop": "2.4.x-dev", + "dev-release-3.0.0": "3.0.x-dev" }, "zf": { "config-provider": "Zend\\Expressive\\Router\\ConfigProvider" @@ -444,87 +406,56 @@ "Zend\\Expressive\\Router\\": "src/" } }, - "autoload-dev": { - "psr-4": { - "ZendTest\\Expressive\\Router\\": "test/" - } - }, - "scripts": { - "check": [ - "@license-check", - "@cs-check", - "@test" - ], - "cs-check": [ - "phpcs" - ], - "cs-fix": [ - "phpcbf" - ], - "test": [ - "phpunit --colors=always" - ], - "test-coverage": [ - "phpunit --colors=always --coverage-clover clover.xml" - ], - "license-check": [ - "docheader check src/ test/" - ] - }, + "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" ], "description": "Router subcomponent for Expressive", "keywords": [ + "ZendFramework", "expressive", "http", "middleware", "psr", "psr-7", "zend-expressive", - "zendframework", "zf" ], - "support": { - "issues": "https://github.com/zendframework/zend-expressive-router/issues", - "source": "https://github.com/zendframework/zend-expressive-router", - "rss": "https://github.com/zendframework/zend-expressive-router/releases.atom", - "slack": "https://zendframework-slack.herokuapp.com", - "forum": "https://discourse.zendframework.com/c/questions/expressive" - }, - "time": "2018-03-08T19:27:02+00:00" + "time": "2018-03-07T16:56:58+00:00" }, { "name": "zendframework/zend-expressive-template", - "version": "1.0.4", + "version": "2.0.0alpha1", "source": { "type": "git", "url": "https://github.com/zendframework/zend-expressive-template.git", - "reference": "23922f96b32ab6e64fc551ec06b81fd047828765" + "reference": "64a8c472eda24926fb1a9466e88ba2a31198de83" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-expressive-template/zipball/23922f96b32ab6e64fc551ec06b81fd047828765", - "reference": "23922f96b32ab6e64fc551ec06b81fd047828765", + "url": "https://api.github.com/repos/zendframework/zend-expressive-template/zipball/64a8c472eda24926fb1a9466e88ba2a31198de83", + "reference": "64a8c472eda24926fb1a9466e88ba2a31198de83", "shasum": "" }, "require": { - "php": ">=5.5" + "php": "^7.1" }, "require-dev": { - "phpunit/phpunit": "^4.7", + "malukenho/docheader": "^0.1.6", + "phpunit/phpunit": "^6.5.3", "zendframework/zend-coding-standard": "~1.0.0" }, "suggest": { - "zendframework/zend-expressive-platesrenderer": "^0.1 to use the Plates template renderer", - "zendframework/zend-expressive-twigrenderer": "^0.1 to use the Twig template renderer", - "zendframework/zend-expressive-zendviewrenderer": "^0.1 to use the zend-view PhpRenderer template renderer" + "zendframework/zend-expressive-platesrenderer": "^2.0 to use the Plates template renderer", + "zendframework/zend-expressive-twigrenderer": "^2.0 to use the Twig template renderer", + "zendframework/zend-expressive-zendviewrenderer": "^2.0 to use the zend-view PhpRenderer template renderer" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0-dev", - "dev-develop": "1.1-dev" + "dev-master": "1.0.x-dev", + "dev-develop": "1.1.x-dev", + "dev-release-2.0.0": "2.0.x-dev" } }, "autoload": { @@ -538,36 +469,97 @@ ], "description": "Template subcomponent for Expressive", "keywords": [ + "ZendFramework", "expressive", - "template" + "template", + "zend-expressive", + "zf" ], - "time": "2017-01-11T18:42:34+00:00" + "time": "2018-02-06T20:09:18+00:00" + }, + { + "name": "zendframework/zend-httphandlerrunner", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/zendframework/zend-httphandlerrunner.git", + "reference": "5e4c1e82a8bb1585020eafd32c49ece5a6ee98df" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zendframework/zend-httphandlerrunner/zipball/5e4c1e82a8bb1585020eafd32c49ece5a6ee98df", + "reference": "5e4c1e82a8bb1585020eafd32c49ece5a6ee98df", + "shasum": "" + }, + "require": { + "php": "^7.1", + "psr/http-message": "^1.0", + "psr/http-message-implementation": "^1.0", + "psr/http-server-handler": "^1.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.3", + "zendframework/zend-coding-standard": "~1.0.0", + "zendframework/zend-diactoros": "^1.7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + }, + "zf": { + "config-provider": "Zend\\HttpHandlerRunner\\ConfigProvider" + } + }, + "autoload": { + "psr-4": { + "Zend\\HttpHandlerRunner\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Execute PSR-15 RequestHandlerInterface instances and emit responses they generate.", + "keywords": [ + "ZendFramework", + "components", + "expressive", + "psr-15", + "psr-7", + "zf" + ], + "time": "2018-02-21T20:33:02+00:00" }, { "name": "zendframework/zend-stratigility", - "version": "2.2.0", + "version": "3.0.0rc1", "source": { "type": "git", "url": "https://github.com/zendframework/zend-stratigility.git", - "reference": "e8c413fcba926ede63099936a5f86acf9b8156c5" + "reference": "6e76a8bd5b50d1cbd0b7f702a6f61293290b4382" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-stratigility/zipball/e8c413fcba926ede63099936a5f86acf9b8156c5", - "reference": "e8c413fcba926ede63099936a5f86acf9b8156c5", + "url": "https://api.github.com/repos/zendframework/zend-stratigility/zipball/6e76a8bd5b50d1cbd0b7f702a6f61293290b4382", + "reference": "6e76a8bd5b50d1cbd0b7f702a6f61293290b4382", "shasum": "" }, "require": { - "php": "^5.6 || ^7.0", + "fig/http-message-util": "^1.1", + "php": "^7.1", "psr/http-message": "^1.0", - "webimpress/http-middleware-compatibility": "^0.1.4", + "psr/http-server-middleware": "^1.0", "zendframework/zend-escaper": "^2.3" }, + "conflict": { + "zendframework/zend-diactoros": "<1.7.1" + }, "require-dev": { - "malukenho/docheader": "^0.1.5", - "phpunit/phpunit": "^5.7.22 || ^6.4.1", + "malukenho/docheader": "^0.1.6", + "phpunit/phpunit": "^7.0.1", "zendframework/zend-coding-standard": "~1.0.0", - "zendframework/zend-diactoros": "^1.0" + "zendframework/zend-diactoros": "^1.7.1" }, "suggest": { "psr/http-message-implementation": "Please install a psr/http-message-implementation to consume Stratigility; e.g., zendframework/zend-diactoros" @@ -575,13 +567,15 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.2.x-dev", - "dev-develop": "3.0.x-dev" + "dev-master": "3.0-dev", + "dev-develop": "3.1-dev", + "dev-release-3.0.0": "3.0.x-dev" } }, "autoload": { "files": [ "src/functions/double-pass-middleware.php", + "src/functions/host.php", "src/functions/middleware.php", "src/functions/path.php" ], @@ -593,16 +587,16 @@ "license": [ "BSD-3-Clause" ], - "description": "Middleware for PHP", - "homepage": "https://github.com/zendframework/zend-stratigility", + "description": "PSR-7 middleware foundation for building and dispatching middleware pipelines", "keywords": [ "ZendFramework", "http", "middleware", + "psr-15", "psr-7", "zf" ], - "time": "2018-03-12T21:04:19+00:00" + "time": "2018-02-26T15:56:54+00:00" } ], "packages-dev": [ @@ -849,6 +843,57 @@ ], "time": "2016-01-20T08:20:44+00:00" }, + { + "name": "jean85/pretty-package-versions", + "version": "1.1", + "source": { + "type": "git", + "url": "https://github.com/Jean85/pretty-package-versions.git", + "reference": "d457344b6a035ef99236bdda4729ad7eeb233f54" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/d457344b6a035ef99236bdda4729ad7eeb233f54", + "reference": "d457344b6a035ef99236bdda4729ad7eeb233f54", + "shasum": "" + }, + "require": { + "ocramius/package-versions": "^1.2.0", + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Jean85\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alessandro Lai", + "email": "alessandro.lai85@gmail.com" + } + ], + "description": "A wrapper for ocramius/pretty-package-versions to get pretty versions strings", + "keywords": [ + "composer", + "package", + "release", + "versions" + ], + "time": "2018-01-21T13:54:22+00:00" + }, { "name": "malukenho/docheader", "version": "0.1.7", @@ -935,80 +980,544 @@ }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Pádraic Brady", + "email": "padraic.brady@gmail.com", + "homepage": "http://blog.astrumfutura.com" + }, + { + "name": "Dave Marshall", + "email": "dave.marshall@atstsolutions.co.uk", + "homepage": "http://davedevelopment.co.uk" + } + ], + "description": "Mockery is a simple yet flexible PHP mock object framework for use in unit testing with PHPUnit, PHPSpec or any other testing framework. Its core goal is to offer a test double framework with a succinct API capable of clearly defining all possible object operations and interactions using a human readable Domain Specific Language (DSL). Designed as a drop in alternative to PHPUnit's phpunit-mock-objects library, Mockery is easy to integrate with PHPUnit and can operate alongside phpunit-mock-objects without the World ending.", + "homepage": "http://github.com/mockery/mockery", + "keywords": [ + "BDD", + "TDD", + "library", + "mock", + "mock objects", + "mockery", + "stub", + "test", + "test double", + "testing" + ], + "time": "2017-10-06T16:20:43+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.7.0", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e", + "reference": "3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0" + }, + "require-dev": { + "doctrine/collections": "^1.0", + "doctrine/common": "^2.6", + "phpunit/phpunit": "^4.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + }, + "files": [ + "src/DeepCopy/deep_copy.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "time": "2017-10-19T19:58:43+00:00" + }, + { + "name": "nette/bootstrap", + "version": "v2.4.5", + "source": { + "type": "git", + "url": "https://github.com/nette/bootstrap.git", + "reference": "804925787764d708a7782ea0d9382a310bb21968" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/bootstrap/zipball/804925787764d708a7782ea0d9382a310bb21968", + "reference": "804925787764d708a7782ea0d9382a310bb21968", + "shasum": "" + }, + "require": { + "nette/di": "~2.4.7", + "nette/utils": "~2.4", + "php": ">=5.6.0" + }, + "conflict": { + "nette/nette": "<2.2" + }, + "require-dev": { + "latte/latte": "~2.2", + "nette/application": "~2.3", + "nette/caching": "~2.3", + "nette/database": "~2.3", + "nette/forms": "~2.3", + "nette/http": "~2.4.0", + "nette/mail": "~2.3", + "nette/robot-loader": "^2.4.2 || ^3.0", + "nette/safe-stream": "~2.2", + "nette/security": "~2.3", + "nette/tester": "~2.0", + "tracy/tracy": "^2.4.1" + }, + "suggest": { + "nette/robot-loader": "to use Configurator::createRobotLoader()", + "tracy/tracy": "to use Configurator::enableTracy()" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.4-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0", + "GPL-3.0" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "🅱 Nette Bootstrap: the simple way to configure and bootstrap your Nette application.", + "homepage": "https://nette.org", + "keywords": [ + "bootstrapping", + "configurator", + "nette" + ], + "time": "2017-08-20T17:36:59+00:00" + }, + { + "name": "nette/di", + "version": "v2.4.10", + "source": { + "type": "git", + "url": "https://github.com/nette/di.git", + "reference": "a4b3be935b755f23aebea1ce33d7e3c832cdff98" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/di/zipball/a4b3be935b755f23aebea1ce33d7e3c832cdff98", + "reference": "a4b3be935b755f23aebea1ce33d7e3c832cdff98", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "nette/neon": "^2.3.3 || ~3.0.0", + "nette/php-generator": "^2.6.1 || ~3.0.0", + "nette/utils": "^2.4.3 || ~3.0.0", + "php": ">=5.6.0" + }, + "conflict": { + "nette/bootstrap": "<2.4", + "nette/nette": "<2.2" + }, + "require-dev": { + "nette/tester": "^2.0", + "tracy/tracy": "^2.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.4-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0", + "GPL-3.0" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "💎 Nette Dependency Injection Container: Flexible, compiled and full-featured DIC with perfectly usable autowiring and support for all new PHP 7.1 features.", + "homepage": "https://nette.org", + "keywords": [ + "compiled", + "di", + "dic", + "factory", + "ioc", + "nette", + "static" + ], + "time": "2017-08-31T22:42:00+00:00" + }, + { + "name": "nette/finder", + "version": "v2.4.1", + "source": { + "type": "git", + "url": "https://github.com/nette/finder.git", + "reference": "4d43a66d072c57d585bf08a3ef68d3587f7e9547" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/finder/zipball/4d43a66d072c57d585bf08a3ef68d3587f7e9547", + "reference": "4d43a66d072c57d585bf08a3ef68d3587f7e9547", + "shasum": "" + }, + "require": { + "nette/utils": "^2.4 || ~3.0.0", + "php": ">=5.6.0" + }, + "conflict": { + "nette/nette": "<2.2" + }, + "require-dev": { + "nette/tester": "^2.0", + "tracy/tracy": "^2.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.4-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0", + "GPL-3.0" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "Nette Finder: Files Searching", + "homepage": "https://nette.org", + "time": "2017-07-10T23:47:08+00:00" + }, + { + "name": "nette/neon", + "version": "v2.4.2", + "source": { + "type": "git", + "url": "https://github.com/nette/neon.git", + "reference": "9eacd50553b26b53a3977bfb2fea2166d4331622" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/neon/zipball/9eacd50553b26b53a3977bfb2fea2166d4331622", + "reference": "9eacd50553b26b53a3977bfb2fea2166d4331622", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "ext-json": "*", + "php": ">=5.6.0" + }, + "require-dev": { + "nette/tester": "~2.0", + "tracy/tracy": "^2.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.4-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0", + "GPL-3.0" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "Nette NEON: parser & generator for Nette Object Notation", + "homepage": "http://ne-on.org", + "time": "2017-07-11T18:29:08+00:00" + }, + { + "name": "nette/php-generator", + "version": "v3.0.2", + "source": { + "type": "git", + "url": "https://github.com/nette/php-generator.git", + "reference": "1652635d312a8db4291b16f3ebf87cb1a15a6257" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/php-generator/zipball/1652635d312a8db4291b16f3ebf87cb1a15a6257", + "reference": "1652635d312a8db4291b16f3ebf87cb1a15a6257", + "shasum": "" + }, + "require": { + "nette/utils": "^2.4.2 || ~3.0.0", + "php": ">=7.0" + }, + "conflict": { + "nette/nette": "<2.2" + }, + "require-dev": { + "nette/tester": "^2.0", + "tracy/tracy": "^2.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0", + "GPL-3.0" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "🐘 Nette PHP Generator: generates neat PHP code for you. Supports new PHP 7.2 features.", + "homepage": "https://nette.org", + "keywords": [ + "code", + "nette", + "php", + "scaffolding" + ], + "time": "2017-09-26T11:19:32+00:00" + }, + { + "name": "nette/robot-loader", + "version": "v3.0.3", + "source": { + "type": "git", + "url": "https://github.com/nette/robot-loader.git", + "reference": "92d4b40b49d5e2d9e37fc736bbcebe6da55fa44a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/robot-loader/zipball/92d4b40b49d5e2d9e37fc736bbcebe6da55fa44a", + "reference": "92d4b40b49d5e2d9e37fc736bbcebe6da55fa44a", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "nette/finder": "^2.3 || ^3.0", + "nette/utils": "^2.4 || ^3.0", + "php": ">=5.6.0" + }, + "conflict": { + "nette/nette": "<2.2" + }, + "require-dev": { + "nette/tester": "^2.0", + "tracy/tracy": "^2.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0", + "GPL-3.0" ], "authors": [ { - "name": "Pádraic Brady", - "email": "padraic.brady@gmail.com", - "homepage": "http://blog.astrumfutura.com" + "name": "David Grudl", + "homepage": "https://davidgrudl.com" }, { - "name": "Dave Marshall", - "email": "dave.marshall@atstsolutions.co.uk", - "homepage": "http://davedevelopment.co.uk" + "name": "Nette Community", + "homepage": "https://nette.org/contributors" } ], - "description": "Mockery is a simple yet flexible PHP mock object framework for use in unit testing with PHPUnit, PHPSpec or any other testing framework. Its core goal is to offer a test double framework with a succinct API capable of clearly defining all possible object operations and interactions using a human readable Domain Specific Language (DSL). Designed as a drop in alternative to PHPUnit's phpunit-mock-objects library, Mockery is easy to integrate with PHPUnit and can operate alongside phpunit-mock-objects without the World ending.", - "homepage": "http://github.com/mockery/mockery", + "description": "🍀 Nette RobotLoader: high performance and comfortable autoloader that will search and autoload classes within your application.", + "homepage": "https://nette.org", "keywords": [ - "BDD", - "TDD", - "library", - "mock", - "mock objects", - "mockery", - "stub", - "test", - "test double", - "testing" + "autoload", + "class", + "interface", + "nette", + "trait" ], - "time": "2017-10-06T16:20:43+00:00" + "time": "2017-09-26T13:42:21+00:00" }, { - "name": "myclabs/deep-copy", - "version": "1.7.0", + "name": "nette/utils", + "version": "v2.5.1", "source": { "type": "git", - "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e" + "url": "https://github.com/nette/utils.git", + "reference": "8a85ce76298c8a8941f912b8fa3ee93ca17d2ebc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e", - "reference": "3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e", + "url": "https://api.github.com/repos/nette/utils/zipball/8a85ce76298c8a8941f912b8fa3ee93ca17d2ebc", + "reference": "8a85ce76298c8a8941f912b8fa3ee93ca17d2ebc", "shasum": "" }, "require": { - "php": "^5.6 || ^7.0" + "php": ">=5.6.0" + }, + "conflict": { + "nette/nette": "<2.2" }, "require-dev": { - "doctrine/collections": "^1.0", - "doctrine/common": "^2.6", - "phpunit/phpunit": "^4.1" + "nette/tester": "~2.0", + "tracy/tracy": "^2.3" + }, + "suggest": { + "ext-gd": "to use Image", + "ext-iconv": "to use Strings::webalize() and toAscii()", + "ext-intl": "for script transliteration in Strings::webalize() and toAscii()", + "ext-json": "to use Nette\\Utils\\Json", + "ext-mbstring": "to use Strings::lower() etc...", + "ext-xml": "to use Strings::length() etc. when mbstring is not available" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.5-dev" + } + }, "autoload": { - "psr-4": { - "DeepCopy\\": "src/DeepCopy/" - }, + "classmap": [ + "src/" + ], "files": [ - "src/DeepCopy/deep_copy.php" + "src/loader.php" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause", + "GPL-2.0", + "GPL-3.0" ], - "description": "Create deep copies (clones) of your objects", - "keywords": [ - "clone", - "copy", - "duplicate", - "object", - "object graph" + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } ], - "time": "2017-10-19T19:58:43+00:00" + "description": "🛠 Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.", + "homepage": "https://nette.org", + "keywords": [ + "array", + "core", + "datetime", + "images", + "json", + "nette", + "paginator", + "password", + "slugify", + "string", + "unicode", + "utf-8", + "utility", + "validation" + ], + "time": "2018-02-19T14:42:42+00:00" }, { "name": "nikic/fast-route", @@ -1056,6 +1565,106 @@ ], "time": "2018-02-13T20:26:39+00:00" }, + { + "name": "nikic/php-parser", + "version": "v3.1.5", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "bb87e28e7d7b8d9a7fda231d37457c9210faf6ce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/bb87e28e7d7b8d9a7fda231d37457c9210faf6ce", + "reference": "bb87e28e7d7b8d9a7fda231d37457c9210faf6ce", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=5.5" + }, + "require-dev": { + "phpunit/phpunit": "~4.0|~5.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "time": "2018-02-28T20:30:58+00:00" + }, + { + "name": "ocramius/package-versions", + "version": "1.3.0", + "source": { + "type": "git", + "url": "https://github.com/Ocramius/PackageVersions.git", + "reference": "4489d5002c49d55576fa0ba786f42dbb009be46f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Ocramius/PackageVersions/zipball/4489d5002c49d55576fa0ba786f42dbb009be46f", + "reference": "4489d5002c49d55576fa0ba786f42dbb009be46f", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0.0", + "php": "^7.1.0" + }, + "require-dev": { + "composer/composer": "^1.6.3", + "ext-zip": "*", + "infection/infection": "^0.7.1", + "phpunit/phpunit": "^7.0.0" + }, + "type": "composer-plugin", + "extra": { + "class": "PackageVersions\\Installer", + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "PackageVersions\\": "src/PackageVersions" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "Composer plugin that provides efficient querying for installed package versions (no runtime IO)", + "time": "2018-02-05T13:05:30+00:00" + }, { "name": "phar-io/manifest", "version": "1.0.1", @@ -1373,42 +1982,194 @@ ], "time": "2018-02-19T10:16:54+00:00" }, + { + "name": "phpstan/phpdoc-parser", + "version": "0.2", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "02f909f134fe06f0cd4790d8627ee24efbe84d6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/02f909f134fe06f0cd4790d8627ee24efbe84d6a", + "reference": "02f909f134fe06f0cd4790d8627ee24efbe84d6a", + "shasum": "" + }, + "require": { + "php": "~7.0" + }, + "require-dev": { + "consistence/coding-standard": "^2.0.0", + "jakub-onderka/php-parallel-lint": "^0.9.2", + "phing/phing": "^2.16.0", + "phpstan/phpstan": "^0.9", + "phpunit/phpunit": "^6.3", + "slevomat/coding-standard": "^3.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "0.1-dev" + } + }, + "autoload": { + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", + "time": "2018-01-13T18:19:41+00:00" + }, + { + "name": "phpstan/phpstan", + "version": "0.9.2", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan.git", + "reference": "e59541bcc7cac9b35ca54db6365bf377baf4a488" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/e59541bcc7cac9b35ca54db6365bf377baf4a488", + "reference": "e59541bcc7cac9b35ca54db6365bf377baf4a488", + "shasum": "" + }, + "require": { + "jean85/pretty-package-versions": "^1.0.3", + "nette/bootstrap": "^2.4 || ^3.0", + "nette/di": "^2.4.7 || ^3.0", + "nette/robot-loader": "^3.0.1", + "nette/utils": "^2.4.5 || ^3.0", + "nikic/php-parser": "^3.1", + "php": "~7.0", + "phpstan/phpdoc-parser": "^0.2", + "symfony/console": "~3.2 || ~4.0", + "symfony/finder": "~3.2 || ~4.0" + }, + "require-dev": { + "consistence/coding-standard": "2.2.1", + "ext-gd": "*", + "ext-intl": "*", + "ext-mysqli": "*", + "jakub-onderka/php-parallel-lint": "^0.9.2", + "phing/phing": "^2.16.0", + "phpstan/phpstan-php-parser": "^0.9", + "phpstan/phpstan-phpunit": "^0.9.3", + "phpstan/phpstan-strict-rules": "^0.9", + "phpunit/phpunit": "^6.5.4", + "slevomat/coding-standard": "4.0.0" + }, + "bin": [ + "bin/phpstan" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "0.9-dev" + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": [ + "src/", + "build/PHPStan" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "time": "2018-01-28T13:22:19+00:00" + }, + { + "name": "phpstan/phpstan-strict-rules", + "version": "0.9", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-strict-rules.git", + "reference": "15be9090622c6b85c079922308f831018d8d9e23" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/15be9090622c6b85c079922308f831018d8d9e23", + "reference": "15be9090622c6b85c079922308f831018d8d9e23", + "shasum": "" + }, + "require": { + "php": "~7.0", + "phpstan/phpstan": "^0.9" + }, + "require-dev": { + "consistence/coding-standard": "^2.0.0", + "jakub-onderka/php-parallel-lint": "^0.9.2", + "phing/phing": "^2.16.0", + "phpstan/phpstan-phpunit": "^0.9", + "phpunit/phpunit": "^6.4", + "slevomat/coding-standard": "^3.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "0.9-dev" + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Extra strict and opinionated rules for PHPStan", + "time": "2017-11-26T20:12:30+00:00" + }, { "name": "phpunit/php-code-coverage", - "version": "5.3.0", + "version": "6.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "661f34d0bd3f1a7225ef491a70a020ad23a057a1" + "reference": "f8ca4b604baf23dab89d87773c28cc07405189ba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/661f34d0bd3f1a7225ef491a70a020ad23a057a1", - "reference": "661f34d0bd3f1a7225ef491a70a020ad23a057a1", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/f8ca4b604baf23dab89d87773c28cc07405189ba", + "reference": "f8ca4b604baf23dab89d87773c28cc07405189ba", "shasum": "" }, "require": { "ext-dom": "*", "ext-xmlwriter": "*", - "php": "^7.0", + "php": "^7.1", "phpunit/php-file-iterator": "^1.4.2", "phpunit/php-text-template": "^1.2.1", - "phpunit/php-token-stream": "^2.0.1", + "phpunit/php-token-stream": "^3.0", "sebastian/code-unit-reverse-lookup": "^1.0.1", "sebastian/environment": "^3.0", "sebastian/version": "^2.0.1", "theseer/tokenizer": "^1.1" }, "require-dev": { - "phpunit/phpunit": "^6.0" + "phpunit/phpunit": "^7.0" }, "suggest": { - "ext-xdebug": "^2.5.5" + "ext-xdebug": "^2.6.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.3.x-dev" + "dev-master": "6.0-dev" } }, "autoload": { @@ -1434,7 +2195,7 @@ "testing", "xunit" ], - "time": "2017-12-06T09:29:45+00:00" + "time": "2018-02-02T07:01:41+00:00" }, { "name": "phpunit/php-file-iterator", @@ -1526,28 +2287,28 @@ }, { "name": "phpunit/php-timer", - "version": "1.0.9", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f" + "reference": "8b8454ea6958c3dee38453d3bd571e023108c91f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", - "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/8b8454ea6958c3dee38453d3bd571e023108c91f", + "reference": "8b8454ea6958c3dee38453d3bd571e023108c91f", "shasum": "" }, "require": { - "php": "^5.3.3 || ^7.0" + "php": "^7.1" }, "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" + "phpunit/phpunit": "^7.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-master": "2.0-dev" } }, "autoload": { @@ -1562,7 +2323,7 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", + "email": "sebastian@phpunit.de", "role": "lead" } ], @@ -1571,33 +2332,33 @@ "keywords": [ "timer" ], - "time": "2017-02-26T11:10:40+00:00" + "time": "2018-02-01T13:07:23+00:00" }, { "name": "phpunit/php-token-stream", - "version": "2.0.2", + "version": "3.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-token-stream.git", - "reference": "791198a2c6254db10131eecfe8c06670700904db" + "reference": "21ad88bbba7c3d93530d93994e0a33cd45f02ace" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/791198a2c6254db10131eecfe8c06670700904db", - "reference": "791198a2c6254db10131eecfe8c06670700904db", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/21ad88bbba7c3d93530d93994e0a33cd45f02ace", + "reference": "21ad88bbba7c3d93530d93994e0a33cd45f02ace", "shasum": "" }, "require": { "ext-tokenizer": "*", - "php": "^7.0" + "php": "^7.1" }, "require-dev": { - "phpunit/phpunit": "^6.2.4" + "phpunit/phpunit": "^7.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-master": "3.0-dev" } }, "autoload": { @@ -1620,20 +2381,20 @@ "keywords": [ "tokenizer" ], - "time": "2017-11-27T05:48:46+00:00" + "time": "2018-02-01T13:16:43+00:00" }, { "name": "phpunit/phpunit", - "version": "6.5.7", + "version": "7.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "6bd77b57707c236833d2b57b968e403df060c9d9" + "reference": "e2f8aa21bc54b6ba218bdd4f9e0dac1e9bc3b4e9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/6bd77b57707c236833d2b57b968e403df060c9d9", - "reference": "6bd77b57707c236833d2b57b968e403df060c9d9", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/e2f8aa21bc54b6ba218bdd4f9e0dac1e9bc3b4e9", + "reference": "e2f8aa21bc54b6ba218bdd4f9e0dac1e9bc3b4e9", "shasum": "" }, "require": { @@ -1645,15 +2406,15 @@ "myclabs/deep-copy": "^1.6.1", "phar-io/manifest": "^1.0.1", "phar-io/version": "^1.0", - "php": "^7.0", + "php": "^7.1", "phpspec/prophecy": "^1.7", - "phpunit/php-code-coverage": "^5.3", + "phpunit/php-code-coverage": "^6.0", "phpunit/php-file-iterator": "^1.4.3", "phpunit/php-text-template": "^1.2.1", - "phpunit/php-timer": "^1.0.9", - "phpunit/phpunit-mock-objects": "^5.0.5", + "phpunit/php-timer": "^2.0", + "phpunit/phpunit-mock-objects": "^6.0", "sebastian/comparator": "^2.1", - "sebastian/diff": "^2.0", + "sebastian/diff": "^3.0", "sebastian/environment": "^3.1", "sebastian/exporter": "^3.1", "sebastian/global-state": "^2.0", @@ -1661,16 +2422,12 @@ "sebastian/resource-operations": "^1.0", "sebastian/version": "^2.0.1" }, - "conflict": { - "phpdocumentor/reflection-docblock": "3.0.2", - "phpunit/dbunit": "<3.0" - }, "require-dev": { "ext-pdo": "*" }, "suggest": { "ext-xdebug": "*", - "phpunit/php-invoker": "^1.1" + "phpunit/php-invoker": "^2.0" }, "bin": [ "phpunit" @@ -1678,7 +2435,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "6.5.x-dev" + "dev-master": "7.0-dev" } }, "autoload": { @@ -1704,33 +2461,30 @@ "testing", "xunit" ], - "time": "2018-02-26T07:01:09+00:00" + "time": "2018-02-26T07:03:12+00:00" }, { "name": "phpunit/phpunit-mock-objects", - "version": "5.0.6", + "version": "6.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", - "reference": "33fd41a76e746b8fa96d00b49a23dadfa8334cdf" + "reference": "e3249dedc2d99259ccae6affbc2684eac37c2e53" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/33fd41a76e746b8fa96d00b49a23dadfa8334cdf", - "reference": "33fd41a76e746b8fa96d00b49a23dadfa8334cdf", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/e3249dedc2d99259ccae6affbc2684eac37c2e53", + "reference": "e3249dedc2d99259ccae6affbc2684eac37c2e53", "shasum": "" }, "require": { "doctrine/instantiator": "^1.0.5", - "php": "^7.0", + "php": "^7.1", "phpunit/php-text-template": "^1.2.1", "sebastian/exporter": "^3.1" }, - "conflict": { - "phpunit/phpunit": "<6.0" - }, "require-dev": { - "phpunit/phpunit": "^6.5" + "phpunit/phpunit": "^7.0" }, "suggest": { "ext-soap": "*" @@ -1738,7 +2492,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0.x-dev" + "dev-master": "6.0.x-dev" } }, "autoload": { @@ -1763,7 +2517,7 @@ "mock", "xunit" ], - "time": "2018-01-06T05:45:45+00:00" + "time": "2018-02-15T05:27:38+00:00" }, { "name": "psr/log", @@ -1923,28 +2677,29 @@ }, { "name": "sebastian/diff", - "version": "2.0.1", + "version": "3.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "347c1d8b49c5c3ee30c7040ea6fc446790e6bddd" + "reference": "e09160918c66281713f1c324c1f4c4c3037ba1e8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/347c1d8b49c5c3ee30c7040ea6fc446790e6bddd", - "reference": "347c1d8b49c5c3ee30c7040ea6fc446790e6bddd", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/e09160918c66281713f1c324c1f4c4c3037ba1e8", + "reference": "e09160918c66281713f1c324c1f4c4c3037ba1e8", "shasum": "" }, "require": { - "php": "^7.0" + "php": "^7.1" }, "require-dev": { - "phpunit/phpunit": "^6.2" + "phpunit/phpunit": "^7.0", + "symfony/process": "^2 || ^3.3 || ^4" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-master": "3.0-dev" } }, "autoload": { @@ -1969,9 +2724,12 @@ "description": "Diff implementation", "homepage": "https://github.com/sebastianbergmann/diff", "keywords": [ - "diff" + "diff", + "udiff", + "unidiff", + "unified diff" ], - "time": "2017-08-03T08:09:46+00:00" + "time": "2018-02-01T13:45:15+00:00" }, { "name": "sebastian/environment", @@ -2746,35 +3504,41 @@ }, { "name": "zendframework/zend-expressive-aurarouter", - "version": "2.2.0", + "version": "3.0.0rc3", "source": { "type": "git", "url": "https://github.com/zendframework/zend-expressive-aurarouter.git", - "reference": "cb0d8b343f11d32f3171c558c7e9e42c16ff5d3b" + "reference": "1ee5c5d9bbd658803dd0a42d5180e9413dc34164" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-expressive-aurarouter/zipball/cb0d8b343f11d32f3171c558c7e9e42c16ff5d3b", - "reference": "cb0d8b343f11d32f3171c558c7e9e42c16ff5d3b", + "url": "https://api.github.com/repos/zendframework/zend-expressive-aurarouter/zipball/1ee5c5d9bbd658803dd0a42d5180e9413dc34164", + "reference": "1ee5c5d9bbd658803dd0a42d5180e9413dc34164", "shasum": "" }, "require": { - "aura/router": "^3.0.1", + "aura/router": "^3.1", "fig/http-message-util": "^1.1.2", - "php": "^5.6 || ^7.0", + "php": "^7.1", "psr/http-message": "^1.0.1", - "zendframework/zend-expressive-router": "^2.4" + "zendframework/zend-expressive-router": "^3.0.0rc4" }, "require-dev": { - "malukenho/docheader": "^0.1.5", - "phpunit/phpunit": "^5.7.23 || ^6.4.3", - "zendframework/zend-coding-standard": "~1.0.0" + "malukenho/docheader": "^0.1.6", + "phpunit/phpunit": "^7.0.2", + "zendframework/zend-coding-standard": "~1.0.0", + "zendframework/zend-diactoros": "^1.7", + "zendframework/zend-stratigility": "^3.0.0rc1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.2.x-dev", - "dev-develop": "3.0.x-dev" + "dev-master": "2.1.x-dev", + "dev-develop": "2.2.x-dev", + "dev-release-3.0.0": "3.0.x-dev" + }, + "zf": { + "config-provider": "Zend\\Expressive\\Router\\AuraRouter\\ConfigProvider" } }, "autoload": { @@ -2798,44 +3562,50 @@ "zend-expressive", "zf" ], - "time": "2018-03-08T17:22:09+00:00" + "time": "2018-03-07T17:14:23+00:00" }, { "name": "zendframework/zend-expressive-fastroute", - "version": "2.2.0", + "version": "3.0.0rc4", "source": { "type": "git", "url": "https://github.com/zendframework/zend-expressive-fastroute.git", - "reference": "e35c040c7b76fd03156e537053b3c05c700028dc" + "reference": "43fac9a08c4eb8587899fb1c6bb916ec8925f5d2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-expressive-fastroute/zipball/e35c040c7b76fd03156e537053b3c05c700028dc", - "reference": "e35c040c7b76fd03156e537053b3c05c700028dc", + "url": "https://api.github.com/repos/zendframework/zend-expressive-fastroute/zipball/43fac9a08c4eb8587899fb1c6bb916ec8925f5d2", + "reference": "43fac9a08c4eb8587899fb1c6bb916ec8925f5d2", "shasum": "" }, "require": { "fig/http-message-util": "^1.1.2", "nikic/fast-route": "^1.2", - "php": "^5.6 || ^7.0", + "php": "^7.1", "psr/container": "^1.0", "psr/http-message": "^1.0.1", - "zendframework/zend-expressive-router": "^2.4", - "zendframework/zend-stdlib": "^3.1 || 2.*" + "zendframework/zend-expressive-router": "^3.0.0rc4", + "zendframework/zend-stdlib": "^2.0 || ^3.1" }, "conflict": { "container-interop/container-interop": "<1.2.0" }, "require-dev": { - "malukenho/docheader": "^0.1.5", - "phpunit/phpunit": "^5.7.23 || ^6.4.3", - "zendframework/zend-coding-standard": "~1.0.0" + "malukenho/docheader": "^0.1.6", + "phpunit/phpunit": "^7.0.2", + "zendframework/zend-coding-standard": "~1.0.0", + "zendframework/zend-diactoros": "^1.7", + "zendframework/zend-stratigility": "^3.0.0rc1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.2.x-dev", - "dev-develop": "3.0.x-dev" + "dev-master": "2.1-dev", + "dev-develop": "2.2-dev", + "dev-release-3.0.0": "3.0.x-dev" + }, + "zf": { + "config-provider": "Zend\\Expressive\\Router\\FastRouteRouter\\ConfigProvider" } }, "autoload": { @@ -2859,41 +3629,46 @@ "zend-expressive", "zf" ], - "time": "2018-03-08T16:09:36+00:00" + "time": "2018-03-07T17:10:51+00:00" }, { "name": "zendframework/zend-expressive-zendrouter", - "version": "2.2.0", + "version": "3.0.0rc3", "source": { "type": "git", "url": "https://github.com/zendframework/zend-expressive-zendrouter.git", - "reference": "c7edcd47eb22842f7120432d068ef832077352d9" + "reference": "0a03edf71e89a78efee2fe18bfa092597738dbec" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-expressive-zendrouter/zipball/c7edcd47eb22842f7120432d068ef832077352d9", - "reference": "c7edcd47eb22842f7120432d068ef832077352d9", + "url": "https://api.github.com/repos/zendframework/zend-expressive-zendrouter/zipball/0a03edf71e89a78efee2fe18bfa092597738dbec", + "reference": "0a03edf71e89a78efee2fe18bfa092597738dbec", "shasum": "" }, "require": { "fig/http-message-util": "^1.1.2", - "php": "^5.6 || ^7.0", + "php": "^7.1", "psr/http-message": "^1.0.1", - "zendframework/zend-expressive-router": "^2.4", + "zendframework/zend-expressive-router": "^3.0.0rc3", "zendframework/zend-psr7bridge": "^0.2.2 || ^1.0.0", "zendframework/zend-router": "^3.0.2" }, "require-dev": { - "malukenho/docheader": "^0.1.5", - "phpunit/phpunit": "^5.7.23 || ^6.4.3", + "malukenho/docheader": "^0.1.6", + "phpunit/phpunit": "^7.0.2", "zendframework/zend-coding-standard": "~1.0.0", - "zendframework/zend-i18n": "^2.7.3" + "zendframework/zend-i18n": "^2.7.4", + "zendframework/zend-stratigility": "^3.0.0rc1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.2.x-dev", - "dev-develop": "3.0.x-dev" + "dev-master": "2.1.x-dev", + "dev-develop": "2.2.x-dev", + "dev-release-3.0.0": "3.0.x-dev" + }, + "zf": { + "config-provider": "Zend\\Expressive\\Router\\ZendRouter\\ConfigProvider" } }, "autoload": { @@ -2916,7 +3691,7 @@ "zend-expressive", "zf" ], - "time": "2018-03-08T17:32:50+00:00" + "time": "2018-03-07T17:17:26+00:00" }, { "name": "zendframework/zend-http", @@ -3361,11 +4136,18 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": { + "zendframework/zend-expressive-router": 5, + "zendframework/zend-expressive-template": 15, + "zendframework/zend-stratigility": 5, + "zendframework/zend-expressive-aurarouter": 5, + "zendframework/zend-expressive-fastroute": 5, + "zendframework/zend-expressive-zendrouter": 5 + }, "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": "^5.6 || ^7.0" + "php": "^7.1" }, "platform-dev": [] } diff --git a/docs/book/cookbook/autowiring-routes-and-pipelines.md b/docs/book/cookbook/autowiring-routes-and-pipelines.md index ee20e0df..edc157f3 100644 --- a/docs/book/cookbook/autowiring-routes-and-pipelines.md +++ b/docs/book/cookbook/autowiring-routes-and-pipelines.md @@ -1,6 +1,6 @@ - + diff --git a/docs/book/cookbook/common-prefix-for-routes.md b/docs/book/cookbook/common-prefix-for-routes.md index 33a493b7..80c7964e 100644 --- a/docs/book/cookbook/common-prefix-for-routes.md +++ b/docs/book/cookbook/common-prefix-for-routes.md @@ -1,8 +1,8 @@ - + diff --git a/docs/book/cookbook/debug-toolbars.md b/docs/book/cookbook/debug-toolbars.md index 3b6208ea..17ff1707 100644 --- a/docs/book/cookbook/debug-toolbars.md +++ b/docs/book/cookbook/debug-toolbars.md @@ -1,6 +1,6 @@ - + diff --git a/docs/book/cookbook/flash-messengers.md b/docs/book/cookbook/flash-messengers.md index 6d62fe8f..e8c89d55 100644 --- a/docs/book/cookbook/flash-messengers.md +++ b/docs/book/cookbook/flash-messengers.md @@ -1,6 +1,6 @@ - + diff --git a/docs/book/cookbook/passing-data-between-middleware.md b/docs/book/cookbook/passing-data-between-middleware.md index 4000aa98..84120a6b 100644 --- a/docs/book/cookbook/passing-data-between-middleware.md +++ b/docs/book/cookbook/passing-data-between-middleware.md @@ -1,6 +1,6 @@ - + diff --git a/docs/book/cookbook/route-specific-pipeline.md b/docs/book/cookbook/route-specific-pipeline.md index f816821e..c12594ff 100644 --- a/docs/book/cookbook/route-specific-pipeline.md +++ b/docs/book/cookbook/route-specific-pipeline.md @@ -1,6 +1,6 @@ - + diff --git a/docs/book/cookbook/setting-locale-depending-routing-parameter.md b/docs/book/cookbook/setting-locale-depending-routing-parameter.md index 4af73769..4bab0a7b 100644 --- a/docs/book/cookbook/setting-locale-depending-routing-parameter.md +++ b/docs/book/cookbook/setting-locale-depending-routing-parameter.md @@ -1,6 +1,6 @@ - + diff --git a/docs/book/cookbook/setting-locale-without-routing-parameter.md b/docs/book/cookbook/setting-locale-without-routing-parameter.md index 734cc7b6..feb8ec3e 100644 --- a/docs/book/cookbook/setting-locale-without-routing-parameter.md +++ b/docs/book/cookbook/setting-locale-without-routing-parameter.md @@ -1,6 +1,6 @@ - + diff --git a/docs/book/cookbook/using-a-base-path.md b/docs/book/cookbook/using-a-base-path.md index 0ca0c006..ec6c8f48 100644 --- a/docs/book/cookbook/using-a-base-path.md +++ b/docs/book/cookbook/using-a-base-path.md @@ -1,6 +1,6 @@ - + diff --git a/docs/book/cookbook/using-custom-view-helpers.md b/docs/book/cookbook/using-custom-view-helpers.md index 24bf8052..567ea8fa 100644 --- a/docs/book/cookbook/using-custom-view-helpers.md +++ b/docs/book/cookbook/using-custom-view-helpers.md @@ -1,6 +1,6 @@ - + diff --git a/docs/book/cookbook/using-routed-middleware-class-as-controller.md b/docs/book/cookbook/using-routed-middleware-class-as-controller.md index 5ee1aa68..7cfbc026 100644 --- a/docs/book/cookbook/using-routed-middleware-class-as-controller.md +++ b/docs/book/cookbook/using-routed-middleware-class-as-controller.md @@ -1,6 +1,6 @@ - + diff --git a/docs/book/cookbook/using-zend-form-view-helpers.md b/docs/book/cookbook/using-zend-form-view-helpers.md index 803ddfdb..04e0d841 100644 --- a/docs/book/cookbook/using-zend-form-view-helpers.md +++ b/docs/book/cookbook/using-zend-form-view-helpers.md @@ -1,6 +1,6 @@ - + diff --git a/docs/book/features/application.md b/docs/book/features/application.md index 86c699e8..ceba35b8 100644 --- a/docs/book/features/application.md +++ b/docs/book/features/application.md @@ -1,6 +1,6 @@ - + diff --git a/docs/book/features/container/aura-di.md b/docs/book/features/container/aura-di.md index 46d367af..8c8c63e6 100644 --- a/docs/book/features/container/aura-di.md +++ b/docs/book/features/container/aura-di.md @@ -1,6 +1,6 @@ - + diff --git a/docs/book/features/container/delegator-factories.md b/docs/book/features/container/delegator-factories.md index e5ec6587..24209f41 100644 --- a/docs/book/features/container/delegator-factories.md +++ b/docs/book/features/container/delegator-factories.md @@ -1,6 +1,6 @@ - + diff --git a/docs/book/features/container/factories.md b/docs/book/features/container/factories.md index 19e48de2..a33da2bd 100644 --- a/docs/book/features/container/factories.md +++ b/docs/book/features/container/factories.md @@ -1,6 +1,6 @@ - + diff --git a/docs/book/features/container/intro.md b/docs/book/features/container/intro.md index 36065673..66910650 100644 --- a/docs/book/features/container/intro.md +++ b/docs/book/features/container/intro.md @@ -1,6 +1,6 @@ - + diff --git a/docs/book/features/container/pimple.md b/docs/book/features/container/pimple.md index 63357718..0dc3a915 100644 --- a/docs/book/features/container/pimple.md +++ b/docs/book/features/container/pimple.md @@ -1,6 +1,6 @@ - + diff --git a/docs/book/features/container/zend-servicemanager.md b/docs/book/features/container/zend-servicemanager.md index 6d04cb97..eb1daca6 100644 --- a/docs/book/features/container/zend-servicemanager.md +++ b/docs/book/features/container/zend-servicemanager.md @@ -1,6 +1,6 @@ - + diff --git a/docs/book/features/emitters.md b/docs/book/features/emitters.md index 7f8c62df..fb29953a 100644 --- a/docs/book/features/emitters.md +++ b/docs/book/features/emitters.md @@ -1,6 +1,6 @@ - + diff --git a/docs/book/features/error-handling.md b/docs/book/features/error-handling.md index 149a1f2d..182afb4c 100644 --- a/docs/book/features/error-handling.md +++ b/docs/book/features/error-handling.md @@ -1,6 +1,6 @@ - + diff --git a/docs/book/features/helpers/body-parse.md b/docs/book/features/helpers/body-parse.md index 2161ecac..ea44d39e 100644 --- a/docs/book/features/helpers/body-parse.md +++ b/docs/book/features/helpers/body-parse.md @@ -1,6 +1,6 @@ - + diff --git a/docs/book/features/helpers/content-length.md b/docs/book/features/helpers/content-length.md index 6c357f54..1ca04357 100644 --- a/docs/book/features/helpers/content-length.md +++ b/docs/book/features/helpers/content-length.md @@ -1,6 +1,6 @@ - + diff --git a/docs/book/features/helpers/intro.md b/docs/book/features/helpers/intro.md index d2260cc8..a2503c5a 100644 --- a/docs/book/features/helpers/intro.md +++ b/docs/book/features/helpers/intro.md @@ -1,6 +1,6 @@ - + diff --git a/docs/book/features/helpers/server-url-helper.md b/docs/book/features/helpers/server-url-helper.md index 4252ab83..67585a7d 100644 --- a/docs/book/features/helpers/server-url-helper.md +++ b/docs/book/features/helpers/server-url-helper.md @@ -1,6 +1,6 @@ - + diff --git a/docs/book/features/helpers/url-helper.md b/docs/book/features/helpers/url-helper.md index f9e0b608..6170b241 100644 --- a/docs/book/features/helpers/url-helper.md +++ b/docs/book/features/helpers/url-helper.md @@ -1,6 +1,6 @@ - + diff --git a/docs/book/features/middleware-types.md b/docs/book/features/middleware-types.md index 5b171c17..0ea1e3cb 100644 --- a/docs/book/features/middleware-types.md +++ b/docs/book/features/middleware-types.md @@ -1,6 +1,6 @@ - + diff --git a/docs/book/features/middleware/implicit-methods-middleware.md b/docs/book/features/middleware/implicit-methods-middleware.md index 53c5f6d9..3fa5fbdc 100644 --- a/docs/book/features/middleware/implicit-methods-middleware.md +++ b/docs/book/features/middleware/implicit-methods-middleware.md @@ -1,6 +1,6 @@ - + diff --git a/docs/book/features/modular-applications.md b/docs/book/features/modular-applications.md index 5f36ce8f..4769bc3a 100644 --- a/docs/book/features/modular-applications.md +++ b/docs/book/features/modular-applications.md @@ -1,6 +1,6 @@ - + diff --git a/docs/book/features/router/aura.md b/docs/book/features/router/aura.md index bb67cd0a..6dd65ffc 100644 --- a/docs/book/features/router/aura.md +++ b/docs/book/features/router/aura.md @@ -1,6 +1,6 @@ - + diff --git a/docs/book/features/router/fast-route.md b/docs/book/features/router/fast-route.md index fea79dd2..1a2e354a 100644 --- a/docs/book/features/router/fast-route.md +++ b/docs/book/features/router/fast-route.md @@ -1,6 +1,6 @@ - + diff --git a/docs/book/features/router/interface.md b/docs/book/features/router/interface.md index 6e10e4bc..737c61fb 100644 --- a/docs/book/features/router/interface.md +++ b/docs/book/features/router/interface.md @@ -1,6 +1,6 @@ - + diff --git a/docs/book/features/router/intro.md b/docs/book/features/router/intro.md index 2c4e740f..b598a8c5 100644 --- a/docs/book/features/router/intro.md +++ b/docs/book/features/router/intro.md @@ -1,6 +1,6 @@ - + diff --git a/docs/book/features/router/piping.md b/docs/book/features/router/piping.md index cae4a12f..96e15412 100644 --- a/docs/book/features/router/piping.md +++ b/docs/book/features/router/piping.md @@ -1,6 +1,6 @@ - + diff --git a/docs/book/features/router/uri-generation.md b/docs/book/features/router/uri-generation.md index ce2436dc..cfaa71a0 100644 --- a/docs/book/features/router/uri-generation.md +++ b/docs/book/features/router/uri-generation.md @@ -1,6 +1,6 @@ - + diff --git a/docs/book/features/router/zf2.md b/docs/book/features/router/zf2.md index 3359c623..603fe9aa 100644 --- a/docs/book/features/router/zf2.md +++ b/docs/book/features/router/zf2.md @@ -1,6 +1,6 @@ - + diff --git a/docs/book/features/template/interface.md b/docs/book/features/template/interface.md index 07843e20..49bef67a 100644 --- a/docs/book/features/template/interface.md +++ b/docs/book/features/template/interface.md @@ -1,6 +1,6 @@ - + diff --git a/docs/book/features/template/intro.md b/docs/book/features/template/intro.md index a10369d2..b35d6533 100644 --- a/docs/book/features/template/intro.md +++ b/docs/book/features/template/intro.md @@ -1,6 +1,6 @@ - + diff --git a/docs/book/features/template/middleware.md b/docs/book/features/template/middleware.md index e14904d0..66f840b3 100644 --- a/docs/book/features/template/middleware.md +++ b/docs/book/features/template/middleware.md @@ -1,6 +1,6 @@ - + diff --git a/docs/book/features/template/plates.md b/docs/book/features/template/plates.md index ce9c5657..32e634e4 100644 --- a/docs/book/features/template/plates.md +++ b/docs/book/features/template/plates.md @@ -1,6 +1,6 @@ - + diff --git a/docs/book/features/template/twig.md b/docs/book/features/template/twig.md index bad0f8c4..d3eda7fb 100644 --- a/docs/book/features/template/twig.md +++ b/docs/book/features/template/twig.md @@ -1,6 +1,6 @@ - + diff --git a/docs/book/features/template/zend-view.md b/docs/book/features/template/zend-view.md index 5d4d70b1..dbf48908 100644 --- a/docs/book/features/template/zend-view.md +++ b/docs/book/features/template/zend-view.md @@ -1,6 +1,6 @@ - + diff --git a/docs/book/getting-started/features.md b/docs/book/getting-started/features.md index 16996869..193df5cb 100644 --- a/docs/book/getting-started/features.md +++ b/docs/book/getting-started/features.md @@ -1,6 +1,6 @@ - + diff --git a/docs/book/getting-started/skeleton.md b/docs/book/getting-started/skeleton.md index 0f347470..f36be001 100644 --- a/docs/book/getting-started/skeleton.md +++ b/docs/book/getting-started/skeleton.md @@ -1,6 +1,6 @@ - + diff --git a/docs/book/getting-started/standalone.md b/docs/book/getting-started/standalone.md index a4739304..f8dce4a5 100644 --- a/docs/book/getting-started/standalone.md +++ b/docs/book/getting-started/standalone.md @@ -1,6 +1,6 @@ - + diff --git a/docs/book/index.html b/docs/book/index.html index 73d38254..2cf82963 100644 --- a/docs/book/index.html +++ b/docs/book/index.html @@ -43,7 +43,7 @@

Middleware

Routing

- Route requests to middleware using the routing library of your choice. + Route requests to middleware using the routing library of your choice.

@@ -58,7 +58,7 @@

Dependency Injection

Make your code flexible and robust, using the - dependency injection container of your choice. + dependency injection container of your choice.

@@ -70,7 +70,7 @@

Dependency Injection

Templating

- Create templated responses, using + Create templated responses, using a variety of template engines.

@@ -83,7 +83,7 @@

Templating

Error Handling

- Handle errors gracefully, using + Handle errors gracefully, using templated error pages, whoops, or your own solution!

@@ -115,7 +115,7 @@

Get Started Now!

- Learn more + Learn more
@@ -126,7 +126,7 @@

Applications, Simplified


 $pathMiddleware = function (
     ServerRequestInterface $request,
-    DelegateInterface $delegate
+    RequestHandlerInterface $handler
 ) {
     $uri  = $request->getUri();
     $path = $uri->getPath();
@@ -141,15 +141,15 @@ 

Applications, Simplified

Learn more

Or use the menu to navigate to the section you're interested in.

diff --git a/docs/book/reference/cli-tooling.md b/docs/book/reference/cli-tooling.md index 00ada7f9..07088bea 100644 --- a/docs/book/reference/cli-tooling.md +++ b/docs/book/reference/cli-tooling.md @@ -1,6 +1,6 @@ - + diff --git a/docs/book/reference/expressive-projects.md b/docs/book/reference/expressive-projects.md index 7a4d5ff2..fb932aa0 100644 --- a/docs/book/reference/expressive-projects.md +++ b/docs/book/reference/expressive-projects.md @@ -1,6 +1,6 @@ - + diff --git a/docs/book/reference/migration/to-v2.md b/docs/book/reference/migration/to-v2.md index b5e0bdbc..9c61ae2f 100644 --- a/docs/book/reference/migration/to-v2.md +++ b/docs/book/reference/migration/to-v2.md @@ -1,6 +1,6 @@ - + diff --git a/docs/book/reference/usage-examples.md b/docs/book/reference/usage-examples.md index 8e50eb86..15f6c537 100644 --- a/docs/book/reference/usage-examples.md +++ b/docs/book/reference/usage-examples.md @@ -1,6 +1,6 @@ - + diff --git a/docs/book/v3/cookbook/autowiring-routes-and-pipelines.md b/docs/book/v3/cookbook/autowiring-routes-and-pipelines.md new file mode 100644 index 00000000..7837368a --- /dev/null +++ b/docs/book/v3/cookbook/autowiring-routes-and-pipelines.md @@ -0,0 +1,210 @@ +# How can I autowire routes and pipelines? + +Expressive 2.0 switched to _programmatic_ pipelines and routes, versus +_configuration-driven_ pipelines and routing as used in version 1. One drawback +is that with configuration-driven approaches, users could provide configuration +via a module `ConfigProvider`, and automatically expose new pipeline middleware +or routes; with a programmatic approach, this is no longer possible. + +Or is it? + +## Delegator Factories + +One possibility available to version 2 applications is to use _delegator +factories_ on the `Zend\Expressive\Application` instance in order to inject +these items. + +A _delegator factory_ is a factory that _delegates_ creation of an instance to a +callback, and then operates on that instance for the purpose of altering the +instance or providing a replacement (e.g., a decorator or proxy). The delegate +callback usually wraps a service factory, or, because delegator factories +_also_ return an instance, additional delegator factories. As such, you assign +delegator _factories_, plural, to instances, allowing multiple delegator +factories to intercept processing of the service initialization. + +For the purposes of this particular example, we will use delegator factories to +both _pipe_ middleware as well as _route_ middleware. + +To demonstrate, we'll take the default pipeline and routing from the skeleton +application, and provide it via a delegator factory instead. + +First, we'll create the class `App\Factory\PipelineAndRoutesDelegator`, with +the following contents: + +```php +pipe(ErrorHandler::class); + $app->pipe(ServerUrlMiddleware::class); + $app->pipeRoutingMiddleware(); + $app->pipe(ImplicitHeadMiddleware::class); + $app->pipe(ImplicitOptionsMiddleware::class); + $app->pipe(UrlHelperMiddleware::class); + $app->pipeDispatchMiddleware(); + $app->pipe(NotFoundHandler::class); + + // Setup routes: + $app->get('/', Action\HomePageAction::class, 'home'); + $app->get('/api/ping', Action\PingAction::class, 'api.ping'); + + return $app; + } +} +``` + +> ### Where to put the factory +> +> You will place the factory class in one of the following locations: +> +> - `src/App/Factory/PipelineAndRoutesDelegator.php` if using the default, flat, +> application structure. +> - `src/App/src/Factory/PipelineAndRoutesDelegator.php` if using the +> recommended, modular, application structure. + +Once you've created this, edit the class `App\ConfigProvider`; in it, we'll +update the `getDependencies()` method to add the delegator factory: + +```php +public function getDependencies() +{ + return [ + /* . . . */ + 'delegators' => [ + \Zend\Expressive\Application::class => [ + Factory\PipelineAndRoutesDelegator::class, + ], + ], + ]; +} +``` + +> ### Where is the ConfigProvider class? +> +> The `ConfigProvider` class is in one of the following locations: +> +> - `src/App/ConfigProvider.php` if using the default, flat, application +> structure. +> - `src/App/src/ConfigProvider.php` using the recommended, modular, application +> structure. + +> ### Why is an array assigned? +> +> As noted above in the description of delegator factories, since each delegator +> factory returns an instance, you can nest multiple delegator factories in +> order to shape initialization of a service. As such, they are assigned as an +> _array_ to the service. + +Once you've done this, you can remove: + +- `config/pipeline.php` +- `config/routes.php` +- The following lines from `public/index.php`: + + ```php + // Import programmatic/declarative middleware pipeline and routing + // configuration statements + require 'config/pipeline.php'; + require 'config/routes.php'; + ``` + +If you reload your application at this point, you should see that everything +continues to work as expected! + +## Caution: pipelines + +Using delegator factories is a nice way to keep your routing and pipeline +configuration close to the modules in which they are defined. However, there is +a caveat: you likely should **not** register pipeline middleware in a delegator +factory _other than within your root application module_. + +The reason for this is simple: pipelines are linear, and specific to your +application. If one module pipes in middleware, there's no guarantee it will be +piped before or after your main pipeline, and no way to pipe the middleware at a +position in the middle of the pipeline! + +As such: + +- Use a `config/pipeline.php` file for your pipeline, **OR** +- Ensure you only define the pipeline in a **single** delegator factory on your + `Application` instance. + +## Caution: third-party, distributed modules + +If you are developing a module to distribute as a package via +[Composer](https://getcomposer.org/), **you should not autowire any delegator +factories that inject pipeline middleware or routes in the `Application`**. + +Why? + +As noted in the above section, pipelines should be created exactly once, at +the application level. Registering pipeline middleware within a distributable +package will very likely not have the intended consequences. + +If you ship with pipeline middleware, we suggest that you: + +- Document the middleware, and where you anticipate it being used in the + middleware pipeline. +- Document how to add the middleware service to dependency configuration, or + provide the dependency configuration via your module's `ConfigProvider`. + +With regards to routes, there are other considerations: + +- Routes defined by the package might conflict with the application, or with + other packages used by the application. + +- Routing definitions are typically highly specific to the router implementation + in use. As an example, each of the currently supported router implementations + has a different syntax for placeholders: + + - `/user/:id` + "constraints" configuration to define constraints (zend-router) + - `/user/{id}` + "tokens" configuration to define constraints (Aura.Router) + - `/user/{id:\d+}` (FastRoute) + +- Your application may have specific routing considerations or design. + +You could, of course, detect what router is in use, and provide routing for each +known, supported router implementation within your delegator factory. We even +recommend doing exactly that. However, we note that such an approach does not +solve the other two points above. + +However, we still recommend _shipping_ a delegator factory that would register +your routes, since routes *are* often a part of module design; just **do not +autowire** that delegator factory. This way, end-users who *can* use the +defaults do not need to cut-and-paste routing definitions from your +documentation into their own applications; they will instead opt-in to your +delegator factory by wiring it into their own configuration. + +## Synopsis + +- We recommend using delegator factories for the purpose of autowiring routes, + and, with caveats, pipeline middleware: + - The pipeline should be created exactly once, so calls to `pipe()` should + occur in exactly _one_ delegator factory. +- Distributable packages should create a delegator factory for _routes only_, + but _should not_ register the delegator factory by default. diff --git a/docs/book/v3/cookbook/common-prefix-for-routes.md b/docs/book/v3/cookbook/common-prefix-for-routes.md new file mode 100644 index 00000000..b8605ada --- /dev/null +++ b/docs/book/v3/cookbook/common-prefix-for-routes.md @@ -0,0 +1,46 @@ +# How can I prepend a common path to all my routes? + +You may have multiple middleware in your project, each providing their own +functionality: + +```php +$middleware1 = new UserMiddleware(); +$middleware2 = new ProjectMiddleware(); + +$app = AppFactory::create(); +$app->pipe($middleware1); +$app->pipe($middleware2); + +$app->run(); +``` + +Let's assume the above represents an API. + +As your application progresses, you may have a mixture of different content, and now want to have +the above segregated under the path `/api`. + +This is essentially the same problem as addressed in the +["Segregating your application to a subpath"](../reference/usage-examples.md#segregating-your-application-to-a-subpath) example. + +To accomplish it: + +- Create a new application. +- Pipe the previous application to the new one, under the path `/api`. + +```php +$middleware1 = new UserMiddleware(); +$middleware2 = new ProjectMiddleware(); + +$api = AppFactory::create(); +$api->pipe($middleware1); +$api->pipe($middleware2); + +$app = AppFactory::create(); +$app->pipe('/api', $api); + +$app->run(); +``` + +The above works, because every `Application` instance is itself middleware, and, more specifically, +an instance of [Stratigility's `MiddlewarePipe`](https://github.com/zendframework/zend-stratigility/blob/master/doc/book/middleware.md), +which provides the ability to compose middleware. diff --git a/docs/book/v3/cookbook/debug-toolbars.md b/docs/book/v3/cookbook/debug-toolbars.md new file mode 100644 index 00000000..b98ebfa3 --- /dev/null +++ b/docs/book/v3/cookbook/debug-toolbars.md @@ -0,0 +1,80 @@ +# How can I get a debug toolbar for my Expressive application? + +Many modern frameworks and applications provide debug toolbars: in-browser +toolbars to provide profiling information of the request executed. These can +provide invaluable details into application objects, database queries, and more. +As an Expressive user, how can you get similar functionality? + +## Zend Server Z-Ray + +[Zend Server](http://www.zend.com/en/products/zend_server) ships with a tool +called [Z-Ray](http://www.zend.com/en/products/server/z-ray), which provides +both a debug toolbar and debug console (for API debugging). Z-Ray is also +currently [available as a standalone technology +preview](http://www.zend.com/en/products/z-ray/z-ray-preview), and can be added +as an extension to an existing PHP installation. + +When using Zend Server or the standalone Z-Ray, you do not need to make any +changes to your application whatsoever to benefit from it; you simply need to +make sure Z-Ray is enabled and/or that you've setup a security token to +selectively enable it on-demand. See the +[Z-Ray documentation](http://files.zend.com/help/Zend-Server/content/z-ray_concept.htm) +for full usage details. + +## bitExpert/prophiler-psr7-middleware + +Another option is bitExpert's [prophiler-psr7-middleware](https://github.com/bitExpert/prophiler-psr7-middleware). +This package wraps [fabfuel/prophiler](https://github.com/fabfuel/prophiler), +which provides a PHP-based profiling tool and toolbar; the bitExpert package +wraps this in PSR-7 middleware to make consumption in those paradigms trivial. + +To add the toolbar middleware to your application, use composer: + +```bash +$ composer require bitExpert/prophiler-psr7-middleware +``` + +From there, you will need to create a factory for the middleware, and add it to +your middleware pipeline. Stephan Hochdörfer, author of the package, has written +a [post detailing these steps](https://blog.bitexpert.de/blog/using-prophiler-with-zend-expressive/). + +> ### Use locally! +> +> One minor change we recommend over the directions Stephan provides is that you +> configure the factory and middleware in the +> `config/autoload/middleware-pipeline.local.php` file, vs the `.global` version. +> Doing so enables the middleware and toolbar only in the local environment +> — and not in production, where you likely do not want to expose such +> information! + +## php-middleware/php-debug-bar + +[php-middleware/php-debug-bar](https://github.com/php-middleware/phpdebugbar) +provides a PSR-7 middleware wrapper around [maximebf/php-debugbar](https://github.com/maximebf/php-debugbar), +a popular framework-agnostic debug bar for PHP projects. + +First, install the middleware in your application: + +```bash +$ composer require php-middleware/php-debug-bar +``` + +This package supplies a config provider, which could be added to your +`config/config.php` when using zend-config-aggregator or +expressive-config-manager. However, because it should only be enabled in +development, we recommend creating a "local" configuration file (e.g., +`config/autoload/php-debugbar.local.php`) when you need to enable it, with the +following contents: + +```php + ### Use locally! +> +> Remember to enable `PhpMiddleware\PhpDebugBar\ConfigProvider` only in your +> development enviroments! diff --git a/docs/book/v3/cookbook/flash-messengers.md b/docs/book/v3/cookbook/flash-messengers.md new file mode 100644 index 00000000..e32d785a --- /dev/null +++ b/docs/book/v3/cookbook/flash-messengers.md @@ -0,0 +1,267 @@ +# How Can I Implement Flash Messages? + +*Flash messages* are used to display one-time messages to a user. A typical use +case is for setting and later displaying a successful submission via a +[Post/Redirect/Get (PRG)](https://en.wikipedia.org/wiki/Post/Redirect/Get) +workflow, where the flash message would be set during the POST request, but +displayed during the GET request. (PRG is used to prevent double-submission of +forms.) As such, flash messages usually are session-based; the message is set in +one request, and accessed and cleared in another. + +Expressive does not provide native session facilities out-of-the-box, which +means you will need: + +- Session functionality. +- Flash message functionality, for handling message expiry from the session + after first access. + +A number of flash message libraries already exist that can be integrated via +middleware, and these typically either use PHP's ext/session functionality or +have a dependency on a session library. Two such libraries are slim/flash and +damess/expressive-session-middleware. + +## slim/flash + +Slim's [Flash messages service provider](https://github.com/slimphp/Slim-Flash) can be +used in Expressive. It uses PHP's native session support. + +First, you'll need to add it to your application: + +```bash +$ composer require slim/flash +``` + +Second, create middleware that will add the flash message provider to the request: + +```php +handle( + $request->withAttribute('flash', new Messages()) + ); + } +} +``` + +Third, we will register the new middleware with our container as an invokable. +Edit either the file `config/autoload/dependencies.global.php` or +`config/autoload/middleware-pipeline.global.php` to add the following: + +```php +return [ + 'dependencies' => [ + 'invokables' => [ + App\SlimFlashMiddleware::class => App\SlimFlashMiddleware::class, + /* ... */ + ], + /* ... */ + ], +]; +``` + +Finally, let's register it with our middleware pipeline. For programmatic +pipelines, pipe the middleware somewhere, generally before the routing middleware: + +```php +$app->pipe(App\SlimFlashMiddleware::class); +``` + +Or as part of a routed middleware pipeline: + +```php +$app->post('/form/handler', [ + App\SlimFlashMiddleware::class, + FormHandlerMiddleware::class, +]); +``` + +If using configuration-driven pipelines, edit +`config/autoload/middleware-pipeline.global.php` to make the following +additions: + +```php +return [ + 'middleware_pipeline' => [ + 'always' => [ + 'middleware' => [ + 'App\SlimFlashMiddleware', + /* ... */ + ], + 'priority' => 10000, + ], + /* ... */ + ], +]; +``` + +> ### Where to register the flash middleware +> +> Sessions can sometimes be expensive. As such, you may not want the flash +> middleware enabled for every request. If this is the case, add the flash +> middleware as part of a route-specific pipeline instead, as demonstrated +> in the programmatic pipelines above. + +From here, you can add and read messages by accessing the request's flash +attribute. As an example, middleware generating messages might read as follows: + +```php +use Psr\Http\Server\RequestHandlerInterface; +use Zend\Diactoros\Response\RedirectResponse; + +function($request, RequestHandlerInterface $handler) +{ + $flash = $request->getAttribute('flash'); + $flash->addMessage('message', 'Hello World!'); + + return new RedirectResponse('/other-middleware'); +} +``` + +And middleware consuming the message might read: + +```php +use Psr\Http\Server\RequestHandlerInterface; + +function($request, RequestHandlerInterface $handler) +{ + $flash = $request->getAttribute('flash'); + $messages = $flash->getMessages(); + // ... +} +``` + +From there, it's a matter of providing the flash messages to your template. + +## damess/expressive-session-middleware and Aura.Session + +[damess/expressive-session-middleware](https://github.com/dannym87/expressive-session-middleware) +provides middleware for initializing an +[Aura.Session](https://github.com/auraphp/Aura.Session) instance; Aura.Session +provides flash messaging capabilities as part of its featureset. + +Install it via Composer: + +```bash +$ composer require damess/expressive-session-middleware +``` + +In `config/autoload/dependencies.global.php`, add an entry for Aura.Session: + +```php +return [ + 'dependencies' => [ + 'factories' => [ + Aura\Session\Session::class => DaMess\Factory\AuraSessionFactory::class, + /* ... */ + ], + /* ... */ + ], +]; +``` + +In either `config/autoload/dependencies.global.php` or +`config/autoload/middleware-pipeline.global.php`, add a factory entry for the +`damess/expressive-session-middleware`: + +```php +return [ + 'dependencies' => [ + 'factories' => [ + DaMess\Http\SessionMiddleware::class => DaMess\Factory\SessionMiddlewareFactory::class, + /* ... */ + ], + /* ... */ + ], +]; +``` + +Finally, add it to your middleware pipeline. For programmatic pipelines: + +```php +use DaMess\Http\SessionMiddleware; + +$app->pipe(SessionMiddleware::class); +/* ... */ +``` + +If using configuration-driven pipelines, edit `config/autoload/middleware-pipeline.global.php` +and add an entry for the new middleware: + +```php +return [ + 'middleware_pipeline' => [ + 'always' => [ + 'middleware' => [ + DaMess\Http\SessionMiddleware::class, + /* ... */ + ], + 'priority' => 10000, + ], + /* ... */ + ], +]; +``` + +> ### Where to register the session middleware +> +> Sessions can sometimes be expensive. As such, you may not want the session +> middleware enabled for every request. If this is the case, add the session +> middleware as part of a route-specific pipeline instead. + +Once enabled, the `SessionMiddleware` will inject the Aura.Session instance into +the request as the `session` attribute; you can thus retrieve it within +middleware using the following: + +```php +$session = $request->getAttribute('session'); +``` + +To create and consume flash messages, use Aura.Session's +[flash values](https://github.com/auraphp/Aura.Session#flash-values). As +an example, the middleware that is processing a POST request might set a flash +message: + +```php +use Psr\Http\Server\RequestHandlerInterface; +use Zend\Diactoros\Response\RedirectResponse; + +function($request, RequestHandlerInterface $handler) +{ + $session = $request->getAttribute('session'); + $session->getSegment(__NAMESPACE__) + ->setFlash('message', 'Hello World!'); + + return new RedirectResponse('/other-middleware'); +} +``` + +Another middleware, to which the original middleware redirects, might look like +this: + +```php +use Psr\Http\Server\RequestHandlerInterface; + +function($request, RequestHandlerInterface $handler) +{ + $session = $request->getAttribute('session'); + $message = $session->getSegment(__NAMESPACE__) + ->getFlash('message'); + // ... +} +``` + +From there, it's a matter of providing the flash messages to your template. diff --git a/docs/book/v3/cookbook/passing-data-between-middleware.md b/docs/book/v3/cookbook/passing-data-between-middleware.md new file mode 100644 index 00000000..a1460565 --- /dev/null +++ b/docs/book/v3/cookbook/passing-data-between-middleware.md @@ -0,0 +1,120 @@ +# Passing Data Between Middleware + +A frequently asked question is how to pass data between middleware. + +The answer is present in every middleware: via request object attributes. + +Middleware is always executed in the order in which it is piped to the +application. This way you can ensure the request object in middleware receiving +data contains an attribute containing data passed by outer middleware. + +In the following example, `PassingDataMiddleware` prepares data to pass as a +request attribute to nested middleware. We use the fully qualified class name +for the attribute name to ensure uniqueness, but you can name it anything you +want. + +```php +namespace App\Middleware; + +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\MiddlewareInterface; +use Psr\Http\Server\RequestHandlerInterface; + +class PassingDataMiddleware implements MiddlewareInterface +{ + // ... + + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface + { + // Step 1: Do something first + $data = [ + 'foo' => 'bar', + ]; + + // Step 2: Inject data into the request, call the next middleware and wait for the response + + // Expressive 3.X: + $response = $handler->handle($request->withAttribute(self::class, $data)); + + // Expressive 2.X: + $response = $handler->process($request->withAttribute(self::class, $data)); + + // Step 3: Optionally, do something (with the response) before returning the response + + // Step 4: Return the response + return $response; + } +} +``` + +Later, `ReceivingDataMiddleware` grabs the data and processes it: + +```php +namespace App\Middleware; + +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\MiddlewareInterface; +use Psr\Http\Server\RequestHandlerInterface; + +class ReceivingDataMiddleware implements MiddlewareInterface +{ + // ... + + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface + { + // Step 1: Grab the data from the request and use it + $data = $request->getAttribute(PassingDataMiddleware::class); + + // Step 2: Call the next middleware and wait for the response + + // Expressive 3.X: + $response = $handler->handle($request); + + // Expressive 2.X: + $response = $handler->process($request); + + // Step 3: Optionally, do something (with the response) before returning the response + + // Step 4: Return the response + return $response; + } +} +``` + +Of course, you could also use the data in routed middleware, which is usually at +the innermost layer of your application. The `ExampleAction` below takes that +information and passes it to the template renderer to create an `HtmlResponse`: + +```php +namespace App\Action; + +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\MiddlewareInterface; +use Psr\Http\Server\RequestHandlerInterface; +use Zend\Diactoros\Response\HtmlResponse; + +class ExampleAction implements MiddlewareInterface +{ + // ... + + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface + { + // Step 1: Grab the data from the request + $data = $request->getAttribute(PassingDataMiddleware::class); + $id = $request->getAttribute('id'); + + // Step 2: Do some more stuff + + // Step 3: Return a Response + return new HtmlResponse( + $this->templateRenderer->render('blog::entry', [ + 'data' => $data, + 'id' => $id, + ]) + ); + } +} +``` diff --git a/docs/book/v3/cookbook/route-specific-pipeline.md b/docs/book/v3/cookbook/route-specific-pipeline.md new file mode 100644 index 00000000..706b5b25 --- /dev/null +++ b/docs/book/v3/cookbook/route-specific-pipeline.md @@ -0,0 +1,200 @@ +# How can I specify a route-specific middleware pipeline? + +Sometimes you may want to use a middleware pipeline only if a particular route +is matched. As an example, for an API resource, you might want to: + +- check for authentication credentials +- check for authorization for the selected action +- parse the incoming body +- validate the parsed body parameters + +*before* you actually execute the selected middleware. The above might each be +encapsulated as discrete middleware, but should be executed within the routed +middleware's context. + +You can accomplish this in one of two ways: + +- Have your middleware service resolve to a `MiddlewarePipe` instance that + composes the various middlewares. +- Specify an array of middlewares (either as actual instances, or as container + service names); this effectively creates and returns a `MiddlewarePipe`. + +## Resolving to a MiddlewarePipe + +You can do this programmatically within a container factory, assuming you are +using a container that supports factories. + +```php +use Psr\Container\ContainerInterface; +use Zend\Stratigility\MiddlewarePipe; + +class ApiResourcePipelineFactory +{ + public function __invoke(ContainerInterface $container) + { + $pipeline = new MiddlewarePipe(); + + // These correspond to the bullet points above + $pipeline->pipe($container->get('AuthenticationMiddleware')); + $pipeline->pipe($container->get('AuthorizationMiddleware')); + $pipeline->pipe($container->get('BodyParsingMiddleware')); + $pipeline->pipe($container->get('ValidationMiddleware')); + + // This is the actual middleware you're routing to. + $pipeline->pipe($container->get('ApiResource')); + + return $pipeline; + } +} +``` + +This gives you full control over the creation of the pipeline. You would, +however, need to ensure that you map the middleware to the pipeline factory when +setting up your container configuration. + +One alternative when using zend-servicemanager is to use a [delegator factory](https://docs.zendframework.com/zend-servicemanager/delegators/). +Delegator factories allow you to decorate the primary factory used to create the +middleware in order to change the instance or return an alternate instance. In +this case, we'd do the latter. The following is an example: + +```php +use Psr\Container\ContainerInterface; +use Zend\ServiceManager\DelegatorFactoryInterface; +use Zend\ServiceManager\ServiceLocatorInterface; +use Zend\Stratigility\MiddlewarePipe; + +class ApiResourcePipelineDelegatorFactory implements DelegatorFactoryInterface +{ + /** + * zend-servicemanager v3 support + */ + public function __invoke( + ContainerInterface $container, + $name, + callable $callback, + array $options = null + ) { + $pipeline = new MiddlewarePipe(); + + // These correspond to the bullet points above + $pipeline->pipe($container->get('AuthenticationMiddleware')); + $pipeline->pipe($container->get('AuthorizationMiddleware')); + $pipeline->pipe($container->get('BodyParsingMiddleware')); + $pipeline->pipe($container->get('ValidationMiddleware')); + + // This is the actual middleware you're routing to. + $pipeline->pipe($callback()); + + return $pipeline; + } + + /** + * zend-servicemanager v2 support + */ + public function createDelegatorWithName( + ServiceLocatorInterface $container, + $name, + $requestedName, + $callback + ) { + return $this($container, $name, $callback); + } +} +``` + +When configuring the container, you'd do something like the following: + +```php +return [ + 'dependencies' => [ + 'factories' => [ + 'AuthenticationMiddleware' => '...', + 'AuthorizationMiddleware' => '...', + 'BodyParsingMiddleware' => '...', + 'ValidationMiddleware' => '...', + 'ApiResourceMiddleware' => '...', + ], + 'delegators' => [ + 'ApiResourceMiddleware' => [ + 'ApiResourcePipelineDelegatorFactory', + ], + ], + ], +]; +``` + +This approach allows you to cleanly separate the factory for your middleware +from the pipeline you want to compose it in, and allows you to re-use the +pipeline creation across multiple middleware if desired. + +## Middleware Arrays + +If you'd rather not create a factory for each such middleware, the other option +is to use arrays of middlewares in your configuration or when routing manually. + +Via configuration looks like this: + +```php +return [ + 'routes' => [ + [ + 'name' => 'api-resource', + 'path' => '/api/resource[/{id:[a-f0-9]{32}}]', + 'allowed_methods' => ['GET', 'POST', 'PATCH', 'DELETE'], + 'middleware' => [ + 'AuthenticationMiddleware', + 'AuthorizationMiddleware', + 'BodyParsingMiddleware', + 'ValidationMiddleware', + 'ApiResourceMiddleware', + ], + ], + ], +]; +``` + +Manual routing looks like this: + +```php +$app->route('/api/resource[/{id:[a-f0-9]{32}}]', [ + 'AuthenticationMiddleware', + 'AuthorizationMiddleware', + 'BodyParsingMiddleware', + 'ValidationMiddleware', + 'ApiResourceMiddleware', +], ['GET', 'POST', 'PATCH', 'DELETE'], 'api-resource'); +``` + +When either of these approaches are used, the individual middleware listed +**MUST** be one of the following: + +- an instance of `Interop\Http\ServerMiddleware\MiddlewareInterface`; +- a callable middleware (will be decorated as interop middleware); +- a service name of middleware available in the container; +- a fully qualified class name of a directly instantiable (no constructor + arguments) middleware class. + +This approach is essentially equivalent to creating a factory that returns a +middleware pipeline. + +## What about pipeline middleware configuration? + +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' => [ + 'api' => [ + 'path' => '/api', + 'middleware' => [ + 'AuthenticationMiddleware', + 'AuthorizationMiddleware', + 'BodyParsingMiddleware', + 'ValidationMiddleware', + ], + 'priority' => 100, + ], + ], +]; +``` diff --git a/docs/book/v3/cookbook/setting-locale-depending-routing-parameter.md b/docs/book/v3/cookbook/setting-locale-depending-routing-parameter.md new file mode 100644 index 00000000..8e020ec0 --- /dev/null +++ b/docs/book/v3/cookbook/setting-locale-depending-routing-parameter.md @@ -0,0 +1,222 @@ +# How can I setup the locale depending on a routing parameter? + +Localized web applications often set the locale (and therefor the language) +based on a routing parameter, the session, or a specialized sub-domain. +In this recipe we will concentrate on using a routing parameter. + +> ### Routing parameters +> +> 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 +> tedious quickly. In such a case, you may want to look at the related recipe +> on [setting the locale without routing parameters](setting-locale-without-routing-parameter.md). + +## Setting up the route + +If you want to set the locale depending on an routing parameter, you first have +to add a locale parameter to each route that requires localization. + +In the following examples, we use the `locale` parameter, which should consist +of two lowercase alphabetical characters. + +### Dependency configuration + +The examples assume the following middleware dependency configuration: + +```php +use Application\Action; + +return [ + 'dependencies' => [ + 'factories' => [ + Action\HomePageAction::class => Action\HomePageFactory::class, + Action\ContactPageAction::class => Action\ContactPageFactory::class, + ], + ], +]; +``` + +### Programmatic routes + +The following describes routing configuration for use when using a +programmatic application. + +```php +use Application\Action\ContactPageAction; +use Application\Action\HomePageAction; + +$localeOptions = ['locale' => '[a-z]{2,3}([-_][a-zA-Z]{2}|)']; + +$app->get('/:locale', HomePageAction::class, 'home') + ->setOptions($localeOptions); +$app->get('/:locale/contact', ContactPageAction::class, 'contact') + ->setOptions($localeOptions); +``` + +### Configuration-based routes + +The following describes routing configuration for use when using a +configuration-driven application. + +```php +return [ + 'routes' => [ + [ + 'name' => 'home', + 'path' => '/:locale', + 'middleware' => Application\Action\HomePageAction::class, + 'allowed_methods' => ['GET'], + 'options' => [ + 'constraints' => [ + 'locale' => '[a-z]{2,3}([-_][a-zA-Z]{2}|)', + ], + ], + ], + [ + 'name' => 'contact', + 'path' => '/:locale/contact', + 'middleware' => Application\Action\ContactPageAction::class, + 'allowed_methods' => ['GET'], + 'options' => [ + 'constraints' => [ + 'locale' => '[a-z]{2,3}([-_][a-zA-Z]{2}|)', + ], + ], + ], + ], +]; +``` +> ### Note: Routing may differ based on router +> +> The routing examples in this recipe use syntax for the zend-mvc router, and, +> as such, may not work in your application. +> +> For Aura.Router, the 'home' route as listed above would read: +> +> ```php +> [ +> 'name' => 'home', +> 'path' => '/{locale}', +> 'middleware' => Application\Action\HomePageAction::class, +> 'allowed_methods' => ['GET'], +> 'options' => [ +> 'constraints' => [ +> 'tokens' => [ +> 'locale' => '[a-z]{2,3}([-_][a-zA-Z]{2}|)', +> ], +> ], +> ], +> ] +> ``` +> +> For FastRoute: +> +> ```php +> [ +> 'name' => 'home', +> 'path' => '/{locale:[a-z]{2,3}([-_][a-zA-Z]{2}|)}', +> 'middleware' => Application\Action\HomePageAction::class, +> 'allowed_methods' => ['GET'], +> ] +> ``` +> +> As such, be aware as you read the examples that you might not be able to +> simply cut-and-paste them without modification. + + +## 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 localization middleware that acts on the route result, and +registered in the pipeline immediately following the routing middleware. + +Such a `LocalizationMiddleware` class could look similar to this: + +```php +getAttribute( + 'locale', + Locale::acceptFromHttp( + $request->getServerParams()['HTTP_ACCEPT_LANGUAGE'] ?? 'en_US' + ) + ); + + // Store the locale as a request attribute + return $handler->handle($request->withAttribute(self::LOCALIZATION_ATTRIBUTE, $locale)); + } +} +``` + +> ### Locale::setDefault is unsafe +> +> Do not use `Locale::setDefault($locale)` to set a global static locale. +> PSR-7 apps may run in async processes, which could lead to another process +> overwriting the value, and thus lead to unexpected results for your users. +> +> Use a request parameter as detailed above instead, as the request is created +> specific to each process. + +Register this new middleware in either `config/autoload/middleware-pipeline.global.php` +or `config/autoload/dependencies.global.php`: + +```php +return [ + 'dependencies' => [ + 'invokables' => [ + LocalizationMiddleware::class => LocalizationMiddleware::class, + /* ... */ + ], + /* ... */ + ], +]; +``` + +If using a programmatic pipeline, pipe it immediately after your routing middleware: + +```php +use Application\I18n\LocalizationMiddleware; + +/* ... */ +$app->pipeRoutingMiddleware(); +$app->pipe(LocalizationMiddleware::class); +/* ... */ +``` + +If using a configuration-driven application, register it within your +`config/autoload/middleware-pipeline.global.php` file, injecting it +into the pipeline following the routing middleware: + +```php +return [ + 'middleware_pipeline' => [ + /* ... */ + [ + 'middleware' => [ + Zend\Expressive\Container\ApplicationFactory::ROUTING_MIDDLEWARE, + Helper\UrlHelperMiddleware::class, + LocalizationMiddleware::class, + Zend\Expressive\Container\ApplicationFactory::DISPATCH_MIDDLEWARE, + ], + 'priority' => 1, + ], + /* ... */ + ], +]; +``` diff --git a/docs/book/v3/cookbook/setting-locale-without-routing-parameter.md b/docs/book/v3/cookbook/setting-locale-without-routing-parameter.md new file mode 100644 index 00000000..35d237b0 --- /dev/null +++ b/docs/book/v3/cookbook/setting-locale-without-routing-parameter.md @@ -0,0 +1,228 @@ +# How can I setup the locale without routing parameters? + +Localized web applications often set the locale (and therefore the language) +based on a routing parameter, the session, or a specialized sub-domain. +In this recipe we will concentrate on introspecting the URI path via middleware, +which allows you to have a global mechanism for detecting the locale without +requiring any changes to existing routes. + +> ## Distinguishing between routes that require localization +> +> If your application has a mixture of routes that require localization, and +> those that do not, the solution in this recipe may lead to multiple URIs +> that resolve to the identical action, which may be undesirable. In such +> cases, you may want to prefix the specific routes that require localization +> 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 locale from the URI + +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 value to setup the locale. It also: + +- amends the request with a truncated path (removing the locale segment). +- adds the locale segment as the base path of the `UrlHelper`. + +```php +helper = $helper; + } + + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface + { + $uri = $request->getUri(); + + $path = $uri->getPath(); + + if (! preg_match('#^/(?P[a-z]{2,3}([-_][a-zA-Z]{2}|))/#', $path, $matches)) { + Locale::setDefault('de_DE'); + // Expressive 3.X: + return $handler->handle($request); + } + + $locale = $matches['locale']; + Locale::setDefault(Locale::canonicalize($locale)); + $this->helper->setBasePath($locale); + + return $handler->handle($request->withUri( + $uri->withPath(substr($path, 3)) + )); + } +} +``` + +Then you will need a factory for the `SetLocaleMiddleware` to inject the +`UrlHelper` instance. + +```php +get(UrlHelper::class) + ); + } +} +``` + +Next, map the middleware to its factory in either +`/config/autoload/dependencies.global.php` or +`/config/autoload/middleware-pipeline.global.php`: + +```php +use Application\I18n\SetLocaleMiddleware; +use Application\I18n\SetLocaleMiddlewareFactory; + +return [ + 'dependencies' => [ + /* ... */ + 'factories' => [ + SetLocaleMiddleware::class => SetLocaleMiddlewareFactory::class, + /* ... */ + ], + ], +]; +``` + +Finally, you will need to configure your middleware pipeline to ensure this +middleware is executed on every request. + +If using a programmatic pipeline: + +```php +use Application\I18n\SetLocaleMiddleware; +use Zend\Expressive\Helper\UrlHelperMiddleware; + +/* ... */ +$app->pipe(SetLocaleMiddleware::class); +/* ... */ +$app->pipeRoutingMiddleware(); +$app->pipe(UrlHelperMiddleware::class); +$app->pipeDispatchMiddleware(); +/* ... */ +``` + +If using a configuration-driven application, update +`/config/autoload/middleware-pipeline.global.php` to add the middleware: + +```php +return [ + 'middleware_pipeline' => [ + [ + 'middleware' => [ + Application\I18n\SetLocaleMiddleware::class, + /* ... */ + ], + 'priority' => 1000, + ], + + /* ... */ + + '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 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. + +```php +url('your-route') ?> +``` + +> ### Helpers differ between template renderers +> +> The above example is specific to zend-view; syntax will differ for +> Twig and Plates. + +## Redirecting within your request handlers + +If you want to add the locale parameter when creating URIs within your +request handlers, you just need to inject the `UrlHelper` into your +handler and use it for URL generation: + +```php +helper = $helper; + } + + public function handle(ServerRequestInterface $request) : RedirectResponse + { + $routeParams = [ /* ... */ ]; + + return new RedirectResponse( + $this->helper->generate('your-route', $routeParams) + ); + } +} +``` + +Injecting the `UrlHelper` into your request handler will also require that the +handler have a factory that manages the injection. As an example, the following +would work for the above middleware: + +```php +namespace Application\Action; + +use Psr\Container\ContainerInterface; +use Zend\Expressive\Helper\UrlHelper; + +class RedirectActionFactory +{ + public function __invoke(ContainerInterface $container) + { + return new RedirectAction( + $container->get(UrlHelper::class) + ); + } +} +``` diff --git a/docs/book/v3/cookbook/using-a-base-path.md b/docs/book/v3/cookbook/using-a-base-path.md new file mode 100644 index 00000000..2b5a2647 --- /dev/null +++ b/docs/book/v3/cookbook/using-a-base-path.md @@ -0,0 +1,177 @@ +# How can I tell my application about a base path? + +In some environments, your application may be running in a subdirectory of your +web root. For example: + +``` +var/ +|- www/ +| |- wordpress/ +| |- expressive/ +| | |- public/ +| | | |- index.php +``` + +where `/var/www` is the web root, and your Expressive application is in the +`expressive/` subdirectory. How can you make your application work correctly in +this environment? + +## .htaccess in the application root. + +If you are using Apache, your first step is to add an `.htaccess` file to your +application root, with directives for rewriting to the `public/` directory: + +```ApacheConf +RewriteEngine On +RewriteRule (.*) ./public/$1 +``` + +> ### Using other web servers +> +> If you are using a web-server other than Apache, and know how to do a similar +> rewrite, we'd love to know! Please submit ideas/instructions to +> [our issue tracker](https://github.com/zendframework/zend-expressive/issues)! + +## Use middleware to rewrite the path + +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 pipeline middleware to intercept the request, and +rewrite the URL accordingly. + +At the time of writing, we have two suggestions: + +- [los/basepath](https://github.com/Lansoweb/basepath) provides the basic + mechanics of rewriting the URL, and has a stable release. +- [mtymek/blast-base-url](https://github.com/mtymek/blast-base-url) provides the + URL rewriting mechanics, as well as utilities for generating URIs that retain + the base path, but does not have a stable release yet. + +### los/basepath + +To use `los/basepath`, install it via Composer, copy the configuration files to +your application, and then edit the configuration. + +To install and copy the configuration: + +```bash +$ composer require los/basepath +$ cp vendor/los/basepath/config/los-basepath.global.php.dist config/autoload/los-basepath.global.php +``` + +We recommend copying the global configuration to a local configuration file as +well; this allows you to have the production settings in your global +configuration, and development settings in a local configuration (which is +excluded from git by default): + +```bash +$ cp config/autoload/los-basepath.global.php config/autoload/los-basepath.local.php +``` + +Then edit one or both, to change the `los_basepath` settings: + +```php +return [ + 'los_basepath' => '', + /* ... */ +]; +``` + +The base path should be the portion of the web root leading up to the +`index.php` of your application. In the above example, this would be +`/expressive`. + +### mtymek/blast-base-url + +To use `mtymek/blast-base-url`, install it via Composer, and register some +configuration. + +To install it: + +```bash +$ composer require mtymek/blast-base-url +``` + +To configure it, update the file `config/autoload/middleware-pipeline.global.php`, +or `config/autoload/dependencies.global.php` to map the middleware to its factory: + +```php +return [ + 'dependencies' => [ + 'factories' => [ + Blast\BaseUrl\BaseUrlMiddleware::class => Blast\BaseUrl\BaseUrlMiddlewareFactory::class, + /* ... */ + ], + /* ... */ + ], +]; +``` + +If using programmatic pipelines, pipe the middleware early in your pipeline: + +```php +$app->pipe(\Blast\BaseUrl\BaseUrlMiddleware::class); +``` + +For configuration-driven pipelines, add an entry in your +`config/autoload/middleware-pipeline.global.php` file: + +```php +'middleware_pipeline' => [ + ['middleware' => [Blast\BaseUrl\BaseUrlMiddleware::class], 'priority' => 1000], + /* ... */ +], +``` + +At this point, the middleware will take care of the rewriting for you. No +configuration is necessary, as it does auto-detection of the base path based on +the request URI and the operating system path to the application. + +The primary advantage of `mtymek/blast-base-url` is in its additional features: + +- it injects `Zend\Expressive\Helper\UrlHelper` with the base path, allowing you + to create relative route-based URLs. +- it provides a new helper, `Blast\BaseUrl\BasePathHelper`, which allows you to + create URLs relative to the base path; this is particularly useful for assets. + +To enable these features, we'll add some configuration to +`config/autoload/dependencies.global.php` file: + +```php +return [ + 'dependencies' => [ + 'invokables' => [ + Blast\BaseUrl\BasePathHelper::class => Blast\BaseUrl\BasePathHelper::class, + /* ... */ + ], + ], +]; +``` + +Finally, if you're using zend-view, you can register a new "basePath" helper in +your `config/autoload/templates.global.php`: + +```php +return [ + /* ... */ + 'view_helpers' => [ + 'factories' => [ + 'basePath' => Blast\BaseUrl\BasePathViewHelperFactory::class, + /* ... */ + ], + /* ... */ + ], +]; +``` + +Usage of the `BasePath` helper is as follows: + +```php +// where $basePathHelper is an instance of Blast\BaseUrl\BasePathHelper +// as pulled from your container: +echo $basePathHelper('/icons/favicon.ico'); + +// or, from zend-view's PhpRenderer: +echo $this->basePath('/icons/favicon.ico'); +``` diff --git a/docs/book/v3/cookbook/using-custom-view-helpers.md b/docs/book/v3/cookbook/using-custom-view-helpers.md new file mode 100644 index 00000000..50a209bb --- /dev/null +++ b/docs/book/v3/cookbook/using-custom-view-helpers.md @@ -0,0 +1,26 @@ +# How do you register custom view helpers when using zend-view? + +If you've selected zend-view as your preferred template renderer, you may want +to define and use custom view helpers. How can you use them? + +Assuming you've used the Expressive skeleton to start your application, you will +already have a factory defined for `Zend\View\HelperPluginManager`, and it will +be injected into the `PhpRenderer` instance used. Since the `HelperPluginManager` +is available, we can configure it. + +Open the file `config/autoload/templates.global.php`. In that file, you'll see +three top-level keys: + +```php +return [ + 'dependencies' => [ /* ... */ ], + 'templates' => [ /* ... */ ], + 'view_helpers' => [ /* ... */ ], +]; +``` + +The last is the one you want. In this, you can define service mappings, +including aliases, invokables, factories, and abstract factories to define how +helpers are named and created. +[See the zend-view custom helpers documentation](https://docs.zendframework.com/zend-view/helpers/advanced-usage/) +for information on how to populate this configuration. diff --git a/docs/book/v3/cookbook/using-routed-middleware-class-as-controller.md b/docs/book/v3/cookbook/using-routed-middleware-class-as-controller.md new file mode 100644 index 00000000..3a9b5be1 --- /dev/null +++ b/docs/book/v3/cookbook/using-routed-middleware-class-as-controller.md @@ -0,0 +1,242 @@ +# Handling multiple routes in a single class + +Typically, in Expressive, we would a single request handler class per route. For +a standard CRUD-style application, however, this leads to multiple related +classes: + +- AlbumPageIndex +- AlbumPageEdit +- AlbumPageAdd + +If you are familiar with frameworks that provide controllers capable of handling +multiple "actions", such as those found in Zend Framework's MVC layer, Symfony, +CodeIgniter, CakePHP, and other popular frameworks, you may want to apply a +similar pattern when using Expressive. + +In other words, what if we want to use only one middleware class to facilitate +all three of the above? + +In the following example, we'll use an `action` routing parameter which our +middleware class will use in order to determine which internal method to invoke. + +Consider the following route configuration: + +```php +use Album\Action\AlbumPage; + +// Programmatic: +$app->get('/album[/{action:add|edit}[/{id}]]', AlbumPage::class, 'album'); + +// Config-driven: +return [ + /* ... */ + 'routes' => [ + /* ... */ + [ + 'name' => 'album', + 'path' => '/album[/{action:add|edit}[/{id}]]', + 'middleware' => AlbumPage::class, + 'allowed_methods' => ['GET'], + ], + /* ... */ + ], +]; +``` +The above each define a route that will match any of the following: + +- `/album` +- `/album/add` +- `/album/edit/3` + +The `action` attribute can thus be one of `add` or `edit`, and we can optionally +also receive an `id` attribute (in the latter example, it would be `3`). + +> ## Routing definitions may vary +> +> Depending on the router you chose when starting your project, your routing +> definition may differ. The above example uses the default `FastRoute` +> implementation. + +We might then implement `Album\Action\AlbumPage` as follows: + +```php +template = $template; + } + + public function handle(ServerRequestInterface $request) : ResponseInterface + { + switch ($request->getAttribute('action', 'index')) { + case 'index': + return $this->indexAction($request); + case 'add': + return $this->addAction($request); + case 'edit': + return $this->editAction($request); + default: + // Invalid; thus, a 404! + return new EmptyResponse(StatusCode::STATUS_NOT_FOUND); + } + } + + public function indexAction(ServerRequestInterface $request) : ResponseInterface + { + return new HtmlResponse($this->template->render('album::album-page')); + } + + public function addAction(ServerRequestInterface $request) : ResponseInterface + { + return new HtmlResponse($this->template->render('album::album-page-add')); + } + + public function editAction(ServerRequestInterface $request) : ResponseInterface + { + $id = $request->getAttribute('id', false); + if (! $id) { + throw new \InvalidArgumentException('id parameter must be provided'); + } + + return new HtmlResponse( + $this->template->render('album::album-page-edit', ['id' => $id]) + ); + } +} +``` + +This allows us to have the same dependencies for a set of related actions, and, +if desired, even have common internal methods each can utilize. + +This approach is reasonable, but requires that I create a similar `handle()` +implementation every time I want to accomplish a similar workflow. Let's create +a generic implementation, via an `AbstractPage` class: + +```php +getAttribute('action', 'index') . 'Action'; + + if (! method_exists($this, $action)) { + return new EmptyResponse(StatusCode::STATUS_NOT_FOUND); + } + + return $this->$action($request); + } +} +``` + +The above abstract class pulls the `action` attribute on invocation, and +concatenates it with the word `Action`. It then uses this value to determine if +a corresponding method exists in the current class, and, if so, calls it with +the arguments it received; otherwise, it returns an empty 404 response. + +Our original `AlbumPage` implementation could then be modified to extend +`AbstractPage`: + +```php +template = $template; + } + + public function indexAction( /* ... */ ) : ResponseInterface { /* ... */ } + public function addAction( /* ... */ ) : ResponseInterface { /* ... */ } + public function editAction( /* ... */ ) : ResponseInterface { /* ... */ } +} +``` + +> ## Or use a trait +> +> As an alternative to an abstract class, you could define the `__invoke()` +> logic in a trait, which you then compose into your middleware: +> +> ```php +> namespace App\Action; +> +> use Fig\Http\Message\StatusCodeInterface as StatusCode; +> use Psr\Http\Message\ResponseInterface; +> use Psr\Http\Message\ServerRequestInterface; +> use Zend\Diactoros\Response\EmptyResponse; +> +> trait ActionBasedInvocation +> { +> public function handle(ServerRequestInterface $request) : ResponseInterface +> { +> $action = $request->getAttribute('action', 'index') . 'Action'; +> +> if (! method_exists($this, $action)) { +> return new EmptyResponse(StatusCode::STATUS_NOT_FOUND); +> } +> +> return $this->$action($request, $handler); +> } +> } +> ``` +> +> You would then compose it into a class as follows: +> +> ```php +> namespace Album\Action; +> +> use App\Action\ActionBasedInvocation; +> use Psr\Http\Message\ResponseInterface; +> use Psr\Http\Server\RequestHandlerInterface; +> use Zend\Expressive\Template\TemplateRendererInterface; +> +> class AlbumPage implements RequestHandlerInterface +> { +> use ActionBasedInvocation; +> +> private $template; +> +> public function __construct(TemplateRendererInterface $template) +> { +> $this->template = $template; +> } +> +> public function indexAction( /* ... */ ) : ResponseInterface { /* ... */ } +> public function addAction( /* ... */ ) : ResponseInterface { /* ... */ } +> public function editAction( /* ... */ ) : ResponseInterface { /* ... */ } +> } +> ``` diff --git a/docs/book/v3/cookbook/using-zend-form-view-helpers.md b/docs/book/v3/cookbook/using-zend-form-view-helpers.md new file mode 100644 index 00000000..8f2a6a0d --- /dev/null +++ b/docs/book/v3/cookbook/using-zend-form-view-helpers.md @@ -0,0 +1,294 @@ +# How can I use zend-form view helpers? + +If you've selected zend-view as your preferred template renderer, you'll likely +want to use the various view helpers available in other components, such as: + +- zend-form +- zend-i18n +- zend-navigation + +By default, only the view helpers directly available in zend-view are available; +how can you add the others? + +To add the zend-form view helpers create a file `config/autoload/zend-form.global.php` +with the contents: + +```php +has('config') ? $container->get('config') : []; + $config = isset($config['view_helpers']) ? $config['view_helpers'] : []; + (new Config($config))->configureServiceManager($manager); + + return $manager; + } +} +``` + +In your `config/autoload/templates.global.php` file, change the line that reads: + +```php +Zend\View\HelperPluginManager::class => Zend\Expressive\ZendView\HelperPluginManagerFactory::class, +``` + +to instead read as: + +```php +Zend\View\HelperPluginManager::class => Your\Application\HelperPluginManagerFactory::class, +``` + +This approach will work for any of the various containers supported. + +## Delegator factories/service extension + +[Delegator factories](https://docs.zendframework.com/zend-servicemanager/delegators/) +and [service extension](https://github.com/silexphp/Pimple/tree/1.1#modifying-services-after-creation) +operate on the same principle: they intercept after the original factory was +called, and then operate on the generated instance, either modifying or +replacing it. We'll demonstrate this for zend-servicemanager and Pimple; at the +time of writing, we're unaware of a mechanism for doing so in Aura.Di. + +### zend-servicemanager + +You'll first need to create a delegator factory: + +```php +namespace Your\Application; + +use Psr\Container\ContainerInterface; +use Zend\ServiceManager\Config; +use Zend\ServiceManager\DelegatorFactoryInterface; +use Zend\ServiceManager\ServiceLocatorInterface; + +class FormHelpersDelegatorFactory +{ + public function __invoke( + ContainerInterface $container, + $name, + callable $callback, + array $options = null + ) { + $helpers = $callback(); + + $config = $container->has('config') ? $container->get('config') : []; + $config = new Config($config['view_helpers']); + $config->configureServiceManager($helpers); + return $helpers; + } +} +``` + +The above creates an instance of `Zend\ServiceManager\Config`, uses it to +configure the already created `Zend\View\HelperPluginManager` instance, and then +returns the plugin manager instance. + +From here, you'll add a `delegators` configuration key in your +`config/autoload/templates.global.php` file: + +```php +return [ + 'dependencies' => [ + 'delegators' => [ + Zend\View\HelperPluginManager::class => [ + Your\Application\FormHelpersDelegatorFactory::class, + ], + ], + /* ... */ + ], + 'templates' => [ + /* ... */ + ], + 'view_helpers' => [ + /* ... */ + ], +]; +``` + +Note: delegator factories are keyed by the service they modify, and the value is +an *array* of delegator factories, to allow multiple such factories to be in +use. + +### Pimple + +For Pimple, we don't currently support configuration of service extensions, so +you'll need to edit the main container configuration file, +`config/container.php`. Place the following anywhere after the factories and +invokables are defined: + +```php +// The following assumes you've added the following import statements to +// the start of the file: +// use Zend\ServiceManager\Config as ServiceConfig; +// use Zend\View\HelperPluginManager; +$container[HelperPluginManager::class] = $container->extend( + HelperPluginManager::class, + function ($helpers, $container) { + $config = isset($container['config']) ? $container['config'] : []; + $config = new ServiceConfig($config['view_helpers']); + $config->configureServiceManager($helpers); + return $helpers; + } +); +``` + +## Pipeline middleware + +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. + +First, define the middleware: + +```php +helpers = $helpers; + } + + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface + { + $config = new FormHelperConfig(); + $config->configureServiceManager($this->helpers); + return $handler->handle($request); + } +} +``` + +You'll also need a factory for the middleware, to ensure it receives the +`HelperPluginManager`: + +```php +namespace Your\Application + +use Zend\View\HelperPluginManager; + +class FormHelpersMiddlewareFactory +{ + public function __invoke($container) + { + return new FormHelpersMiddleware( + $container->get(HelperPluginManager::class) + ); + } +} +``` + +Next, register the middleware with its factory in one of +`config/autoload/middleware-pipeline.global.php` or +`config/autoload/dependencies.global.php`: + +```php +return [ + 'dependencies' => [ + 'factories' => [ + Your\Application\FormHelpersMiddleware::class => Your\Application\FormHelpersMiddlewareFactory::class + /* ... */ + ], + /* ... */ + ], +]; +``` + +If using programmatic pipelines, pipe the middleware in an appropriate location +in your pipeline: + +```php +$app->pipe(FormHelpersMiddleware::class); + +// or, perhaps, in a route-specific middleware pipeline: +$app->post('/register', [ + FormHelpersMiddleware::class, + RegisterMiddleware::class, +], 'register'); +``` + +If using configuration-driven pipelines or routing: + +``` +// Via the middleware pipeline: +'middleware_pipeline' => [ + ['middleware' => Your\Application\FormHelpersMiddleware::class, 'priority' => 1000], +], + +// Or via routes: +'routes' => [ + [ + 'name' => 'register', + 'path' => '/register', + 'middleware' => [ + FormHelpersMiddleware::class, + RegisterMiddleware::class, + ], + 'allowed_methods' => ['POST'], + ] +] +``` + +At that point, you're all set! + +## Registering more helpers + +What if you need to register helpers from multiple components? + +You can do so using the same technique above. Better yet, do them all at once! + +- If you chose to use delegator factories/service extension, do all helper + configuration registrations for all components in the same factory. +- If you chose to use middleware, do all helper configuration registrations for + all components in the same middleware. diff --git a/docs/book/v3/features/application.md b/docs/book/v3/features/application.md new file mode 100644 index 00000000..df937b56 --- /dev/null +++ b/docs/book/v3/features/application.md @@ -0,0 +1,160 @@ +# Applications + +In zend-expressive, you define a `Zend\Expressive\Application` instance and +execute it. The `Application` instance is itself [middleware](https://docs.zendframework.com/zend-stratigility/middleware/) +that composes: + +- a [router](router/intro.md), for dynamically routing requests to middleware. +- a [dependency injection container](container/intro.md), for retrieving + middleware to dispatch. +- a [default delegate](error-handling.md#default-delegates) (Expressive 2.X) + or [final handler](error-handling.md#version-1-error-handling) +- an [emitter](https://docs.zendframework.com/zend-httphandlerrunner/emitters/), + for emitting the response when application execution is complete. + +You can define the `Application` instance in several ways: + +- Direct instantiation, which requires providing several dependencies. +- The `AppFactory`, which will use some common defaults, but allows injecting alternate + container and/or router implementations. +- Via a dependency injection container; we provide a factory for setting up all + aspects of the instance via configuration and other defined services. + +Regardless of how you setup the instance, there are several methods you will +likely interact with at some point or another. + +## Instantiation + +As noted at the start of this document, we provide several ways to create an +`Application` instance. + +### Constructor + +If you wish to manually instantiate the `Application` instance, it has the +following constructor: + +```php +public function __construct( + Zend\Expressive\MiddlewareFactory $factory, + Zend\Stratigility\MiddlewarePipeInterface $pipeline, + Zend\Expressive\Router\PathBasedRoutingMiddleware $routes, + Zend\HttpHandlerRunner\RequestHandlerRunner $runner +) { +``` + +### Container factory + +We also provide a factory that can be consumed by a [PSR-11](https://www.php-fig.org/psr/psr-11/) +dependency injection container; see the [container factories documentation](container/factories.md) +for details. + +## Adding routable middleware + +We [discuss routing vs piping elsewhere](router/piping.md); routing is the act +of dynamically matching an incoming request against criteria, and it is one of +the primary features of zend-expressive. + +Regardless of which [router implementation](router/interface.md) you use, you +can use the following `Application` methods to provide routable middleware: + +### route() + +`route()` has the following signature: + +```php +public function route( + string $path, + $middleware, + array $methods = null, + string $name = null +) : Zend\Expressive\Router\Route +``` + +where: + +- `$path` must be a string path to match. +- `$middleware` **must** be: + - a service name that resolves to valid middleware in the container; + - a fully qualified class name of a constructor-less class that represents a + PSR-15 `MiddlewareInterface` or `RequestHandlerInterface` instance; + - an array of any of the above; these will be composed in order into a + `Zend\Stratigility\MiddlewarePipe` instance. +- `$methods` must be an array of HTTP methods valid for the given path and + middleware. If null, it assumes any method is valid. +- `$name` is the optional name for the route, and is used when generating a URI + from known routes. See the section on [route naming](router/uri-generation.md#generating-uris) + for details. + +This method is typically only used if you want a single middleware to handle +multiple HTTP request methods. + +### get(), post(), put(), patch(), delete(), any() + +Each of the methods `get()`, `post()`, `put()`, `patch()`, `delete()`, and `any()` +proxies to `route()` and has the signature: + +```php +function ( + string $path, + $middleware, + string $name = null +) : Zend\Expressive\Router\Route +``` + +Essentially, each calls `route()` and specifies an array consisting solely of +the corresponding HTTP method for the `$methods` argument. + +### Piping + +Because zend-expressive builds on [zend-stratigility](https://docs.zendframework.com/zend-stratigility/), +and, more specifically, its `MiddlewarePipe` definition, you can also pipe +(queue) middleware to the application. This is useful for adding middleware that +should execute on each request, defining error handlers, and/or segregating +applications by subpath. + +The signature of `pipe()` is: + +```php +public function pipe($middlewareOrPath, $middleware = null) +``` + +where: + +- `$middlewareOrPath` is either a string URI path (for path segregation), PSR-15 + `MiddlewareInterface` or `RequestHandlerInterface`, or the service name for a + middleware or request handler to fetch from the composed container. +- `$middleware` is required if `$middlewareOrPath` is a string URI path. It can + be one of: + - a service name that resolves to valid middleware in the container; + - a fully qualified class name of a constructor-less class that represents a + PSR-15 `MiddlewareInterface` or `RequestHandlerInterface` instance; + - an array of any of the above; these will be composed in order into a + `Zend\Stratigility\MiddlewarePipe` instance. + +Unlike `Zend\Stratigility\MiddlewarePipe`, `Application::pipe()` *allows +fetching middleware and request handlers by service name*. This facility allows +lazy-loading of middleware only when it is invoked. Internally, it wraps the +call to fetch and dispatch the middleware inside a +`Zend\Expressive\Middleware\LazyLoadingMiddleware` instance. + +Read the section on [piping vs routing](router/piping.md) for more information. + +### Registering routing and dispatch middleware + +Routing and dispatch middleware must be piped to the application like any other +middleware. You can do so using the following: + +```php +$app->pipe(Zend\Expressive\Router\Middleware\PathBasedRoutingMiddleware::class); +$app->pipe(Zend\Expressive\Router\Middleware\DispatchMiddleware::class); +``` + +See the section on [piping](router/piping.md) to see how you can register +non-routed middleware and create layered middleware applications. + +## Executing the application: run() + +When the application is completely setup, you can execute it with the `run()` +method. The method proxies to the underlying `RequestHandlerRunner`, which will +create a PSR-7 server request instance, pass it to the composed middleware +pipeline, and then emit the response returned. diff --git a/docs/book/v3/features/container/aura-di.md b/docs/book/v3/features/container/aura-di.md new file mode 100644 index 00000000..dbc4c6ba --- /dev/null +++ b/docs/book/v3/features/container/aura-di.md @@ -0,0 +1,53 @@ +# Using Aura.Di + +[Aura.Di](https://github.com/auraphp/Aura.Di/) provides a serializable dependency +injection container with the following features: + +- constructor and setter injection. +- inheritance of constructor parameter and setter method values from parent + classes. +- inheritance of setter method values from interfaces and traits. +- lazy-loaded instances, services, includes/requires, and values. +- instance factories. +- optional auto-resolution of typehinted constructor parameter values. + +## Installing Aura.Di + +Aura.Di implements [PSR-11](https://www.php-fig.org/psr/psr-11/) as of +version 3. To use Aura.Di as a dependency injection container, we recommend using +[zendframework/zend-auradi-config](https://github.com/zendframework/zend-auradi-config), +which helps you to configure its container. First, install the package: + +```bash +$ composer require zendframework/zend-auradi-config +``` + +## Configuration + +To configure Aura.Di, create the file `config/container.php` with the following +contents: + +```php +get(Zend\Expressive\Application::class); +require 'config/pipeline.php'; +require 'config/routes.php'; +$app->run(); +``` + +For more information, please see the +[zend-auradi-config documentation](https://github.com/zendframework/zend-auradi-config/blob/master/README.md) diff --git a/docs/book/v3/features/container/delegator-factories.md b/docs/book/v3/features/container/delegator-factories.md new file mode 100644 index 00000000..10409af5 --- /dev/null +++ b/docs/book/v3/features/container/delegator-factories.md @@ -0,0 +1,62 @@ +# Delegator Factories + +Expressive supports the concept of _delegator factories_, which allow decoration +of services created by your dependency injection container, across all +dependency injection containers supported by Expressive. + +_Delegator factories_ accept the following arguments: + +- The container itself; +- The name of the service whose creation is being decorated; +- A callback that will produce the service being decorated. + +As an example, let's say we have a `UserRepository` class that composes some sort of +event manager. We might want to attach listeners to that event manager, but not +wish to alter the basic creation logic for the repository itself. As such, we +might write a _delegator factory_ as follows: + +```php +namespace Acme; + +use Psr\Container\ContainerInterface; +use Psr\Log\LoggerInterface; + +class UserRepositoryListenerDelegatorFactory +{ + /** + * @param ContainerInterface $container + * @param string $name + * @param callable $callback + * @return UserRepository + */ + public function __invoke(ContainerInterface $container, $name, callable $callback) + { + $listener = new LoggerListener($container->get(LoggerInterface::class)); + $repository = $callback(); + $repository->getEventManager()->attach($listener); + return $repository; + } +} +``` + +To notify the container about this delegator factory, we would add the following +configuration to our application: + +```php +'dependencies' => [ + 'delegators' => [ + Acme\UserRepository::class => [ + Acme\UserRepositoryListenerDelegatorFactory::class, + ], + ], +], +``` + +Note that you specify delegator factories using the service name being decorated +as the key, with an _array_ of delegator factories as a value. **You may attach +multiple delegator factories to any given service**, which can be a very +powerful feature. + +At the time of writing, this feature works for each of the Aura.Di, Pimple, and +zend-servicemanager container implementations. Delegator factories have been +supported with Pimple and zend-servicemanager since the 1.X series. diff --git a/docs/book/v3/features/container/factories.md b/docs/book/v3/features/container/factories.md new file mode 100644 index 00000000..3bbae804 --- /dev/null +++ b/docs/book/v3/features/container/factories.md @@ -0,0 +1,390 @@ +# Provided Factories + +Expressive provides several factories compatible with +[PSR-11 Container](https://www.php-fig.org/psr/psr-11/) to facilitate +setting up common dependencies. The following is a list of provided +containers, what they will create, the suggested service name, and any +additional dependencies they may require. + +All factories, unless noted otherwise, are in the `Zend\Expressive\Container` +namespace, and define an `__invoke()` method that accepts an +`Psr\Container\ContainerInterface` instance as the sole argument. + +## ApplicationFactory + +- **Provides**: `Zend\Expressive\Application` +- **Suggested Name**: `Zend\Expressive\Application` +- **Requires**: no additional services are required. +- **Optional**: + - `Zend\Expressive\Router\RouterInterface`. When provided, the service will + be used to construct the `Application` instance; otherwise, an FastRoute router + implementation will be used. + - `Zend\Expressive\Delegate\DefaultDelegate`. This should return an + `Interop\Http\ServerMiddleware\DelegateInterface` instance to process + when the middleware pipeline is exhausted without returning a response; + by default, this will be a `Zend\Expressive\Delegate\NotFoundDelegate` + instance. + - `Zend\Diactoros\Response\EmitterInterface`. If none is provided, an instance + of `Zend\Expressive\Emitter\EmitterStack` composing a + `Zend\Diactoros\Response\SapiEmitter` instance will be used. + - `config`, an array or `ArrayAccess` instance. This _may_ be used to seed the + application instance with pipeline middleware and/or routed + middleware (see more below). + +Additionally, the container instance itself is injected into the `Application` +instance. + +When the `config` service is present, the factory can utilize several keys in +order to seed the `Application` instance: + +- `programmatic_pipeline` (bool) (Since 1.1.0): when enabled, + `middleware_pipeline` and `routes` configuration are ignored, and the factory + will assume that these are injected programmatically elsewhere. + +- `raise_throwables` (bool) (Since 1.1.0; obsolete as of 2.0.0): when enabled, + this flag will prevent the Stratigility middleware dispatcher from catching + exceptions, and instead allow them to bubble outwards. + +- `middleware_pipeline` can be used to seed the middleware pipeline: + + ```php + 'middleware_pipeline' => [ + // An array of middleware to register. + [ /* ... */ ], + + // Expressive 1.0: + Zend\Expressive\Container\ApplicationFactory::ROUTING_MIDDLEWARE, + Zend\Expressive\Container\ApplicationFactory::DISPATCH_MIDDLEWARE, + + // Expressive 1.1 and above (above constants will still work, though): + Zend\Expressive\Application::ROUTING_MIDDLEWARE, + Zend\Expressive\Application::DISPATCH_MIDDLEWARE, + + [ /* ... */ ], + ], + ``` + + Each item of the array, other than the entries for routing and dispatch + middleware, must be an array itself, with the following structure: + + ```php + [ + // required: + 'middleware' => 'Name of middleware service, valid middleware, or an array of these', + // optional: + 'path' => '/path/to/match', + 'priority' => 1, // Integer + + // optional under Expressive 1.X; ignored under 2.X: + 'error' => false, // boolean + ], + ``` + + The `middleware` key itself is the middleware to execute, and must be a + service name resolving to valid middleware, middleware instances (either + http-interop middleware or callable double-pass middleware), or an array of + these values. If an array is provided, the specified middleware will be + composed into a `Zend\Stratigility\MiddlewarePipe` instance. + + If the `path` key is present, that key will be used to segregate the + middleware to a specific matched path (in other words, it will not execute if + the path is not matched). + + 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. + + Under Expressive 1.X, if the `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.) We recommend _not_ using this feature; + see the chapter on [error handling](../error-handling.md) for details. + +- `routes` is used to define routed middleware. The value must be an array, + consisting of arrays defining each middleware: + + ```php + 'routes' => [ + [ + 'path' => '/path/to/match', + 'middleware' => 'Middleware service name, valid middleware, or array of these values', + 'allowed_methods' => ['GET', 'POST', 'PATCH'], + 'options' => [ + 'stuff' => 'to', + 'pass' => 'to', + 'the' => 'underlying router', + ], + ], + // etc. + ], + ``` + + Each route *requires*: + + - `path`: the path to match. Format will be based on the router you choose for + your project. + + - `middleware`: a service name resolving to valid middleware, valid + middleware (either http-interop middleware or callable double-pass + middleware), or an array of such values (which will be composed into + a `Zend\Stratigility\MiddlewarePipe` instance); this middleware will be + dispatched when the route matches. + + Optionally, the route definition may provide: + + - `allowed_methods`: an array of allowed HTTP methods. If not provided, the + application assumes any method is allowed. + + - `name`: if not provided, the path will be used as the route name (and, if + specific HTTP methods are allowed, a list of those). + + - `options`: a key/value set of additional options to pass to the underlying + router implementation for the given route. (Typical use cases include + passing constraints or default values.) + +## ErrorHandlerFactory + +- **Provides**: `Zend\Stratigility\Middleware\ErrorHandler` +- **Suggested Name**: `Zend\Stratigility\Middleware\ErrorHandler` +- **Requires**: no additional services are required. +- **Optional**: + - `Zend\Expressive\Middleware\ErrorResponseGenerator`. If not provided, the error + handler will not compose an error response generator, making it largely + useless other than to provide an empty response. + +## ErrorResponseGeneratorFactory + +- **Provides**: `Zend\Expressive\Middleware\ErrorResponseGenerator` +- **Suggested Name**: `Zend\Stratigility\Middleware\ErrorResponseGenerator` +- **Requires**: no additional services are required. +- **Optional**: + - `Zend\Expressive\Template\TemplateRendererInterface`. If not provided, the + error response generator will provide a plain text response instead of a + templated one. + - `config`, an array or `ArrayAccess` instance. This will be used to seed the + `ErrorResponseGenerator` instance with a template name to use for errors (see + more below), and/or a "debug" flag value. + +When the `config` service is present, the factory can utilize two values: + +- `debug`, a flag indicating whether or not to provide debug information when + creating an error response. +- `zend-expressive.error_handler.template_error`, a name of an alternate + template to use (instead of the default represented in the + `Zend\Expressive\Middleware\ErrorResponseGenerator::TEMPLATE_DEFAULT` + constant). + +As an example: + +```php +'debug' => true, +'zend-expressive' => [ + 'error_handler' => [ + 'template_error' => 'name of error template', + ], +], +``` + +## NotFoundDelegateFactory + +- **Provides**: `Zend\Expressive\Delegate\NotFoundDelegate` +- **Suggested Name**: `Zend\Expressive\Delegate\NotFoundDelegate`, and aliased + to `Zend\Expressive\Delegate\DefaultDelegate`. +- **Requires**: no additional services are required. +- **Optional**: + - `Zend\Expressive\Template\TemplateRendererInterface`. If not provided, the + delegate will provide a plain text response instead of a templated one. + - `config`, an array or `ArrayAccess` instance. This will be used to seed the + `NotFoundDelegate` instance with a template name to use. + +When the `config` service is present, the factory can utilize two values: + +- `zend-expressive.error_handler.template_404`, a name of an alternate + template to use (instead of the default represented in the + `Zend\Expressive\Delegate\NotFoundDelegate::TEMPLATE_DEFAULT` constant). + +As an example: + +```php +'zend-expressive' => [ + 'error_handler' => [ + 'template_404' => 'name of 404 template', + ], +], +``` + +## NotFoundHandlerFactory + +- **Provides**: `Zend\Expressive\Middleware\NotFoundHandler` +- **Suggested Name**: `Zend\Expressive\Middleware\NotFoundHandler` +- **Requires**: `Zend\Expressive\Delegate\DefaultDelegate` + +## WhoopsErrorResponseGeneratorFactory + +- **Provides**: `Zend\Expressive\Middleware\WhoopsErrorResponseGenerator` +- **Suggested Name**: `Zend\Expressive\Middleware\ErrorResponseGenerator` +- **Requires**: `Zend\Expressive\Whoops` (see [WhoopsFactory](#whoopsfactory), +below) + +## WhoopsFactory + +- **Provides**: `Whoops\Run` +- **Suggested Name**: `Zend\Expressive\Whoops` +- **Requires**: + - `Zend\Expressive\WhoopsPageHandler` +- **Optional**: + - `config`, an array or `ArrayAccess` instance. This will be used to seed + additional page handlers, specifically the `JsonResponseHandler` (see + more below). + +This factory creates and configures a `Whoops\Run` instance so that it will work +properly with `Zend\Expressive\Application`; this includes disabling immediate +write-to-output, disabling immediate quit, etc. The `PrettyPageHandler` returned +for the `Zend\Expressive\WhoopsPageHandler` service will be injected. + +It consumes the following `config` structure: + +```php +'whoops' => [ + 'json_exceptions' => [ + 'display' => true, + 'show_trace' => true, + 'ajax_only' => true, + ], +], +``` + +If no `whoops` top-level key is present in the configuration, a default instance +with no `JsonResponseHandler` composed will be created. + +## WhoopsPageHandlerFactory + +- **Provides**: `Whoops\Handler\PrettyPageHandler` +- **Suggested Name**: `Zend\Expressive\WhoopsPageHandler` +- **Optional**: + - `config`, an array or `ArrayAccess` instance. This will be used to further + configure the `PrettyPageHandler` instance, specifically with editor + configuration (for linking files such that they open in the configured + editor). + +It consumes the following `config` structure: + +```php +'whoops' => [ + 'editor' => 'editor name, editor service name, or callable', +], +``` + +The `editor` value must be a known editor name (see the Whoops documentation for +pre-configured editor types), a callable, or a service name to use. + +## PlatesRendererFactory + +- **Provides**: `Zend\Expressive\Template\PlatesRenderer` +- **FactoryName**: `Zend\Expressive\Plates\PlatesRendererFactory` +- **Suggested Name**: `Zend\Expressive\Template\TemplateRendererInterface` +- **Requires**: no additional services are required. +- **Optional**: + - `config`, an array or `ArrayAccess` instance. This will be used to further + configure the `Plates` instance, specifically with the filename extension + to use, and paths to inject. + +It consumes the following `config` structure: + +```php +'templates' => [ + 'extension' => 'file extension used by templates; defaults to html', + 'paths' => [ + // namespace / path pairs + // + // Numeric namespaces imply the default/main namespace. Paths may be + // strings or arrays of string paths to associate with the namespace. + ], +] +``` + +One note: Due to a limitation in the Plates engine, you can only map one path +per namespace when using Plates. + +## TwigRendererFactory + +- **Provides**: `Zend\Expressive\Template\TwigRenderer` +- **FactoryName**: `Zend\Expressive\Twig\TwigRendererFactory` +- **Suggested Name**: `Zend\Expressive\Template\TemplateRendererInterface` +- **Requires**: no additional services are required. +- **Optional**: + - `Zend\Expressive\Router\RouterInterface`; if found, it will be used to + seed a `Zend\Expressive\Twig\TwigExtension` instance for purposes + of rendering application URLs. + - `config`, an array or `ArrayAccess` instance. This will be used to further + configure the `Twig` instance, specifically with the filename extension, + paths to assets (and default asset version to use), and template paths to + inject. + +It consumes the following `config` structure: + +```php +'debug' => boolean, +'templates' => [ + 'cache_dir' => 'path to cached templates', + 'assets_url' => 'base URL for assets', + 'assets_version' => 'base version for assets', + 'extension' => 'file extension used by templates; defaults to html.twig', + 'paths' => [ + // namespace / path pairs + // + // Numeric namespaces imply the default/main namespace. Paths may be + // strings or arrays of string paths to associate with the namespace. + ], +] +``` + +When `debug` is true, it disables caching, enables debug mode, enables strict +variables, and enables auto reloading. The `assets_*` values are used to seed +the `TwigExtension` instance (assuming the router was found). + +## ZendViewRendererFactory + +- **Provides**: `Zend\Expressive\Template\ZendViewRenderer` +- **FactoryName**: `Zend\Expressive\ZendView\ZendViewRendererFactory` +- **Suggested Name**: `Zend\Expressive\Template\TemplateRendererInterface` +- **Requires**: no additional services are required. + - `Zend\Expressive\Router\RouterInterface`, in order to inject the custom + url helper implementation. +- **Optional**: + - `config`, an array or `ArrayAccess` instance. This will be used to further + configure the `ZendView` instance, specifically with the layout template + name, entries for a `TemplateMapResolver`, and and template paths to + inject. + - `Zend\View\HelperPluginManager`; if present, will be used to inject the + `PhpRenderer` instance. + +It consumes the following `config` structure: + +```php +'templates' => [ + 'layout' => 'name of layout view to use, if any', + 'map' => [ + // template => filename pairs + ], + 'paths' => [ + // namespace / path pairs + // + // Numeric namespaces imply the default/main namespace. Paths may be + // strings or arrays of string paths to associate with the namespace. + ], +] +``` + +When creating the `PhpRenderer` instance, it will inject it with a +`Zend\View\HelperPluginManager` instance (either pulled from the container, or +instantiated directly). It injects the helper plugin manager with custom url and +serverurl helpers, `Zend\Expressive\ZendView\UrlHelper` and +`Zend\Expressive\ZendView\ServerUrlHelper`, respetively. diff --git a/docs/book/v3/features/container/intro.md b/docs/book/v3/features/container/intro.md new file mode 100644 index 00000000..c735fe00 --- /dev/null +++ b/docs/book/v3/features/container/intro.md @@ -0,0 +1,45 @@ +# Containers + +Expressive promotes and advocates the usage of +[Dependency Injection](http://www.martinfowler.com/articles/injection.html)/[Inversion of Control](https://en.wikipedia.org/wiki/Inversion_of_control) +(also referred to as DI — or DIC — and IoC, respectively) +containers when writing your applications. These should be used for the +following: + +- Defining *application* dependencies: routers, template engines, error + handlers, even the `Application` instance itself. + +- Defining *middleware* and related dependencies. + +The `Application` instance itself stores a container, from which it fetches +middleware when ready to dispatch it; this encourages the idea of defining +middleware-specific dependencies, and factories for ensuring they are injected. + +To facilitate this and allow you as a developer to choose the container you +prefer, zend-expressive typehints against [PSR-11 Container](https://www.php-fig.org/psr/psr-11/), +and throughout this manual, we attempt to show using a variety of containers in +examples. + +At this time, we document support for the following specific containers: + +- [zend-servicemanager](zend-servicemanager.md) +- [pimple-interop](pimple.md) +- [aura.di](aura-di.md) + +> ## Service Names +> +> We recommend using fully-qualified class names whenever possible as service +> names, with one exception: in cases where a service provides an implementation +> of an interface used for typehints, use the interface name. +> +> Following these practices encourages the following: +> +> - Consumers have a reasonable idea of what the service should return. +> - Using interface names as service names promotes re-use and substitution. +> +> In a few cases, we define "virtual service" names. These are cases where there is no +> clear typehint to follow (e.g., most middleware only uses `callable` as a +> typehint, or where we want to imply specific configuration is necessary (e.g., +> [Whoops](http://filp.github.io/whoops/) requires specific configuration to +> work correctly with Expressive, and thus we do not want a generic service name +> for it). We try to keep these to a minimum, however. diff --git a/docs/book/v3/features/container/pimple.md b/docs/book/v3/features/container/pimple.md new file mode 100644 index 00000000..a9447f83 --- /dev/null +++ b/docs/book/v3/features/container/pimple.md @@ -0,0 +1,53 @@ +# Using Pimple + +[Pimple](http://pimple.sensiolabs.org/) is a widely used, code-driven, +dependency injection container provided as a standalone component by SensioLabs. +It features: + +- combined parameter and service storage. +- ability to define factories for specific classes. +- lazy-loading via factories. + +Pimple only supports programmatic creation at this time. + +## Installing and configuring Pimple + +Pimple implements [PSR-11 Container](https://github.com/php-fig/container) +as of version 3.2. To use Pimple as a dependency injection container, we +recommend using [zendframework/zend-pimple-config](https://github.com/zendframework/zend-pimple-config), +which helps you to configure the PSR-11 container. First install the package: + +```bash +$ composer require zendframework/zend-pimple-config +``` + +Now, create the file `config/container.php` with the following contents: + +```php +get(Zend\Expressive\Application::class); + +require 'config/pipeline.php'; +require 'config/routes.php'; + +$app->run(); +``` diff --git a/docs/book/v3/features/container/zend-servicemanager.md b/docs/book/v3/features/container/zend-servicemanager.md new file mode 100644 index 00000000..1787e2cf --- /dev/null +++ b/docs/book/v3/features/container/zend-servicemanager.md @@ -0,0 +1,291 @@ +# Using zend-servicemanager + +[zend-servicemanager](https://docs.zendframework.com//zend-servicemanager/) is a +code-driven dependency injection container provided as a standalone component by +Zend Framework. It features: + +- lazy-loading of invokable (constructor-less) classes. +- ability to define factories for specific classes. +- ability to define generalized factories for classes with identical + construction patterns (aka *abstract factories*). +- ability to create lazy-loading proxies. +- ability to intercept before or after instantiation to alter the construction + workflow (aka *delegator factories*). +- interface injection (via *initializers*). + +zend-servicemanager may either be created and populated programmatically, or via +configuration. Configuration uses the following structure: + +```php +[ + 'services' => [ + 'service name' => $serviceInstance, + ], + 'invokables' => [ + 'service name' => 'class to instantiate', + ], + 'factories' => [ + 'service name' => 'callable, Zend\ServiceManager\FactoryInterface instance, or name of factory class returning the service', + ], + 'abstract_factories' => [ + 'class name of Zend\ServiceManager\AbstractFactoryInterface implementation', + ], + 'delegators' => [ + 'service name' => [ + 'class name of Zend\ServiceManager\DelegatorFactoryInterface implementation', + ], + ], + 'lazy_services' => [ + 'class_map' => [ + 'service name' => 'Class\Name\Of\Service', + ], + ], + 'initializers' => [ + 'callable, Zend\ServiceManager\InitializerInterface implementation, or name of initializer class', + ], +] +``` + +Read more about zend-servicemanager in [its documentation](https://docs.zendframework.com/zend-servicemanager/). + +## Installing zend-servicemanager + +To use zend-servicemanager with zend-expressive, you can install it via +composer: + +```bash +$ composer require zendframework/zend-servicemanager +``` + +## Configuring zend-servicemanager + +You can configure zend-servicemanager either programmatically or via +configuration. We'll show you both methods. + +### Programmatically + +To use zend-servicemanager programatically, you'll need to create a +`Zend\ServiceManager\ServiceManager` instance, and then start populating it. + +For this example, we'll assume your application configuration (used by several +factories to configure instances) is in `config/config.php`, and that that file +returns an array. + +We'll create a `config/container.php` file that creates and returns a +`Zend\ServiceManager\ServiceManager` instance as follows: + +```php +use Zend\ServiceManager\ServiceManager; + +$container = new ServiceManager(); + +// Application and configuration +$container->setService('config', include 'config/config.php'); +$container->setFactory( + Zend\Expressive\Application::class, + Zend\Expressive\Container\ApplicationFactory::class +); + +// Routing +// In most cases, you can instantiate the router you want to use without using a +// factory: +$container->setInvokableClass( + Zend\Expressive\Router\RouterInterface::class, + Zend\Expressive\Router\AuraRouter::class +); + +// Templating +// In most cases, you can instantiate the template renderer you want to use +// without using a factory: +$container->setInvokableClass( + Zend\Expressive\Template\TemplateRendererInterface::class, + Zend\Expressive\Plates\PlatesRenderer::class +); + +// These next two can be added in any environment; they won't be used unless +// you add the WhoopsErrorResponseGenerator as the ErrorResponseGenerator +// implementation: +$container->setFactory( + 'Zend\Expressive\Whoops', + Zend\Expressive\Container\WhoopsFactory::class +); +$container->setFactory( + 'Zend\Expressive\WhoopsPageHandler', + Zend\Expressive\Container\WhoopsPageHandlerFactory::class +); + +// Error Handling + +// All environments: +$container->setFactory( + Zend\Expressive\Middleware\ErrorHandler::class, + Zend\Expressive\Container\ErrorHandlerFactory::class +); + +// If in development: +$container->setFactory( + Zend\Expressive\Middleware\ErrorResponseGenerator::class, + Zend\Expressive\Container\WhoopsErrorResponseGeneratorFactory::class +); + +// If in production: +$container->setFactory( + Zend\Expressive\Middleware\ErrorResponseGenerator::class, + Zend\Expressive\Container\ErrorResponseGeneratorFactory::class +); + +return $container; +``` + +Your bootstrap (typically `public/index.php`) will then look like this: + +```php +chdir(dirname(__DIR__)); +require 'vendor/autoload.php'; +$container = require 'config/container.php'; +$app = $container->get(\Zend\Expressive\Application::class); + +require 'config/pipeline.php'; +require 'config/routes.php'; + +// All versions: +$app->run(); +``` + +### Configuration-Driven Container + +Alternately, you can use a configuration file to define the container. As +before, we'll define our configuration in `config/config.php`, and our +`config/container.php` file will still return our service manager instance; we'll +define the service configuration in `config/dependencies.php`: + +```php +return [ + 'services' => [ + 'config' => include __DIR__ . '/config.php', + ], + 'aliases' => [ + 'Zend\Expressive\Delegate\DefaultDelegate' => 'Zend\Expressive\Delegate\NotFoundDelegate', + ], + 'invokables' => [ + Zend\Expressive\Router\RouterInterface::class => Zend\Expressive\Router\AuraRouter::class, + Zend\Expressive\Template\TemplateRendererInterface::class => 'Zend\Expressive\Plates\PlatesRenderer::class + ], + 'factories' => [ + Zend\Expressive\Application::class => Zend\Expressive\Container\ApplicationFactory::class, + 'Zend\Expressive\Whoops' => Zend\Expressive\Container\WhoopsFactory::class, + 'Zend\Expressive\WhoopsPageHandler' => Zend\Expressive\Container\WhoopsPageHandlerFactory::class, + + Zend\Stratigility\Middleware\ErrorHandler::class => Zend\Expressive\Container\ErrorHandlerFactory::class, + Zend\Expressive\Delegate\NotFoundDelegate::class => Zend\Expressive\Container\NotFoundDelegateFactory::class, + Zend\Expressive\Middleware\NotFoundHandler::class => Zend\Expressive\Container\NotFoundHandlerFactory::class, + ], +]; +``` + +`config/container.php` becomes: + +```php +use Zend\ServiceManager\Config; +use Zend\ServiceManager\ServiceManager; + +return new ServiceManager(new Config(include 'config/dependencies.php')); +``` + +There is one problem, however: you may want to vary error handling strategies +based on whether or not you're in production: You have two choices on how to +approach this: + +- Selectively inject the factory in the bootstrap. +- Define the final handler service in an environment specific file and use file + globbing to merge files. + +In the first case, you would change the `config/container.php` example to look +like this: + +```php +use Zend\ServiceManager\Config; +use Zend\ServiceManager\ServiceManager; + +$container = new ServiceManager(new Config(include 'config/container.php')); +switch ($variableOrConstantIndicatingEnvironment) { + case 'development': + $container->setFactory( + Zend\Expressive\Middleware\ErrorResponseGenerator::class, + Zend\Expressive\Container\WhoopsErrorResponseGeneratorFactory::class + ); + break; + case 'production': + default: + $container->setFactory( + Zend\Expressive\Middleware\ErrorResponseGenerator::class, + Zend\Expressive\Container\ErrorResponseGeneratorFactory::class + ); +} +return $container; +``` + +In the second case, you will need to install zend-config: + +```bash +$ composer require zendframework/zend-config +``` + +Then, create the directory `config/autoload/`, and create two files, +`dependencies.global.php` and `dependencies.local.php`. In your `.gitignore`, +add an entry for `config/autoload/*local.php` to ensure "local" +(environment-specific) files are excluded from the repository. + +`config/dependencies.php` will look like this: + +```php +use Zend\Config\Factory as ConfigFactory; + +return ConfigFactory::fromFiles( + glob('config/autoload/dependencies.{global,local}.php', GLOB_BRACE) +); +``` + +`config/autoload/dependencies.global.php` will look like this: + +```php +return [ + 'services' => [ + 'config' => include __DIR__ . '/config.php', + ], + 'aliases' => [ + 'Zend\Expressive\Delegate\DefaultDelegate' => Zend\Expressive\Delegate\NotFoundDelegate::class, + ], + 'invokables' => [ + Zend\Expressive\Router\RouterInterface::class => Zend\Expressive\Router\AuraRouter::class, + Zend\Expressive\Template\TemplateRendererInterface::class => 'Zend\Expressive\Plates\PlatesRenderer::class + ], + 'factories' => [ + Zend\Expressive\Application::class => Zend\Expressive\Container\ApplicationFactory::class, + 'Zend\Expressive\Whoops' => Zend\Expressive\Container\WhoopsFactory::class, + 'Zend\Expressive\WhoopsPageHandler' => Zend\Expressive\Container\WhoopsPageHandlerFactory::class, + + Zend\Expressive\Middleware\ErrorResponseGenerator::class => Zend\Expressive\Container\ErrorResponseGeneratorFactory::class, + Zend\Stratigility\Middleware\ErrorHandler::class => Zend\Expressive\Container\ErrorHandlerFactory::class, + 'Zend\Expressive\Delegate\NotFoundDelegate' => Zend\Expressive\Container\NotFoundDelegateFactory::class, + Zend\Expressive\Middleware\NotFoundHandler::class => Zend\Expressive\Container\NotFoundHandlerFactory::class, + ], +]; +``` + +`config/autoload/dependencies.local.php` on your development machine can look +like this: + +```php +return [ + 'factories' => [ + 'Zend\Expressive\Whoops' => Zend\Expressive\Container\WhoopsFactory::class, + 'Zend\Expressive\WhoopsPageHandler' => Zend\Expressive\Container\WhoopsPageHandlerFactory::class, + Zend\Expressive\Middleware\ErrorResponseGenerator::class => 'Zend\Expressive\Container\WhoopsErrorResponseGeneratorFactory::class, + ], +]; +``` + +Using the above approach allows you to keep the bootstrap file minimal and +agnostic of environment. (Note: you can take a similar approach with +the application configuration.) diff --git a/docs/book/v3/features/emitters.md b/docs/book/v3/features/emitters.md new file mode 100644 index 00000000..40cf5271 --- /dev/null +++ b/docs/book/v3/features/emitters.md @@ -0,0 +1,31 @@ +# Emitters + +To simplify the usage of Expressive, we added the `run()` method, which handles +the incoming request, and emits a response. + +The latter aspect, emitting the response, is the responsibility of an +[emitter](https://docs.zendframework.com/zend-httphandlerrunner/emitters/). +An emitter accepts a response instance, and then does something with it, usually +sending the response back to a browser. + +zend-httphandlerrunner defines an `EmitterInterface`, and three emitter +implementations. Two of these, `Zend\HttpHandlerRunner\Emitter\SapiEmitter` and +`Zend\HttpHandlerRunner\Emitter\SapiStreamEmitter`, send headers and output +using PHP's standard SAPI mechanisms (the `header()` method and the output +buffer). + +We recognize that there are times when you may want to use alternate emitter +implementations; for example, if you use [React](http://reactphp.org), the SAPI +emitter will likely not work for you. + +To facilitate alternate emitters, we offer two facilities: + +- First, a `Zend\HttpHandlerRunner\RequestHandlerRunner` instance is composed + in the `Application` instance, and you can specify an alternate + emitter during instantiation, or via the `Zend\HttpHandlerRunner\Emitter\EmitterInterface` + service when using the container factory. +- Second, we provide `Zend\HttpHandlerRunner\Emitter\EmitterStack`, which allows + you to compose multiple emitter strategies; the first to return a boolean true + will cause execution of the stack to short-circuit. The `RequestHandlerRunner` + service composes an `EmitterStack` by default, with an `SapiEmitter` composed + at the bottom of the stack. diff --git a/docs/book/v3/features/error-handling.md b/docs/book/v3/features/error-handling.md new file mode 100644 index 00000000..5b25d81d --- /dev/null +++ b/docs/book/v3/features/error-handling.md @@ -0,0 +1,318 @@ +# Error Handling + +We recommend that your code raise exceptions for conditions where it cannot +gracefully recover. Additionally, we recommend that you have a reasonable PHP +`error_reporting` setting that includes warnings and fatal errors: + +```php +error_reporting(E_ALL & ~E_USER_DEPRECATED & ~E_DEPRECATED & ~E_STRICT & ~E_NOTICE); +``` + +If you follow these guidelines, you can then write or use middleware that does +the following: + +- sets an error handler that converts PHP errors to `ErrorException` instances. +- wraps execution of the handler (`$handler->handle()`) with a try/catch block. + +As an example: + +```php +function (ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface +{ + set_error_handler(function ($errno, $errstr, $errfile, $errline) { + if (! (error_reporting() & $errno)) { + // Error is not in mask + return; + } + throw new ErrorException($errstr, 0, $errno, $errfile, $errline); + }); + + try { + $response = $handler->handle($request); + return $response; + } catch (Throwable $e) { + } + + restore_error_handler(); + + $response = new TextResponse(sprintf( + "[%d] %s\n\n%s", + $e->getCode(), + $e->getMessage(), + $e->getTraceAsString() + ), 500); +} +``` + +You would then pipe this as the outermost (or close to outermost) layer of your +application: + +```php +$app->pipe($errorMiddleware); +``` + +So that you do not need to do this, we provide an error handler for you, via +zend-stratigility: `Zend\Stratigility\Middleware\ErrorHandler`. + +This implementation allows you to both: + +- provide a response generator, invoked when an error is caught; and +- register listeners to trigger when errors are caught. + +We provide the factory `Zend\Expressive\Container\ErrorHandlerFactory` for +generating the instance; it should be mapped to the service +`Zend\Stratigility\Middleware\ErrorHandler`. + +We provide two error response generators for you: + +- `Zend\Expressive\Middleware\ErrorResponseGenerator`, which optionally will + accept a `Zend\Expressive\Template\TemplateRendererInterface` instance, and a + template name. When present, these will be used to generate response content; + otherwise, a plain text response is generated that notes the request method + and URI. + +- `Zend\Expressive\Middleware\WhoopsErrorResponseGenerator`, which uses + [whoops](http://filp.github.io/whoops/) to present detailed exception + and request information; this implementation is intended for development + purposes. + +Each also has an accompanying factory for generating the instance: + +- `Zend\Expressive\Container\ErrorResponseGeneratorFactory` +- `Zend\Expressive\Container\WhoopsErrorResponseGeneratorFactory` + +Map the service `Zend\Expressive\Middleware\ErrorResponseGenerator` to one of +these two factories in your configuration: + +```php +use Zend\Expressive\Container; +use Zend\Expressive\Middleware; +use Zend\Stratigility\Middleware\ErrorHandler; + +return [ + 'dependencies' => [ + 'factories' => [ + ErrorHandler::class => Container\ErrorHandlerFactory::class, + Middleware\ErrorResponseGenerator::class => Container\ErrorResponseGeneratorFactory::class, + ], + ], +]; +``` + +> ### Use development mode configuration to enable whoops +> +> You can specify the above in one of your `config/autoload/*.global.php` files, +> to ensure you have a production-capable error response generator. +> +> If you are using [zf-development-mode](https://github.com/zfcampus/zf-development-mode) +> in your application (which is provided by default in the skeleton +> application), you can toggle usage of whoops by adding configuration to the file +> `config/autoload/development.local.php.dist`: +> +> ```php +> use Zend\Expressive\Container; +> use Zend\Expressive\Middleware; +> +> return [ +> 'dependencies' => [ +> 'factories' => [ +> Middleware\WhoopsErrorResponseGenerator::class => Container\WhoopsErrorResponseGeneratorFactory::class, +> ], +> ], +> ]; +> ``` +> +> When you enable development mode, whoops will then be enabled; when you +> disable development mode, you'll be using your production generator. +> +> If you are not using zf-development-mode, you can define a +> `config/autoload/*.local.php` file with the above configuration whenever you +> want to enable whoops. + +## Listening for errors + +When errors occur, you may want to _listen_ for them in order to provide +features such as logging. `Zend\Stratigility\Middleware\ErrorHandler` provides +the ability to do so via its `attachListener()` method. + +This method accepts a callable with the following signature: + +```php +function ( + Throwable $error, + ServerRequestInterface $request, + ResponseInterface $response +) : void +``` + +The response provided is the response returned by your error response generator, +allowing the listener the ability to introspect the generated response as well. + +As an example, you could create a logging listener as follows: + +```php +namespace Acme; + +use Exception; +use Psr\Log\LoggerInterface; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Throwable; + +class LoggingErrorListener +{ + /** + * Log format for messages: + * + * STATUS [METHOD] path: message + */ + const LOG_FORMAT = '%d [%s] %s: %s'; + + private $logger; + + public function __construct(LoggerInterface $logger) + { + $this->logger = $logger; + } + + public function __invoke(Throwable $error, ServerRequestInterface $request, ResponseInterface $response) + { + $this->logger->error(sprintf( + self::LOG_FORMAT, + $response->getStatusCode(), + $request->getMethod(), + (string) $request->getUri(), + $error->getMessage() + )); + } +} +``` + +You could then use a [delegator factory](container/delegator-factories.md) to +create your logger listener and attach it to your error handler: + +```php +namespace Acme; + +use Psr\Container\ContainerInterface; +use Psr\Log\LoggerInterface; +use Zend\Stratigility\Middleware\ErrorHandler; + +class LoggingErrorListenerDelegatorFactory +{ + public function __invoke(ContainerInterface $container, string $name, callable $callback) : ErrorHandler + { + $listener = new LoggingErrorListener($container->get(LoggerInterface::class)); + $errorHandler = $callback(); + $errorHandler->attachListener($listener); + return $errorHandler; + } +} +``` + +## Handling more specific error types + +You could also write more specific error handlers. As an example, you might want +to catch `UnauthorizedException` instances specifically, and display a login +page: + +```php +function (ServerRequestInterface $request, RequestHandlerInterface $handler) use ($renderer) : ResponseInterface +{ + try { + $response = $handler->handle($request); + return $response; + } catch (UnauthorizedException $e) { + } + + return new HtmlResponse( + $renderer->render('error::unauthorized'), + 401 + ); +} +``` + +You could then push this into a middleware pipe only when it's needed: + +```php +$app->get('/dashboard', [ + $unauthorizedHandlerMiddleware, + $middlewareThatChecksForAuthorization, + $middlewareBehindAuthorizationWall, +], 'dashboard'); +``` + +## Default delegates + +`Zend\Expressive\Application` manages an internal middleware pipeline; when you +call `$handler->handle()`, `Application` is popping off the next middleware in +the queue and dispatching it. + +What happens when that queue is exhausted? + +That situation indicates an error condition: no middleware was capable of +returning a response. This could either mean a problem with the request (HTTP +400 "Bad Request" status) or inability to route the request (HTTP 404 "Not +Found" status). + +In order to report that information, we provide a specialized handler, +`Zend\Expressive\Handler\NotFoundHandler`, which you should compose as the +innermost layer of your application pipeline. It will report a 404 +response, optionally using a composed template renderer to do so. + +We provide a factory, `Zend\Expressive\Container\NotFoundHandlerFactory`, for +creating an instance, and this should be mapped to the +`Zend\Expressive\Handler\NotFoundHandler` service: + +```php +use Zend\Expressive\Container; +use Zend\Expressive\Handler; + +return [ + 'dependencies' => [ + 'factories' => [ + Handler\NotFoundHandler::class => Container\NotFoundHandlerFactory::class, + ], + ], +]; +``` + +The factory will consume the following services: + +- `Zend\Expressive\Template\TemplateRendererInterface` (optional): if present, + the renderer will be used to render a template for use as the response + content. + +- `config` (optional): if present, it will use the + `$config['zend-expressive']['error_handler']['template_404']` value + as the template to use when rendering; if not provided, defaults to + `error::404`. + +If you wish to provide an alternate response status or use a canned response, +you should provide your own handler and pipe it to your application. + +## Page not found + +Error handlers work at the outermost layer, and are used to catch exceptions and +errors in your application. At the _innermost_ layer of your application, you +should ensure you have middleware that is _guaranteed_ to return a response; +this will prevent the default delegate from needing to execute by ensuring that +the middleware queue never fully depletes. This in turn allows you to fully +craft what sort of response is returned. + +Generally speaking, reaching the innermost middleware layer indicates that no +middleware was capable of handling the request, and thus an HTTP 404 Not Found +condition. + +To simplify such responses, we provide `Zend\Expressive\Handler\NotFoundHandler`, +detailed int he above section. + +You should pipe it as the innermost layer of your application: + +```php +// A basic application: +$app->pipe(ErrorHandler::class); +$app->pipe(PathBasedRoutingMiddleware::class); +$app->pipe(DispatchMiddleware::class); +$app->pipe(NotFoundHandler::class); +``` diff --git a/docs/book/v3/features/helpers/body-parse.md b/docs/book/v3/features/helpers/body-parse.md new file mode 100644 index 00000000..762538c0 --- /dev/null +++ b/docs/book/v3/features/helpers/body-parse.md @@ -0,0 +1,197 @@ +# Body Parsing Middleware + +`Zend\Expressive\Helper\BodyParams\BodyParamsMiddleware` provides generic +[PSR-7](https://www.php-fig.org/psr/psr-7/) middleware for parsing the request +body into parameters, and returning a new request instance that composes them. +The subcomponent provides a strategy pattern around matching the request +`Content-Type`, and then parsing it, giving you a flexible approach that can +grow with your accepted content types. + +By default, this middleware will detect the following content types: + +- `application/x-www-form-urlencoded` (standard web-based forms, without file + uploads) +- `application/json`, `application/*+json` (JSON payloads) + +## Registering the middleware + +You can register it programmatically: + +```php +$app->pipe(BodyParamsMiddleware::class); +``` + +Alternately, register it via configuration, if using configuration-based applications: + +```php +// config/autoload/middleware-pipeline.global.php +use Zend\Expressive\Helper; + +return [ + 'dependencies' => [ + 'invokables' => [ + Helper\BodyParams\BodyParamsMiddleware::class => Helper\BodyParams\BodyParamsMiddleware::class, + /* ... */ + ], + 'factories' => [ + /* ... */ + ], + ], + 'middleware_pipeline' => [ + ['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, + ], + /* ... */ + ], +]; +``` + +Since body parsing does not necessarily need to happen for every request, you +can also choose to incorporate it in route-specific middleware pipelines: + +```php +$app->post('/login', [ + BodyParamsMiddleware::class, + LoginMiddleware::class, +]); +``` + +If using a configuration-based application: + +```php +// config/autoload/routes.global.php +use Zend\Expressive\Helper\BodyParams\BodyParamsMiddleware; + +return [ + 'dependencies' => [ + 'invokables' => [ + Helper\BodyParams\BodyParamsMiddleware::class => Helper\BodyParams\BodyParamsMiddleware::class, + /* ... */ + ], + 'factories' => [ + /* ... */ + ], + ], + 'routes' => [ + [ + 'name' => 'contact:process', + 'path' => '/contact/process', + 'middleware' => [ + BodyParamsMiddleware::class, + Contact\Process::class, + ], + 'allowed_methods' => ['POST'], + ] + ], +]; +``` + +Using route-based middleware pipelines has the advantage of ensuring that the +body parsing middleware only executes for routes that require the processing. +While the middleware has some checks to ensure it only triggers for HTTP +methods that accept bodies, those checks are still overhead that you might want +to avoid; the above strategy of using the middleware only with specific routes +can accomplish that. + +## Strategies + +If you want to intercept and parse other payload types, you can add *strategies* +to the middleware. Strategies implement `Zend\Expressive\Helper\BodyParams\StrategyInterface`: + +```php +namespace Zend\Expressive\Helper\BodyParams; + +use Psr\Http\Message\ServerRequestInterface; + +interface StrategyInterface +{ + /** + * Match the content type to the strategy criteria. + * + * @param string $contentType + * @return bool Whether or not the strategy matches. + */ + public function match($contentType); + + /** + * Parse the body content and return a new response. + * + * @param ServerRequestInterface $request + * @return ServerRequestInterface + */ + public function parse(ServerRequestInterface $request); +} +``` + +You then register them with the middleware using the `addStrategy()` method: + +```php +$bodyParams->addStrategy(new MyCustomBodyParamsStrategy()); +``` + +To automate the registration, we recommend writing a factory for the +`BodyParamsMiddleware`, and replacing the `invokables` registration with a +registration in the `factories` section of the `middleware-pipeline.config.php` +file: + +```php +use Zend\Expressive\Helper\BodyParams\BodyParamsMiddleware; + +class MyCustomBodyParamsStrategyFactory +{ + public function __invoke($container) + { + $bodyParams = new BodyParamsMiddleware(); + $bodyParams->addStrategy(new MyCustomBodyParamsStrategy()); + return $bodyParams; + } +} + +// In config/autoload/middleware-pipeline.config.php: +use Zend\Expressive\Helper; + +return [ + 'dependencies' => [ + 'invokables' => [ + // Remove this line: + Helper\BodyParams\BodyParamsMiddleware::class => Helper\BodyParams\BodyParamsMiddleware::class, + /* ... */ + ], + 'factories' => [ + // Add this line: + Helper\BodyParams\BodyParamsMiddleware::class => MyCustomBodyParamsStrategyFactory::class, + /* ... */ + ], + ], +]; +``` + +## Removing the default strategies + +By default, `BodyParamsMiddleware` composes the following strategies: + +- `Zend\Expressive\Helper\BodyParams\FormUrlEncodedStrategy` +- `Zend\Expressive\Helper\BodyParams\JsonStrategy` + +These provide the most basic approaches to parsing the request body. They +operate in the order they do to ensure the most common content type — +`application/x-www-form-urlencoded` — matches first, as the middleware +delegates parsing to the first match. + +If you do not want to use these default strategies, you can clear them from the +middleware using `clearStrategies()`: + +```php +$bodyParamsMiddleware->clearStrategies(); +``` + +Note: if you do this, **all** strategies will be removed! As such, we recommend +doing this only immediately before registering any custom strategies you might +be using. diff --git a/docs/book/v3/features/helpers/content-length.md b/docs/book/v3/features/helpers/content-length.md new file mode 100644 index 00000000..14d3dd9f --- /dev/null +++ b/docs/book/v3/features/helpers/content-length.md @@ -0,0 +1,81 @@ +# Content-Length Middleware + +- **Available since zend-expressive-helpers version 4.1.0.** + +In some cases, you may want to include an explicit `Content-Length` response +header, without having to inject it manually. To facilitate this, we provide +`Zend\Expressive\Helper\ContentLengthMiddleware`. + +> ### When to use this middleware +> +> In most cases, you do not need to provide an explicit Content-Length value +> in your responses. While the HTTP/1.1 specification indicates the header +> SHOULD be provided, most clients will not degrade to HTTP/1.0 if the header +> is omitted. +> +> The one exception that has been reported is when working with +> [New Relic](https://newrelic.com), which requires valid `Content-Length` +> headers for some of its analytics; in such cases, enabling this middleware +> will fix those situations. + +This middleware delegates the request, and operates on the returned response. It +will return a new response with the `Content-Length` header injected under the +following conditions: + +- No `Content-Length` header is already present AND +- the body size is non-null. + +To register it in your application, you will need to do two things: register the +middleware with the container, and register the middleware in either your +application pipeline, or within routed middleware. + +To add it to your container, add the following configuration: + +```php +// In a `config/autoload/*.global.php` file, or a `ConfigProvider` class: + +use Zend\Expressive\Helper; + +return [ + 'dependencies' => [ + 'invokables' => [ + Helper\ContentLengthMiddleware::class => Helper\ContentLengthMiddleware::class, + ], + ], +]; +``` + +To register it as pipeline middleware to execute on any request: + +```php +// In `config/pipeline.php`: + +use Zend\Expressive\Helper; + +$app->pipe(Helper\ContentLengthMiddleware::class); +``` + +To register it within a routed middleware pipeline: + +```php +// In `config/routes.php`: + +use Zend\Expressive\Helper; + +$app->get('/download/tarball', [ + Helper\ContentLengthMiddleware::class, + Download\Tarball::class, +], 'download-tar'); +``` + +## Caveats + +One caveat to note is that if you use this middleware, but also write directly +to the output buffer (e.g., via a `var_dump`, or if `display_errors` is on and +an uncaught error or exception occurs), the output will not appear as you +expect. Generally in such situations, the contents of the output buffer will +appear, up to the specified `Content-Length` value. This can lead to truncated +error content and/or truncated application content. + +We recommend that if you use this feature, you also use a PHP error and/or +exception handler that logs errors in order to prevent truncated output. diff --git a/docs/book/v3/features/helpers/intro.md b/docs/book/v3/features/helpers/intro.md new file mode 100644 index 00000000..621f51bf --- /dev/null +++ b/docs/book/v3/features/helpers/intro.md @@ -0,0 +1,23 @@ +# Helpers + +Some tasks and features will be common to many if not all applications. For +those, Expressive provides *helpers*. These are typically utility classes that +may integrate features or simply provide standalone benefits. + +Currently, these include: + +- [Body Parsing Middleware](body-parse.md) +- [Content-Length Middleware](content-length.md) (since zend-expressive-helpers 4.1.0) +- [UrlHelper](url-helper.md) +- [ServerUrlHelper](server-url-helper.md) + +## Installation + +If you started your project using the Expressive skeleton package, the helpers +are already installed. + +If not, you can install them as follows: + +```bash +$ composer require zendframework/zend-expressive-helpers +``` diff --git a/docs/book/v3/features/helpers/server-url-helper.md b/docs/book/v3/features/helpers/server-url-helper.md new file mode 100644 index 00000000..d6a2aa28 --- /dev/null +++ b/docs/book/v3/features/helpers/server-url-helper.md @@ -0,0 +1,159 @@ +# ServerUrlHelper + +`Zend\Expressive\Helper\ServerUrlHelper` provides the ability to generate a full +URI by passing only the path to the helper; it will then use that path with the +current `Psr\Http\Message\UriInterface` instance provided to it in order to +generate a fully qualified URI. + +## Usage + +When you have an instance, use either its `generate()` method, or call the +instance as an invokable: + +```php +// Using the generate() method: +$url = $helper->generate('/foo'); + +// is equivalent to invocation: +$url = $helper('/foo'); +``` + +The helper is particularly useful when used in conjunction with the +[UrlHelper](url-helper.md), as you can then create fully qualified URIs for use +with headers, API hypermedia links, etc.: + +```php +$url = $serverUrl($url('resource', ['id' => 'sha1'])); +``` + +The signature for the ServerUrlHelper `generate()` and `__invoke()` methods is: + +```php +function ($path = null) : string +``` + +Where: + +- `$path`, when provided, can be a string path to use to generate a URI. + +## Creating an instance + +In order to use the helper, you will need to inject it with the current +`UriInterface` from the request instance. To automate this, we provide +`Zend\Expressive\Helper\ServerUrlMiddleware`, which composes a `ServerUrl` +instance, and, when invoked, injects it with the URI instance. + +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 pipeline middleware, anytime + before the routing middleware. + +The following examples demonstrate registering the services. + +```php +use Zend\Expressive\Helper\ServerUrlHelper; +use Zend\Expressive\Helper\ServerUrlMiddleware; +use Zend\Expressive\Helper\ServerUrlMiddlewareFactory; + +// zend-servicemanager: +$services->setInvokableClass(ServerUrlHelper::class, ServerUrlHelper::class); +$services->setFactory(ServerUrlMiddleware::class, ServerUrlMiddlewareFactory::class); + +// Pimple: +$pimple[ServerUrlHelper::class] = function ($container) { + return new ServerUrlHelper(); +}; +$pimple[ServerUrlMiddleware::class] = function ($container) { + $factory = new ServerUrlMiddlewareFactory(); + return $factory($container); +}; + +// Aura.Di: +$container->set(ServerUrlHelper::class, $container->lazyNew(ServerUrlHelper::class)); +$container->set(ServerUrlMiddlewareFactory::class, $container->lazyNew(ServerUrlMiddlewareFactory::class)); +$container->set( + ServerUrlMiddleware::class, + $container->lazyGetCall(ServerUrlMiddlewareFactory::class, '__invoke', $container) +); +``` + +To register the `ServerUrlMiddleware` as pipeline middleware anytime before the +routing middleware: + +```php +use Zend\Expressive\Helper\ServerUrlMiddleware; + +// Programmatically: +$app->pipe(ServerUrlMiddleware::class); +$app->pipeRoutingMiddleware(); +$app->pipeDispatchMiddleware(); + +// Or use configuration: +// [ +// 'middleware_pipeline' => [ +// ['middleware' => ServerUrlMiddleware::class, 'priority' => PHP_INT_MAX], +// /* ... */ +// ], +// ] +``` + +The following dependency configuration will work for all three when using the +Expressive skeleton: + +```php +return [ + 'dependencies' => [ + 'invokables' => [ + ServerUrlHelper::class => ServerUrlHelper::class, + ], + 'factories' => [ + ServerUrlMiddleware::class => ServerUrlMiddlewareFactory::class, + ], + ], + 'middleware_pipeline' => [ + ['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` pipeline middleware. + +## Using the helper in middleware + +Compose the helper in your middleware (or elsewhere), and then use it to +generate URI paths: + +```php +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\MiddlewareInterfacel +use Psr\Http\Server\RequestHandlerInterface; +use Zend\Expressive\Helper\ServerUrlHelper; + +class FooMiddleware implements MiddlewareInterface +{ + private $helper; + + public function __construct(ServerUrlHelper $helper) + { + $this->helper = $helper; + } + + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface + { + $response = $handler->handle($request); + + return $response->withHeader( + 'Link', + $this->helper->generate() . '; rel="self"' + ); + } +} +``` diff --git a/docs/book/v3/features/helpers/url-helper.md b/docs/book/v3/features/helpers/url-helper.md new file mode 100644 index 00000000..a7a701de --- /dev/null +++ b/docs/book/v3/features/helpers/url-helper.md @@ -0,0 +1,307 @@ +# UrlHelper + +`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 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 + +When you have an instance, use either its `generate()` method, or call the +instance as an invokable: + +```php +// Using the generate() method: +$url = $helper->generate('resource', ['id' => 'sha1']); + +// is equivalent to invocation: +$url = $helper('resource', ['id' => 'sha1']); +``` + +The signature for both is: + +```php +function ( + $routeName, + array $routeParams = [], + $queryParams = [], + $fragmentIdentifier = null, + array $options = [] +) : string +``` + +Where: + +- `$routeName` is the name of a route defined in the composed router. You may + omit this argument if you want to generate the path for the currently matched + request. +- `$routeParams` is an array of substitutions to use for the provided route, with the + following behavior: + - If a `RouteResult` is composed in the helper, and the `$routeName` matches + it, the provided `$params` will be merged with any matched parameters, with + those provided taking precedence. + - If a `RouteResult` is not composed, or if the composed result does not match + the provided `$routeName`, then only the `$params` provided will be used + for substitutions. + - If no `$params` are provided, and the `$routeName` matches the currently + matched route, then any matched parameters found will be used. + parameters found will be used. + - If no `$params` are provided, and the `$routeName` does not match the + currently matched route, or if no route result is present, then no + substitutions will be made. +- `$queryParams` is an array of query string arguments to include in the + generated URI. +- `$fragmentIdentifier` is a string to use as the URI fragment. +- `$options` is an array of options to provide to the router for purposes of + controlling URI generation. As an example, zend-router can consume "translator" + and "text_domain" options in order to provide translated URIs. + +Each method will raise an exception if: + +- No `$routeName` is provided, and no `RouteResult` is composed. +- No `$routeName` is provided, a `RouteResult` is composed, but that result + represents a matching failure. +- The given `$routeName` is not defined in the router. + +> ### Signature changes +> +> The signature listed above is current as of version 3.0.0 of +> zendframework/zend-expressive-helpers. Prior to that version, the helper only +> accepted the route name and route parameters. + +## Creating an instance + +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 +[PSR-11 Container](https://www.php-fig.org/psr/psr-11/). Additionally, +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 pipeline middleware, immediately + following the routing middleware. + +### Registering the helper service + +The following examples demonstrate programmatic registration of the `UrlHelper` +service in your selected dependency injection container. + +```php +use Zend\Expressive\Helper\UrlHelper; +use Zend\Expressive\Helper\UrlHelperFactory; + +// zend-servicemanager: +$services->setFactory(UrlHelper::class, UrlHelperFactory::class); + +// Pimple: +$pimple[UrlHelper::class] = function ($container) { + $factory = new UrlHelperFactory(); + return $factory($container); +}; + +// Aura.Di: +$container->set(UrlHelperFactory::class, $container->lazyNew(UrlHelperFactory::class)); +$container->set( + UrlHelper::class, + $container->lazyGetCall(UrlHelperFactory::class, '__invoke', $container) +); +``` + +The following dependency configuration will work for all three when using the +Expressive skeleton: + +```php +return ['dependencies' => [ + 'factories' => [ + UrlHelper::class => UrlHelperFactory::class, + ], +]] +``` + +> #### UrlHelperFactory requires RouterInterface +> +> The factory requires that a service named `Zend\Expressive\Router\RouterInterface` is present, +> and will raise an exception if the service is not found. + +### Registering the pipeline middleware + +To register the `UrlHelperMiddleware` as pipeline middleware following the +routing middleware: + +```php +use Zend\Expressive\Helper\UrlHelperMiddleware; + +// Programmatically: +$app->pipeRoutingMiddleware(); +$app->pipe(UrlHelperMiddleware::class); +$app->pipeDispatchMiddleware(); + +// Or use configuration: +// [ +// 'middleware_pipeline' => [ +// /* ... */ +// Zend\Expressive\Application::ROUTING_MIDDLEWARE, +// ['middleware' => UrlHelperMiddleware::class], +// Zend\Expressive\Application::DISPATCH_MIDDLEWARE, +// /* ... */ +// ], +// ] +// +// Alternately, create a nested middleware pipeline for the routing, UrlHelper, +// and dispatch middleware: +// [ +// 'middleware_pipeline' => [ +// /* ... */ +// 'routing' => [ +// 'middleware' => [ +// Zend\Expressive\Application::ROUTING_MIDDLEWARE, +// UrlHelperMiddleware::class +// Zend\Expressive\Application::DISPATCH_MIDDLEWARE, +// ], +// 'priority' => 1, +// ], +// /* ... */ +// ], +// ] + +``` + +The following dependency configuration will work for all three when using the +Expressive skeleton: + +```php +return [ + 'dependencies' => [ + 'factories' => [ + UrlHelper::class => UrlHelperFactory::class, + UrlHelperMiddleware::class => UrlHelperMiddlewareFactory::class, + ], + ], + 'middleware_pipeline' => [ + Zend\Expressive\Application::ROUTING_MIDDLEWARE, + ['middleware' => UrlHelperMiddleware::class], + Zend\Expressive\Application::DISPATCH_MIDDLEWARE, + ], +]; + +// OR: +return [ + 'dependencies' => [ + 'factories' => [ + UrlHelper::class => UrlHelperFactory::class, + UrlHelperMiddleware::class => UrlHelperMiddlewareFactory::class, + ], + ], + 'middleware_pipeline' => [ + 'routing' => [ + 'middleware' => [ + Zend\Expressive\Application::ROUTING_MIDDLEWARE, + UrlHelperMiddleware::class, + Zend\Expressive\Application::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` pipeline middleware. + +## Using the helper in middleware + +Compose the helper in your middleware (or elsewhere), and then use it to +generate URI paths: + +```php +helper = $helper; + } + + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface + { + $response = $handler->handle($request); + return $response->withHeader( + 'Link', + $this->helper->generate('resource', ['id' => 'sha1']) + ); + } +} +``` + +## Base Path support + +If your application is running under a subdirectory, or if you are running +pipeline middleware that is intercepting on a subpath, the paths generated +by the router may not reflect the *base path*, and thus be invalid. To +accommodate this, the `UrlHelper` supports injection of the base path; when +present, it will be prepended to the path generated by the router. + +As an example, perhaps you have middleware running to intercept a language +prefix in the URL; this middleware could then inject the `UrlHelper` with the +detected language, before stripping it off the request URI instance to pass on +to the router: + +```php +helper = $helper; + } + + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface + { + $uri = $request->getUri(); + $path = $uri->getPath(); + if (! preg_match('#^/(?P[a-z]{2,3}([-_][a-zA-Z]{2}|))/#', $path, $matches)) { + return $handler->handle($request); + } + + $locale = $matches['locale']; + Locale::setDefault(Locale::canonicalize($locale)); + $this->helper->setBasePath($locale); + + return $handler->handle($request->withUri( + $uri->withPath(substr($path, (strlen($locale) + 1))) + )); + } +} +``` + +(Note: if the base path injected is not prefixed with `/`, the helper will add +the slash.) + +Paths generated by the `UriHelper` from this point forward will have the +detected language prefix. diff --git a/docs/book/v3/features/middleware-types.md b/docs/book/v3/features/middleware-types.md new file mode 100644 index 00000000..d381306f --- /dev/null +++ b/docs/book/v3/features/middleware-types.md @@ -0,0 +1,114 @@ +# Middleware Types + +Expressive allows you to compose applications out of _pipeline_ and _routed_ +middleware. + +**Pipeline** middleware is middleware that defines the workflow of your +application. These generally run on every execution of the application, and +include such aspects as: + +- Error handling +- Locale detection +- Session setup +- Authentication and authorization + +**Routed** middleware is middleware that responds only to specific URI paths and +HTTP methods. As an example, you might want middleware that only responds to +HTTP POST requests to the path `/users`. + +Expressive allows you to define middleware using any of the following: + +- [PSR-15 middleware](https://www.php-fig.org/psr/psr-15/) instances. +- [PSR-15 request handler](https://www.php-fig.org/psr/psr-15/) instances. +- Service names resolving to one of the above middleware types. +- Callable middleware that implements the PSR-15 `MiddlewareInterface` signature. +- Middleware pipelines expressed as arrays of the above middleware types. + +## PSR-15 middleware + +The PSR-15 specification covers HTTP server middleware and request handlers that +consume [PSR-7](http://www.php-fig.org/psr/psr-7) HTTP messages. Expressive +accepts both middleware that implements the `MiddlewareInterface` and request +handlers that implement `RequestHandlerInterface`. As an example: + +```php +use Interop\Http\Server\MiddlewareInterface; +use Interop\Http\Server\RequestHandlerInterface; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; + +class SomeMiddleware implements MiddlewareInterface +{ + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface + { + // do something and return a response, or + // delegate to another request handler capable + // of returning a response via: + // + // return $handler->handle($request); + } +} +``` + +You could also implement such middleware via an anonymous class. + +## Callable middleware + +Sometimes you may not want to create a class for one-off middleware. As such, +Expressive allows you to provide a PHP callable that uses the same signature as +`Psr\Http\Server\MiddlewareInterface`: + +```php +use Psr\Http\Server\RequestHandlerInterface; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; + +function (ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface +{ + // do something and return a response, or + // delegate to another request handler capable + // of returning a response via: + // + // return $handler->handle($request); +} +``` + +One note: the `$request` argument does not require a typehint, and examples +throughout the manual will omit the typehint when demonstrating callable +middleware. + +## Service-based middleware + +We encourage the use of a dependency injection container for providing your +middleware. As such, Expressive also allows you to use _service names_ for both +pipeline and routed middleware. Generally, service names will be the specific +middleware class names, but can be any valid string that resolves to a service. + +When Expressive is provided a service name for middleware, it internally +decorates the middleware in a `Zend\Expressive\Middleware\LazyLoadingMiddleware` +instance, allowing it to be loaded only when dispatched. + +## Middleware pipelines + +Expressive allows any pipeline or routed middleware to be self-contained +[middleware pipelines](https://docs.zendframework.com/zend-stratigility/api/#middleware). +To prevent the need for instantiating a `Zend\Stratigility\MiddlewarePipe` or +`Zend\Expressive\Application` instance when defining the pipeline, Expressive +allows you to provide an array of middleware: + +```php +// Pipeline middleware: +$app->pipe([ + FirstMiddleware::class, + SecondMiddleware::class, +]); + +// Routed middleware: +$app->get([ + FirstMiddleware::class, + SecondMiddleware::class, +]); +``` + +The values in these arrays may be any valid middleware type as defined in this +chapter. diff --git a/docs/book/v3/features/middleware/implicit-methods-middleware.md b/docs/book/v3/features/middleware/implicit-methods-middleware.md new file mode 100644 index 00000000..ce0b9174 --- /dev/null +++ b/docs/book/v3/features/middleware/implicit-methods-middleware.md @@ -0,0 +1,141 @@ +# ImplicitHeadMiddleware and ImplicitOptionsMiddleware + +Expressive offers middleware for implicitly supporting `HEAD` and `OPTIONS` +requests. The HTTP/1.1 specifications indicate that all server implementations +_must_ support `HEAD` requests for any given URI, and that they _should_ support +`OPTIONS` requests. To make this possible, we have added features to our routing +layer, and middleware that can detect _implicit_ support for these methods +(i.e., the route was not registered _explicitly_ with the method). + +## ImplicitHeadMiddleware + +`Zend\Expressive\Middleware\ImplicitHeadMiddleware` provides support for +handling `HEAD` requests to routed middleware when the route does not expliclity +allow for the method. It should be registered _between_ the routing and dispatch +middleware. + +By default, it can be instantiated with no extra arguments. However, you _may_ +provide a response instance to use by default to the constructor if you need to +craft special headers, status code, etc. + +Register the dependency via `dependencies` configuration: + +```php +use Zend\Expressive\Middleware\ImplicitHeadMiddleware; + +return [ + 'dependencies' => [ + 'invokables' => [ + ImplicitHeadMiddleware::class => ImplicitHeadMiddleware::class, + ], + + // or, if you have defined a factory to inject a response: + 'factories' => [ + ImplicitHeadMiddleware::class => \Your\ImplicitHeadMiddlewareFactory::class, + ], + ], +]; +``` + +Within your application pipeline, add the middleware between the routing and +dispatch middleware: + +```php +$app->pipeRoutingMiddleware(); +$app->pipe(ImplicitHeadMiddleware::class); +// ... +$app->pipeDispatchMiddleware(); +``` + +(Note: if you used the `expressive-pipeline-from-config` tool to create your +programmatic pipeline, or if you used the Expressive skeleton, this middleware +is likely already in your pipeline, as is a dependency entry.) + +When in place, it will do the following: + +- If the request method is `HEAD`, AND +- the request composes a `RouteResult` attribute, AND +- the route result composes a `Route` instance, AND +- the route returns true for the `implicitHead()` method, THEN +- the middleware will return a response. + +In all other cases, it returns the result of delegating to the next middleware +layer. + +When `implicitHead()` is matched, one of two things may occur. First, if the +route does not support the `GET` method, then the middleware returns the +composed response (either the one injected at instantiation, or an empty +instance). However, if `GET` is supported, it will dispatch the next layer, but +with a `GET` request instead of `HEAD`; additionally, it will inject the +returned response with an empty response body before returning it. + +### Detecting forwarded requests + +- Since 2.1.0 + +When the next layer is dispatched, the request will have an additional +attribute, `Zend\Expressive\Middleware\ImplicitHeadMiddleware::FORWARDED_HTTP_METHOD_ATTRIBUTE`, +with a value of `HEAD`. As such, you can check for this value in order to vary +the headers returned if desired. + +## ImplicitOptionsMiddleware + +`Zend\Expressive\Middleware\ImplicitOptionsMiddleware` provides support for +handling `OPTIONS` requests to routed middleware when the route does not +expliclity allow for the method. Like the `ImplicitHeadMiddleware`, it should be +registered _between_ the routing and dispatch middleware. + +By default, it can be instantiated with no extra arguments. However, you _may_ +provide a response prototype instance to use by default to the constructor if +you need to craft special headers, status code, etc. + +Register the dependency via `dependencies` configuration: + +```php +use Zend\Expressive\Middleware\ImplicitOptionsMiddleware; + +return [ + 'dependencies' => [ + 'invokables' => [ + ImplicitOptionsMiddleware::class => ImplicitOptionsMiddleware::class, + ], + + // or, if you have defined a factory to inject a response: + 'factories' => [ + ImplicitOptionsMiddleware::class => \Your\ImplicitOptionsMiddlewareFactory::class, + ], + ], +]; +``` + +Within your application pipeline, add the middleware between the routing and +dispatch middleware: + +```php +$app->pipeRoutingMiddleware(); +$app->pipe(ImplicitOptionsMiddleware::class); +// ... +$app->pipeDispatchMiddleware(); +``` + +(Note: if you used the `expressive-pipeline-from-config` tool to create your +programmatic pipeline, or if you used the Expressive skeleton, this middleware +is likely already in your pipeline, as is a dependency entry.) + +When in place, it will do the following: + +- If the request method is `OPTIONS`, AND +- the request composes a `RouteResult` attribute, AND +- the route result composes a `Route` instance, AND +- the route returns true for the `implicitOptions()` method, THEN +- the middleware will return a response with an `Allow` header indicating + methods the route allows. + +In all other cases, it returns the result of delegating to the next middleware +layer. + +One thing to note: the allowed methods reported by the route and/or route +result, and returned via the `Allow` header, may vary based on router +implementation. In most cases, it should be an aggregate of all routes using the +same path specification; however, it *could* be only the methods supported +explicitly by the matched route. diff --git a/docs/book/v3/features/modular-applications.md b/docs/book/v3/features/modular-applications.md new file mode 100644 index 00000000..5e660e74 --- /dev/null +++ b/docs/book/v3/features/modular-applications.md @@ -0,0 +1,203 @@ +# Modular applications + +Zend Framework 2+ applications have a concept of _modules_, independent units that +can provide configuration, services, and hooks into its MVC lifecycle. This +functionality is provided by zend-modulemanager. + +Expressive provides similar functionality by incorporating two packages within +the default skeleton application: + +- [zendframework/zend-config-aggregator](https://github.com/zendframework/zend-config-aggregator), + which provides features for aggregating configuration from a variety of + sources, including: + - PHP files globbed from the filesystem that return an array of configuration. + - [zend-config](https://docs.zendframework.com/zend-config)-compatible + configuration files globbed from the filesystem. + - Configuration provider classes; these are invokable classes which return an + array of configuration. +- [zendframework/zend-component-installer](https://github.com/zendframework/zend-component-installer), + a Composer plugin that looks for an `extra.zf.config-provider` entry in a + package to install, and, if found, adds an entry for that provider to the + `config/config.php` file (if it uses zend-config-aggregator). + +These features allow you to install packages via composer and expose their +configuration — which may include dependency information — to your +application. + +## Making your application modular + +When using the Expressive installer via the skeleton application, the first +question asked is the installation type, which includes the options: + +- Minimal (no default middleware, templates, or assets; configuration only) +- Flat (flat source code structure; default selection) +- Modular (modular source code structure; recommended) + +We recommend choosing the "Modular" option from the outset. + +If you do not, you can still create and use modules in your application; +however, the initial "App" module will not be modular. + +## Module structure + +Expressive does not force you to use any particular structure for your +module; its only requirement is to expose default configuration using a "config +provider", which is simply an invokable class that returns a configuration +array. + +We generally recommend that a module have a [PSR-4](http://www.php-fig.org/psr/psr-4/) +structure, and that the module contain a `src/` directory at the minimum, along +with directories for other module-specific content, such as templates, tests, and +assets: + +```text +src/ + Acme/ + src/ + ConfigProvider.php + Container/ + VerifyUserFactory.php + Helper/ + AuthorizationHelper.php + Middleware/ + VerifyUser.php + templates/ + verify-user.php + test/ + Helper/ + AuthorizationHelperTest.php + Middleware/ + VerifyUserTest.php +``` + +If you use the above structure, you would then add an entry in your +`composer.json` file to provide autoloading: + +```json +"autoload": { + "psr-4": { + "Acme\\": "src/Acme/src/" + } +} +``` + +Don't forget to execute `composer dump-autoload` after making the change! + +## Creating and enabling a module + +The only _requirement_ for creating a module is that you define a "config +provider", which is simply an invokable class that returns a configuration +array. + +Generally, a config provider will return dependency information, and +module-specific configuration: + +```php +namespace Acme; + +class ConfigProvider +{ + public function __invoke() + { + return [ + 'dependencies' => $this->getDependencies(), + 'acme' => [ + 'some-setting' => 'default value', + ], + 'templates' => [ + 'paths' => [ + 'acme' => [__DIR__ . '/../templates'], + ], + ] + ]; + } + + public function getDependencies() + { + return [ + 'invokables' => [ + Helper\AuthorizationHelper::class => Helper\AuthorizationHelper::class, + ], + 'factories' => [ + Middleware\VerifyUser::class => Container\VerifyUserFactory::class, + ], + ]; + } +} +``` + +You would then add the config provider to the top (or towards the top) of your +`config/config.php`: + +```php +$aggregator = new ConfigAggregator([ + Acme\ConfigProvider::class, + /* ... */ +``` + +This approach allows your `config/autoload/*` files to take precedence over the +module configuration, allowing you to override the values. + +## Caching configuration + +In order to provide configuration caching, two things must occur: + +- First, you must define a `config_cache_enabled` key in your configuration + somewhere. +- Second, you must pass a second argument to the `ConfigManager`, the location + of the cache file to use. + +The `config_cache_enabled` key can be defined in any of your configuration +providers, including the autoloaded configuration files. We recommend defining +them in two locations: + +- `config/autoload/global.php` should define the value to `true`, as the + production setting. +- `config/autoload/local.php` should also define the setting, and use a value + appropriate to the current environment. In development, for instance, this + would be `false`. + +```php +// config/autoload/global.php + +return [ + 'config_cache_enabled' => true, + /* ... */ +]; + +// config/autoload/local.php + +return [ + 'config_cache_enabled' => false, // <- development! + /* ... */ +]; +``` + +You would then alter your `config/config.php` file to add the second argument. +The following example builds on the previous, and demonstrates having the +`AppConfig` entry enabled. The configuration will be cached to +`data/config-cache.php` in the application root: + +```php +$configManager = new ConfigManager([ + App\AppConfig::class, + new PhpFileProvider('config/autoload/{{,*.}global,{,*.}local}.php'), +], 'data/config-cache.php'); +``` + +When the configuration cache path is present, if the `config_cache_enabled` flag +is enabled, then configuration will be read from the cached configuration, +instead of parsing and merging the various configuration sources. + +## Final notes + +This approach may look simple, but it is flexible and powerful: + +- You pass a list of config providers to the `ConfigAggregator` constructor. +- Configuration is merged in the same order as it is passed, with later entries + having precedence. +- You can override module configuration using `*.global.php` and `*.local.php` files. +- If cached config is found, `ConfigAggregator` does not iterate over provider list. + +For more details, please refer to the [zend-config-aggregator +documentation](https://docs.zendframework.com/zend-config-aggregator/). diff --git a/docs/book/v3/features/router/aura.md b/docs/book/v3/features/router/aura.md new file mode 100644 index 00000000..685a4eea --- /dev/null +++ b/docs/book/v3/features/router/aura.md @@ -0,0 +1,244 @@ +# Using Aura.Router + +[Aura.Router](https://github.com/auraphp/Aura.Router) provides a plethora of +methods for further configuring the router instance. One of the more useful +configuration is to provide default specifications: + +- A regular expression that applies the same for a given routing match: + + ```php + // Parameters named "id" will only match digits by default: + $router->addTokens([ + 'id' => '\d+', + ]); + ``` + +- A default parameter and/or its default value to always provide: + + ```php + // mediatype defaults to "application/xhtml+xml" and will be available in all + // requests: + $router->addValues([ + 'mediatype' => 'application/xhtml+xml', + ]); + ``` + +- Only match if secure (i.e., under HTTPS): + + ```php + $router->setSecure(true); + ``` + +In order to specify these, you need access to the underlying Aura.Router +instance, however, and the `RouterInterface` does not provide an accessor! + +The answer, then, is to use dependency injection. This can be done in two ways: +programmatically, or via a factory to use in conjunction with your container +instance. + +## Installing Aura.Router + +To use Aura.Router, you will first need to install the Aura.Router integration: + +```bash +$ composer require zendframework/zend-expressive-aurarouter +``` + +## Quick Start + +At its simplest, you can instantiate a `Zend\Expressive\Router\AuraRouter` instance +with no arguments; it will create the underlying Aura.Router objects required +and compose them for you: + +```php +use Zend\Expressive\Router\AuraRouter; + +$router = new AuraRouter(); +``` + +## Programmatic Creation + +If you need greater control over the Aura.Router setup and configuration, you +can create the instances necessary and inject them into +`Zend\Expressive\Router\AuraRouter` during instantiation. + +```php +newInstance(); +$auraRouter->setSecure(true); +$auraRouter->addValues([ + 'mediatype' => 'application/xhtml+xml', +]); + +$router = new AuraBridge($auraRouter); + +// First argument is the container to use, if not using the default; +// second is the router. +$app = AppFactory::create(null, $router); +``` + +> ### Piping the route middleware +> +> As a reminder, you will need to ensure that middleware is piped in the order +> in which it needs to be executed; please see the section on "Controlling +> middleware execution order" in the [piping documentation](piping.md). This is +> particularly salient when defining routes before injecting the router in the +> application instance! + +## Factory-Driven Creation + +[We recommend using an Inversion of Control container](../container/intro.md) +for your applications; as such, in this section we will demonstrate +two strategies for creating your Aura.Router implementation. + +### Basic Router + +If you don't need to provide any setup or configuration, you can simply +instantiate and return an instance of `Zend\Expressive\Router\AuraRouter` for the +service name `Zend\Expressive\Router\RouterInterface`. + +A factory would look like this: + +```php +// in src/Application/Container/RouterFactory.php +namespace Application\Container; + +use Psr\Container\ContainerInterface; +use Zend\Expressive\Router\AuraRouter; + +class RouterFactory +{ + /** + * @param ContainerInterface $container + * @return AuraRouter + */ + public function __invoke(ContainerInterface $container) + { + return new AuraRouter(); + } +} +``` + +You would register this with zend-servicemanager using: + +```php +$container->setFactory( + Zend\Expressive\Router\RouterInterface::class, + Application\Container\RouterFactory::class +); +``` + +And in Pimple: + +```php +$pimple[Zend\Expressive\Router\RouterInterface::class] = new Application\Container\RouterFactory(); +``` + +For zend-servicemanager, you can omit the factory entirely, and register the +class as an invokable: + +```php +$container->setInvokableClass( + Zend\Expressive\Router\RouterInterface::class, + Zend\Expressive\Router\AuraRouter::class +); +``` + +### Advanced Configuration + +If you want to provide custom setup or configuration, you can do so. In this +example, we will be defining two factories: + +- A factory to register as and generate an `Aura\Router\Router` instance. +- A factory registered as `Zend\Expressive\Router\RouterInterface`, which + creates and returns a `Zend\Expressive\Router\AuraRouter` instance composing the + `Aura\Router\Router` instance. + +Sound difficult? It's not; we've essentially done it above already! + +```php +// in src/Application/Container/AuraRouterFactory.php: +namespace Application\Container; + +use Aura\Router\RouterFactory; +use Psr\Container\ContainerInterface; + +class AuraRouterFactory +{ + /** + * @param ContainerInterface $container + * @return \Aura\Router\Router + */ + public function __invoke(ContainerInterface $container) + { + $router = (new RouterFactory())->newInstance(); + $router->setSecure(true); + $router->addValues([ + 'mediatype' => 'application/xhtml+xml', + ]); + + return $router; + } +} + +// in src/Application/Container/RouterFactory.php +namespace Application\Container; + +use Psr\Container\ContainerInterface; +use Zend\Expressive\Router\AuraRouter as AuraBridge; + +class RouterFactory +{ + /** + * @param ContainerInterface $container + * @return AuraBridge + */ + public function __invoke(ContainerInterface $container) + { + return new AuraBridge($container->get('Aura\Router\Router')); + } +} +``` + +From here, you will need to register your factories with your IoC container. + +If you are using zend-servicemanager, this will look like: + +```php +// Programmatically: +use Zend\ServiceManager\ServiceManager; + +$container = new ServiceManager(); +$container->addFactory( + 'Aura\Router\Router', + Application\Container\AuraRouterFactory::class +); +$container->addFactory( + Zend\Expressive\Router\RouterInterface::class, + 'Application\Container\RouterFactory' +); + +// Alternately, via configuration: +return [ + 'factories' => [ + 'Aura\Router\Router' => Application\Container\AuraRouterFactory::class, + Zend\Expressive\Router\RouterInterface::class => 'Application\Container\RouterFactory::class, + ], +]; +``` + +For Pimple, configuration looks like: + +```php +use Application\Container\AuraRouterFactory; +use Application\Container\RouterFactory; +use Interop\Container\Pimple\PimpleInterop as Pimple; + +$container = new Pimple(); +$container['Aura\Router\Router'] = new AuraRouterFactory(); +$container[Zend\Expressive\Router\RouterInterface::class] = new RouterFactory(); +``` diff --git a/docs/book/v3/features/router/fast-route.md b/docs/book/v3/features/router/fast-route.md new file mode 100644 index 00000000..c748a859 --- /dev/null +++ b/docs/book/v3/features/router/fast-route.md @@ -0,0 +1,351 @@ +# Using FastRoute + +[FastRoute](https://github.com/nikic/FastRoute) provides a number of different +combinations for how to both parse routes and match incoming requests against +them. + +Internally, we use the standard route parser (`FastRoute\RouterParser\Std`) to +parse routes, a `RouteCollector` to collect them, and the "Group Count Based" +dispatcher to match incoming requests against routes. + +If you wish to use a different combination — e.g., to use the Group Position +Based route matcher — you will need to create your own instances and inject them +into the `Zend\Expressive\Router\FastRouteRouter` class, at instantiation. + +The `FastRouteRouter` bridge class accepts two arguments at instantiation: + +- A `FastRoute\RouteCollector` instance +- A callable that will return a `FastRoute\Dispatcher\RegexBasedAbstract` + instance. + +Injection can be done either programmatically or via a factory to use in +conjunction with your container instance. + +## Installing FastRoute + +To use FastRoute, you will first need to install the FastRoute integration: + +```bash +$ composer require zendframework/zend-expressive-fastroute +``` + +## Quick Start + +At its simplest, you can instantiate a `Zend\Expressive\Router\FastRouteRouter` instance +with no arguments; it will create the underlying FastRoute objects required +and compose them for you: + +```php +use Zend\Expressive\Router\FastRouteRouter; + +$router = new FastRouteRouter(); +``` + +## Programmatic Creation + +If you need greater control over the FastRoute setup and configuration, you +can create the instances necessary and inject them into +`Zend\Expressive\Router\FastRouteRouter` during instantiation. + +To do so, you will need to setup your `RouteCollector` instance and/or +optionally callable to return your `RegexBasedAbstract` instance manually, +inject them in your `Zend\Expressive\Router\FastRouteRouter` instance, and inject use +that when creating your `Application` instance. + +```php + ### Piping the route middleware +> +> As a reminder, you will need to ensure that middleware is piped in the order +> in which it needs to be executed; please see the section on "Controlling +> middleware execution order" in the [piping documentation](piping.md). This is +> particularly salient when defining routes before injecting the router in the +> application instance! + +## Factory-Driven Creation + +[We recommend using an Inversion of Control container](../container/intro.md) +for your applications; as such, in this section we will demonstrate +two strategies for creating your FastRoute implementation. + +### Basic Router + +If you don't need to provide any setup or configuration, you can simply +instantiate and return an instance of `Zend\Expressive\Router\FastRouteRouter` for the +service name `Zend\Expressive\Router\RouterInterface`. + +A factory would look like this: + +```php +// in src/App/Container/RouterFactory.php +namespace App\Container; + +use Psr\Container\ContainerInterface; +use Zend\Expressive\Router\FastRouteRouter; + +class RouterFactory +{ + /** + * @param ContainerInterface $container + * @return FastRouteRouter + */ + public function __invoke(ContainerInterface $container) + { + return new FastRouteRouter(); + } +} +``` + +You would register this with zend-servicemanager using: + +```php +$container->setFactory( + Zend\Expressive\Router\RouterInterface::class, + App\Container\RouterFactory::class +); +``` + +And in Pimple: + +```php +$pimple[Zend\Expressive\Router\RouterInterface::class] = new App\Container\RouterFactory(); +``` + +For zend-servicemanager, you can omit the factory entirely, and register the +class as an invokable: + +```php +$container->setInvokableClass( + Zend\Expressive\Router\RouterInterface::class, + Zend\Expressive\Router\FastRouteRouter::class +); +``` + +### Advanced Configuration + +If you want to provide custom setup or configuration, you can do so. In this +example, we will be defining three factories: + +- A factory to register as and generate a `FastRoute\RouteCollector` instance. +- A factory to register as `FastRoute\DispatcherFactory` and return a callable + factory that returns a `RegexBasedAbstract` instance. +- A factory registered as `Zend\Expressive\Router\RouterInterface`, which + creates and returns a `Zend\Expressive\Router\FastRouteRouter` instance composing the + two services. + +Sound difficult? It's not; we've essentially done it above already! + +```php +get(FastRoute\RouteCollector::class), + $container->get(FastRoute\DispatcherFactory::class) + ); + } +} +``` + +From here, you will need to register your factories with your IoC container. + +If you are using zend-servicemanager, this will look like: + +```php +// Programmatically: +use Zend\ServiceManager\ServiceManager; + +$container = new ServiceManager(); +$container->addFactory( + FastRoute\RouteCollector::class, + App\Container\FastRouteCollectorFactory::class +); +$container->addFactory( + FastRoute\DispatcherFactory::class, + App\Container\FastRouteDispatcherFactory::class +); +$container->addFactory( + Zend\Expressive\Router\RouterInterface::class, + App\Container\RouterFactory::class +); + +// Alternately, via configuration: +return [ + 'factories' => [ + 'FastRoute\RouteCollector' => App\Container\FastRouteCollectorFactory::class, + 'FastRoute\DispatcherFactory' => App\Container\FastRouteDispatcherFactory::class, + Zend\Expressive\Router\RouterInterface::class => App\Container\RouterFactory::class, + ], +]; +``` + +For Pimple, configuration looks like: + +```php +use App\Container\FastRouteCollectorFactory; +use App\Container\FastRouteDispatcherFactory; +use App\Container\RouterFactory; +use Interop\Container\Pimple\PimpleInterop as Pimple; + +$container = new Pimple(); +$container[FastRoute\RouteCollector::class] = new FastRouteCollectorFactory(); +$container[FastRoute\RouteDispatcher::class] = new FastRouteDispatcherFactory(); +$container[Zend\Expressive\Router\RouterInterface::class] = new RouterFactory(); +``` + +### FastRoute caching support + +- Since zend-expressive-fastroute 1.3.0. + +Starting from version 1.3.0, zend-expressive-fastroute comes with support +for FastRoute native dispatch data caching. + +Enabling this feature requires changes to your configuration. Typically, router +configuration occurs in `config/autoload/routes.global.php`; as such, we will +reference that file when indicating configuration changes. + +The changes required are: + +- You will need to delegate creation of the router instance to a new factory. + +- You will need to add a new configuration entry, `$config['router']['fastroute']`. + The options in this entry will be used by the factory to build the router + instance in order to toggle caching support and to specify a custom cache + file. + +As an example: + +``` php +// File config/autoload/routes.global.php + +return [ + 'dependencies' => [ + //.. + 'invokables' => [ + /* ... */ + // Comment out or remove the following line: + // Zend\Expressive\Router\RouterInterface::class => Zend\Expressive\Router\FastRouteRouter::class, + /* ... */ + ], + 'factories' => [ + /* ... */ + // Add this line; the specified factory now creates the router instance: + Zend\Expressive\Router\RouterInterface::class => Zend\Expressive\Router\FastRouteRouterFactory::class, + /* ... */ + ], + ], + + // Add the following to enable caching support: + 'router' => [ + 'fastroute' => [ + // Enable caching support: + 'cache_enabled' => true, + // Optional (but recommended) cache file path: + 'cache_file' => 'data/cache/fastroute.php.cache', + ], + ], + + 'routes' => [ /* ... */ ], +] +``` + +The FastRoute-specific caching options are as follows: + +- `cache_enabled` (bool) is used to toggle caching support. It's advisable to enable + caching in a production environment and leave it disabled for the development + environment. Commenting or omitting this option is equivalent to having it set + to `false`. We recommend enabling it in `config/autoload/routes.global.php`, + and, in development, disabling it within `config/autoload/routes.local.php` or + `config/autoload/local.php`. + +- `cache_file` (string) is an optional parameter that represents the path of + the dispatch data cache file. It can be provided as an absolute file path or + as a path relative to the zend-expressive working directory. + + It defaults to `data/cache/fastroute.php.cache`, where `data/cache/` is the + cache directory defined within the zend-expressive skeleton application. An + explicit absolute file path is recommended since the php `include` construct + will skip searching the `include_path` and the current directory. + + If you choose a custom path, make sure that the directory exists and is + writable by the owner of the PHP process. As with any other zend-expressive + cached configuration, you will need to purge this file in order to enable any + newly added route when FastRoute caching is enabled. diff --git a/docs/book/v3/features/router/interface.md b/docs/book/v3/features/router/interface.md new file mode 100644 index 00000000..c38fd2aa --- /dev/null +++ b/docs/book/v3/features/router/interface.md @@ -0,0 +1,286 @@ +# Routing Interface + +Expressive defines `Zend\Expressive\Router\RouterInterface`, which is used by +the `Zend\Expressive\Router\PathBasedRoutingMiddleware` consumed by +`Zend\Expressive\Application` in order to provide dynamic routing capabilities +to middleware. The interface serves as an abstraction to allow routers with +varying capabilities to be used with an application. + +The interface is defined as follows: + +```php +namespace Zend\Expressive\Router; + +use Psr\Http\Message\ServerRequestInterface as Request; + +/** + * Interface defining required router capabilities. + */ +interface RouterInterface +{ + /** + * Add a route. + * + * This method adds a route against which the underlying implementation may + * match. Implementations MUST aggregate route instances, but MUST NOT use + * the details to inject the underlying router until `match()` and/or + * `generateUri()` is called. This is required to allow consumers to + * modify route instances before matching (e.g., to provide route options, + * inject a name, etc.). + * + * The method MUST raise Exception\RuntimeException if called after either `match()` + * or `generateUri()` have already been called, to ensure integrity of the + * router between invocations of either of those methods. + * + * @throws Exception\RuntimeException when called after match() or + * generateUri() have been called. + */ + public function addRoute(Route $route) : void; + + /** + * Match a request against the known routes. + * + * Implementations will aggregate required information from the provided + * request instance, and pass them to the underlying router implementation; + * when done, they will then marshal a `RouteResult` instance indicating + * the results of the matching operation and return it to the caller. + */ + public function match(Request $request) : RouteResult; + + /** + * Generate a URI from the named route. + * + * Takes the named route and any substitutions, and attempts to generate a + * URI from it. Additional router-dependent options may be passed. + * + * The URI generated MUST NOT be escaped. If you wish to escape any part of + * the URI, this should be performed afterwards; consider passing the URI + * to league/uri to encode it. + * + * @see https://github.com/auraphp/Aura.Router/blob/3.x/docs/generating-paths.md + * @see https://docs.zendframework.com/zend-router/routing/ + * @throws Exception\RuntimeException if unable to generate the given URI. + */ + public function generateUri(string $name, array $substitutions = [], array $options = []) : string; +} +``` + +Developers may create and use their own implementations. We recommend +registering your implementation as the service +`Zend\Expressive\Router\RouterInterface` in your container to ensure other +factories provided by zend-expressive will receive your custom service. + +Implementors should also read the following sections detailing the `Route` and +`RouteResult` classes, to ensure that their implementations interoperate +correctly. + +## Routes + +Routes are defined via `Zend\Expressive\Router\Route`, and aggregate the +following information: + +- Path to match. +- Middleware to use when the route is matched. This may be a callable or a + service name resolving to middleware. +- HTTP methods allowed for the route; if none are provided, all are assumed. +- Optionally, a name by which to reference the route. + +The `Route` class has the following signature: + +```php +namespace Zend\Expressive\Router; + +use Fig\Http\Message\RequestMethodInterface as RequestMethod; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\MiddlewareInterface; +use Psr\Http\Server\RequestHandlerInterface; + +class Route implements MiddlewareInterface +{ + public const HTTP_METHOD_ANY = null; + public const HTTP_METHOD_SEPARATOR = ':'; + + /** + * @param string $path Path to match. + * @param MiddlewareInterface $middleware Middleware to use when this route is matched. + * @param null|string[] $methods Allowed HTTP methods; defaults to HTTP_METHOD_ANY. + * @param null|string $name the route name + */ + public function __construct( + string $path, + MiddlewareInterface $middleware, + array $methods = self::HTTP_METHOD_ANY, + string $name = null + ); + + /** + * Proxies to the middleware composed during instantiation. + */ + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface; + + public function getPath() : string; + + /** + * Set the route name. + */ + public function setName(string $name) : void; + + public function getName() : string; + + public function getMiddleware() : MiddlewareInterface; + + /** + * @return null|string[] Returns HTTP_METHOD_ANY or array of allowed methods. + */ + public function getAllowedMethods() : ?array; + + /** + * Indicate whether the specified method is allowed by the route. + * + * @param string $method HTTP method to test. + */ + public function allowsMethod(string $method) : bool; + + public function setOptions(array $options) : void; + + public function getOptions() : array; + + /** + * Whether or not HEAD support is implicit (i.e., not explicitly specified) + */ + public function implicitHead() : bool; + + /** + * Whether or not OPTIONS support is implicit (i.e., not explicitly specified) + */ + public function implicitOptions() : bool; +} +``` + +Typically, developers will use `Zend\Expressive\Application::route()` (or one of +the HTTP-specific routing methods) to create routes, and will not need to +interact with `Route` instances. Additionally, when working with `RouteResult` +instances, you may pull the `Route` instance from that in order to obtain data +about the matched route. + +## Matching and RouteResults + +Internally, routing middleware calls on `RouterInterface::match()`, +passing it the current request instance. This allows implementations to pull +what they may need from the request in order to perform their routing logic; for +example, they may need the request method, the URI path, the value of the +`HTTPS` server variable, etc. + +Implementations are expected to return a `Zend\Expressive\Router\RouteResult` +instance, which is then injected as a request attribute under the name +`Zend\Expressive\Router\RouteResult` when passing processing of the request to +the provided handler. Additionally, in the event of success, it will pull any +matched parameters from the result and inject them as request attributes as +well. + +Dispatch middleware can then retrieve the route result from the request and +process it, passing the route result its own request and handler. + +The zend-expressive-router package also provides a number of middleware geared +towards handling failed results which can be placed between routing and dispatch +middleware: + +- `Zend\Expressive\Router\Middleware\MethodNotAllowedMiddleware` checks to see + if the route failures was due to the HTTP method, and, if so, return a 405 + response with an appropriate `Allow` header. + +- `Zend\Expressive\Router\Middleware\ImplicitHeadMiddleware` checks to see if a + routing failure was due to a route match using a `HEAD` request, and will then + dispatch the appropriate route via `GET` request, and inject an empty body + into the returned response. + +- `Zend\Expressive\Router\Middleware\ImplicitOptionsMiddleware` checks to see if a + routing failure was due to a route match using a `OPTIONS` request; if so, it + will return a 200 response with an appropriate `Allow `header. + +The `RouteResult` signature is as follows: + +```php +namespace Zend\Expressive\Router; + +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\MiddlewareInterface; +use Psr\Http\Server\RequestHandlerInterface; + +class RouteResult implements MiddlewareInterface +{ + /** + * Create an instance representing a route succes from the matching route. + * + * @param array $params Parameters associated with the matched route, if any. + */ + public static function fromRoute(Route $route, array $params = []) : self; + + /** + * Create an instance representing a route failure. + * + * @param null|array $methods HTTP methods allowed for the current URI, if any. + * null is equivalent to allowing any HTTP method; empty array means none. + */ + public static function fromRouteFailure(?array $methods) : self; + + /** + * Process the result as middleware. + * + * If the result represents a failure, it passes handling to the handler. + * + * Otherwise, it processes the composed middleware using the provide request + * and handler. + */ + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface; + + /** + * Does the result represent successful routing? + */ + public function isSuccess() : bool; + + /** + * Retrieve the route that resulted in the route match. + * + * @return false|null|Route false if representing a routing failure; + * null if not created via fromRoute(); Route instance otherwise. + */ + public function getMatchedRoute(); + + /** + * Retrieve the matched route name, if possible. + * + * If this result represents a failure, return false; otherwise, return the + * matched route name. + * + * @return false|string + */ + public function getMatchedRouteName(); + + /** + * Returns the matched params. + */ + public function getMatchedParams() : array; + + /** + * Is this a routing failure result? + */ + public function isFailure() : bool; + + /** + * Does the result represent failure to route due to HTTP method? + */ + public function isMethodFailure() : bool; + + /** + * Retrieve the allowed methods for the route failure. + * + * @return string[] HTTP methods allowed + */ + public function getAllowedMethods() : array; +} +``` + +Typically, only those implementing routers will interact with this class. diff --git a/docs/book/v3/features/router/intro.md b/docs/book/v3/features/router/intro.md new file mode 100644 index 00000000..267e0c6c --- /dev/null +++ b/docs/book/v3/features/router/intro.md @@ -0,0 +1,116 @@ +# Routing + +One fundamental feature of zend-expressive is that it provides mechanisms for +implementing dynamic routing, a feature required in most modern web +applications. As an example, you may want to allow matching both a resource, as +well as individual items of that resource: + +- `/books` might return a collection of books +- `/books/zend-expressive` might return the individual book identified by + "zend-expressive". + +Expressive does not provide routing on its own; you must choose a routing +adapter that implements `Zend\Expressive\Router\RouterInterface` and provide it +to the `Application` instance. This allows you to choose the router with the +capabilities that best match your own needs, while still providing a common +abstraction for defining and aggregating routes and their related middleware. + +## Retrieving matched parameters + +Routing enables the ability to match dynamic path segments (or other +criteria). Typically, you will want access to the values matched. The routing +middleware injects any matched parameters as returned by the underlying router +into the request as *attributes*. + +In the example above, let's assume the route was defined as `/books/:id`, where +`id` is the name of the dynamic segment. This means that in the middleware +invoked for this route, you can fetch the `id` attribute to discover what was +matched: + +```php +$id = $request->getAttribute('id'); +``` + +## Retrieving the matched route + +When routing is successful, the routing middleware injects a +`Zend\Expressive\Router\RouteResult` instance as a request attribute, using that +class name as the attribute name. The `RouteResult` instance provides you access +to the following: + +- The matched `Zend\Expressive\Router\Route` instance, via `$result->getMatchedRoute()`. +- The matched route name, via `$result->getMatchedRouteName()` (or via + `$result->getMatchedRoute()->getName()`). +- The matched middleware, via `$result->getMatchedMiddleware()` (or via + `$result->getMatchedRoute()->getMiddleware()`). +- Matched parameters, via `$result->getMatchedParams()` (as noted above, these + are also each injected as discrete request attributes). +- Allowed HTTP methods, via `$result->getAllowedMethods()`. + +As an example, you could use middleware similar to the following to return a 403 +response if routing was successful, but no `Authorization` header is present: + +```php +use Interop\Http\ServerMiddleware\DelegateInterface; +use Zend\Diactoros\Response\EmptyResponse; +use Zend\Expressive\Router\RouteResult; + +function ($request, DelegateInterface $delegate) use ($routesRequiringAuthorization, $validator) { + if (! ($result = $request->getAttribute(RouteResult::class, false))) { + // No route matched; delegate to next middleware + return $delegate->process($request); + } + + if (! in_array($result->getMatchedRouteName(), $routesRequiringAuthorization, true)) { + // Not a route requiring authorization + return $delegate->process($request); + } + + $header = $request->getHeaderLine('Authorization'); + if (! $validator($header)) { + return new EmptyResponse(403); + } + + return $delegate->process($request); +} +``` + +Note that the first step is to determine if we have a `RouteResult`; if we do +not have one, we should either delegate to the next middleware, or return some +sort of response (generally a 404). In the case of Expressive, a later +middleware will generate the 404 response for us, so we can safely delegate. + +## URI generation + +Because routers have knowledge of the various paths they can match, they are +also typically used within applications to generate URIs to other application +resources. Expressive provides this capability in the `RouterInterface`, +either delegating to the underlying router implementations or providing a +compatible implementation of its own. + +At it's most basic level, you call the `generateUri()` method with a route name +and any substitutions you want to make: + +```php +$uri = $router->generateUri('book', ['id' => 'zend-expressive']); +``` + +Some routers may support providing _options_ during URI generation. Starting in +zend-expressive-router 2.0, which ships with Expressive starting with version +2.0, you may also pass a third argument to `generateUri()`, an array of router +options: + +```php +$uri = $router->generateUri('book', ['id' => 'zend-expressive'], [ + 'translator' => $translator, + 'text_domain' => $currentLocale, +]); +``` + +## Supported implementations + +Expressive currently ships with adapters for the following routers: + +- [Aura.Router](aura.md) +- [FastRoute](fast-route.md) +- [zend-mvc Router](zf2.md) diff --git a/docs/book/v3/features/router/piping.md b/docs/book/v3/features/router/piping.md new file mode 100644 index 00000000..139ef8e4 --- /dev/null +++ b/docs/book/v3/features/router/piping.md @@ -0,0 +1,131 @@ +# Routing vs Piping + +Expressive provides two mechanisms for adding middleware to your +application: + +- piping, which is a foundation feature of the underlying + [zend-stratigility](https://docs.zendframework.com/zend-stratigility/) + implementation. +- routing, which is an additional feature provided by zend-expressive. + +## Piping + +zend-stratigility provides a mechanism termed *piping* for composing middleware +in an application. When you *pipe* middleware to the application, it is added to +a queue, and dequeued in order until a middleware returns a response instance. +If none ever returns a response instance, execution is delegated to a "final +handler", which determines whether or not to return an error, and, if so, what +kind of error to return. + +Stratigility also allows you to segregate piped middleware to specific paths. As +an example: + +```php +$app->pipe('/api', $apiMiddleware); +``` + +will execute `$apiMiddleware` only if the path matches `/api`; otherwise, it +will skip over that middleware. + +This path segregation, however, is limited: it will only match literal paths. +This is done purposefully, to provide excellent baseline performance, and to +prevent feature creep in the library. + +Expressive uses and exposes piping to users, with one addition: **middleware +may be specified by service name, and zend-expressive will lazy-load the service +only when the middleware is invoked**. + +In order to accomplish the lazy-loading, zend-expressive wraps the calls to +fetch and dispatch the middleware inside a +`Zend\Expressive\Middleware\LazyLoadingMiddleware` instance; as such, there is +no overhead to utilizing service-based middleware _until it is dispatched_. + +## Routing + +Routing is the process of discovering values from the incoming request based on +defined criteria. That criteria might look like: + +- `/book/:id` (ZF2) +- `/book/{id}` (Aura.Router) +- `/book/{id:\d+}` (FastRoute) + +In each of the above, if the router determines that the request matches the +criteria, it will indicate: + +- the route that matched +- the `id` parameter was matched, and the value matched + +Most routers allow you to define arbitrarily complex rules, and many even allow +you to define: + +- default values for unmatched parameters +- criteria for evaluating a match (such as a regular expression) +- additional criteria to meet (such as SSL usage, allowed query string + variables, etc.) + +As such, routing is more powerful than the literal path matching used when +piping, but it is also more costly (though routers such as FastRoute largely +make such performance issues moot). + +## When to Pipe + +In Expressive, we recommend that you pipe middleware in the following +circumstances: + +- It should (potentially) run on every execution. Examples for such usage + include: + - Logging requests + - Performing content negotiation + - Handling cookies +- Error handling. +- Application segregation. You can write re-usable middleware, potentially even + based off of Expressive, that contains its own routing logic, and compose it + such that it only executes if it matches a sub-path. + +## When to Route + +Use routing when: + +- Your middleware is reacting to a given path. +- You want to use dynamic routing. +- You want to restrict usage of middleware to specific HTTP methods. +- You want to be able to generate URIs to your middleware. + +The above cover most use cases; *in other words, most middleware should be added +to the application as routed middleware*. + +## Controlling middleware execution order + +As noted in the earlier section on piping, piped middleware is *queued*, meaning +it has a FIFO ("first in, first out") execution order. + +Additionally, zend-expressive's routing and dispatch capabilities are themselves +implemented as piped middleware. + +To ensure your middleware is piped correctly, keep in mind the following: + +- If middleware should execute on _every request_, pipe it early. +- Pipe routing and dispatch middleware using their dedicated application methods + (more on this below), optionally with middleware between them to further shape + application flow. +- Pipe middleware guaranteed to return a response (such as a "not found" handler + or similar) _last_. + +To use the shipped routing and dispatch middleware (likely a good idea!), use +the dedicated application methods `pipeRoutingMiddleware()` and +`pipeDispatchMiddleware()`; `Application` contains logic to ensure neither of +these are called more than once. + +As an example: + +```php +$app->pipe(OriginalMessages::class); +$app->pipe(ServerUrlMiddleware::class); +$app->pipe(XClacksOverhead::class); +$app->pipe(ErrorHandler::class); +$app->pipeRoutingMiddleware(); +$app->pipe(UrlHelperMiddleware::class); +$app->pipe(AuthorizationCheck::class); +$app->pipeDispatchMiddleware(); +$app->pipe(NotFoundHandler::class); +``` diff --git a/docs/book/v3/features/router/uri-generation.md b/docs/book/v3/features/router/uri-generation.md new file mode 100644 index 00000000..4144e55e --- /dev/null +++ b/docs/book/v3/features/router/uri-generation.md @@ -0,0 +1,91 @@ +# URI Generation + +One aspect of the `Zend\Expressive\Router\RouterInterface` is that it provides a +`generateUri()` method. This method accepts a route name, and optionally an +associative array of substitutions to use in the generated URI (e.g., if the URI +has any named placeholders). Starting in zend-expressive-router 2.0, shipped +with Expressive 2.0, you may also pass router-specific options to use during +URI generation as a third argument. + +## Naming routes + +By default, routes use a combination of the path and HTTP methods supported as +the name: + +- If you call `route()` with no HTTP methods, the name is the literal path with + no changes. + + ```php + $app->route('/foo', $middleware); // "foo" + ``` + +- If you call `get()`, `post()`, `put()`, `patch()`, or `delete()`, the name + will be the literal path, followed by a caret (`^`), followed by the + uppercase HTTP method name: + + ```php + $app->get('/foo', $middleware); // "foo^GET" + ``` + + Alternately, these methods return a `Route` instance, and you can set the + name on it: + + ```php + $app->get('/foo', $middleware)->setName('foo'); // "foo" + ``` + +- If you call `route()` and specify a list of HTTP methods accepted, the name + will be the literal path, followed by a caret (`^`), followed by a colon + (`:`)-separated list of the uppercase HTTP method names, in the order in which + they were added. + + ```php + $app->route('/foo', $middleware, ['GET', 'POST']); // "foo^GET:POST" + ``` + + Like the HTTP-specific methods, `route()` also returns a `Route` instance, + and you can set the name on it: + + ```php + $route = $app->route('/foo', $middleware, ['GET', 'POST']); // "foo^GET:POST" + $route->setName('foo'); // "foo" + ``` + +Clearly, this can become difficult to remember. As such, Expressive offers the +ability to specify a custom string for the route name as an additional, optional +argument to any of the above: + +```php +$app->route('/foo', $middleware, 'foo'); // 'foo' +$app->get('/foo/:id', $middleware, 'foo-item'); // 'foo-item' +$app->route('/foo', $middleware, ['GET', 'POST'], 'foo-collection'); // 'foo-collection' +``` + +As noted above, these methods also return `Route` instances, allowing you to +set the name after-the-fact; this is particularly useful with the `route()` +method, where you may want to omit the HTTP methods if any HTTP method is +allowed: + +```php +$app->route('/foo', $middleware)->setName('foo'); // 'foo' +``` + +We recommend that if you plan on generating URIs for given routes, you provide a +custom name. + +## Generating URIs + +Once you know the name of a URI you wish to generate, you can do so from the +router instance: + +```php +$uri = $router->generateUri('foo-item', ['id' => 'bar']); // "/foo/bar" +``` + +You can omit the second argument if no substitutions are necessary. + +> ### Compose the router +> +> For this to work, you'll need to compose the router instance in any class that +> requires the URI generation facility. Inject the +> `Zend\Expressive\Router\RouterInterface` service in these situations. diff --git a/docs/book/v3/features/router/zf2.md b/docs/book/v3/features/router/zf2.md new file mode 100644 index 00000000..8c12cda5 --- /dev/null +++ b/docs/book/v3/features/router/zf2.md @@ -0,0 +1,242 @@ +# Using zend-router + +[zend-router](https://docs.zendframework.com/zend-router/) provides several +router implementations used for ZF2+ applications; the default is +`Zend\Router\Http\TreeRouteStack`, which can compose a number of different +routes of differing types in order to perform routing. + +The ZF2 bridge we provide, `Zend\Expressive\Router\ZendRouter`, uses the +`TreeRouteStack`, and injects `Segment` routes to it; these are in turn injected +with `Method` routes, and a special "method not allowed" route at negative +priority to enable us to distinguish between failure to match the path and +failure to match the HTTP method. + +If you instantiate it with no arguments, it will create an empty +`TreeRouteStack`. Thus, the simplest way to start with this router is: + +```php +use Zend\Expressive\AppFactory; +use Zend\Expressive\Router\ZendRouter; + +$app = AppFactory::create(null, new ZendRouter()); +``` + +The `TreeRouteStack` offers some unique features: + +- Route "prototypes". These are essentially like child routes that must *also* + match in order for a given route to match. These are useful for implementing + functionality such as ensuring the request comes in over HTTPS, or over a + specific subdomain. +- Base URL functionality. If a base URL is injected, comparisons will be + relative to that URL. This is mostly unnecessary with Stratigility-based + middleware, but could solve some edge cases. + +To specify these, you need access to the underlying `TreeRouteStack` +instance, however, and the `RouterInterface` does not provide an accessor! + +The answer, then, is to use dependency injection. This can be done in two ways: +programmatically, or via a factory to use in conjunction with your container +instance. + +## Installing the ZF2 Router + +To use the ZF2 router, you will need to install the zend-mvc router integration: + +```bash +$ composer require zendframework/zend-expressive-zendrouter +``` + +## Quick Start + +At its simplest, you can instantiate a `Zend\Expressive\Router\ZendRouter` instance +with no arguments; it will create the underlying zend-mvc routing objects +required and compose them for you: + +```php +use Zend\Expressive\Router\ZendRouter; + +$router = new ZendRouter(); +``` + +## Programmatic Creation + +If you need greater control over the zend-mvc router setup and configuration, +you can create the instances necessary and inject them into +`Zend\Expressive\Router\ZendRouter` during instantiation. + +```php +use Zend\Expressive\AppFactory; +use Zend\Expressive\Router\ZendRouter as Zf2Bridge; +use Zend\Router\Http\TreeRouteStack; + +$zendRouter = new TreeRouteStack(); +$zendRouter->addPrototypes(/* ... */); +$zendRouter->setBaseUrl(/* ... */); + +$router = new Zf2Bridge($zendRouter); + +// First argument is the container to use, if not using the default; +// second is the router. +$app = AppFactory::create(null, $router); +``` + +> ### Piping the route middleware +> +> As a reminder, you will need to ensure that middleware is piped in the order +> in which it needs to be executed; please see the section on "Controlling +> middleware execution order" in the [piping documentation](piping.md). This is +> particularly salient when defining routes before injecting the router in the +> application instance! + +## Factory-Driven Creation + +[We recommend using an Inversion of Control container](../container/intro.md) +for your applications; as such, in this section we will demonstrate +two strategies for creating your zend-mvc router implementation. + +### Basic Router + +If you don't need to provide any setup or configuration, you can simply +instantiate and return an instance of `Zend\Expressive\Router\ZendRouter` for the +service name `Zend\Expressive\Router\RouterInterface`. + +A factory would look like this: + +```php +// in src/App/Container/RouterFactory.php +namespace App\Container; + +use Psr\Container\ContainerInterface; +use Zend\Expressive\Router\ZendRouter; + +class RouterFactory +{ + /** + * @param ContainerInterface $container + * @return ZendRouter + */ + public function __invoke(ContainerInterface $container) + { + return new ZendRouter(); + } +} +``` + +You would register this with zend-servicemanager using: + +```php +$container->setFactory( + Zend\Expressive\Router\RouterInterface::class, + App\Container\RouterFactory::class +); +``` + +And in Pimple: + +```php +$pimple[Zend\Expressive\Router\RouterInterface::class] = new Application\Container\RouterFactory(); +``` + +For zend-servicemanager, you can omit the factory entirely, and register the +class as an invokable: + +```php +$container->setInvokableClass( + Zend\Expressive\Router\RouterInterface::class, + Zend\Expressive\Router\ZendRouter::class +); +``` + +### Advanced Configuration + +If you want to provide custom setup or configuration, you can do so. In this +example, we will be defining two factories: + +- A factory to register as and generate an `Zend\Router\Http\TreeRouteStack` + instance. +- A factory registered as `Zend\Expressive\Router\RouterInterface`, which + creates and returns a `Zend\Expressive\Router\ZendRouter` instance composing the + `Zend\Mvc\Router\Http\TreeRouteStack` instance. + +Sound difficult? It's not; we've essentially done it above already! + +```php +// in src/App/Container/TreeRouteStackFactory.php: +namespace App\Container; + +use Psr\Container\ContainerInterface; +use Zend\Http\Router\TreeRouteStack; + +class TreeRouteStackFactory +{ + /** + * @param ContainerInterface $container + * @return TreeRouteStack + */ + public function __invoke(ContainerInterface $container) + { + $router = new TreeRouteStack(); + $router->addPrototypes(/* ... */); + $router->setBaseUrl(/* ... */); + + return $router; + } +} + +// in src/App/Container/RouterFactory.php +namespace App\Container; + +use Psr\Container\ContainerInterface; +use Zend\Expressive\Router\ZendRouter as Zf2Bridge; + +class RouterFactory +{ + /** + * @param ContainerInterface $container + * @return Zf2Bridge + */ + public function __invoke(ContainerInterface $container) + { + return new Zf2Bridge($container->get(Zend\Mvc\Router\Http\TreeRouteStack::class)); + } +} +``` + +From here, you will need to register your factories with your IoC container. + +If you are using zend-servicemanager, this will look like: + +```php +// Programmatically: +use Zend\ServiceManager\ServiceManager; + +$container = new ServiceManager(); +$container->addFactory( + Zend\Router\Http\TreeRouteStack::class, + App\Container\TreeRouteStackFactory::class +); +$container->addFactory( + Zend\Expressive\Router\RouterInterface::class, + App\Container\RouterFactory::class +); + +// Alternately, via configuration: +return [ + 'factories' => [ + Zend\Router\Http\TreeRouteStack::class => App\Container\TreeRouteStackFactory::class, + Zend\Expressive\Router\RouterInterface::class => App\Container\RouterFactory::class, + ], +]; +``` + +For Pimple, configuration looks like: + +```php +use Application\Container\TreeRouteStackFactory; +use Application\Container\ZfRouterFactory; +use Interop\Container\Pimple\PimpleInterop; + +$container = new PimpleInterop(); +$container[Zend\Router\Http\TreeRouteStackFactory::class] = new TreeRouteStackFactory(); +$container[Zend\Expressive\Router\RouterInterface::class] = new RouterFactory(); +``` diff --git a/docs/book/v3/features/template/interface.md b/docs/book/v3/features/template/interface.md new file mode 100644 index 00000000..a60951b2 --- /dev/null +++ b/docs/book/v3/features/template/interface.md @@ -0,0 +1,192 @@ +# The Template Renderer Interface + +Expressive defines `Zend\Expressive\Template\TemplateRendererInterface`, which can be +injected into middleware in order to create templated response bodies. The +interface is defined as follows: + +```php +namespace Zend\Expressive\Template; + +interface TemplateRendererInterface +{ + /** + * Render a template, optionally with parameters. + * + * Implementations MUST support the `namespace::template` naming convention, + * and allow omitting the filename extension. + * + * @param string $name + * @param array|object $params + * @return string + */ + public function render($name, $params = []); + + /** + * Add a template path to the engine. + * + * Adds a template path, with optional namespace the templates in that path + * provide. + * + * @param string $path + * @param string $namespace + */ + public function addPath($path, $namespace = null); + + /** + * Retrieve configured paths from the engine. + * + * @return TemplatePath[] + */ + public function getPaths(); + + /** + * Add a default parameter to use with a template. + * + * Use this method to provide a default parameter to use when a template is + * rendered. The parameter may be overridden by providing it when calling + * `render()`, or by calling this method again with a null value. + * + * The parameter will be specific to the template name provided. To make + * the parameter available to any template, pass the TEMPLATE_ALL constant + * for the template name. + * + * If the default parameter existed previously, subsequent invocations with + * the same template name and parameter name will overwrite. + * + * @param string $templateName Name of template to which the param applies; + * use TEMPLATE_ALL to apply to all templates. + * @param string $param Param name. + * @param mixed $value + */ + public function addDefaultParam($templateName, $param, $value); +} +``` + +> ### Namespaces +> +> Unfortunately, namespace syntax varies between different template engine +> implementations. As an example: +> +> - Plates uses the syntax `namespace::template`. +> - Twig uses the syntax `@namespace/template`. +> - zend-view does not natively support namespaces, though custom resolvers +> can provide the functionality. +> +> To make different engines compatible, we require implementations to support +> the syntax `namespace::template` (where `namespace::` is optional) when +> rendering. Additionally, we require that engines allow omitting the filename +> suffix. +> +> When using a `TemplateRendererInterface` implementation, feel free to use namespaced +> templates, and to omit the filename suffix; this will make your code portable +> and allow it to use alternate template engines. + + +## Paths + +Most template engines and implementations will require that you specify one or +more paths to templates; these are then used when resolving a template name to +the actual template. You may use the `addPath()` method to do so: + +```php +$renderer->addPath('templates'); +``` + +Template engines adapted for zend-expressive are also required to allow +*namespacing* templates; when adding a path, you specify the template +*namespace* that it fulfills, and the engine will only return a template from +that path if the namespace provided matches the namespace for the path. + +```php +// Resolves to a path registered with the namespace "error"; +// this example is specific to the Plates engine. +$content = $renderer->render('error::404'); +``` + +You can provide a namespace when registering a path via an optional second +argument: + +```php +// Registers the "error" namespace to the path "templates/error/" +$renderer->addPath('templates/error/', 'error'); +``` + +## Rendering + +To render a template, call the `render()` method. This method requires the name +of a template as the first argument: + +```php +$content = $renderer->render('foo'); +``` + +You can specify a namespaced template using the syntax `namespace::template`; +the `template` segment of the template name may use additional directory +separators when necessary. + +One key reason to use templates is to dynamically provide data to inject in the +template. You may do so by passing either an associative array or an object as +the second argument to `render()`: + +```php +$content = $renderer->render('message', [ + 'greeting' => 'Hello', + 'recipient' => 'World', +]); +``` + +It is up to the underlying template engine to determine how to perform the +injections. + +### Default params + +The `TemplateRendererInterface` defines the method `addDefaultParam()`. This +method can be used to specify default parameters to use when rendering a +template. The signature is: + +```php +public function addDefaultParam($templateName, $param, $value) +``` + +If you want a parameter to be used for *every* template, you can specify the +constant `TemplateRendererInterface::TEMPLATE_ALL` for the `$templateName` +parameter. + +When rendering, parameters are considered in the following order, with later +items having precedence over earlier ones: + +- Default parameters specified for all templates. +- Default parameters specified for the template specified at rendering. +- Parameters specified when rendering. + +As an example, if we did the following: + +```php +$renderer->addDefaultParam($renderer::TEMPLATE_ALL, 'foo', 'bar'); +$renderer->addDefaultParam($renderer::TEMPLATE_ALL, 'bar', 'baz'); +$renderer->addDefaultParam($renderer::TEMPLATE_ALL, 'baz', 'bat'); + +$renderer->addDefaultParam('example', 'foo', 'template default foo'); +$renderer->addDefaultParam('example', 'bar', 'template default bar'); + +$content = $renderer->render('example', [ + 'foo' => 'override', +]); +``` + +Then we can expect the following substitutions will occur when rendering: + +- References to the "foo" variable will contain "override". +- References to the "bar" variable will contain "template default bar". +- References to the "baz" variable will contain "bat". + +> #### Support for default params +> +> The support for default params will often be renderer-specific. The reason is +> because the `render()` signature does not specify a type for `$params`, in +> order to allow passing alternative arguments such as view models. In such +> cases, the implementation will indicate its behavior when default parameters +> are specified, but a given `$params` argument does not support it. +> +> At the time of writing, each of the Plates, Twig, and zend-view +> implementations support the feature. diff --git a/docs/book/v3/features/template/intro.md b/docs/book/v3/features/template/intro.md new file mode 100644 index 00000000..52dcc0ee --- /dev/null +++ b/docs/book/v3/features/template/intro.md @@ -0,0 +1,33 @@ +# Templating + +By default, no middleware in Expressive is templated. We do not even +provide a default templating engine, as the choice of templating engine is often +very specific to the project and/or organization. + +We do, however, provide abstraction for templating via the interface +`Zend\Expressive\Template\TemplateRendererInterface`, which allows you to write +middleware that is engine-agnostic. For Expressive, this means: + +- All adapters MUST support template namespacing. Namespaces MUST be referenced + using the notation `namespace::template` when rendering. +- Adapters MUST allow rendering templates that omit the extension; they will, of + course, resolve to whatever default extension they require (or as configured). +- Adapters SHOULD allow passing an extension in the template name, but how that + is handled is left up to the adapter. +- Adapters SHOULD abstract layout capabilities. Many templating systems provide + this out of the box, or similar, compatible features such as template + inheritance. This should be transparent to end-users; they should be able to + simply render a template and assume it has the full content to return. + +In this documentation, we'll detail the features of this interface, the various +implementations we provide, and how you can configure, inject, and consume +templating in your middleware. + +We currently support: + +- [Plates](plates.md) +- [Twig](twig.md) +- [zend-view](zend-view.md) + +Each has an associated container factory; details are found in the +[factories documentation](../container/factories.md). diff --git a/docs/book/v3/features/template/middleware.md b/docs/book/v3/features/template/middleware.md new file mode 100644 index 00000000..88b53fd7 --- /dev/null +++ b/docs/book/v3/features/template/middleware.md @@ -0,0 +1,100 @@ +# Templated request handlers + +The primary use case for templating is within request handlers, to provide templated +responses. To do this, you will: + +- Inject an instance of `Zend\Expressive\Template\TemplateRendererInterface` into your + request handler. +- Potentially add paths to the templating instance. +- Render a template. +- Add the results of rendering to your response. + +## Injecting a TemplateRendererInterface + +We encourage the use of dependency injection. As such, we recommend writing your +request handler to accept the `TemplateRendererInterface` via either the constructor or a +setter. As an example: + +```php +namespace Acme\Blog; + +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\RequestHandlerInterface; +use Zend\Expressive\Template\TemplateRendererInterface; + +class EntryHandler implements RequestHandlerInterface +{ + private $templateRenderer; + + public function __construct(TemplateRendererInterface $renderer) + { + $this->templateRenderer = $renderer; + } + + public function handler(ServerRequestInterface $request) : ResponseInterface + { + // ... + } +} +``` + +This will necessitate having a factory for your request handler: + +```php +namespace Acme\Blog\Container; + +use Acme\Blog\EntryHandler; +use Psr\Container\ContainerInterface; +use Zend\Expressive\Template\TemplateRendererInterface; + +class EntryHandlerFactory +{ + public function __invoke(ContainerInterface $container) + { + return new EntryHandler( + $container->get(TemplateRendererInterface::class) + ); + } +} +``` + +And, of course, you'll need to tell your container to use the factory; see the +[container documentation](../container/intro.md) for more information on how you +might accomplish that. + +## Consuming templates + +Now that we have the templating engine injected into our request handler, we can +consume it. Most often, we will want to render a template, optionally with +substitutions to pass to it. This will typically look like the following: + +```php +namespace Acme\Blog; + +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\RequestHandlerInterface; +use Zend\Diactoros\Response\HtmlResponse; +use Zend\Expressive\Template\TemplateRendererInterface; + +class EntryHandler implements RequestHandlerInterface +{ + private $templateRenderer; + + public function __construct(TemplateRendererInterface $renderer) + { + $this->templateRenderer = $renderer; + } + + public function handle(ServerRequestInterface $request) : ResponseInterface + { + // do some work... + return new HtmlResponse( + $this->templateRenderer->render('blog::entry', [ + 'entry' => $entry, + ]) + ); + } +} +``` diff --git a/docs/book/v3/features/template/plates.md b/docs/book/v3/features/template/plates.md new file mode 100644 index 00000000..d762775a --- /dev/null +++ b/docs/book/v3/features/template/plates.md @@ -0,0 +1,49 @@ +# Using Plates + +[Plates](https://github.com/thephpleague/plates) is a native PHP template system +maintained by [The League of Extraordinary Packages](http://thephpleague.com). +it provides: + +- Layout facilities. +- Template inheritance. +- Helpers for escaping, and the ability to provide custom helper extensions. + +We provide a [TemplateRendererInterface](interface.md) wrapper for Plates via +`Zend\Expressive\Plates\PlatesRenderer`. + +## Installing Plates + +To use the Plates wrapper, you must install the Plates integration: + +```bash +$ composer require zendframework/zend-expressive-platesrenderer +``` + +## Using the wrapper + +If instantiated without arguments, `Zend\Expressive\Plates\PlatesRenderer` will create +an instance of the Plates engine, which it will then proxy to. + +```php +use Zend\Expressive\Plates\PlatesRenderer; + +$renderer = new PlatesRenderer(); +``` + +Alternately, you can instantiate and configure the engine yourself, and pass it +to the `Zend\Expressive\Plates\PlatesRenderer` constructor: + +```php +use League\Plates\Engine as PlatesEngine; +use Zend\Expressive\Plates\PlatesRenderer; + +// Create the engine instance: +$plates = new PlatesEngine(); + +// Configure it: +$plates->addFolder('error', 'templates/error/'); +$plates->loadExtension(new CustomExtension(); + +// Inject: +$renderer = new PlatesRenderer($plates); +``` diff --git a/docs/book/v3/features/template/twig.md b/docs/book/v3/features/template/twig.md new file mode 100644 index 00000000..7befeae7 --- /dev/null +++ b/docs/book/v3/features/template/twig.md @@ -0,0 +1,134 @@ +# Using Twig + +[Twig](http://twig.sensiolabs.org/) is a template language and engine provided +as a standalone component by SensioLabs. It provides: + +- Layout facilities. +- Template inheritance. +- Helpers for escaping, and the ability to provide custom helper extensions. + +We provide a [TemplateRendererInterface](interface.md) wrapper for Twig via +`Zend\Expressive\Twig\TwigRenderer`. + +## Installing Twig + +To use the Twig wrapper, you must first install the Twig integration: + +```bash +$ composer require zendframework/zend-expressive-twigrenderer +``` + +## Using the wrapper + +If instantiated without arguments, `Zend\Expressive\Twig\TwigRenderer` will create +an instance of the Twig engine, which it will then proxy to. + +```php +use Zend\Expressive\Twig\TwigRenderer; + +$renderer = new TwigRenderer(); +``` + +Alternately, you can instantiate and configure the engine yourself, and pass it +to the `Zend\Expressive\Twig\TwigRenderer` constructor: + +```php +use Twig_Environment; +use Twig_Loader_Array; +use Zend\Expressive\Twig\TwigRenderer; + +// Create the engine instance: +$loader = new Twig_Loader_Array(include 'config/templates.php'); +$twig = new Twig_Environment($loader); + +// Configure it: +$twig->addExtension(new CustomExtension()); +$twig->loadExtension(new CustomExtension(); + +// Inject: +$renderer = new TwigRenderer($twig); +``` + +## Included extensions and functions + +The included Twig extension adds support for url generation. The extension is +automatically activated if the [UrlHelper](../helpers/url-helper.md) and +[ServerUrlHelper](../helpers/server-url-helper.md) are registered with the +container. + +The following template functions are exposed: + +- ``path``: Render the relative path for a given route and parameters. If there + is no route, it returns the current path. + + ```twig + {{ path('article_show', {'id': '3'}) }} + Generates: /article/3 + ``` + +- ``url``: Render the absolute url for a given route with its route parameters, + query string arguments, and fragment. If there is no route, it returns the + current url. + + ```twig + {{ url('article_show', {'id': '3'}, {'foo': 'bar'}, 'fragment') }} + Generates: http://example.com/article/3?foo=bar#fragment + ``` + +- ``absolute_url``: Render the absolute url from a given path. If the path is + empty, it returns the current url. + + ```twig + {{ absolute_url('path/to/something') }} + Generates: http://example.com/path/to/something + ``` + +- ``asset`` Render an (optionally versioned) asset url. + + ```twig + {{ asset('path/to/asset/name.ext', version=3) }} + Generates: path/to/asset/name.ext?v=3 + ``` + + To get the absolute url for an asset: + + ```twig + {{ absolute_url(asset('path/to/asset/name.ext', version=3)) }} + Generates: http://example.com/path/to/asset/name.ext?v=3 + ``` + +## Configuration + +The following details configuration specific to Twig, as consumed by the +`TwigRendererFactory`: + +```php +return [ + 'templates' => [ + 'extension' => 'file extension used by templates; defaults to html.twig', + 'paths' => [ + // namespace / path pairs + // + // Numeric namespaces imply the default/main namespace. Paths may be + // strings or arrays of string paths to associate with the namespace. + ], + ], + 'twig' => [ + 'cache_dir' => 'path to cached templates', + 'assets_url' => 'base URL for assets', + 'assets_version' => 'base version for assets', + 'extensions' => [ + // extension service names or instances + ], + 'globals' => [ + // Global variables passed to twig templates + 'ga_tracking' => 'UA-XXXXX-X' + ], + ], +]; +``` + +When specifying the `twig.extensions` values, always use fully qualified class +names or actual extension instances to ensure compatibility with any version of +Twig used. Version 2 of Twig _requires_ that a fully qualified class name is +used, and not a short-name alias. diff --git a/docs/book/v3/features/template/zend-view.md b/docs/book/v3/features/template/zend-view.md new file mode 100644 index 00000000..b831d6a6 --- /dev/null +++ b/docs/book/v3/features/template/zend-view.md @@ -0,0 +1,181 @@ +# Using zend-view + +[zend-view](https://docs.zendframework.com/zend-view/) provides a native PHP +template system via its `PhpRenderer`, and is maintained by Zend Framework. It +provides: + +- Layout facilities. +- Helpers for escaping, and the ability to provide custom helper extensions. + +We provide a [TemplateRendererInterface](interface.md) wrapper for zend-view's +`PhpRenderer` via `Zend\Expressive\ZendView\ZendViewRenderer`. + +## Installing zend-view + +To use the zend-view wrapper, you must first install the zend-view integration: + +```bash +$ composer require zendframework/zend-expressive-zendviewrenderer +``` + +## Using the wrapper + +If instantiated without arguments, `Zend\Expressive\ZendView\ZendViewRenderer` will create +an instance of the `PhpRenderer`, which it will then proxy to. + +```php +use Zend\Expressive\ZendView\ZendViewRenderer; + +$renderer = new ZendViewRenderer(); +``` + +Alternately, you can instantiate and configure the engine yourself, and pass it +to the `Zend\Expressive\ZendView\ZendViewRenderer` constructor: + +```php +use Zend\Expressive\ZendView\ZendViewRenderer; +use Zend\View\Renderer\PhpRenderer; +use Zend\View\Resolver; + +// Create the engine instance: +$renderer = new PhpRenderer(); + +// Configure it: +$resolver = new Resolver\AggregateResolver(); +$resolver->attach( + new Resolver\TemplateMapResolver(include 'config/templates.php'), + 100 +); +$resolver->attach( + (new Resolver\TemplatePathStack()) + ->setPaths(include 'config/template_paths.php') +); +$renderer->setResolver($resolver); + +// Inject: +$renderer = new ZendViewRenderer($renderer); +``` + +> ### Namespaced path resolving +> +> Expressive defines a custom zend-view resolver, +> `Zend\Expressive\ZendView\NamespacedPathStackResolver`. This resolver +> provides the ability to segregate paths by namespace, and later resolve a +> template according to the namespace, using the `namespace::template` notation +> required of `TemplateRendererInterface` implementations. +> +> The `ZendView` adapter ensures that: +> +> - An `AggregateResolver` is registered with the renderer. If the registered +> resolver is not an `AggregateResolver`, it creates one and adds the original +> resolver to it. +> - A `NamespacedPathStackResolver` is registered with the `AggregateResolver`, at +> a low priority (0), ensuring attempts to resolve hit it later. +> +> With resolvers such as the `TemplateMapResolver`, you can also resolve +> namespaced templates, mapping them directly to the template on the filesystem +> that matches; adding such a resolver can be a nice performance boost! + +## Layouts + +Unlike the other supported template engines, zend-view does not support layouts +out-of-the-box. Expressive abstracts this fact away, providing two facilities +for doing so: + +- You may pass a layout template name or `Zend\View\Model\ModelInterface` + instance representing the layout as the second argument to the constructor. +- You may pass a "layout" parameter during rendering, with a value of either a + layout template name or a `Zend\View\Model\ModelInterface` + instance representing the layout. Passing a layout this way will override any + layout provided to the constructor. + +In each case, the zend-view implementation will do a depth-first, recursive +render in order to provide content within the selected layout. + +- Since 1.3: You may also pass a boolean `false` value to either + `addDefaultParam()` or via the template variables for the `layout` key; doing + so will disable the layout. + +### Layout name passed to constructor + +```php +use Zend\Expressive\ZendView\ZendViewRenderer; + +// Create the engine instance with a layout name: +$renderer = new ZendViewRenderer(null, 'layout::layout'); +``` + +### Layout view model passed to constructor + +```php +use Zend\Expressive\ZendView\ZendViewRenderer; +use Zend\View\Model\ViewModel; + +// Create the layout view model: +$layout = new ViewModel([ + 'encoding' => 'utf-8', + 'cssPath' => '/css/prod/', +]); +$layout->setTemplate('layout::layout'); + +// Create the engine instance with the layout: +$renderer = new ZendViewRenderer(null, $layout); +``` + +### Provide a layout name when rendering + +```php +$content = $renderer->render('blog/entry', [ + 'layout' => 'layout::blog', + 'entry' => $entry, +]); +``` + +### Provide a layout view model when rendering + +```php +use Zend\View\Model\ViewModel; + +// Create the layout view model: +$layout = new ViewModel([ + 'encoding' => 'utf-8', + 'cssPath' => '/css/blog/', +]); +$layout->setTemplate('layout::layout'); + +$content = $renderer->render('blog/entry', [ + 'layout' => $layout, + 'entry' => $entry, +]); +``` + +## Helpers + +Expressive provides overrides of specific view helpers in order to better +integrate with [PSR-7](https://www.php-fig.org/psr/psr-7/). These include: + +- `Zend\Expressive\ZendView\UrlHelper`. This helper consumes the + application's `Zend\Expressive\Router\RouterInterface` instance in order + to generate URIs. Its signature is: + `url($routeName, array $routeParams = [], array $queryParams = [], $fragmentIdentifier = null, array $options = [])` +- `Zend\Expressive\ZendView\ServerUrlHelper`. This helper consumes the + URI from the application's request in order to provide fully qualified URIs. + Its signature is: `serverUrl($path = null)`. + + To use this particular helper, you will need to inject it with the request URI + somewhere within your application: + + ```php + $serverUrlHelper->setUri($request->getUri()); + ``` + + We recommend doing this within a pre-pipeline middleware. + +## Recommendations + +We recommend the following practices when using the zend-view adapter: + +- If using a layout, create a factory to return the layout view model as a + service; this allows you to inject it into middleware and add variables to it. +- While we support passing the layout as a rendering parameter, be aware that if + you change engines, this may not be supported. diff --git a/docs/book/v3/getting-started/features.md b/docs/book/v3/getting-started/features.md new file mode 100644 index 00000000..dc57ddac --- /dev/null +++ b/docs/book/v3/getting-started/features.md @@ -0,0 +1,199 @@ +# Overview + +Expressive allows you to write [PSR-7](http://www.php-fig.org/psr/psr-7/) +[middleware](https://docs.zendframework.com/zend-stratigility/middleware/) +applications for the web. + +PSR-7 is a standard defining HTTP message interfaces; these are the incoming +request and outgoing response for your application. By using PSR-7, we ensure +that your applications will work in other PSR-7 contexts. + +Middleware is any code sitting between a request and a response; it typically +analyzes the request to aggregate incoming data, delegates it to another layer +to process, and then creates and returns a response. Middleware can and should +be relegated only to those tasks, and should be relatively easy to write and +maintain. + +Middleware is also designed for composability; you should be able to nest +middleware and re-use middleware. + +With Expressive, you can build PSR-7-based middleware applications: + +- APIs +- Websites +- Single Page Applications +- and more. + +## Features + +Expressive builds on [zend-stratigility](https://docs.zendframework.com/zend-stratigility/) +to provide a robust convenience layer on which to build applications. The +features it provides include: + +- **Routing** + + Stratigility provides limited, literal matching only. Expressive allows you + to utilize dynamic routing capabilities from a variety of routers, providing + much more fine-grained matching capabilities. The routing layer also allows + restricting matched routes to specific HTTP methods, and will return "405 Not + Allowed" responses with an "Allow" HTTP header containing allowed HTTP + methods for invalid requests. + + Routing is abstracted in Expressive, allowing the developer to choose the + routing library that best fits the project needs. By default, we provide + wrappers for Aura.Router, FastRoute, and the zend-mvc router. + +- **PSR-11 Container** + + Expressive encourages the use of Dependency Injection, and defines its + `Application` class to compose a [PSR-11](https://www.php-fig.org/psr/psr-11) + `ContainerInterface` instance. The container is used to lazy-load middleware, + whether it is piped (Stratigility interface) or routed (Expressive). + +- **Templating** + + While Expressive does not assume templating is being used, it provides a + templating abstraction. Developers can write middleware that typehints on + this abstraction, and assume that the underlying adapter will provide + layout support and namespaced template support. + +- **Error Handling** + + Applications should handle errors gracefully, but also handle them differently + in development versus production. Expressive provides both basic error + handling via Stratigility's own `ErrorHandler` implementation, providing + specialized error response generators that can perform templating or use + Whoops. + +## Flow Overview + +Below is a diagram detailing the workflow used by Expressive. + +![Expressive Architectural Flow](../../images/architecture.png) + +The `Application` acts as an "onion"; in the diagram above, the top is the +outer-most layer of the onion, while the bottom is the inner-most. + +The `Application` dispatches each middleware. Each middleware receives a request +and a delegate for handing off processing of the request should the middleware +not be able to fully process it itself. Internally, the delegate composes a +queue of middleware, and invokes the next in the queue when invoked. + +Any given middleware can return a *response*, at which point execution winds +its way back out the onion. + +> ### Pipelines +> +> The terminology "pipeline" is often used to describe the onion. One way of +> looking at the "onion" is as a *queue*, which is first-in-first-out (FIFO) in +> operation. This means that the first middleware on the queue is executed first, +> and this invokes the next, and so on (and hence the "next" terminology). When +> looked at from this perspective: +> +> - In most cases, the entire queue *will not* be traversed. +> - The inner-most layer of the onion represents the last item in the queue, and +> should be guaranteed to return a response; usually this is indicative of +> a malformed request (HTTP 400 response status) and/or inability to route +> the middleware to a handler (HTTP 404 response status). +> - Responses are returned back *through* the pipeline, in reverse order of +> traversal. + +> ### Double pass middleware +> +> The system described above is what is known as _lambda middleware_. Each +> middleware receives the request and the delegate, and you pass only the +> request to the delegate when wanting to hand off processing: +> +> ```php +> function (ServerRequestInterface $request, DelegateInterface $delegate) +> { +> $response = $delegate->process($request); +> return $response->withHeader('X-Test', time()); +> } +> ``` +> +> In Expressive 1.X, the default middleware style was what is known as _double +> pass_ middleware. Double pass middleware receives both the request and a +> response in addition to the delegate, and passes both the request and response +> to the delegate when invoking it: +> +> ```php +> function (ServerRequestInterface $request, ResponseInterface $response, callable $next) +> { +> $response = $next($request, $response); +> return $response->withHeader('X-Test', time()); +> } +> ``` +> +> It is termed "double pass" because you pass both the request and response when +> delegating to the next layer. +> +> Expressive 2.X still supports double-pass middleware, though we recommend the +> lambda style. + +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 priorities are attached (and +thus executed) earlier, while those specifying lower and/or negative integers +are attached later. The default priority is 1. + +Expressive provides default implementations of "routing" and "dispatch" +middleware, which you either attach to the middleware pipeline manually, or via +configuration. These are implemented as the classes +`Zend\Expressive\Middleware\RouteMiddleware` and +`Zend\Expressive\Middleware\DispatchMiddleware`, respectively. + +Routing within Expressive consists of decomposing the request to match it to +middleware that can handle that given request. This typically consists of a +combination of matching the requested URI path along with allowed HTTP methods: + +- map a GET request to the path `/api/ping` to the `PingMiddleware` +- 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. + +Middleware piped to the application earlier than routing should be middleware +that you wish to execute for every request. These might include: + +- 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; +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. + +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 such middleware is to provide a "default" +error response for your application, usually as an HTTP 404 Not Found response. + +The main points to remember are: + +- The application is a queue, and operates in FIFO order. +- Each middleware can choose whether to return a response, which will cause + the queue to unwind, or to traverse to the next middleware. +- Most of the time, you will be defining *routed middleware*, and the routing + rules that map to them. +- *You* get to control the workflow of your application by deciding the order in + which middleware is queued. diff --git a/docs/book/v3/getting-started/skeleton.md b/docs/book/v3/getting-started/skeleton.md new file mode 100644 index 00000000..5f9fd3e6 --- /dev/null +++ b/docs/book/v3/getting-started/skeleton.md @@ -0,0 +1,631 @@ +# Quick Start: Using the Skeleton + Installer + +The easiest way to get started with Expressive is to use the [skeleton +application and installer](https://github.com/zendframework/zend-expressive-skeleton). +The skeleton provides a generic structure for creating your applications, and +prompts you to choose a router, dependency injection container, template +renderer, and error handler from the outset. + +## Create a new project + +First, we'll create a new project, using Composer's `create-project` command: + +```bash +$ composer create-project zendframework/zend-expressive-skeleton expressive +``` + +This will prompt you to choose: + +- Whether to install a minimal skeleton (no default middleware), a flat + application structure (all code under `src/`), or a modular structure + (directories under `src/` are modules, each with source code and potentially + templates, configuration, assets, etc.). + +- A dependency injection container. We recommend using the default, Zend + ServiceManager. + +- A router. We recommend using the default, FastRoute. + +- A template renderer. You can ignore this when creating an API project, but if + you will be creating any HTML pages, we recommend installing one. We prefer + Plates. + +- An error handler. Whoops is a very nice option for development, as it gives + you extensive, browseable information for exceptions and errors raised. + +## Start a web server + +The Skeleton + Installer creates a full application structure that's ready-to-go +when complete. You can test it out using [built-in web +server](http://php.net/manual/en/features.commandline.webserver.php). + +From the project root directory, execute the following: + +```bash +$ composer run --timeout=0 serve +``` + +This starts up a web server on localhost port 8080; browse to +http://localhost:8080/ to see if your application responds correctly! + +> ### Setting a timeout +> +> Composer commands time out after 300 seconds (5 minutes). On Linux-based +> systems, the `php -S` command that `composer serve` spawns continues running +> as a background process, but on other systems halts when the timeout occurs. +> +> As such, we recommend running the `serve` script using a timeout. This can +> be done by using `composer run` to execute the `serve` script, with a +> `--timeout` option. When set to `0`, as in the previous example, no timeout +> will be used, and it will run until you cancel the process (usually via +> `Ctrl-C`). Alternately, you can specify a finite timeout; as an example, +> the following will extend the timeout to a full day: +> +> ```bash +> $ composer run --timeout=86400 serve +> ``` + +## Development Tools + +We ship tools in our skeleton application to make development easier. + +### Development Mode + +[zf-development-mode](https://github.com/zfcampus/zf-development-mode) allows +you to enable and disable development mode from your cli. + +```bash +$ composer development-enable # enable development mode +$ composer development-disable # disable development mode +$ composer development-status # show development status +``` + +The development configuration is set in `config/autoload/development.local.php.dist`. +It also allows you to specify configuration and modules that should only be enabled +when in development, and not when in production. + +### Clear config cache + +Production settings are the default, which means enabling the configuration cache. +However, it must be easy for developers to clear the configuration cache. That's +what this command does. + +```bash +$ composer clear-config-cache +``` + +### Testing Your Code + +[PHPUnit](https://phpunit.de) and +[PHP_CodeSniffer](https://github.com/squizlabs/PHP_CodeSniffer) are now +installed by default. To execute tests and detect coding standards violations, +run the following command: + +```bash +$ composer check +``` + +### Security Advisories + +We have included the [security-advisories](https://github.com/Roave/SecurityAdvisories) +package to notify you about installed dependencies with known security +vulnerabilities. Each time you run `composer update`, `composer install`, or +`composer require`, it prevents installation of software with known and +documented security issues. + +## Modules + +Composer will prompt you during installation to ask if you want a +minimal application (no structure or default middleware provided), flat +application (all source code under the same tree, and the default selection), or +modular application. This latter option is new in the version 2 series, and +allows you to segregate discrete areas of application functionality into +_modules_, which can contain source code, templates, assets, and more; these can +later be repackaged for re-use if desired. + +Support for modules is available via the +[zend-component-installer](https://docs.zendframework.com/zend-component-installer/) +and [zend-config-aggregator](https://docs.zendframework.com/zend-config-aggregator/) +packages; the [zend-expressive-tooling](https://github.com/zendframework/zend-expressive-tooling). +package provides tools for creating and manipulating modules in your +application. + +### Component Installer + +Whenever you add a component or module that exposes itself as such, the +[zend-component-installer](https://docs.zendframework.com/zend-component-installer/) +composer plugin will prompt you, asking if and where you want to inject its +configuration. This ensures that components are wired automatically for you. + +In most cases, you will choose to inject in the `config/config.php` file; for +tools intended only for usage during development, choose +`config/development.config.php.dist`. + +### Config Aggregator + +The [zend-config-aggregator](https://docs.zendframework.com/zend-config-aggregator/) +library collects and merges configuration from different sources. It also supports +configuration caching. + +As an example, your `config/config.php` file might read as follows in order to +aggregate configuration from development mode settings, application +configuration, and theoretical `User`, `Blog`, and `App` modules: + +```php +getMergedConfig(); +``` + +The configuration is merged in the same order as it is passed, with later entries +having precedence. + +### Config Providers + +`ConfigAggregator` works by aggregating "Config Providers" passed to its +constructor. Each provider should be a callable class that requires no +constructor parameters, where invocation returns a configuration array (or a PHP +generator) to be merged. + +Libraries or modules can have configuration providers that provide default values +for a library or module. For the `UserModule\ConfigProvider` class loaded in the +`ConfigAggregator` above, the `ConfigProvider` might look like this: + +```php + $this->getDependencies(), + 'users' => $this->getConfig(), + ]; + } + + /** + * Returns the container dependencies + * + * @return array + */ + public function getDependencies() + { + return [ + 'factories' => [ + Action\LoginAction::class => + Factory\Action\LoginActionFactory::class, + + Middleware\AuthenticationMiddleware::class => + Factory\Middleware\AuthenticationMiddlewareFactory::class, + ], + ]; + } + + /** + * Returns the default module configuration + * + * @return array + */ + public function getConfig() + { + return [ + 'paths' => [ + 'enable_registration' => true, + 'enable_username' => false, + 'enable_display_name' => true, + ], + ]; + } +} +``` + +### expressive-module command + +To aid in the creation, registration, and deregistration of modules in your +application, the installer will add the [zendframework/zend-expressive-tooling](https://github.com/zendframework/zend-expressive-tooling) +as a development requirement when you choose the modular application layout. + +The tool is available from your application root directory via +`./vendor/bin/expressive-module`. For brevity, we will only reference the tool's +name, `expressive-module`, when describing its capabilities. + +This tool provides the following functionality: + +- `expressive-module create ` will create the default directory + structure for the named module, create a `ConfigProvider` for the module, add + an autoloading rule to `composer.json`, and register the `ConfigProvider` with + the application configuration. +- `expressive-module register ` will add an autoloading rule to + `composer.json` for the module, and register its `ConfigProvider`, if found, + with the application configuration. +- `expressive-module deregister ` will remove any autoloading rules + for the module from `composer.json`, and deregister its `ConfigProvider`, if + found, from the application configuration. + +You can find out more about its features in the [command line tooling +documentation](../reference/cli-tooling.md#modules). + +## Adding Middleware + +The skeleton makes the assumption that you will be writing your middleware as +classes, and uses [piping and routing](../features/router/piping.md) to add +your middleware. + +### Piping + +[Piping](../features/router/piping.md#piping) is a foundation feature of the +underlying [zend-stratigility](https://docs.zendframework.com/zend-stratigility/) +implementation. You can setup the middleware pipeline in `config/pipeline.php`. +In this section, we'll demonstrate setting up a basic pipeline that includes +error handling, segregated applications, routing, middleware dispatch, and more. + +The error handler should be the first (most outer) middleware to catch all +exceptions. + +```php +$app->pipe(ErrorHandler::class); +$app->pipe(ServerUrlMiddleware::class); +``` + +After the `ErrorHandler` you can pipe more middleware that you want to execute +on every request, such as bootstrapping, pre-conditions, and modifications to +outgoing responses: + +```php +$app->pipe(ServerUrlMiddleware::class); +``` + +Piped middleware may be either callables or service names. Middleware may also +be passed as an array; each item in the array must resolve to middleware +eventually (i.e., callable or service name); underneath, Expressive creates +`Zend\Stratigility\MiddlewarePipe` instances with each of the middleware listed +piped to it. + +Middleware can be attached to specific paths, allowing you to mix and match +applications under a common domain. The handlers in each middleware attached +this way will see a URI with the **MATCHED PATH SEGMENT REMOVED!!!** + +```php +$app->pipe('/api', $apiMiddleware); +$app->pipe('/docs', $apiDocMiddleware); +$app->pipe('/files', $filesMiddleware); +``` + +Next, you should register the routing middleware in the middleware pipeline: + +```php +$app->pipeRoutingMiddleware(); +``` + +Add more middleware that needs to introspect the routing results; this might +include: + +- handling for HTTP `HEAD` requests +- handling for HTTP `OPTIONS` requests +- middleware for handling URI generation +- route-based authentication +- route-based validation +- etc. + +```php +$app->pipe(ImplicitHeadMiddleware::class); +$app->pipe(ImplicitOptionsMiddleware::class); +$app->pipe(UrlHelperMiddleware::class); +``` + +Next, register the dispatch middleware in the middleware pipeline: + +```php +$app->pipeDispatchMiddleware(); +``` + +At this point, if no response is return by any middleware, we need to provide a +way of notifying the user of this; by default, we use the `NotFoundHandler`, but +you can provide any other fallback middleware you wish: + +```php +$app->pipe(NotFoundHandler::class); +``` + +The full example then looks something like this: + +```php +// In config/pipeline.php: + +use Zend\Expressive\Helper\ServerUrlMiddleware; +use Zend\Expressive\Helper\UrlHelperMiddleware; +use Zend\Expressive\Middleware\ImplicitHeadMiddleware; +use Zend\Expressive\Middleware\ImplicitOptionsMiddleware; +use Zend\Expressive\Middleware\NotFoundHandler; +use Zend\Stratigility\Middleware\ErrorHandler; + +$app->pipe(ErrorHandler::class); +$app->pipe(ServerUrlMiddleware::class); + +// These assume that the variables listed are defined in this scope: +$app->pipe('/api', $apiMiddleware); +$app->pipe('/docs', $apiDocMiddleware); +$app->pipe('/files', $filesMiddleware); + +$app->pipeRoutingMiddleware(); +$app->pipe(ImplicitHeadMiddleware::class); +$app->pipe(ImplicitOptionsMiddleware::class); +$app->pipe(UrlHelperMiddleware::class); +$app->pipeDispatchMiddleware(); + +$app->pipe(NotFoundHandler::class); +``` + +### Routing + +[Routing](../features/router/piping.md#routing) is an additional feature +provided by Expressive. Routing is setup in `config/routes.php`. + +You can setup routes with a single request method: + +```php +$app->get('/', App\Action\HomePageAction::class, 'home'); +$app->post('/album', App\Action\AlbumCreateAction::class, 'album.create'); +$app->put('/album/:id', App\Action\AlbumUpdateAction::class, 'album.put'); +$app->patch('/album/:id', App\Action\AlbumUpdateAction::class, 'album.patch'); +$app->delete('/album/:id', App\Action\AlbumDeleteAction::class, 'album.delete'); +``` + +Or with multiple request methods: + +```php +$app->route('/contact', App\Action\ContactAction::class, ['GET', 'POST', ...], 'contact'); +``` + +Or handling all request methods: + +```php +$app->route('/contact', App\Action\ContactAction::class)->setName('contact'); +``` + +Alternately, to be explicit, the above could be written as: + +```php +$app->route( + '/contact', + App\Action\ContactAction::class, + Zend\Expressive\Router\Route::HTTP_METHOD_ANY, + 'contact' +); +``` + +We recommend a single middleware class per combination of route and request +method. + +## Next Steps + +The skeleton provides a default structure for templates, if you choose to use them. +Let's see how you can create your first vanilla middleware, and templated middleware. + +### Creating middleware + +To create middleware, create a class implementing +`Psr\Http\Server\MiddlewareInterface`. This interface defines a single method, +`process()`, which accepts a `Psr\Http\Message\ServerRequestInterface` instance +and a `Psr\Http\Server\RequestHandlerInterface` instance. + +> ### Legacy double-pass middleware +> +> Prior to Expressive 2.0, the default middleware style was what is termed +> "double-pass", for the fact that it passes both the request and response between +> layers. This middleware did not require an interface, and relied on a +> conventional definition of: +> +> ```php +> use Psr\Http\Message; +> +> function ( +> Message\ServerRequestInterface $request, +> Message\ResponseInterface $response, +> callable $next +> ) : Message\ResponseInterface +> ``` +> +> While this style of middleware is still quite wide-spread and used in a number +> of projects, it has some flaws. Chief among them is the fact that middleware +> should not rely on the `$response` instance provided to them (as it may have +> modifications unacceptable for the current context), and that a response +> returned from inner layers may not be based off the `$response` provided to them +> (as inner layers may create and return a completely different response). +> +> Expressive 3.0 only supports [PSR-15 middleware and request handlers](https://www.php-fig.org/psr/psr-15/). +> Double-pass middleware is no longer supported. +> +> You may use +> `Zend\Stratigility\Middleware\DoublePassMiddlewareDecorator` (or the utility +> function `Zend\Stratigility\doublePassMiddleware()`) in order to decorate such +> middleware for use in Expressive 3. + +The skeleton defines an `App` namespace for you, and suggests placing middleware +under the namespace `App\Handler`. + +Let's create a "Hello" action. Place the following in +`src/App/Handler/HelloHandler.php`: + +```php +getQueryParams()['target'] ?? 'World'; + + $target = htmlspecialchars($target, ENT_HTML5, 'UTF-8'); + + return new HtmlResponse(sprintf( + '

Hello, %s!

', + $target + )); + } +} +``` + +The above looks for a query string parameter "target", and uses its value to +provide a message, which is then returned in an HTML response. + +Now we need to inform the application of this middleware, and indicate what +path will invoke it. Open the file `config/autoload/dependencies.global.php`. +Edit that file to add an _invokable_ entry for the new middleware: + +```php +return [ + 'dependencies' => [ + /* ... */ + 'invokables' => [ + App\Handler\HelloHandler::class => App\Handler\HelloHandler::class, + /* ... */ + ], + /* ... */ + ], +]; +``` + +Now open the file `config/routes.php`, and add the following at the bottom of +the file: + +```php +$app->get('/hello', App\Handler\HelloHandler::class, 'hello'); +``` + +Once you've completed the above, give it a try by going to each of the +following URIs: + +- http://localhost:8080/hello +- http://localhost:8080/hello?target=ME + +You should see the message change as you go between the two URIs! + +### Using templates + +You likely don't want to hardcode HTML into your middleware; so, let's use +templates. This particular exercise assumes you chose to use the +[Plates](http://platesphp.com) integration. + +Templates are installed under the `templates/` subdirectory. By default, we also +register the template namespace `app` to correspond with the `templates/app` +subdirectory. Create the file `templates/app/hello-world.phtml` with the +following contents: + +```php +layout('layout::default', ['title' => 'Greetings']) ?> + +

Hello, e($target) ?>

+``` + +Now that we have a template, we need to: + +- Inject a renderer into our action class. +- Use the renderer to render the contents. + +Replace your `src/App/Handler/HelloHandler.php` file with the following contents: + +```php +renderer = $renderer; + } + + public function handle(ServerRequestInterface $request) : ResponseInterface + { + $target = $request->getQueryParams()['target'] ?? 'World'; + + return new HtmlResponse( + $this->renderer->render('app::hello-world', ['target' => $target]) + ); + } +} +``` + +The above modifies the class to accept a renderer to the constructor, and then +calls on it to render a template. Note that we no longer need to escape our +target; the template takes care of that for us. + +How does the template renderer get into the action? The answer is dependency +injection. + +For the next part of the example, we'll be creating and wiring a factory for +creating the `HelloHandler` instance. + +zend-expressive-tooling provides a tool for generating factories based on +reflecting a class; we'll use that to generate our factory: + +```bash +$ composer expressive factory:create "App\\Handler\\HelloHandler" +``` + +This command not only creates the factory, but also registers it in our +configuration! + +Now re-visit the URIs: + +- http://localhost:8080/hello +- http://localhost:8080/hello?target=ME + +Your page should now have the same layout as the landing page of the skeleton +application! + +## Congratulations! + +Congratulations! You've now created your application, and started writing +middleware! It's time to start learning about the rest of the features of +Expressive: + +- [Containers](../features/container/intro.md) +- [Routing](../features/router/intro.md) +- [Templating](../features/template/intro.md) +- [Error Handling](../features/error-handling.md) diff --git a/docs/book/v3/getting-started/standalone.md b/docs/book/v3/getting-started/standalone.md new file mode 100644 index 00000000..cfaa510c --- /dev/null +++ b/docs/book/v3/getting-started/standalone.md @@ -0,0 +1,157 @@ +# Quick Start: Standalone Usage + +Expressive allows you to get started at your own pace. You can start with +the simplest example, detailed below, or move on to a more structured, +configuration-driven approach as detailed in the [use case examples](../reference/usage-examples.md). + +## 1. Create a new project directory + +First, let's create a new project directory and enter it: + +```bash +$ mkdir expressive +$ cd expressive +``` + +## 2. Install Expressive + +If you haven't already, [install Composer](https://getcomposer.org). Once you +have, we can install Expressive, along with a router and a container: + +```bash +$ composer require zendframework/zend-expressive zendframework/zend-expressive-fastroute zendframework/zend-servicemanager +``` + +> ### Routers +> +> Expressive needs a routing implementation in order to create routed +> middleware. We suggest FastRoute in the quick start, but you can also +> currently choose from Aura.Router and zend-router. + +> ### Containers +> +> We highly recommend using dependency injection containers with Expressive; +> they allow you to define dependencies for your middleware, as well as to lazy +> load your middleware only when it needs to be executed. We suggest +> zend-servicemanager in the quick start, but you can also use any container +> supporting [PSR-11 Container](https://github.com/php-fig/container). + +## 3. Create a web root directory + +You'll need a directory from which to serve your application, and for security +reasons, it's a good idea to keep it separate from your source code. We'll +create a `public/` directory for this: + +```bash +$ mkdir public +``` + +## 4. Create your bootstrap script + +Next, we'll create a bootstrap script. Such scripts typically setup the +environment, setup the application, and invoke it. This needs to be in our web +root, and we want it to intercept any incoming request; as such, we'll use +`public/index.php`: + +```php +get('/', function ($request, DelegateInterface $delegate) { + return new TextResponse('Hello, world!'); +}); + +$app->pipeRoutingMiddleware(); +$app->pipeDispatchMiddleware(); +$app->run(); +``` + +> ### Rewriting URLs +> +> Many web servers will not rewrite URLs to the bootstrap script by default. If +> you use Apache, for instance, you'll need to setup rewrite rules to ensure +> your bootstrap is invoked for unknown URLs. We'll cover that in a later +> chapter. + +> ### Routing and dispatching +> +> Note the lines from the above: +> +> ```php +> $app->pipeRoutingMiddleware(); +> $app->pipeDispatchMiddleware(); +> ``` +> +> Expressive's `Application` class provides two separate middlewares, one for +> routing, and one for dispatching middleware matched by routing. This allows +> you to slip in validations between the two activities if desired. They are +> not automatically piped to the application, however, to allow exactly that +> situation, which means they must be piped manually. + +## 5. Start a web server + +Since we're just testing out the basic functionality of our application, we'll +use PHP's [built-in web server](http://php.net/manual/en/features.commandline.webserver.php). + +From the project root directory, execute the following: + +```bash +$ php -S 0.0.0.0:8080 -t public/ +``` + +This starts up a web server on localhost port 8080; browse to +http://localhost:8080/ to see if your application responds correctly! + +> ### Tip: Serve via Composer +> +> To simplify starting up a local web server, try adding the following to your +> `composer.json`: +> +> ```json +> "scripts": { +> "serve": "php -S 0.0.0.0:8080 -t public/" +> } +> ``` +> +> Once you've added that, you can fire up the web server using: +> +> ```bash +> $ composer serve +> ``` + +> ### Setting a timeout +> +> Composer commands time out after 300 seconds (5 minutes). On Linux-based +> systems, the `php -S` command that `composer serve` spawns continues running +> as a background process, but on other systems halts when the timeout occurs. +> +> As such, we recommend running the `serve` script using a timeout. This can +> be done by using `composer run` to execute the `serve` script, with a +> `--timeout` option. When set to `0`, as in the previous example, no timeout +> will be used, and it will run until you cancel the process (usually via +> `Ctrl-C`). Alternately, you can specify a finite timeout; as an example, +> the following will extend the timeout to a full day: +> +> ```bash +> $ composer run --timeout=86400 serve +> ``` + +## Next steps + +At this point, you have a working zend-expressive application, that responds to +a single route. From here, you may want to read up on: + +- [Applications](../features/application.md) +- [Containers](../features/container/intro.md) +- [Routing](../features/router/intro.md) +- [Templating](../features/template/intro.md) +- [Error Handling](../features/error-handling.md) + +Additionally, we have more [use case examples](../reference/usage-examples.md). diff --git a/docs/book/v3/index.md b/docs/book/v3/index.md new file mode 100644 index 00000000..8452cc12 --- /dev/null +++ b/docs/book/v3/index.md @@ -0,0 +1,33 @@ +# Expressive: PSR-7 Middleware in Minutes + +Expressive builds on [Stratigility](https://docs.zendframework.com/zend-stratigility/) +to provide a minimalist [PSR-7](http://www.php-fig.org/psr/psr-7/) middleware +framework for PHP, with the following features: + +- Routing. Choose your own router; we support: + - [Aura.Router](https://github.com/auraphp/Aura.Router) + - [FastRoute](https://github.com/nikic/FastRoute) + - [zend-router](https://github.com/zendframework/zend-router) +- DI Containers, via [PSR-11 Container](https://www.php-fig.org/psr/psr-11/). + All middleware composed in Expressive may be retrieved from the composed + container. +- Optionally, templating. We support: + - [Plates](http://platesphp.com/) + - [Twig](http://twig.sensiolabs.org/) + - [zend-view's PhpRenderer](https://docs.zendeframework..com/zend-view/) +- Error handling. Create templated error pages, or use tools like + [whoops](https://github.com/filp/whoops) for debugging purposes. +- Nested middleware applications. Write an application, and compose it later + in another, optionally under a separate subpath. +- [Simplified installation](getting-started/skeleton.md). Our custom + [Composer](https://getcomposer.org)-based installer prompts you for your + initial stack choices, giving you exactly the base you want to start from. + +Essentially, Expressive allows *you* to develop using the tools *you* prefer, +and provides minimal structure and facilities to ease your development. + +Should I choose it over Zend\Mvc? +That’s a good question. [Here’s what we recommend.](why-expressive.md) + +If you’re keen to get started, then [keep reading](getting-started/features.md) +and get started writing your first middleware application today! diff --git a/docs/book/v3/reference/cli-tooling.md b/docs/book/v3/reference/cli-tooling.md new file mode 100644 index 00000000..c65f8b34 --- /dev/null +++ b/docs/book/v3/reference/cli-tooling.md @@ -0,0 +1,285 @@ +# Command Line Tooling + +Expressive offers a number of tools for assisting in project development. This +page catalogues each. + +## Development Mode + +The package [zfcampus/zf-development-mode](https://github.com/zfcampus/zf-development-mode) +provides a simple way to toggle in and out of _development mode_. Doing so +allows you to ship known development-specific settings within your repository, +while ensuring they are not enabled in production. The tooling essentially +enables optional, development-specific configuration in your application by: + +- Copying the file `config/development.config.php.dist` to + `config/development.config.php`; this can be used to enable + development-specific modules or settings (such as the `debug` flag). +- Copying the file `config/autoload/development.local.php.dist` to + `config/autoload/development.local.php`; this can be used to provide local + overrides of a number of configuration settings. + +The package provides the tooling via `vendor/bin/zf-development-mode`. If you +are using the Expressive skeleton, it provides aliases via Composer: + +```php +$ composer development-enable +$ composer development-disable +$ composer development-status +``` + +Add settings to your `development.*.php.dist` files, and commit those files to +your repository; always toggle out of and into development mode after making +changes, to ensure they pick up in your development environment. + +## Expressive command-line tool + +The package [zendframework/zend-expressive-tooling](https://github.com/zendframework/zend-expressive-tooling) +provides the script `vendor/bin/expressive`, which contains a number of commands +related to migration, modules, and middleware. + +You can install it if it is not already present in your application: + +```bash +$ composer require --dev zendframework/zend-expressive-tooling +``` + +If you installed the Expressive skeleton prior to version 2.0.2, you will want +to update the tooling to get the latest release, which contains the `expressive` +binary, as follows: + +```bash +$ composer require --dev "zendframework/zend-expressive-tooling:^0.4.1" +``` + +Once installed, invoking the binary without arguments will give a listing of +available tools: + +```bash +$ ./vendor/bin/expressive +``` + +Commands supported include: + +- **`middleware:create `**: Create a class file for the named + middleware class. The class _must_ use a namespace already declared in your + application, and will be created relative to the path associated with that + namespace. + +- **`migrate:error-middleware-scanner [--dir|-d]`**: Scan the associated + directory (defaults to `src`) for declarations of legacy Stratigility v1 error + middleware, or invocations of `$next()` that provide an error argument. See + the [section on detecting legacy error middleware](#detect-usage-of-legacy-error-middleware) + for more details. + +- **`migrate:original-messages [--src|-s]`**: Scan the associated source directory + (defaults to `src`) for `getOriginal*()` method calls and replace them with + `getAttribute()` calls. See the [section on detecting legacy + calls](#detect-usage-of-legacy-getoriginal-calls) for more details. + +- **`migrate:pipeline [--config-file|-c]`**: Convert configuration-driven + pipelines and routing to programmatic declarations. See the [section on + migrating to programmatic pipelines](#migrate-to-programmatic-pipelines) for + more details. + +- **`module:create [--composer|-c] [--modules-path|-p] `**: Create the + named module, add and generate autoloading rules for it, and register the + module's `ConfigProvider` with your application. + +- **`module:register [--composer|-c] [--modules-path|-p] `**: Add and + generate autoloading rules for the named module, and register the module's + `ConfigProvider` with your application. + +- **`module:deregister [--composer|-c] [--modules-path|-p] `**: Remove + autoloading rules for the named module and regenerate autoloading rules; + remove the module's `ConfigProvider` from the application configuration. + +You may obtain full help for each command by invoking: + +```bash +$ ./vendor/bin/expressive help +``` + +## Modules + +- Deprecated since zend-expressive-tooling 0.4.0; see the [Expressive CLI tool + section above](#expressive-command-line-tool). + +The package [zendframework/zend-expressive-tooling](https://github.com/zendframework/zend-expressive-tooling) +provides the binary `vendor/bin/expressive-module`, which allows you to create, +register, and deregister modules, assuming you are using a [modular application +layout](../features/modular-applications.md). + +For instance, if you wish to create a new module for managing users, you might +execute the following: + +```bash +$ ./vendor/bin/expressive-module create User +``` + +Which would create the following tree: + +```text +src/ + User/ + src/ + ConfigProvider.php + templates/ +``` + +It would also create an autoloading rule within your `composer.json` for the +`User` namespace, pointing it at the `src/User/src/` tree (and updating the +autoloader in the process), and register the new module's `ConfigProvider` +within your `config/config.php`. + +The `register` command will take an existing module and: + +- Add an autoloading rule for it to your `composer.json`, if necessary. +- Add an entry for the module's `ConfigProvider` class to your + `config/config.php`, if possible. + +```bash +$ ./vendor/bin/expressive-module register Account +``` + +The `deregister` command does the opposite of `register`. + +```bash +$ ./vendor/bin/expressive-module deregister Account +``` + +## Migrate to programmatic pipelines + +- Deprecated since zend-expressive-tooling 0.4.0; see the [Expressive CLI tool + section above](#expressive-command-line-tool). + +We recommend using _programmatic pipelines_, versus configuration-defined +pipelines. For those upgrading their applications from 1.X versions, we provide +a tool that will read their application configuration and generate: + +- `config/pipeline.php`, with the middleware pipeline +- `config/routes.php`, with routing directives +- `config/autoload/zend-expressive.global.php`, with settings to ensure + programmatic pipelines are used, and new middleware provided for Expressive + 2.0 is registered. +- directives within `public/index.php` for using the generated pipeline and + routes directives. + +To use this feature, you will need to first install +zendframework/zend-expressive-tooling: + +```bash +$ composer require --dev zendframework/zend-expressive-tooling +``` + +Invoke it as follows: + +```bash +$ ./vendor/bin/expressive-pipeline-from-config generate +``` + +The tool will notify you of any errors, including whether or not it found (and +skipped) Stratigility v1-style "error middleware". + +## Detect usage of legacy getOriginal*() calls + +- Deprecated since zend-expressive-tooling 0.4.0; see the [Expressive CLI tool + section above](#expressive-command-line-tool). + +When upgrading to version 2.0, you will also receive an upgrade to +zendframework/zend-stratigility 2.0. That version eliminates internal decorator +classes for the request and response instances, which were used to provide +access to the outermost request/response; internal layers could use these to +determine the full URI that resulted in their invocation, which is useful when +you pipe using a path argument (as the path provided during piping is stripped +from the URI when invoking the matched middleware). + +This affects the following methods: + +- `Request::getOriginalRequest()` +- `Request::getOriginalUri()` +- `Response::getOriginalResponse()` + +To provide equivalent functionality, we provide a couple of tools. + +First, Stratigility provides middleware, `Zend\Stratigility\Middleware\OriginalMessages`, +which will inject the current request, its URI, and, if invoked as double-pass +middleware, current response, as _request attributes_, named, respectively, +`originalRequest`, `originalUri`, and `originalResponse`. (Since Expressive 2.0 +decorates double-pass middleware using a wrapper that composes a response, the +"original response" will be the response prototype composed in the `Application` +instance.) This should be registered as the outermost middleware layer. +Middleware that needs access to these instances can then use the following +syntax to retrieve them: + +```php +$originalRequest = $request->getAttribute('originalRequest', $request); +$originalUri = $request->getAttribute('originalUri', $request->getUri(); +$originalResponse = $request->getAttribute('originalResponse') ?: new Response(); +``` + +> ### Original response is not trustworthy +> +> As noted above, the "original response" will likely be injected with the +> response prototype from the `Application` instance. We recommend not using it, +> and instead either composing a pristine response instance in your middleware, +> or creating a new instance on-the-fly. + +To aid you in migrating your existing code to use the new `getAttribute()` +syntax, zendframework/zend-expressive-tooling provides a binary, +`vendor/bin/expressive-migrate-original-messages`. First, install that package: + +```bash +$ composer require --dev zendframework/zend-expressive-tooling +``` + +Then invoke it as follows: + +```bash +$ ./vendor/bin/expressive-migrate-original-messages scan +``` + +This script will update any `getOriginalRequest()` and `getOriginalUri()` calls, +and notify you of any `getOriginalResponse()` calls, providing you with details +on how to correct those manually. + +## Detect usage of legacy error middleware + +- Deprecated since zend-expressive-tooling 0.4.0; see the [Expressive CLI tool + section above](#expressive-command-line-tool). + +When upgrading to version 2.0, you will also receive an upgrade to +zendframework/zend-stratigility 2.0. That version eliminates what was known as +"error middleware", middleware that either implemented +`Zend\Stratigility\ErrorMiddlewareInterface`, or duck-typed it by implementing +the signature `function ($error, $request, $response, callable $next)`. + +Such "error middleware" allowed other middleware to invoke the `$next` argument +with an additional, third argument representing an error condition; when that +occurred, Stratigility/Expressive would start iterating through error middleware +until one was able to return a response. Each would receive the error as the +first argument, and determine how to act upon it. + +With version 2.0 of each project, such middleware is now no longer accepted, and +users should instead be using [the new error handling +features](../features/error-handling.md). However, you may find that: + +- You have defined error middleware in your application. +- You have standard middleware in your application that invokes `$next` with the + third, error argument. + +To help you identify such instances, zendframework/zend-expressive-tooling +provides the script `vendor/bin/expressive-scan-for-error-middleware`. First, +install that package: + +```bash +$ composer require --dev zendframework/zend-expressive-tooling +``` + +Then invoke it as follows: + +```bash +$ ./vendor/bin/expressive-scan-for-error-middleware scan +``` + +The script will notify you of any places where it finds either use case, and +provide feedback on how to update your application. diff --git a/docs/book/v3/reference/expressive-projects.md b/docs/book/v3/reference/expressive-projects.md new file mode 100644 index 00000000..bc13ebaf --- /dev/null +++ b/docs/book/v3/reference/expressive-projects.md @@ -0,0 +1,17 @@ +# Projects powered by zend-expressive + +zend-expressive can be used for anything. Here are some projects, tutorials and +the related source code. Have a look around and see how others have used +zend-expressive. + +## Sample Code & Tutorials +- Expressive Tutorial (WIP) - [*source*](https://github.com/RalfEggert/zend-expressive-tutorial) +- [AstroSplash](http://astrosplash.com/) - [*source*](https://github.com/AndrewCarterUK/AstroSplash) + (Also, read the [related article on sitepoint](http://www.sitepoint.com/build-nasa-photo-gallery-zend-expressive/)) +- [php-ddd-cargo-sample](https://codeliner.github.io/php-ddd-cargo-sample/) - [*source*](https://github.com/codeliner/php-ddd-cargo-sample) + +## Personal Sites +- [mwop.net](https://mwop.net/) - [*source*](https://github.com/weierophinney/mwop.net) +- [xtreamwayz.com](https://xtreamwayz.com/) - [*source*](https://github.com/xtreamwayz/xtreamwayz.com) +- [alejandrocelaya.com](http://www.alejandrocelaya.com/) - [*source*](https://github.com/acelaya/website-expressive) +- [zimuel.it](http://www.zimuel.it) - [*source*](https://github.com/ezimuel/zimuel.it) diff --git a/docs/book/v3/reference/migration.md b/docs/book/v3/reference/migration.md new file mode 100644 index 00000000..f4192ebf --- /dev/null +++ b/docs/book/v3/reference/migration.md @@ -0,0 +1,52 @@ +# Migration to Expressive 3.0 + +Expressive 3.0 should not result in many upgrade problems for users. However, +starting in this version, we offer a few changes affecting the following that +you should be aware of, and potentially update your application to adopt: + +- [PHP 7.1 support](#php-7.1-support) +- [Signature changes](#signature-changes) +- [Removed functionality](#removed-functionality) +- [PSR-15 support](#psr-15-support) + +## PHP 7.1 support + +Starting in Expressive 3.0 we support only PHP 7.1+. + +## Signature changes + +All middlewares and delegators implements now interfaces from PSR-15 +`http-interop/http-server-middleware`. It means the following changes: + +- middleware's `process` method type hint `RequestHandlerInterface` as + the second parameter instead of `DelegateInterface`, +- middleware's `process` method has now return type + `\Psr\Http\Message\ResponseInterface`, +- delegators are changed to request handlers: these now implements interface + `RequestHandlerInterface` instead of `DelegateInterface`, +- delegator's `process` method has been renamed to `handle` and + return type `\Psr\Http\Message\ResponseInterface` has been declared. + +The following signature changes were made that could affect _class extensions_: + +- `Zend\Expressive\Application::__construct(...)` + Third parameter is now `RequestHandlerInterface` instead of `DelegateInterface` + +## Removed functionality + +- double-pass middlewares (introduced in Expressive 1.X, deprecated in Expressive 2.X) + +## PSR-15 Support + +As said before, all middlewares and request handlers now implements PSR-15 +interfaces. It means `process` method (of middleware) and `handle` method +(of request handler) have declared return type `\Psr\Http\Message\ResponseInterface`. + +To update your middlewares you can use tool available in `zend-expressive-tooling`: + +```console +$ vendor/bin/expressive migrate:interop-middleware [--src|-s=] +``` + +It looks for all interop middlewares and delegators and convert them to PSR-15 +middlewares and request delegators. diff --git a/docs/book/v3/reference/usage-examples.md b/docs/book/v3/reference/usage-examples.md new file mode 100644 index 00000000..f4f98828 --- /dev/null +++ b/docs/book/v3/reference/usage-examples.md @@ -0,0 +1,617 @@ +# Usage Examples + +Below are several usage examples, covering a variety of ways of creating and +managing an application. + +In all examples, the assumption is the following directory structure: + +``` +. +├── config +├── data +├── composer.json +├── public +│   └── index.php +├── src +└── vendor +``` + +We assume also that: + +- You have installed zend-expressive per the [installation instructions](../../index.md#installation). +- `public/` will be the document root of your application. +- Your own classes are under `src/` with the top-level namespace `App`, + and you have configured [autoloading](https://getcomposer.org/doc/01-basic-usage.md#autoloading) + in your `composer.json` for those classes (this should be done for you during + installation). + +> ## Using the built-in web server +> +> You can use the built-in web server to run the examples. Run: +> +> ```bash +> $ php -S 0.0.0.0:8080 -t public +> ``` +> +> from the application root to start up a web server running on port 8080, and +> then browse to http://localhost:8080/. If you used the Expressive installer, +> the following is equivalent: +> +> ```bash +> $ composer run --timeout=0 serve +> ``` + +> ## Setting up autoloading for the Application namespace +> +> In your `composer.json` file, place the following: +> +> ```json +> "autoload": { +> "psr-4": { +> "Application\\": "src/" +> } +> }, +> ``` +> +> Once done, run: +> +> ```bash +> $ composer dump-autoload +> ``` + +### Routing + +As noted in the [Application documentation](../features/application.md#adding-routable-middleware), +routing is abstracted and can be accomplished by calling any of the following +methods: + +- `route($path, $middleware, array $methods = null, $name = null)` to route to a + path and match any HTTP method, multiple HTTP methods, or custom HTTP methods. +- `get($path, $middleware, $name = null)` to route to a path that will only + respond to the GET HTTP method. +- `post($path, $middleware, $name = null)` to route to a path that will only + respond to the POST HTTP method. +- `put($path, $middleware, $name = null)` to route to a path that will only + respond to the PUT HTTP method. +- `patch($path, $middleware, $name = null)` to route to a path that will only + respond to the PATCH HTTP method. +- `delete($path, $middleware, $name = null)` to route to a path that will only + respond to the DELETE HTTP method. + +All methods return a `Zend\Expressive\Router\Route` method, which allows you to +specify additional options to associate with the route (e.g., for specifying +criteria, default values to match, etc.). + +As examples: + +```php +// GET +// This demonstrates passing a middleware instance (assuming $helloWorld is +// valid middleware) +$app->get('/', $helloWorld); + +// POST +// This example specifies the middleware as a service name instead of as +// actual executable middleware. +$app->post('/trackback', 'TrackBack'); + +// PUT +// This example shows operating on the returned route. In this case, it's adding +// regex tokens to restrict what values for {id} will match. (The tokens feature +// is specific to Aura.Router.) +$app->put('/post/{id}', 'ReplacePost') + ->setOptions([ + 'tokens' => ['id' => '\d+'], + ]); + +// PATCH +// This example builds on the one above. Expressive allows you to specify +// the same path for a route matching on a different HTTP method, and +// corresponding to different middleware. +$app->patch('/post/{id}', 'UpdatePost') + ->setOptions([ + 'tokens' => ['id' => '\d+'], + ]); + +// DELETE +$app->delete('/post/{id}', 'DeletePost') + ->setOptions([ + 'tokens' => ['id' => '\d+'], + ]); + +// Matching ALL HTTP methods +// If the underlying router supports matching any HTTP method, the following +// will do so. Note: FastRoute *requires* you to specify the HTTP methods +// allowed explicitly, and does not support wildcard routes. As such, the +// following example maps to the combination of HEAD, OPTIONS, GET, POST, PATCH, +// PUT, TRACE, and DELETE. +// Just like the previous examples, it returns a Route instance that you can +// further manipulate. +$app->route('/post/{id}', 'HandlePost') + ->setOptions([ + 'tokens' => ['id' => '\d+'], + ]); + +// Matching multiple HTTP methods +// You can pass an array of HTTP methods as a third argument to route(); in such +// cases, routing will match if any of the specified HTTP methods are provided. +$app->route('/post', 'HandlePostCollection', ['GET', 'POST']); + +// Matching NO HTTP methods +// Pass an empty array to the HTTP methods. HEAD and OPTIONS will still be +// honored. (In FastRoute, GET is also honored.) +$app->route('/post', 'WillThisHandlePost', []); +``` + +Finally, if desired, you can create a `Zend\Expressive\Router\Route` instance +manually and pass it to `route()` as the sole argument: + +```php +$route = new Route('/post', 'HandlePost', ['GET', 'POST']); +$route->setOptions($options); + +$app->route($route); +``` + +## Hello World using a Container + +Expressive works with [PSR-11 Containers](https://www.php-fig.org/psr/psr-11/), +though it's an optional feature. By default, if you use the `AppFactory`, it +will use [zend-servicemanager](https://docs.zendframework.com/zend-servicemanager/) +so long as that package is installed. + +In the following example, we'll populate the container with our middleware, and +the application will pull it from there when matched. + +Edit your `public/index.php` to read as follows: + +```php +use Psr\Http\Server\RequestHandlerInterface; +use Zend\Diactoros\Response\JsonResponse; +use Zend\Diactoros\Response\TextResponse; +use Zend\Expressive\AppFactory; +use Zend\ServiceManager\ServiceManager; + +require __DIR__ . '/../vendor/autoload.php'; + +$container = new ServiceManager(); + +$container->setFactory('HelloWorld', function ($container) { + return function ($request, RequestHandlerInterface $handler) { + return new TextResponse('Hello, world!'); + }; +}); + +$container->setFactory('Ping', function ($container) { + return function ($request, RequestHandlerInterface $handler) { + return new JsonResponse(['ack' => time()]); + }; +}); + +$app = AppFactory::create($container); +$app->get('/', 'HelloWorld'); +$app->get('/ping', 'Ping'); + +$app->pipeRoutingMiddleware(); +$app->pipeDispatchMiddleware(); + +$app->run(); +``` + +In the example above, we pass our container to `AppFactory`. We could have also +done this instead: + +```php +$app = AppFactory::create(); +$container = $app->getContainer(); +``` + +and then added our service definitions. We recommend passing the container to +the factory instead; if we ever change which container we use by default, your +code might not work! + +The following two lines are the ones of interest: + +```php +$app->get('/', 'HelloWorld'); +$app->get('/ping', 'Ping'); +``` + +These map the two paths to *service names* instead of callables. When routing +matches a path, it does the following: + +- If the middleware provided when defining the route is callable, it uses it + directly. +- If the middleware is a valid service name in the container, it pulls it from + the container. *This is what happens in this example.* +- Finally, if no container is available, or the service name is not found in the + container, it checks to see if it's a valid class name; if so, it instantiates + and returns the class instance. + +If you fire up your web server, you'll find that the `/` and `/ping` paths +continue to work. + +One other approach you could take would be to define the application itself in +the container, and then pull it from there: + +```php +$container->setFactory('Zend\Expressive\Application', function ($container) { + $app = AppFactory::create($container); + $app->get('/', 'HelloWorld'); + $app->get('/ping', 'Ping'); + return $app; +}); + +$app = $container->get('Zend\Expressive\Application'); +$app->run(); +``` + +This is a nice way to encapsulate the application creation. You could then +potentially move all service configuration to another file! (We already +[document an ApplicationFactory for exactly this scenario.](../features/container/factories.md#applicationfactory)) + +## Hello World using a Configuration-Driven Container + +In the above example, we configured our middleware as services, and then passed +our service container to the application. At the end, we hinted that you could +potentially define the application itself as a service. + +Expressive already provides a service factory for the application instance +to provide fine-grained control over your application. In this example, we'll +leverage it, defining our routes via configuration. + +First, we're going to leverage zend-config to merge configuration files. This is +important, as it allows us to define local, environment-specific configuration +in files that we then can exclude from our repository. This practice ensures +that things like credentials are not accidentally published in a public +repository, and also provides a mechanism for slip-streaming in +configuration based on our environment (you might use different settings in +development than in production, after all!). + +First, install zend-config and zend-stdlib: + +```bash +$ composer require zendframework/zend-config zendframework/zend-stdlib +``` + +Now we can start creating our configuration files and container factories. + +In `config/config.php`, place the following: + +```php +configureServiceManager($container); + +// Inject config +$container->setService('config', $config); + +return $container; +``` + +In `config/autoload/dependencies.global.php`, place the following: + +```php + [ + 'invokables' => [ + \Application\HelloWorldAction::class => InvokableFactory::class, + \Application\PingAction::class => InvokableFactory::class, + ], + 'factories' => [ + \Zend\Expressive\Application::class => \Zend\Expressive\Container\ApplicationFactory::class, + ], + ] +]; +``` + +In `config/autoload/routes.global.php`, place the following: + +```php + [ + [ + 'path' => '/', + 'middleware' => \Application\HelloWorldAction::class, + 'allowed_methods' => ['GET'], + ], + [ + 'path' => '/ping', + 'middleware' => \Application\PingAction::class, + 'allowed_methods' => ['GET'], + ], + ], +]; +``` + +In `src/Application/HelloWorld.php`, place the following: + +```php +getBody()->write('Hello, world!'); + return $res; + } +} +``` + +In `src/Application/Ping.php`, place the following: + +```php + time()]); + } +} +``` + +After that’s done run: + +``` +composer dump-autoload +``` + +Finally, in `public/index.php`, place the following: + +```php +get(Zend\Expressive\Application::class); +$app->run(); +``` + +Notice that our index file now doesn't have any code related to setting up the +application any longer! All it does is setup autoloading, retrieve our service +container, pull the application from it, and run it. Our choices for container, +router, etc. are all abstracted, and if we change our mind later, this code will +continue to work. + +Firing up the web server, you'll see the same responses as the previous +examples. + +## Hybrid Container and Programmatic Creation + +The above example may look a little daunting at first. By making everything +configuration-driven, you sometimes lose a sense for how the code all fits +together. + +Fortunately, you can mix the two. Building on the example above, we'll add a new +route and middleware. Between pulling the application from the container and +calling `$app->run()`, add the following in your `public/index.php`: + +```php +$app->post('/post', function ($request, \Psr\Http\Server\RequestHandlerInterface $handler) { + return new \Zend\Diactoros\Response\TextResponse('IN POST!'); +}); +``` + +Note that we're using `post()` here; that means you'll have to use cURL, HTTPie, +Postman, or some other tool to test making a POST request to the path: + +```bash +$ curl -X POST http://localhost:8080/post +``` + +You should see `IN POST!` for the response! + +Using this approach, you can build re-usable applications that are +container-driven, and add one-off routes and middleware as needed. + +### Using the container to register middleware + +If you use a container to fetch your application instance, you have an +additional option for specifying middleware for the pipeline: configuration: + +```php + [ + [ + 'path' => '/path/to/match', + 'middleware' => 'Middleware Service Name or Callable', + 'allowed_methods' => ['GET', 'POST', 'PATCH'], + 'options' => [ + 'stuff' => 'to', + 'pass' => 'to', + 'the' => 'underlying router', + ], + ], + // etc. + ], + 'middleware_pipeline' => [ + // See specification below + ], +]; +``` + +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 must be in the following form: + +```php +[ + // required: + 'middleware' => 'Name of middleware service, or a callable', + // optional: + 'path' => '/path/to/match', + 'priority' => 1, // Integer +] +``` + +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. + +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\Application::ROUTING_MIDDLEWARE` for the + routing middleware; this should always come before the dispatch middleware. +- `Zend\Expressive\Application::DISPATCH_MIDDLEWARE` for the + dispatch middleware. + +As an example: + +```php +return [ + 'middleware_pipeline' => [ + [ /* ... */ ], + Zend\Expressive\Application::ROUTING_MIDDLEWARE, + Zend\Expressive\Application::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. +> +> Use priority to shape the specific workflow you want for your middleware. + +Middleware items may be any [valid middleware](../features/middleware-types.md), +including _arrays_ of middleware, which indicate a nested middleware pipeline; +these may even contain the routing and dispatch middleware constants: + +```php +return [ + 'middleware_pipeline' => [ + [ /* ... */ ], + 'routing' => [ + 'middleware' => [ + Zend\Expressive\Application::ROUTING_MIDDLEWARE, + /* ... middleware that introspects routing results ... */ + Zend\Expressive\Application::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 +application that match a common path root. + +## Segregating your application to a subpath + +One benefit of a middleware-based application is the ability to compose +middleware and segregate them by paths. `Zend\Expressive\Application` is itself +middleware, allowing you to do exactly that if desired. + +In the following example, we'll assume that `$api` and `$blog` are +`Zend\Expressive\Application` instances, and compose them into a +`Zend\Stratigility\MiddlewarePipe`. + +```php +use Zend\Diactoros\Server; +use Zend\Diactoros\ServerRequestFactory; +use Zend\Stratigility\MiddlewarePipe; + +require __DIR__ . '/../vendor/autoload.php'; + +$app = new MiddlewarePipe(); +$app->pipe('/blog', $blog); +$app->pipe('/api', $api); + +$server = Server::createServerFromRequest( + $app, + ServerRequestFactory::fromGlobals() +); +$server->listen(); +``` + +You could also compose them in an `Application` instance, and utilize `run()`: + +```php +$app = AppFactory::create(); +$app->pipe('/blog', $blog); +$app->pipe('/api', $api); + +$app->run(); +``` + +This approach allows you to develop discrete applications and compose them +together to create a website. diff --git a/docs/book/v3/why-expressive.md b/docs/book/v3/why-expressive.md new file mode 100644 index 00000000..6382720c --- /dev/null +++ b/docs/book/v3/why-expressive.md @@ -0,0 +1,85 @@ +# Should You Choose zend-expressive Over zend-mvc? + +We recommend that you choose Expressive for any new project — _if the +choice is yours to make_. + +## Why Use zend-mvc? + +zend-mvc is a proven platform, with more than half a decade of development +behind it. It is stable and battle-tested in production platforms. + +Because it is opinionated about project structure and architecture, fewer +decisions need be made up front; developers know where new code goes, and how it +will wire into the overall application. + +Additionally, a number of training courses exist, including [offerings by +Zend](http://www.zend.com/en/services/training/zf-fundamentals-i), allowing you +or your team to fully learn the framework and take advantage of all its features. + +Finally, zend-mvc has a lively [module ecosystem](https://packagist.org/search/?q=zf2), +allowing you to add features and capabilities to your application without +needing to develop them from scratch. + +## We Recommend Expressive + +[zend-mvc](https://github.com/zendframework/zend-mvc) has many preconceptions +about how things work, yet they're very broad and general. What’s more, it +also has several pre-wired structures in place that may either aid you — +or get in your way. + +As a result, you are required to know a lot of what those things are — *if* you +want to use it optimally. And to acquire that depth of knowledge, you’re going +to need to spend a lot of time digging deep into zend-mvc’s internals before +you begin to get the most out of it. + +To quote Zend Framework project lead, [Matthew Weier O’Phinney](https://mwop.net): + +> The problem is that zend-mvc is anything but beginner-friendly at this point. +> You're required to deep dive into the event manager, service manager, and +> module system — right from the outset; And to do this you need more than a +> passing understanding of object-oriented programming and a range of design +> patterns. + +Expressive (specifically applications based on +[the Expressive Skeleton Installer](getting-started/skeleton.md)) +on the other hand, comes with barely any of these assumptions and requirements. + +It provides a very minimalist structure. Essentially all you have to become +familiar with are five core components. These are: + +- A DI container. +- A router. +- An error handler for development. +- A template engine (if you’re not creating an API). +- PSR-7 messages and http-interop (future PSR-15) middleware. + +In many cases, these are provided for you by the skeleton, and do not require +any additional knowledge on your part. Given that, you can quickly get up to +speed with the framework and begin creating the application that you need. We +believe that this approach — in contrast to the zend-mvc approach — +is more flexible and accommodating. + +What’s more, you can mix and match the types of applications that you create. + +- Do you just need an API? Great; you can do that quite quickly. +- Do you want an HTML-based front-end? That’s available too. + +When building applications with Expressive, you can make use of the various Zend +components, or any third-party components or middleware. You can pick and +choose what you need, as and when you need it. You’re not bound by many, if +any, constraints and design decisions. + +## In Conclusion + +For what it’s worth, we’re **not** saying that zend-mvc is a poor choice! What +we are saying is: + +1. The learning curve, from getting started to building the first application, + is _significantly_ lower with Expressive +2. The ways in which you can create applications, whether through multiple + pieces of middleware or by combining multiple Expressive apps, into one + larger one, is a much more efficient and fluid way to work + +Ultimately, the choice is always up to you, your team, and your project’s needs. +We just want to ensure that you’ve got all the information you need, to make an +informed decision. diff --git a/docs/book/why-expressive.md b/docs/book/why-expressive.md index 6604e92f..6c779bc8 100644 --- a/docs/book/why-expressive.md +++ b/docs/book/why-expressive.md @@ -1,6 +1,6 @@ - + diff --git a/mkdocs.yml b/mkdocs.yml index 6dca4a2e..639f1d09 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -3,59 +3,125 @@ site_dir: docs/html pages: - index.md - 'Getting Started': - - 'Overview and Features': v2/getting-started/features.md - - 'Quick Start: Standalone': v2/getting-started/standalone.md - - 'Quick Start: Skeleton Installer': v2/getting-started/skeleton.md + - 'Overview and Features': v3/getting-started/features.md + - 'Quick Start: Standalone': v3/getting-started/standalone.md + - 'Quick Start: Skeleton Installer': v3/getting-started/skeleton.md - Features: - - "Middleware Types": v2/features/middleware-types.md - - Applications: v2/features/application.md + - "Middleware Types": v3/features/middleware-types.md + - Applications: v3/features/application.md - Containers: - - Introduction: v2/features/container/intro.md - - 'Container Factories': v2/features/container/factories.md - - 'Delegator Factories': v2/features/container/delegator-factories.md - - 'Using zend-servicemanager': v2/features/container/zend-servicemanager.md - - 'Using Pimple': v2/features/container/pimple.md - - 'Using Aura.Di': v2/features/container/aura-di.md + - Introduction: v3/features/container/intro.md + - 'Container Factories': v3/features/container/factories.md + - 'Delegator Factories': v3/features/container/delegator-factories.md + - 'Using zend-servicemanager': v3/features/container/zend-servicemanager.md + - 'Using Pimple': v3/features/container/pimple.md + - 'Using Aura.Di': v3/features/container/aura-di.md - 'Routing Adapters': - - Introduction: v2/features/router/intro.md - - 'Routing Interface': v2/features/router/interface.md - - 'URI Generation': v2/features/router/uri-generation.md - - 'Routing vs Piping': v2/features/router/piping.md - - 'Using Aura': v2/features/router/aura.md - - 'Using FastRoute': v2/features/router/fast-route.md - - 'Using the ZF2 Router': v2/features/router/zf2.md + - Introduction: v3/features/router/intro.md + - 'Routing Interface': v3/features/router/interface.md + - 'URI Generation': v3/features/router/uri-generation.md + - 'Routing vs Piping': v3/features/router/piping.md + - 'Using Aura': v3/features/router/aura.md + - 'Using FastRoute': v3/features/router/fast-route.md + - 'Using zend-router': v3/features/router/zf2.md - Templating: - - Introduction: v2/features/template/intro.md - - 'Template Renderer Interface': v2/features/template/interface.md - - 'Templated Middleware': v2/features/template/middleware.md - - 'Using Plates': v2/features/template/plates.md - - 'Using Twig': v2/features/template/twig.md - - 'Using zend-view': v2/features/template/zend-view.md - - 'Error Handling': v2/features/error-handling.md - - 'Modular Applications': v2/features/modular-applications.md + - Introduction: v3/features/template/intro.md + - 'Template Renderer Interface': v3/features/template/interface.md + - 'Templated Middleware': v3/features/template/middleware.md + - 'Using Plates': v3/features/template/plates.md + - 'Using Twig': v3/features/template/twig.md + - 'Using zend-view': v3/features/template/zend-view.md + - 'Error Handling': v3/features/error-handling.md + - 'Modular Applications': v3/features/modular-applications.md - Middleware: - - 'Implicit HEAD and OPTIONS Middleware': v2/features/middleware/implicit-methods-middleware.md + - 'Implicit HEAD and OPTIONS Middleware': v3/features/middleware/implicit-methods-middleware.md - Helpers: - - Introduction: v2/features/helpers/intro.md - - UrlHelper: v2/features/helpers/url-helper.md - - ServerUrlHelper: v2/features/helpers/server-url-helper.md - - 'Body Parsing Middleware': v2/features/helpers/body-parse.md - - 'Content-Length Middleware': v2/features/helpers/content-length.md - - Emitters: v2/features/emitters.md + - Introduction: v3/features/helpers/intro.md + - UrlHelper: v3/features/helpers/url-helper.md + - ServerUrlHelper: v3/features/helpers/server-url-helper.md + - 'Body Parsing Middleware': v3/features/helpers/body-parse.md + - 'Content-Length Middleware': v3/features/helpers/content-length.md + - Emitters: v3/features/emitters.md - Cookbook: - - 'Autowiring routes and pipeline middleware': v2/cookbook/autowiring-routes-and-pipelines.md - - 'Prepending a common path to all routes': v2/cookbook/common-prefix-for-routes.md - - 'Route-specific middleware pipelines': v2/cookbook/route-specific-pipeline.md - - 'Registering custom view helpers when using zend-view': v2/cookbook/using-custom-view-helpers.md - - 'Using zend-form view helpers': v2/cookbook/using-zend-form-view-helpers.md - - 'Using Expressive from a subdirectory': v2/cookbook/using-a-base-path.md - - 'Setting a locale based on a routing parameter': v2/cookbook/setting-locale-depending-routing-parameter.md - - 'Setting a locale without a routing parameter': v2/cookbook/setting-locale-without-routing-parameter.md - - 'Enabling debug toolbars': v2/cookbook/debug-toolbars.md - - 'Handling multiple routes in a single class': v2/cookbook/using-routed-middleware-class-as-controller.md - - 'Flash Messengers': v2/cookbook/flash-messengers.md - - 'Passing data between middleware': v2/cookbook/passing-data-between-middleware.md + - 'Autowiring routes and pipeline middleware': v3/cookbook/autowiring-routes-and-pipelines.md + - 'Prepending a common path to all routes': v3/cookbook/common-prefix-for-routes.md + - 'Route-specific middleware pipelines': v3/cookbook/route-specific-pipeline.md + - 'Registering custom view helpers when using zend-view': v3/cookbook/using-custom-view-helpers.md + - 'Using zend-form view helpers': v3/cookbook/using-zend-form-view-helpers.md + - 'Using Expressive from a subdirectory': v3/cookbook/using-a-base-path.md + - 'Setting a locale based on a routing parameter': v3/cookbook/setting-locale-depending-routing-parameter.md + - 'Setting a locale without a routing parameter': v3/cookbook/setting-locale-without-routing-parameter.md + - 'Enabling debug toolbars': v3/cookbook/debug-toolbars.md + - 'Handling multiple routes in a single class': v3/cookbook/using-routed-middleware-class-as-controller.md + - 'Flash Messengers': v3/cookbook/flash-messengers.md + - 'Passing data between middleware': v3/cookbook/passing-data-between-middleware.md - Reference: + - "Why choose Expressive?": v3/why-expressive.md + - "CLI Tooling": v3/reference/cli-tooling.md + - Examples: v3/reference/usage-examples.md + - 'Expressive Projects': v3/reference/expressive-projects.md + - Migration: v3/reference/migration.md + - v2: + - v2/index.md + - 'Getting Started': + - 'Overview and Features': v2/getting-started/features.md + - 'Quick Start: Standalone': v2/getting-started/standalone.md + - 'Quick Start: Skeleton Installer': v2/getting-started/skeleton.md + - Features: + - "Middleware Types": v2/features/middleware-types.md + - Applications: v2/features/application.md + - Containers: + - Introduction: v2/features/container/intro.md + - 'Container Factories': v2/features/container/factories.md + - 'Delegator Factories': v2/features/container/delegator-factories.md + - 'Using zend-servicemanager': v2/features/container/zend-servicemanager.md + - 'Using Pimple': v2/features/container/pimple.md + - 'Using Aura.Di': v2/features/container/aura-di.md + - 'Routing Adapters': + - Introduction: v2/features/router/intro.md + - 'Routing Interface': v2/features/router/interface.md + - 'URI Generation': v2/features/router/uri-generation.md + - 'Routing vs Piping': v2/features/router/piping.md + - 'Using Aura': v2/features/router/aura.md + - 'Using FastRoute': v2/features/router/fast-route.md + - 'Using the ZF2 Router': v2/features/router/zf2.md + - Templating: + - Introduction: v2/features/template/intro.md + - 'Template Renderer Interface': v2/features/template/interface.md + - 'Templated Middleware': v2/features/template/middleware.md + - 'Using Plates': v2/features/template/plates.md + - 'Using Twig': v2/features/template/twig.md + - 'Using zend-view': v2/features/template/zend-view.md + - 'Error Handling': v2/features/error-handling.md + - 'Modular Applications': v2/features/modular-applications.md + - Middleware: + - 'Implicit HEAD and OPTIONS Middleware': v2/features/middleware/implicit-methods-middleware.md + - Helpers: + - Introduction: v2/features/helpers/intro.md + - UrlHelper: v2/features/helpers/url-helper.md + - ServerUrlHelper: v2/features/helpers/server-url-helper.md + - 'Body Parsing Middleware': v2/features/helpers/body-parse.md + - 'Content-Length Middleware': v2/features/helpers/content-length.md + - Emitters: v2/features/emitters.md + - Cookbook: + - 'Autowiring routes and pipeline middleware': v2/cookbook/autowiring-routes-and-pipelines.md + - 'Prepending a common path to all routes': v2/cookbook/common-prefix-for-routes.md + - 'Route-specific middleware pipelines': v2/cookbook/route-specific-pipeline.md + - 'Registering custom view helpers when using zend-view': v2/cookbook/using-custom-view-helpers.md + - 'Using zend-form view helpers': v2/cookbook/using-zend-form-view-helpers.md + - 'Using Expressive from a subdirectory': v2/cookbook/using-a-base-path.md + - 'Setting a locale based on a routing parameter': v2/cookbook/setting-locale-depending-routing-parameter.md + - 'Setting a locale without a routing parameter': v2/cookbook/setting-locale-without-routing-parameter.md + - 'Enabling debug toolbars': v2/cookbook/debug-toolbars.md + - 'Handling multiple routes in a single class': v2/cookbook/using-routed-middleware-class-as-controller.md + - 'Flash Messengers': v2/cookbook/flash-messengers.md + - 'Passing data between middleware': v2/cookbook/passing-data-between-middleware.md + - Reference: + - "Why choose Expressive?": v2/why-expressive.md + - "CLI Tooling": v2/reference/cli-tooling.md + - Examples: v2/reference/usage-examples.md + - 'Expressive Projects': v2/reference/expressive-projects.md + - Migration: v2/reference/migration.md - "Why choose Expressive?": v2/why-expressive.md - "CLI Tooling": v2/reference/cli-tooling.md - Examples: v2/reference/usage-examples.md diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 00000000..3b288ded --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,15 @@ +includes: + - vendor/phpstan/phpstan-strict-rules/rules.neon +parameters: + fileExtensions: + - php + reportUnmatchedIgnoredErrors: true + ignoreErrors: + # Virtual classes can be safely ignored + - '#Class Zend\\Expressive\\ApplicationPipeline not found#' + # @see src/Application.php and https://github.com/zendframework/zend-expressive/pull/564 + - '#Parameter \#1 \$path of function Zend\\Stratigility\\path expects string, array\|callable\|Psr\\Http\\Server\\MiddlewareInterface\|Psr\\Http\\Server\\RequestHandlerInterface\|string given.#' + # @see src/Container/WhoopsFactory.php (zend-expressive supports Whoops 1.x) + - '#Call to an undefined method Whoops\\Handler\\JsonResponseHandler::onlyForAjaxRequests\(\).#' + # @see src/MiddlewareFactory + - '#PHPDoc tag @param for parameter \$middleware with type array\|string\|Zend\\Stratigility\\MiddlewarePipe is not subtype of native type array\#' diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 1383783d..c01c6916 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -4,8 +4,16 @@ bootstrap="vendor/autoload.php" colors="true"> - + + ./test/Container/ResponseFactoryFactoryWithoutDiactorosTest.php + ./test/Container/ServerRequestFactoryFactoryWithoutDiactorosTest.php + ./test/Container/StreamFactoryFactoryWithoutDiactorosTest.php + + ./test + ./test/Container/ResponseFactoryFactoryWithoutDiactorosTest.php + ./test/Container/ServerRequestFactoryFactoryWithoutDiactorosTest.php + ./test/Container/StreamFactoryFactoryWithoutDiactorosTest.php diff --git a/src/AppFactory.php b/src/AppFactory.php deleted file mode 100644 index cfe185dc..00000000 --- a/src/AppFactory.php +++ /dev/null @@ -1,96 +0,0 @@ -push(new SapiEmitter()); - - return new Application($router, $container, null, $emitter); - } - - /** - * Do not allow instantiation. - * @codeCoverageIgnore - */ - private function __construct() - { - } -} diff --git a/src/Application.php b/src/Application.php index 6661b6fc..119b0942 100644 --- a/src/Application.php +++ b/src/Application.php @@ -1,516 +1,208 @@ router = $router; - $this->container = $container; - $this->defaultDelegate = $defaultDelegate; - $this->emitter = $emitter; - - $this->setResponsePrototype(new Response()); + $this->factory = $factory; + $this->pipeline = $pipeline; + $this->routes = $routes; + $this->runner = $runner; } /** - * @param string|Router\Route $path - * @param callable|string $middleware Middleware (or middleware service name) to associate with route. - * @param null|string $name The name of the route. - * @return Router\Route + * Proxies to composed pipeline to handle. + * {@inheritDocs} */ - public function get($path, $middleware, $name = null) + public function handle(ServerRequestInterface $request) : ResponseInterface { - return $this->route($path, $middleware, ['GET'], $name); + return $this->pipeline->handle($request); } /** - * @param string|Router\Route $path - * @param callable|string $middleware Middleware (or middleware service name) to associate with route. - * @param null|string $name The name of the route. - * @return Router\Route + * Proxies to composed pipeline to process. + * {@inheritDocs} */ - public function post($path, $middleware, $name = null) + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface { - return $this->route($path, $middleware, ['POST'], $name); + return $this->pipeline->process($request, $handler); } /** - * @param string|Router\Route $path - * @param callable|string $middleware Middleware (or middleware service name) to associate with route. - * @param null|string $name The name of the route. - * @return Router\Route - */ - public function put($path, $middleware, $name = null) - { - return $this->route($path, $middleware, ['PUT'], $name); - } - - /** - * @param string|Router\Route $path - * @param callable|string $middleware Middleware (or middleware service name) to associate with route. - * @param null|string $name The name of the route. - * @return Router\Route - */ - public function patch($path, $middleware, $name = null) - { - return $this->route($path, $middleware, ['PATCH'], $name); - } - - /** - * @param string|Router\Route $path - * @param callable|string $middleware Middleware (or middleware service name) to associate with route. - * @param null|string $name The name of the route. - * @return Router\Route - */ - public function delete($path, $middleware, $name = null) - { - return $this->route($path, $middleware, ['DELETE'], $name); - } - - /** - * @param string|Router\Route $path - * @param callable|string $middleware Middleware (or middleware service name) to associate with route. - * @param null|string $name The name of the route. - * @return Router\Route + * Run the application. + * + * Proxies to the RequestHandlerRunner::run() method. */ - public function any($path, $middleware, $name = null) + public function run() : void { - return $this->route($path, $middleware, null, $name); + $this->runner->run(); } /** - * Overload pipe() operation. - * - * Middleware piped may be either callables or service names. Middleware - * specified as services will be wrapped in a closure similar to the - * following: + * Pipe middleware to the pipeline. * - * - * 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); - * }; - * + * If two arguments are present, they are passed to pipe(), after first + * passing the second argument to the factory's prepare() method. * - * This is done to delay fetching the middleware until it is actually used; - * the upshot is that you will not be notified if the service is invalid to - * use as middleware until runtime. + * If only one argument is presented, it is passed to the factory prepare() + * method. * - * Middleware may also be passed as an array; each item in the array must - * resolve to middleware eventually (i.e., callable or service name). + * The resulting middleware, in both cases, is piped to the pipeline. * - * Finally, ensures that the route middleware is only ever registered - * once. - * - * @param string|array|callable $path Either a URI path prefix, or middleware. - * @param null|string|array|callable $middleware Middleware - * @return self + * @param string|array|callable|MiddlewareInterface|RequestHandlerInterface $middlewareOrPath + * Either the middleware to pipe, or the path to segregate the $middleware + * by, via a PathMiddlewareDecorator. + * @param null|string|array|callable|MiddlewareInterface|RequestHandlerInterface $middleware + * If present, middleware or request handler to segregate by the path + * specified in $middlewareOrPath. */ - public function pipe($path, $middleware = null) + public function pipe($middlewareOrPath, $middleware = null) : void { - if (null === $middleware) { - $middleware = $this->prepareMiddleware( - $path, - $this->router, - $this->responsePrototype, - $this->container - ); - $path = '/'; - } - - if (! is_callable($middleware) - && (is_string($middleware) || is_array($middleware)) - ) { - $middleware = $this->prepareMiddleware( - $middleware, - $this->router, - $this->responsePrototype, - $this->container - ); - } - - if ($middleware instanceof Router\Middleware\RouteMiddleware && $this->routeMiddlewareIsRegistered) { - return $this; - } + $middleware = $middleware ?: $middlewareOrPath; + $path = $middleware === $middlewareOrPath ? '/' : $middlewareOrPath; - if ($middleware instanceof Router\Middleware\DispatchMiddleware && $this->dispatchMiddlewareIsRegistered) { - return $this; - } + $middleware = $path !== '/' + ? path($path, $this->factory->prepare($middleware)) + : $this->factory->prepare($middleware); - if (! in_array($path, ['', '/'], true)) { - $middleware = path($path, $middleware); - } - - parent::pipe($middleware); - - if ($middleware instanceof Router\Middleware\RouteMiddleware) { - $this->routeMiddlewareIsRegistered = true; - } - - if ($middleware instanceof Router\Middleware\DispatchMiddleware) { - $this->dispatchMiddlewareIsRegistered = true; - } - - return $this; - } - - /** - * Register the routing middleware in the middleware pipeline. - * - * @deprecated since 2.2.0; to be removed in 3.0.0. Use pipe() with routing - * middleware or a service name resolving to routing middleware instead. - * @return void - */ - public function pipeRoutingMiddleware() - { - if ($this->routeMiddlewareIsRegistered) { - return; - } - $this->pipe(self::ROUTING_MIDDLEWARE); - } - - /** - * Register the dispatch middleware in the middleware pipeline. - * - * @deprecated since 2.2.0; to be removed in 3.0.0. Use pipe() with dispatch - * middleware or a service name resolving to dispatch middleware instead. - * @return void - */ - public function pipeDispatchMiddleware() - { - if ($this->dispatchMiddlewareIsRegistered) { - return; - } - $this->pipe(self::DISPATCH_MIDDLEWARE); + $this->pipeline->pipe($middleware); } /** * Add a route for the route middleware to match. * - * Accepts either a Router\Route instance, or a combination of a path and - * middleware, and optionally the HTTP methods allowed. - * - * On first invocation, pipes the route middleware to the middleware - * pipeline. - * - * @param string|Router\Route $path - * @param callable|string|array $middleware Middleware (or middleware service name) to associate with route. + * @param string|array|callable|MiddlewareInterface|RequestHandlerInterface $middleware + * Middleware or request handler (or service name resolving to one of + * those types) to associate with route. * @param null|array $methods HTTP method to accept; null indicates any. * @param null|string $name The name of the route. - * @return Router\Route - * @throws Exception\InvalidArgumentException if $path is not a Router\Route AND middleware is null. */ - public function route($path, $middleware = null, array $methods = null, $name = null) + public function route(string $path, $middleware, array $methods = null, string $name = null) : Router\Route { - if (! $path instanceof Router\Route && null === $middleware) { - throw new Exception\InvalidArgumentException(sprintf( - '%s expects either a route argument, or a combination of a path and middleware arguments', - __METHOD__ - )); - } - - if ($path instanceof Router\Route) { - $route = $path; - $path = $route->getPath(); - $methods = $route->getAllowedMethods(); - $name = $route->getName(); - } - - $this->checkForDuplicateRoute($path, $methods); - - if (! isset($route)) { - $methods = null === $methods ? Router\Route::HTTP_METHOD_ANY : $methods; - $middleware = $this->prepareMiddleware( - $middleware, - $this->router, - $this->responsePrototype, - $this->container - ); - $route = new Router\Route($path, $middleware, $methods, $name); - } - - $this->routes[] = $route; - $this->router->addRoute($route); - - return $route; + return $this->routes->route( + $path, + $this->factory->prepare($middleware), + $methods, + $name + ); } /** - * Retrieve all directly registered routes with the application. - * - * @return Router\Route[] + * @param string|array|callable|MiddlewareInterface|RequestHandlerInterface $middleware + * Middleware or request handler (or service name resolving to one of + * those types) to associate with route. + * @param null|string $name The name of the route. */ - public function getRoutes() + public function get(string $path, $middleware, string $name = null) : Router\Route { - return $this->routes; + return $this->route($path, $middleware, ['GET'], $name); } /** - * Run the application - * - * If no request or response are provided, the method will use - * ServerRequestFactory::fromGlobals to create a request instance, and - * instantiate a default response instance. - * - * It retrieves the default delegate using getDefaultDelegate(), and - * uses that to process itself. - * - * Once it has processed itself, it emits the returned response using the - * composed emitter. - * - * @param null|ServerRequestInterface $request - * @param null|ResponseInterface $response - * @return void + * @param string|array|callable|MiddlewareInterface|RequestHandlerInterface $middleware + * Middleware or request handler (or service name resolving to one of + * those types) to associate with route. + * @param null|string $name The name of the route. */ - public function run(ServerRequestInterface $request = null, ResponseInterface $response = null) + public function post(string $path, $middleware, $name = null) : Router\Route { - try { - $request = $request ?: ServerRequestFactory::fromGlobals(); - } catch (InvalidArgumentException $e) { - // Unable to parse uploaded files - $this->emitMarshalServerRequestException($e); - return; - } catch (UnexpectedValueException $e) { - // Invalid request method - $this->emitMarshalServerRequestException($e); - return; - } - - $response = $response ?: new Response(); - $request = $request->withAttribute('originalResponse', $response); - $delegate = $this->getDefaultDelegate(); - - $response = $this->process($request, $delegate); - - $emitter = $this->getEmitter(); - $emitter->emit($response); + return $this->route($path, $middleware, ['POST'], $name); } /** - * Retrieve the IoC container. - * - * If no IoC container is registered, we raise an exception. - * - * @deprecated since 2.2.0; to be removed in 3.0.0. This feature is - * replaced by Zend\Expressive\MiddlewareFactory in that release, which - * can be retrieved as a service from the application container. - * @return ContainerInterface - * @throws Exception\ContainerNotRegisteredException + * @param string|array|callable|MiddlewareInterface|RequestHandlerInterface $middleware + * Middleware or request handler (or service name resolving to one of + * those types) to associate with route. + * @param null|string $name The name of the route. */ - public function getContainer() + public function put(string $path, $middleware, string $name = null) : Router\Route { - if (null === $this->container) { - throw new Exception\ContainerNotRegisteredException(); - } - return $this->container; + return $this->route($path, $middleware, ['PUT'], $name); } /** - * Return the default delegate to use during `run()` if the stack is exhausted. - * - * If no default delegate is present, attempts the following: - * - * - If a container is composed, and it has the 'Zend\Expressive\Delegate\DefaultDelegate' - * service, pulls that service, assigns it, and returns it. - * - If no container is composed, creates an instance of Delegate\NotFoundDelegate - * using the current response prototype only (i.e., no templating). - * - * @deprecated since 2.2.0; to be removed in 3.0.0. This feature has no - * equivalent in that version. - * @return DelegateInterface + * @param string|array|callable|MiddlewareInterface|RequestHandlerInterface $middleware + * Middleware or request handler (or service name resolving to one of + * those types) to associate with route. + * @param null|string $name The name of the route. */ - public function getDefaultDelegate() + public function patch(string $path, $middleware, string $name = null) : Router\Route { - if ($this->defaultDelegate) { - return $this->defaultDelegate; - } - - if ($this->container && $this->container->has('Zend\Expressive\Delegate\DefaultDelegate')) { - $this->defaultDelegate = $this->container->get('Zend\Expressive\Delegate\DefaultDelegate'); - return $this->defaultDelegate; - } - - if ($this->container) { - $factory = new Container\NotFoundDelegateFactory(); - $this->defaultDelegate = $factory($this->container); - return $this->defaultDelegate; - } - - $this->defaultDelegate = new Delegate\NotFoundDelegate($this->responsePrototype); - return $this->defaultDelegate; + return $this->route($path, $middleware, ['PATCH'], $name); } /** - * Retrieve an emitter to use during run(). - * - * If none was registered during instantiation, this will lazy-load an - * EmitterStack composing an SapiEmitter instance. - * - * @deprecated since 2.2.0; to be removed in 3.0.0. This feature has no - * equivalent in that version; the responsibility has been moved to a - * new collaborator. - * @return EmitterInterface + * @param string|array|callable|MiddlewareInterface|RequestHandlerInterface $middleware + * Middleware or request handler (or service name resolving to one of + * those types) to associate with route. + * @param null|string $name The name of the route. */ - public function getEmitter() + public function delete(string $path, $middleware, string $name = null) : Router\Route { - if (! $this->emitter) { - $this->emitter = new Emitter\EmitterStack(); - $this->emitter->push(new SapiEmitter()); - } - return $this->emitter; + return $this->route($path, $middleware, ['DELETE'], $name); } /** - * Determine if the route is duplicated in the current list. - * - * Checks if a route with the same name or path exists already in the list; - * if so, and it responds to any of the $methods indicated, raises - * a DuplicateRouteException indicating a duplicate route. - * - * @param string $path - * @param null|array $methods - * @throws Exception\DuplicateRouteException on duplicate route detection. + * @param string|array|callable|MiddlewareInterface|RequestHandlerInterface $middleware + * Middleware or request handler (or service name resolving to one of + * those types) to associate with route. + * @param null|string $name The name of the route. */ - private function checkForDuplicateRoute($path, $methods = null) + public function any(string $path, $middleware, string $name = null) : Router\Route { - if (null === $methods) { - $methods = Router\Route::HTTP_METHOD_ANY; - } - - $matches = array_filter($this->routes, function (Router\Route $route) use ($path, $methods) { - if ($path !== $route->getPath()) { - return false; - } - - if ($methods === Router\Route::HTTP_METHOD_ANY) { - return true; - } - - return array_reduce($methods, function ($carry, $method) use ($route) { - return ($carry || $route->allowsMethod($method)); - }, false); - }); - - if (! empty($matches)) { - throw new Exception\DuplicateRouteException(sprintf( - 'Duplicate route detected; same name or path ("%s"),' - . ' and one or more HTTP methods intersect (%s)', - $path, - is_array($methods) ? implode(', ', $methods) : '*' - )); - } + return $this->route($path, $middleware, null, $name); } /** - * @param \Exception|\Throwable $exception - * @return void + * Retrieve all directly registered routes with the application. + * + * @return Router\Route[] */ - private function emitMarshalServerRequestException($exception) + public function getRoutes() : array { - if ($this->container && $this->container->has(Middleware\ErrorResponseGenerator::class)) { - $generator = $this->container->get(Middleware\ErrorResponseGenerator::class); - $response = $generator($exception, new ServerRequest(), $this->responsePrototype); - } else { - $response = $this->responsePrototype - ->withStatus(StatusCode::STATUS_BAD_REQUEST); - } - - $emitter = $this->getEmitter(); - $emitter->emit($response); + return $this->routes->getRoutes(); } } diff --git a/src/ApplicationConfigInjectionTrait.php b/src/ApplicationConfigInjectionTrait.php deleted file mode 100644 index 7be1b540..00000000 --- a/src/ApplicationConfigInjectionTrait.php +++ /dev/null @@ -1,61 +0,0 @@ -container || ! $this->container->has('config')) - ) { - return; - } - - ApplicationConfigInjectionDelegator::injectPipelineFromConfig( - $this, - is_array($config) ? $config : $this->container->get('config') - ); - } - - /** - * Inject routes from configuration. - * - * Proxies to ApplicationConfigInjectionDelegator::injectRoutesFromConfig - * - * @param null|array $config If null, attempts to pull the 'config' service - * from the composed container. - * @return void - * @throws Exception\InvalidArgumentException - */ - public function injectRoutesFromConfig(array $config = null) - { - if (! is_array($config) - && (! $this->container || ! $this->container->has('config')) - ) { - return; - } - - ApplicationConfigInjectionDelegator::injectRoutesFromConfig( - $this, - is_array($config) ? $config : $this->container->get('config') - ); - } -} diff --git a/src/ConfigProvider.php b/src/ConfigProvider.php index 015b9daa..829a684d 100644 --- a/src/ConfigProvider.php +++ b/src/ConfigProvider.php @@ -1,14 +1,19 @@ $this->getDependencies(), ]; } - /** - * @return array - */ - public function getDependencies() + public function getDependencies() : array { // @codingStandardsIgnoreStart return [ 'aliases' => [ - Delegate\NotFoundDelegate::class => Handler\NotFoundHandler::class, - Middleware\DispatchMiddleware::class => Router\Middleware\DispatchMiddleware::class, - Middleware\ImplicitHeadMiddleware::class => Router\Middleware\ImplicitHeadMiddleware::class, - Middleware\ImplicitOptionsMiddleware::class => Router\Middleware\ImplicitOptionsMiddleware::class, - Middleware\RouteMiddleware::class => Router\Middleware\RouteMiddleware::class, - 'Zend\Expressive\Delegate\DefaultDelegate' => Handler\NotFoundHandler::class, - ], - 'invokables' => [ - Router\Middleware\DispatchMiddleware::class => Router\Middleware\DispatchMiddleware::class, + DEFAULT_DELEGATE => Handler\NotFoundHandler::class, + DISPATCH_MIDDLEWARE => Router\Middleware\DispatchMiddleware::class, + IMPLICIT_HEAD_MIDDLEWARE => Router\Middleware\ImplicitHeadMiddleware::class, + IMPLICIT_OPTIONS_MIDDLEWARE => Router\Middleware\ImplicitOptionsMiddleware::class, + NOT_FOUND_MIDDLEWARE => Handler\NotFoundHandler::class, + ROUTE_MIDDLEWARE => Router\Middleware\PathBasedRoutingMiddleware::class, ], 'factories' => [ Application::class => Container\ApplicationFactory::class, + ApplicationPipeline::class => Container\ApplicationPipelineFactory::class, + EmitterInterface::class => Container\EmitterFactory::class, ErrorHandler::class => Container\ErrorHandlerFactory::class, - Handler\NotFoundHandler::class => Container\NotFoundDelegateFactory::class, + Handler\NotFoundHandler::class => Container\NotFoundHandlerFactory::class, + MiddlewareContainer::class => Container\MiddlewareContainerFactory::class, + MiddlewareFactory::class => Container\MiddlewareFactoryFactory::class, // Change the following in development to the WhoopsErrorResponseGeneratorFactory: Middleware\ErrorResponseGenerator::class => Container\ErrorResponseGeneratorFactory::class, - Middleware\NotFoundHandler::class => Container\NotFoundHandlerFactory::class, + RequestHandlerRunner::class => Container\RequestHandlerRunnerFactory::class, ResponseInterface::class => Container\ResponseFactoryFactory::class, + Response\ServerRequestErrorResponseGenerator::class => Container\ServerRequestErrorResponseGeneratorFactory::class, + ServerRequestInterface::class => Container\ServerRequestFactoryFactory::class, StreamInterface::class => Container\StreamFactoryFactory::class, - - // These are duplicates, in case the zend-expressive-router package ConfigProvider is not wired: - Router\Middleware\ImplicitHeadMiddleware::class => Router\Middleware\ImplicitHeadMiddlewareFactory::class, - Router\Middleware\ImplicitOptionsMiddleware::class => Router\Middleware\ImplicitOptionsMiddlewareFactory::class, - Router\Middleware\RouteMiddleware::class => Router\Middleware\RouteMiddlewareFactory::class, ], ]; // @codingStandardsIgnoreEnd diff --git a/src/Container/ApplicationConfigInjectionDelegator.php b/src/Container/ApplicationConfigInjectionDelegator.php index e90b1685..ec30e06c 100644 --- a/src/Container/ApplicationConfigInjectionDelegator.php +++ b/src/Container/ApplicationConfigInjectionDelegator.php @@ -1,17 +1,22 @@ [ * // An array of middleware to register with the pipeline. * // entries to register prior to routing/dispatching... - * // - entry for \Zend\Expressive\Router\Middleware\RouteMiddleware::class + * // - entry for \Zend\Expressive\Router\Middleware\PathBasedRoutingMiddleware::class + * // - entry for \Zend\Expressive\Router\Middleware\MethodNotAllowedMiddleware::class * // - entry for \Zend\Expressive\Router\Middleware\DispatchMiddleware::class * // entries to register after routing/dispatching... * ], @@ -102,10 +106,8 @@ public function __invoke(ContainerInterface $container, $serviceName, callable $ * the `middleware` value of a specification. Internally, this will create * a `Zend\Stratigility\MiddlewarePipe` instance, with the middleware * specified piped in the order provided. - * - * @return void */ - public static function injectPipelineFromConfig(Application $application, array $config) + public static function injectPipelineFromConfig(Application $application, array $config) : void { if (empty($config['middleware_pipeline'])) { return; @@ -119,7 +121,7 @@ public static function injectPipelineFromConfig(Application $application, array ); foreach ($queue as $spec) { - $path = isset($spec['path']) ? $spec['path'] : '/'; + $path = $spec['path'] ?? '/'; $application->pipe($path, $spec['middleware']); } } @@ -127,43 +129,11 @@ public static function injectPipelineFromConfig(Application $application, array /** * Inject routes from configuration. * - * Introspects the provided configuration for routes to inject in the - * application instance. - * - * The following configuration structure can be used to define routes: - * - * - * return [ - * 'routes' => [ - * [ - * 'path' => '/path/to/match', - * 'middleware' => 'Middleware Service Name or Callable', - * 'allowed_methods' => ['GET', 'POST', 'PATCH'], - * 'options' => [ - * 'stuff' => 'to', - * 'pass' => 'to', - * 'the' => 'underlying router', - * ], - * ], - * // etc. - * ], - * ]; - * - * - * Each route MUST have a path and middleware key at the minimum. - * - * The "allowed_methods" key may be omitted, can be either an array or the - * value of the Zend\Expressive\Router\Route::HTTP_METHOD_ANY constant; any - * valid HTTP method token is allowed, which means you can specify custom HTTP - * methods as well. - * - * The "options" key may also be omitted, and its interpretation will be - * dependent on the underlying router used. + * Proxies to ApplicationConfigInjectionDelegator::injectRoutesFromConfig * - * @return void * @throws InvalidArgumentException */ - public static function injectRoutesFromConfig(Application $application, array $config) + public static function injectRoutesFromConfig(Application $application, array $config) : void { if (! isset($config['routes']) || ! is_array($config['routes'])) { return; @@ -185,7 +155,7 @@ public static function injectRoutesFromConfig(Application $application, array $c } } - $name = isset($spec['name']) ? $spec['name'] : null; + $name = $spec['name'] ?? null; $route = $application->route( $spec['path'], $spec['middleware'], @@ -219,21 +189,11 @@ public static function injectRoutesFromConfig(Application $application, array $c * If the 'middleware' value is missing, or not viable as middleware, it * raises an exception, to ensure the pipeline is built correctly. * - * @return callable * @throws InvalidArgumentException */ - private static function createCollectionMapper() + private static function createCollectionMapper() : callable { - $appMiddleware = [ - Application::ROUTING_MIDDLEWARE, - Application::DISPATCH_MIDDLEWARE, - ]; - - return function ($item) use ($appMiddleware) { - if (in_array($item, $appMiddleware, true)) { - return ['middleware' => $item]; - } - + return function ($item) { if (! is_array($item) || ! array_key_exists('middleware', $item)) { throw new InvalidArgumentException(sprintf( 'Invalid pipeline specification received; must be an array' @@ -257,10 +217,8 @@ private static function createCollectionMapper() * * The function is useful to reduce an array of pipeline middleware to a * priority queue. - * - * @return callable */ - private static function createPriorityQueueReducer() + private static function createPriorityQueueReducer() : callable { // $serial is used to ensure that items of the same priority are enqueued // in the order in which they are inserted. diff --git a/src/Container/ApplicationFactory.php b/src/Container/ApplicationFactory.php index 1849b7b4..e9f21b6f 100644 --- a/src/Container/ApplicationFactory.php +++ b/src/Container/ApplicationFactory.php @@ -1,176 +1,42 @@ has('config') ? $container->get('config') : []; - $config = $config instanceof ArrayObject ? $config->getArrayCopy() : $config; - - $router = $container->has(RouterInterface::class) - ? $container->get(RouterInterface::class) - : new FastRouteRouter(); - - $delegate = $container->has('Zend\Expressive\Delegate\DefaultDelegate') - ? $container->get('Zend\Expressive\Delegate\DefaultDelegate') - : null; - - $emitter = $container->has(EmitterInterface::class) - ? $container->get(EmitterInterface::class) - : null; - - $app = new Application($router, $container, $delegate, $emitter); - - if (empty($config['zend-expressive']['programmatic_pipeline'])) { - $this->injectRoutesAndPipeline($container, $router, $app, $config); - } - - return $app; - } - - /** - * Injects routes and the middleware pipeline into the application. - * - * @return void - */ - private function injectRoutesAndPipeline( - ContainerInterface $container, - RouterInterface $router, - Application $app, - array $config - ) { - if (empty($config['middleware_pipeline']) - && (! isset($config['routes']) || ! is_array($config['routes'])) - ) { - return; - } - - if (empty($config['middleware_pipeline']) && isset($config['routes']) && is_array($config['routes'])) { - $app->pipe($this->getRoutingMiddleware($container, $router, $app)); - $app->pipe($this->getDispatchMiddleware($container, $app)); - } - - ApplicationConfigInjectionDelegator::injectRoutesFromConfig($app, $config); - ApplicationConfigInjectionDelegator::injectPipelineFromConfig($app, $config); - } - - /** - * Discovers or creates the route middleware. - * - * If the RouteMiddleware is present in the container, it returns the - * service. - * - * Otherwise, it creates RouteMiddleware using the router being composed in - * the application, along with a response prototype. - * - * @return MiddlewareInterface - */ - private function getRoutingMiddleware(ContainerInterface $container, RouterInterface $router, Application $app) - { - if ($container->has(RouteMiddleware::class)) { - return $container->get(RouteMiddleware::class); - } - - return new RouteMiddleware( - $router, - $this->getResponsePrototype($container, $app) + return new Application( + $container->get(MiddlewareFactory::class), + $container->get(ApplicationPipeline::class), + $container->get(PathBasedRoutingMiddleware::class), + $container->get(RequestHandlerRunner::class) ); } - - /** - * Discover or create the dispatch middleware. - * - * If the DispatchMiddleware is present in the application's container, it - * returns the service. Otherwise, instantiates and returns it directly. - * - * @return MiddlewareInterface - */ - private function getDispatchMiddleware(ContainerInterface $container, Application $app) - { - return $container->has(DispatchMiddleware::class) - ? $container->get(DispatchMiddleware::class) - : new DispatchMiddleware(); - } - - /** - * Get the response prototype. - * - * If not available in the container, uses reflection to pull it from the - * application. - * - * If in the container, fetches it. If the value is callable, uses it as - * a factory to generate and return the response. - * - * @return ResponseInterface - */ - private function getResponsePrototype(ContainerInterface $container, Application $app) - { - if (! $container->has(ResponseInterface::class)) { - $r = new ReflectionProperty($app, 'responsePrototype'); - $r->setAccessible(true); - return $r->getValue($app); - } - - $response = $container->get(ResponseInterface::class); - return is_callable($response) ? $response() : $response; - } } diff --git a/src/Container/ApplicationPipelineFactory.php b/src/Container/ApplicationPipelineFactory.php new file mode 100644 index 00000000..5cd9772a --- /dev/null +++ b/src/Container/ApplicationPipelineFactory.php @@ -0,0 +1,22 @@ +push(new SapiEmitter()); + return $stack; + } +} diff --git a/src/Container/ErrorHandlerFactory.php b/src/Container/ErrorHandlerFactory.php index bedfb94f..73b2470d 100644 --- a/src/Container/ErrorHandlerFactory.php +++ b/src/Container/ErrorHandlerFactory.php @@ -1,29 +1,27 @@ has(ErrorResponseGenerator::class) ? $container->get(ErrorResponseGenerator::class) : null; - return new ErrorHandler(new Response(), $generator); + return new ErrorHandler($container->get(ResponseInterface::class), $generator); } } diff --git a/src/Container/ErrorResponseGeneratorFactory.php b/src/Container/ErrorResponseGeneratorFactory.php index e3008724..8ac93e49 100644 --- a/src/Container/ErrorResponseGeneratorFactory.php +++ b/src/Container/ErrorResponseGeneratorFactory.php @@ -1,10 +1,12 @@ has('config') ? $container->get('config') : []; - $debug = isset($config['debug']) ? $config['debug'] : false; + $debug = $config['debug'] ?? false; - $template = isset($config['zend-expressive']['error_handler']['template_error']) - ? $config['zend-expressive']['error_handler']['template_error'] - : ErrorResponseGenerator::TEMPLATE_DEFAULT; + $template = $config['zend-expressive']['error_handler']['template_error'] + ?? ErrorResponseGenerator::TEMPLATE_DEFAULT; $renderer = $container->has(TemplateRendererInterface::class) ? $container->get(TemplateRendererInterface::class) diff --git a/src/Container/Exception/ExceptionInterface.php b/src/Container/Exception/ExceptionInterface.php index 6a853c12..5527afc5 100644 --- a/src/Container/Exception/ExceptionInterface.php +++ b/src/Container/Exception/ExceptionInterface.php @@ -1,10 +1,12 @@ get(MiddlewareContainer::class) + ); + } +} diff --git a/src/Container/NotFoundDelegateFactory.php b/src/Container/NotFoundDelegateFactory.php deleted file mode 100644 index df0cc58f..00000000 --- a/src/Container/NotFoundDelegateFactory.php +++ /dev/null @@ -1,41 +0,0 @@ -has('config') ? $container->get('config') : []; - $renderer = $container->has(TemplateRendererInterface::class) - ? $container->get(TemplateRendererInterface::class) - : null; - $template = isset($config['zend-expressive']['error_handler']['template_404']) - ? $config['zend-expressive']['error_handler']['template_404'] - : NotFoundDelegate::TEMPLATE_DEFAULT; - $layout = isset($config['zend-expressive']['error_handler']['layout']) - ? $config['zend-expressive']['error_handler']['layout'] - : NotFoundDelegate::LAYOUT_DEFAULT; - - return new NotFoundDelegate(new Response(), $renderer, $template, $layout); - } -} diff --git a/src/Container/NotFoundHandlerFactory.php b/src/Container/NotFoundHandlerFactory.php index b84b921c..83704d7f 100644 --- a/src/Container/NotFoundHandlerFactory.php +++ b/src/Container/NotFoundHandlerFactory.php @@ -1,24 +1,37 @@ get(NotFoundDelegate::class)); + $config = $container->has('config') ? $container->get('config') : []; + $renderer = $container->has(TemplateRendererInterface::class) + ? $container->get(TemplateRendererInterface::class) + : null; + $template = $config['zend-expressive']['error_handler']['template_404'] + ?? NotFoundHandler::TEMPLATE_DEFAULT; + $layout = $config['zend-expressive']['error_handler']['layout'] + ?? NotFoundHandler::LAYOUT_DEFAULT; + + return new NotFoundHandler( + $container->get(ResponseInterface::class), + $renderer, + $template, + $layout + ); } } diff --git a/src/Container/RequestHandlerRunnerFactory.php b/src/Container/RequestHandlerRunnerFactory.php new file mode 100644 index 00000000..32c3819c --- /dev/null +++ b/src/Container/RequestHandlerRunnerFactory.php @@ -0,0 +1,47 @@ +get(ApplicationPipeline::class), + $container->get(EmitterInterface::class), + $container->get(ServerRequestInterface::class), + $container->get(ServerRequestErrorResponseGenerator::class) + ); + } +} diff --git a/src/Container/ResponseFactoryFactory.php b/src/Container/ResponseFactoryFactory.php index f9ef4e8a..1fa30db0 100644 --- a/src/Container/ResponseFactoryFactory.php +++ b/src/Container/ResponseFactoryFactory.php @@ -1,10 +1,12 @@ has('config') ? $container->get('config') : []; + $debug = $config['debug'] ?? false; + + $renderer = $container->has(TemplateRendererInterface::class) + ? $container->get(TemplateRendererInterface::class) + : null; + + $template = $config['zend-expressive']['error_handler']['template_error'] + ?? ServerRequestErrorResponseGenerator::TEMPLATE_DEFAULT; + + return new ServerRequestErrorResponseGenerator( + $container->get(ResponseInterface::class), + $debug, + $renderer, + $template + ); + } +} diff --git a/src/Container/ServerRequestFactoryFactory.php b/src/Container/ServerRequestFactoryFactory.php new file mode 100644 index 00000000..46b04356 --- /dev/null +++ b/src/Container/ServerRequestFactoryFactory.php @@ -0,0 +1,45 @@ +get('Zend\Expressive\Whoops') diff --git a/src/Container/WhoopsFactory.php b/src/Container/WhoopsFactory.php index 8ac66336..b79cdc98 100644 --- a/src/Container/WhoopsFactory.php +++ b/src/Container/WhoopsFactory.php @@ -1,10 +1,12 @@ has('config') ? $container->get('config') : []; - $config = isset($config['whoops']) ? $config['whoops'] : []; + $config = $config['whoops'] ?? []; $whoops = new Whoops(); $whoops->writeToOutput(false); @@ -67,7 +66,7 @@ public function __invoke(ContainerInterface $container) * @param array|\ArrayAccess $config * @return void */ - private function registerJsonHandler(Whoops $whoops, $config) + private function registerJsonHandler(Whoops $whoops, $config) : void { if (empty($config['json_exceptions']['display'])) { return; diff --git a/src/Container/WhoopsPageHandlerFactory.php b/src/Container/WhoopsPageHandlerFactory.php index f962c833..79140ce7 100644 --- a/src/Container/WhoopsPageHandlerFactory.php +++ b/src/Container/WhoopsPageHandlerFactory.php @@ -1,10 +1,12 @@ has('config') ? $container->get('config') : []; - $config = isset($config['whoops']) ? $config['whoops'] : []; + $config = $config['whoops'] ?? []; $pageHandler = new PrettyPageHandler(); @@ -51,13 +49,10 @@ public function __invoke(ContainerInterface $container) * Inject an editor into the whoops configuration. * * @see https://github.com/filp/whoops/blob/master/docs/Open%20Files%20In%20An%20Editor.md - * @param PrettyPageHandler $handler * @param array|\ArrayAccess $config - * @param ContainerInterface $container - * @return void * @throws Exception\InvalidServiceException for an invalid editor definition. */ - private function injectEditor(PrettyPageHandler $handler, $config, ContainerInterface $container) + private function injectEditor(PrettyPageHandler $handler, $config, ContainerInterface $container) : void { if (! isset($config['editor'])) { return; diff --git a/src/Delegate/NotFoundDelegate.php b/src/Delegate/NotFoundDelegate.php deleted file mode 100644 index 1b1cbf33..00000000 --- a/src/Delegate/NotFoundDelegate.php +++ /dev/null @@ -1,18 +0,0 @@ -emit($response)) { - return null; - } - } - - return false; - } - - /** - * Set an emitter on the stack by index. - * - * @param mixed $index - * @param EmitterInterface $emitter - * @return void - * @throws InvalidArgumentException if not an EmitterInterface instance - */ - public function offsetSet($index, $emitter) - { - $this->validateEmitter($emitter); - parent::offsetSet($index, $emitter); - } - - /** - * Push an emitter to the stack. - * - * @param EmitterInterface $emitter - * @return void - * @throws InvalidArgumentException if not an EmitterInterface instance - */ - public function push($emitter) - { - $this->validateEmitter($emitter); - parent::push($emitter); - } - - /** - * Unshift an emitter to the stack. - * - * @param EmitterInterface $emitter - * @return void - * @throws InvalidArgumentException if not an EmitterInterface instance - */ - public function unshift($emitter) - { - $this->validateEmitter($emitter); - parent::unshift($emitter); - } - - /** - * Validate that an emitter implements EmitterInterface. - * - * @param mixed $emitter - * @return void - * @throws InvalidArgumentException for non-emitter instances - */ - private function validateEmitter($emitter) - { - if (! $emitter instanceof EmitterInterface) { - throw new Exception\InvalidArgumentException(sprintf( - '%s expects an EmitterInterface implementation', - __CLASS__ - )); - } - } -} diff --git a/src/Exception/BadMethodCallException.php b/src/Exception/BadMethodCallException.php index 66f5fadf..fa26caa1 100644 --- a/src/Exception/BadMethodCallException.php +++ b/src/Exception/BadMethodCallException.php @@ -1,10 +1,12 @@ responsePrototype = $responsePrototype; + // Factory cast to closure in order to provide return type safety. + $this->responseFactory = function () use ($responseFactory) : ResponseInterface { + return $responseFactory(); + }; $this->renderer = $renderer; $this->template = $template; $this->layout = $layout; @@ -59,33 +58,30 @@ public function __construct( /** * Creates and returns a 404 response. * - * @param ServerRequestInterface $request - * @return ResponseInterface + * @param ServerRequestInterface $request Passed to internal handler */ - public function process(ServerRequestInterface $request) + public function handle(ServerRequestInterface $request) : ResponseInterface { - if (! $this->renderer) { + if ($this->renderer === null) { return $this->generatePlainTextResponse($request); } - return $this->generateTemplatedResponse($request); + return $this->generateTemplatedResponse($this->renderer, $request); } /** * Generates a plain text response indicating the request method and URI. - * - * @param ServerRequestInterface $request - * @return ResponseInterface */ - private function generatePlainTextResponse(ServerRequestInterface $request) + private function generatePlainTextResponse(ServerRequestInterface $request) : ResponseInterface { - $response = $this->responsePrototype->withStatus(StatusCodeInterface::STATUS_NOT_FOUND); + $response = ($this->responseFactory)()->withStatus(StatusCodeInterface::STATUS_NOT_FOUND); $response->getBody() ->write(sprintf( 'Cannot %s %s', $request->getMethod(), (string) $request->getUri() )); + return $response; } @@ -93,15 +89,15 @@ private function generatePlainTextResponse(ServerRequestInterface $request) * Generates a response using a template. * * Template will receive the current request via the "request" variable. - * - * @param ServerRequestInterface $request - * @return ResponseInterface */ - private function generateTemplatedResponse(ServerRequestInterface $request) - { - $response = $this->responsePrototype->withStatus(StatusCodeInterface::STATUS_NOT_FOUND); + private function generateTemplatedResponse( + TemplateRendererInterface $renderer, + ServerRequestInterface $request + ) : ResponseInterface { + + $response = ($this->responseFactory)()->withStatus(StatusCodeInterface::STATUS_NOT_FOUND); $response->getBody()->write( - $this->renderer->render($this->template, ['request' => $request, 'layout' => $this->layout]) + $renderer->render($this->template, ['request' => $request, 'layout' => $this->layout]) ); return $response; diff --git a/src/IsCallableInteropMiddlewareTrait.php b/src/IsCallableInteropMiddlewareTrait.php deleted file mode 100644 index a99a3e30..00000000 --- a/src/IsCallableInteropMiddlewareTrait.php +++ /dev/null @@ -1,90 +0,0 @@ -isCallable($middleware)) { - return false; - } - - $r = $this->reflectMiddleware($middleware); - $paramsCount = $r->getNumberOfParameters(); - - return $paramsCount === 2; - } - - /** - * Reflect a callable middleware. - * - * Duplicates MiddlewarePipe::getReflectionFunction, but that method is not - * callable due to private visibility. - * - * @param callable $middleware - * @return \ReflectionFunctionAbstract - */ - private function reflectMiddleware(callable $middleware) - { - if (is_array($middleware)) { - $class = array_shift($middleware); - $method = array_shift($middleware); - return new ReflectionMethod($class, $method); - } - - if ($middleware instanceof Closure || ! is_object($middleware)) { - return new ReflectionFunction($middleware); - } - - return new ReflectionMethod($middleware, '__invoke'); - } -} diff --git a/src/MarshalMiddlewareTrait.php b/src/MarshalMiddlewareTrait.php deleted file mode 100644 index 8b3fd1b1..00000000 --- a/src/MarshalMiddlewareTrait.php +++ /dev/null @@ -1,230 +0,0 @@ -triggerLegacyMiddlewareDeprecation($middleware); - return $container && $container->has(RouteMiddleware::class) - ? $container->get(RouteMiddleware::class) - : new RouteMiddleware($router, $responsePrototype); - } - - if ($middleware === Application::DISPATCH_MIDDLEWARE) { - $this->triggerLegacyMiddlewareDeprecation($middleware); - return $container && $container->has(DispatchMiddleware::class) - ? $container->get(DispatchMiddleware::class) - : new DispatchMiddleware(); - } - - if ($middleware instanceof MiddlewareInterface) { - return $middleware; - } - - if ($this->isCallableInteropMiddleware($middleware)) { - return middleware($middleware); - } - - if ($this->isCallable($middleware)) { - $this->triggerDoublePassMiddlewareDeprecation($middleware); - return doublePassMiddleware($middleware, $responsePrototype); - } - - if (is_array($middleware)) { - return $this->marshalMiddlewarePipe($middleware, $router, $responsePrototype, $container); - } - - if (is_string($middleware) && $container && $container->has($middleware)) { - return new Middleware\LazyLoadingMiddleware($container, $responsePrototype, $middleware); - } - - if (is_string($middleware)) { - return $this->marshalInvokableMiddleware($middleware, $responsePrototype); - } - - throw new Exception\InvalidMiddlewareException(sprintf( - 'Unable to resolve middleware "%s" to a callable or MiddlewareInterface implementation', - is_object($middleware) ? get_class($middleware) . '[Object]' : gettype($middleware) . '[Scalar]' - )); - } - - /** - * 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 Router\RouterInterface $router - * @param ResponseInterface $responsePrototype - * @param null|ContainerInterface $container - * @return MiddlewarePipe - * @throws Exception\InvalidMiddlewareException for any invalid middleware items. - */ - private function marshalMiddlewarePipe( - array $middlewares, - Router\RouterInterface $router, - ResponseInterface $responsePrototype, - ContainerInterface $container = null - ) { - $middlewarePipe = new MiddlewarePipe(); - $middlewarePipe->setResponsePrototype($responsePrototype); - - foreach ($middlewares as $middleware) { - $middlewarePipe->pipe( - $this->prepareMiddleware($middleware, $router, $responsePrototype, $container) - ); - } - - return $middlewarePipe; - } - - /** - * Attempt to instantiate the given middleware. - * - * @param string $middleware - * @param ResponseInterface $responsePrototype - * @return ServerMiddlewareInterface - * @throws Exception\InvalidMiddlewareException if $middleware is not a class. - * @throws Exception\InvalidMiddlewareException if $middleware does not resolve - * to either an invokable class or ServerMiddlewareInterface instance. - */ - private function marshalInvokableMiddleware($middleware, ResponseInterface $responsePrototype) - { - if (! class_exists($middleware)) { - throw new Exception\InvalidMiddlewareException(sprintf( - 'Unable to create middleware "%s"; not a valid class or service name', - $middleware - )); - } - - $instance = new $middleware(); - - if ($instance instanceof MiddlewareInterface) { - return $instance; - } - - if ($this->isCallableInteropMiddleware($instance)) { - return middleware($instance); - } - - if (! is_callable($instance)) { - throw new Exception\InvalidMiddlewareException(sprintf( - 'Middleware of class "%s" is invalid; neither invokable nor a MiddlewareInterface instance', - $middleware - )); - } - - $this->triggerDoublePassMiddlewareDeprecation($instance); - return doublePassMiddleware($instance, $responsePrototype); - } - - /** - * @param string $middlewareType - * @return void - */ - private function triggerLegacyMiddlewareDeprecation($middlewareType) - { - switch ($middlewareType) { - case (Application::ROUTING_MIDDLEWARE): - $constant = sprintf('%s::ROUTING_MIDDLEWARE', Application::class); - $type = 'routing'; - $useInstead = RouteMiddleware::class; - break; - case (Application::DISPATCH_MIDDLEWARE): - $constant = sprintf('%s::DISPATCH_MIDDLEWARE', Application::class); - $type = 'dispatch'; - $useInstead = DispatchMiddleware::class; - break; - } - - trigger_error(sprintf( - 'Usage of the %s constant for specifying %s middleware is deprecated;' - . ' pipe() the middleware directly, or reference it by its service name "%s"', - $constant, - $type, - $useInstead - ), E_USER_DEPRECATED); - } - - /** - * @param callable $middleware - * @return void - */ - private function triggerDoublePassMiddlewareDeprecation(callable $middleware) - { - if (is_object($middleware)) { - $type = get_class($middleware); - } elseif (is_string($middleware)) { - $type = 'callable:' . $middleware; - } else { - $type = 'callable'; - } - - trigger_error(sprintf( - 'Detected double-pass middleware (%s).' - . ' Usage of callable double-pass middleware is deprecated. Before piping or routing' - . ' such middleware, pass it to Zend\Stratigility\doublePassMiddleware(), along with' - . ' a PSR-7 response instance.', - $type - ), E_USER_DEPRECATED); - } -} diff --git a/src/Middleware/DispatchMiddleware.php b/src/Middleware/DispatchMiddleware.php deleted file mode 100644 index 7ca87eb6..00000000 --- a/src/Middleware/DispatchMiddleware.php +++ /dev/null @@ -1,30 +0,0 @@ -debug = (bool) $isDevelopmentMode; + $this->debug = $isDevelopmentMode; $this->renderer = $renderer; $this->template = $template; } - /** - * @param \Throwable|\Exception $e - * @param ServerRequestInterface $request - * @param ResponseInterface $response - * @return ResponseInterface - */ - public function __invoke($e, ServerRequestInterface $request, ResponseInterface $response) - { + public function __invoke( + Throwable $e, + ServerRequestInterface $request, + ResponseInterface $response + ) : ResponseInterface { $response = $response->withStatus(Utils::getStatusCode($e, $response)); if ($this->renderer) { - return $this->prepareTemplatedResponse($e, $request, $response); - } - - return $this->prepareDefaultResponse($e, $response); - } - - /** - * @param \Throwable|\Exception $e - * @param ServerRequestInterface $request - * @param ResponseInterface $response - * @return ResponseInterface - */ - private function prepareTemplatedResponse($e, ServerRequestInterface $request, ResponseInterface $response) - { - $templateData = [ - 'response' => $response, - 'request' => $request, - 'uri' => (string) $request->getUri(), - 'status' => $response->getStatusCode(), - 'reason' => $response->getReasonPhrase(), - ]; - - if ($this->debug) { - $templateData['error'] = $e; - } - - $response->getBody()->write( - $this->renderer->render($this->template, $templateData) - ); - - return $response; - } - - /** - * @param \Throwable|\Exception $e - * @param ResponseInterface $response - * @return ResponseInterface - */ - private function prepareDefaultResponse($e, ResponseInterface $response) - { - $message = 'An unexpected error occurred'; - - if ($this->debug) { - $message .= "; strack trace:\n\n" . $this->prepareStackTrace($e); - } - - $response->getBody()->write($message); - - return $response; - } - - /** - * Prepares a stack trace to display. - * - * @param \Throwable|\Exception $e - * @return string - */ - private function prepareStackTrace($e) - { - $message = ''; - do { - $message .= sprintf( - $this->stackTraceTemplate, - get_class($e), - $e->getFile(), - $e->getLine(), - $e->getMessage(), - $e->getTraceAsString() + return $this->prepareTemplatedResponse( + $e, + $this->renderer, + [ + 'response' => $response, + 'request' => $request, + 'uri' => (string) $request->getUri(), + 'status' => $response->getStatusCode(), + 'reason' => $response->getReasonPhrase(), + ], + $this->debug, + $response ); - } while ($e = $e->getPrevious()); + } - return $message; + return $this->prepareDefaultResponse($e, $this->debug, $response); } } diff --git a/src/Middleware/ImplicitHeadMiddleware.php b/src/Middleware/ImplicitHeadMiddleware.php deleted file mode 100644 index cbe61fc7..00000000 --- a/src/Middleware/ImplicitHeadMiddleware.php +++ /dev/null @@ -1,48 +0,0 @@ -container = $container; - $this->responsePrototype = $responsePrototype; $this->middlewareName = $middlewareName; } /** - * @param ServerRequestInterface $request - * @param DelegateInterface $delegate - * @return ResponseInterface * @throws InvalidMiddlewareException for invalid middleware types pulled * from the container. */ - public function process(ServerRequestInterface $request, DelegateInterface $delegate) + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface { $middleware = $this->container->get($this->middlewareName); - - // http-interop middleware - if ($middleware instanceof ServerMiddlewareInterface) { - return $middleware->process($request, $delegate); - } - - // Unknown - invalid! - if (! is_callable($middleware)) { - throw new InvalidMiddlewareException(sprintf( - 'Lazy-loaded middleware "%s" is neither invokable nor implements %s', - $this->middlewareName, - ServerMiddlewareInterface::class - )); - } - - // Callable http-interop middleware - if ($this->isCallableInteropMiddleware($middleware)) { - return $middleware($request, $delegate); - } - - // Legacy double-pass signature - return $middleware($request, $this->responsePrototype, function ($request, $response) use ($delegate) { - return $delegate->process($request); - }); + return $middleware->process($request, $handler); } } diff --git a/src/Middleware/NotFoundHandler.php b/src/Middleware/NotFoundHandler.php deleted file mode 100644 index 7b7f36f9..00000000 --- a/src/Middleware/NotFoundHandler.php +++ /dev/null @@ -1,47 +0,0 @@ -internalDelegate = $internalDelegate; - } - - /** - * Creates and returns a 404 response. - * - * @param ServerRequestInterface $request Passed to internal delegate - * @param DelegateInterface $delegate Ignored. - * @return ResponseInterface - */ - public function process(ServerRequestInterface $request, DelegateInterface $delegate) - { - return $this->internalDelegate->process($request); - } -} diff --git a/src/Middleware/RouteMiddleware.php b/src/Middleware/RouteMiddleware.php deleted file mode 100644 index 9f854013..00000000 --- a/src/Middleware/RouteMiddleware.php +++ /dev/null @@ -1,31 +0,0 @@ -whoops = $whoops; } - /** - * @param \Throwable|\Exception $e - * @param ServerRequestInterface $request - * @param ResponseInterface $response - * @return ResponseInterface - */ - public function __invoke($e, ServerRequestInterface $request, ResponseInterface $response) - { + public function __invoke( + Throwable $e, + ServerRequestInterface $request, + ResponseInterface $response + ) : ResponseInterface { // Walk through all handlers foreach ($this->whoops->getHandlers() as $handler) { // Add fancy data for the PrettyPageHandler @@ -82,18 +82,14 @@ public function __invoke($e, ServerRequestInterface $request, ResponseInterface /** * Prepare the Whoops page handler with a table displaying request information - * - * @param ServerRequestInterface $request - * @param PrettyPageHandler $handler - * @return void */ - private function prepareWhoopsHandler(ServerRequestInterface $request, PrettyPageHandler $handler) + private function prepareWhoopsHandler(ServerRequestInterface $request, PrettyPageHandler $handler) : void { $uri = $request->getAttribute('originalUri', false) ?: $request->getUri(); $request = $request->getAttribute('originalRequest', false) ?: $request; $serverParams = $request->getServerParams(); - $scriptName = isset($serverParams['SCRIPT_NAME']) ? $serverParams['SCRIPT_NAME'] : ''; + $scriptName = $serverParams['SCRIPT_NAME'] ?? ''; $handler->addDataTable('Expressive Application Request', [ 'HTTP Method' => $request->getMethod(), diff --git a/src/MiddlewareContainer.php b/src/MiddlewareContainer.php new file mode 100644 index 00000000..646e140f --- /dev/null +++ b/src/MiddlewareContainer.php @@ -0,0 +1,74 @@ +container = $container; + } + + /** + * Returns true if the service is in the container, or resolves to an + * autoloadable class name. + * + * @param string $service + */ + public function has($service) : bool + { + if ($this->container->has($service)) { + return true; + } + + return class_exists($service); + } + + /** + * Returns middleware pulled from container, or directly instantiated if + * not managed by the container. + * + * @param string $service + * @throws Exception\MissingDependencyException if the service does not + * exist, or is not a valid class name. + * @throws Exception\InvalidMiddlewareException if the service is not + * an instance of MiddlewareInterface. + */ + public function get($service) : MiddlewareInterface + { + if (! $this->has($service)) { + throw Exception\MissingDependencyException::forMiddlewareService($service); + } + + $middleware = $this->container->has($service) + ? $this->container->get($service) + : new $service(); + + $middleware = $middleware instanceof RequestHandlerInterface + ? new RequestHandlerMiddleware($middleware) + : $middleware; + + if (! $middleware instanceof MiddlewareInterface) { + throw Exception\InvalidMiddlewareException::forMiddlewareService($service, $middleware); + } + + return $middleware; + } +} diff --git a/src/MiddlewareFactory.php b/src/MiddlewareFactory.php new file mode 100644 index 00000000..b6671d0d --- /dev/null +++ b/src/MiddlewareFactory.php @@ -0,0 +1,139 @@ +container = $container; + } + + /** + * @param string|array|callable|MiddlewareInterface|RequestHandlerInterface $middleware + * @throws Exception\InvalidMiddlewareException if argument is not one of + * the specified types. + */ + public function prepare($middleware) : MiddlewareInterface + { + if ($middleware instanceof MiddlewareInterface) { + return $middleware; + } + + if ($middleware instanceof RequestHandlerInterface) { + return new RequestHandlerMiddleware($middleware); + } + + if (is_callable($middleware)) { + return $this->callable($middleware); + } + + if (is_array($middleware)) { + return $this->pipeline(...$middleware); + } + + if (! is_string($middleware) || $middleware === '') { + throw Exception\InvalidMiddlewareException::forMiddleware($middleware); + } + + return $this->lazy($middleware); + } + + /** + * Decorate callable standards-signature middleware via a CallableMiddlewareDecorator. + */ + public function callable(callable $middleware) : CallableMiddlewareDecorator + { + return new CallableMiddlewareDecorator($middleware); + } + + /** + * Decorate a RequestHandlerInterface as middleware via RequestHandlerMiddleware. + */ + public function handler(RequestHandlerInterface $handler) : RequestHandlerMiddleware + { + return new RequestHandlerMiddleware($handler); + } + + /** + * Create lazy loading middleware based on a service name. + */ + public function lazy(string $middleware) : Middleware\LazyLoadingMiddleware + { + return new Middleware\LazyLoadingMiddleware($this->container, $middleware); + } + + /** + * Create a middleware pipeline from an array of middleware. + * + * This method allows passing an array of middleware as either: + * + * - discrete arguments + * - an array of middleware, using the splat operator: pipeline(...$array) + * - an array of middleware as the sole argument: pipeline($array) + * + * Each item is passed to prepare() before being passed to the + * MiddlewarePipe instance the method returns. + * + * @param string|array|MiddlewarePipe $middleware + */ + public function pipeline(...$middleware) : MiddlewarePipe + { + // Allow passing arrays of middleware or individual lists of middleware + if (is_array($middleware[0]) + && count($middleware) === 1 + ) { + $middleware = array_shift($middleware); + } + + $pipeline = new MiddlewarePipe(); + foreach ($middleware as $m) { + $pipeline->pipe($this->prepare($m)); + } + return $pipeline; + } +} diff --git a/src/Response/ErrorResponseGeneratorTrait.php b/src/Response/ErrorResponseGeneratorTrait.php new file mode 100644 index 00000000..c53223d2 --- /dev/null +++ b/src/Response/ErrorResponseGeneratorTrait.php @@ -0,0 +1,100 @@ +getBody() + ->write($renderer->render($this->template, $templateData)); + + return $response; + } + + private function prepareDefaultResponse( + Throwable $e, + bool $debug, + ResponseInterface $response + ) : ResponseInterface { + $message = 'An unexpected error occurred'; + + if ($debug) { + $message .= "; stack trace:\n\n" . $this->prepareStackTrace($e); + } + + $response->getBody()->write($message); + + return $response; + } + + /** + * Prepares a stack trace to display. + */ + private function prepareStackTrace(Throwable $e) : string + { + $message = ''; + do { + $message .= sprintf( + $this->stackTraceTemplate, + get_class($e), + $e->getFile(), + $e->getLine(), + $e->getMessage(), + $e->getTraceAsString() + ); + } while ($e = $e->getPrevious()); + + return $message; + } +} diff --git a/src/Response/ServerRequestErrorResponseGenerator.php b/src/Response/ServerRequestErrorResponseGenerator.php new file mode 100644 index 00000000..5c81ab13 --- /dev/null +++ b/src/Response/ServerRequestErrorResponseGenerator.php @@ -0,0 +1,69 @@ +responseFactory = function () use ($responseFactory) : ResponseInterface { + return $responseFactory(); + }; + + $this->debug = $isDevelopmentMode; + $this->renderer = $renderer; + $this->template = $template; + } + + public function __invoke(Throwable $e) : ResponseInterface + { + $response = ($this->responseFactory)(); + $response = $response->withStatus(Utils::getStatusCode($e, $response)); + + if ($this->renderer) { + return $this->prepareTemplatedResponse( + $e, + $this->renderer, + [ + 'response' => $response, + 'status' => $response->getStatusCode(), + 'reason' => $response->getReasonPhrase(), + ], + $this->debug, + $response + ); + } + + return $this->prepareDefaultResponse($e, $this->debug, $response); + } +} diff --git a/src/constants.php b/src/constants.php new file mode 100644 index 00000000..10a5b605 --- /dev/null +++ b/src/constants.php @@ -0,0 +1,80 @@ +noopMiddleware = new TestAsset\InteropMiddleware(); - $this->router = $this->prophesize(RouterInterface::class); - $this->disregardDeprecationNotices(); - } - - public function tearDown() - { - restore_error_handler(); - self::$existingClasses = null; - } - - public function disregardDeprecationNotices() - { - set_error_handler(function ($errno, $errstr) { - if (strstr($errstr, 'AppFactory is deprecated')) { - return true; - } - return false; - }, E_USER_DEPRECATED); - } - - - public function getRouterFromApplication(Application $app) - { - $r = new ReflectionProperty($app, 'router'); - $r->setAccessible(true); - return $r->getValue($app); - } - - public function testFactoryReturnsApplicationInstance() - { - $app = AppFactory::create(); - $this->assertInstanceOf(Application::class, $app); - } - - public function testFactoryUsesFastRouteByDefault() - { - $app = AppFactory::create(); - $router = $this->getRouterFromApplication($app); - $this->assertInstanceOf(FastRouteRouter::class, $router); - } - - public function testFactoryUsesZf2ServiceManagerByDefault() - { - $app = AppFactory::create(); - $container = $app->getContainer(); - $this->assertInstanceOf(ServiceManager::class, $container); - } - - public function testFactoryUsesEmitterStackWithSapiEmitterComposedByDefault() - { - $app = AppFactory::create(); - $emitter = $app->getEmitter(); - $this->assertInstanceOf(EmitterStack::class, $emitter); - - $this->assertCount(1, $emitter); - $this->assertInstanceOf(SapiEmitter::class, $emitter->pop()); - } - - public function testFactoryAllowsPassingContainerToUse() - { - $container = $this->prophesize(ContainerInterface::class); - $app = AppFactory::create($container->reveal()); - $test = $app->getContainer(); - $this->assertSame($container->reveal(), $test); - } - - public function testFactoryAllowsPassingRouterToUse() - { - $router = $this->prophesize(RouterInterface::class); - $app = AppFactory::create(null, $router->reveal()); - $test = $this->getRouterFromApplication($app); - $this->assertSame($router->reveal(), $test); - } - - /** - * @see http://stackoverflow.com/questions/4753811/php-unit-tests-is-it-possible-to-test-for-a-fatal-error - */ - public function testCannotInstantiateExternally() - { - $reflection = new ReflectionClass(AppFactory::class); - $constructor = $reflection->getConstructor(); - $this->assertFalse($constructor->isPublic()); - } - - public function testThrowExceptionWhenContainerNotProvidedAndServiceManagerNotExists() - { - self::$existingClasses = [ - FastRouteRouter::class, - ]; - - $this->expectException(MissingDependencyException::class); - - AppFactory::create(); - } - - public function testThrowExceptionWhenContainerNotProvidedAndFastRouteRouterNotExists() - { - self::$existingClasses = [ - ServiceManager::class, - ]; - - $this->expectException(MissingDependencyException::class); - - AppFactory::create(); - } -} diff --git a/test/Application/ConfigInjectionTest.php b/test/Application/ConfigInjectionTest.php deleted file mode 100644 index 702eb49d..00000000 --- a/test/Application/ConfigInjectionTest.php +++ /dev/null @@ -1,507 +0,0 @@ -container = $this->mockContainerInterface(); - $this->router = $this->prophesize(RouterInterface::class); - $this->disregardDeprecationNotices(); - } - - public function tearDown() - { - restore_error_handler(); - } - - public function disregardDeprecationNotices() - { - set_error_handler(function ($errno, $errstr) { - if (strstr($errstr, 'pipe() the middleware directly')) { - return true; - } - if (strstr($errstr, 'doublePassMiddleware()')) { - return true; - } - return false; - }, E_USER_DEPRECATED); - } - - public function createApplication() - { - return new Application($this->router->reveal(), $this->container->reveal()); - } - - public static function assertRoute($spec, array $routes) - { - Assert::assertThat( - array_reduce($routes, function ($found, $route) use ($spec) { - if ($found) { - return $found; - } - - if ($route->getPath() !== $spec['path']) { - return false; - } - - // We're just testing that middleware is present; since it may - // be decorated, this might fail otherwise. - if (! $route->getMiddleware()) { - return false; - } - - if (isset($spec['allowed_methods']) - && $route->getAllowedMethods() !== $spec['allowed_methods'] - ) { - return false; - } - - if (! isset($spec['allowed_methods']) - && $route->getAllowedMethods() !== Route::HTTP_METHOD_ANY - ) { - return false; - } - - return true; - }, false), - Assert::isTrue(), - 'Route created does not match any specifications' - ); - } - - public static function assertPipelineContainsInstanceOf($class, $pipeline, $message = null) - { - $message = $message ?: 'Did not find expected middleware class type in pipeline'; - $found = false; - - foreach ($pipeline as $middleware) { - if ($middleware instanceof $class) { - $found = true; - break; - } - } - - Assert::assertThat($found, Assert::isTrue(), $message); - } - - public function injectableMiddleware() - { - return [ - [CallableInteropMiddleware::class], - [ - function ($request, DelegateInterface $delegate) { - }, - ], - [[CallableInteropMiddleware::class, 'staticallyCallableMiddleware']], - ]; - } - - /** - * @dataProvider injectableMiddleware - * - * @param callable|array|string $middleware - */ - public function testInjectRoutesFromConfigSetsUpRoutesFromConfig($middleware) - { - $pingMiddleware = $this->prophesize(MiddlewareInterface::class)->reveal(); - $config = [ - 'routes' => [ - [ - 'path' => '/', - 'middleware' => $middleware, - 'allowed_methods' => ['GET'], - ], - [ - 'path' => '/ping', - 'middleware' => $pingMiddleware, - 'allowed_methods' => ['GET'], - ], - ], - ]; - - $app = $this->createApplication(); - - $app->injectRoutesFromConfig($config); - - $routes = $app->getRoutes(); - - foreach ($config['routes'] as $route) { - $this->assertRoute($route, $routes); - } - } - - public function testNoRoutesAreAddedIfSpecDoesNotProvidePathOrMiddleware() - { - $config = [ - 'routes' => [ - [ - 'allowed_methods' => ['GET'], - ], - [ - 'allowed_methods' => ['POST'], - ], - ], - ]; - - $app = $this->createApplication(); - - $app->injectRoutesFromConfig($config); - - $routes = $app->getRoutes(); - $this->assertCount(0, $routes); - } - - public function testPipelineContainingRoutingMiddlewareConstantPipesRoutingMiddleware() - { - $config = [ - 'middleware_pipeline' => [ - Application::ROUTING_MIDDLEWARE, - ], - ]; - $app = $this->createApplication(); - - $app->injectPipelineFromConfig($config); - - $this->assertAttributeSame(true, 'routeMiddlewareIsRegistered', $app); - } - - public function testPipelineContainingDispatchMiddlewareConstantPipesDispatchMiddleware() - { - $config = [ - 'middleware_pipeline' => [ - Application::DISPATCH_MIDDLEWARE, - ], - ]; - $app = $this->createApplication(); - - $app->injectPipelineFromConfig($config); - - $this->assertAttributeSame(true, 'dispatchMiddlewareIsRegistered', $app); - } - - public function testInjectPipelineFromConfigHonorsPriorityOrderWhenAttachingMiddleware() - { - $middleware = new TestAsset\InteropMiddleware(); - - $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]; - - $app = $this->createApplication(); - - $app->injectPipelineFromConfig($config); - - $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() - { - $middleware = new TestAsset\InteropMiddleware(); - - $pipeline1 = [['middleware' => clone $middleware]]; - $pipeline2 = [['middleware' => clone $middleware]]; - $pipeline3 = [['middleware' => clone $middleware]]; - - $pipeline = array_merge($pipeline3, $pipeline1, $pipeline2); - $config = ['middleware_pipeline' => $pipeline]; - - $app = $this->createApplication(); - - $app->injectPipelineFromConfig($config); - - $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() - { - $middleware = new TestAsset\InteropMiddleware(); - - $pipeline = [ - ['middleware' => clone $middleware, 'priority' => -100], - Application::ROUTING_MIDDLEWARE, - ['middleware' => clone $middleware, 'priority' => 1], - ['middleware' => clone $middleware], - Application::DISPATCH_MIDDLEWARE, - ['middleware' => clone $middleware, 'priority' => 100], - ]; - - $config = ['middleware_pipeline' => $pipeline]; - - $app = $this->createApplication(); - - $app->injectPipelineFromConfig($config); - - $r = new ReflectionProperty($app, 'pipeline'); - $r->setAccessible(true); - $test = $r->getValue($app); - - $this->assertSame($pipeline[5]['middleware'], $test->dequeue()->handler); - $this->assertInstanceOf(RouteMiddleware::class, $test->dequeue()->handler); - $this->assertSame($pipeline[2]['middleware'], $test->dequeue()->handler); - $this->assertSame($pipeline[3]['middleware'], $test->dequeue()->handler); - $this->assertInstanceOf(DispatchMiddleware::class, $test->dequeue()->handler); - $this->assertSame($pipeline[0]['middleware'], $test->dequeue()->handler); - } - - public function specMiddlewareContainingRoutingAndOrDispatchMiddleware() - { - // @codingStandardsIgnoreStart - return [ - 'routing-only' => [[['middleware' => [Application::ROUTING_MIDDLEWARE]]]], - 'dispatch-only' => [[['middleware' => [Application::DISPATCH_MIDDLEWARE]]]], - 'both-routing-and-dispatch' => [[['middleware' => [Application::ROUTING_MIDDLEWARE, Application::DISPATCH_MIDDLEWARE]]]], - ]; - // @codingStandardsIgnoreEnd - } - - /** - * @dataProvider specMiddlewareContainingRoutingAndOrDispatchMiddleware - * - * @param array $pipeline - */ - public function testRoutingAndDispatchMiddlewareCanBeComposedWithinArrayStandardSpecification(array $pipeline) - { - $expected = $pipeline[0]['middleware']; - $config = ['middleware_pipeline' => $pipeline]; - - $app = $this->createApplication(); - - $app->injectPipelineFromConfig($config); - - $r = new ReflectionProperty($app, 'pipeline'); - $r->setAccessible(true); - $appPipeline = $r->getValue($app); - - $this->assertCount(1, $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 Application::ROUTING_MIDDLEWARE: - $middleware = RouteMiddleware::class; - $message = 'Did not find routing middleware in pipeline'; - break; - case Application::DISPATCH_MIDDLEWARE: - $middleware = DispatchMiddleware::class; - $message = 'Did not find dispatch middleware in pipeline'; - break; - default: - $this->fail('Unexpected value in pipeline passed from data provider'); - } - $this->assertPipelineContainsInstanceOf($middleware, $innerPipeline, $message); - } - } - - public function testInjectPipelineFromConfigWithEmptyConfigAndNoConfigServiceDoesNothing() - { - $this->container->has('config')->willReturn(false); - $app = $this->createApplication(); - - $app->injectPipelineFromConfig(); - - $r = new ReflectionProperty($app, 'pipeline'); - $r->setAccessible(true); - $pipeline = $r->getValue($app); - $this->assertInstanceOf(SplQueue::class, $pipeline); - - $this->assertEquals(0, $pipeline->count()); - } - - public function testInjectRoutesFromConfigWithEmptyConfigAndNoConfigServiceDoesNothing() - { - $this->container->has('config')->willReturn(false); - $app = $this->createApplication(); - - $app->injectRoutesFromConfig(); - $this->assertAttributeEquals([], 'routes', $app); - } - - public function testInjectRoutesFromConfigRaisesExceptionIfAllowedMethodsIsInvalid() - { - $config = [ - 'routes' => [ - [ - 'path' => '/', - 'middleware' => new TestAsset\InteropMiddleware(), - 'allowed_methods' => 'not-valid', - ], - ], - ]; - $this->container->has('config')->willReturn(false); - $app = $this->createApplication(); - - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Allowed HTTP methods'); - $app->injectRoutesFromConfig($config); - } - - public function testInjectRoutesFromConfigRaisesExceptionIfOptionsIsNotAnArray() - { - $config = [ - 'routes' => [ - [ - 'path' => '/', - 'middleware' => new TestAsset\InteropMiddleware(), - 'allowed_methods' => ['GET'], - 'options' => 'invalid', - ], - ], - ]; - $this->container->has('config')->willReturn(false); - $app = $this->createApplication(); - - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Route options must be an array'); - $app->injectRoutesFromConfig($config); - } - - public function testInjectRoutesFromConfigCanProvideRouteOptions() - { - $config = [ - 'routes' => [ - [ - 'path' => '/', - 'middleware' => new TestAsset\InteropMiddleware(), - 'allowed_methods' => ['GET'], - 'options' => [ - 'foo' => 'bar', - ], - ], - ], - ]; - $this->container->has('config')->willReturn(false); - $app = $this->createApplication(); - - $app->injectRoutesFromConfig($config); - - $routes = $app->getRoutes(); - - $route = array_shift($routes); - $this->assertEquals($config['routes'][0]['options'], $route->getOptions()); - } - - public function testInjectRoutesFromConfigWillSkipSpecsThatOmitPath() - { - $config = [ - 'routes' => [ - [ - 'middleware' => new TestAsset\InteropMiddleware(), - 'allowed_methods' => ['GET'], - 'options' => [ - 'foo' => 'bar', - ], - ], - ], - ]; - $this->container->has('config')->willReturn(false); - $app = $this->createApplication(); - - $app->injectPipelineFromConfig($config); - $this->assertAttributeEquals([], 'routes', $app); - } - - public function testInjectRoutesFromConfigWillSkipSpecsThatOmitMiddleware() - { - $config = [ - 'routes' => [ - [ - 'path' => '/', - 'allowed_methods' => ['GET'], - 'options' => [ - 'foo' => 'bar', - ], - ], - ], - ]; - $this->container->has('config')->willReturn(false); - $app = $this->createApplication(); - - $app->injectPipelineFromConfig($config); - $this->assertAttributeEquals([], 'routes', $app); - } - - public function testInjectPipelineFromConfigRaisesExceptionForSpecsOmittingMiddlewareKey() - { - $config = [ - 'middleware_pipeline' => [ - [ - 'this' => 'will not work', - ], - ], - ]; - $app = $this->createApplication(); - - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Invalid pipeline specification received'); - $app->injectPipelineFromConfig($config); - } -} diff --git a/test/Application/MarshalMiddlewareTraitTest.php b/test/Application/MarshalMiddlewareTraitTest.php deleted file mode 100644 index f34bbb37..00000000 --- a/test/Application/MarshalMiddlewareTraitTest.php +++ /dev/null @@ -1,339 +0,0 @@ -container = $this->prophesize(ContainerInterface::class); - $this->router = $this->prophesize(RouterInterface::class); - $this->responsePrototype = $this->prophesize(ResponseInterface::class); - $this->application = new Application($this->router->reveal()); - $this->disregardDeprecationNotices(); - } - - public function tearDown() - { - restore_error_handler(); - } - - public function disregardDeprecationNotices() - { - set_error_handler(function ($errno, $errstr) { - if (strstr($errstr, 'pipe() the middleware directly')) { - return true; - } - if (strstr($errstr, 'doublePassMiddleware()')) { - return true; - } - return false; - }, E_USER_DEPRECATED); - } - - public function prepareMiddleware($middleware) - { - $r = new ReflectionMethod($this->application, 'prepareMiddleware'); - $r->setAccessible(true); - return $r->invoke( - $this->application, - $middleware, - $this->router->reveal(), - $this->responsePrototype->reveal(), - $this->container->reveal() - ); - } - - public function prepareMiddlewareWithoutContainer($middleware) - { - $r = new ReflectionMethod($this->application, 'prepareMiddleware'); - $r->setAccessible(true); - return $r->invoke( - $this->application, - $middleware, - $this->router->reveal(), - $this->responsePrototype->reveal() - ); - } - - public function testPreparingRoutingMiddlewareReturnsRoutingMiddleware() - { - $middleware = $this->prepareMiddleware(Application::ROUTING_MIDDLEWARE); - $this->assertInstanceOf(RouteMiddleware::class, $middleware); - $this->assertAttributeSame($this->router->reveal(), 'router', $middleware); - $this->assertAttributeSame($this->responsePrototype->reveal(), 'responsePrototype', $middleware); - } - - public function testPreparingRoutingMiddlewareWithoutContainerReturnsRoutingMiddleware() - { - $middleware = $this->prepareMiddlewareWithoutContainer(Application::ROUTING_MIDDLEWARE); - $this->assertInstanceOf(RouteMiddleware::class, $middleware); - $this->assertAttributeSame($this->router->reveal(), 'router', $middleware); - $this->assertAttributeSame($this->responsePrototype->reveal(), 'responsePrototype', $middleware); - } - - public function testPreparingDispatchMiddlewareReturnsDispatchMiddleware() - { - $middleware = $this->prepareMiddleware(Application::DISPATCH_MIDDLEWARE); - $this->assertInstanceOf(DispatchMiddleware::class, $middleware); - } - - public function testPreparingDispatchMiddlewareWithoutContainerReturnsDispatchMiddleware() - { - $middleware = $this->prepareMiddlewareWithoutContainer(Application::DISPATCH_MIDDLEWARE); - $this->assertInstanceOf(DispatchMiddleware::class, $middleware); - } - - public function testPreparingInteropMiddlewareReturnsMiddlewareVerbatim() - { - $base = $this->prophesize(ServerMiddlewareInterface::class)->reveal(); - $middleware = $this->prepareMiddleware($base); - $this->assertSame($base, $middleware); - } - - public function testPreparingInteropMiddlewareWithoutContainerReturnsMiddlewareVerbatim() - { - $base = $this->prophesize(ServerMiddlewareInterface::class)->reveal(); - $middleware = $this->prepareMiddlewareWithoutContainer($base); - $this->assertSame($base, $middleware); - } - - public function testPreparingDuckTypedInteropMiddlewareReturnsDecoratedInteropMiddleware() - { - $base = function ($request, DelegateInterface $delegate) { - }; - $middleware = $this->prepareMiddleware($base); - $this->assertInstanceOf(CallableMiddlewareDecorator::class, $middleware); - $this->assertAttributeSame($base, 'middleware', $middleware); - } - - public function testPreparingDuckTypedInteropMiddlewareWithoutContainerReturnsDecoratedInteropMiddleware() - { - $base = function ($request, DelegateInterface $delegate) { - }; - $middleware = $this->prepareMiddlewareWithoutContainer($base); - $this->assertInstanceOf(CallableMiddlewareDecorator::class, $middleware); - $this->assertAttributeSame($base, 'middleware', $middleware); - } - - public function testPreparingCallableMiddlewareReturnsDecoratedMiddleware() - { - $base = function ($request, $response, callable $next) { - }; - $middleware = $this->prepareMiddleware($base); - $this->assertInstanceOf(DoublePassMiddlewareDecorator::class, $middleware); - $this->assertAttributeSame($base, 'middleware', $middleware); - $this->assertAttributeSame($this->responsePrototype->reveal(), 'responsePrototype', $middleware); - } - - public function testPreparingCallableMiddlewareWithoutContainerReturnsDecoratedMiddleware() - { - $base = function ($request, $response, callable $next) { - }; - $middleware = $this->prepareMiddlewareWithoutContainer($base); - $this->assertInstanceOf(DoublePassMiddlewareDecorator::class, $middleware); - $this->assertAttributeSame($base, 'middleware', $middleware); - $this->assertAttributeSame($this->responsePrototype->reveal(), 'responsePrototype', $middleware); - } - - public function testPreparingArrayOfMiddlewareReturnsMiddlewarePipe() - { - $first = $this->prophesize(ServerMiddlewareInterface::class)->reveal(); - $second = function ($request, DelegateInterface $delegate) { - }; - $third = function ($request, $response, callable $next) { - }; - $fourth = 'fourth'; - $fifth = TestAsset\CallableMiddleware::class; - $sixth = TestAsset\CallableInteropMiddleware::class; - $seventh = [$first, $second]; - - $this->container->has('fourth')->willReturn(true); - $this->container->has(TestAsset\CallableMiddleware::class)->willReturn(false); - $this->container->has(TestAsset\CallableInteropMiddleware::class)->willReturn(false); - - $base = [ - $first, - $second, - $third, - $fourth, - $fifth, - $sixth, - $seventh, - ]; - - $middleware = $this->prepareMiddleware($base); - $this->assertInstanceOf(MiddlewarePipe::class, $middleware); - - $r = new ReflectionProperty($middleware, 'pipeline'); - $r->setAccessible(true); - $pipeline = $r->getValue($middleware); - - $this->assertCount(7, $pipeline); - } - - public function testPreparingArrayOfMiddlewareWithoutContainerReturnsMiddlewarePipe() - { - $first = $this->prophesize(ServerMiddlewareInterface::class)->reveal(); - $second = function ($request, DelegateInterface $delegate) { - }; - $third = function ($request, $response, callable $next) { - }; - $fifth = TestAsset\CallableMiddleware::class; - $sixth = TestAsset\CallableInteropMiddleware::class; - - $base = [ - $first, - $second, - $third, - $fifth, - $sixth, - ]; - - $middleware = $this->prepareMiddleware($base); - $this->assertInstanceOf(MiddlewarePipe::class, $middleware); - - $r = new ReflectionProperty($middleware, 'pipeline'); - $r->setAccessible(true); - $pipeline = $r->getValue($middleware); - - $this->assertCount(5, $pipeline); - } - - public function testPreparingArrayOfMiddlewareRaisesExceptionWhenContainerMissingAndServiceInvalid() - { - $first = $this->prophesize(ServerMiddlewareInterface::class)->reveal(); - $second = 'second-middleware'; - $third = 'third-middleware'; - - // No container is passed to the method, so this will not matter - $this->container->has('second-middleware')->willReturn(true); - $this->container->has('third-middleware')->willReturn(true); - - $base = [$first, $second, $third]; - - $this->expectException(InvalidMiddlewareException::class); - $this->expectExceptionMessage('second-middleware'); - $this->prepareMiddlewareWithoutContainer($base); - } - - public function testPreparingServiceBasedMiddlewareReturnsLazyLoadingMiddleware() - { - $middlewareName = 'middleware'; - $this->container->has($middlewareName)->willReturn(true); - - $middleware = $this->prepareMiddleware($middlewareName); - $this->assertInstanceOf(LazyLoadingMiddleware::class, $middleware); - - $this->assertAttributeSame($this->container->reveal(), 'container', $middleware); - $this->assertAttributeSame($this->responsePrototype->reveal(), 'responsePrototype', $middleware); - $this->assertAttributeEquals($middlewareName, 'middlewareName', $middleware); - } - - public function testPreparingInvokableInteropMiddlewareReturnsDecoratedInteropMiddleware() - { - $base = TestAsset\CallableInteropMiddleware::class; - $this->container->has(TestAsset\CallableInteropMiddleware::class)->willReturn(false); - - $middleware = $this->prepareMiddleware($base); - - $this->assertInstanceOf(CallableMiddlewareDecorator::class, $middleware); - $this->assertAttributeInstanceOf(TestAsset\CallableInteropMiddleware::class, 'middleware', $middleware); - } - - public function testPreparingInvokableCallableMiddlewareReturnsDecoratedMiddleware() - { - $base = TestAsset\CallableMiddleware::class; - $this->container->has(TestAsset\CallableMiddleware::class)->willReturn(false); - $middleware = $this->prepareMiddleware($base); - $this->assertInstanceOf(DoublePassMiddlewareDecorator::class, $middleware); - $this->assertAttributeInstanceOf(TestAsset\CallableMiddleware::class, 'middleware', $middleware); - } - - public function testPreparingInvalidInvokableMiddlewareRaisesException() - { - $base = stdClass::class; - $this->container->has(stdClass::class)->willReturn(false); - - $this->expectException(InvalidMiddlewareException::class); - $this->expectExceptionMessage('invalid; neither invokable'); - $this->prepareMiddleware($base); - } - - public function testPreparingInvokableInteropMiddlewareThatIsRegisteredInContainerReturnsLazyMiddleware() - { - $base = TestAsset\CallableMiddleware::class; - $this->container->has(TestAsset\CallableMiddleware::class)->willReturn(true); - $middleware = $this->prepareMiddleware($base); - - $this->assertInstanceOf(LazyLoadingMiddleware::class, $middleware); - $this->assertAttributeEquals($base, 'middlewareName', $middleware); - } - - public function invalidMiddlewareTypes() - { - $defaultExpectedMessage = 'Unable to resolve middleware'; - return [ - 'null' => [null, $defaultExpectedMessage], - 'true' => [true, $defaultExpectedMessage], - 'false' => [false, $defaultExpectedMessage], - 'zero' => [0, $defaultExpectedMessage], - 'int' => [1, $defaultExpectedMessage], - 'zero-float' => [0.0, $defaultExpectedMessage], - 'float' => [1.1, $defaultExpectedMessage], - 'non-class-name-string' => ['not-a-class-name', 'not a valid class or service name'], - 'non-callable-class-name' => [stdClass::class, 'invalid; neither invokable'], - 'non-callable-object' => [new stdClass(), $defaultExpectedMessage], - ]; - } - - /** - * @dataProvider invalidMiddlewareTypes - * - * @param mixed $invalid - * @param string $expectedMessage - */ - public function testPreparingUnknownMiddlewareTypeRaisesException($invalid, $expectedMessage) - { - $this->expectException(InvalidMiddlewareException::class); - $this->expectExceptionMessage($expectedMessage); - $this->prepareMiddlewareWithoutContainer($invalid); - } -} diff --git a/test/ApplicationTest.php b/test/ApplicationTest.php index 6ce96bbb..6f00210e 100644 --- a/test/ApplicationTest.php +++ b/test/ApplicationTest.php @@ -1,759 +1,343 @@ noopMiddleware = new TestAsset\InteropMiddleware(); - $this->router = $this->prophesize(RouterInterface::class); - $this->disregardDeprecationNotices(); - } - - public function tearDown() - { - restore_error_handler(); - } - - public function disregardDeprecationNotices() - { - set_error_handler(function ($errno, $errstr) { - if (strstr($errstr, 'pipe() the middleware directly')) { - return true; - } - return false; - }, E_USER_DEPRECATED); - } - - public function getApp() - { - return new Application($this->router->reveal()); - } - - public function commonHttpMethods() - { - return [ - 'GET' => ['GET'], - 'POST' => ['POST'], - 'PUT' => ['PUT'], - 'PATCH' => ['PATCH'], - 'DELETE' => ['DELETE'], - ]; - } + $this->factory = $this->prophesize(MiddlewareFactory::class); + $this->pipeline = $this->prophesize(MiddlewarePipeInterface::class); + $this->routes = $this->prophesize(RouteMiddleware::class); + $this->runner = $this->prophesize(RequestHandlerRunner::class); - public function testConstructorAcceptsRouterAsAnArgument() - { - $app = $this->getApp(); - $this->assertInstanceOf(Application::class, $app); - } - - public function testApplicationIsAMiddlewarePipe() - { - $app = $this->getApp(); - $this->assertInstanceOf(MiddlewarePipe::class, $app); + $this->app = new Application( + $this->factory->reveal(), + $this->pipeline->reveal(), + $this->routes->reveal(), + $this->runner->reveal() + ); } - public function testRouteMethodReturnsRouteInstance() + public function createMockMiddleware() { - $this->router->addRoute(Argument::type(Route::class))->shouldBeCalled(); - $route = $this->getApp()->route('/foo', $this->noopMiddleware); - $this->assertInstanceOf(Route::class, $route); - $this->assertEquals('/foo', $route->getPath()); - $this->assertSame($this->noopMiddleware, $route->getMiddleware()); + return $this->prophesize(MiddlewareInterface::class)->reveal(); } - public function testAnyRouteMethod() + public function testHandleProxiesToPipelineToHandle() { - $this->router->addRoute(Argument::type(Route::class))->shouldBeCalled(); - $route = $this->getApp()->any('/foo', $this->noopMiddleware); - $this->assertInstanceOf(Route::class, $route); - $this->assertEquals('/foo', $route->getPath()); - $this->assertSame($this->noopMiddleware, $route->getMiddleware()); - $this->assertSame(Route::HTTP_METHOD_ANY, $route->getAllowedMethods()); - } + $request = $this->prophesize(ServerRequestInterface::class)->reveal(); + $response = $this->prophesize(ResponseInterface::class)->reveal(); - /** - * @dataProvider commonHttpMethods - * - * @param string $method - */ - public function testCanCallRouteWithHttpMethods($method) - { - $this->router->addRoute(Argument::type(Route::class))->shouldBeCalled(); - $route = $this->getApp()->route('/foo', $this->noopMiddleware, [$method]); - $this->assertInstanceOf(Route::class, $route); - $this->assertEquals('/foo', $route->getPath()); - $this->assertSame($this->noopMiddleware, $route->getMiddleware()); - $this->assertTrue($route->allowsMethod($method)); - $this->assertSame([$method], $route->getAllowedMethods()); - } + $this->pipeline->handle($request)->willReturn($response); - public function testCanCallRouteWithMultipleHttpMethods() - { - $this->router->addRoute(Argument::type(Route::class))->shouldBeCalled(); - $methods = array_keys($this->commonHttpMethods()); - $route = $this->getApp()->route('/foo', $this->noopMiddleware, $methods); - $this->assertInstanceOf(Route::class, $route); - $this->assertEquals('/foo', $route->getPath()); - $this->assertSame($this->noopMiddleware, $route->getMiddleware()); - $this->assertSame($methods, $route->getAllowedMethods()); + $this->assertSame($response, $this->app->handle($request)); } - public function testCanCallRouteWithARoute() + public function testProcessProxiesToPipelineToProcess() { - $route = new Route('/foo', $this->noopMiddleware); - $this->router->addRoute($route)->shouldBeCalled(); - $app = $this->getApp(); - $test = $app->route($route); - $this->assertSame($route, $test); - } + $request = $this->prophesize(ServerRequestInterface::class)->reveal(); + $response = $this->prophesize(ResponseInterface::class)->reveal(); + $handler = $this->prophesize(RequestHandlerInterface::class)->reveal(); - public function testCallingRouteWithExistingPathAndOmittingMethodsArgumentRaisesException() - { - $this->router->addRoute(Argument::type(Route::class))->shouldBeCalledTimes(2); - $app = $this->getApp(); - $app->route('/foo', $this->noopMiddleware); - $app->route('/bar', $this->noopMiddleware); - $this->expectException(DomainException::class); - $app->route('/foo', function ($req, $res, $next) { - }); - } + $this->pipeline->process($request, $handler)->willReturn($response); - public function testCallingRouteWithOnlyAPathRaisesAnException() - { - $app = $this->getApp(); - $this->expectException(Exception\InvalidArgumentException::class); - $app->route('/path'); + $this->assertSame($response, $this->app->process($request, $handler)); } - public function invalidPathTypes() + public function testRunProxiesToRunner() { - return [ - 'null' => [null], - 'true' => [true], - 'false' => [false], - 'zero' => [0], - 'int' => [1], - 'zero-float' => [0.0], - 'float' => [1.1], - 'array' => [['path' => 'route']], - 'object' => [(object) ['path' => 'route']], - ]; + $this->runner->run(null)->shouldBeCalled(); + $this->assertNull($this->app->run()); } - /** - * @dataProvider invalidPathTypes - * - * @param mixed $path - */ - public function testCallingRouteWithAnInvalidPathTypeRaisesAnException($path) + public function validMiddleware() : iterable { - $app = $this->getApp(); - $this->expectException(RouterException\InvalidArgumentException::class); - $app->route($path, new TestAsset\InteropMiddleware()); + // @codingStandardsIgnoreStart + yield 'string' => ['service']; + yield 'array' => [['middleware', 'service']]; + yield 'callable' => [function ($request, $response) {}]; + yield 'instance' => [new MiddlewarePipe()]; + // @codingStandardsIgnoreEnd } /** - * @dataProvider commonHttpMethods - * - * @param mixed $method + * @dataProvider validMiddleware + * @param string|array|callable|MiddlewareInterface $middleware */ - public function testCommonHttpMethodsAreExposedAsClassMethodsAndReturnRoutes($method) - { - $app = $this->getApp(); - $route = $app->{$method}('/foo', $this->noopMiddleware); - $this->assertInstanceOf(Route::class, $route); - $this->assertEquals('/foo', $route->getPath()); - $this->assertSame($this->noopMiddleware, $route->getMiddleware()); - $this->assertEquals([$method], $route->getAllowedMethods()); - } - - public function testCreatingHttpRouteMethodWithExistingPathButDifferentMethodCreatesNewRouteInstance() - { - $this->router->addRoute(Argument::type(Route::class))->shouldBeCalledTimes(2); - $app = $this->getApp(); - $route = $app->route('/foo', $this->noopMiddleware, []); - - $middleware = new TestAsset\InteropMiddleware(); - $test = $app->get('/foo', $middleware); - $this->assertNotSame($route, $test); - $this->assertSame($route->getPath(), $test->getPath()); - $this->assertSame(['GET'], $test->getAllowedMethods()); - $this->assertSame($middleware, $test->getMiddleware()); - } - - public function testCreatingHttpRouteWithExistingPathAndMethodRaisesException() - { - $this->router->addRoute(Argument::type(Route::class))->shouldBeCalledTimes(1); - $app = $this->getApp(); - $app->get('/foo', $this->noopMiddleware); - - $this->expectException(DomainException::class); - $app->get('/foo', function ($req, $res, $next) { - }); - } - - public function testRouteAndDispatchMiddlewareAreNotPipedAtInstantation() + public function testPipeCanAcceptSingleMiddlewareArgument($middleware) { - $app = $this->getApp(); + $preparedMiddleware = $this->createMockMiddleware(); + $this->factory + ->prepare($middleware) + ->willReturn($preparedMiddleware); - $r = new ReflectionProperty($app, 'pipeline'); - $r->setAccessible(true); - $pipeline = $r->getValue($app); + $this->pipeline + ->pipe(Argument::that(function ($test) use ($preparedMiddleware) { + Assert::assertSame($preparedMiddleware, $test); + return $test; + })) + ->shouldBeCalled(); - $this->assertCount(0, $pipeline); + $this->assertNull($this->app->pipe($middleware)); } - public function testCannotPipeRouteMiddlewareMoreThanOnce() + /** + * @dataProvider validMiddleware + * @param string|array|callable|MiddlewareInterface $middleware + */ + public function testPipeCanAcceptAPathArgument($middleware) { - $app = $this->getApp(); - $routeMiddleware = Application::ROUTING_MIDDLEWARE; - - $app->pipe($routeMiddleware); - $app->pipe($routeMiddleware); - - $r = new ReflectionProperty($app, 'pipeline'); - $r->setAccessible(true); - $pipeline = $r->getValue($app); + $preparedMiddleware = $this->createMockMiddleware(); + $this->factory + ->prepare($middleware) + ->willReturn($preparedMiddleware); - $this->assertCount(1, $pipeline); - $route = $pipeline->dequeue(); - $this->assertInstanceOf(StratigilityRoute::class, $route); - $test = $route->handler; + $this->pipeline + ->pipe(Argument::that(function ($test) use ($preparedMiddleware) { + Assert::assertInstanceOf(PathMiddlewareDecorator::class, $test); + Assert::assertAttributeSame('/foo', 'prefix', $test); + Assert::assertAttributeSame($preparedMiddleware, 'middleware', $test); + return $test; + })) + ->shouldBeCalled(); - $this->assertInstanceOf(RouteMiddleware::class, $test); + $this->assertNull($this->app->pipe('/foo', $middleware)); } - public function testCannotPipeDispatchMiddlewareMoreThanOnce() + public function testPipeNonSlashPathOnNonStringPipeProduceTypeError() { - $app = $this->getApp(); - $dispatchMiddleware = Application::DISPATCH_MIDDLEWARE; - - $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->assertInstanceOf(DispatchMiddleware::class, $test); - } + $middleware1 = function ($request, $response) { + return $response; + }; + $middleware2 = $this->createMockMiddleware(); - public function testCanInjectDefaultDelegateViaConstructor() - { - $defaultDelegate = $this->prophesize(DelegateInterface::class)->reveal(); - $app = new Application($this->router->reveal(), null, $defaultDelegate); - $test = $app->getDefaultDelegate(); - $this->assertSame($defaultDelegate, $test); + $this->expectException(TypeError::class); + $this->app->pipe($middleware1, $middleware2); } - public function testDefaultDelegateIsUsedAtInvocationIfNoOutArgumentIsSupplied() + /** + * @dataProvider validMiddleware + * @param string|array|callable|MiddlewareInterface $middleware + */ + public function testRouteAcceptsPathAndMiddlewareOnly($middleware) { - $routeResult = RouteResult::fromRouteFailure(); - $this->router->match()->willReturn($routeResult); - - $finalResponse = $this->prophesize(ResponseInterface::class)->reveal(); - $defaultDelegate = $this->prophesize(DelegateInterface::class); - $defaultDelegate->process(Argument::type(ServerRequestInterface::class)) - ->willReturn($finalResponse); + $preparedMiddleware = $this->createMockMiddleware(); - $emitter = $this->prophesize(EmitterInterface::class); - $emitter->emit($finalResponse)->shouldBeCalled(); + $this->factory + ->prepare($middleware) + ->willReturn($preparedMiddleware); - $app = new Application($this->router->reveal(), null, $defaultDelegate->reveal(), $emitter->reveal()); + $route = $this->prophesize(Route::class)->reveal(); - $request = new Request([], [], 'http://example.com/'); + $this->routes + ->route( + '/foo', + $preparedMiddleware, + null, + null + ) + ->willReturn($route); - $app->run($request); + $this->assertSame($route, $this->app->route('/foo', $middleware)); } - public function testComposesEmitterStackWithSapiEmitterByDefault() - { - $app = $this->getApp(); - $stack = $app->getEmitter(); - $this->assertInstanceOf(EmitterStack::class, $stack); - - $this->assertCount(1, $stack); - $test = $stack->pop(); - $this->assertInstanceOf(SapiEmitter::class, $test); - } - - public function testAllowsInjectingEmitterAtInstantiation() - { - $emitter = $this->prophesize(EmitterInterface::class); - $app = new Application( - $this->router->reveal(), - null, - null, - $emitter->reveal() - ); - $test = $app->getEmitter(); - $this->assertSame($emitter->reveal(), $test); - } - - public function testComposedEmitterIsCalledByRun() + /** + * @dataProvider validMiddleware + * @param string|array|callable|MiddlewareInterface $middleware + */ + public function testRouteAcceptsPathMiddlewareAndMethodsOnly($middleware) { - $routeResult = RouteResult::fromRouteFailure(); - $this->router->match()->willReturn($routeResult); - - $finalResponse = $this->prophesize(ResponseInterface::class)->reveal(); - $defaultDelegate = $this->prophesize(DelegateInterface::class); - $defaultDelegate->process(Argument::type(ServerRequestInterface::class)) - ->willReturn($finalResponse); + $preparedMiddleware = $this->createMockMiddleware(); - $emitter = $this->prophesize(EmitterInterface::class); - $emitter->emit( - Argument::type(ResponseInterface::class) - )->shouldBeCalled(); + $this->factory + ->prepare($middleware) + ->willReturn($preparedMiddleware); - $app = new Application($this->router->reveal(), null, $defaultDelegate->reveal(), $emitter->reveal()); + $route = $this->prophesize(Route::class)->reveal(); - $request = new Request([], [], 'http://example.com/'); - $response = $this->prophesize(ResponseInterface::class); - $response->withStatus(StatusCode::STATUS_NOT_FOUND)->will([$response, 'reveal']); + $this->routes + ->route( + '/foo', + $preparedMiddleware, + ['GET', 'POST'], + null + ) + ->willReturn($route); - $app->run($request, $response->reveal()); - } - - public function testCallingGetContainerReturnsComposedInstance() - { - $container = $this->prophesize(ContainerInterface::class); - $app = new Application($this->router->reveal(), $container->reveal()); - $this->assertSame($container->reveal(), $app->getContainer()); - } - - public function testCallingGetContainerWhenNoContainerComposedWillRaiseException() - { - $app = new Application($this->router->reveal()); - $this->expectException(RuntimeException::class); - $app->getContainer(); + $this->assertSame($route, $this->app->route('/foo', $middleware, ['GET', 'POST'])); } /** - * @group 64 + * @dataProvider validMiddleware + * @param string|array|callable|MiddlewareInterface $middleware */ - public function testCanTriggerPipingOfRouteMiddleware() + public function testRouteAcceptsPathMiddlewareMethodsAndName($middleware) { - $app = $this->getApp(); - $app->pipeRoutingMiddleware(); + $preparedMiddleware = $this->createMockMiddleware(); - $r = new ReflectionProperty($app, 'pipeline'); - $r->setAccessible(true); - $pipeline = $r->getValue($app); + $this->factory + ->prepare($middleware) + ->willReturn($preparedMiddleware); - $this->assertCount(1, $pipeline); + $route = $this->prophesize(Route::class)->reveal(); - $route = $pipeline->dequeue(); - $this->assertInstanceOf(StratigilityRoute::class, $route); - $this->assertInstanceOf(RouteMiddleware::class, $route->handler); - $this->assertEquals('/', $route->path); + $this->routes + ->route( + '/foo', + $preparedMiddleware, + ['GET', 'POST'], + 'foo' + ) + ->willReturn($route); + + $this->assertSame($route, $this->app->route('/foo', $middleware, ['GET', 'POST'], 'foo')); } - public function testCanTriggerPipingOfDispatchMiddleware() + public function requestMethodsWithValidMiddleware() : iterable { - $app = $this->getApp(); - $app->pipeDispatchMiddleware(); - - $r = new ReflectionProperty($app, 'pipeline'); - $r->setAccessible(true); - $pipeline = $r->getValue($app); - - $this->assertCount(1, $pipeline); - - $route = $pipeline->dequeue(); - $this->assertInstanceOf(StratigilityRoute::class, $route); - $this->assertInstanceOf(DispatchMiddleware::class, $route->handler); - $this->assertEquals('/', $route->path); + foreach (['get', 'post', 'put', 'patch', 'delete'] as $method) { + foreach ($this->validMiddleware() as $key => $data) { + array_unshift($data, $method); + $name = sprintf('%s-%s', $method, $key); + yield $name => $data; + } + } } /** - * @group lazy-piping + * @dataProvider requestMethodsWithValidMiddleware + * @param string|array|callable|MiddlewareInterface $middleware */ - public function testPipingAllowsPassingMiddlewareServiceNameAsSoleArgument() + public function testSpecificRouteMethodsCanAcceptOnlyPathAndMiddleware(string $method, $middleware) { - $middleware = new TestAsset\InteropMiddleware(); + $preparedMiddleware = $this->createMockMiddleware(); - $container = $this->mockContainerInterface(); - $this->injectServiceInContainer($container, 'foo', $middleware); + $this->factory + ->prepare($middleware) + ->willReturn($preparedMiddleware); - $app = new Application($this->router->reveal(), $container->reveal()); - $app->pipe('foo'); + $route = $this->prophesize(Route::class)->reveal(); - $r = new ReflectionProperty($app, 'pipeline'); - $r->setAccessible(true); - $pipeline = $r->getValue($app); + $this->routes + ->route( + '/foo', + $preparedMiddleware, + [strtoupper($method)], + null + ) + ->willReturn($route); - $route = $pipeline->dequeue(); - $this->assertInstanceOf(StratigilityRoute::class, $route); - $handler = $route->handler; - $this->assertInstanceOf(Middleware\LazyLoadingMiddleware::class, $handler); - $this->assertAttributeEquals('foo', 'middlewareName', $handler); + $this->assertSame($route, $this->app->{$method}('/foo', $middleware)); } /** - * @group lazy-piping + * @dataProvider requestMethodsWithValidMiddleware + * @param string|array|callable|MiddlewareInterface $middleware */ - public function testAllowsPipingMiddlewareAsServiceNameWithPath() + public function testSpecificRouteMethodsCanAcceptPathMiddlewareAndName(string $method, $middleware) { - $middleware = new TestAsset\InteropMiddleware(); + $preparedMiddleware = $this->createMockMiddleware(); - $container = $this->mockContainerInterface(); - $this->injectServiceInContainer($container, 'foo', $middleware); + $this->factory + ->prepare($middleware) + ->willReturn($preparedMiddleware); - $app = new Application($this->router->reveal(), $container->reveal()); - $app->pipe('/foo', 'foo'); + $route = $this->prophesize(Route::class)->reveal(); - $r = new ReflectionProperty($app, 'pipeline'); - $r->setAccessible(true); - $pipeline = $r->getValue($app); + $this->routes + ->route( + '/foo', + $preparedMiddleware, + [strtoupper($method)], + 'foo' + ) + ->willReturn($route); - $route = $pipeline->dequeue(); - $this->assertInstanceOf(StratigilityRoute::class, $route); - $handler = $route->handler; - $this->assertInstanceOf(PathMiddlewareDecorator::class, $handler); - - $r = new ReflectionProperty($handler, 'middleware'); - $r->setAccessible(true); - $handler = $r->getValue($handler); - - $this->assertAttributeEquals('foo', 'middlewareName', $handler); + $this->assertSame($route, $this->app->{$method}('/foo', $middleware, 'foo')); } /** - * @group lazy-piping + * @dataProvider validMiddleware + * @param string|array|callable|MiddlewareInterface $middleware */ - public function testPipingNotInvokableMiddlewareRaisesExceptionWhenInvokingRoute() + public function testAnyMethodPassesNullForMethodWhenNoNamePresent($middleware) { - $middleware = 'not callable'; - - $container = $this->mockContainerInterface(); - $this->injectServiceInContainer($container, 'foo', $middleware); - - $app = new Application($this->router->reveal(), $container->reveal()); - $app->pipe('/foo', 'foo'); + $preparedMiddleware = $this->createMockMiddleware(); - $r = new ReflectionProperty($app, 'pipeline'); - $r->setAccessible(true); - $pipeline = $r->getValue($app); + $this->factory + ->prepare($middleware) + ->willReturn($preparedMiddleware); - $route = $pipeline->dequeue(); - $this->assertInstanceOf(StratigilityRoute::class, $route); - $handler = $route->handler; + $route = $this->prophesize(Route::class)->reveal(); - $request = new ServerRequest([], [], '/foo', RequestMethod::METHOD_GET); - - $delegate = $this->prophesize(DelegateInterface::class)->reveal(); - - $this->expectException(InvalidMiddlewareException::class); - $handler->process($request, $delegate); - } - - public function invalidRequestExceptions() - { - return [ - 'invalid file' => [ - InvalidArgumentException::class, - 'Invalid value in files specification', - ], - 'invalid protocol version' => [ - UnexpectedValueException::class, - 'Unrecognized protocol version (foo-bar)', - ], - ]; - } - - /** - * @runInSeparateProcess - * @preserveGlobalState disabled - * - * @dataProvider invalidRequestExceptions - * - * @param string $expectedException - * @param string $message - */ - public function testRunReturnsResponseWithBadRequestStatusWhenServerRequestFactoryRaisesException( - $expectedException, - $message - ) { - // try/catch is necessary in the case that the test fails. - // Assertion exceptions raised inside prophecy expectations normally - // are fine, but in the context of runInSeparateProcess, these - // lead to closure serialization errors. try/catch allows us to - // catch those and provide failure assertions. - try { - Mockery::mock('alias:' . ServerRequestFactory::class) - ->shouldReceive('fromGlobals') - ->withNoArgs() - ->andThrow($expectedException, $message) - ->once() - ->getMock(); - - $emitter = $this->prophesize(EmitterInterface::class); - $emitter->emit(Argument::that(function ($response) { - $this->assertInstanceOf(ResponseInterface::class, $response, 'Emitter did not receive a response'); - $this->assertEquals(StatusCode::STATUS_BAD_REQUEST, $response->getStatusCode()); - return true; - }))->shouldBeCalled(); - - $app = new Application($this->router->reveal(), null, null, $emitter->reveal()); - - $app->run(); - } catch (\Throwable $e) { - $this->fail($e->getMessage()); - } catch (\Exception $e) { - $this->fail($e->getMessage()); - } - } + $this->routes + ->route( + '/foo', + $preparedMiddleware, + null, + null + ) + ->willReturn($route); - public function testRetrieveRegisteredRoutes() - { - $route = new Route('/foo', $this->noopMiddleware); - $this->router->addRoute($route)->shouldBeCalled(); - $app = $this->getApp(); - $test = $app->route($route); - $this->assertSame($route, $test); - $routes = $app->getRoutes(); - $this->assertCount(1, $routes); - $this->assertInstanceOf(Route::class, $routes[0]); + $this->assertSame($route, $this->app->any('/foo', $middleware)); } /** - * This test verifies that if the ErrorResponseGenerator service is available, - * it will be used to generate a response related to exceptions raised when - * creating the server request. - * - * @runInSeparateProcess - * @preserveGlobalState disabled - * - * @dataProvider invalidRequestExceptions - * - * @param string $expectedException - * @param string $message + * @dataProvider validMiddleware + * @param string|array|callable|MiddlewareInterface $middleware */ - public function testRunReturnsResponseGeneratedByErrorResponseGeneratorWhenServerRequestFactoryRaisesException( - $expectedException, - $message - ) { - // try/catch is necessary in the case that the test fails. - // Assertion exceptions raised inside prophecy expectations normally - // are fine, but in the context of runInSeparateProcess, these - // lead to closure serialization errors. try/catch allows us to - // catch those and provide failure assertions. - try { - $generator = $this->prophesize(Middleware\ErrorResponseGenerator::class); - $generator - ->__invoke( - Argument::type($expectedException), - Argument::type(ServerRequestInterface::class), - Argument::type(ResponseInterface::class) - )->will(function ($args) { - return $args[2]; - }); - - $container = $this->mockContainerInterface(); - $this->injectServiceInContainer($container, Middleware\ErrorResponseGenerator::class, $generator); - - Mockery::mock('alias:' . ServerRequestFactory::class) - ->shouldReceive('fromGlobals') - ->withNoArgs() - ->andThrow($expectedException, $message) - ->once() - ->getMock(); - - $expectedResponse = $this->prophesize(ResponseInterface::class)->reveal(); - - $emitter = $this->prophesize(EmitterInterface::class); - $emitter->emit(Argument::that(function ($response) use ($expectedResponse) { - $this->assertSame($expectedResponse, $response, 'Unexpected response provided to emitter'); - return true; - }))->shouldBeCalled(); - - $app = new Application($this->router->reveal(), $container->reveal(), null, $emitter->reveal()); - $app->setResponsePrototype($expectedResponse); - - $app->run(); - } catch (\Throwable $e) { - $this->fail(sprintf("(%d) %s:\n%s", $e->getCode(), $e->getMessage(), $e->getTraceAsString())); - } catch (\Exception $e) { - $this->fail(sprintf("(%d) %s:\n%s", $e->getCode(), $e->getMessage(), $e->getTraceAsString())); - } - } - - public function testGetDefaultDelegateWillPullFromContainerIfServiceRegistered() + public function testAnyMethodPassesNullForMethodWhenAllArgumentsPresent($middleware) { - $delegate = $this->prophesize(DelegateInterface::class)->reveal(); - $container = $this->mockContainerInterface(); - $this->injectServiceInContainer($container, 'Zend\Expressive\Delegate\DefaultDelegate', $delegate); + $preparedMiddleware = $this->createMockMiddleware(); - $app = new Application($this->router->reveal(), $container->reveal()); + $this->factory + ->prepare($middleware) + ->willReturn($preparedMiddleware); - $test = $app->getDefaultDelegate(); + $route = $this->prophesize(Route::class)->reveal(); - $this->assertSame($delegate, $test); - } + $this->routes + ->route( + '/foo', + $preparedMiddleware, + null, + 'foo' + ) + ->willReturn($route); - public function testWillCreateAndConsumeNotFoundDelegateFactoryToCreateDelegateIfNoDelegateInContainer() - { - $container = $this->mockContainerInterface(); - $container->has('Zend\Expressive\Delegate\DefaultDelegate')->willReturn(false); - $container->has(TemplateRendererInterface::class)->willReturn(false); - $app = new Application($this->router->reveal(), $container->reveal()); - - $delegate = $app->getDefaultDelegate(); - - $this->assertInstanceOf(Delegate\NotFoundDelegate::class, $delegate); - - $r = new ReflectionProperty($app, 'responsePrototype'); - $r->setAccessible(true); - $appResponsePrototype = $r->getValue($app); - - $this->assertAttributeNotSame($appResponsePrototype, 'responsePrototype', $delegate); - $this->assertAttributeEmpty('renderer', $delegate); + $this->assertSame($route, $this->app->any('/foo', $middleware, 'foo')); } - public function testWillUseConfiguredTemplateRendererWhenCreatingDelegateFromNotFoundDelegateFactory() + public function testGetRoutesProxiesToRouteMiddleware() { - $container = $this->mockContainerInterface(); - $container->has('Zend\Expressive\Delegate\DefaultDelegate')->willReturn(false); - - $renderer = $this->prophesize(TemplateRendererInterface::class)->reveal(); - $this->injectServiceInContainer($container, TemplateRendererInterface::class, $renderer); - - $app = new Application($this->router->reveal(), $container->reveal()); - - $delegate = $app->getDefaultDelegate(); - - $this->assertInstanceOf(Delegate\NotFoundDelegate::class, $delegate); - - $r = new ReflectionProperty($app, 'responsePrototype'); - $r->setAccessible(true); - $appResponsePrototype = $r->getValue($app); - - $this->assertAttributeNotSame($appResponsePrototype, 'responsePrototype', $delegate); - $this->assertAttributeSame($renderer, 'renderer', $delegate); - } - - public function testAllowsNestedMiddlewarePipelines() - { - $app = $this->getApp(); - $counter = function (ServerRequestInterface $request, DelegateInterface $delegate) { - $count = $request->getAttribute('count', 0); - $request = $request->withAttribute('count', $count + 1); - - return $delegate->process($request); - }; - - $app->pipe([ - // First level - $counter, - [ - // Second level - $counter, - $counter - ], - [ - [ - // Third level - $counter, - $counter - ] - ] - ]); - - $request = new ServerRequest(); - $response = new Response(); - $delegate = $this->prophesize(DelegateInterface::class); - - $delegate->process($request->withAttribute('count', 5)) - ->shouldBeCalled() - ->willReturn($response); - - $this->assertSame($response, $app->process($request, $delegate->reveal())); - } - - public function testPreparingArrayWithPairOfObjectAndStringMiddlewaresShouldNotBeTreatedAsCallable() - { - $first = $this->prophesize(MiddlewareInterface::class)->reveal(); - $second = TestAsset\CallableInteropMiddleware::class; - $queue = [$first, $second]; - - $router = $this->router->reveal(); - $response = $this->prophesize(ResponseInterface::class)->reveal(); - - $app = $this->getApp(); - $r = new ReflectionMethod($app, 'prepareMiddleware'); - $r->setAccessible(true); - - $middleware = $r->invoke($app, $queue, $router, $response); - - $this->assertInstanceOf(MiddlewarePipe::class, $middleware); + $route = $this->prophesize(Route::class)->reveal(); + $this->routes->getRoutes()->willReturn([$route]); - $r = new ReflectionProperty($middleware, 'pipeline'); - $r->setAccessible(true); - $this->assertCount(2, $r->getValue($middleware)); + $this->assertSame([$route], $this->app->getRoutes()); } } diff --git a/test/ConfigProviderTest.php b/test/ConfigProviderTest.php index eeb4338d..861b1f2c 100644 --- a/test/ConfigProviderTest.php +++ b/test/ConfigProviderTest.php @@ -1,26 +1,40 @@ provider->getDependencies(); $aliases = $config['aliases']; - $this->assertArrayHasKey(Middleware\DispatchMiddleware::class, $aliases); - $this->assertArrayHasKey(Middleware\ImplicitHeadMiddleware::class, $aliases); - $this->assertArrayHasKey(Middleware\ImplicitOptionsMiddleware::class, $aliases); - $this->assertArrayHasKey(Middleware\RouteMiddleware::class, $aliases); - $this->assertArrayHasKey(NotFoundDelegate::class, $aliases); - $this->assertArrayHasKey('Zend\Expressive\Delegate\DefaultDelegate', $aliases); - } - - public function testProviderDefinesExpectedInvokableServices() - { - $config = $this->provider->getDependencies(); - $invokables = $config['invokables']; - $this->assertArrayHasKey(RouterMiddleware\DispatchMiddleware::class, $invokables); + $this->assertArrayHasKey(DEFAULT_DELEGATE, $aliases); + $this->assertArrayHasKey(DISPATCH_MIDDLEWARE, $aliases); + $this->assertArrayHasKey(IMPLICIT_HEAD_MIDDLEWARE, $aliases); + $this->assertArrayHasKey(IMPLICIT_OPTIONS_MIDDLEWARE, $aliases); + $this->assertArrayHasKey(NOT_FOUND_MIDDLEWARE, $aliases); + $this->assertArrayHasKey(ROUTE_MIDDLEWARE, $aliases); } public function testProviderDefinesExpectedFactoryServices() @@ -58,22 +65,67 @@ public function testProviderDefinesExpectedFactoryServices() $this->assertArrayHasKey(Application::class, $factories); $this->assertArrayHasKey(ErrorHandler::class, $factories); $this->assertArrayHasKey(Middleware\ErrorResponseGenerator::class, $factories); - $this->assertArrayHasKey(Middleware\NotFoundHandler::class, $factories); $this->assertArrayHasKey(NotFoundHandler::class, $factories); $this->assertArrayHasKey(ResponseInterface::class, $factories); $this->assertArrayHasKey(StreamInterface::class, $factories); - $this->assertArrayHasKey(RouterMiddleware\ImplicitHeadMiddleware::class, $factories); - $this->assertArrayHasKey(RouterMiddleware\ImplicitOptionsMiddleware::class, $factories); - $this->assertArrayHasKey(RouterMiddleware\RouteMiddleware::class, $factories); } public function testInvocationReturnsArrayWithDependencies() { - $config = $this->provider->__invoke(); + $config = ($this->provider)(); $this->assertInternalType('array', $config); $this->assertArrayHasKey('dependencies', $config); $this->assertArrayHasKey('aliases', $config['dependencies']); - $this->assertArrayHasKey('invokables', $config['dependencies']); $this->assertArrayHasKey('factories', $config['dependencies']); } + + public function testServicesDefinedInConfigProvider() + { + $config = ($this->provider)(); + + $json = json_decode( + file_get_contents(__DIR__ . '/../composer.lock'), + true + ); + foreach ($json['packages'] as $package) { + if (isset($package['extra']['zf']['config-provider'])) { + $configProvider = new $package['extra']['zf']['config-provider'](); + $config = array_merge_recursive($config, $configProvider()); + } + } + + $routerInterface = $this->prophesize(RouterInterface::class)->reveal(); + $config['dependencies']['services'][RouterInterface::class] = $routerInterface; + $container = $this->getContainer($config['dependencies']); + + $dependencies = $this->provider->getDependencies(); + foreach ($dependencies['factories'] as $name => $factory) { + $this->assertTrue($container->has($name), sprintf('Container does not contain service %s', $name)); + $this->assertInternalType( + 'object', + $container->get($name), + sprintf('Cannot get service %s from container using factory %s', $name, $factory) + ); + } + + foreach ($dependencies['aliases'] as $alias => $dependency) { + $this->assertTrue( + $container->has($alias), + sprintf('Container does not contain service with alias %s', $alias) + ); + $this->assertInternalType( + 'object', + $container->get($alias), + sprintf('Cannot get service %s using alias %s', $dependency, $alias) + ); + } + } + + private function getContainer(array $dependencies) : ServiceManager + { + $container = new ServiceManager(); + (new Config($dependencies))->configureServiceManager($container); + + return $container; + } } diff --git a/test/Container/ApplicationConfigInjectionDelegatorTest.php b/test/Container/ApplicationConfigInjectionDelegatorTest.php index 939d2745..7e398062 100644 --- a/test/Container/ApplicationConfigInjectionDelegatorTest.php +++ b/test/Container/ApplicationConfigInjectionDelegatorTest.php @@ -1,18 +1,19 @@ dispatchMiddleware = $this->prophesize(DispatchMiddleware::class)->reveal(); + $this->methodNotAllowedMiddleware = $this->prophesize(MethodNotAllowedMiddleware::class)->reveal(); } public function createApplication() { + $container = new MiddlewareContainer($this->container->reveal()); + $factory = new MiddlewareFactory($container); + $pipeline = new MiddlewarePipe(); + $runner = $this->prophesize(RequestHandlerRunner::class)->reveal(); return new Application( - $this->router->reveal(), - $this->container->reveal() + $factory, + $pipeline, + $this->routeMiddleware, + $runner ); } @@ -69,7 +83,11 @@ public function getQueueFromApplicationPipeline(Application $app) { $r = new ReflectionProperty($app, 'pipeline'); $r->setAccessible(true); - return $r->getValue($app); + $pipeline = $r->getValue($app); + + $r = new ReflectionProperty($pipeline, 'pipeline'); + $r->setAccessible(true); + return $r->getValue($pipeline); } public static function assertRoute($spec, array $routes) @@ -160,7 +178,26 @@ public static function assertDispatchMiddleware(MiddlewareInterface $middleware) ); } - public function injectableMiddleware() + public static function assertMethodNotAllowedMiddleware(MiddlewareInterface $middleware) + { + if ($middleware instanceof MethodNotAllowedMiddleware) { + Assert::assertInstanceOf(MethodNotAllowedMiddleware::class, $middleware); + return; + } + + if (! $middleware instanceof Middleware\LazyLoadingMiddleware) { + Assert::fail('Middleware is not an instance of MethodNotAllowedMiddleware'); + } + + Assert::assertAttributeSame( + MethodNotAllowedMiddleware::class, + 'middlewareName', + $middleware, + 'Middleware is not an instance of MethodNotAllowedMiddleware' + ); + } + + public function callableMiddlewares() { return [ [CallableInteropMiddleware::class], @@ -185,7 +222,7 @@ public function testInvocationAsDelegatorFactoryRaisesExceptionIfCallbackIsNotAn } /** - * @dataProvider injectableMiddleware + * @dataProvider callableMiddlewares * * @param callable|array|string $middleware */ @@ -243,7 +280,7 @@ public function testNoRoutesAreAddedIfSpecDoesNotProvidePathOrMiddleware() public function testInjectPipelineFromConfigHonorsPriorityOrderWhenAttachingMiddleware() { - $middleware = new InteropMiddleware(); + $middleware = new TestAsset\InteropMiddleware(); $pipeline1 = [['middleware' => clone $middleware, 'priority' => 1]]; $pipeline2 = [['middleware' => clone $middleware, 'priority' => 100]]; @@ -258,14 +295,14 @@ public function testInjectPipelineFromConfigHonorsPriorityOrderWhenAttachingMidd $pipeline = $this->getQueueFromApplicationPipeline($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); + $this->assertSame($pipeline2[0]['middleware'], $pipeline->dequeue()); + $this->assertSame($pipeline1[0]['middleware'], $pipeline->dequeue()); + $this->assertSame($pipeline3[0]['middleware'], $pipeline->dequeue()); } public function testMiddlewareWithoutPriorityIsGivenDefaultPriorityAndRegisteredInOrderReceived() { - $middleware = new InteropMiddleware(); + $middleware = new TestAsset\InteropMiddleware(); $pipeline1 = [['middleware' => clone $middleware]]; $pipeline2 = [['middleware' => clone $middleware]]; @@ -280,9 +317,9 @@ public function testMiddlewareWithoutPriorityIsGivenDefaultPriorityAndRegistered $pipeline = $this->getQueueFromApplicationPipeline($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); + $this->assertSame($pipeline3[0]['middleware'], $pipeline->dequeue()); + $this->assertSame($pipeline1[0]['middleware'], $pipeline->dequeue()); + $this->assertSame($pipeline2[0]['middleware'], $pipeline->dequeue()); } public function testInjectPipelineFromConfigWithEmptyConfigDoesNothing() @@ -308,7 +345,7 @@ public function testInjectRoutesFromConfigRaisesExceptionIfAllowedMethodsIsInval 'routes' => [ [ 'path' => '/', - 'middleware' => new InteropMiddleware(), + 'middleware' => new TestAsset\InteropMiddleware(), 'allowed_methods' => 'not-valid', ], ], @@ -327,7 +364,7 @@ public function testInjectRoutesFromConfigRaisesExceptionIfOptionsIsNotAnArray() 'routes' => [ [ 'path' => '/', - 'middleware' => new InteropMiddleware(), + 'middleware' => new TestAsset\InteropMiddleware(), 'allowed_methods' => ['GET'], 'options' => 'invalid', ], @@ -347,7 +384,7 @@ public function testInjectRoutesFromConfigCanProvideRouteOptions() 'routes' => [ [ 'path' => '/', - 'middleware' => new InteropMiddleware(), + 'middleware' => new TestAsset\InteropMiddleware(), 'allowed_methods' => ['GET'], 'options' => [ 'foo' => 'bar', @@ -371,7 +408,7 @@ public function testInjectRoutesFromConfigWillSkipSpecsThatOmitPath() $config = [ 'routes' => [ [ - 'middleware' => new InteropMiddleware(), + 'middleware' => new TestAsset\InteropMiddleware(), 'allowed_methods' => ['GET'], 'options' => [ 'foo' => 'bar', diff --git a/test/Container/ApplicationFactoryTest.php b/test/Container/ApplicationFactoryTest.php index 495fb326..d7ec0d48 100644 --- a/test/Container/ApplicationFactoryTest.php +++ b/test/Container/ApplicationFactoryTest.php @@ -1,614 +1,47 @@ container = $this->mockContainerInterface(); - $this->factory = new ApplicationFactory(); - - $this->router = $this->prophesize(RouterInterface::class); - $this->emitter = $this->prophesize(EmitterInterface::class); - $this->delegate = $this->prophesize(DelegateInterface::class)->reveal(); - - $this->injectServiceInContainer($this->container, RouterInterface::class, $this->router->reveal()); - $this->injectServiceInContainer($this->container, EmitterInterface::class, $this->emitter->reveal()); - $this->injectServiceInContainer($this->container, 'Zend\Expressive\Delegate\DefaultDelegate', $this->delegate); - - $this->disregardDeprecationNotices(); - } - - public function tearDown() - { - restore_error_handler(); - } - - public function disregardDeprecationNotices() - { - set_error_handler(function ($errno, $errstr) { - if (strstr($errstr, 'pipe() the middleware directly')) { - return true; - } - if (strstr($errstr, 'doublePassMiddleware()')) { - return true; - } - return false; - }, E_USER_DEPRECATED); - } - - public static function assertRoute($spec, array $routes) - { - Assert::assertThat( - array_reduce($routes, function ($found, $route) use ($spec) { - if ($found) { - return $found; - } - - if ($route->getPath() !== $spec['path']) { - return false; - } - - // We're just testing that middleware is present; since it may - // be decorated, this might fail otherwise. - if (! $route->getMiddleware()) { - return false; - } - - if (isset($spec['allowed_methods'])) { - if ($route->getAllowedMethods() !== $spec['allowed_methods']) { - return false; - } - } - - if (! isset($spec['allowed_methods'])) { - if ($route->getAllowedMethods() !== Route::HTTP_METHOD_ANY) { - return false; - } - } - - return true; - }, false), - Assert::isTrue(), - 'Route matching specification not found' - ); - } - - public function getRouterFromApplication(Application $app) - { - $r = new ReflectionProperty($app, 'router'); - $r->setAccessible(true); - return $r->getValue($app); - } - - public function testFactoryWillPullAllReplaceableDependenciesFromContainerWhenPresent() + public function testFactoryProducesAnApplication() { - $app = $this->factory->__invoke($this->container->reveal()); - $this->assertInstanceOf(Application::class, $app); - $test = $this->getRouterFromApplication($app); - $this->assertSame($this->router->reveal(), $test); - $this->assertSame($this->container->reveal(), $app->getContainer()); - $this->assertSame($this->emitter->reveal(), $app->getEmitter()); - $this->assertSame($this->delegate, $app->getDefaultDelegate()); - } - - public function injectableMiddleware() - { - $middlewareTypes = [ - [CallableInteropMiddleware::class], - [ - function ($request, DelegateInterface $delegate) { - }, - ], - [[CallableInteropMiddleware::class, 'staticallyCallableMiddleware']], - ]; - - $configTypes = [ - 'array' => null, - 'array-object' => ArrayObject::class, - ]; - - foreach ($configTypes as $configType => $config) { - foreach ($middlewareTypes as $middlewareType => $middleware) { - $name = sprintf('%s-%s', $configType, $middlewareType); - yield $name => [$middleware, $config]; - } - } - } - - /** - * @dataProvider injectableMiddleware - * - * @param callable|array|string $middleware - * @param string $configType - */ - public function testFactorySetsUpRoutesFromConfig($middleware, $configType) - { - $pingMiddleware = $this->prophesize(MiddlewareInterface::class)->reveal(); - $config = [ - 'routes' => [ - [ - 'path' => '/', - 'middleware' => $middleware, - 'allowed_methods' => ['GET'], - ], - [ - 'path' => '/ping', - 'middleware' => $pingMiddleware, - 'allowed_methods' => ['GET'], - ], - ], - ]; + $middlewareFactory = $this->prophesize(MiddlewareFactory::class)->reveal(); + $pipeline = $this->prophesize(MiddlewarePipeInterface::class)->reveal(); + $routeMiddleware = $this->prophesize(PathBasedRoutingMiddleware::class)->reveal(); + $runner = $this->prophesize(RequestHandlerRunner::class)->reveal(); - $config = $configType ? new $configType($config) : $config; + $container = $this->prophesize(ContainerInterface::class); + $container->get(MiddlewareFactory::class)->willReturn($middlewareFactory); + $container->get(ApplicationPipeline::class)->willReturn($pipeline); + $container->get(PathBasedRoutingMiddleware::class)->willReturn($routeMiddleware); + $container->get(RequestHandlerRunner::class)->willReturn($runner); - $this->injectServiceInContainer($this->container, 'config', $config); - - $app = $this->factory->__invoke($this->container->reveal()); - - $routes = $app->getRoutes(); - - foreach ($config['routes'] as $route) { - $this->assertRoute($route, $routes); - } - } - - public function testWillUseSaneDefaultsForOptionalServices() - { - $container = $this->mockContainerInterface(); $factory = new ApplicationFactory(); - $app = $factory->__invoke($container->reveal()); - $this->assertInstanceOf(Application::class, $app); - $router = $this->getRouterFromApplication($app); - $this->assertInstanceOf(FastRouteRouter::class, $router); - $this->assertSame($container->reveal(), $app->getContainer()); - $this->assertInstanceOf(EmitterStack::class, $app->getEmitter()); - $this->assertCount(1, $app->getEmitter()); - $this->assertInstanceOf(SapiEmitter::class, $app->getEmitter()->pop()); - $this->assertInstanceOf(NotFoundDelegate::class, $app->getDefaultDelegate()); - } - - public function configTypes() - { - return [ - 'array' => [null], - 'array-object' => [ArrayObject::class], - ]; - } - - /** - * @dataProvider configTypes - * @group piping - * - * @param null|string $configType - */ - public function testMiddlewareIsNotAddedIfSpecIsInvalid($configType) - { - $config = [ - 'middleware_pipeline' => [ - ['foo' => 'bar'], - ['path' => '/foo'], - ], - ]; - - $config = $configType ? new $configType($config) : $config; - - $this->injectServiceInContainer($this->container, 'config', $config); - - $this->expectException(ExpressiveException\InvalidArgumentException::class); - $this->expectExceptionMessage('pipeline'); - $this->factory->__invoke($this->container->reveal()); - } - - /** - * @dataProvider configTypes - * - * @param null|string $configType - */ - public function testCanSpecifyRouteViaConfigurationWithNoMethods($configType) - { - $config = [ - 'routes' => [ - [ - 'path' => '/', - 'middleware' => $this->prophesize(MiddlewareInterface::class)->reveal(), - ], - ], - ]; - - $config = $configType ? new $configType($config) : $config; - - $this->injectServiceInContainer($this->container, 'config', $config); - - $app = $this->factory->__invoke($this->container->reveal()); - - $routes = $app->getRoutes(); - - foreach ($config['routes'] as $route) { - $this->assertRoute($route, $routes); - } - } - - /** - * @dataProvider configTypes - * - * @param null|string $configType - */ - public function testCanSpecifyRouteOptionsViaConfiguration($configType) - { - $expected = [ - 'values' => [ - 'foo' => 'bar', - ], - 'tokens' => [ - 'bar' => 'foo', - ], - ]; - $config = [ - 'routes' => [ - [ - 'path' => '/', - 'middleware' => $this->prophesize(MiddlewareInterface::class)->reveal(), - 'name' => 'home', - 'allowed_methods' => ['GET'], - 'options' => $expected, - ], - ], - ]; - - $config = $configType ? new $configType($config) : $config; - - $this->injectServiceInContainer($this->container, 'config', $config); - - $app = $this->factory->__invoke($this->container->reveal()); - - $routes = $app->getRoutes(); - $route = array_shift($routes); - - $this->assertInstanceOf(Route::class, $route); - $this->assertEquals($expected, $route->getOptions()); - } - - /** - * @dataProvider configTypes - * - * @param null|string $configType - */ - public function testExceptionIsRaisedInCaseOfInvalidRouteMethodsConfiguration($configType) - { - $config = [ - 'routes' => [ - [ - 'path' => '/', - 'middleware' => 'HelloWorld', - 'allowed_methods' => 'invalid', - ], - ], - ]; - - $config = $configType ? new $configType($config) : $config; - - $this->injectServiceInContainer($this->container, 'config', $config); - - $this->expectException(ExpressiveException\InvalidArgumentException::class); - $this->expectExceptionMessage('route must be in form of an array; received "string"'); - $this->factory->__invoke($this->container->reveal()); - } - - /** - * @dataProvider configTypes - * - * @param null|string $configType - */ - public function testExceptionIsRaisedInCaseOfInvalidRouteOptionsConfiguration($configType) - { - $middleware = $this->prophesize(MiddlewareInterface::class)->reveal(); - $config = [ - 'routes' => [ - [ - 'path' => '/', - 'middleware' => $middleware, - 'options' => 'invalid', - ], - ], - ]; - - $config = $configType ? new $configType($config) : $config; - - $this->injectServiceInContainer($this->container, 'config', $config); - - $this->expectException(ExpressiveException\InvalidArgumentException::class); - $this->expectExceptionMessage('options must be an array; received "string"'); - $this->factory->__invoke($this->container->reveal()); - } - - /** - * @dataProvider configTypes - * - * @param null|string $configType - */ - public function testWillCreatePipelineBasedOnMiddlewareConfiguration($configType) - { - $api = new InteropMiddleware(); - - $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]; - $config = $configType ? new $configType($config) : $config; - $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('/', $test->path); - $this->assertInstanceOf(PathMiddlewareDecorator::class, $test->handler); - $handler = $test->handler; - $this->assertAttributeSame('/api', 'prefix', $handler); - $this->assertAttributeSame($api, 'middleware', $handler); - - // Lazy middleware is not marshaled until invocation - $test = $pipeline->dequeue(); - $this->assertEquals('/', $test->path); - $this->assertInstanceOf(PathMiddlewareDecorator::class, $test->handler); - $handler = $test->handler; - $this->assertAttributeSame('/dynamic-path', 'prefix', $handler); - $this->assertAttributeNotSame($dynamicPath, 'middleware', $handler); - $this->assertAttributeInstanceOf(LazyLoadingMiddleware::class, 'middleware', $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(LazyLoadingMiddleware::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(LazyLoadingMiddleware::class, $test->handler); - - $test = $nestedPipeline->dequeue(); - $this->assertSame($pipelineLast, $test->handler); - } - - public function configWithRoutesButNoPipeline() - { - $middleware = function ($request, $response, $next) { - }; - - $routes = [ - [ - 'path' => '/', - 'middleware' => clone $middleware, - 'allowed_methods' => ['GET'], - ], - ]; - - $configs = [ - 'no-pipeline-defined' => ['routes' => $routes], - 'empty-pipeline' => ['middleware_pipeline' => [], 'routes' => $routes], - 'null-pipeline' => ['middleware_pipeline' => null, 'routes' => $routes], - ]; - - $configTypes = [ - 'array' => null, - 'array-object' => ArrayObject::class, - ]; - - foreach ($configTypes as $configName => $configType) { - foreach ($configs as $name => $config) { - $caseName = sprintf('%s-%s', $configName, $name); - yield $caseName => [$config, $configType]; - } - } - } - - /** - * @dataProvider configWithRoutesButNoPipeline - * - * @param array $config - * @param string|null $configType - */ - public function testProvidingRoutesAndNoPipelineImplicitlyRegistersRoutingAndDispatchMiddleware( - array $config, - $configType - ) { - $config = $configType ? new $configType($config) : $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->assertInstanceOf(RouteMiddleware::class, $test->handler); - - $test = $pipeline->dequeue(); - $this->assertEquals('/', $test->path); - $this->assertInstanceOf(DispatchMiddleware::class, $test->handler); - } - - /** - * @dataProvider configTypes - * @group programmatic - * - * @param null|string $configType - */ - public function testWillNotInjectConfiguredRoutesOrPipelineIfProgrammaticPipelineFlagEnabled($configType) - { - $api = new InteropMiddleware(); - - $dynamicPath = clone $api; - $noPath = clone $api; - $goodbye = clone $api; - $pipelineFirst = clone $api; - $hello = clone $api; - $pipelineLast = clone $api; - - $config = [ - 'middleware_pipeline' => [ - ['path' => '/api', 'middleware' => $api], - ['path' => '/dynamic-path', 'middleware' => 'DynamicPath'], - ['middleware' => $noPath], - ['middleware' => 'Goodbye'], - [ - 'middleware' => [ - $pipelineFirst, - 'Hello', - $pipelineLast, - ], - ], - ], - 'routes' => [ - [ - 'path' => '/', - 'middleware' => 'HelloWorld', - 'name' => 'home', - 'allowed_methods' => ['GET'], - 'options' => [], - ], - ], - 'zend-expressive' => [ - 'programmatic_pipeline' => true, - ], - ]; - - $config = $configType ? new $configType($config) : $config; - - $this->injectServiceInContainer($this->container, 'DynamicPath', $dynamicPath); - $this->injectServiceInContainer($this->container, 'Goodbye', $goodbye); - $this->injectServiceInContainer($this->container, 'Hello', $hello); - $this->injectServiceInContainer($this->container, 'config', $config); - - $app = $this->factory->__invoke($this->container->reveal()); - - $this->assertAttributeSame(false, 'routeMiddlewareIsRegistered', $app); - $this->assertAttributeSame(false, 'dispatchMiddlewareIsRegistered', $app); - - $r = new ReflectionProperty($app, 'pipeline'); - $r->setAccessible(true); - $pipeline = $r->getValue($app); - $this->assertCount(0, $pipeline, 'Pipeline contains entries and should not'); + $application = $factory($container->reveal()); - $routes = $app->getRoutes(); - $this->assertEmpty($routes, 'Routes exist, and should not'); + $this->assertInstanceOf(Application::class, $application); + $this->assertAttributeSame($middlewareFactory, 'factory', $application); + $this->assertAttributeSame($pipeline, 'pipeline', $application); + $this->assertAttributeSame($routeMiddleware, 'routes', $application); + $this->assertAttributeSame($runner, 'runner', $application); } } diff --git a/test/Container/EmitterFactoryTest.php b/test/Container/EmitterFactoryTest.php new file mode 100644 index 00000000..cb95b331 --- /dev/null +++ b/test/Container/EmitterFactoryTest.php @@ -0,0 +1,34 @@ +prophesize(ContainerInterface::class)->reveal(); + $factory = new EmitterFactory(); + + $emitter = $factory($container); + + $this->assertInstanceOf(EmitterStack::class, $emitter); + + $emitters = iterator_to_array($emitter); + $this->assertCount(1, $emitters); + + $emitter = array_shift($emitters); + $this->assertInstanceOf(SapiEmitter::class, $emitter); + } +} diff --git a/test/Container/ErrorHandlerFactoryTest.php b/test/Container/ErrorHandlerFactoryTest.php index a1ea5859..faa594a6 100644 --- a/test/Container/ErrorHandlerFactoryTest.php +++ b/test/Container/ErrorHandlerFactoryTest.php @@ -1,16 +1,21 @@ container = $this->prophesize(ContainerInterface::class); } + public function testFactoryFailsIfResponseServiceIsMissing() + { + $exception = new RuntimeException(); + $this->container->has(ErrorResponseGenerator::class)->willReturn(false); + $this->container->get(ErrorResponseGenerator::class)->shouldNotBeCalled(); + $this->container->get(ResponseInterface::class)->willThrow($exception); + + $factory = new ErrorHandlerFactory(); + + $this->expectException(RuntimeException::class); + $factory($this->container->reveal()); + } + + public function testFactoryFailsIfResponseServiceReturnsResponse() + { + $response = $this->prophesize(ResponseInterface::class)->reveal(); + $this->container->has(ErrorResponseGenerator::class)->willReturn(false); + $this->container->get(ErrorResponseGenerator::class)->shouldNotBeCalled(); + $this->container->get(ResponseInterface::class)->willReturn($response); + + $factory = new ErrorHandlerFactory(); + + $this->expectException(TypeError::class); + $factory($this->container->reveal()); + } + public function testFactoryCreatesHandlerWithStratigilityGeneratorIfNoGeneratorServiceAvailable() { $this->container->has(ErrorResponseGenerator::class)->willReturn(false); + $this->container->get(ErrorResponseGenerator::class)->shouldNotBeCalled(); + + $this->container->get(ResponseInterface::class)->willReturn(function () { + }); $factory = new ErrorHandlerFactory(); $handler = $factory($this->container->reveal()); $this->assertInstanceOf(ErrorHandler::class, $handler); - $this->assertAttributeInstanceOf(ResponseInterface::class, 'responsePrototype', $handler); + $this->assertAttributeInstanceOf(Closure::class, 'responseFactory', $handler); $this->assertAttributeInstanceOf(StratigilityGenerator::class, 'responseGenerator', $handler); } @@ -44,11 +79,14 @@ public function testFactoryCreatesHandlerWithGeneratorIfGeneratorServiceAvailabl $this->container->has(ErrorResponseGenerator::class)->willReturn(true); $this->container->get(ErrorResponseGenerator::class)->willReturn($generator); + $this->container->get(ResponseInterface::class)->willReturn(function () { + }); + $factory = new ErrorHandlerFactory(); $handler = $factory($this->container->reveal()); $this->assertInstanceOf(ErrorHandler::class, $handler); - $this->assertAttributeInstanceOf(ResponseInterface::class, 'responsePrototype', $handler); + $this->assertAttributeInstanceOf(Closure::class, 'responseFactory', $handler); $this->assertAttributeSame($generator, 'responseGenerator', $handler); } } diff --git a/test/Container/ErrorResponseGeneratorFactoryTest.php b/test/Container/ErrorResponseGeneratorFactoryTest.php index d5a3d9d8..9872c615 100644 --- a/test/Container/ErrorResponseGeneratorFactoryTest.php +++ b/test/Container/ErrorResponseGeneratorFactoryTest.php @@ -1,10 +1,12 @@ prophesize(ContainerInterface::class)->reveal(); + $factory = new MiddlewareContainerFactory(); + + $middlewareContainer = $factory($container); + + $this->assertInstanceOf(MiddlewareContainer::class, $middlewareContainer); + $this->assertAttributeSame($container, 'container', $middlewareContainer); + } +} diff --git a/test/Container/MiddlewareFactoryFactoryTest.php b/test/Container/MiddlewareFactoryFactoryTest.php new file mode 100644 index 00000000..481b004e --- /dev/null +++ b/test/Container/MiddlewareFactoryFactoryTest.php @@ -0,0 +1,34 @@ +prophesize(MiddlewareContainer::class)->reveal(); + + $container = $this->prophesize(ContainerInterface::class); + $container->get(MiddlewareContainer::class)->willReturn($middlewareContainer); + + $factory = new MiddlewareFactoryFactory(); + + $middlewareFactory = $factory($container->reveal()); + + $this->assertInstanceOf(MiddlewareFactory::class, $middlewareFactory); + $this->assertAttributeSame($middlewareContainer, 'container', $middlewareFactory); + } +} diff --git a/test/Container/NotFoundDelegateFactoryTest.php b/test/Container/NotFoundDelegateFactoryTest.php deleted file mode 100644 index 4667025f..00000000 --- a/test/Container/NotFoundDelegateFactoryTest.php +++ /dev/null @@ -1,79 +0,0 @@ -container = $this->prophesize(ContainerInterface::class); - } - - public function testFactoryCreatesInstanceWithoutRendererIfRendererServiceIsMissing() - { - $this->container->has('config')->willReturn(false); - $this->container->has(TemplateRendererInterface::class)->willReturn(false); - $factory = new NotFoundDelegateFactory(); - - $delegate = $factory($this->container->reveal()); - $this->assertInstanceOf(NotFoundDelegate::class, $delegate); - $this->assertAttributeInstanceOf(Response::class, 'responsePrototype', $delegate); - $this->assertAttributeEmpty('renderer', $delegate); - } - - public function testFactoryCreatesInstanceUsingRendererServiceWhenPresent() - { - $renderer = $this->prophesize(TemplateRendererInterface::class)->reveal(); - $this->container->has('config')->willReturn(false); - $this->container->has(TemplateRendererInterface::class)->willReturn(true); - $this->container->get(TemplateRendererInterface::class)->willReturn($renderer); - $factory = new NotFoundDelegateFactory(); - - $delegate = $factory($this->container->reveal()); - $this->assertAttributeSame($renderer, 'renderer', $delegate); - } - - public function testFactoryUsesConfigured404TemplateWhenPresent() - { - $config = [ - 'zend-expressive' => [ - 'error_handler' => [ - 'layout' => 'layout::error', - 'template_404' => 'foo::bar', - ], - ], - ]; - $this->container->has('config')->willReturn(true); - $this->container->get('config')->willReturn($config); - $this->container->has(TemplateRendererInterface::class)->willReturn(false); - $factory = new NotFoundDelegateFactory(); - - $delegate = $factory($this->container->reveal()); - $this->assertAttributeEquals( - $config['zend-expressive']['error_handler']['layout'], - 'layout', - $delegate - ); - $this->assertAttributeEquals( - $config['zend-expressive']['error_handler']['template_404'], - 'template', - $delegate - ); - } -} diff --git a/test/Container/NotFoundHandlerFactoryTest.php b/test/Container/NotFoundHandlerFactoryTest.php index 54580e29..63bb3d12 100644 --- a/test/Container/NotFoundHandlerFactoryTest.php +++ b/test/Container/NotFoundHandlerFactoryTest.php @@ -1,30 +1,88 @@ prophesize(NotFoundDelegate::class)->reveal(); - $container = $this->prophesize(ContainerInterface::class); - $container->get(NotFoundDelegate::class)->willReturn($delegate); - $factory = new NotFoundHandlerFactory(); + $this->response = $this->prophesize(ResponseInterface::class)->reveal(); + $this->container = $this->prophesize(ContainerInterface::class); + $this->container->get(ResponseInterface::class)->willReturn(function () { + return $this->response; + }); + } - $handler = $factory($container->reveal()); + public function testFactoryCreatesInstanceWithoutRendererIfRendererServiceIsMissing() + { + $this->container->has('config')->willReturn(false); + $this->container->has(TemplateRendererInterface::class)->willReturn(false); + $factory = new NotFoundHandlerFactory(); + $handler = $factory($this->container->reveal()); $this->assertInstanceOf(NotFoundHandler::class, $handler); - $this->assertAttributeSame($delegate, 'internalDelegate', $handler); + $this->assertAttributeInternalType('callable', 'responseFactory', $handler); + $this->assertAttributeEmpty('renderer', $handler); + } + + public function testFactoryCreatesInstanceUsingRendererServiceWhenPresent() + { + $renderer = $this->prophesize(TemplateRendererInterface::class)->reveal(); + $this->container->has('config')->willReturn(false); + $this->container->has(TemplateRendererInterface::class)->willReturn(true); + $this->container->get(TemplateRendererInterface::class)->willReturn($renderer); + $factory = new NotFoundHandlerFactory(); + + $handler = $factory($this->container->reveal()); + $this->assertAttributeSame($renderer, 'renderer', $handler); + } + + public function testFactoryUsesConfigured404TemplateWhenPresent() + { + $config = [ + 'zend-expressive' => [ + 'error_handler' => [ + 'layout' => 'layout::error', + 'template_404' => 'foo::bar', + ], + ], + ]; + $this->container->has('config')->willReturn(true); + $this->container->get('config')->willReturn($config); + $this->container->has(TemplateRendererInterface::class)->willReturn(false); + $factory = new NotFoundHandlerFactory(); + + $handler = $factory($this->container->reveal()); + $this->assertAttributeEquals( + $config['zend-expressive']['error_handler']['layout'], + 'layout', + $handler + ); + $this->assertAttributeEquals( + $config['zend-expressive']['error_handler']['template_404'], + 'template', + $handler + ); } } diff --git a/test/Container/RequestHandlerRunnerFactoryTest.php b/test/Container/RequestHandlerRunnerFactoryTest.php new file mode 100644 index 00000000..1db128f4 --- /dev/null +++ b/test/Container/RequestHandlerRunnerFactoryTest.php @@ -0,0 +1,92 @@ +prophesize(ContainerInterface::class); + $handler = $this->registerHandlerInContainer($container); + $emitter = $this->registerEmitterInContainer($container); + $serverRequestFactory = $this->registerServerRequestFactoryInContainer($container); + $errorGenerator = $this->registerServerRequestErrorResponseGeneratorInContainer($container); + + $factory = new RequestHandlerRunnerFactory(); + + $runner = $factory($container->reveal()); + + $this->assertInstanceOf(RequestHandlerRunner::class, $runner); + $this->assertAttributeSame($handler, 'handler', $runner); + $this->assertAttributeSame($emitter, 'emitter', $runner); + + $this->assertAttributeNotSame($serverRequestFactory, 'serverRequestFactory', $runner); + $this->assertAttributeNotSame($errorGenerator, 'serverRequestErrorResponseGenerator', $runner); + + $r = new ReflectionProperty($runner, 'serverRequestFactory'); + $r->setAccessible(true); + $toTest = $r->getValue($runner); + $this->assertSame($serverRequestFactory(), $toTest()); + + $r = new ReflectionProperty($runner, 'serverRequestErrorResponseGenerator'); + $r->setAccessible(true); + $toTest = $r->getValue($runner); + $e = new RuntimeException(); + $this->assertSame($errorGenerator($e), $toTest($e)); + } + + public function registerHandlerInContainer($container) : RequestHandlerInterface + { + $app = $this->prophesize(RequestHandlerInterface::class)->reveal(); + $container->get(ApplicationPipeline::class)->willReturn($app); + return $app; + } + + public function registerEmitterInContainer($container) : EmitterInterface + { + $emitter = $this->prophesize(EmitterInterface::class)->reveal(); + $container->get(EmitterInterface::class)->willReturn($emitter); + return $emitter; + } + + public function registerServerRequestFactoryInContainer($container) : callable + { + $request = $this->prophesize(ServerRequestInterface::class)->reveal(); + $factory = function () use ($request) { + return $request; + }; + $container->get(ServerRequestInterface::class)->willReturn($factory); + return $factory; + } + + public function registerServerRequestErrorResponseGeneratorInContainer($container) : callable + { + $response = $this->prophesize(ResponseInterface::class)->reveal(); + $generator = $this->prophesize(ServerRequestErrorResponseGenerator::class); + $generator->__invoke(Argument::type(Throwable::class))->willReturn($response); + $container->get(ServerRequestErrorResponseGenerator::class)->willReturn($generator->reveal()); + return $generator->reveal(); + } +} diff --git a/test/Container/ResponseFactoryFactoryTest.php b/test/Container/ResponseFactoryFactoryTest.php index 3e90ffd9..3802a300 100644 --- a/test/Container/ResponseFactoryFactoryTest.php +++ b/test/Container/ResponseFactoryFactoryTest.php @@ -1,10 +1,12 @@ container = $this->prophesize(ContainerInterface::class)->reveal(); + $this->factory = new ResponseFactoryFactory(); + + $this->autoloadFunctions = spl_autoload_functions(); + foreach ($this->autoloadFunctions as $autoloader) { + spl_autoload_unregister($autoloader); + } + } + + private function reloadAutoloaders() + { + foreach ($this->autoloadFunctions as $autoloader) { + spl_autoload_register($autoloader); + } + } + + public function testFactoryRaisesAnExceptionIfDiactorosIsNotLoaded() + { + $this->expectException(InvalidServiceException::class); + $this->expectExceptionMessage('zendframework/zend-diactoros'); + + try { + ($this->factory)($this->container); + } finally { + $this->reloadAutoloaders(); + } + } +} diff --git a/test/Container/ServerRequestErrorResponseGeneratorFactoryTest.php b/test/Container/ServerRequestErrorResponseGeneratorFactoryTest.php new file mode 100644 index 00000000..5b4d52dc --- /dev/null +++ b/test/Container/ServerRequestErrorResponseGeneratorFactoryTest.php @@ -0,0 +1,99 @@ +prophesize(ContainerInterface::class); + $container->has('config')->willReturn(false); + $container->get('config')->shouldNotBeCalled(); + $container->has(TemplateRendererInterface::class)->willReturn(false); + $container->get(TemplateRendererInterface::class)->shouldNotBeCalled(); + + $exception = new RuntimeException(); + $container->get(ResponseInterface::class)->willThrow($exception); + + $factory = new ServerRequestErrorResponseGeneratorFactory(); + + $this->expectException(RuntimeException::class); + $factory($container->reveal()); + } + + public function testFactoryCreatesGeneratorWhenOnlyResponseServiceIsPresent() + { + $container = $this->prophesize(ContainerInterface::class); + $container->has('config')->willReturn(false); + $container->get('config')->shouldNotBeCalled(); + $container->has(TemplateRendererInterface::class)->willReturn(false); + $container->get(TemplateRendererInterface::class)->shouldNotBeCalled(); + + $responseFactory = function () { + }; + $container->get(ResponseInterface::class)->willReturn($responseFactory); + + $factory = new ServerRequestErrorResponseGeneratorFactory(); + + $generator = $factory($container->reveal()); + + $this->assertAttributeNotSame($responseFactory, 'responseFactory', $generator); + $this->assertAttributeInstanceOf(Closure::class, 'responseFactory', $generator); + $this->assertAttributeSame(false, 'debug', $generator); + $this->assertAttributeEmpty('renderer', $generator); + $this->assertAttributeSame(ServerRequestErrorResponseGenerator::TEMPLATE_DEFAULT, 'template', $generator); + } + + public function testFactoryCreatesGeneratorUsingConfiguredServices() + { + $config = [ + 'debug' => true, + 'zend-expressive' => [ + 'error_handler' => [ + 'template_error' => 'some::template', + ], + ], + ]; + $renderer = $this->prophesize(TemplateRendererInterface::class)->reveal(); + + $container = $this->prophesize(ContainerInterface::class); + $container->has('config')->willReturn(true); + $container->get('config')->willReturn($config); + $container->has(TemplateRendererInterface::class)->willReturn(true); + $container->get(TemplateRendererInterface::class)->willReturn($renderer); + + $responseFactory = function () { + }; + $container->get(ResponseInterface::class)->willReturn($responseFactory); + + $factory = new ServerRequestErrorResponseGeneratorFactory(); + + $generator = $factory($container->reveal()); + + $this->assertAttributeNotSame($responseFactory, 'responseFactory', $generator); + $this->assertAttributeInstanceOf(Closure::class, 'responseFactory', $generator); + $this->assertAttributeSame(true, 'debug', $generator); + $this->assertAttributeSame($renderer, 'renderer', $generator); + $this->assertAttributeSame( + $config['zend-expressive']['error_handler']['template_error'], + 'template', + $generator + ); + } +} diff --git a/test/Container/ServerRequestFactoryFactoryTest.php b/test/Container/ServerRequestFactoryFactoryTest.php new file mode 100644 index 00000000..3978bcfb --- /dev/null +++ b/test/Container/ServerRequestFactoryFactoryTest.php @@ -0,0 +1,47 @@ +prophesize(ContainerInterface::class)->reveal(); + $factory = new ServerRequestFactoryFactory(); + + $generatedFactory = $factory($container); + + $this->assertInternalType('callable', $generatedFactory); + + return $generatedFactory; + } + + /** + * Some containers do not allow returning generic PHP callables, and will + * error when one is returned; one example is Auryn. As such, the factory + * cannot simply return a callable referencing the + * ServerRequestFactory::fromGlobals method, but must be decorated as a + * closure. + * + * @depends testFactoryReturnsCallable + */ + public function testFactoryIsAClosure(callable $factory) + { + $this->assertNotSame([ServerRequestFactory::class, 'fromGlobals'], $factory); + $this->assertNotSame(ServerRequestFactory::class . '::fromGlobals', $factory); + $this->assertInstanceOf(Closure::class, $factory); + } +} diff --git a/test/Container/ServerRequestFactoryFactoryWithoutDiactorosTest.php b/test/Container/ServerRequestFactoryFactoryWithoutDiactorosTest.php new file mode 100644 index 00000000..3194d44e --- /dev/null +++ b/test/Container/ServerRequestFactoryFactoryWithoutDiactorosTest.php @@ -0,0 +1,60 @@ +container = $this->prophesize(ContainerInterface::class)->reveal(); + $this->factory = new ServerRequestFactoryFactory(); + + $this->autoloadFunctions = spl_autoload_functions(); + foreach ($this->autoloadFunctions as $autoloader) { + spl_autoload_unregister($autoloader); + } + } + + private function reloadAutoloaders() + { + foreach ($this->autoloadFunctions as $autoloader) { + spl_autoload_register($autoloader); + } + } + + public function testFactoryRaisesAnExceptionIfDiactorosIsNotLoaded() + { + $this->expectException(InvalidServiceException::class); + $this->expectExceptionMessage('zendframework/zend-diactoros'); + + try { + ($this->factory)($this->container); + } finally { + $this->reloadAutoloaders(); + } + } +} diff --git a/test/Container/StreamFactoryFactoryTest.php b/test/Container/StreamFactoryFactoryTest.php index e0466f39..62b70895 100644 --- a/test/Container/StreamFactoryFactoryTest.php +++ b/test/Container/StreamFactoryFactoryTest.php @@ -1,10 +1,12 @@ container = $this->prophesize(ContainerInterface::class)->reveal(); + $this->factory = new StreamFactoryFactory(); + + foreach (spl_autoload_functions() as $autoloader) { + if (! is_array($autoloader)) { + continue; + } + + $context = $autoloader[0]; + + if (! is_object($context) + || ! preg_match('/^Composer.*?ClassLoader$/', get_class($context)) + ) { + continue; + } + + $this->autoloadFunctions[] = $autoloader; + + spl_autoload_unregister($autoloader); + } + } + + public function tearDown() + { + $this->reloadAutoloaders(); + } + + public function reloadAutoloaders() + { + foreach ($this->autoloadFunctions as $autoloader) { + spl_autoload_register($autoloader); + } + $this->autoloadFunctions = []; + } + + public function testFactoryRaisesAnExceptionIfDiactorosIsNotLoaded() + { + $e = null; + + try { + ($this->factory)($this->container); + } catch (Throwable $e) { + } + + $this->reloadAutoloaders(); + + $this->assertInstanceOf(InvalidServiceException::class, $e); + $this->assertContains('zendframework/zend-diactoros', $e->getMessage()); + } +} diff --git a/test/Application/TestAsset/CallableInteropMiddleware.php b/test/Container/TestAsset/CallableInteropMiddleware.php similarity index 60% rename from test/Application/TestAsset/CallableInteropMiddleware.php rename to test/Container/TestAsset/CallableInteropMiddleware.php index eaa8b34d..4f27b3cd 100644 --- a/test/Application/TestAsset/CallableInteropMiddleware.php +++ b/test/Container/TestAsset/CallableInteropMiddleware.php @@ -1,17 +1,19 @@ emitter = new EmitterStack(); - } - - public function testIsAnSplStack() - { - $this->assertInstanceOf(SplStack::class, $this->emitter); - } - - public function testIsAnEmitterImplementation() - { - $this->assertInstanceOf(EmitterInterface::class, $this->emitter); - } - - public function nonEmitterValues() - { - return [ - 'null' => [null], - 'true' => [true], - 'false' => [false], - 'zero' => [0], - 'int' => [1], - 'zero-float' => [0.0], - 'float' => [1.1], - 'string' => ['emitter'], - 'array' => [[$this->prophesize(EmitterInterface::class)->reveal()]], - 'object' => [(object) []], - ]; - } - - /** - * @dataProvider nonEmitterValues - * - * @param mixed $value - */ - public function testCannotPushNonEmitterToStack($value) - { - $this->expectException(InvalidArgumentException::class); - $this->emitter->push($value); - } - - /** - * @dataProvider nonEmitterValues - * - * @param mixed $value - */ - public function testCannotUnshiftNonEmitterToStack($value) - { - $this->expectException(InvalidArgumentException::class); - $this->emitter->unshift($value); - } - - /** - * @dataProvider nonEmitterValues - * - * @param mixed $value - */ - public function testCannotSetNonEmitterToSpecificIndex($value) - { - $this->expectException(InvalidArgumentException::class); - $this->emitter->offsetSet(0, $value); - } - - public function testOffsetSetReplacesExistingValue() - { - $first = $this->prophesize(EmitterInterface::class); - $replacement = $this->prophesize(EmitterInterface::class); - $this->emitter->push($first->reveal()); - $this->emitter->offsetSet(0, $replacement->reveal()); - $this->assertSame($replacement->reveal(), $this->emitter->pop()); - } - - public function testUnshiftAddsNewEmitter() - { - $first = $this->prophesize(EmitterInterface::class); - $second = $this->prophesize(EmitterInterface::class); - $this->emitter->push($first->reveal()); - $this->emitter->unshift($second->reveal()); - $this->assertSame($first->reveal(), $this->emitter->pop()); - } - - public function testEmitLoopsThroughEmittersUntilOneReturnsNonFalseValue() - { - $first = $this->prophesize(EmitterInterface::class); - $first->emit()->shouldNotBeCalled(); - - $second = $this->prophesize(EmitterInterface::class); - $second->emit(Argument::type(ResponseInterface::class)) - ->willReturn(null); - - $third = $this->prophesize(EmitterInterface::class); - $third->emit(Argument::type(ResponseInterface::class)) - ->willReturn(false); - - $this->emitter->push($first->reveal()); - $this->emitter->push($second->reveal()); - $this->emitter->push($third->reveal()); - - $response = $this->prophesize(ResponseInterface::class); - - $this->assertNull($this->emitter->emit($response->reveal())); - } - - public function testEmitReturnsFalseIfLastEmmitterReturnsFalse() - { - $first = $this->prophesize(EmitterInterface::class); - $first->emit(Argument::type(ResponseInterface::class)) - ->willReturn(false); - - $this->emitter->push($first->reveal()); - - $response = $this->prophesize(ResponseInterface::class); - - $this->assertFalse($this->emitter->emit($response->reveal())); - } - - public function testEmitReturnsFalseIfNoEmittersAreComposed() - { - $response = $this->prophesize(ResponseInterface::class); - - $this->assertFalse($this->emitter->emit($response->reveal())); - } -} diff --git a/test/ExceptionTest.php b/test/ExceptionTest.php new file mode 100644 index 00000000..2aaa7ae0 --- /dev/null +++ b/test/ExceptionTest.php @@ -0,0 +1,55 @@ + [$namespace . $class]; + } + } + + /** + * @dataProvider exception + */ + public function testExceptionIsInstanceOfExceptionInterface(string $exception) : void + { + $this->assertContains('Exception', $exception); + $this->assertTrue(is_a($exception, ExceptionInterface::class, true)); + } + + public function containerException() : Generator + { + yield InvalidMiddlewareException::class => [InvalidMiddlewareException::class]; + yield MissingDependencyException::class => [MissingDependencyException::class]; + } + + /** + * @dataProvider containerException + */ + public function testExceptionIsInstanceOfContainerExceptionInterface(string $exception) : void + { + $this->assertTrue(is_a($exception, ContainerExceptionInterface::class, true)); + } +} diff --git a/test/Handler/NotFoundHandlerTest.php b/test/Handler/NotFoundHandlerTest.php index cd868057..e3a4a141 100644 --- a/test/Handler/NotFoundHandlerTest.php +++ b/test/Handler/NotFoundHandlerTest.php @@ -1,10 +1,12 @@ request = $this->prophesize(ServerRequestInterface::class); $this->response = $this->prophesize(ResponseInterface::class); + $this->responseFactory = function () { + return $this->response->reveal(); + }; + } + + public function testImplementsRequesthandler() + { + $handler = new NotFoundHandler($this->responseFactory); + $this->assertInstanceOf(RequestHandlerInterface::class, $handler); } public function testConstructorDoesNotRequireARenderer() { - $handler = new NotFoundHandler($this->response->reveal()); + $handler = new NotFoundHandler($this->responseFactory); $this->assertInstanceOf(NotFoundHandler::class, $handler); - $this->assertAttributeSame($this->response->reveal(), 'responsePrototype', $handler); } public function testConstructorCanAcceptRendererAndTemplate() @@ -40,7 +58,7 @@ public function testConstructorCanAcceptRendererAndTemplate() $template = 'foo::bar'; $layout = 'layout::error'; - $handler = new NotFoundHandler($this->response->reveal(), $renderer, $template, $layout); + $handler = new NotFoundHandler($this->responseFactory, $renderer, $template, $layout); $this->assertInstanceOf(NotFoundHandler::class, $handler); $this->assertAttributeSame($renderer, 'renderer', $handler); @@ -59,9 +77,9 @@ public function testRendersDefault404ResponseWhenNoRendererPresent() $this->response->withStatus(StatusCode::STATUS_NOT_FOUND)->will([$this->response, 'reveal']); $this->response->getBody()->will([$stream, 'reveal']); - $handler = new NotFoundHandler($this->response->reveal()); + $handler = new NotFoundHandler($this->responseFactory); - $response = $handler->process($request->reveal()); + $response = $handler->handle($request->reveal()); $this->assertSame($this->response->reveal(), $response); } @@ -87,9 +105,9 @@ public function testUsesRendererToGenerateResponseContentsWhenPresent() $this->response->withStatus(StatusCode::STATUS_NOT_FOUND)->will([$this->response, 'reveal']); $this->response->getBody()->will([$stream, 'reveal']); - $handler = new NotFoundHandler($this->response->reveal(), $renderer->reveal()); + $handler = new NotFoundHandler($this->responseFactory, $renderer->reveal()); - $response = $handler->process($request); + $response = $handler->handle($request); $this->assertSame($this->response->reveal(), $response); } diff --git a/test/IntegrationTest.php b/test/IntegrationTest.php deleted file mode 100644 index 89e44d18..00000000 --- a/test/IntegrationTest.php +++ /dev/null @@ -1,116 +0,0 @@ -response = null; - $this->errorHandler = null; - } - - public function tearDown() - { - if ($this->errorHandler) { - set_error_handler($this->errorHandler); - $this->errorHandler = null; - } - } - - public function getEmitter() - { - $self = $this; - $emitter = $this->prophesize(EmitterInterface::class); - $emitter - ->emit(Argument::type(ResponseInterface::class)) - ->will(function ($args) use ($self) { - $response = array_shift($args); - $self->response = $response; - return null; - }) - ->shouldBeCalled(); - return $emitter->reveal(); - } - - public function testDefaultFinalHandlerCanEmitA404WhenNoMiddlewareMatches() - { - $app = new Application(new FastRouteRouter(), null, null, $this->getEmitter()); - $request = new ServerRequest([], [], 'https://example.com/foo', 'GET'); - $response = new Response(); - - $app->run($request, $response); - - $this->assertInstanceOf(ResponseInterface::class, $this->response); - $this->assertEquals(StatusCode::STATUS_NOT_FOUND, $this->response->getStatusCode()); - } - - public function testInjectedFinalHandlerCanEmitA404WhenNoMiddlewareMatches() - { - $request = new ServerRequest([], [], 'https://example.com/foo', 'GET'); - $response = new Response(); - $delegate = new NotFoundDelegate($response); - $app = new Application(new FastRouteRouter(), null, $delegate, $this->getEmitter()); - - $app->run($request, $response); - - $this->assertInstanceOf(ResponseInterface::class, $this->response); - $this->assertEquals(StatusCode::STATUS_NOT_FOUND, $this->response->getStatusCode()); - } - - public function testCallableClassInteropMiddlewareNotRegisteredWithContainerCanBeComposedSuccessfully() - { - $response = new Response(); - $routedMiddleware = $this->prophesize(MiddlewareInterface::class); - $routedMiddleware - ->process( - Argument::type(ServerRequestInterface::class), - Argument::type(DelegateInterface::class) - ) - ->willReturn($response); - - $container = $this->prophesize(ContainerInterface::class); - $container->has('RoutedMiddleware')->willReturn(true); - $container->get('RoutedMiddleware')->will([$routedMiddleware, 'reveal']); - $container->has(TestAsset\CallableInteropMiddleware::class)->willReturn(false); - - $delegate = new NotFoundDelegate($response); - $app = new Application(new FastRouteRouter(), $container->reveal(), $delegate, $this->getEmitter()); - - $app->pipe(TestAsset\CallableInteropMiddleware::class); - $app->get('/', 'RoutedMiddleware'); - - $request = new ServerRequest([], [], 'https://example.com/foo', 'GET'); - $app->run($request, new Response()); - - $this->assertInstanceOf(ResponseInterface::class, $this->response); - $this->assertTrue($this->response->hasHeader('X-Callable-Interop-Middleware')); - $this->assertEquals( - TestAsset\CallableInteropMiddleware::class, - $this->response->getHeaderLine('X-Callable-Interop-Middleware') - ); - } -} diff --git a/test/Middleware/ErrorResponseGeneratorTest.php b/test/Middleware/ErrorResponseGeneratorTest.php index f0b8a0dc..eb8381a4 100644 --- a/test/Middleware/ErrorResponseGeneratorTest.php +++ b/test/Middleware/ErrorResponseGeneratorTest.php @@ -1,10 +1,12 @@ false]; - set_error_handler(function ($errno, $errstr) use ($test) { - $test->message = $errstr; - return true; - }, E_USER_DEPRECATED); - - $middleware = new ImplicitHeadMiddleware(); - restore_error_handler(); - - $this->assertInstanceOf(BaseImplicitHeadMiddleware::class, $middleware); - $this->assertInternalType('string', $test->message); - $this->assertContains('deprecated starting with zend-expressive 2.2', $test->message); - } -} diff --git a/test/Middleware/ImplicitOptionsMiddlewareTest.php b/test/Middleware/ImplicitOptionsMiddlewareTest.php deleted file mode 100644 index f138ac19..00000000 --- a/test/Middleware/ImplicitOptionsMiddlewareTest.php +++ /dev/null @@ -1,31 +0,0 @@ - false]; - set_error_handler(function ($errno, $errstr) use ($test) { - $test->message = $errstr; - return true; - }, E_USER_DEPRECATED); - - $middleware = new ImplicitOptionsMiddleware(); - restore_error_handler(); - - $this->assertInstanceOf(BaseImplicitOptionsMiddleware::class, $middleware); - $this->assertInternalType('string', $test->message); - $this->assertContains('deprecated starting with zend-expressive 2.2', $test->message); - } -} diff --git a/test/Middleware/LazyLoadingMiddlewareTest.php b/test/Middleware/LazyLoadingMiddlewareTest.php index 34755907..92635106 100644 --- a/test/Middleware/LazyLoadingMiddlewareTest.php +++ b/test/Middleware/LazyLoadingMiddlewareTest.php @@ -1,135 +1,76 @@ container = $this->prophesize(ContainerInterface::class); - $this->response = $this->prophesize(ResponseInterface::class); + $this->container = $this->prophesize(MiddlewareContainer::class); $this->request = $this->prophesize(ServerRequestInterface::class); - $this->delegate = $this->prophesize(DelegateInterface::class); + $this->handler = $this->prophesize(RequestHandlerInterface::class); } public function buildLazyLoadingMiddleware($middlewareName) { return new LazyLoadingMiddleware( $this->container->reveal(), - $this->response->reveal(), $middlewareName ); } - public function testInvokesInteropMiddlewarePulledFromContainer() + public function testProcessesMiddlewarePulledFromContainer() { - $expected = $this->prophesize(ResponseInterface::class)->reveal(); - - $middleware = $this->prophesize(ServerMiddlewareInterface::class); + $response = $this->prophesize(ResponseInterface::class)->reveal(); + $middleware = $this->prophesize(MiddlewareInterface::class); $middleware ->process( $this->request->reveal(), - $this->delegate->reveal() - ) - ->willReturn($expected); - - $this->container->get('middleware')->will([$middleware, 'reveal']); - - $lazyLoadingMiddleware = $this->buildLazyLoadingMiddleware('middleware'); - $this->assertSame( - $expected, - $lazyLoadingMiddleware->process($this->request->reveal(), $this->delegate->reveal()) - ); - } - - public function testInvokesDuckTypedInteropMiddlewarePulledFromContainer() - { - $expected = $this->prophesize(ResponseInterface::class)->reveal(); - - $middleware = function ($request, DelegateInterface $delegate) use ($expected) { - return $expected; - }; + $this->handler->reveal() + )->willReturn($response); - $this->container->get('middleware')->willReturn($middleware); + $this->container->get('foo')->will([$middleware, 'reveal']); - $lazyLoadingMiddleware = $this->buildLazyLoadingMiddleware('middleware'); + $lazyloader = $this->buildLazyLoadingMiddleware('foo'); $this->assertSame( - $expected, - $lazyLoadingMiddleware->process($this->request->reveal(), $this->delegate->reveal()) + $response, + $lazyloader->process($this->request->reveal(), $this->handler->reveal()) ); } - public function testInvokesDoublePassMiddlewarePulledFromContainerUsingResponsePrototype() - { - $expected = $this->prophesize(ResponseInterface::class)->reveal(); - - $middleware = function ($request, $response, callable $next) use ($expected) { - return $expected; - }; - - $this->container->get('middleware')->willReturn($middleware); - - $lazyLoadingMiddleware = $this->buildLazyLoadingMiddleware('middleware'); - $this->assertSame( - $expected, - $lazyLoadingMiddleware->process($this->request->reveal(), $this->delegate->reveal()) - ); - } - - public function invalidMiddleware() - { - return [ - 'null' => [null], - 'true' => [true], - 'false' => [false], - 'zero' => [0], - 'int' => [1], - 'zero-float' => [0.0], - 'float' => [1.1], - 'non-invokable-string' => ['not-real-middleware'], - 'non-invokable-array' => [['not', 'real', 'middleware']], - 'non-invokable-object' => [(object) ['middleware' => false]], - ]; - } - - /** - * @dataProvider invalidMiddleware - * - * @param mixed $middleware - */ - public function testRaisesExceptionIfMiddlewarePulledFromContainerIsInvalid($middleware) + public function testDoesNotCatchContainerExceptions() { - $this->container->get('middleware')->willReturn($middleware); - $lazyLoadingMiddleware = $this->buildLazyLoadingMiddleware('middleware'); + $exception = new InvalidMiddlewareException(); + $this->container->get('foo')->willThrow($exception); + $lazyloader = $this->buildLazyLoadingMiddleware('foo'); $this->expectException(InvalidMiddlewareException::class); - $lazyLoadingMiddleware->process($this->request->reveal(), $this->delegate->reveal()); + $lazyloader->process($this->request->reveal(), $this->handler->reveal()); } } diff --git a/test/Middleware/NotFoundHandlerTest.php b/test/Middleware/NotFoundHandlerTest.php deleted file mode 100644 index 8511b3c8..00000000 --- a/test/Middleware/NotFoundHandlerTest.php +++ /dev/null @@ -1,54 +0,0 @@ -internal = $this->prophesize(NotFoundDelegate::class); - $this->request = $this->prophesize(ServerRequestInterface::class); - - $this->delegate = $this->prophesize(DelegateInterface::class); - $this->delegate->process(Argument::type(ServerRequestInterface::class))->shouldNotBeCalled(); - } - - public function testImplementsInteropMiddleware() - { - $handler = new NotFoundHandler($this->internal->reveal()); - $this->assertInstanceOf(MiddlewareInterface::class, $handler); - } - - public function testProxiesToInternalDelegate() - { - $this->internal - ->process(Argument::that([$this->request, 'reveal'])) - ->willReturn('CONTENT'); - - $handler = new NotFoundHandler($this->internal->reveal()); - $this->assertEquals('CONTENT', $handler->process($this->request->reveal(), $this->delegate->reveal())); - } -} diff --git a/test/Middleware/WhoopsErrorResponseGeneratorTest.php b/test/Middleware/WhoopsErrorResponseGeneratorTest.php index 125cf166..d0870ede 100644 --- a/test/Middleware/WhoopsErrorResponseGeneratorTest.php +++ b/test/Middleware/WhoopsErrorResponseGeneratorTest.php @@ -1,10 +1,12 @@ originContainer = $this->prophesize(ContainerInterface::class); + $this->container = new MiddlewareContainer($this->originContainer->reveal()); + } + + public function testHasReturnsTrueIfOriginContainerHasService() + { + $this->originContainer->has('foo')->willReturn(true); + $this->assertTrue($this->container->has('foo')); + } + + public function testHasReturnsTrueIfOriginContainerDoesNotHaveServiceButClassExists() + { + $this->originContainer->has(__CLASS__)->willReturn(false); + $this->assertTrue($this->container->has(__CLASS__)); + } + + public function testHasReturnsFalseIfOriginContainerDoesNotHaveServiceAndClassDoesNotExist() + { + $this->originContainer->has('not-a-class')->willReturn(false); + $this->assertFalse($this->container->has('not-a-class')); + } + + public function testGetRaisesExceptionIfServiceIsUnknown() + { + $this->originContainer->has('not-a-service')->willReturn(false); + + $this->expectException(Exception\MissingDependencyException::class); + $this->container->get('not-a-service'); + } + + public function testGetRaisesExceptionIfServiceSpecifiedDoesNotImplementMiddlewareInterface() + { + $this->originContainer->has(__CLASS__)->willReturn(true); + $this->originContainer->get(__CLASS__)->willReturn($this); + + $this->expectException(Exception\InvalidMiddlewareException::class); + $this->container->get(__CLASS__); + } + + public function testGetRaisesExceptionIfClassSpecifiedDoesNotImplementMiddlewareInterface() + { + $this->originContainer->has(__CLASS__)->willReturn(false); + $this->originContainer->get(__CLASS__)->shouldNotBeCalled(); + + $this->expectException(Exception\InvalidMiddlewareException::class); + $this->container->get(__CLASS__); + } + + public function testGetReturnsServiceFromOriginContainer() + { + $middleware = $this->prophesize(MiddlewareInterface::class)->reveal(); + $this->originContainer->has('middleware-service')->willReturn(true); + $this->originContainer->get('middleware-service')->willReturn($middleware); + + $this->assertSame($middleware, $this->container->get('middleware-service')); + } + + public function testGetReturnsInstantiatedClass() + { + $this->originContainer->has(DispatchMiddleware::class)->willReturn(false); + $this->originContainer->get(DispatchMiddleware::class)->shouldNotBeCalled(); + + $middleware = $this->container->get(DispatchMiddleware::class); + $this->assertInstanceOf(DispatchMiddleware::class, $middleware); + } + + public function testGetWillDecorateARequestHandlerAsMiddleware() + { + $handler = $this->prophesize(RequestHandlerInterface::class)->reveal(); + + $this->originContainer->has('AHandlerNotMiddleware')->willReturn(true); + $this->originContainer->get('AHandlerNotMiddleware')->willReturn($handler); + + $middleware = $this->container->get('AHandlerNotMiddleware'); + + // Test that we get back middleware decorating the handler + $this->assertInstanceOf(RequestHandlerMiddleware::class, $middleware); + $this->assertAttributeSame($handler, 'handler', $middleware); + } +} diff --git a/test/MiddlewareFactoryTest.php b/test/MiddlewareFactoryTest.php new file mode 100644 index 00000000..21ce26ab --- /dev/null +++ b/test/MiddlewareFactoryTest.php @@ -0,0 +1,215 @@ +container = $this->prophesize(MiddlewareContainer::class); + $this->factory = new MiddlewareFactory($this->container->reveal()); + } + + public function assertLazyLoadingMiddleware(string $expectedMiddlewareName, MiddlewareInterface $middleware) + { + $this->assertInstanceOf(LazyLoadingMiddleware::class, $middleware); + $this->assertAttributeSame($this->container->reveal(), 'container', $middleware); + $this->assertAttributeSame($expectedMiddlewareName, 'middlewareName', $middleware); + } + + public function assertCallableMiddleware(callable $expectedCallable, MiddlewareInterface $middleware) + { + $this->assertInstanceOf(CallableMiddlewareDecorator::class, $middleware); + $this->assertAttributeSame($expectedCallable, 'middleware', $middleware); + } + + public function assertPipeline(array $expectedPipeline, MiddlewareInterface $middleware) + { + $this->assertInstanceOf(MiddlewarePipe::class, $middleware); + $pipeline = $this->reflectPipeline($middleware); + $this->assertSame($expectedPipeline, $pipeline); + } + + public function reflectPipeline(MiddlewarePipe $pipeline) : array + { + $r = new ReflectionProperty($pipeline, 'pipeline'); + $r->setAccessible(true); + return iterator_to_array($r->getValue($pipeline)); + } + + public function testCallableDecoratesCallableMiddleware() + { + $callable = function ($request, $handler) { + }; + + $middleware = $this->factory->callable($callable); + $this->assertCallableMiddleware($callable, $middleware); + } + + public function testLazyLoadingMiddlewareDecoratesMiddlewareServiceName() + { + $middleware = $this->factory->lazy('service'); + $this->assertLazyLoadingMiddleware('service', $middleware); + } + + public function testPrepareReturnsMiddlewareImplementationsVerbatim() + { + $middleware = $this->prophesize(MiddlewareInterface::class)->reveal(); + $this->assertSame($middleware, $this->factory->prepare($middleware)); + } + + public function testPrepareDecoratesCallables() + { + $callable = function ($request, $handler) { + }; + + $middleware = $this->factory->prepare($callable); + $this->assertInstanceOf(CallableMiddlewareDecorator::class, $middleware); + $this->assertAttributeSame($callable, 'middleware', $middleware); + } + + public function testPrepareDecoratesServiceNamesAsLazyLoadingMiddleware() + { + $middleware = $this->factory->prepare('service'); + $this->assertInstanceOf(LazyLoadingMiddleware::class, $middleware); + $this->assertAttributeSame('service', 'middlewareName', $middleware); + $this->assertAttributeSame($this->container->reveal(), 'container', $middleware); + } + + public function testPrepareDecoratesArraysAsMiddlewarePipes() + { + $middleware1 = $this->prophesize(MiddlewareInterface::class)->reveal(); + $middleware2 = $this->prophesize(MiddlewareInterface::class)->reveal(); + $middleware3 = $this->prophesize(MiddlewareInterface::class)->reveal(); + + $middleware = $this->factory->prepare([$middleware1, $middleware2, $middleware3]); + $this->assertPipeline([$middleware1, $middleware2, $middleware3], $middleware); + } + + public function invalidMiddlewareTypes() : iterable + { + yield 'null' => [null]; + yield 'false' => [false]; + yield 'true' => [true]; + yield 'zero' => [0]; + yield 'int' => [1]; + yield 'zero-float' => [0.0]; + yield 'float' => [1.1]; + yield 'object' => [(object) ['foo' => 'bar']]; + } + + /** + * @dataProvider invalidMiddlewareTypes + */ + public function testPrepareRaisesExceptionForTypesItDoesNotUnderstand($middleware) + { + $this->expectException(Exception\InvalidMiddlewareException::class); + $this->factory->prepare($middleware); + } + + public function testPipelineAcceptsMultipleArguments() + { + $middleware1 = $this->prophesize(MiddlewareInterface::class)->reveal(); + $middleware2 = $this->prophesize(MiddlewareInterface::class)->reveal(); + $middleware3 = $this->prophesize(MiddlewareInterface::class)->reveal(); + + $middleware = $this->factory->pipeline($middleware1, $middleware2, $middleware3); + $this->assertPipeline([$middleware1, $middleware2, $middleware3], $middleware); + } + + public function testPipelineAcceptsASingleArrayArgument() + { + $middleware1 = $this->prophesize(MiddlewareInterface::class)->reveal(); + $middleware2 = $this->prophesize(MiddlewareInterface::class)->reveal(); + $middleware3 = $this->prophesize(MiddlewareInterface::class)->reveal(); + + $middleware = $this->factory->pipeline([$middleware1, $middleware2, $middleware3]); + $this->assertPipeline([$middleware1, $middleware2, $middleware3], $middleware); + } + + public function validPrepareTypes() + { + yield 'service' => ['service', 'assertLazyLoadingMiddleware', 'service']; + + $callable = function ($request, $handler) { + }; + yield 'callable' => [$callable, 'assertCallableMiddleware', $callable]; + + $middleware = new DispatchMiddleware(); + yield 'instance' => [$middleware, 'assertSame', $middleware]; + } + + /** + * @dataProvider validPrepareTypes + * @param string|callable|MiddlewareInterface $middleware + * @param mixed $expected Expected type or value for use with assertion + */ + public function testPipelineAllowsAnyTypeSupportedByPrepare( + $middleware, + string $assertion, + $expected + ) { + $pipeline = $this->factory->pipeline($middleware); + $this->assertInstanceOf(MiddlewarePipe::class, $pipeline); + + $r = new ReflectionProperty($pipeline, 'pipeline'); + $r->setAccessible(true); + $values = iterator_to_array($r->getValue($pipeline)); + $received = array_shift($values); + + $this->{$assertion}($expected, $received); + } + + public function testPipelineAllowsPipingArraysOfMiddlewareAndCastsThemToInternalPipelines() + { + $callable = function ($request, $handler) { + }; + $middleware = new DispatchMiddleware(); + + $internalPipeline = [$callable, $middleware]; + + $pipeline = $this->factory->pipeline($internalPipeline); + + $this->assertInstanceOf(MiddlewarePipe::class, $pipeline); + $received = $this->reflectPipeline($pipeline); + $this->assertCount(2, $received); + $this->assertCallableMiddleware($callable, $received[0]); + $this->assertSame($middleware, $received[1]); + } + + public function testPrepareDecoratesRequestHandlersAsMiddleware() + { + $handler = $this->prophesize(RequestHandlerInterface::class)->reveal(); + $middleware = $this->factory->prepare($handler); + $this->assertInstanceOf(RequestHandlerMiddleware::class, $middleware); + $this->assertAttributeSame($handler, 'handler', $middleware); + } + + public function testHandlerDecoratesRequestHandlersAsMiddleware() + { + $handler = $this->prophesize(RequestHandlerInterface::class)->reveal(); + $middleware = $this->factory->handler($handler); + $this->assertInstanceOf(RequestHandlerMiddleware::class, $middleware); + $this->assertAttributeSame($handler, 'handler', $middleware); + } +} diff --git a/test/Response/ServerRequestErrorResponseGeneratorTest.php b/test/Response/ServerRequestErrorResponseGeneratorTest.php new file mode 100644 index 00000000..a1a5edb1 --- /dev/null +++ b/test/Response/ServerRequestErrorResponseGeneratorTest.php @@ -0,0 +1,120 @@ +response = $this->prophesize(ResponseInterface::class); + $this->responseFactory = function () { + return $this->response->reveal(); + }; + + $this->renderer = $this->prophesize(TemplateRendererInterface::class); + } + + public function testPreparesTemplatedResponseWhenRendererPresent() + { + $stream = $this->prophesize(StreamInterface::class); + $stream->write('data from template')->shouldBeCalled(); + + $this->response->withStatus(422)->will([$this->response, 'reveal']); + $this->response->getBody()->will([$stream, 'reveal']); + $this->response->getStatusCode()->willReturn(422); + $this->response->getReasonPhrase()->willReturn('Unexpected entity'); + + $template = 'some::template'; + $e = new RuntimeException('This is the exception message', 422); + $this->renderer + ->render($template, [ + 'response' => $this->response->reveal(), + 'status' => 422, + 'reason' => 'Unexpected entity', + 'error' => $e, + ]) + ->willReturn('data from template'); + + $generator = new ServerRequestErrorResponseGenerator( + $this->responseFactory, + true, + $this->renderer->reveal(), + $template + ); + + $this->assertSame($this->response->reveal(), $generator($e)); + } + + public function testPreparesResponseWithDefaultMessageOnlyWhenNoRendererPresentAndNotInDebugMode() + { + $stream = $this->prophesize(StreamInterface::class); + $stream->write('An unexpected error occurred')->shouldBeCalled(); + + $this->response->withStatus(422)->will([$this->response, 'reveal']); + $this->response->getBody()->will([$stream, 'reveal']); + $this->response->getStatusCode()->shouldNotBeCalled(); + $this->response->getReasonPhrase()->shouldNotBeCalled(); + + $e = new RuntimeException('This is the exception message', 422); + + $generator = new ServerRequestErrorResponseGenerator($this->responseFactory); + + $this->assertSame($this->response->reveal(), $generator($e)); + } + + public function testPreparesResponseWithDefaultMessageAndStackTraceWhenNoRendererPresentAndInDebugMode() + { + $stream = $this->prophesize(StreamInterface::class); + $stream + ->write(Argument::that(function ($message) { + if (! preg_match('/^An unexpected error occurred; stack trace:/', $message)) { + echo "Failed first assertion: $message\n"; + return false; + } + if (false === strpos($message, 'Stack Trace:')) { + echo "Failed second assertion: $message\n"; + return false; + } + return $message; + })) + ->shouldBeCalled(); + + $this->response->withStatus(422)->will([$this->response, 'reveal']); + $this->response->getBody()->will([$stream, 'reveal']); + $this->response->getStatusCode()->shouldNotBeCalled(); + $this->response->getReasonPhrase()->shouldNotBeCalled(); + + $e = new RuntimeException('This is the exception message', 422); + + $generator = new ServerRequestErrorResponseGenerator($this->responseFactory, true); + + $this->assertSame($this->response->reveal(), $generator($e)); + } +} diff --git a/test/RouteResultTrait.php b/test/RouteResultTrait.php deleted file mode 100644 index c0c48a5c..00000000 --- a/test/RouteResultTrait.php +++ /dev/null @@ -1,28 +0,0 @@ -prophesize(Route::class); - $route->getMiddleware()->willReturn($middleware); - $route->getPath()->willReturn($name); - $route->getName()->willReturn(null); - - return RouteResult::fromRoute($route->reveal(), $params); - } -} diff --git a/test/Router/IntegrationTest.php b/test/Router/IntegrationTest.php index 950cc57f..21439e9b 100644 --- a/test/Router/IntegrationTest.php +++ b/test/Router/IntegrationTest.php @@ -1,28 +1,38 @@ response = new Response(); + $this->responseFactory = function () { + return $this->response; + }; $this->router = $this->prophesize(RouterInterface::class); $this->container = $this->mockContainerInterface(); $this->disregardDeprecationNotices(); @@ -72,9 +88,21 @@ public function disregardDeprecationNotices() public function getApplication() { + return $this->createApplicationFromRouter($this->router->reveal()); + } + + public function createApplicationFromRouter(RouterInterface $router) + { + $container = new MiddlewareContainer($this->container->reveal()); + $factory = new MiddlewareFactory($container); + $pipeline = new MiddlewarePipe(); + $routeMiddleware = new RouteMiddleware($router); + $runner = $this->prophesize(RequestHandlerRunner::class)->reveal(); return new Application( - $this->router->reveal(), - $this->container->reveal() + $factory, + $pipeline, + $routeMiddleware, + $runner ); } @@ -101,18 +129,20 @@ public function routerAdapters() */ private function createApplicationWithGetPost($adapter, $getName = null, $postName = null) { - $app = new Application(new $adapter()); - $app->pipeRoutingMiddleware(); + $router = new $adapter(); + $app = $this->createApplicationFromRouter($router); + $app->pipe(new RouteMiddleware($router)); + $app->pipe(new MethodNotAllowedMiddleware($this->responseFactory)); - $app->get('/foo', function ($req, $res, $next) { + $app->get('/foo', function ($req, $handler) { $stream = new Stream('php://temp', 'w+'); $stream->write('Middleware GET'); - return $res->withBody($stream); + return $this->response->withBody($stream); }, $getName); - $app->post('/foo', function ($req, $res, $next) { + $app->post('/foo', function ($req, $handler) { $stream = new Stream('php://temp', 'w+'); $stream->write('Middleware POST'); - return $res->withBody($stream); + return $this->response->withBody($stream); }, $postName); return $app; @@ -129,18 +159,20 @@ private function createApplicationWithGetPost($adapter, $getName = null, $postNa */ private function createApplicationWithRouteGetPost($adapter, $getName = null, $postName = null) { - $app = new Application(new $adapter()); - $app->pipeRoutingMiddleware(); + $router = new $adapter(); + $app = $this->createApplicationFromRouter($router); + $app->pipe(new RouteMiddleware($router)); + $app->pipe(new MethodNotAllowedMiddleware($this->responseFactory)); - $app->route('/foo', function ($req, $res, $next) { + $app->route('/foo', function ($req, $handler) { $stream = new Stream('php://temp', 'w+'); $stream->write('Middleware GET'); - return $res->withBody($stream); + return $this->response->withBody($stream); }, ['GET'], $getName); - $app->route('/foo', function ($req, $res, $next) { + $app->route('/foo', function ($req, $handler) { $stream = new Stream('php://temp', 'w+'); $stream->write('Middleware POST'); - return $res->withBody($stream); + return $this->response->withBody($stream); }, ['POST'], $postName); return $app; @@ -154,12 +186,12 @@ private function createApplicationWithRouteGetPost($adapter, $getName = null, $p public function testRoutingDoesNotMatchMethod($adapter) { $app = $this->createApplicationWithGetPost($adapter); - $delegate = $this->prophesize(DelegateInterface::class); - $delegate->process(Argument::type(ServerRequest::class)) + $handler = $this->prophesize(RequestHandlerInterface::class); + $handler->handle(Argument::type(ServerRequest::class)) ->shouldNotBeCalled(); $request = new ServerRequest(['REQUEST_METHOD' => 'DELETE'], [], '/foo', 'DELETE'); - $result = $app->process($request, $delegate->reveal()); + $result = $app->process($request, $handler->reveal()); $this->assertSame(StatusCode::STATUS_METHOD_NOT_ALLOWED, $result->getStatusCode()); $headers = $result->getHeaders(); @@ -177,19 +209,19 @@ public function testRoutingDoesNotMatchMethod($adapter) public function testRoutingWithSamePathWithoutName($adapter) { $app = $this->createApplicationWithGetPost($adapter); - $app->pipeDispatchMiddleware(); + $app->pipe(new DispatchMiddleware()); - $delegate = $this->prophesize(DelegateInterface::class); - $delegate->process(Argument::type(ServerRequest::class)) + $handler = $this->prophesize(RequestHandlerInterface::class); + $handler->handle(Argument::type(ServerRequest::class)) ->shouldNotBeCalled(); $request = new ServerRequest(['REQUEST_METHOD' => 'GET'], [], '/foo', 'GET'); - $result = $app->process($request, $delegate->reveal()); + $result = $app->process($request, $handler->reveal()); $this->assertEquals('Middleware GET', (string) $result->getBody()); $request = new ServerRequest(['REQUEST_METHOD' => 'POST'], [], '/foo', 'POST'); - $result = $app->process($request, $delegate->reveal()); + $result = $app->process($request, $handler->reveal()); $this->assertEquals('Middleware POST', (string) $result->getBody()); } @@ -205,19 +237,20 @@ public function testRoutingWithSamePathWithoutName($adapter) public function testRoutingWithSamePathWithName($adapter) { $app = $this->createApplicationWithGetPost($adapter, 'foo-get', 'foo-post'); - $app->pipeDispatchMiddleware(); + $app->pipe(new DispatchMiddleware()); - $delegate = $this->prophesize(DelegateInterface::class); - $delegate->process(Argument::type(ServerRequest::class)) + $handler = $this->prophesize(RequestHandlerInterface::class); + $handler + ->handle(Argument::type(ServerRequest::class)) ->shouldNotBeCalled(); $request = new ServerRequest(['REQUEST_METHOD' => 'GET'], [], '/foo', 'GET'); - $result = $app->process($request, $delegate->reveal()); + $result = $app->process($request, $handler->reveal()); $this->assertEquals('Middleware GET', (string) $result->getBody()); $request = new ServerRequest(['REQUEST_METHOD' => 'POST'], [], '/foo', 'POST'); - $result = $app->process($request, $delegate->reveal()); + $result = $app->process($request, $handler->reveal()); $this->assertEquals('Middleware POST', (string) $result->getBody()); } @@ -233,19 +266,19 @@ public function testRoutingWithSamePathWithName($adapter) public function testRoutingWithSamePathWithRouteWithoutName($adapter) { $app = $this->createApplicationWithRouteGetPost($adapter); - $app->pipeDispatchMiddleware(); + $app->pipe(new DispatchMiddleware()); - $delegate = $this->prophesize(DelegateInterface::class); - $delegate->process(Argument::type(ServerRequest::class)) + $handler = $this->prophesize(RequestHandlerInterface::class); + $handler->handle(Argument::type(ServerRequest::class)) ->shouldNotBeCalled(); $request = new ServerRequest(['REQUEST_METHOD' => 'GET'], [], '/foo', 'GET'); - $result = $app->process($request, $delegate->reveal()); + $result = $app->process($request, $handler->reveal()); $this->assertEquals('Middleware GET', (string) $result->getBody()); $request = new ServerRequest(['REQUEST_METHOD' => 'POST'], [], '/foo', 'POST'); - $result = $app->process($request, $delegate->reveal()); + $result = $app->process($request, $handler->reveal()); $this->assertEquals('Middleware POST', (string) $result->getBody()); } @@ -260,19 +293,20 @@ public function testRoutingWithSamePathWithRouteWithoutName($adapter) public function testRoutingWithSamePathWithRouteWithName($adapter) { $app = $this->createApplicationWithRouteGetPost($adapter, 'foo-get', 'foo-post'); - $app->pipeDispatchMiddleware(); + $app->pipe(new DispatchMiddleware()); - $delegate = $this->prophesize(DelegateInterface::class); - $delegate->process(Argument::type(ServerRequest::class)) + $handler = $this->prophesize(RequestHandlerInterface::class); + $handler + ->handle(Argument::type(ServerRequest::class)) ->shouldNotBeCalled(); $request = new ServerRequest(['REQUEST_METHOD' => 'GET'], [], '/foo', 'GET'); - $result = $app->process($request, $delegate->reveal()); + $result = $app->process($request, $handler->reveal()); $this->assertEquals('Middleware GET', (string) $result->getBody()); $request = new ServerRequest(['REQUEST_METHOD' => 'POST'], [], '/foo', 'POST'); - $result = $app->process($request, $delegate->reveal()); + $result = $app->process($request, $handler->reveal()); $this->assertEquals('Middleware POST', (string) $result->getBody()); } @@ -287,35 +321,41 @@ public function testRoutingWithSamePathWithRouteWithName($adapter) */ public function testRoutingWithSamePathWithRouteWithMultipleMethods($adapter) { - $app = new Application(new $adapter()); - $app->pipeRoutingMiddleware(); - $app->pipeDispatchMiddleware(); - - $app->route('/foo', function ($req, $res, $next) { + $router = new $adapter(); + $app = $this->createApplicationFromRouter($router); + $app->pipe(new RouteMiddleware($router)); + $app->pipe(new MethodNotAllowedMiddleware($this->responseFactory)); + $app->pipe(new DispatchMiddleware()); + + $response = clone $this->response; + $app->route('/foo', function ($req, $handler) use ($response) { $stream = new Stream('php://temp', 'w+'); $stream->write('Middleware GET, POST'); - return $res->withBody($stream); + return $response->withBody($stream); }, ['GET', 'POST']); - $app->route('/foo', function ($req, $res, $next) { + + $deleteResponse = clone $this->response; + $app->route('/foo', function ($req, $handler) use ($deleteResponse) { $stream = new Stream('php://temp', 'w+'); $stream->write('Middleware DELETE'); - return $res->withBody($stream); + return $deleteResponse->withBody($stream); }, ['DELETE']); - $delegate = $this->prophesize(DelegateInterface::class); - $delegate->process(Argument::type(ServerRequest::class)) + $handler = $this->prophesize(RequestHandlerInterface::class); + $handler + ->handle(Argument::type(ServerRequest::class)) ->shouldNotBeCalled(); $request = new ServerRequest(['REQUEST_METHOD' => 'GET'], [], '/foo', 'GET'); - $result = $app->process($request, $delegate->reveal()); + $result = $app->process($request, $handler->reveal()); $this->assertEquals('Middleware GET, POST', (string) $result->getBody()); $request = new ServerRequest(['REQUEST_METHOD' => 'POST'], [], '/foo', 'POST'); - $result = $app->process($request, $delegate->reveal()); + $result = $app->process($request, $handler->reveal()); $this->assertEquals('Middleware GET, POST', (string) $result->getBody()); $request = new ServerRequest(['REQUEST_METHOD' => 'DELETE'], [], '/foo', 'DELETE'); - $result = $app->process($request, $delegate->reveal()); + $result = $app->process($request, $handler->reveal()); $this->assertEquals('Middleware DELETE', (string) $result->getBody()); } @@ -347,23 +387,27 @@ public function routerAdaptersForHttpMethods() */ public function testMatchWithAllHttpMethods($adapter, $method) { - $app = new Application(new $adapter()); - $app->pipeRoutingMiddleware(); - $app->pipeDispatchMiddleware(); + $router = new $adapter(); + $app = $this->createApplicationFromRouter($router); + $app->pipe(new RouteMiddleware($router)); + $app->pipe(new MethodNotAllowedMiddleware($this->responseFactory)); + $app->pipe(new DispatchMiddleware()); // Add a route with Zend\Expressive\Router\Route::HTTP_METHOD_ANY - $app->route('/foo', function ($req, $res, $next) { + $response = clone $this->response; + $app->route('/foo', function ($req, $handler) use ($response) { $stream = new Stream('php://temp', 'w+'); $stream->write('Middleware'); - return $res->withBody($stream); + return $response->withBody($stream); }); - $delegate = $this->prophesize(DelegateInterface::class); - $delegate->process(Argument::type(ServerRequest::class)) + $handler = $this->prophesize(RequestHandlerInterface::class); + $handler + ->handle(Argument::type(ServerRequest::class)) ->shouldNotBeCalled(); $request = new ServerRequest(['REQUEST_METHOD' => $method], [], '/foo', $method); - $result = $app->process($request, $delegate->reveal()); + $result = $app->process($request, $handler->reveal()); $this->assertEquals('Middleware', (string) $result->getBody()); } @@ -379,26 +423,6 @@ public function allowedMethod() ]; } - public function notAllowedMethod() - { - return [ - 'aura-get' => [AuraRouter::class, RequestMethod::METHOD_GET], - 'aura-post' => [AuraRouter::class, RequestMethod::METHOD_POST], - 'aura-put' => [AuraRouter::class, RequestMethod::METHOD_PUT], - 'aura-delete' => [AuraRouter::class, RequestMethod::METHOD_DELETE], - 'aura-patch' => [AuraRouter::class, RequestMethod::METHOD_PATCH], - 'fast-route-post' => [FastRouteRouter::class, RequestMethod::METHOD_POST], - 'fast-route-put' => [FastRouteRouter::class, RequestMethod::METHOD_PUT], - 'fast-route-delete' => [FastRouteRouter::class, RequestMethod::METHOD_DELETE], - 'fast-route-patch' => [FastRouteRouter::class, RequestMethod::METHOD_PATCH], - 'zf2-get' => [ZendRouter::class, RequestMethod::METHOD_GET], - 'zf2-post' => [ZendRouter::class, RequestMethod::METHOD_POST], - 'zf2-put' => [ZendRouter::class, RequestMethod::METHOD_PUT], - 'zf2-delete' => [ZendRouter::class, RequestMethod::METHOD_DELETE], - 'zf2-patch' => [ZendRouter::class, RequestMethod::METHOD_PATCH], - ]; - } - /** * @dataProvider allowedMethod * @@ -407,11 +431,14 @@ public function notAllowedMethod() */ public function testAllowedMethodsWhenOnlyPutMethodSet($adapter, $method) { - $app = new Application(new $adapter()); - $app->pipeRoutingMiddleware(); - $app->pipe(new Middleware\ImplicitHeadMiddleware()); - $app->pipe(new Middleware\ImplicitOptionsMiddleware()); - $app->pipeDispatchMiddleware(); + $router = new $adapter(); + $app = $this->createApplicationFromRouter($router); + $app->pipe(new RouteMiddleware($router)); + $app->pipe(new ImplicitHeadMiddleware($router, function () { + })); + $app->pipe(new ImplicitOptionsMiddleware($this->responseFactory)); + $app->pipe(new MethodNotAllowedMiddleware($this->responseFactory)); + $app->pipe(new DispatchMiddleware()); // Add a PUT route $app->put('/foo', function ($req, $res, $next) { @@ -420,76 +447,19 @@ public function testAllowedMethodsWhenOnlyPutMethodSet($adapter, $method) return $res->withBody($stream); }); - $delegate = $this->prophesize(DelegateInterface::class); - $delegate->process(Argument::type(ServerRequest::class)) + $handler = $this->prophesize(RequestHandlerInterface::class); + $handler->handle(Argument::type(ServerRequest::class)) ->shouldNotBeCalled(); - $request = new ServerRequest(['REQUEST_METHOD' => $method], [], '/foo', $method); - $result = $app->process($request, $delegate->reveal()); - - $this->assertEquals(StatusCode::STATUS_OK, $result->getStatusCode()); - $this->assertEquals('', (string) $result->getBody()); - } - - /** - * @dataProvider allowedMethod - * - * @param string $adapter - * @param string $method - */ - public function testAllowedMethodsWhenNoHttpMethodsSet($adapter, $method) - { - $app = new Application(new $adapter()); - $app->pipeRoutingMiddleware(); - $app->pipe(new Middleware\ImplicitHeadMiddleware()); - $app->pipe(new Middleware\ImplicitOptionsMiddleware()); - $app->pipeDispatchMiddleware(); - - // Add a route with empty array - NO HTTP methods - $app->route('/foo', function ($req, $res, $next) { - $stream = new Stream('php://temp', 'w+'); - $stream->write('Middleware'); - return $res->withBody($stream); - }, []); - - $delegate = $this->prophesize(DelegateInterface::class); - $delegate->process(Argument::type(ServerRequest::class)) - ->willReturn($this->response); - $request = new ServerRequest(['REQUEST_METHOD' => $method], [], '/foo', $method); - $result = $app->process($request, $delegate->reveal()); - - $this->assertEquals(StatusCode::STATUS_OK, $result->getStatusCode()); - $this->assertEquals('', (string) $result->getBody()); - } - - /** - * @dataProvider notAllowedMethod - * - * @param string $adapter - * @param string $method - */ - public function testNotAllowedMethodWhenNoHttpMethodsSet($adapter, $method) - { - $app = new Application(new $adapter()); - $app->pipeRoutingMiddleware(); - $app->pipeDispatchMiddleware(); + $result = $app->process($request, $handler->reveal()); - // Add a route with empty array - NO HTTP methods - $app->route('/foo', function ($req, $res, $next) { - $stream = new Stream('php://temp', 'w+'); - $stream->write('Middleware'); - return $res->withBody($stream); - }, []); - - $delegate = $this->prophesize(DelegateInterface::class); - $delegate->process(Argument::type(ServerRequest::class)) - ->shouldNotBeCalled(); - - $request = new ServerRequest(['REQUEST_METHOD' => $method], [], '/foo', $method); - $result = $app->process($request, $delegate->reveal()); - $this->assertEquals(StatusCode::STATUS_METHOD_NOT_ALLOWED, $result->getStatusCode()); - $this->assertNotContains('Middleware', (string) $result->getBody()); + if ($method === RequestMethod::METHOD_OPTIONS) { + $this->assertSame(StatusCode::STATUS_OK, $result->getStatusCode()); + } else { + $this->assertSame(StatusCode::STATUS_METHOD_NOT_ALLOWED, $result->getStatusCode()); + } + $this->assertSame('', (string) $result->getBody()); } /** @@ -501,22 +471,25 @@ public function testNotAllowedMethodWhenNoHttpMethodsSet($adapter, $method) */ public function testWithOnlyRootPathRouteDefinedRoutingToSubPathsShouldDelegate($adapter) { - $app = new Application(new $adapter()); - $app->pipeRoutingMiddleware(); + $router = new $adapter(); + $app = $this->createApplicationFromRouter($router); + $app->pipe(new RouteMiddleware($router)); - $app->route('/', function ($req, $res, $next) { + $response = clone $this->response; + $app->route('/', function ($req, $handler) use ($response) { $stream = new Stream('php://temp', 'w+'); $stream->write('Middleware'); - return $res->withBody($stream); + return $response->withBody($stream); }, ['GET']); - $expected = (new Response())->withStatus(StatusCode::STATUS_NOT_FOUND); - $delegate = $this->prophesize(DelegateInterface::class); - $delegate->process(Argument::type(ServerRequest::class)) + $expected = $this->response->withStatus(StatusCode::STATUS_NOT_FOUND); + $handler = $this->prophesize(RequestHandlerInterface::class); + $handler + ->handle(Argument::type(ServerRequest::class)) ->willReturn($expected); $request = new ServerRequest(['REQUEST_METHOD' => 'GET'], [], '/foo', 'GET'); - $result = $app->process($request, $delegate->reveal()); + $result = $app->process($request, $handler->reveal()); $this->assertSame($expected, $result); } } diff --git a/test/TestAsset/CallableInteropMiddleware.php b/test/TestAsset/CallableInteropMiddleware.php index 74e99801..cf620678 100644 --- a/test/TestAsset/CallableInteropMiddleware.php +++ b/test/TestAsset/CallableInteropMiddleware.php @@ -1,20 +1,23 @@ process($request); + $response = $handler->handle($request); + return $response->withHeader('X-Callable-Interop-Middleware', __CLASS__); } diff --git a/test/TestAsset/ContainerException.php b/test/TestAsset/ContainerException.php index d438ef8e..5d672192 100644 --- a/test/TestAsset/ContainerException.php +++ b/test/TestAsset/ContainerException.php @@ -1,10 +1,12 @@