Projectsphp-dataLaravel

PHP Data

Package

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

Laravel Integration

Laravel-specific features for Eloquent models and HTTP responses. The core Data class stays framework-agnostic — these APIs light up when Illuminate packages are present. See Framework-agnostic usage.

Hydrating from models

Data::from($model) / fromModel() reads Eloquent attributes (and relations when loaded). Combine with name mapping when attribute names differ from PHP properties.

$user = UserData::from(User::find(1));
$user = UserData::fromModel($model);

Conditional hydration attributes

These live under BradieTilley\Data\Attributes\Laravel and mirror common JsonResource helpers (whenLoaded, whenCounted, whenAppended, when). They are evaluated only when hydrating from an Eloquent model (Data::from($model) / fromModel()). Array, JSON, and other inputs ignore them and hydrate normally.

Type conditional properties as Sometimes|T so a failed condition becomes an omitted key in toArray() (the same role as JsonResource’s MissingValue). See Sometimes.

AttributeIncluded when…Value source
WhenLoaded('roles')$model->relationLoaded('roles')$model->getRelation('roles') (never lazy-loads)
WhenCounted('views') / WhenCounted('postViews')snake {relation}_count is on attributes, or the relation is loadede.g. views_count, post_views_count
WhenAppended('url')$model->hasAppended('url')$model->getAttribute('url')
When(IsAuthenticated::class)condition callable returns truenormal model attribute via the property / MapName input key

Condition signature (When): (Model $model): bool

using signature (optional on all four): (mixed $value, Model $model): mixed

use BradieTilley\Data\Attributes\Laravel\When;
use BradieTilley\Data\Attributes\Laravel\WhenAppended;
use BradieTilley\Data\Attributes\Laravel\WhenCounted;
use BradieTilley\Data\Attributes\Laravel\WhenLoaded;
use BradieTilley\Data\Attributes\Sometimes;

class PostData extends Data
{
    public function __construct(
        public string $title,
        #[WhenLoaded('user')]
        public Sometimes|UserData $user,
        #[WhenLoaded('roles', using: RoleNames::class)]
        public Sometimes|array $roleNames,
        #[WhenLoaded('roles', using: [RolePresenter::class, 'names'])]
        public Sometimes|array $roleLabels,
        #[WhenCounted('views')]
        public Sometimes|int $viewsCount,
        #[WhenAppended('url')]
        public Sometimes|string $url,
        #[When(IsAuthenticated::class)]
        public Sometimes|string $email,
    ) {}
}

Attribute callables

Closures cannot be used in PHP attributes (attribute arguments must be constant expressions). For When conditions and optional using: transforms, pass:

  • an invokable class-stringRoleNames::class where RoleNames implements __invoke(...)
  • a [ClassName, 'method'] pair — static or instance methods are supported
final class RoleNames
{
    public function __invoke(mixed $value, Model $model): array
    {
        return $value->pluck('name')->all();
    }
}

final class RolePresenter
{
    public static function names(mixed $value, Model $model): array
    {
        return $value->pluck('label')->all();
    }
}

Callables are resolved by AttributeCallable:

  • Instantiated with no constructor arguments — there is no service container
  • Invokable classes must be callable via __invoke
  • [Class, 'method'] uses a static call when the method is static; otherwise a fresh instance is created and the method is called

Responses

Every Data object implements JsonSerializable. On Laravel, return a Data object straight from a controller by adding the ResponsableData trait (and the matching interfaces):

use BradieTilley\Data\Concerns\ResponsableData;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Contracts\Support\Responsable;

class ProductData extends Data implements Arrayable, Responsable
{
    use ResponsableData;
    // ...
}

// In a controller — returns a JsonResponse:
return ProductData::from($product);

Or extend BradieTilley\Data\LaravelData instead of Data — it already implements Arrayable + Responsable via the trait:

use BradieTilley\Data\LaravelData;

class ProductData extends LaravelData
{
    // ...
}

The trait/base are opt-in so the framework-agnostic Data core never references Illuminate contracts.

For API envelopes and paginator wrappers, see Predefined DTOs. For wrapping and partials, see Serialization.