Casts & Transformers
Custom casts convert raw input into typed values during hydration. Transformers convert complex values into simple types during serialization.
Import attributes from BradieTilley\Data\Attributes.
WithCast
Attach a custom cast to convert raw input into a typed value (e.g. a value object)
during hydration, without touching the Data core. A cast implements
BradieTilley\Data\Casts\Cast:
use BradieTilley\Data\Casts\Cast;
use BradieTilley\Data\Typing\ReflectedParameter;
final class MoneyCast implements Cast
{
public function cast(mixed $value, ReflectedParameter $param): mixed
{
return new Money((int) round(((float) $value) * 100));
}
}
Attach it with #[WithCast], optionally passing constructor arguments to the cast:
use BradieTilley\Data\Attributes\WithCast;
class ProductData extends Data
{
public function __construct(
#[WithCast(MoneyCast::class)]
public Money $price,
#[WithCast(RoundedCast::class, 2)] // extra args go to the cast constructor
public float $weight,
) {}
}
A cast takes precedence over the built-in Carbon/enum/array/nested-Data casting.
It runs after null handling, so a null on a nullable/optional property is not
passed to the cast.
See also Spatie's creating a cast documentation for the same methodology.
Casting iterable items
When a property is an array or Collection whose element type is declared via
#[ArrayOf] or a phpdoc
annotation, each element is cast individually. Built-in casting covers nested
Data, backed enums, Carbon and DateTimeInterface types:
class ReleaseData extends Data
{
public function __construct(
public string $title,
/**
* @var array<int, DateTime>
*/
public array $releaseDates,
) {}
}
ReleaseData::from([
'title' => 'Never Gonna Give You Up',
'releaseDates' => [
'1987-07-27T12:00:00Z',
'1987-07-28T12:00:00Z',
],
]);
// releaseDates is list<DateTime>
For custom value objects, implement IterableItemCast alongside Cast so
#[WithCast] applies per element instead of to the iterable as a whole:
use BradieTilley\Data\Casts\Cast;
use BradieTilley\Data\Casts\IterableItemCast;
final class MoneyCast implements Cast, IterableItemCast
{
public function cast(mixed $value, ReflectedParameter $param): mixed { /* … */ }
public function castIterableItem(mixed $value, ReflectedParameter $param): mixed
{
return $this->cast($value, $param);
}
}
class OrderData extends Data
{
public function __construct(
#[WithCast(MoneyCast::class)]
#[ArrayOf(Money::class)]
public array $prices,
) {}
}
See also Spatie's casts documentation for the same iterable-typing pattern.
WithTransformer
Attach a custom transformer to convert a complex property value into a simple
type during serialization (toArray()). A transformer implements
BradieTilley\Data\Transformers\Transformer:
use BradieTilley\Data\Transformers\Transformer;
use BradieTilley\Data\Typing\ReflectedProperty;
final class MoneyTransformer implements Transformer
{
public function transform(mixed $value, ReflectedProperty $prop): mixed
{
return $value->cents / 100;
}
}
Attach it with #[WithTransformer], optionally passing constructor arguments:
use BradieTilley\Data\Attributes\WithTransformer;
class ProductData extends Data
{
public function __construct(
#[WithTransformer(MoneyTransformer::class)]
public Money $price,
) {}
}
A local transformer takes precedence over built-in type handling and
DataSerializeable. It never receives null — nullable
properties that are null stay null.
See also Spatie's creating a transformer documentation for the same methodology.
WithCastAndTransformer
When the same class should handle both hydration and serialization, implement
both Cast and Transformer and attach it with #[WithCastAndTransformer]:
use BradieTilley\Data\Attributes\WithCastAndTransformer;
use BradieTilley\Data\Casts\Cast;
use BradieTilley\Data\Transformers\Transformer;
use BradieTilley\Data\Typing\ReflectedParameter;
use BradieTilley\Data\Typing\ReflectedProperty;
final class MoneyCastAndTransformer implements Cast, Transformer
{
public function cast(mixed $value, ReflectedParameter $param): mixed
{
return new Money((int) round(((float) $value) * 100));
}
public function transform(mixed $value, ReflectedProperty $prop): mixed
{
return $value->cents / 100;
}
}
class ProductData extends Data
{
public function __construct(
#[WithCastAndTransformer(MoneyCastAndTransformer::class)]
public Money $price,
) {}
}
Constructor arguments are forwarded the same way as #[WithCast] /
#[WithTransformer]. Prefer this over separate cast + transformer attributes
when the conversion is symmetric.
DateFormat
Formats a DateTimeInterface property when serializing. Without it, dates
serialize as ISO-8601 (c).
use BradieTilley\Data\Attributes\DateFormat;
class EventData extends Data
{
public function __construct(
#[DateFormat('Y-m-d')]
public Carbon $date,
) {}
}
DataSerializeable
Implement BradieTilley\Data\DataSerializeable on any value object to control how
it appears in toArray() when no local transformer is attached. This remains the
fallback for value objects that self-serialize:
use BradieTilley\Data\DataSerializeable;
final class Money implements DataSerializeable
{
public function __construct(public int $cents) {}
public function toDataSerializedValue(): mixed
{
return $this->cents / 100;
}
}