Installation:
composer require dunglas/action-bundle
Register the bundle in config/bundles.php:
return [
// ...
Dunglas\ActionBundle\DunglasActionBundle::class => ['all' => true],
];
First Action Class:
Create a class in src/Action/ (e.g., src/Action/HelloAction.php):
namespace App\Action;
use Symfony\Component\HttpFoundation\Response;
class HelloAction
{
public function __invoke(): Response
{
return new Response('Hello, Action!');
}
}
Routing:
Define a route in config/routes.yaml:
hello:
path: /hello
controller: App\Action\HelloAction
Autowiring Dependencies:
Inject services via constructor (e.g., src/Action/UserAction.php):
namespace App\Action;
use App\Service\UserService;
use Symfony\Component\HttpFoundation\Response;
class UserAction
{
private $userService;
public function __invoke(UserService $userService): Response
{
$this->userService = $userService;
return new Response($this->userService->getName());
}
}
Replace a traditional controller with an action:
// src/Action/Api/UserAction.php
namespace App\Action\Api;
use App\Service\UserRepository;
use Symfony\Component\HttpFoundation\JsonResponse;
class UserAction
{
public function __invoke(UserRepository $userRepo): JsonResponse
{
return new JsonResponse($userRepo->findAll());
}
}
Route:
api_users:
path: /api/users
controller: App\Action\Api\UserAction
Break down logic into smaller, reusable actions:
// src/Action/User/ShowAction.php
namespace App\Action\User;
use App\Service\UserFinder;
use Symfony\Component\HttpFoundation\Response;
class ShowAction
{
public function __invoke(UserFinder $finder, int $id): Response
{
$user = $finder->find($id);
return new Response($user->getName());
}
}
Route with parameters:
user_show:
path: /users/{id}
controller: App\Action\User\ShowAction
Leverage the same pattern for console commands:
// src/Action/Console/GenerateSitemapAction.php
namespace App\Action\Console;
use App\Service\SitemapGenerator;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class GenerateSitemapAction extends Command
{
protected static $defaultName = 'app:generate-sitemap';
public function __invoke(SitemapGenerator $generator, InputInterface $input, OutputInterface $output): int
{
$generator->generate();
$output->writeln('Sitemap generated!');
return Command::SUCCESS;
}
}
Register as a service (automatically handled by the bundle).
Use Symfony’s middleware stack with actions:
// src/Action/Api/PostAction.php
namespace App\Action\Api;
use App\Middleware\AuthMiddleware;
use Symfony\Component\HttpFoundation\Response;
class PostAction
{
public function __invoke(AuthMiddleware $auth): Response
{
$auth->check();
return new Response('Authenticated!');
}
}
Middleware is injected like any other service.
Dispatch events within actions:
// src/Action/User/RegisterAction.php
namespace App\Action\User;
use App\Event\UserRegisteredEvent;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Response;
class RegisterAction
{
public function __invoke(EventDispatcherInterface $dispatcher, array $data): Response
{
$event = new UserRegisteredEvent($data);
$dispatcher->dispatch($event);
return new Response('User registered!');
}
}
Integrate with Symfony Forms:
// src/Action/User/CreateAction.php
namespace App\Action\User;
use App\Form\UserType;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class CreateAction
{
public function __invoke(FormFactoryInterface $factory, Request $request): Response
{
$form = $factory->create(UserType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
return new Response('Success!');
}
return new Response($form->createView());
}
}
config/services.yaml:
# config/services.yaml
parameters:
container.autowiring.strict_mode: true
#[Autowire] attribute (PHP 8+) or annotations (@Autowire) for clarity:
use Symfony\Component\DependencyInjection\Attribute\Autowire;
class UserAction
{
#[Autowire]
private UserService $userService;
}
# Wrong (Symfony 5+ style)
controller: App\Action\UserAction::class::__invoke
# Correct
controller: App\Action\UserAction
routes:
user:
path: /users
controller: App\Action\User\ListAction
user_show:
path: /users/{id}
controller: App\Action\User\ShowAction
null for optional dependencies:
public function __invoke(?LoggerInterface $logger = null): Response
{
if ($logger) {
$logger->info('Action invoked');
}
return new Response('OK');
}
try-catch or use a global exception listener:
// src/EventListener/ActionExceptionListener.php
namespace App\EventListener;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
use Symfony\Component\HttpKernel\KernelEvents;
class ActionExceptionListener
{
public function onKernelException(ExceptionEvent $event): void
{
if (!$event->getThrowable() instanceof HttpExceptionInterface) {
$event->setResponse(new Response('An error occurred', 500));
}
}
}
Register the listener in config/services.yaml:
services:
App\EventListener\ActionExceptionListener:
tags:
- { name: kernel.event_listener, event: kernel.exception }
# config/services.yaml
services:
App\Action\HelloAction:
public: true
tags: ['controller.service_arguments']
public function testUserAction()
{
$userService = $this->createMock(UserService::class);
$userService->method('getName')->willReturn('John Doe');
$action = new UserAction();
$response = $action($userService);
$this->assertEquals('John Doe', $response->getContent());
}
Request object when testing actions that use it.# Replace:
# class UserController extends AbstractController
# with:
# class UserAction
controller: App\Controller\UserController::indexAction → controller: App\Action\UserAction).How can I help you explore Laravel packages today?