# Laravel Data Package Laravel Data is a powerful package by Spatie that creates unified data objects for Laravel applications, eliminating the need for separate form requests, API resources, and TypeScript definitions. By extending from a single `Data` class, you can define your data structure once and use it throughout your application for validation, transformation, API responses, and even TypeScript generation. The package automatically handles type casting, validation, and transformation between different formats (arrays, JSON, Eloquent models) while maintaining type safety across your entire stack. The package provides a complete data layer abstraction with support for nested data objects, collections, lazy loading, partial responses, and automatic validation. It seamlessly integrates with Laravel's ecosystem including Eloquent models, HTTP requests, API resources, and even Inertia.js and Livewire. With features like magical creation methods, automatic route parameter injection, and computed properties, Laravel Data reduces boilerplate code while providing a consistent, type-safe way to handle data throughout your application. ## Creating Basic Data Objects Define a data object by extending the `Data` class and using constructor property promotion for automatic property declaration and initialization. ```php use Spatie\LaravelData\Data; class SongData extends Data { public function __construct( public string $title, public string $artist, ) { } } // Create from array $song = SongData::from([ 'title' => 'Never Gonna Give You Up', 'artist' => 'Rick Astley' ]); // Create from Eloquent model $song = SongData::from(Song::findOrFail($id)); // Create from JSON $song = SongData::from('{"title":"Never Gonna Give You Up","artist":"Rick Astley"}'); // Create directly $song = new SongData('Never Gonna Give You Up', 'Rick Astley'); // Optional creation (returns null if input is null) $song = SongData::optional(null); // null ``` ## Magical Creation Methods Add custom static methods starting with `from` to define type-specific creation logic for your data objects. ```php class SongData extends Data { public function __construct( public string $title, public string $artist, ) { } // Custom creation from model with year public static function fromModel(Song $song): self { return new self("{$song->title} ({$song->year})", $song->artist); } // Custom creation from delimited string public static function fromString(string $string): self { [$title, $artist] = explode('|', $string); return new self($title, $artist); } // Multi-argument creation public static function fromMultiple(string $title, string $artist): self { return new self($title, $artist); } } // Automatically calls fromModel when Song instance is passed $song = SongData::from(Song::first()); // Automatically calls fromString when string with | is passed $song = SongData::from('Never Gonna Give You Up|Rick Astley'); // Automatically calls fromMultiple when multiple string arguments are passed $song = SongData::from('Never Gonna Give You Up', 'Rick Astley'); ``` ## Request Validation and Dependency Injection Inject data objects directly in controllers for automatic validation and population from request data. ```php class SongData extends Data { public function __construct( #[Max(100)] public string $title, #[Max(50)] public string $artist, ) { } } // Controller with automatic dependency injection class UpdateSongController { public function __invoke(Song $model, SongData $data) { // $data is automatically created from request and validated // ValidationException thrown if validation fails $model->update($data->toArray()); return redirect()->back(); } } // Or manually from request class UpdateSongController { public function __invoke(Song $model, Request $request) { // Validates request data before creating data object $data = SongData::from($request); $model->update($data->toArray()); return redirect()->back(); } } // Resolve from container anywhere $songData = app(SongData::class); // Filled with current request data ``` ## Validation Attributes Add Laravel validation rules as PHP attributes on data object properties for automatic validation. ```php use Spatie\LaravelData\Attributes\Validation\*; class SongData extends Data { public function __construct( #[Uuid] public string $uuid, #[Max(100), Min(3)] public string $title, #[Email] public string $contact_email, #[Max(15), IP, StartsWith('192.')] public string $ip_address, #[Between(1, 10)] public int $rating, #[Url] public ?string $website, ) { } } // Generated rules (required/string/int automatically inferred) [ 'uuid' => ['required', 'string', 'uuid'], 'title' => ['required', 'string', 'max:100', 'min:3'], 'contact_email' => ['required', 'string', 'email'], 'ip_address' => ['required', 'string', 'max:15', 'ip', 'starts_with:192.'], 'rating' => ['required', 'integer', 'between:1,10'], 'website' => ['nullable', 'string', 'url'], ] // Validate data manually $validated = SongData::validate($request->all()); // Validate and create $song = SongData::validateAndCreate($request->all()); // Get validation rules $rules = SongData::getValidationRules($request->all()); ``` ## Validation References Reference route parameters, authenticated users, and other fields in validation attributes. ```php use Spatie\LaravelData\Support\Validation\References\*; class SongData extends Data { public function __construct( public string $title, // Reference route parameter (for unique validation ignoring current record) #[Unique('songs', ignore: new RouteParameterReference('song'))] public int $id, // Reference authenticated user property #[Max(new AuthenticatedUserReference('max_song_title_length'))] public string $description, // Reference container dependency #[Max(new ContainerReference(SongSettings::class, 'max_title_length'))] public string $subtitle, // Reference other field in same data object #[RequiredIf('title', 'Never Gonna Give You Up')] public ?string $artist, ) { } } // For model route parameters with different property #[Unique('songs', ignore: new RouteParameterReference('song', 'uuid'))] public string $uuid; // For different authentication guard #[Unique('users', 'email', ignore: new AuthenticatedUserReference(guard: 'api'))] public string $email; // Reference field from root in nested data objects #[RequiredIf(new FieldReference('album_name', fromRoot: true), 'Greatest Hits')] public string $artist; ``` ## Nested Data Objects and Collections Compose data objects within other data objects and validate collections automatically. ```php use Spatie\LaravelData\Attributes\DataCollectionOf; use Spatie\LaravelData\DataCollection; class ArtistData extends Data { public function __construct( public string $name, public int $age, ) { } } class AlbumData extends Data { public function __construct( public string $title, public ArtistData $artist, #[DataCollectionOf(SongData::class)] public DataCollection $songs, ) { } } // Create with nested data $album = AlbumData::from([ 'title' => 'Whenever You Need Somebody', 'artist' => [ 'name' => 'Rick Astley', 'age' => 22 ], 'songs' => [ ['title' => 'Never Gonna Give You Up', 'artist' => 'Rick Astley'], ['title' => 'Together Forever', 'artist' => 'Rick Astley'] ] ]); // Automatic nested validation rules [ 'title' => ['required', 'string'], 'artist' => ['required', 'array'], 'artist.name' => ['required', 'string'], 'artist.age' => ['required', 'integer'], 'songs' => ['required', 'array'], 'songs.*.title' => ['required', 'string'], 'songs.*.artist' => ['required', 'string'], ] // Access nested data echo $album->artist->name; // Rick Astley echo $album->songs[0]->title; // Never Gonna Give You Up ``` ## Data Collections Create type-safe collections of data objects with automatic transformation and validation. ```php // Create collection from array of arrays $songs = SongData::collect([ ['title' => 'Song 1', 'artist' => 'Artist 1'], ['title' => 'Song 2', 'artist' => 'Artist 2'], ]); // Create from Eloquent collection $songs = SongData::collect(Song::all()); // Create from paginator $songs = SongData::collect(Song::paginate(15)); // Collection methods (implements Laravel Enumerable) $songs->count(); $songs->filter(fn($song) => $song->artist === 'Rick Astley'); $songs->map(fn($song) => strtoupper($song->title)); // Array access $firstSong = $songs[0]; $songs[] = new SongData('New Song', 'New Artist'); $songs[1] = SongData::from(['title' => 'Updated', 'artist' => 'Artist']); // Transform collection $array = $songs->toArray(); $json = $songs->toJson(); // Get underlying collection $collection = $songs->toCollection(); $items = $songs->items(); ``` ## API Resources and Responses Return data objects directly from controllers for automatic JSON transformation with proper HTTP status codes. ```php use Spatie\LaravelData\Resource; class SongResource extends Resource { public function __construct( public string $title, public string $artist, ) { } } class SongController { // Return single resource public function show(Song $song) { return SongData::from($song); // Automatic 200 OK response: // {"title":"Never Gonna Give You Up","artist":"Rick Astley"} } // Return collection public function index() { return SongData::collect(Song::all()); // Automatic JSON array response: // [{"title":"Song 1","artist":"Artist 1"},{"title":"Song 2","artist":"Artist 2"}] } // Return paginated collection public function paginated() { return SongData::collect(Song::paginate(15)); // Automatic paginated response with meta: // { // "data": [{"title":"Song 1","artist":"Artist 1"}], // "meta": {"current_page":1,"last_page":5,"per_page":15,"total":73} // } } // Create resource (automatic 201 CREATED on POST) public function store(SongData $data) { $song = Song::create($data->toArray()); return SongData::from($song); // 201 CREATED status code } // Return empty blueprint for forms public function create() { return SongData::empty(); // {"title":null,"artist":null} } } ``` ## Partial Responses with Include/Exclude Control which properties are included in API responses using query parameters and lazy properties. ```php use Spatie\LaravelData\Lazy; use Spatie\LaravelData\Attributes\*; class SongData extends Data { public function __construct( public string $title, public string $artist, #[AutoLazy] public int $play_count, #[AutoLazy] public string $lyrics, ) { } public static function allowedRequestIncludes(): ?array { return ['play_count', 'lyrics']; } public static function allowedRequestExcludes(): ?array { return ['artist']; } } // Query with includes: GET /api/songs/1?include=play_count,lyrics // Response: {"title":"...","artist":"...","play_count":1000000,"lyrics":"..."} // Query with excludes: GET /api/songs/1?exclude=artist // Response: {"title":"..."} // Query with only: GET /api/songs/1?only=title // Response: {"title":"..."} // Query with except: GET /api/songs/1?except=title // Response: {"artist":"..."} // Programmatic control $song = SongData::from($model); $song->include('play_count')->toArray(); $song->exclude('artist')->toArray(); $song->only('title', 'artist')->toArray(); $song->except('lyrics')->toArray(); ``` ## Eloquent Model Casting Cast Eloquent model attributes to data objects and collections automatically. ```php class Song extends Model { protected $casts = [ 'metadata' => SongMetadataData::class, 'artists' => DataCollection::class . ':' . ArtistData::class, ]; } // Store data object in model $song = Song::create([ 'title' => 'Never Gonna Give You Up', 'metadata' => new SongMetadataData( genre: 'Pop', duration: 213 ), 'artists' => [ ['name' => 'Rick Astley', 'age' => 57] ] ]); // Or use array (automatically converted to data object) $song = Song::create([ 'title' => 'Never Gonna Give You Up', 'metadata' => [ 'genre' => 'Pop', 'duration' => 213 ], 'artists' => [ ['name' => 'Rick Astley', 'age' => 57] ] ]); // Retrieve as data object $metadata = Song::find($id)->metadata; // SongMetadataData object $artists = Song::find($id)->artists; // DataCollection of ArtistData // Access typed properties echo $song->metadata->genre; // Pop echo $song->artists[0]->name; // Rick Astley ``` ## Polymorphic and Abstract Data Objects Handle multiple data object types with abstract parent classes and morph maps for refactor-safe polymorphism. ```php abstract class RecordConfigData extends Data { public function __construct( public int $tracks, ) {} } class CdRecordConfigData extends RecordConfigData { public function __construct( int $tracks, public int $bytes, ) { parent::__construct($tracks); } } class VinylRecordConfigData extends RecordConfigData { public function __construct( int $tracks, public int $rpm, ) { parent::__construct($tracks); } } // Register morph map (in AppServiceProvider) use Spatie\LaravelData\Support\DataConfig; app(DataConfig::class)->enforceMorphMap([ 'cd' => CdRecordConfigData::class, 'vinyl' => VinylRecordConfigData::class, ]); // Cast abstract parent class class Record extends Model { protected $casts = [ 'config' => RecordConfigData::class, 'configs' => DataCollection::class . ':' . RecordConfigData::class, ]; } // Store different child types $cd = Record::create([ 'config' => new CdRecordConfigData(tracks: 12, bytes: 1000) ]); $vinyl = Record::create([ 'config' => new VinylRecordConfigData(tracks: 12, rpm: 33) ]); // Retrieve correct type $cd->config; // CdRecordConfigData object $vinyl->config; // VinylRecordConfigData object // Stored as: {"type":"cd","data":{"tracks":12,"bytes":1000}} ``` ## Property Injection Attributes Automatically inject dependencies, route parameters, and authenticated user data into data objects. ```php use Spatie\LaravelData\Attributes\*; class CreateSongData extends Data { public function __construct( public string $title, // Inject authenticated user automatically #[FromAuthenticatedUser] public UserData $creator, // Inject specific property from authenticated user #[FromAuthenticatedUserProperty('id')] public int $creator_id, // Inject from service container #[FromContainer(SongSettings::class)] public SongSettings $settings, // Inject route parameter #[FromRouteParameter('artist')] public Artist $artist, // Inject and load Eloquent relation #[LoadRelation('albums')] public ?Collection $albums, ) { } } // In controller - dependencies automatically injected public function store(CreateSongData $data) { // $data->creator is populated with Auth::user() // $data->creator_id is populated with Auth::id() // $data->settings is resolved from container // $data->artist is from route parameter Song::create($data->toArray()); } ``` ## Data Transformation Transform data objects to arrays and JSON with control over property names and values. ```php use Spatie\LaravelData\Attributes\MapName; use Spatie\LaravelData\Attributes\MapInputName; use Spatie\LaravelData\Attributes\MapOutputName; class SongData extends Data { public function __construct( public string $title, // Map both input and output #[MapName('artist_name')] public string $artist, // Map only input (snake_case from API to camelCase) #[MapInputName('created_at')] public Carbon $createdAt, // Map only output (internal name to API name) #[MapOutputName('total_plays')] public int $playCount, ) { } } // Input with snake_case $song = SongData::from([ 'title' => 'Never Gonna Give You Up', 'artist_name' => 'Rick Astley', 'created_at' => '2024-01-01', ]); // Transform to array $array = $song->toArray(); // [ // "title" => "Never Gonna Give You Up", // "artist_name" => "Rick Astley", // "createdAt" => "2024-01-01T00:00:00.000000Z", // "total_plays" => 1000000 // ] // Transform to JSON $json = $song->toJson(); // Get all without transformation $all = $song->all(); // JSON serialize json_encode($song); ``` ## Advanced Creation with Factories Configure data object creation with validation control, custom casts, and property mapping. ```php // Create without validation $song = SongData::factory() ->withoutValidation() ->from($untrustedData); // Create with validation $song = SongData::factory() ->validate() ->from($request); // Create with custom casts $song = SongData::factory() ->withCasts([CustomDateCast::class]) ->from($data); // Create without property name mapping $song = SongData::factory() ->withoutPropertyNameMapping() ->from($data); // Create without magic methods $song = SongData::factory() ->withoutMagicalCreation() ->from($data); // Create with optional values (using Optional wrapper instead of null) $song = SongData::factory() ->useOptionalValues() ->from($data); // Chain multiple configurations $song = SongData::factory() ->validate() ->mapPropertyNames() ->withCasts([DateCast::class]) ->from($request); ``` ## Custom Validation Rules and Hooks Override validation behavior with custom rules, messages, and validator hooks. ```php class SongData extends Data { public function __construct( public string $title, public string $artist, ) { } // Define custom validation rules public static function rules(ValidationContext $context): array { return [ 'title' => ['required', 'string', 'min:3'], 'artist' => ['required', 'string', new CustomArtistRule()], ]; } // Custom validation messages public static function messages(): array { return [ 'title.required' => 'A song must have a title', 'title.min' => 'The title must be at least 3 characters', ]; } // Custom attribute names public static function attributes(): array { return [ 'title' => 'song title', 'artist' => 'artist name', ]; } // Hook into validator for custom logic public static function withValidator(Validator $validator): void { $validator->after(function ($validator) { if ($this->title === 'Forbidden Title') { $validator->errors()->add('title', 'This title is not allowed'); } }); } } ``` ## Lazy Properties and Computed Values Define lazy-loaded properties and computed values that are conditionally included in responses. ```php use Spatie\LaravelData\Lazy; use Spatie\LaravelData\Attributes\*; class SongData extends Data { public function __construct( public string $title, public string $artist, // Manual lazy with Lazy::create() public Lazy|string $lyrics, // Automatic lazy loading #[AutoLazy] public int $play_count, // Lazy with closure #[AutoClosureLazy] public string $expensive_calculation, // Lazy only when relation is loaded #[AutoWhenLoadedLazy('albums')] public Collection $albums, // Computed property (not stored, only calculated) #[Computed] public string $formatted_title, ) { } public static function fromModel(Song $model): self { return new self( title: $model->title, artist: $model->artist, lyrics: Lazy::create(fn() => $model->lyrics), play_count: $model->play_count, expensive_calculation: $model->expensiveCalculation(), albums: $model->albums, formatted_title: strtoupper($model->title) ); } } // Default: lazy properties not included $song = SongData::from($model); $song->toArray(); // {"title":"...","artist":"...","formatted_title":"..."} // Include lazy properties $song->include('lyrics', 'play_count')->toArray(); // {"title":"...","artist":"...","lyrics":"...","play_count":1000000,"formatted_title":"..."} ``` ## Wrapping Responses Wrap data objects in custom keys for consistent API response structure. ```php class SongData extends Data { public function __construct( public string $title, public string $artist, ) { } } // Wrap individual response $song = SongData::from($model); $wrapped = $song->wrap('song')->toArray(); // {"song": {"title":"...","artist":"..."}} // Wrap with nested key $wrapped = $song->wrap('data.song')->toArray(); // {"data": {"song": {"title":"...","artist":"..."}}} // Disable wrapping $song->withoutWrapping()->toArray(); // Global wrapping (in config/data.php) 'wrap' => 'data', // Collections automatically wrapped when paginated SongData::collect(Song::paginate()); // { // "data": [...], // "meta": {...} // } ``` ## TypeScript Generation Generate TypeScript interfaces from data objects for type-safe frontend development. ```php // Data object class SongData extends Data { public function __construct( public int $id, public string $title, public string $artist, #[AutoLazy] public ?string $lyrics, #[DataCollectionOf(AlbumData::class)] public DataCollection $albums, ) { } } // Generate TypeScript (artisan command) php artisan data:generate-types // Generated TypeScript interface export interface SongData { id: number; title: string; artist: string; lyrics?: string | null; albums: AlbumData[]; } // Use in frontend import { SongData } from './generated-types'; const song: SongData = { id: 1, title: 'Never Gonna Give You Up', artist: 'Rick Astley', albums: [] }; ``` ## Empty Data Objects Generate empty data objects with default values for form initialization and blueprints. ```php class SongData extends Data { public function __construct( public string $title = 'Untitled Song', public string $artist = 'Unknown Artist', public int $year = 2024, public ?string $album = null, ) { } } // Get empty data with defaults $empty = SongData::empty(); // [ // "title" => "Untitled Song", // "artist" => "Unknown Artist", // "year" => 2024, // "album" => null // ] // Override defaults $empty = SongData::empty([ 'title' => 'New Song Title', 'year' => 2025 ]); // [ // "title" => "New Song Title", // "artist" => "Unknown Artist", // "year" => 2025, // "album" => null // ] // Filter properties $empty = SongData::empty(only: ['title', 'artist']); // ["title" => "Untitled Song", "artist" => "Unknown Artist"] $empty = SongData::empty(except: ['album']); // ["title" => "Untitled Song", "artist" => "Unknown Artist", "year" => 2024] ``` ## Summary and Integration Patterns Laravel Data serves as a unified data layer that eliminates redundant code across your application. Instead of maintaining separate form requests for validation, API resources for transformation, and TypeScript definitions for the frontend, you define your data structure once in a Data class. This single source of truth automatically handles validation when data enters your application via HTTP requests, transforms seamlessly when returning API responses, casts to and from Eloquent model attributes, and even generates TypeScript interfaces for frontend type safety. The package reduces boilerplate by inferring validation rules from PHP types, automatically handling nested validation, and providing automatic dependency injection in controllers. The integration patterns are designed for real-world Laravel applications. Use Data objects as controller method parameters to automatically validate and populate from requests. Return them directly from controllers for automatic JSON responses with proper status codes and pagination support. Cast Eloquent model attributes to Data objects or DataCollections for type-safe database interactions. Leverage lazy properties and partial responses for optimized API performance. Use injection attributes to automatically populate data from authenticated users, route parameters, or the service container. With support for magical creation methods, custom transformers and casts, computed properties, and wrapping configurations, Laravel Data provides a complete, flexible, and type-safe data layer that scales from simple DTOs to complex API resources with nested relationships and polymorphic data structures.