Map arrays and JSON to strongly-typed DTOs with attribute-driven configuration
composer require philiprehberger/php-dto-mapperMap arrays and JSON to strongly-typed DTOs with attribute-driven configuration.
composer require philiprehberger/php-dto-mapper
use PhilipRehberger\DtoMapper\Attributes\MapFrom;
use PhilipRehberger\DtoMapper\Attributes\Optional;
use PhilipRehberger\DtoMapper\Attributes\CastWith;
use PhilipRehberger\DtoMapper\Casters\DateTimeCaster;
class UserDto
{
public function __construct(
public readonly string $name,
#[MapFrom('email_address')]
public readonly string $email,
#[Optional]
public readonly ?string $nickname = null,
#[CastWith(DateTimeCaster::class)]
public readonly ?\DateTimeImmutable $createdAt = null,
) {}
}
use PhilipRehberger\DtoMapper\DtoMapper;
$dto = DtoMapper::map([
'name' => 'John',
'email_address' => 'john@example.com',
'createdAt' => '2026-01-15 10:30:00',
], UserDto::class);
$dto->name; // 'John'
$dto->email; // 'john@example.com'
$dto->createdAt; // DateTimeImmutable
$dto = DtoMapper::mapJson('{"name": "Jane", "email_address": "jane@example.com"}', UserDto::class);
$dtos = DtoMapper::mapCollection([
['name' => 'Alice', 'email_address' => 'alice@example.com'],
['name' => 'Bob', 'email_address' => 'bob@example.com'],
], UserDto::class);
$dto = DtoMapper::tryMap($data, UserDto::class); // Returns null on failure
Reject unknown source keys to catch API contract violations and typos:
$dto = DtoMapper::strict([
'name' => 'John',
'email_address' => 'john@example.com',
], UserDto::class);
// Throws MappingException: Unknown field "extra_key"
DtoMapper::strict([
'name' => 'John',
'email_address' => 'john@example.com',
'extra_key' => 'oops',
], UserDto::class);
class AddressDto
{
public function __construct(
public readonly string $street,
public readonly string $city,
) {}
}
class PersonDto
{
public function __construct(
public readonly string $name,
public readonly AddressDto $address,
) {}
}
$dto = DtoMapper::map([
'name' => 'Alice',
'address' => ['street' => '123 Main St', 'city' => 'Springfield'],
], PersonDto::class);
$dto->address->city; // 'Springfield'
Access nested source data with dot-notation in #[MapFrom]:
class ProfileDto
{
public function __construct(
public readonly string $name,
#[MapFrom('user.profile.email')]
public readonly string $email,
) {}
}
$dto = DtoMapper::map([
'name' => 'Alice',
'user' => [
'profile' => ['email' => 'alice@example.com'],
],
], ProfileDto::class);
$dto->email; // 'alice@example.com'
Map incomplete data without errors for missing non-nullable fields:
class ProfileDto
{
public function __construct(
public readonly string $name,
public readonly int $age,
public readonly ?string $bio = null,
public readonly string $role = 'member',
) {}
}
$dto = DtoMapper::mapPartial([
'name' => 'Alice',
], ProfileDto::class);
$dto->name; // 'Alice'
$dto->bio; // null (nullable gets null)
$dto->role; // 'member' (default preserved)
// $dto->age is not set (non-nullable without default, skipped)
Properties with union types are coerced by trying each type in declaration order:
class EventDto
{
public function __construct(
public readonly string $name,
public readonly int|string $identifier,
) {}
}
$dto = DtoMapper::map(['name' => 'Login', 'identifier' => '99'], EventDto::class);
$dto->identifier; // 99 (coerced to int, the first type)
$dto = DtoMapper::map(['name' => 'Login', 'identifier' => 'abc-123'], EventDto::class);
$dto->identifier; // 'abc-123' (kept as string)
Implement the Caster interface:
use PhilipRehberger\DtoMapper\Contracts\Caster;
class MoneyFromCentsCaster implements Caster
{
public function cast(mixed $value): float
{
return (int) $value / 100;
}
}
Use with the #[CastWith] attribute:
class OrderDto
{
public function __construct(
#[CastWith(MoneyFromCentsCaster::class)]
public readonly float $total,
) {}
}
Map arrays of items to typed DTO arrays:
use PhilipRehberger\DtoMapper\Attributes\CastWith;
use PhilipRehberger\DtoMapper\Casters\CollectionCaster;
class ItemDto
{
public function __construct(
public readonly string $name,
public readonly int $quantity,
) {}
}
class OrderDto
{
public function __construct(
public readonly string $orderId,
#[CastWith(CollectionCaster::class, args: [ItemDto::class])]
public readonly array $items,
) {}
}
$dto = DtoMapper::map([
'orderId' => 'ORD-001',
'items' => [
['name' => 'Widget', 'quantity' => 3],
['name' => 'Gadget', 'quantity' => 1],
],
], OrderDto::class);
$dto->items[0]->name; // 'Widget'
use PhilipRehberger\DtoMapper\Attributes\CastWith;
use PhilipRehberger\DtoMapper\Casters\EnumCaster;
enum Status: string
{
case Active = 'active';
case Inactive = 'inactive';
}
class AccountDto
{
public function __construct(
public readonly string $name,
#[CastWith(EnumCaster::class, args: [Status::class])]
public readonly Status $status,
) {}
}
| Method | Description |
|---|---|
DtoMapper::map(array $data, string $class): object | Map an associative array to a DTO |
DtoMapper::strict(array $data, string $class): object | Map with unknown key rejection |
DtoMapper::mapJson(string $json, string $class): object | Map a JSON string to a DTO |
DtoMapper::mapPartial(array $data, string $class): object | Map without requiring all fields |
DtoMapper::mapCollection(array $items, string $class): array | Map an array of arrays to DTOs |
DtoMapper::tryMap(array $data, string $class): ?object | Map returning null on failure |
| Attribute | Target | Description |
|---|---|---|
#[MapFrom('key')] | Property | Map from a different source key (supports dot-notation) |
#[Optional] | Property | Allow missing keys, use default value |
#[CastWith(Caster::class)] | Property | Apply a custom caster |
| Caster | Description |
|---|---|
DateTimeCaster | Casts string to DateTimeImmutable |
EnumCaster | Casts string/int to a backed enum |
CollectionCaster | Casts array of arrays to array of DTOs |
composer install
vendor/bin/phpunit
vendor/bin/pint --test
If you find this project useful: