philiprehberger/php-dto-mapper

Map arrays and JSON to strongly-typed DTOs with attribute-driven configuration

Maintainers

Package info

github.com/philiprehberger/php-dto-mapper

pkg:composer/philiprehberger/php-dto-mapper

Fund package maintenance!

philiprehberger

Statistics

Installs: 38

Dependents: 0

Suggesters: 0

Stars: 1

Open Issues: 0

v1.2.0 2026-03-28 00:57 UTC

This package is auto-updated.

Last update: 2026-04-22 16:00:31 UTC


README

Tests Latest Version on Packagist Last updated

Map arrays and JSON to strongly-typed DTOs with attribute-driven configuration.

Requirements

  • PHP 8.2+

Installation

composer require philiprehberger/php-dto-mapper

Usage

Define a DTO

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,
    ) {}
}

Map from array

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

Map from JSON

$dto = DtoMapper::mapJson('{"name": "Jane", "email_address": "jane@example.com"}', UserDto::class);

Map a collection

$dtos = DtoMapper::mapCollection([
    ['name' => 'Alice', 'email_address' => 'alice@example.com'],
    ['name' => 'Bob', 'email_address' => 'bob@example.com'],
], UserDto::class);

Safe mapping

$dto = DtoMapper::tryMap($data, UserDto::class); // Returns null on failure

Strict mapping

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);

Nested DTOs

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'

Dot-notation access

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'

Partial mapping

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)

Union types

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)

Custom casters

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,
    ) {}
}

Collection caster

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'

Enum casting

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,
    ) {}
}

API

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

Attributes

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

Built-in Casters

Caster Description
DateTimeCaster Casts string to DateTimeImmutable
EnumCaster Casts string/int to a backed enum
CollectionCaster Casts array of arrays to array of DTOs

Development

composer install
vendor/bin/phpunit
vendor/bin/pint --test

Support

If you find this project useful:

Star the repo

🐛 Report issues

💡 Suggest features

❤️ Sponsor development

🌐 All Open Source Projects

💻 GitHub Profile

🔗 LinkedIn Profile

License

MIT