Skip to content

Property Mapping

Learn how to map source keys to different property names using MapFrom attribute.

Property mapping allows you to map source data keys to different property names in your Dto:

class UserDto extends SimpleDto
{
public function __construct(
#[MapFrom('full_name')]
public readonly string $name,
#[MapFrom('email_address')]
public readonly string $email,
) {}
}
$dto = UserDto::fromArray([
'full_name' => 'John Doe',
'email_address' => 'john@example.com',
]);
echo $dto->name; // 'John Doe'
echo $dto->email; // 'john@example.com'
use event4u\DataHelpers\SimpleDto\Attributes\MapFrom;
class ProductDto extends SimpleDto
{
public function __construct(
#[MapFrom('product_name')]
public readonly string $name,
#[MapFrom('product_price')]
public readonly float $price,
#[MapFrom('product_sku')]
public readonly string $sku,
) {}
}
class UserDto extends SimpleDto
{
public function __construct(
public readonly string $name,
#[MapFrom('contact.email')]
public readonly string $email,
#[MapFrom('contact.phone')]
public readonly ?string $phone = null,
) {}
}
$dto = UserDto::fromArray([
'name' => 'John Doe',
'contact' => [
'email' => 'john@example.com',
'phone' => '+1234567890',
],
]);
class UserDto extends SimpleDto
{
public function __construct(
#[MapFrom('user_id')]
public readonly int $id,
#[MapFrom('user_name')]
public readonly string $name,
#[MapFrom('user_email')]
public readonly string $email,
#[MapFrom('created_at')]
public readonly Carbon $createdAt,
) {}
}
// Map from API response
$dto = UserDto::fromArray([
'user_id' => 1,
'user_name' => 'John Doe',
'user_email' => 'john@example.com',
'created_at' => '2024-01-15',
]);
class OrderDto extends SimpleDto
{
public function __construct(
#[MapFrom('order_id')]
public readonly int $id,
#[MapFrom('customer_name')]
public readonly string $customerName,
#[MapFrom('order_total')]
public readonly float $total,
#[MapFrom('order_status')]
public readonly string $status,
) {}
}
class LegacyUserDto extends SimpleDto
{
public function __construct(
#[MapFrom('usr_id')]
public readonly int $userId,
#[MapFrom('usr_nm')]
public readonly string $userName,
#[MapFrom('usr_eml')]
public readonly string $userEmail,
#[MapFrom('usr_crt_dt')]
public readonly Carbon $createdDate,
) {}
}
class EventDto extends SimpleDto
{
public function __construct(
#[MapFrom('event_name')]
public readonly string $name,
#[MapFrom('event_date'), Cast(DateTimeCast::class)]
public readonly Carbon $date,
) {}
}
class UserDto extends SimpleDto
{
public function __construct(
#[MapFrom('full_name'), Required, Min(3)]
public readonly string $name,
#[MapFrom('email_address'), Required, Email]
public readonly string $email,
) {}
}
// ✅ Good - clear property names
#[MapFrom('usr_nm')]
public readonly string $userName;
// ❌ Bad - unclear abbreviation
#[MapFrom('usr_nm')]
public readonly string $usrNm;
/**
* @property int $id Mapped from 'user_id'
* @property string $name Mapped from 'full_name'
*/
class UserDto extends SimpleDto
{
public function __construct(
#[MapFrom('user_id')]
public readonly int $id,
#[MapFrom('full_name')]
public readonly string $name,
) {}
}
// ✅ Good - map external API to clean internal names
#[MapFrom('external_api_field_name')]
public readonly string $cleanPropertyName;

For complex mappings, you can define a template property in your DTO:

use event4u\DataHelpers\SimpleDto;
use event4u\DataHelpers\SimpleDtoMapperTrait;
class UserDto extends SimpleDto
{
use SimpleDtoMapperTrait;
protected ?array $mapperTemplate = [
'name' => '{{ user.name }}',
'email' => '{{ user.email }}',
'city' => '{{ user.address.city }}',
];
public function __construct(
public readonly string $name,
public readonly string $email,
public readonly string $city,
) {}
}
// Template is automatically applied
$dto = UserDto::fromArray([
'user' => [
'name' => 'John Doe',
'email' => 'john@example.com',
'address' => [
'city' => 'New York',
],
],
]);
echo $dto->name; // 'John Doe'
echo $dto->city; // 'New York'

Alternatively, you can override the getMapperTemplate() method:

use event4u\DataHelpers\SimpleDto;
use event4u\DataHelpers\SimpleDtoMapperTrait;
class UserDto extends SimpleDto
{
use SimpleDtoMapperTrait;
public function __construct(
public readonly string $name,
public readonly string $email,
public readonly string $city,
) {}
public function getMapperTemplate(): array
{
return [
'name' => '{{ user.name }}',
'email' => '{{ user.email }}',
'city' => '{{ user.address.city }}',
];
}
}
// Template is automatically applied
$dto = UserDto::fromArray([
'user' => [
'name' => 'John Doe',
'email' => 'john@example.com',
'address' => [
'city' => 'New York',
],
],
]);
echo $dto->name; // 'John Doe'
echo $dto->city; // 'New York'

Dynamic Template Changes with setMapperTemplate()

Section titled “Dynamic Template Changes with setMapperTemplate()”

You can dynamically change the template on a DTO instance:

use event4u\DataHelpers\SimpleDto;
use event4u\DataHelpers\SimpleDtoMapperTrait;
class UserDto extends SimpleDto
{
use SimpleDtoMapperTrait;
protected ?array $mapperTemplate = [
'id' => '{{ user.id }}',
'name' => '{{ user.name }}',
];
public function __construct(
public readonly int $id = 0,
public readonly string $name = '',
) {}
}
// First mapping with initial template
$dto1 = UserDto::fromArray([
'user' => [
'id' => 100,
'name' => 'Initial Name',
],
]);
echo $dto1->id; // 100
echo $dto1->name; // 'Initial Name'
// Change template on instance
$instance = new UserDto();
$instance->setMapperTemplate([
'id' => '{{ customer.customer_id }}',
'name' => '{{ customer.full_name }}',
]);
// Second mapping with new template
$dto2 = UserDto::fromArray([
'customer' => [
'customer_id' => 200,
'full_name' => 'New Customer Name',
],
], $instance->getMapperTemplate());
echo $dto2->id; // 200
echo $dto2->name; // 'New Customer Name'

Apply filters to specific properties during mapping:

use event4u\DataHelpers\SimpleDto;
use event4u\DataHelpers\SimpleDtoMapperTrait;
use event4u\DataHelpers\Filters\TrimStrings;
use event4u\DataHelpers\Filters\LowercaseStrings;
class UserDto extends SimpleDto
{
use SimpleDtoMapperTrait;
public function __construct(
public readonly string $name,
public readonly string $email,
) {}
protected function mapperFilters(): array
{
return [
'name' => new TrimStrings(),
'email' => [new TrimStrings(), new LowercaseStrings()],
];
}
}
// Filters are automatically applied
$dto = UserDto::fromArray([
'name' => ' John Doe ',
'email' => ' JOHN@EXAMPLE.COM ',
]);
echo $dto->name; // 'John Doe' (trimmed)
echo $dto->email; // 'john@example.com' (trimmed and lowercased)

Apply global filters to all properties:

use event4u\DataHelpers\SimpleDto;
use event4u\DataHelpers\SimpleDtoMapperTrait;
use event4u\DataHelpers\Filters\TrimStrings;
class UserDto extends SimpleDto
{
use SimpleDtoMapperTrait;
public function __construct(
public readonly string $name,
public readonly string $email,
public readonly string $city,
) {}
protected function mapperPipeline(): array
{
return [new TrimStrings()];
}
}
// Pipeline is automatically applied to all properties
$dto = UserDto::fromArray([
'name' => ' John Doe ',
'email' => ' john@example.com ',
'city' => ' New York ',
]);
echo $dto->name; // 'John Doe' (trimmed)
echo $dto->email; // 'john@example.com' (trimmed)
echo $dto->city; // 'New York' (trimmed)

You can combine all three for powerful data transformation:

use event4u\DataHelpers\SimpleDto;
use event4u\DataHelpers\SimpleDtoMapperTrait;
use event4u\DataHelpers\Filters\TrimStrings;
use event4u\DataHelpers\Filters\LowercaseStrings;
class UserDto extends SimpleDto
{
use SimpleDtoMapperTrait;
protected ?array $mapperTemplate = [
'name' => '{{ user.name }}',
'email' => '{{ user.email }}',
'city' => '{{ user.address.city }}',
];
public function __construct(
public readonly string $name,
public readonly string $email,
public readonly string $city,
) {}
protected function mapperFilters(): array
{
return [
'email' => new LowercaseStrings(),
];
}
protected function mapperPipeline(): array
{
return [new TrimStrings()];
}
}
// All transformations are automatically applied
$dto = UserDto::fromArray([
'user' => [
'name' => ' John Doe ',
'email' => ' JOHN@EXAMPLE.COM ',
'address' => [
'city' => ' New York ',
],
],
]);
echo $dto->name; // 'John Doe' (template + pipeline)
echo $dto->email; // 'john@example.com' (template + filters + pipeline)
echo $dto->city; // 'New York' (template + pipeline)

You can override DTO configuration at runtime:

// Override template
$dto = UserDto::from($data, [
'name' => '{{ custom.path }}',
]);
// Override filters
$dto = UserDto::from($data, null, [
'name' => new CustomFilter(),
]);
// Override pipeline (merged with DTO pipeline)
$dto = UserDto::from($data, null, null, [
new AdditionalFilter(),
]);

Priority:

  1. Runtime parameters (highest priority)
  2. DTO configuration ($mapperTemplate property or getMapperTemplate() method, mapperFilters(), mapperPipeline())
  3. #[MapFrom] attributes (only when no template is applied)
  4. Auto-mapping (lowest priority)

The following working examples demonstrate this feature:

All examples are fully tested and can be run directly.

The functionality is thoroughly tested. Key test files:

Run the tests:

Terminal window
# Run tests
task test:unit -- --filter=PropertyMapping