Weave Code
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

Rich Bundle Laravel Package

1tomany/rich-bundle

Symfony bundle implementing the RICH (Request, Input, Command, Handler) architecture. Encourages single-responsibility actions with explicit Input/Command/Handler classes for clear, safe, and futureproof backend development without heavy DDD/CQRS overhead.

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal Steps

  1. Installation

    composer require 1tomany/rich-bundle
    

    Ensure OneToMany\RichBundle\OneToManyRichBundle is registered in config/bundles.php.

  2. Generate Module Structure Use the provided CLI command to scaffold a module (e.g., Account):

    ./vendor/bin/create-rich-module Account
    

    This creates the directory structure and basic contracts (RepositoryInterface, ExceptionInterface).

  3. First Use Case: Create a Command

    • Define a CreateAccountCommand in src/Module/Account/Action/Command/ implementing CommandInterface.
    • Example:
      final readonly class CreateAccountCommand implements CommandInterface {
          public function __construct(
              public string $name,
              public string $email,
          ) {}
      }
      
  4. Create an Input Class

    • Define CreateAccountInput in src/Module/Account/Action/Input/ implementing InputInterface.
    • Use attributes like #[SourceRequest] to map request data and add validation constraints.
    • Implement toCommand() to convert input to a command.
  5. Create a Handler

    • Define CreateAccountHandler in src/Module/Account/Action/Handler/:
      final readonly class CreateAccountHandler {
          public function __construct(
              private AccountRepositoryInterface $repository,
          ) {}
      
          public function __invoke(CreateAccountCommand $command) {
              $account = new Account($command->name, $command->email);
              $this->repository->save($account);
          }
      }
      
  6. Register Handler in Symfony Add the handler as a service in config/services.yaml:

    services:
        App\Module\Account\Action\Handler\CreateAccountHandler:
            arguments:
                $repository: '@App\Repository\AccountRepository'
    
  7. Create a Controller

    • Use #[AsController] and inject the handler:
      final readonly class AccountController {
          public function __construct(
              private CreateAccountHandler $handler,
          ) {}
      
          #[Route('/accounts', methods: ['POST'])]
          public function create(CreateAccountInput $input): JsonResponse {
              $this->handler($input->toCommand());
              return new JsonResponse(['status' => 'created']);
          }
      }
      

Implementation Patterns

Workflows

  1. Request Handling

    • Web/API Requests: Use #[SourceRequest], #[SourceUser], etc., to map request data to input properties. The bundle automatically validates and converts the input to a command.
    • Console Commands: Pass data directly to the input constructor or use custom logic to populate properties.
  2. Command-Handler Binding

    • Manual Dispatch: Call the handler directly in controllers or services:
      $handler($command);
      
    • Event Dispatching: Use Symfony’s event system to trigger handlers asynchronously (e.g., via EventDispatcherInterface).
  3. Error Handling

    • Exceptions: Throw exceptions in handlers that implement ExceptionInterface. Catch them in controllers or middleware:
      try {
          $handler($command);
      } catch (AccountExceptionInterface $e) {
          return new JsonResponse(['error' => $e->getMessage()], 400);
      }
      
    • Validation Errors: Return validation errors from the input class:
      if (!$input->isValid()) {
          return new JsonResponse(['errors' => $input->getErrors()], 400);
      }
      
  4. Testing

    • Unit Tests: Mock handlers and test command-to-command interactions.
    • Integration Tests: Use HttpClient to test full request-handler workflows:
      $client = static::createClient();
      $client->request('POST', '/accounts', ['json' => ['name' => 'Test']]);
      $this->assertResponseIsSuccessful();
      

Integration Tips

  1. Doctrine Integration

    • Use repository interfaces to abstract database operations. Implement them in your existing ServiceEntityRepository classes.
    • Example:
      class AccountRepository implements AccountRepositoryInterface {
          public function findOneById(?int $id): ?Account {
              return $this->createQueryBuilder('a')
                  ->where('a.id = :id')
                  ->setParameter('id', $id)
                  ->getQuery()
                  ->getOneOrNullResult();
          }
      }
      
  2. Symfony Forms

    • Create forms that map to input classes for web requests. Use #[Assert] constraints for validation:
      $form = $this->createForm(CreateAccountInput::class);
      
  3. Messaging (Async)

    • Decouple handlers from the request lifecycle by publishing commands to a message queue (e.g., Symfony Messenger):
      # config/packages/messenger.yaml
      framework:
          messenger:
              transports:
                  async: '%env(MESSENGER_TRANSPORT_DSN)%'
              routing:
                  'App\Module\Account\Action\Command\CreateAccountCommand': async
      
  4. API Platform

    • Use input classes as DTOs in API Platform resources:
      #[ApiResource(
          operations: [
              new Create(
                  input: CreateAccountInput::class,
                  processor: CreateAccountHandler::class,
              ),
          ],
      )]
      class Account {}
      

Gotchas and Tips

Pitfalls

  1. Immutability Violations

    • Issue: Handlers or commands marked as readonly or final may throw errors if modified.
    • Fix: Ensure all command properties are set via constructor and never modified afterward. Use constructor promotion for clarity:
      final readonly class CreateAccountCommand {
          public function __construct(
              public string $name,
              public string $email,
          ) {}
      }
      
  2. Circular Dependencies

    • Issue: Modules depending on each other’s handlers or repositories can create tight coupling.
    • Fix: Use interfaces for dependencies and inject concrete implementations via Symfony’s container. Avoid direct module-to-module calls.
  3. Validation Overhead

    • Issue: Overusing #[SourceRequest] or #[SourceUser] can lead to verbose input classes.
    • Fix: Group related properties into nested objects (e.g., AddressInput) and validate them separately.
  4. Handler State Management

    • Issue: Handlers assuming Doctrine entities are managed (e.g., $entityManager->persist($entity)) may fail in async contexts.
    • Fix: Always re-fetch entities or use EntityManagerInterface to manage the lifecycle:
      $account = $this->repository->findOneById($command->id);
      if (!$account) {
          throw new AccountNotFoundException();
      }
      $account->update($command->name);
      $this->entityManager->flush();
      
  5. Attribute Conflicts

    • Issue: Custom attributes or Symfony’s #[MapRequestPayload] may conflict with bundle attributes.
    • Fix: Use fully qualified attribute names (e.g., \OneToMany\RichBundle\Attribute\SourceRequest) and prioritize bundle attributes in validation.

Debugging

  1. Validation Errors

    • Tip: Enable Symfony’s validator debug mode to see detailed error messages:
      php bin/console debug:validator App\Module\Account\Action\Input\CreateAccountInput
      
    • Fix: Use #[Assert\Callback] for complex validation logic:
      #[Assert\Callback]
      public function validate(ExecutionContextInterface $context) {
          if ($this->email === $this->name) {
              $context->buildViolation('Email and name cannot match.')
                      ->addViolation();
          }
      }
      
  2. Handler Not Found

    • Tip: Ensure the handler is registered as a service and tagged correctly (if using Symfony’s autowiring).
    • Fix: Explicitly bind the handler in services.yaml:
      services:
          App\Module\Account\Action\Handler\CreateAccountHandler:
              tags: ['controller.service_arguments']
      
  3. Request Data Mapping

    • Tip: Use #[SourceRequest(path: 'data.name')] to map nested JSON payloads.
    • Fix: For custom request sources (e.g., headers), create a custom attribute extending AbstractSourceAttribute.

Extension Points

  1. Custom Attributes

    • Extend AbstractSourceAttribute to create new data sources (e.g., #[SourceHeader]):
      #[Attribute(Attribute::TARGET_PROPERTY)]
      final class SourceHeaderAttribute extends AbstractSourceAttribute {
          public function getValue(mixed $request, string $property): mixed {
              return $request->headers->get($this->name);
          }
      }
      
  2. Command Serialization

    • Implement JsonSerializable or use Symfony’s SerializerInterface to customize command serialization for async queues:
      final readonly class CreateAccountCommand implements CommandInterface, JsonSerializable {
          public function jsonSerialize(): array {
              return [
                  'name' => $this->name
      
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.
terminal42/code-quality-tools
codifyo/ts-generator-bundle
testo/fiber
mintobit/jobqueue
a4sex/maintenance-bundle
a4sex/entity-date-update
a4sex/client-identifier
a4sex/base-utilites
a4sex/key-value-storage
a4sex/micro-status
chilldev/dependency-injection-extra
datinglibre/datinglibre-app-api
biberltd/corebundle
bricre/symfony-bundle-test
biberltd/logbundle
dominium/http-adapter-bundle
dominium/google-analytics
a4sex/auto-clean-entity
christhompsontldr/laravel-inky
spatie/mailcoach-vapor