pitch/symfony-adr

This bundle makes it easier to follow ADR pattern while writing a Symfony application

Installs: 264

Dependents: 1

Suggesters: 1

Security: 0

Stars: 2

Watchers: 2

Forks: 0

Open Issues: 1

Type:symfony-bundle

v1.4.0 2022-06-20 14:16 UTC

README

This bundle makes it easier to follow ADR pattern while writing a Symfony application.

Usage

Turn controller into action

Decouple responder logic from action logic by moving it out of the controller. Just return the payload!

If a controller returns anything but a Response object, Symfony dispatches a kernel.view event.

Now instead of registering a bunch of event listeners to be iterated through, implement ResponseHandlerInterface.

namespace App\Responder;

use Pitch\AdrBundle\Responder\ResponseHandlerInterface;
use Pitch\AdrBundle\Responder\ResponsePayloadEvent;
use Symfony\Component\HttpFoundation\Response;

use App\Entity\MyPayload;

class MyPayloadHandler implements ResponseHandlerInterface
{
    public function getSupportedPayloadTypes(): array
    {
        return [
            MyPayload::class,
        ];
    }

    public function handleResponsePayload(
        ResponsePayloadEvent $payloadEvent
    ): void {
        $response = new Response();

        // prepare the response
        if ($payloadEvent->request->getAttribute('_foo') === 'bar') {
            // adjust the response according to the request
        }

        $payloadEvent->payload = $response;
    }
}

If your handler class is available as a service according to your config/services.yaml, it will be discovered and used whenever a MyPayload object is returned by a controller.

With default config just put the class into src/Responder/MyPayloadHandler.php and you are done.

Your response handler can report its priority in getSupportedTypes.

class MyPayloadHandler implements ResponseHandlerInterface
{
    public function getSupportedPayloadTypes(): array
    {
        return [
            MyPayload::class => 123,
            MyOtherPayload:class => 456,
        ];
    }
    //...
}

Or you can overwrite the handled types and priorities for response handlers in your services.yml.

services:
  App\Responder\MyPayloadHandler:
    tags:
      - name: pitch_adr.responder
        for: [App\Entity\MyPayload]
        priority: 1000
      - name: pitch_adr.responder
        for: [App\Entity\MyOtherPayload]
        priority: 0

You can easily debug your responder config per console command.

$ php bin/console debug:responder MyPayload

Treat some exceptions as response payload

A robust domain will have strict constraints and throw exceptions whenever an unexpected or invalid condition occurs and for every exception falling through your controller/action Symfony dispatches a kernel.exception event.

You can reserve this event for truly unexpected behavior without repeating similar try-catch-blocks across your controllers.

Define which exceptions should be catched for all controllers and be treated as response payload:

pitch_adr:
    graceful:
        - { value: RuntimeException, not: [BadRuntime, OtherBadRuntime] }
        - Foo
        - { not: GloballyBadException }
        - { value: Bar, not: BadBar }

If Doctrine Annotations is installed, you can define extra rules for your controller methods per annotation:

namespace App\Controller;

use Pitch\AdrBundle\Configuration\Graceful;

class MyController
{
    /**
     * @Graceful(not=LocallyBadException::class)
     * @Graceful(LocallyGoodException::class, not={ButNotThisOne::class, OrThatOne::class})
     */
    public function __invoke(
        Request $request
    ) {
        /// ...
    }
}

With PHP8 you can define extra rules per Attribute:

namespace App\Controller;

use Pitch\AdrBundle\Configuration\Graceful;

class MyController
{
    #[Graceful(not: LocallyBadException::class)]
    #[Graceful(LocallyGoodException::class, not: [ButNotThisOne::class, OrThatOne::class])]
    public function __invoke(
        Request $request
    ) {
        /// ...
    }
}

Rules are applied in the order of appearance, method rules after global rules.

Now you can just create a App\Responder\MyGoodRuntimeExceptionHandler as described above.

Default response handlers

The bundle automatically adds some response handlers for basic types with negative priority so that they will be called if none of your response handlers stops propagation earlier. If you don't want the default handlers to be added, you can modify this behavior per bundle configuration.

pitch_adr:
    defaultResponseHandlers: false # defaults to true

Prioritised response handlers

If consecutive response handlers (in the order of config priority) implement PrioritisedResponseHandlerInterface, that block of handlers will be reordered on runtime according the priority they report for the specific request per getResponseHandlerPriority.

Given the following response handlers are configured to handle a `SomePayloadType`:

900: HandlerA
600: PrioritisedHandler1 with getResponseHandlerPriority(): 1
500: PrioritisedHandler2 with getResponseHandlerPriority(): 2
400: PrioritisedHandler3 with getResponseHandlerPriority(): 0
100: HandlerB

these will be executed in the following order:

HandlerA
PrioritisedHandler2
PrioritisedHandler1
PrioritisedHandler3
HandlerB

Negotiating content type in response handlers

See JsonResponder on how to implement your own prioritised response handlers that handle a payload according to the Accept header on the request.

You can set a default content type for requests that don't include an Accept header. This can be done per container parameter or as controller annotation.

parameters:
    pitch_adr.defaultContentType: 'application/json'
use Pitch\AdrBundle\Configuration\DefaultContentType;

class MyController
{
    #[DefaultContentType('application/json')]
    public function __invoke()
    {
        // ...
    }
}