Installation
composer require bujanov/dto-bundle
Add to config/bundles.php:
return [
// ...
Bujanov\DtoBundle\BujanovDtoBundle::class => ['all' => true],
];
First DTO Class
Create a class (e.g., src/Dto/UserDto.php) extending AbstractDto:
namespace App\Dto;
use Bujanov\DtoBundle\AbstractDto;
class UserDto extends AbstractDto
{
/** @var int */
public $id;
/** @var string */
public $name;
}
Instantiation & Usage
use App\Dto\UserDto;
$dto = new UserDto();
$dto->id = 1;
$dto->name = 'John Doe';
// Hydrate from array
$dto->hydrate(['id' => 2, 'name' => 'Jane Smith']);
src/AbstractDto.php (Core functionality)src/DtoInterface.php (Contract)src/DtoTrait.php (Optional trait for reusable logic)Request/Response DTOs
// In a controller
public function create(Request $request, UserDto $dto)
{
$dto->hydrate($request->all());
$user = $this->userService->create($dto);
return new JsonResponse($dto->toArray());
}
Validation Integration Combine with Symfony Validator:
use Symfony\Component\Validator\Validator\ValidatorInterface;
$errors = $validator->validate($dto);
if (count($errors) > 0) {
// Handle validation errors
}
Type Safety with PHP 8 Attributes
use Bujanov\DtoBundle\Attributes\DtoProperty;
class UserDto extends AbstractDto
{
#[DtoProperty(type: 'int')]
public $id;
#[DtoProperty(type: 'string', required: true)]
public $name;
}
DTO Factories
class UserDtoFactory
{
public static function fromEntity(User $user): UserDto
{
$dto = new UserDto();
$dto->id = $user->getId();
$dto->name = $user->getName();
return $dto;
}
}
Nested DTOs
class AddressDto extends AbstractDto { /* ... */ }
class UserDto extends AbstractDto
{
public $address; // Type: AddressDto
}
No Built-in Validation
symfony/validator separately.AbstractDto and add validation logic.No Magic Methods
getName() vs. name property).#[DtoProperty] attributes to document types and requirements.Limited Type Safety
#[Assert\Type] from Symfony Validator for runtime checks.No Immutable Support
hydrate() or use readonly properties (PHP 8.1+).Serialization Quirks
toArray() includes all public properties, even "private" ones (via visibility tricks).toArray() to filter properties:
public function toArray(): array
{
return array_filter($this->toArray(), fn($k) => !str_starts_with($k, '_'));
}
var_dump($dto->hydrate($data, true)) (third param for strict mode).UserDto->address->user).Custom Hydration
Override hydrate() to add logic:
public function hydrate(array $data, bool $strict = false, bool $deep = false): void
{
parent::hydrate($data, $strict, $deep);
$this->normalizeName(); // Custom logic
}
Custom Serialization
Override toArray() or add JsonSerializable:
public function jsonSerialize(): array
{
return $this->toArray();
}
Event Dispatching Integrate with Symfony Events:
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
class UserDto extends AbstractDto
{
public function __construct(private EventDispatcherInterface $dispatcher) {}
public function hydrate(array $data): void
{
$this->dispatcher->dispatch(new DtoHydrateEvent($this, $data));
parent::hydrate($data);
}
}
Doctrine Integration
Use Hydrator to map DTOs to entities:
$user = new User();
$hydrator = new ObjectPropertyHydrator();
$hydrator->hydrate($dto->toArray(), $user);
config/packages/bujanov_dto.yaml; all settings are code-based.App\Dto\).clone sparingly; DTOs are lightweight but nested objects may not be.How can I help you explore Laravel packages today?