I'd like to highlight a practical example of a case where the proposed
middleware standard creates problems.

I had this one middleware already built:

https://github.com/mindplay-dk/middlemark

It basically takes a root-path and, if a given path matches a file in that
folder, it renders a markdown-file.

I had this other project, which also implements a piece of middleware, and
it needs to do that for markdown files, but only under certain conditions -
sometimes it renders a markdown file, sometimes it does something else.

Could I refactor and extract a service or something to make that possible?
Sure, but the component was already there, and it already does exactly what
I need.

The only problem was it's interface, which requires me to pass a delegate
as the second argument, even though I know it won't need to delegate - the
component that invokes it has already checked for the existence of the
file; if it had to delegate, that would mean there's an error in the
component that calls it.

If I want to reuse that middleware, I have two choices:

1. Implement a "fake" instance of DelegateInterface - it won't do anything,
it'll just satisfy the delegate argument.

2. Pull out a middleware stack (or "pipe") and dispatch the middleware
through it.

The first solution is an ugly work-around. Having to write a fake
implementation of DelegateInterface and having to explain why it's there
points at something being wrong, right?

The second solution is probably the more correct, but has it's own
problems: the middleware that calls the markdown middleware now needs to
depend on a specific implementation of a middleware-stack/pipe.

Essentially, the markdown middleware is a lambda - a function - it's
responsibility is to take a request and return a response, but it's method
signature makes it difficult to compose it by simply running it.

The proposed middleware interface effectively declares "there will always
be a delegate", which we know to be false - there is sometimes nothing to
delegate to, and attempting to delegate will trigger an exception, or some
other behavior defined by the middleware stack.

In other words, one of the reasons we need the middleware stack in the
first place, is to account for incidents where reality doesn't match what
was declared by the formal interface.

We've effectively managed to hide and important piece of information from
middleware components: we've made it impossible for a middleware component
to know if there's another component to delegate to or not. It has to
assume that there is - even though we know this to be false.

We've also made it mandatory for middleware components that don't delegate
to receive an unused delegate argument - the 404-middleware, for example,
has to consider a given delegate and ignore it.

The fact is that, some middleware depends on a delegate, and some
middleware does not - but we've effectively removed the ability for
middleware to declare it's own dependency, or non-dependency, on a delegate.

We've also removed the ability for middleware to depend on multiple
delegates, except by clumsy work-arounds as described above, for a
middleware component that conditionally depends on one of two other
middleware components. (if you think that's an unusual or exotic scenario,
it's because that's not really practical or possible with the current
middleware proposal, so you likely wouldn't even consider it.)

Contrast this with the simple interface, which places the responsibility on
the middleware component itself to declare it's dependencies, via the
constructor, you know, like normal components. I could have used a
constructor like:

    public function __construct(MarkdownMiddleware $mm, Middleware $next =
null)

That is, this middleware optionally takes a delegate - it's up to the
middleware to define it's own default behavior (404 page? exception? who
knows) since it has made the delegate optional. If somebody doesn't like
the default behavior, they can pass in a delegate and define their own.

Internally, the middleware can simply call either of these middlewares,
without constructing a fake delegate or depending on a middleware stack to
do it.

It's not just "lambda-style", it's a true lambda: takes a request, returns
a response.

The reason we were able to turn the "onion" structure into a flat list, is
because we're ignoring two facts:

1. It's not always a list (and when it's not, the complexity that hides
that fact just breeds more complexity)

2. There is not always one delegate (sometimes zero, sometimes one,
sometimes zero or one, sometimes more)

It would be nice if it was always a flat list, and every component had
exactly the same dependencies, but that's just not reality - software in
general tends to be more structured than that.

It's an onion structure alright, but sometimes an onion looks more like
this:

http://i.imgur.com/bNiMycp.png

I'm afraid we're trying to over-simplify - I'm just not convinced we'll
make things simpler for anyone by attempting to hide complexity with more
complexity.

I also think we have a tendency to overrate the importance of making it
easy to consume middleware. How much of your time is spent creating or
modifying a middleware stack? You created one per project, right? So like,
1% of your time? And I think that's high.

Hopefully, most of your time is spent building the actual domain, not the
middleware in front of it.

I'm a big believer in simple over easy - for something like database
interactions or rendering/validating forms, stuff you do all day long, I
think you can justify some complexity in favor of making things easy, but
for this?

You're going to be spending very little time actually bootstrapping
middleware day-to-day - you'll spend a lot more time writing middleware and
the stuff behind it than you will simply configuring the middleware
components for your project.

I'm afraid we may be doing ourselves a huge disservice in the long run by
favoring easy over simple.


On Sat, Apr 22, 2017 at 11:28 AM, Rasmus Schultz <[email protected]> wrote:

> Having slept on it, I see that you have valid concerns to which I don't
> have good solutions, at least not at this time.
>
> However, it seems that all of those concerns stem from two fixed ideas:
>
> 1. Creating individual middleware components must happen on-the-fly,
> possibly coupled to a DI container.
>
> 2. The composition of a middleware stack is somehow easier to understand
> or work with when represented as a flat stack or list.
>
> Neither of those ideas are fixed in my head, perhaps that's where our
> perspectives differ.
>
> I think we have very different ideas about what middleware can or should
> be used for.
>
> To me, it's a modular approach for classification and filtering of HTTP
> requests - it's not a new kind of application framework.
>
> The projects I've been building with middleware the past two years largely
> consist of something like:
>
> 1. Error handler
> 2. Application dispatch (router, controller dispatch)
> 3. 404 page
>
> In some cases, there's a middleware for caching etc. in there, or a logger
> - so with that, I think the most I've ever used is 5 components.
>
> I long since gave up on trying to put things like routing and
> session-management in there as separate components from application
> dispatch - the component I call application dispatch handles both routing,
> sessions and a cookie abstraction.
>
> If that sounds like a lot to put in a single component, it's not - we're
> literally talking about 25 lines of (trivial) code: invoke the router,
> create the session and cookie abstraction and inject them into a request
> context (a DI container), create the controller, and run it.
>
> Trying to spread those concerns across several components feels like
> "doing the right thing", because separation of concerns, right?
>
> But the more I thought about it, the more I found that this point of view
> is actually contrived.
>
> Classifying the request actually ends with the router - once the
> path/method have been mapped to a controller, the request has been
> classified, and we're ready to dispatch - we're out of the HTTP domain into
> the application domain.
>
> At that point, things like the cookie and session abstractions are
> dependencies of my controllers - they're how I choose to model and
> integrate with the HTTP domain on a high level.
>
> There's no reason that needs to be middleware. It's contrived, and only
> works because you put those middlewares in the correct order, so they can
> communicate - which is to say, they're really acting as a single component,
> they can't operate correctly in isolation (or out of order) and the
> apparent separation is only skin deep.
>
> Components like this one, for example, don't make sense to me:
>
> https://github.com/middlewares/fast-route/blob/master/src/FastRoute.php
>
> It's a vestigial router integration that doesn't actually route to
> anything - it doesn't dispatch anything.
>
> In fact, it doesn't really do anything - not directly - it only produces
> side-effects, by injecting the result into attributes, which is a means of
> communicating it's result to the component that is going to actually
> dispatch the controller. Clearly one is a dependency of the other.
>
> It's a clever way to hide your dependencies, but they're still there -
> only hidden, and much harder to debug. What does that accomplish?
>
> I mean, I get it, you want everything to be modular - you want things such
> as different router implementations to "plug and play" so you can just
> throw them on the stack and have them work.
>
> But that's not really possible - it doesn't really work that way, and
> implementing it as middleware doesn't change that fact, it just hides that
> fact. You haven't managed to abstract from anything - the dependency of
> some other component, on the result produced by this one, is still there,
> you've only managed to hide that fact. It's throwing complexity after more
> complexity - fighting fire with fire.
>
> You don't get modularity - what you get is fragmentation.
>
> I'm stomping on this particular case because it's a perfect example of
> what I would call middleware abuse.
>
> If you'd stop trying to do that, your typical application would have much
> fewer middleware components.
>
> It wouldn't matter (much) if you were creating the individual middleware
> components on-the-fly or not.
>
> Your application would bootstrap a single HTTP kernel (in a DI container)
> composed of some number of proxies/decorators.
>
> I think where we differ is, you want to actually build the application
> itself out of middleware components? I think that's contrived. The DI
> container and abstractions is a better way to structure the application
> itself.
>
> To me, middleware is just a better, more structured way to classify and
> filter HTTP requests - it's a well-structured approach to building
> front-controllers.
>
> It's not a "framework" or "architecture" for applications - that's a
> different layer.
>
> To me, a simple transaction like "takes a request, returns a response"
> perfectly describes a front-controller, which is all I need to do at this
> layer.
>
> I don't know, maybe I just have a very different idea of middleware from
> most people :-)
>
>
> On Fri, Apr 21, 2017 at 11:30 PM, Matthieu Napoli <[email protected]>
> wrote:
>
>> > This is Symfony's HttpKernelInterface and StackPHP and it has already
>>> been discussed at length
>>>
>>> Same principle, yes, but what I recall being discussed at length was the
>>> lambda-style vs double-pass aspect, which seems unrelated to this
>>> discussion.
>>>
>>> I found one or two older threads on this subject, here's one:
>>>
>>> https://groups.google.com/d/msg/php-fig/Ew36Ng5EwXE/52dAzZAbAQAJ
>>>
>>> The discussion quickly turns to the other, at the time dominant subject
>>> though.
>>>
>>
>> OK I've started to re-read the mega-thread (https://groups.google.com/for
>> um/#!topic/php-fig/vTtGxdIuBX8%5B101-125%5D - 7 pages long) but most of
>> the debate was indeed about lambda-style/double-pass, and I'm clearly not
>> masochistic enough to read all of it again ^^
>>
>> So yes maybe we should discuss that (to me it feels like "again" but
>> maybe that's just me).
>>
>>
>>> > I'm not sure why we are starting it all again?
>>>
>>> Are you're saying all of the concerns I described here have been
>>> discussed and are all invalid?
>>>
>>> According to the PSR-15 meta:
>>>
>>> - "There are currently two common approaches to server middleware that
>>> use HTTP Messages", single-pass and double-pass.
>>>
>>> - There's no mention of the fact that HttpKernelInterface doesn't have
>>> the delegate in the interface.
>>>
>>> - The section on "Delegate Design" talks about the design of that
>>> interface, but doesn't say why it exists in the first place.
>>>
>>> I recall there being lengthy discussions about how to name and describe
>>> the delegate interface, but skimming back through the discussion threads
>>> that are listed in the meta, it seems that the discussions all start from
>>> the assumption that a middleware interface has the delegate argument.
>>>
>>
>> So yes even if it was discussed or not, the fact that it lacks from the
>> metadocument is a sign there is something to improve here.
>>
>> My 2 cents about the Stack approach:
>>
>> - AFAIK it didn't work. And it was not tried in some weekend project: it
>> was built on Symfony's interfaces, so pretty solid stuff and it got a good
>> chance for success. On the other hand PSR-7 middlewares were incompatible
>> with most of the existing architectures at the time (incompatible with
>> Symfony for example) and without a standard interface (rather a "callable"
>> convention) and it worked out! So to me that's a clear sign.
>> - it's harder to compose middlewares because they are all nested: the
>> "pipe" pattern was a huge help IMO in spreading the concept
>> - it's harder to write reusable middlewares because you need to
>> standardize the constructor (see the Stack "convention" about the
>> constructor which is less than ideal)
>> - it's harder to wire in DI containers
>> - lazy middlewares are doable but not as easy
>>
>> That's all I have on my mind right now. And we may or may not have
>> discussed it before, but the PHP community has already tried it. That kind
>> of middleware has existed for a long time, and right now what works and
>> what's spreading is not that kind of middlewares. I believe that's because
>> of good reasons and we should not ignore that and start all over again.
>>
>> Matthieu
>>
>> --
>> You received this message because you are subscribed to a topic in the
>> Google Groups "PHP Framework Interoperability Group" group.
>> To unsubscribe from this topic, visit https://groups.google.com/d/to
>> pic/php-fig/B3jtdJA7-6w/unsubscribe.
>> To unsubscribe from this group and all its topics, send an email to
>> [email protected].
>> To post to this group, send email to [email protected].
>> To view this discussion on the web visit https://groups.google.com/d/ms
>> gid/php-fig/b8317921-172a-476c-9df7-5b47e1f089da%40googlegroups.com
>> <https://groups.google.com/d/msgid/php-fig/b8317921-172a-476c-9df7-5b47e1f089da%40googlegroups.com?utm_medium=email&utm_source=footer>
>> .
>>
>> For more options, visit https://groups.google.com/d/optout.
>>
>
>

-- 
You received this message because you are subscribed to the Google Groups "PHP 
Framework Interoperability Group" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To post to this group, send email to [email protected].
To view this discussion on the web visit 
https://groups.google.com/d/msgid/php-fig/CADqTB_jXrJ8tXtrxmHDj2SWg4adz41L3fkL9DXdyXPzj2pVBkA%40mail.gmail.com.
For more options, visit https://groups.google.com/d/optout.

Reply via email to