Projectsphp-dataCasts And Transformers

PHP Data

Package

Lightweight, framework-agnostic Data Transfer Object (DTO) library with first-class Laravel integration.

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