An HTTP State story
An HTTP state story About HTTP framework architecture and the state of MVC.
# MVC architecture
MVC originated in the Smalltalk-80 system to promote a layered approach when developing graphical user interfaces. (…) It defined three different components: The model handles application and business logic. The view handles presentation logic. The controller accepts and interprets keyboard and mouse input.
— Knight and Dai. “Objects and the Web.” IEEE Software, March/April 2002.
In the above mentioned paper, authors analyse the usage of MVC and its relation with the Web and HTTP. In correlation with the “Model View Controller” chapter from Martin Fowler’s book “Patterns of Enterprise Application Architecture” we understand that this pattern needs to evolve further than these three components to adapt to the Web and how we design Websites. Using Symfony, in an HTML context, we see these shortcuts implementing an MVC architecture:
- a twig template is the view
- the Form content is the Model
- the controller handles the model transformations through a helper service and uses that to fill the view
I read a lot on the subject in the past few days. In every implementation of the MVC pattern, when one handles complex logic separation, one uses many controllers to handle a single HTTP request. To quote a few, Martin Fowler writes about different patterns as the Front Controller, the Page Controller or the Application Controller. The later is also described in the “Objects and the Web.” paper in addition to an Input Controller.
# State of Art in HTTP frameworks
One certainty of software development is that it never stands still.
— Martin Fowler. “Patterns of Enterprise Application Architecture.” IEEE Software, 2003.
Symfony is, to me, one of the most accessible HTTP framework, especially because PHP is an awesome and easy-enough-to-learn language. Within its many years of existence it has provided many conventions we love, and brought amazing things to the developper community. Now, Symfony is an HTTP-oriented framework, and not (yet) API-oriented, nor REST-oriented in my opinion. Today, at least in Enterprise Applications, we’re using Web technologies to create APIs instead of standard HTML websites with forms. As developers, we like shortcuts and we’ve seen some recent patches that want to make Symfony more accessible to build APIs:
This is quite nice and it reminds us to the J2EE framework where you compose your input action (also called “Intercepting Filters” in Core J2EE Patterns: Best Practices and Design Strategies. Alur, Crupi, and Malks. Prentice Hall, 2001.).
Example in J2EE 7:
@Path("/myResource")
@Consumes("multipart/related")
public class SomeResource {
@POST
@Consumes("application/x-www-form-urlencoded")
public void post(@FormParam("name") String name) {
// Store the message
}
}
A few differences with Symfony to note here:
- the code is Resource oriented, the Controller is defined inside this Resource (just like it is in [API Platform])(https://api-platform.com/)
- it provides HTTP helpers that are shortcuts to retrieve Request parameters:
QueryParam
,PathParam
,FormParam
- it uses annotation composition to do so
The J2EE RESTful framework introduces the concept of Entity Providers (shortcut with @Provider
) to “map an HTTP request entity body to method parameters”. It also provides a few common Entity Providers but you need to implement one if your format is not covered.
The Spring framework also has the same concept called HttpMessageConverters
and a @RequestBody
shortcut even though the more you dig the more you get complexity to manage validation and content negotiation.
In Javascript, the modern NestJs framework has many attributes as shortcuts to access HTTP request informations. Because Javascript has a tight link to JSON (Javascript Object Notation), you can have a @Body()
annotation to map your JSON request body to a DTO. I had issues in the past in trying to implement enriched formats like JSON-LD and you’d have a hard time using this with other formats. Note that they have a Middleware concepts to intercept and modify models or views but you can not use the @Body
annotation for something else then application/json
.
Within Symfony, the #[Serialize]
, or the #[MapRequestBody]
are a step forward and helps building APIs with Symfony although they’re not resource-oriented. I prefer things like the @Consumes
annotation of the J2EE framework that solves the content negotiation problem, although they make it clear that it is a RESTful framework. I’m also looking forward to the #[Validate]
shortcut (@lyrixx <3)!
Also to note that @dunglas already had something in mind like this in 2016 while working on autowiring.
# API and RESTful frameworks
When we look at Ruby on Rails, which architecture was a good inspiration back in the days. They have an API controller but note that they use the concept of Modules as extensions to prevent the drawbacks of the single controller in a resource oriented design. We can implement this using API Platform components and I was trying to get my head around that in api-platform/core#5463.
“Intercepting Filters” by Martin Fowler and every RESTful oriented framework has these extension points. In API Platform with @dunglas we’re working on pushing these concepts even further, and you can read our Architectural Decision Record on the use of Providers and Processors. The Symfony ecosystem already has most of the infrastructure needed to implement modern and well-designed APIs: API Platform (which is available as a set of small standalone PHP components).
With the release of API Platform 3.2 this is now a reality and API Platform can be built as a Controller within Symfony like this:
use ApiPlatform\State\Util\OperationRequestInitiatorTrait;
use Symfony\Component\HttpFoundation\Request;
class MainController
{
use OperationRequestInitiatorTrait;
public function __invoke(Request $request): Response
{
$operation = $this->initializeOperation($request);
$uriVariables = [];
try {
$uriVariables = $this->getOperationUriVariables($operation, $request->attributes->all(), $operation->getClass());
} catch (InvalidIdentifierException|InvalidUriVariableException $e) {
throw new NotFoundHttpException('Invalid uri variables.', $e);
}
$context = [
'request' => $request,
'uri_variables' => $uriVariables,
'resource_class' => $operation->getClass(),
];
$body = $this->provider->provide($operation, $uriVariables, $context);
return $this->processor->process($body, $operation, $uriVariables, $context);
}
}
The important part of this architecture to note are provider
and processor
. I find these to two implementations of the concept of “Intercepting filters”, one is working over defining the “Request” part, the other the “Response” part.
The call to $provider->provide
does call all these “state providers”:
- ContentNegotiationProvider
- DeserializeProvider
- ReadProvider
Each of these can intercept the Request, and choose the State of that same request for the next provider to use. This is a really powerful mechanic as it allows to modify any part of the chain in user-land. On top of that, we can imagine porting these providers as attributes like J2EE (resource-oriented):
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
#[ApiResource('/books')]
class Book {
#[Get] // implies all the above providers
public function get(Book $body) {
}
}
Or a version where we pick providers/processors (note that this is not implemented yet):
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
#[ApiResource('/books')]
class Book {
#[ContentNegotiation('jsonld')]
#[ReadProvider]
public function get(Book $body) {
return $this->processor->process($body);
}
}
This reflexion was a follow up and thoughts I had while discussing on Github with Symfony contributors, and sharing with collegues. Even if we disagree it makes software evolve and never stand still! Long life to open source software !