7/25/2021
Attune to a higher pitch

PHP is great for backend development.
It is fast, it is reliable and it makes it easy to write clean code. There also are tons of well-written and battle-tested libraries at Packagist.
The language and its ecosystem came a long way from just adding some content to HTML at runtime.

And Symfony is one of the most mature frameworks out there. It allows easy setups for small projects and powerful integration with the tools needed in large scale applications.
But one thing bugged me for a while: Symfony follows - or better followed - some Model-View-Controller pattern. A Controller is required to return a Response object.
For example when creating an HTML page per Twig templates, the Controller is supposed to call the twig service and apply the appropriate template to any data it wants to return to the client.

php
namespace App\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
class MyController extends AbstractController
{
public function __invoke(
Request $request,
) {
// ... handle request data ...
// ... read from / write to database or other endpoint ...
// ... prepare response data ...
return $this->render('myTemplate.html.twig', [
// some data to use in the template
]);
}
}

Often there can be invalid requests or multiple different outputs so there are multiple calls to render and often enough multiple instances of data preparations.
This approach quickly bloats the Controller and leads to duplicate code. While there are solutions for repeated code (Inheritance, Traits), there is another problem with this in my opinion:
It mixes different concerns inside the Controller - request handling, data handling and response handling.

Separation of concerns is always underrated.

With Pitch I started a collection of packages that helps to boil down the Controller code to just the data handling.

Symfony as ADR

One of the lesser known facts is that Symfony does not actually require your Controller to return a Response like it says in the docs.
If it does not, Symfony dispatches a kernel.view event with the ControllerResult. Then Event Subscribers can act on this event and set a Response before Symfony would throw an exception.
(This is for example exploited by the popular sensio/framwork-extra-bundle for its @Template annotation.)

Pitch/symfony-adr, the cornerstone of Pitch package collection, allows you to easily follow Action-Domain-Responder pattern in your Symfony application:
It adds a Responder to the kernel.view event and instead of walking through a bunch of Event Subscribers it allows you to register Response Handlers for specific types of return values.

php
namespace App\Controller;
use App\Entity\MyEntity;
class MyController
{
public function __invoke()
{
// just return e.g. an entity
return new MyEntity();
}
}
php
namespace App\Responder;
use App\Entity\MyEntity;
use Pitch\AdrBundle\Responder\ResponseHandlerInterface;
use Pitch\AdrBundle\Responder\ResponsePayloadEvent;
class MyEntityHandler implements ResponseHandlerInterface
{
public function getSupportedPayloadTypes(): array
{
return [MyEntity::class];
}
public function handleResponsePayload(ResponsePayloadEvent $event)
{
// Create the appropriate response
// to return MyEntity to the client.
// E.g. convert per Twig.
}
}

It also comes with a Graceful annotation/configuration that allows you to define which exceptions in your Controllers are supposed to be handled as regular response by a Response Handler. This allows you to not create the same try..catch blocks in multiple controllers.

Outlook

The project is still young but I'm really excited about the changes it brings to Symfony applications.

If you like what you see and/or can make use of this in one of your applications, please spread the word! Issues and PRs to improve this are always welcome. :)