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 Request Dto Bundle Laravel Package

dualmedia/symfony-request-dto-bundle

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal Setup

  1. Installation:

    composer require dualmedia/symfony-request-dto-bundle
    

    Register the bundle in config/bundles.php:

    DualMedia\DtoRequestBundle\DtoBundle::class => ['all' => true],
    
  2. First DTO: Create a DTO class extending AbstractDto in src/Dto/.

    // src/Dto/ProfileDto.php
    namespace App\Dto;
    
    use DualMedia\DtoRequestBundle\Dto\AbstractDto;
    
    class ProfileDto extends AbstractDto
    {
        public string $name;
        public int $age;
    }
    
  3. First Usage: Inject the DTO directly into a controller method. The bundle auto-resolves request data:

    // src/Controller/ProfileController.php
    use App\Dto\ProfileDto;
    
    class ProfileController extends AbstractController
    {
        public function update(ProfileDto $dto): Response
        {
            // $dto->name and $dto->age are automatically populated and validated
            return new Response('DTO processed');
        }
    }
    

Key First Use Case

Replace manual request handling (e.g., $request->request->get('name')) with type-safe DTOs. The bundle handles:

  • Type coercion (e.g., "25" → int).
  • Validation (via Symfony constraints).
  • Doctrine entity loading (e.g., #[FindOneBy]).

Implementation Patterns

Core Workflow

  1. DTO Design:

    • Use AbstractDto as the base class.
    • Annotate properties with Symfony constraints (e.g., #[Assert\NotBlank]).
    • Leverage attributes for custom behavior:
      use DualMedia\DtoRequestBundle\Dto\Attribute\Bag;
      use DualMedia\DtoRequestBundle\Metadata\Enum\BagEnum;
      
      class ProfileDto extends AbstractDto
      {
          #[Bag(BagEnum::Query)]
          public ?string $filter = null;
      
          #[Bag(BagEnum::Body)]
          public array $data;
      }
      
    • For nested DTOs, define them as class properties:
      public AddressDto $address;
      
  2. Controller Integration:

    • Automatic Resolution: The bundle resolves DTOs from request bags (body, query, headers, etc.) automatically.
      public function create(CreateUserDto $dto): Response
      {
          // $dto is populated from request body/query/headers
      }
      
    • Manual Resolution: Use DtoResolver service for custom logic:
      use DualMedia\DtoRequestBundle\Resolver\DtoResolverInterface;
      
      public function __construct(private DtoResolverInterface $resolver) {}
      
      public function customAction(Request $request): Response
      {
          $dto = $this->resolver->resolve(ProfileDto::class, $request);
      }
      
  3. Entity Loading: Use #[FindOneBy] or #[FindBy] to load Doctrine entities:

    use DualMedia\DtoRequestBundle\Dto\Attribute\FindOneBy;
    
    class UserDto extends AbstractDto
    {
        #[FindOneBy(['email' => 'email'])]
        public ?User $user = null;
    
        public string $email;
    }
    
  4. Validation:

    • Add Symfony constraints to DTO properties:
      use Symfony\Component\Validator\Constraints as Assert;
      
      class LoginDto extends AbstractDto
      {
          #[Assert\NotBlank]
          #[Assert\Email]
          public string $email;
      
          #[Assert\NotBlank]
          #[Assert\Length(min: 8)]
          public string $password;
      }
      
    • Custom validation groups:
      #[Assert\All({
          new Assert\NotBlank(),
          new Assert\Type('string')
      })]
      public array $tags;
      
  5. Events and Actions:

    • Listen to DtoInvalidEvent or DtoActionEvent for custom responses:
      # config/services.yaml
      DualMedia\DtoRequestBundle\EventListener\DtoInvalidEventListener:
          tags:
              kernel.event_listener:
                  event: dto.invalid
                  method: onInvalidDto
      
    • Use #[Action] to trigger custom logic:
      use DualMedia\DtoRequestBundle\Dto\Attribute\Action;
      
      class ProfileDto extends AbstractDto
      {
          #[Action('validate_avatar_size', when: 'avatar !== null')]
          public ?UploadedFile $avatar = null;
      }
      
  6. API Documentation:

    • Nelmio API Doc integrates automatically. No manual @param annotations needed.
    • Customize OpenAPI schemas with #[OpenApi] attributes:
      use DualMedia\DtoRequestBundle\Dto\Attribute\OpenApi;
      
      class PaginationDto extends AbstractDto
      {
          #[OpenApi(description: 'Page number', example: 1)]
          public int $page = 1;
      }
      

Integration Tips

  • Doctrine: Ensure doctrine/orm-bundle is installed for entity loading.
  • Validation: Combine with Symfony’s Validator for custom constraints.
  • Testing: Use DtoResolver in tests to mock request data:
    $dto = $this->resolver->resolve(ProfileDto::class, $request);
    $this->assertEquals('John', $dto->name);
    
  • Performance: The bundle caches metadata via opcache, reducing reflection overhead after warm-up.

Gotchas and Tips

Pitfalls

  1. Caching Quirks:

    • Metadata is cached in PHP files (var/cache). Clear cache after DTO changes:
      php bin/console cache:clear
      
    • Debugging: Disable caching in config/packages/dm_dto.yaml:
      dm_dto:
          cache: false
      
  2. Validation Groups:

    • Default groups are applied automatically. Override with #[Groups]:
      use Symfony\Component\Validator\Constraints as Assert;
      
      class UserDto extends AbstractDto
      {
          #[Assert\NotBlank(groups: ['registration'])]
          public string $email;
      }
      
    • Gotcha: Forgetting to include groups in validation calls:
      $validator->validate($dto, ['registration']); // Explicitly specify groups
      
  3. Entity Loading:

    • #[FindOneBy] requires the property to be nullable (?User).
    • Gotcha: Non-nullable properties will throw NullPointerException if the entity isn’t found.
    • Use #[FindComplex] for custom queries:
      #[FindComplex('SELECT u FROM App\Entity\User u WHERE u.status = :status')]
      public ?User $user = null;
      
  4. Nested DTOs:

    • Arrays of DTOs are supported but require explicit typing:
      public array $tags; // Each item must be a DTO or scalar
      
    • Gotcha: Mixed arrays (e.g., [string, Dto]) may cause resolution errors.
  5. File Uploads:

    • Use #[Bag(BagEnum::Files)] for UploadedFile properties.
    • Gotcha: Files are only available in FILES request bag. Ensure the correct bag is specified:
      #[Bag(BagEnum::Files)]
      public ?UploadedFile $avatar = null;
      
  6. Events:

    • DtoActionEvent is fired only for valid DTOs (since v4.0.20).
    • Gotcha: Actions won’t trigger if validation fails. Use DtoInvalidEvent for invalid cases.
  7. OpenAPI Issues:

    • Customize schemas with #[OpenApi] to avoid duplicates or incorrect formats.
    • Gotcha: Complex nested DTOs may require manual schema adjustments in Nelmio API Doc.

Debugging Tips

  1. Profiler Panel:

    • Enable the Symfony profiler to inspect DTO resolution timings and metadata:
      # config/packages/dev/dm_dto.yaml
      dm_dto:
          profiler: true
      
  2. Logging:

    • Enable debug logging for DTO events:
      # config/packages/dev/monolog.yaml
      handlers:
          dto:
              type: stream
              path: "%kernel.logs_dir%/dto.log"
              level: debug
              channels: ["dto"]
      
  3. Validation Errors:

    • Access validation errors via DtoInvalidEvent:
      public function onInvalidDto(DtoInvalidEvent $event): void
      {
          $errors = $event->getErrors();
          // $errors contains ConstraintViolationList
      }
      
  4. Custom Resolvers:

    • Implement ProviderInterface for custom logic (e.g., database lookups):
      use DualMedia\DtoRequestBundle\Provider\ProviderInterface;
      
      class CustomUserProvider implements ProviderInterface
      {
          public function provide(string $value, string $type): mixed
          {
      
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.
oxygen/data
oxygen/core
jengo/base
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin
artisan-build/community-prompts
vleroy/smart
ayles-software/laravel-australian-public-holidays
cboxdk/laravel-id-spatie
cboxdk/laravel-id-billing
cboxdk/laravel-id-billing-bridge
simple-as-fuck/php-performance-log
hsluv/hsluv
seampact/codegen
seampact/hooks
seampact/ir
mehedimi/laravel-amp-mail
happones/laravel-vue-i18n-generator
jwerd/inertia-table