Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Symfony Adr Laravel Package

pitch/symfony-adr

Symfony ADR bundle implementing the Action-Domain-Responder pattern: dedicated action classes, domain logic, and responders for clean, testable request handling outside the traditional MVC flow.

View on GitHub
Deep Wiki
Context7

Getting Started

composer require phfritsche/symfony-adr
  • Reality check first: this is a Symfony bundle (Action–Domain–Responder), not a Laravel package. It hooks into HttpKernel (bundles, route defaults, view/response listeners) and has no service provider or laravel/framework constraint. Install it in a Symfony app, or use it as a design reference in Laravel (see below). Do not add it to a Laravel composer.json expecting responses to change.
  • Register Phfritsche\SymfonyAdr\SymfonyAdrBundle in config/bundles.php (['all' => true]) unless a Flex recipe already did it.
  • Version floor: Symfony 4.4/5.x is supported by the 1.3.x line; Symfony 6 requires ^1.4. Pin "phfritsche/symfony-adr": "^1.4" on Symfony 6.
  • Look first at: the Responder contract shipped with the bundle, the _responder route default, and the payload type (the package provides a marker interface if you want one; otherwise the payload is just whatever your Action returns).
  • First use case: take one existing controller that builds a Response and split it. The Action keeps domain/service calls and returns a payload; the Responder turns that payload into the Response:
final class ShowPost
{
    public function __construct(private PostRepository $posts) {}

    public function __invoke(int $id): array          // Action: no HTTP types
    {
        return ['post' => $this->posts->findOrFail($id)];
    }
}
final class ShowPostResponder implements Responder
{
    public function __construct(private Environment $twig) {}

    public function __invoke(array $payload): Response
    {
        return new Response($this->twig->render('post/show.html.twig', $payload));
    }
}
#[Route('/posts/{id}', defaults: ['_responder' => ShowPostResponder::class])]

Wire the responder via the route default (_responder), not inside the Action.

Implementation Patterns

  • Responder per representation. Because the responder lives in route defaults, the same Action can serve HTML, JSON, or a redirect by registering two routes with the same controller and different _responder values. This is the cleanest lever the bundle gives you; use it instead of Accept header branching inside controllers.
  • Fat domain, thin Action. Keep Actions to: validate/authorize input, call domain services, return a payload. All HttpFoundation, Twig, serializer, and URL generation belongs in the Responder. This is the whole point — Actions become unit-testable without a kernel.
  • Responders are normal services. Constructor-inject RouterInterface, Environment, SerializerInterface, logger, etc. Register them as services (autowiring + autoconfigure) so the route default can resolve them by class name.
  • Payloads as typed DTOs. Prefer a small readonly DTO per use case over raw arrays — it makes the Action/Responder contract explicit and gives static analysis something to check.
  • Escape hatch: returning a Response directly from an Action bypasses responder resolution. Convenient for health checks/redirects, but it means two code paths in one controller set — document when you use it.
  • ADR events: if you enable the bundle's event dispatching (adr.event_dispatcher in the adr config namespace), you get hooks around action/responder execution for logging, timing, or instrumentation without touching either class. Leave it off unless you need it — it adds a listener to every ADR request.
  • PSR-17 responders: the bundle ships PSR-17-oriented responder helpers; if you already build responses with Nyholm/Laminas factories, use those responders so the same responder code works outside Symfony.
  • Testing. Action: instantiate with stubbed domain services, assert the payload (assertSame/DTO equality). Responder: instantiate with a real Twig/mocked serializer, assert status, headers, and body. No KernelBrowser needed for either — reserve functional tests for routing/_responder wiring.
  • Porting the pattern to Laravel. Don't install the bundle. Emulate it: invokable controller returns a DTO, a Responder service (resolved via container or a route default you read in the controller) converts it to Illuminate\Http\Response. You keep the testability win and lose nothing, since Laravel has no equivalent of the kernel.view hook the bundle relies on.

Gotchas and Tips

  • Symfony 6 is only in 1.4.0+. On Symfony 6 with an older constraint, Composer will resolve to an incompatible version or fail. Conversely, ^1.4 is fine on 4.4/5.x, so one constraint works across a mixed estate.
  • Responder resolution is runtime, not compile-time. If the _responder class isn't a resolvable service (private/non-autowired/typo), you get a failure when the route is hit, not at cache warmup. Run bin/console debug:router <name> and confirm _responder appears in the defaults, then debug:container <Responder> to confirm it's public or reachable.
  • Missing _responder = silent passthrough. A route without a responder default isn't an error in the usual "your Action returned a payload and nothing rendered" sense — the payload just never reaches a responder. If a page renders empty, check the route default before debugging the template.
  • kernel.view ordering. The bundle works by intercepting the Action's return value before a Response exists. Other kernel.view listeners (or anything expecting a Response from the controller) now see a payload instead. Audit existing listeners when retrofitting an app.
  • kernel.response listeners still run after your Responder returns, so security headers, Vary, and debug toolbars behave normally — but kernel.controller-based logic that assumed AbstractController (e.g. $this->denyAccessUnlessGranted) doesn't exist on a plain Action. Inject AuthorizationCheckerInterface or use attributes/voters.
  • Debugging: dump() the payload at the top of the Responder; if it never fires, the problem is routing/_responder, not your domain code. bin/console debug:event-dispatcher kernel.view shows whether the ADR listener is registered.
  • Extension points: decorate the payload/responder resolution service rather than subclassing; the Responder contract is a single method, so decorating responders (add caching, ETag handling, content negotiation) is cheap and keeps your concrete responders unchanged.
  • Bundle version pinning in CI. Because 1.4.0's only change is Symfony 6 support, no application code changes are needed when upgrading from 1.3.x — but run your responder tests anyway, since Symfony 6 tightened several HttpFoundation/ArgumentResolver signatures that custom responders may have relied on loosely.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle