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

Cqs Routing Laravel Package

digital-craftsman/cqs-routing

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal Setup

  1. Install the Package

    composer require digital-craftsman/cqs-routing
    

    Ensure your Laravel project uses Symfony’s HttpKernel (via symfony/http-kernel-bundle or spatie/laravel-symfony-support).

  2. Configure the Package Create config/cqs-routing.php (or merge into an existing config file):

    return [
        'request_decoder' => \DigitalCraftsman\CQSRouting\RequestDecoder\JsonRequestDecoder::class,
        'response_constructor' => \DigitalCraftsman\CQSRouting\ResponseConstructor\SerializerJsonResponseConstructor::class,
        'dto_constructor' => \DigitalCraftsman\CQSRouting\DTOConstructor\SerializerDTOConstructor::class,
        'handler_wrapper' => \DigitalCraftsman\CQSRouting\HandlerWrapper\ConnectionTransactionWrapper::class,
        'dto_validators' => [
            // Example: AccessValidator
            \App\Application\CQSRouting\DTOValidator\AccessValidator::class => [
                'parameters' => \App\Application\CQSRouting\DTOValidator\DTO\AccessValidatorParameters::class,
            ],
        ],
    ];
    
  3. First Use Case: Command/Query Endpoint Define a command (e.g., CreateUserCommand) and a handler (e.g., CreateUserCommandHandler). Register the route in routes/web.php:

    use DigitalCraftsman\CQSRouting\CQSRouting;
    use App\Application\Commands\CreateUserCommand;
    
    Route::post('/users', function () {
        return app(CQSRouting::class)
            ->handle(CreateUserCommand::class);
    });
    

    Ensure your command has a #[AsRequest] attribute (or configure via DI).


Implementation Patterns

1. Separation of Concerns with CQS

  • Commands (write operations) and Queries (read operations) are decoupled.
  • Use traits or interfaces (e.g., #[AsCommand], #[AsQuery]) to tag DTOs.
  • Example:
    #[AsCommand]
    class UpdateProfileCommand {
        public function __construct(
            public string $userId,
            public array $data,
        ) {}
    }
    

2. Request/Response Pipeline

  • Request Decoding: Convert HTTP requests to DTOs (e.g., JsonRequestDecoder for JSON payloads).
  • Response Construction: Serialize handler results (e.g., SerializerJsonResponseConstructor for JSON APIs).
  • Customize via config or DI:
    $routing = app(CQSRouting::class)
        ->setRequestDecoder(new CustomRequestDecoder())
        ->setResponseConstructor(new CustomResponseConstructor());
    

3. Validation and Middleware

  • DTO Validation: Use DTOValidator interfaces (e.g., AccessValidator) to enforce rules.
    #[AsCommand]
    class DeletePostCommand {
        public function __construct(
            public string $postId,
        ) {}
    }
    
    Register validator in config:
    'dto_validators' => [
        \App\Application\CQSRouting\DTOValidator\OwnershipValidator::class,
    ],
    
  • Symfony Middleware: Integrate with Laravel middleware via Kernel::addMiddleware():
    $routing->addMiddleware(new AuthMiddleware());
    

4. Transaction Handling

  • Wrap handlers in ConnectionTransactionWrapper (or custom) to manage DB transactions:
    'handler_wrapper' => \DigitalCraftsman\CQSRouting\HandlerWrapper\ConnectionTransactionWrapper::class,
    
  • Extend for async tasks (e.g., queues):
    class QueueHandlerWrapper implements HandlerWrapperInterface {
        public function wrap(HandlerInterface $handler): HandlerInterface {
            return new QueuedHandler($handler);
        }
    }
    

5. Testing

  • Mock the CQSRouting service in tests:
    $routing = $this->createMock(CQSRouting::class);
    $routing->method('handle')
        ->with(CreateUserCommand::class)
        ->willReturn(new CreateUserResponse('user-123'));
    
  • Use HttpTestCase for full request/response cycles:
    $response = $this->postJson('/users', ['name' => 'John']);
    $response->assertJson(['id' => 'user-123']);
    

Gotchas and Tips

Pitfalls

  1. Attribute vs. Config Conflicts

    • If using #[AsRequest] attributes, ensure they align with your config (e.g., JsonRequestDecoder expects JSON payloads).
    • Fix: Explicitly set the decoder in config or use #[AsRequest(decoder: CustomDecoder::class)].
  2. Circular Dependencies in Validators

    • Validators may need access to services (e.g., AuthService). Use constructor injection:
      public function __construct(private AuthService $auth) {}
      
    • Tip: Register validators as services in Laravel’s container:
      $this->app->bind(
          \App\Application\CQSRouting\DTOValidator\OwnershipValidator::class,
          function ($app) {
              return new OwnershipValidator($app->make(AuthService::class));
          }
      );
      
  3. Transaction Scope Leaks

    • ConnectionTransactionWrapper defaults to a single DB connection. For multi-DB setups:
      'handler_wrapper' => function () {
          return new ConnectionTransactionWrapper(
              app('db.connection.primary'),
              app('db.connection.secondary')
          );
      },
      
  4. Performance with Large DTOs

    • Serialization overhead can slow down requests. Optimize with:
      • DTO Constructor: Use SerializerDTOConstructor with JMS\Serializer or Symfony\Component\Serializer.
      • Caching: Cache validated DTOs or responses for read-heavy queries.
  5. Route Caching Incompatibilities

    • Laravel’s route caching may conflict with dynamic CQSRouting handlers. Disable caching for CQS routes:
      Route::post('/users', function () { /* ... */ })
          ->middleware('cache.off');
      

Debugging Tips

  1. Log DTO Validation Errors

    • Extend DTOValidator to log failures:
      public function validate($dto, array $parameters): void {
          if (!$this->isValid($dto)) {
              \Log::error('Validation failed for ' . get_class($dto), [
                  'errors' => $this->getErrors(),
              ]);
              throw new ValidationException($this->getErrors());
          }
      }
      
  2. Inspect Handler Execution

    • Use a custom HandlerWrapper to log input/output:
      class LoggingHandlerWrapper implements HandlerWrapperInterface {
          public function wrap(HandlerInterface $handler): HandlerInterface {
              return new class($handler) implements HandlerInterface {
                  public function __invoke($command) {
                      \Log::debug('Handling', ['command' => get_class($command)]);
                      $result = $this->handler($command);
                      \Log::debug('Result', ['result' => $result]);
                      return $result;
                  }
                  public function __construct(private HandlerInterface $handler) {}
              };
          }
      }
      
  3. Symfony vs. Laravel Kernel

    • If using spatie/laravel-symfony-support, ensure the Symfony kernel is the primary HTTP kernel:
      // config/app.php
      'kernel' => \Spatie\SymfonySupport\Laravel\Kernel::class,
      

Extension Points

  1. Custom Request Decoders

    • Decode non-JSON payloads (e.g., form data, XML):
      class FormRequestDecoder implements RequestDecoderInterface {
          public function decode(Request $request): object {
              return (new FormDataDTO())->populate($request->all());
          }
      }
      
  2. Dynamic Route Binding

    • Bind DTOs to route parameters:
      Route::put('/users/{userId}', function (CreateUserCommand $command) {
          return app(CQSRouting::class)->handle($command);
      })->bind('userId', function ($userId) {
          return new CreateUserCommand($userId, [/* ... */]);
      });
      
  3. Event-Driven Handlers

    • Dispatch events after command execution:
      class EventDispatchingHandlerWrapper implements HandlerWrapperInterface {
          public function wrap(HandlerInterface $handler): HandlerInterface {
              return new class($handler) implements HandlerInterface {
                  public function __invoke($command) {
                      $result = $this->handler($command);
                      event(new CommandHandled($command, $result));
                      return $result;
                  }
                  public function __construct(private HandlerInterface $handler) {}
              };
          }
      }
      

4

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.
elnasnato/laraliveui
labrodev/rest-sdk
sampaui/sampaui
babelqueue/php-sdk
facebook/capi-param-builder-php
babelqueue/symfony
hamzi/corewatch
minionfactory/raw-hydrator
hexters/coinpayment
rjcodes/rjcms
act-training/laravel-permissions-manager
alimarchal/laravel-chart-of-accounts
babenkoivan/elastic-scout-driver
mkwebdesign/filament-watchdog-v5
renatomarinho/laravel-page-speed
zedmagdy/filament-business-hours
renatovdemoura/blade-elements-ui
devgeek/beacon-admin
benjamin-rqt/data-watcher-bundle
atriumphp/atrium