Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings
This repository was archived by the owner on Jan 29, 2020. It is now read-only.

Refactor: extract middleware marshaling, routing, and app running#543

Merged
weierophinney merged 53 commits intozendframework:release-3.0.0fromweierophinney:feature/stratigility-bc-breaks
Feb 5, 2018
Merged

Refactor: extract middleware marshaling, routing, and app running#543

weierophinney merged 53 commits intozendframework:release-3.0.0fromweierophinney:feature/stratigility-bc-breaks
Feb 5, 2018

Conversation

@weierophinney
Copy link
Member

@weierophinneyweierophinney commentedJan 25, 2018
edited
Loading

This patch implements the changes as outlined in theRFC: Expressive 3 Design Changes, as well as those as outlined inthis comment andthis comment, below.

In particular:

  • Updates to consume and implement PSR-15.

  • The following changes were made to middleware marshaling:

    • A new class,MiddlewareContainer, implementing PSR-11'sContainerInterface and decorating aContainerInterface. This class ensures that middleware pulled from the container implements the PSR-15MiddlewareInterface. It also allows retrieving middleware by class name if the class does not require constructor arguments.

    • A new class,MiddlewareFactory, accepts a PSR-11ContainerInterface to its constructor, which it decorates internally as aMiddlewareContainer. It then exposes methods for marshaling middleware:

      • callable(callable $middleware) : CallableMiddlewareDecorator
      • lazy(string $middleware) : LazyLoadingMiddleware
      • pipeline(...$middleware) : MiddlewarePipeline
      • path(string $path, $middleware) : PathMiddlewareDecorator
      • prepare($middleware) : MiddlewareInterface; this one proxies to the other methods.

      Double-pass middleware is not supported.

  • A new class,ApplicationRunner, composes:

    • ARequestHandlerInterface instance (typically, aMiddlewarePipeInterface)
    • AnEmitterInterface instance (typically, anEmitterStack composing aSapiEmitter)
    • A callable factory for generating aServerRequestInterface instance
    • A callable response generator, used when the server request factory raises a throwable or exception.

    Whenrun() is called, the class marshals a request, potentially generates an error response, and otherwise passes handling of the request to the handler; it then passes the returned response to the emitter.

    This class can potentially be extracted to its own library.

  • TheRouteMiddleware has been updated to add the methodsroute,get,post, etc. fromApplication. UnlikeApplication, they only acceptMiddlewareInterface instances.

  • New factories (in theZend\Expressive\Container namespace):

    • RouteMiddlewareFactory
    • DispatchMiddlewareFactory
    • EmitterFactory should be mapped toZend\Diactros\Response\EmitterInterface.
    • ServerRequestFactoryFactory should be mapped toZend\Expressive\ServerRequestFactory
    • ServerRequestErrorResponseGeneratorFactory should be mapped toZend\Expressive\ServerRequestErrorResponseGenerator.
  • Application has been completely rewritten. It now composes:

    • AMiddlewareFactory instance
    • AMiddlewarePipeInterface instance, generally aMiddlewarePipe
    • ARouteMiddleware instance
    • AnApplicationRunner instance

    It implementsRequestHandlerInterface andMiddlewareInterface, and composes theApplicationConfigInjectionTrait. It defines the same public API as previously, minus the various getters (as none of them are applicable anymore). Each method now proxies to the appropriate collaborator, pre-processing arguments as necessary:

    • handle() delegates to theMiddlewarePipeInterface
    • process() delegates to theMiddlewarePipeInterface
    • run() delegates to theApplicationRunner
    • pipe() delegates to theMiddlewarePipeInterface, after first passing its arguments to theMiddlewareFactory, and, in the case of two arguments, theZend\Stratigility\path() utility function.
    • route() and related methods delegate to theRouteMiddleware, after first passing the$middleware argument to theMiddlewareFactory
    • pipeRoutingMiddleware andpipeDispatchMiddleware are removed; users will now pipe these middleware just like any other middleware.
  • Zend\Expressive\Delegate\NotFoundDelegate was renamed toZend\Expressive\Handler\NotFoundHandler.

  • Zend\Expressive\Middleware\NotFoundHandler was renamed toZend\Expressive\Middleware\NotFoundMiddleware; its related factory in theZend\Expressive\Container namespace was similarly renamed.

  • Tests were updated to reflect the changes, as necessary.

Updates

  • 2018-01-30: Updated to reflect refactors of routing and app running.

@weierophinneyweierophinney added this to the3.0.0alpha1 milestoneJan 25, 2018

namespaceZend\Expressive;

usePsr\Http\Server\MiddlewareInterface;
Copy link
Member

Choose a reason for hiding this comment

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

MiddlewareInterface is not used.


$app =newApplication($router,$container,$delegate,$emitter);
$response =$container->has(ResponseInterface::class)
?$container->get(ResponseInterface::class)
Copy link
Member

Choose a reason for hiding this comment

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

ResponseInterface needs import.

Copy link
MemberAuthor

Choose a reason for hiding this comment

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

Done.


useInterop\Http\Server\MiddlewareInterface;
useInterop\Http\Server\RequestHandlerInterface;
usePsr\Container\ContainerInterface;
Copy link
Member

Choose a reason for hiding this comment

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

ContainerInterface not used.

usePsr\Http\Message\ServerRequestInterface;
usePsr\Http\Server\MiddlewareInterface;
usePsr\Http\Server\RequestHandlerInterface;
useZend\Expressive\MarshalMiddlewareTrait;
Copy link
Member

Choose a reason for hiding this comment

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

MarshalMiddlewareTrait doesn't exist.

usePsr\Http\Server\RequestHandlerInterface;
useZend\Expressive\MarshalMiddlewareTrait;
useZend\Expressive\Router\RouteResult;
useZend\Expressive\Router\RouterInterface;
Copy link
Member

Choose a reason for hiding this comment

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

RouterInterface isn't used.

composer.json Outdated
"http-interop/http-server-middleware":"^1.0.1",
"psr/container":"^1.0",
"psr/http-message":"^1.0.1",
"psr/http-server-middleware":"^1.0.0",

Choose a reason for hiding this comment

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

Usually we use just^1.0.

{
return$this->route($path,$middleware, ['POST'],$name);
try {
$request =$request ?: ServerRequestFactory::fromGlobals();

Choose a reason for hiding this comment

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

One space before= please

/**
* @param callable|string $middleware Middleware (or middleware service name) to associate with route.
* @param null|string $name The name of the route.
* @param string|array|callable|MiddlewareInterface $middleware Middleware

Choose a reason for hiding this comment

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

As I've suggested in another place order of types -null|(simple types in alphabetical order)|(complex types in alphabetical order) - so here we should havearray|callable|string|MiddlewareInterface.

Copy link
MemberAuthor

Choose a reason for hiding this comment

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

I would argue for the following order:

  • null
  • all other scalar types, in alphabetical order
  • complex built-in types (array,callable,resource) in alphabetical order
  • instance types, in alphabetical order

So, clearly we have differing opinions, and we'll need to make a decision. In the meantime, this is not the main point of this PR at this time; I need feedback onarchitecture anddesign.

* @copyright Copyright (c) 2018 Zend Technologies USA Inc. (https://www.zend.com)
* @license https://github.com/zendframework/zend-expressive/blob/master/LICENSE.md New BSD License
*/

Choose a reason for hiding this comment

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

No new line before strict_type declaration please.

Copy link
Member

Choose a reason for hiding this comment

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

@webimpress let's try reducing noise over CS by moving these discussions to the CS checks repo instead 👍

froschdesign and byan reacted with thumbs up emoji
Copy link
Member

Choose a reason for hiding this comment

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

I'm also little bit unhappy with this type of comments. Because the first comments are always related to the coding standard. After thenoise, there are often no other comments. CS is important, but code formatting should not have the first place in this and other discussions. (imo)

Copy link
Member

Choose a reason for hiding this comment

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

Coding Standard Police 😉

Choose a reason for hiding this comment

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

This is why we need proper CS for zf and then we are not going to have these comments at all. Sorry, I'm adding comments about everything I can see at the beginning because otherwise (it happened already couple times) it could be merged without fixing...

Copy link
Member

@froschdesignfroschdesignJan 26, 2018
edited
Loading

Choose a reason for hiding this comment

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

@webimpress
Sorry, my comment was not only for you. It was a hint to:zendframework/zend-coding-standard/pull/1

ping@weierophinney

Choose a reason for hiding this comment

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

Sorry guys, actually I was wrong... Probably we should keep the line. See my comments there:
zendframework/zend-expressive-aurarouter#30 (comment)

{
publicfunction__invoke(ContainerInterface$container) :DispatchMiddleware
{
returnnewDispatchMiddleware();

Choose a reason for hiding this comment

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

Do we need that factory? Isn't itinvokable?

Copy link
MemberAuthor

Choose a reason for hiding this comment

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

Itis invokable, but it may not always be. Additionally, having it here simplifies how we refer to it when writing configuration for different container types.

}

/**
* @var mixed $service The actual service created by the container.

Choose a reason for hiding this comment

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

@param instead of@var please

return$this->pipeline(...$middleware);
}

if ((!is_string($middleware) ||empty($middleware))) {

Choose a reason for hiding this comment

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

remove one pair of parentheses and the second part can be just! $middleware

{
if (!$this->container) {
thrownewException\ContainerNotRegisteredException(sprintf(
'Cannot marshal middleware by service name "%s"; no container registered',

Choose a reason for hiding this comment

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

Maybe it should be moved to the static method in the exception?


publicfunction__construct(ContainerInterface$container =null)
{
$this->container =$container ?newMiddlewareContainer($container) :null;

Choose a reason for hiding this comment

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

For better testability we should passMiddlewareContainer as param instead of creating one in the constructor.

Copy link
MemberAuthor

Choose a reason for hiding this comment

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

It makes usability harder, however, as developers who are creating an instance on the fly now need to do the following:

$factory =newMiddlewareFactory(newMiddlewareContainer($app->getContainer()));

Additionally, the above example I present would fail with anE_FATAL if$app->getContainer() returnsnull (which is possible currently), which means it technically needs to become:

$factory =newMiddlewareFactory($app->getContainer()    ?newMiddlewareContainer($app->getContainer())    :null);

which is not user friendly at all. (FTR, if the middleware in a specification or a pipeline resolves to an constructor-less class, a callable, or aMiddlewareInterface instance, the factory can still do its work; it's only if a service name is used that the container becomes necessary. At that point, it raises an exception when a container is missing.)

Copy link
MemberAuthor

Choose a reason for hiding this comment

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

I've since changed this to accept only aMiddlewareContainer instance.

michalbundyra reacted with thumbs up emoji
<?php
/**
* @see https://github.com/zendframework/zend-expressive for the canonical source repository
* @copyright Copyright (c) 2016-2017 Zend Technologies USA Inc. (https://www.zend.com)

Choose a reason for hiding this comment

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

2018 only, please

Copy link
MemberAuthor

Choose a reason for hiding this comment

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

This file was renamed fromNotFoundHandler; date range is correct.

Choose a reason for hiding this comment

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

Did you usegit mv to rename the file? We are loosing the connection between new and old file, it looks like new file...
Shouldn't we update year range as the file is updated (renamed)?

Copy link
Member

Choose a reason for hiding this comment

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

best way is to move file 100% unchanged, and then edit it in another commit as git does not really have "move" type of change. Simple diff might not detect it anyway as it uses similarity threshold. It is more for a 3-way merge benefit.

Copy link
Member

Choose a reason for hiding this comment

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

... or use PhpStorm :D

It keeps track of moved files (most of the time) so you can move and edit it in the same commit. Not sure how to do that in git. But as PhpStorm can do it, it must be doable with git as well.

Copy link
MemberAuthor

Choose a reason for hiding this comment

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

I did agit mv, but then also added the new file in the same commit, which is why this happened.

Note to self: be more granular in commits.

michalbundyra reacted with thumbs up emoji
@michalbundyra
Copy link
Member

@weierophinney

A new class, MiddlewareContainer, implementing PSR-11's ContainerInterface and decorating a ContainerInterface. This class ensures that middleware pulled from the container implements the PSR-15 MiddlewareInterface. It also allows retrieving middleware by class name if the class does not require constructor arguments.

Do we need it? Maybe we should just register these type of middlewares as invokable in container?

*
* @param string|array|MiddlewareInterface $middleware
*/
publicfunctionpath(string$path,$middleware) :PathMiddlewareDecorator

Choose a reason for hiding this comment

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

Maybe the same way we should add methodhost?

I'm looking again through the code, and it doesn't to be right, imho. Looks like we can do the same things in multiple ways now, and it's confusing. I think we should deliver simple interface for devs. Need a bit more time to think about it...

Copy link
MemberAuthor

Choose a reason for hiding this comment

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

I'm considering removing thepath() method, as the same can be accomplished with the following:

usefunctionZend\Stratigility\path;$app->pipe(path('/api',$factory->prepare($middleware));

If we keep the methods down to only those needed for preparing middleware of different types (prepare(),callable(),lazy()) and pipelines (pipeline()), we can use these with any of the various decorator classes and/or their related utility functions. That means we can add new decorators and utility functions in Stratigility or Expressive, and they're automatically supported.

geerteltink, danizord, and michalbundyra reacted with thumbs up emoji
@weierophinney
Copy link
MemberAuthor

weierophinney commentedJan 29, 2018
edited
Loading

A new class, MiddlewareContainer, implementing PSR-11's ContainerInterface and decorating a ContainerInterface. This class ensures that middleware pulled from the container implements the PSR-15 MiddlewareInterface. It also allows retrieving middleware by class name if the class does not require constructor arguments.

Do we need it? Maybe we should just register these type of middlewares as invokable in container?

Ideally, that's what we'd do. However, that poses a few problems:

  • For users relying on that functionality currently, their code would break after upgrading, until they add theseinvokables declarations.
  • From a usability perspective, if you're declaring a one-off class that has no dependencies, requiring adding configuration for it is more work that is prone to error (forgetting to add the factory).
  • From a context perspective: if we are pulling middleware services from the container, we want to be assured that we don't accidentally pull a non-middleware service due to misconfiguration. Having a check on the type fetched is useful in that regard. (And analogous to the scopedControllerManager in zend-mvc.)

We can largely solve the second use case via the zend-expressive-tooling project; in fact,there is an open issue requesting creation of a factory when middleware:create is called. The first is harder, however; unless we can write a tool to solve that migration problem, it's simpler to have the dedicated container.

The third case, however, is probably the more winning argument here, as it ensures that weonly fetch middleware in the context ofmarshaling middleware for the pipeline and/or routing.

@weierophinney
Copy link
MemberAuthor

weierophinney commentedJan 29, 2018
edited
Loading

@Ocramius ,@froschdesign ,@webimpress,@ezimuel,@xtreamwayz — any feedback on design/implementation?

Thoughts I've had since implementing (some of which are due to discussions with@ezimuel and@xtreamwayz):

  • Creating factories for both theMiddlewareContainer andMiddlewareFactory classes. This would allow us to re-use each (though the former would not likely get used outside the latter), and allow us to provide stronger type-hinting on theMiddlewareFactory, resolving the discussion with@webimpress, above.

  • Pushing all routing-related methods, the router itself, and aMiddlewareFactory instance, into theRouteMiddleware. Users would register route-based middleware with that instance:

    $routes->get('/', HomeMiddleware::class,'home');$routes->post('/api/books', [    CorsMiddleware::class,    AuthenticationMiddleware::class,    CreateBookMiddleware::class]);

    My inclination is also to have the$middleware argument be strongly typed againstMiddlewareInterface (more on that below).

    This has the benefit of simplifying theApplication API, and moving the routing functionality closer to the middleware that uses it.

  • HaveApplication implementMiddlewarePipeInterface directly, and only accept middleware instances. This means theApplication instance does not need to compose a container any longer, much less theMiddlewareFactory. More on this later.

  • Splitting the functionality of piping/processing/handling fromrunning an application.

    What do I mean by that?

    Right now, the remaining complexity of theApplication class, once routing concerns are removed, is around therun() method, as that method requires the following:

    • A way to generate aServerRequestInterface instance if none is provided. This is currently via the static methodServerRequestFactory::fromGlobals(), but we should likely make this configurable in order to accommodate PSR-17 factories in the future.
    • A way to generate an error response in the case that the server request factory raises an exception. This is done currently in two ways, depending on collaborators present:
      • If the instance has a container, and the container has anErrorResponseGenerator, that instance is used to generate the response, by passing an emptyServerRequest instance (created on the fly) and a composed response prototype.
      • Otherwise, the response is generated by manipulating the composed response prototype.
    • An emitter, for emitting the response.

    If we were to split responsibilities, theApplication class, as it is currently named would compose only:

    • a default handler, for times whenhandle() is called.

    We could then create a new class, perhaps anApplicationRunner, that could compose the following:

    • An emitter. The container would define anEmitterInterface service that resolves to anEmitterStack with theSapiEmitter composed by default.
    • AServerRequestErrorResponseGenerator service; this would be used when unable to marshal the server request, in order to generate the response. It should be capable of receiving only an exception. (A factory could close over theErrorResponseGenerator in order to provide a defaultServerRequest and response prototype, so as to allow re-use of existing functionality.)
  • Create a proxy class that composes theRouteMiddleware,Application, andApplicationRunner, along with aMiddlewareFactory. This class wouldmimic the current v2 API with regards to piping and routing. Its primary use case would be for applicationsupgrading from v2.

Essentially, for usersupgrading from v2, we'd update theirApplication service to resolve to the proxy class. This would allow them to gradually upgrade to the v3 APIs. In v3, we'd alter thepublic/index.php self-executing function: we'd import the application, routing middleware, and middleware factory as variables before calling therequire statements:

$app =$container->get(Application::class);$factory =$container->get(MiddlewareFactory::class);$routes =$container->get(RouteMiddleware::class);require'config/pipeline.php';require'config/routes.php';

These files would then look like the following:

// config/pipeline.php:usefunctionZend\Stratigility\path;$app->pipe($factory->lazy(ErrorMiddleware::class));$app->pipe(path('/api',$factory->pipeline(    CorsMiddleware::class,    AuthenticationMiddleware::class,    AuthorizationMiddleware::class));// Since we already have it instantiated, we can pipe the routing middleware as// an instance:$app->pipe($routes);$app->pipe($factory->lazy(ImplicitHeadMiddleware::class));$app->pipe($factory->lazy(ImplicitOptionsMiddleware::class));$app->pipe($factory->lazy(DispatchMiddleware::class));$app->pipe($factory->lazy(NotFoundMiddleware::class));// config/routes.php:$routes->get('/',$factory->lazy(HomeMiddleware::class));$routes->post('/api/books',$factory->pipeline(    BookValidationMiddleware::class,    CreateBookMiddleware::class));

Essentially, separating these concerns further means we can have more type-safety, less overall code, and more flexibility for users.

The proxy class provides both a migration tool, but also potentially ausability layer for developers who do not want to always use the factory to prepare middleware for piping or routing, but instead just provide the arguments.

Updates

  • 2018-01-30: Updated last example to use$factory->lazy(<class name>) instead of<class name> to demonstrate suggested design, vs. current.

@geerteltink
Copy link
Member

About everything you write sounds good. I'm not sure about theApplicationRunner though. I mean right now I need to grab the application from the container and execute$app->run(). That's pretty easy. How would this work with theApplicationRunner? On the other hand, if the complexity of the Application class itself is reduced that might be worth it.

The proxy class for backward compatibility sounds good as well.

@weierophinney
Copy link
MemberAuthor

I mean right now I need to grab the application from the container and execute $app->run().

TheApplicationRunner would compose theApplication instance as well, and exposerun():

$runner =$container->get(ApplicationRunner::class);$runner->run();

Internally,run() would marshal the request and response (if not provided), call on the composed application'shandle() method (passing it the request), and then pass the response to the emitter.

geerteltink reacted with thumbs up emoji

@Xerkus
Copy link
Member

May i suggest a following change:

(require'config/pipeline.php')($app,$factory,$router);(require'config/routes.php')($app,$factory,$router);
useZend\Expressive\Application;usefunctionZend\Stratigility\path;returnfunction (Application$app,MiddlewareFactory$factory,RouteMiddleware$routes) {$app->pipe(ErrorMiddleware::class);$app->pipe(path('/api',$factory->pipeline(        CorsMiddleware::class,        AuthenticationMiddleware::class,        AuthorizationMiddleware::class    ));// Since we already have it instantiated, we can pipe the routing middleware as// an instance:$app->pipe($routes);$app->pipe(ImplicitHeadMiddleware::class);$app->pipe(ImplicitOptionsMiddleware::class);$app->pipe(DispatchMiddleware::class);$app->pipe(NotFoundMiddleware::class);}
weierophinney, geerteltink, michalbundyra, malukenho, knoxzin1, and alextech reacted with thumbs up emoji

@weierophinney
Copy link
MemberAuthor

May i suggest a following change:

I really like that idea!

@Xerkus
Copy link
Member

We could then create a new class, perhaps an ApplicationRunner, that could compose the following:

  • An emitter. The container would define an EmitterInterface service that resolves to an EmitterStack with the SapiEmitter composed by default.
  • A ServerRequestErrorResponseGenerator service; this would be used when unable to marshal the server request, in order to generate the response. It should be capable of receiving only an exception. (A factory could close over the ErrorResponseGenerator in order to provide a default ServerRequest and response prototype, so as to allow re-use of existing functionality.)

This is a good functionality to be split into separate package to be used with any request handler.
In zend-mvc refactoring for PSR I made Application a request handler and could reuse ApplicationRunner and emitter stack.

@geerteltink
Copy link
Member

May i suggest a following change:

You have shown me this before. I really like it.

@weierophinney
Copy link
MemberAuthor

You have shown me this before. I really like it.

The pattern used to be very common in node, before theimport construct was added for ES6. As such, it's quite familiar to me; just not in aPHP context!

@ezimuel
Copy link
Contributor

ezimuel commentedJan 30, 2018
edited
Loading

@weierophinney I really like the idea of having routes specified on aRouteMiddleware instance ($routes). In this way the routing configuration is related to a route object and not the application itself.

I see the need for apath() function that returns a middleware but I would like to simplify the $factory usage for the middleware pipeline. IMHO, the syntax that you proposed it's too verbose:

$app->pipe(path('/api',$factory->pipeline(    CorsMiddleware::class,    AuthenticationMiddleware::class,    AuthorizationMiddleware::class)));

I think we should continue to supportarray as a way to specify a pipeline. I think this is a very intuitive idea to think about anordered sequence of middleware (a pipeline). I would like to propose this change:

$app->pipe(path('/api, [    CorsMiddleware::class,    AuthenticationMiddleware::class,    AuthorizationMiddleware::class]));

We can accomplish this managing the array of middleware in thepath() function itself, without changing theApplication::pipe function. The same idea can be used for the variousget,post,put, ... functions of routes. Again, the usage of array is a very intuitive way to define a pipeline, and people using Expressive are already familiar with that, we should not change this API.

This routing is very intuitive, even for people not familiar with Expressive:

$routes->post('/api/books', [    BookValidationMiddleware::class,    CreateBookMiddleware::class]);

Regarding the other proposals, I'm in favor of it. It will be awesome to see a complete skeleton application to analyze the changes from a user API point of view.

@weierophinney
Copy link
MemberAuthor

I'm just about done handling the changesas suggested in my previous large design comment, and in doing so, discovered something interesting: separating the routing concerns, removing the application running concerns, and making thepipe() method strongly typed means that theApplication class is essentially just aMiddlewarePipe instance.

As such, I'm re-creating theApplication class to compose the following:

  • aMiddlewareFactory instance
  • aMiddlewarePipeInterface implementation
  • aRouteMiddleware instance
  • anApplicationRunner instance

The class then implements bothRequestHandlerInterface andMiddlewareInterface, and keeps most public methods from the current v2 implementation (minusgetContainer(),getEmitter(), andgetDefaultHandler() methods). The methods it exposes all proxy to one or more of the above classes.

What's interesting about this approach is that it keeps the existing signatures and APIentirely.

This then moves the discussion to: how do we want to write the skeleton?

It could literally stay exactly the same as it is currently. Alternately, we could do like@Xerkus suggested above, and have the routes and pipeline files return callbacks that accept specific arguments (likely the middleware factory, and then one of either the middleware pipeline or route middleware).

What's interesting to me is that this refactoring exercise leaves us with far more flexibility; simpler, more maintainable code; stronger typing; and choice in how we have users consume the project.

Once the changes are complete, I'll push here.

@weierophinneyweierophinney changed the titleRefactor: extract middleware marshalingRefactor: extract middleware marshaling, routing, and app runningJan 30, 2018
@weierophinney
Copy link
MemberAuthor

All major refactors are now complete. The only bits I'm mulling over at this time are:

  • Moving the functionality ofApplicationConfigInjectionTrait into another class using static methods:ApplicationConfigInjector::injectPipelineFromConfig(Application $application, array $config), etc.
  • Removing theMiddlewareFactory::path() method.

Thoughts on these proposals and/or the shape of the refactor at this time?

* Creates and returns a PathMiddlewareDecorator instance after first
* passing the $middleware argument to prepare().
*
* @param string|array|MiddlewareInterface $middleware
Copy link
Member

@XerkusXerkusJan 30, 2018
edited
Loading

Choose a reason for hiding this comment

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

missing@param for first parameter. I don't know if all IDEs will interpret this properly.

Copy link
MemberAuthor

Choose a reason for hiding this comment

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

This is standard across major IDEs, at this point, and phpdocumentor infers types from typehints as well. We've already decided for zend-coding-standard to require annotations only if they're adding information, or the type is unspecified.

michalbundyra reacted with thumbs up emoji
returnnewApplicationRunner(
$container->get(Application::class),
$container->get(EmitterInterface::class),
$container->get(ServerRequestFactory::class),
Copy link
Member

Choose a reason for hiding this comment

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

ServerRequestFactory and ServerRequestErrorResponseGenerator are not real classes, shouldn't they be used as string literals?

Copy link
MemberAuthor

Choose a reason for hiding this comment

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

I go back and forth on this, particularly when the service namespace doesn't match the current one.

We can address later.

usePsr\Container\ContainerInterface;
usePsr\Http\Message\ResponseInterface;
useZend\Expressive\Middleware\RouteMiddleware;
useZend\Expressive\Router\RouteInterface;
Copy link
Member

Choose a reason for hiding this comment

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

Typo, should be RouterInterface

* Add a route for the route middleware to match.
*
* Accepts either a Route instance, or a combination of a path and
* middleware, and optionally the HTTP methods allowed.
Copy link
Member

Choose a reason for hiding this comment

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

This description is wrong, Route instance is not accepted

@danizord
Copy link
Contributor

@weierophinney

Removing the MiddlewareFactory::path() method.

I'm 👍 for this. IMHO theMiddlewareFactory should focus only on creating middleware fromstring|callable|array, otherwise we would end up adding various other methods for each decorator likehost()doublePass() and such.

For the shape of the refactor, I need to review it with more time tomorrow, if you can wait one more day before merging. I missed the notification of this PR. 😄

Just a few comments from what I've read so far:

  • I love the idea of extracting all theApplication behaviors into separate participants and making them usable outside ofApplication. That makes Expressive even more flexible and composable. Now I can switch to another middleware pipeline implementation, use a 3rd party router/dispatch middleware, or even build my own framework on top of Expressive components, which is great :)
  • What about renamingApplicationRunner toHttpServer or something like that? Also, if you could extract it to a separate package, it would be really nice :)
  • Please renameMiddlewareFactory:: prepare() to__invoke() so that we can use it like a function:$middleware(function ($req, $handler) {...}).
  • Not sure, but I think route configuration methods (post(),get(), ...) could be moved toZend\Expressive\Router\RouterInterface directly. That would simplifyRouterMiddleware implementation and allow using a custom router middleware that consumes the Expressive router but behaves diferently. For instance, I'd like to have a router that returns 404 response directly instead of calling the next middleware.

btw, congrats for the great work done in PSR-15 and Expressive 3 :)

@weierophinney
Copy link
MemberAuthor

What about renaming ApplicationRunner to HttpServer or something like that? Also, if you could extract it to a separate package, it would be really nice :)

@Xerkus has indicated he could use this for the zend-mvc refactor as well, so we will definitely extract it. My thought is to also extract the emitter interface and emitters from zend-stratigility either into their own package, or into the same package as the runner (which will consume them).

Please rename MiddlewareFactory:: prepare() to __invoke() so that we can use it like a function: $middleware(function ($req, $handler) {...}).

This makes usage more difficultwithin theApplication class andApplicationConfigInjectionTrait, however, as we either need to wrap a property (($this->factory)($middleware)) or call it directly ($this->factory->__invoke($middleware)).

I'll think on it.

Not sure, but I think route configuration methods (post(), get(), ...) could be moved to Zend\Expressive\Router\RouterInterface directly. That would simplify RouterMiddleware implementation and allow using a custom router middleware that consumes the Expressive router but behaves diferently. For instance, I'd like to have a router that returns 404 response directly instead of calling the next middleware.

TheRouterInterface is purposely small, to make it simpler to use. Because it's an interface, we cannot require features such as route duplication detection, which is currently something theApplication class handles (and, in this patch, theRouteMiddeware. Additionally, the routing methods we expose are aroundpath-based routing specifically; routingcould happen around such things as the scheme, host/authority, and even query parameters, as routes are passed a PSR-7 request itself.

My thought is that we could bring theRouteMiddleware from this patch into the zend-expressive-router package, and maybe call itPathBasedRoutingMiddleware to indicate that it's specific to that purpose. It could even expose a method to addRoute instances directly, for those cases when you want to add routes that are not examining the path.

If we were to go that route, I'd suggest:

  • A new minor release of zend-expressive-router that brings over the current v2RouteMiddleware as-is, but renaming it as suggested above, and providing a factory for it.
  • A new minor release of zend-expressive that hasRouteMiddleware extend the zend-expressive-router version.
  • In zend-expressive-router v3, we add the routing methods as we have them in this patch.
  • In zend-expressive v3, we removeRouteMiddleware entirely, and consume the middleware from zend-expressive-router.

Thoughts on that approach,@danizord?

I determined the `ZendTest\Expressive\IntegrationTest` to be obsolete;it's testing workflows that are tested elsewhere.The `ZendTest\Expressive\Router\IntegrationTest`, however, was aninteresting testbed in terms of seeing how the middleware pipeline,route middleware/router, and dispatch middleware work together,particularly in terms of the supported router _implementations_. Imodified the test setup to create a `Proxy` instance instead, to see iftests continued to pass. They did!
The new `Proxy` class will soon become the `Application` class.Since the AppFactory relied on the original design, it's obsolete.Particularly since the responsibilities are now separated between thefactory, pipeline, route middleware, and runner, using that approach nolonger seems appropriate.This patch also removes the ApplicationFactory, and all tests related tothe removed classes.
This patch renames the `Proxy` class to `Application`, and modifies itto use the `ApplicationConfigInjectionTrait` as well. Minormodifications were made on the trait due to the changes in the`Application` API and collaborators, and the trait's tests were updatedto properly configure the `Application` instance.
It now composes the required collaborators.It DOES NOT do configuration injection. Users will need to do that ontheir own via the `injectPipelineFromConfig()` and`injectRoutesFromConfig()` methods. Which I think I want to extract intoa purely functional utility class.
Ensures that the typo that snuck in during development will not continueto be an issue.
This patch resolves a circular dependency issue spotted by@Xerkus withregards to the `ApplicationRunner` and `Application` factories. It addsa new factory, `ApplicationPipelineFactory`, which will create andreturn a shared `Zend\Stratigility\MiddlewarePipe` instance; each of`ApplicationRunnerFactory` and `ApplicationFactory` now consume thisservice.
…onseGeneratorThis is what I'd intended originally.
This patch provides a ConfigProvider, which will allow offloading muchof the initial configuration provided in the skeleton to the componentitself.
Instead of using `path()` to create a `PathMiddlewareDecorator`, just`pipe()` both arguments to the `Application` instance.To make this work properly, the patch also does some additional logic tono longer create a `PathMiddlewareDecorator` if the the `$path` is `/`.
Also, discovered test case file did not reflect its class name.
This delegator factory decorates the application instance created by thecallback by injecting a pipeline and routes based on configurationpulled from the container.The functionality is directly from the ApplicationConfigInjectionTrait,which we can deprecate and remove now that this functionality is inplace.
This functionality is now in the ApplicationConfigInjectionDelegator.
This patch marks the two public injection methods in the delegatorfactory as static, allowing them to be used without instantiating thefactory.It also correctly places the tests for the delegator factory, and movesrequired test assests in a tree beneath it.
This is better accomplished using `Zend\Stratigility\path()` + thefactory.
The return type could potentially NOT be an `Application` in thesituation that `$callback` returns something else.
…ssive-router v3This patch updates zend-expressive to consume the `DispatchMiddleware`and `PathBasedRoutingMiddleware` classes from zend-expressive-routerv3.0.0alpha1 and up. Doing so allows removal of those classes andrelated tests from this package, but requires:- `Application` now needs to depend on `PathBasedRoutingMiddleware`- `ApplicationConfigInjectionDelegator` needs to use the new classes  when injecting routing and dispatch middleware.- tests needed updating to refer to the new classes.
This patch removes the `ApplicationRunner`, and replaces it withzend-httphandlerrunner's `RequestHandlerInterface` and emitters.
This patch updates the `MiddlewareContainer` and `MiddlewareFactory` toallow them to detect PSR-15 request handlers, and decorate them usingzend-stratigility's `RequestHandlerMiddleware`. This ensures type-safetywhen used in either pipelines or with routed middleware, but alsoensures we can use the full-spectrum of available handlers with thesystem.A new method was also added to `MiddlewareFactory`: `handler()`. Thismethod accepts a `RequestHandlerInterface` instance and returns a`RequestHandlerMiddleware` instance decorating it, making it simple touse request handlers with the explicit functionality ofzend-expressive-router's `PathBasedRoutingMiddleware` API.
@weierophinneyweierophinney merged commit6514d54 intozendframework:release-3.0.0Feb 5, 2018
@weierophinneyweierophinney deleted the feature/stratigility-bc-breaks branchFebruary 6, 2018 16:05
Sign up for freeto subscribe to this conversation on GitHub. Already have an account?Sign in.

Reviewers

6 more reviewers

@froschdesignfroschdesignfroschdesign left review comments

@OcramiusOcramiusOcramius left review comments

@XerkusXerkusXerkus left review comments

@danizorddanizorddanizord left review comments

@michalbundyramichalbundyramichalbundyra requested changes

@geerteltinkgeerteltinkgeerteltink left review comments

Reviewers whose approvals may not affect merge requirements

Assignees

No one assigned

Projects

None yet

Milestone

3.0.0alpha1

Development

Successfully merging this pull request may close these issues.

8 participants

@weierophinney@michalbundyra@geerteltink@Xerkus@ezimuel@danizord@froschdesign@Ocramius

[8]ページ先頭

©2009-2025 Movatter.jp