Advent 2023: PSR-15
I've mentioned a few times over the course of this 2023 Advent series that the longer I'm in the tech field, the more I appreciate and favor simple solutions. I was reminded of this yesterday when I read this article on return types in Laravel controllers by Joel Clermont.
Request
Please, please, please do not take this as an attack on Laravel or on Joel. I have nothing but respect for Joel, and while I'm not a fan of Laravel, I'm also not a hater. It's never a bad thing to have a popular framework that brings folks to a language; Laravel has done that in spades for PHP.
Summarize the article, already...
In the article, Joel notes the problem with providing return types in a Laravel controller is due to the fact that it could return a view, a JSON response, an array, a redirect, or more. If there are multiple types that could be returned, based on the request context, you would need to provide a union type. And if you refactor or make changes to the controller later that result in new types being returned, you now need to remember to change the return type declaration.
In other words, it introduces brittleness.
So what?
I've worked on multiple iterations of a major MVC framework, and I ran into these same issues. As PHP's type system got incrementally better, the cracks in how frameworks interact with controllers became more evident. Personally, I find the increasing number of type capabilities in PHP to be a huge boon in helping the correctness of applications, and preventing whole classes of errors. But if the framework prevents you from using the type system, or makes adding type declarations into a situation that can now introduce errors, it puts the developer and maintainer of an application into a problematic situation.
What are the alternatives?
I worked for quite some time on PSR-7 HTTP Message Interfaces, largely so that we could have a proper HTTP message abstraction in PHP on which to build a better foundation for applications and frameworks. From this emerged PSR-15 HTTP Server Request Handlers (which I sponsored and collaborated on, but was not primary author of).
What I love about PSR-15 is that there is no ambiguity about what you return from middleware or a handler. You return a response. That's all you can return.
This means there's no magic about different return values resulting in different behavior from the framework. You don't need to keep a mental map about what will happen, or do a deep dive into the framework internals to understand the ramifications of returning a view versus an array.
Instead, your handler will create a response, and provide the logic for how that is done.
If you need HTML, you render a template, and feed it to the response.
If you need JSON, you serialize data to JSON, and feed it to the response.
If you need a redirect, you create a response with the appropriate status code and Location
header.
And so on and on.
Yes, this can lead to a little extra code at times, but:
- You can see exactly what you intend to return to the user, and why.
- If you try and return anything but a response, it'll result in a
TypeError
. - You can test all of the different possible returns easily, by doing assertions on the returned response based on different requests provided to the handler or middleware.
But should you do everything in a handler? What about things that will happen for whole sections of the site, or will be repeated in many locations, like initializing a session, or checking for an authenticated user, or validating headers, or caching?
For those things, PSR-15 provides middleware. These are expected to be chained together, like a pipeline or a command bus, and the request is passed down through them, and a response returned on the way back up. They're a powerful way to provide re-usable pieces of functionality to your application.
What's more, using middleware is often far easier to understand than how and when various events will intercept a request. You can see the list of middleware for a given handler, and understand that they act either as filters on the incoming request (authentication, caching, etc.), or as decorators on the response (e.g. encoding or compressing the response, caching, etc.). Since each does exactly one thing (ideally), you can test how each works, and understand how and when to compose each, and how they might work in combination.
Building complex behavior via piping one thing to another is hugely powerful. There's a reason that the Unix Philosophy has existed as long as it has, and I can appreciate an approach to web development that builds on it.