Skip to content
This repository has been archived by the owner on Jan 29, 2020. It is now read-only.

Implement programmatic pipeline features #396

Merged

Conversation

weierophinney
Copy link
Member

@weierophinney weierophinney commented Nov 11, 2016

This patch implements the Programmatic Pipelines RFC requirements.

TODO

  • Create Zend\Expressive\Middleware\ErrorResponseGenerator
  • Create Zend\Expressive\Middleware\WhoopsErrorResponseGenerator
  • Create Zend\Expressive\Middleware\NotFoundHandler
  • Create Zend\Expressive\Container\ErrorResponseGeneratorFactory
  • Create Zend\Expressive\Container\WhoopsErrorResponseGeneratorFactory
  • Create Zend\Expressive\Container\NotFoundHandlerFactory
  • Create Zend\Expressive\Container\ErrorHandlerFactory
  • Create Zend\Expressive\ApplicationConfigInjectionTrait, and compose it into Application.
  • Update Zend\Expressive\Container\ApplicationFactory to vary creation and return of the Application instance based on the zend-expressive.programmatic_pipeline configuration flag.
  • Update Zend\Expressive\Container\ApplicationFactory to use the methods defined in ApplicationConfigInjectionTrait to inject pipeline and routed middleware from configuration into the Application instance.
  • Update Zend\Expressive\Application to emit a deprecation notice from pipeErrorMiddleware().
  • Update Zend\Expressive\MarshalMiddlewareTrait to allow marshalling http-interop middleware.
  • Add support for "raise throwables" flag to ApplicationFactory.
  • Documentation

Tooling support

The following tools were written in zendframework/zend-expressive-tooling to support user migrations to work with the above changes:

  • Tool for generating programmatic pipelines and routing from existing configuration.
  • Tool for migrating getOriginal*() calls to getAttribute(*) calls on the request object.
  • Tool for identifying error middleware in an existing application.

References

@weierophinney weierophinney added this to the 1.1.0 milestone Nov 11, 2016
@weierophinney weierophinney changed the title [WIP] Implement programmatic pipeline features Implement programmatic pipeline features Nov 17, 2016
@weierophinney weierophinney removed the WIP label Nov 17, 2016
@weierophinney
Copy link
Member Author

Ready for review! Pinging:

Thanks in advance!

"cs-fix": "phpcbf",
"test": "phpunit",
"cs-check": "phpcs --colors",
"cs-fix": "phpcbf --colors",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--colors for phpcs and phpcbf is not needed since the new coding standard will have them enabled via the ruleset.xml

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good to know; I'll push that change shortly, after verifying the other build changes pass.

Copy link
Member

@geerteltink geerteltink left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've read the documentation changes so far. Later tonight or tomorrow night I'll go through the code.

I like vfsStream. We should used use it for the skeleton as well. It will make testing a lot cleaner and safer.

- `delete($path, $middleware, $name = null)`
- `route($path, $middleware, array $methods = null, $name = null)`

Each returns a `Zend\Expressive\Router\Route` instance; this is useful if you
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanx for mentioning this. No need anymore for routes via config.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has actually been true all along... just not well documented.

- Decorate the middleware in a `Zend\Stratigility\Middleware\CallableMiddlewareWrapper`
instance (which also requires a `$responsePrototype`).

We recommend that you begin writing middleware to follow the http-interop
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add an example here to clarify it a bit. I think it's something like this? (Disclaimer: from memory and untested)

<?php

namespace App\Middleware;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Interop\Http\Middleware\ServerMiddlewareInterface;
use Interop\Http\Middleware\DelegateInterface;

class MyMiddleware implements ServerMiddlewareInterface
{
    public function __construct()
    {
    }

    /**
     * @param Request           $request
     * @param DelegateInterface $delegate
     *
     * @return Response
     */
    public function process(ServerRequestInterface $request, DelegateInterface $delegate): ResponseInterface
    {
        // Do something (with the request) first

        // Call the next middleware and wait for the response
        $response = $delegate->process($request);

        // Do something (with the response) before returning the response

        // Return the response
        return $response;
    }
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent feedback! I'll incorporate that shortly!

middleware_ using the caught exception as the `$err` argument.

- The "Final Handler". This is a special middleware type with the signature
`function ServerRequestInterface $request, ResponseInterface $response, $err = null)`,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably missed a (


```php
include 'config/pipeline.php';
include 'config/routes.php';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't these use require?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, probably. Thanks for the suggestion; will implement shortly.

namespace ZendTest\Expressive;

use Closure;
use InvalidArgumentException;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is never used.


namespace Zend\Expressive;

use ReflectionProperty;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is never used.

use Zend\Diactoros\Response\EmitterInterface;
use Zend\Expressive\Application;
use Zend\Expressive\ApplicationUtils;
use Zend\Expressive\Exception;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is no longer used here, can be removed.

use Interop\Container\ContainerInterface;
use PHPUnit_Framework_TestCase as TestCase;
use Zend\Expressive\Container\ErrorResponseGeneratorFactory;
use Zend\Expressive\Middleware\ErrorResponseGenerator;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is never used.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is - but using the ::class notation, for assertInstanceOf checks.

use org\bovigo\vfs\vfsStream;
use org\bovigo\vfs\vfsStreamDirectory;
use PHPUnit_Framework_TestCase as TestCase;
use Prophecy\Argument;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused, please remove.

use Zend\Diactoros\Response\EmitterInterface;
use Zend\Expressive\Application;
use Zend\Expressive\ApplicationUtils;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't see this class in the library. Where is it?

* - 'Zend\Diactoros\Response\EmitterInterface'. If missing, an EmitterStack is
* created, adding a SapiEmitter to the bottom of the stack.
* - 'config' (an array or ArrayAccess object). If present, and it contains route
* definitions, these will be used to seed routes in the Application instance
* before returning it.
*
* When introspecting the `config` service, the following structure can be used
* to define routes:
* Please see the `Zend\Expressive\ApplicationUtils` class for details on how
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As above, I can't see this class in the library. Am I missing something?

*/
private function createOptionValue($value, $indentLevel = 1)
{
if (is_array($value) || $value instanceof Traversable) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Traversable should be imported or \Traversable here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

class WhoopsErrorResponseGenerator
{
/**
* @var Whoops
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems to be wrong type here.

* @param int $indentLevel
* @return string
*/
private function formatOptions($options, $indentLevel = 1)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is $options arg always an array? Can we add type hint here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It could be an ArrayObject.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So then PHPDoc should be updated.

## http-interop

Stratigility 1.3 provides the ability to work with [http-interop middleware
0.2.0](https://github.com/http-interop/http-middleware/tree/ff545c87e97bf4d88f0cb7eb3e89f99aaa53d7a9).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe better here link to tagged version:
https://github.com/http-interop/http-middleware/tree/0.2.0

@michalbundyra
Copy link
Member

In Zend\Expressive\Application there are some methods and properties marked as:

@deprecated This property will be removed in v1.1.

and also:

@todo For 1.1, remove the RouteResultSubjectInterface implementation, and
      all deprecated properties and methods.

Is it gonna be done in this PR, or PR #376 will be merged before release 1.1?

<?php
/**
* Expressive middleware pipeline
*/
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe will be nice to add here also:
@var \Zend\Expressive\Application $app
because then, in this file $app variable is gonna be used.
The same about TEMPLATE_ROUTES.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

@michalbundyra
Copy link
Member

Migration script:
I would expect that after using migration script my application will work straight away, but I've got error:

Fatal error: Uncaught exception 'Zend\Expressive\Exception\InvalidMiddlewareException' with message 'Unable to resolve middleware "string[Scalar]" to a callable' in my-app\vendor\zendframework\zend-expressive\src\MarshalMiddlewareTrait.php on line 65

This is because migration script is not adding:

    'dependencies' => [
        'invokables' => [
            OriginalMessages::class => OriginalMessages::class,

Into configuration, but:

$app->pipe(\Zend\Stratigility\Middleware\OriginalMessages::class);

is added into config/pipeline.php.

I think it should be done automatically (in migration script) or it should be noted in migration notes, that this action is required to be done manually.

@michalbundyra
Copy link
Member

michalbundyra commented Nov 18, 2016

One more issue. This was my config based configuration - config/autoload/middleware-pipeline.global.php:

[...]
    'middleware_pipeline' => 
        'my-path' => [
            'path' => '/my-path',
            'middleware' => [
                ApplicationFactory::ROUTING_MIDDLEWARE,
                \App\Middleware\MyMiddleware::class,
                \App\Middleware\OtherMiddleware::class,
            ],
        ],

it was successfully converted to middleware pipeline (via migration script) - config/pipeline.php:

[...]
$app->pipe('/my-path', [
    \Zend\Expressive\Application::ROUTING_MIDDLEWARE,
    \App\Middleware\MyMiddleware::class,
    \App\Middleware\OtherMiddleware::class,
]);
$app->pipeRoutingMiddleware();
[...]

And then piping is failing, because pipe method from Application call prepareMiddleware method from Zend\Expressive\MarshalMiddlewareTrait with $middleware = "EXPRESSIVE_ROUTING_MIDDLEWARE", and we got fatal:

Fatal error: Uncaught exception 'Zend\Expressive\Exception\InvalidMiddlewareException' with message 'Unable to resolve middleware "string[Scalar]" to a callable' in my-app\vendor\zendframework\zend-expressive\src\MarshalMiddlewareTrait.php on line 65

Am I doing something wrong or there are missing some scenarios, when we have some routing/dispatch middlewares only for some paths?

Update:

Ok, I think I know. So - config/pipeline.php should be:

[...]
$app->pipeRoutingMiddleware();
$app->pipe('/my-path', [
    \App\Middleware\MyMiddleware::class,
    \App\Middleware\OtherMiddleware::class,
]);
[...]

The question is now: is my previous middleware-pipeline configuration wrong or the migration script is doing it wrongly?

@michalbundyra
Copy link
Member

Another issue. "Method not allowed". I have routing, let say:

$app->post('/foo', \App\Action\Foo::class, 'foo');

So only POST request are allowed. If I try to do GET /foo I'm getting 200 response with no body.
I should get 405.

I think the problem is in \Zend\Expressive\Application::routeMiddleware:452 where we have:

        $result = $this->router->match($request);

        if ($result->isFailure()) {
            if ($result->isMethodFailure()) {
                $response = $response->withStatus(405)
                    ->withHeader('Allow', implode(',', $result->getAllowedMethods()));
                return $next($request, $response, 405); // <-------
            }
[...]

and then, the 3rd argument (405) is passed through \Zend\Stratigility\Next:119 where $err = 405. Error is not thrown (because $err is 405 not an instance of \Exception|\Throwable).
I can write here more, but I think it should be enough to investigate and fix this issue.

@weierophinney
Copy link
Member Author

Is it gonna be done in this PR, or PR #376 will be merged before release 1.1?

I'll be merging #376 before release, which covers these.

@weierophinney
Copy link
Member Author

Another issue. "Method not allowed". I

Should be resolved now; thanks for finding the issue!

@weierophinney
Copy link
Member Author

And then piping is failing, because pipe method from Application call prepareMiddleware method from Zend\Expressive\MarshalMiddlewareTrait with $middleware = "EXPRESSIVE_ROUTING_MIDDLEWARE", and we got fatal

I think this is an edge case we never tested.

When you use configuration-based pipelines, as it loops through nested pipelines, if it encounters either of those special constants, it replaces the middleware with the appropriate callable ([$this, 'routeMiddleware] and [$this, 'dispatchMiddleware]).

However, in the MarshalMiddlewareTrait, we do not.

I'm going to see if I can create a test case and resolve this; I think it should be relatively easy to accomplish.

Copy link
Contributor

@danizord danizord left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @weierophinney, thanks for this awesome work! This will definitely make the pipeline way more explicit and clear :)

? $container->get(ErrorResponseGenerator::class)
: null;

return new ErrorHandler(new Response(), $generator);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@weierophinney any chance to make it possible to configure ErrorHandler listeners via config? Or should I override this factory to attach my custom listeners?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use a delegator factory. 😉

@geerteltink
Copy link
Member

geerteltink commented Nov 18, 2016

@weierophinney I only see the config setting for the new error handling in the migration guide:
'zend-expressive' => ['raise_throwables' => true,],

Did you forgot $app->raiseThrowables(); or is the config setting preferred?

EDIT: Thinking about this, I think the config option would be preferred so an upgrade to v2.0 might go smoother since $app->raiseThrowables(); doesn't need to be removed.


Stratigility 1.3 deprecates its internal request and response decorators,
`Zend\Stratigility\Http\Request` and `Zend\Stratigility\Http\Response`,
respsectively. The main utility of these instances was to provide access in
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: respectively

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed, thanks!

@geerteltink
Copy link
Member

Also phpunit can be upgraded to 5.x since PHP 5.5 support is removed.

programmatic/declarative statements. Specifically:

- We recommend putting the pipeline declarations into `config/pipeline.php`.
- We recommend putting the pipeline declarations into `config/routes.php`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We recommend putting the pipeline declarations into `config/pipeline.php`.
We recommend putting the routing declarations into `config/routes.php`.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed, thanks!

@weierophinney
Copy link
Member Author

Did you forgot $app->raiseThrowables(); or is the config setting preferred?

You can do either; I didn't document the method, as technically it's part of the Stratigility 1.3 API, and documented there.

@weierophinney
Copy link
Member Author

Also phpunit can be upgraded to 5.x since PHP 5.5 support is removed.

Done in latest commit; thanks!

This patch adds `bin/expressive-tooling`, which will help you
install and remove the zendframework/zend-expressive-tooling package.
- is_executable does not work correctly on Windows; use file_exists.
- Grab $argv[1] instead of $argv[2]; default to an empty string.
…ckage

Details how to install and/or uninstall the tooling, and the commands it
exposes.
- Adds fig/http-message-util as a dependency
- Updates `NotFoundHandler` to use `StatusCodeInterface::STATUS_NOT_FOUND` instead of `404`.
@weierophinney weierophinney merged commit 33ff374 into zendframework:develop Dec 8, 2016
weierophinney added a commit that referenced this pull request Dec 8, 2016
weierophinney added a commit that referenced this pull request Dec 8, 2016
@weierophinney weierophinney deleted the feature/programmatic branch January 30, 2017 20:11
@weierophinney weierophinney modified the milestones: 1.1.0, 2.0.0 Jan 30, 2017
weierophinney added a commit that referenced this pull request Feb 8, 2017
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants