Customization

How to extend the boilerplate with new features

Customization

This guide shows how to extend the boilerplate with new features.

Adding a New Command (Write Operation)

Example: adding an "Update Profile" feature.

1. Create the Feature Folder

src/UserManagement/Application/UpdateProfile/

2. Create the Command DTO

final readonly class UpdateProfileCommand
{
    public function __construct(
        public string $userId,
        public string $name,
    ) {}
}

3. Create the Handler

#[AsMessageHandler(bus: 'command.bus')]
final readonly class UpdateProfileHandler
{
    public function __construct(
        private UserRepositoryInterface $userRepository,
    ) {}

    public function __invoke(UpdateProfileCommand $command): void
    {
        $user = $this->userRepository->get(UserId::fromString($command->userId));
        $user->updateName($command->name);
        $this->userRepository->save($user);
    }
}

4. Create the Controller

Thin controller that parses the request, dispatches the command, and returns a JSON response.

5. Add the Route

api_update_profile:
    path: /api/v1/user/profile
    methods: [PUT]
    controller: App\UserManagement\Infrastructure\Http\UpdateProfileController

6. Write Tests

Create both a unit test (with fakes) and a functional test (with HTTP requests).

Adding a New Query (Read Operation)

  1. Create the feature folder with Query DTO and ViewModel
  2. Create a read-model repository interface in Application
  3. Implement with DBAL (raw SQL, no ORM) in Infrastructure
  4. Create a handler on the query.bus
  5. Wire up the controller and route

Adding a New Bounded Context

src/NewContext/
├── Domain/
│   ├── Model/
│   ├── Port/
│   ├── Event/
│   └── Exception/
├── Application/
└── Infrastructure/
    ├── Http/
    ├── Persistence/
    │   ├── Mapping/
    │   └── ReadModel/
    └── Console/

Then add ORM mapping in config/packages/doctrine.yaml, register controllers in config/services.yaml, and add routes.

Adding a New Domain Event

  1. Create the event class in Domain/Event/
  2. Dispatch from a handler via $this->eventBus->dispatch(...)
  3. Create a listener handler on the event.bus

Adding a New Email Template

  1. Create the template in Brevo, note the ID
  2. Add the template ID to config/packages/brevo.yaml
  3. Add the method to EmailSenderInterface
  4. Implement in both the Brevo and Symfony Mailer adapters

Adding a New Stripe Plan

  1. Edit config/packages/stripe.yaml with the new plan
  2. Add the plan type enum case to PlanType.php
  3. Define its features in the features() method
  4. Sync to Stripe: docker compose exec php bin/console app:stripe:sync-plans

Switching to Async Events

By default, events are dispatched synchronously. To process them asynchronously via Redis:

  1. Update the transport DSN in config/packages/messenger.yaml
  2. Set MESSENGER_TRANSPORT_DSN=redis://redis:6379/messages in .env.local
  3. Start a worker: docker compose exec php bin/console messenger:consume async

In production, run the worker as a supervised process.