dualmedia/symfony-request-dto-bundle
Installation:
composer require dualmedia/symfony-request-dto-bundle
Register the bundle in config/bundles.php:
DualMedia\DtoRequestBundle\DtoBundle::class => ['all' => true],
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;
}
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');
}
}
Replace manual request handling (e.g., $request->request->get('name')) with type-safe DTOs. The bundle handles:
"25" → int).#[FindOneBy]).DTO Design:
AbstractDto as the base class.#[Assert\NotBlank]).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;
}
public AddressDto $address;
Controller Integration:
public function create(CreateUserDto $dto): Response
{
// $dto is populated from request body/query/headers
}
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);
}
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;
}
Validation:
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;
}
#[Assert\All({
new Assert\NotBlank(),
new Assert\Type('string')
})]
public array $tags;
Events and Actions:
DtoInvalidEvent or DtoActionEvent for custom responses:
# config/services.yaml
DualMedia\DtoRequestBundle\EventListener\DtoInvalidEventListener:
tags:
kernel.event_listener:
event: dto.invalid
method: onInvalidDto
#[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;
}
API Documentation:
@param annotations needed.#[OpenApi] attributes:
use DualMedia\DtoRequestBundle\Dto\Attribute\OpenApi;
class PaginationDto extends AbstractDto
{
#[OpenApi(description: 'Page number', example: 1)]
public int $page = 1;
}
doctrine/orm-bundle is installed for entity loading.Validator for custom constraints.DtoResolver in tests to mock request data:
$dto = $this->resolver->resolve(ProfileDto::class, $request);
$this->assertEquals('John', $dto->name);
Caching Quirks:
var/cache). Clear cache after DTO changes:
php bin/console cache:clear
config/packages/dm_dto.yaml:
dm_dto:
cache: false
Validation Groups:
#[Groups]:
use Symfony\Component\Validator\Constraints as Assert;
class UserDto extends AbstractDto
{
#[Assert\NotBlank(groups: ['registration'])]
public string $email;
}
$validator->validate($dto, ['registration']); // Explicitly specify groups
Entity Loading:
#[FindOneBy] requires the property to be nullable (?User).NullPointerException if the entity isn’t found.#[FindComplex] for custom queries:
#[FindComplex('SELECT u FROM App\Entity\User u WHERE u.status = :status')]
public ?User $user = null;
Nested DTOs:
public array $tags; // Each item must be a DTO or scalar
[string, Dto]) may cause resolution errors.File Uploads:
#[Bag(BagEnum::Files)] for UploadedFile properties.FILES request bag. Ensure the correct bag is specified:
#[Bag(BagEnum::Files)]
public ?UploadedFile $avatar = null;
Events:
DtoActionEvent is fired only for valid DTOs (since v4.0.20).DtoInvalidEvent for invalid cases.OpenAPI Issues:
#[OpenApi] to avoid duplicates or incorrect formats.Profiler Panel:
# config/packages/dev/dm_dto.yaml
dm_dto:
profiler: true
Logging:
# config/packages/dev/monolog.yaml
handlers:
dto:
type: stream
path: "%kernel.logs_dir%/dto.log"
level: debug
channels: ["dto"]
Validation Errors:
DtoInvalidEvent:
public function onInvalidDto(DtoInvalidEvent $event): void
{
$errors = $event->getErrors();
// $errors contains ConstraintViolationList
}
Custom Resolvers:
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
{
How can I help you explore Laravel packages today?