Skip to content

Data objects ​

Data objects allow us to strictly type data structures that would normally be loose arrays or plain objects. This improves static analysis and IDE inference, and hence code quality.

spatie/laravel-data ​

The spatie/laravel-data package provides a way to define data objects that are well-integrated with Laravel.

bash
composer require spatie/laravel-data

Use cases ​

Data objects can be used in many ways. Here are some examples.

Casts on models for JSON columns ​

A Data class can be used directly as an Eloquent cast, so a JSON column is read and written as a typed object instead of a raw array:

php
<?php

namespace App\Data;

use Spatie\LaravelData\Data;

class AddressData extends Data
{
    public function __construct(
        public string $line1,
        public ?string $line2,
        public string $city,
        public string $postcode,
    ) {}
}
php
<?php

namespace App\Models;

use App\Data\AddressData;
use Illuminate\Database\Eloquent\Model;

class Order extends Model
{
    protected $casts = [
        'shipping_address' => AddressData::class,
    ];
}
php
$order->shipping_address->city;

$order->shipping_address = new AddressData(
    line1: '1 Example Street',
    line2: null,
    city: 'London',
    postcode: 'SW1A 1AA',
);

$order->save();

TIP

A plain array is accepted too ($order->shipping_address = ['line1' => ..., 'city' => ..., ...]), which keeps factories and seeders simple.

For a column holding a list of objects, such as an order's line items, cast to a DataCollection instead:

php
use Spatie\LaravelData\DataCollection;

protected $casts = [
    'line_items' => DataCollection::class.':'.LineItemData::class,
];

Request validation ​

Define the shape of an action's input as a Data class rather than an array of $request->validate() rules:

php
<?php

namespace App\Data;

use Spatie\LaravelData\Attributes\Validation\Max;
use Spatie\LaravelData\Attributes\Validation\Min;
use Spatie\LaravelData\Data;

class PlaceOrderData extends Data
{
    public function __construct(
        #[Min(1)]
        public string $paymentToken,

        #[Max(500)]
        public ?string $notes,
    ) {}
}

Validation rules are automatically inferred from property types (string is required, ?string is nullable), and attributes like Min/Max add anything the type alone can't express.

Type-hint the Data class directly in a controller method and request validation will be performed automatically before the method runs – similar to how Laravel form requests work.

php
<?php

namespace App\Http\Controllers;

use App\Actions\PlaceOrder;
use App\Data\PlaceOrderData;
use App\Models\Order;

class OrderController extends Controller
{
    public function store(Order $order, PlaceOrderData $data, PlaceOrder $placeOrder)
    {
        // request already validated

        $placeOrder->handle($order, $data);

        return redirect()->route('orders.show', $order);
    }
}

Passing data to actions ​

Actions can reuse the same typed object:

php
<?php

namespace App\Actions;

use App\Data\PlaceOrderData;
use App\Mail\OrderConfirmation;
use App\Models\Order;
use Illuminate\Support\Facades\Mail;

class PlaceOrder
{
    public function handle(Order $order, PlaceOrderData $data): Order
    {
        $order->charge($data->paymentToken);

        $order->placed_at = $order->freshTimestamp();
        $order->notes = $data->notes;
        $order->save();

        Mail::to($order->user)->send(new OrderConfirmation($order));

        return $order;
    }
}

PlaceOrderData is now the single definition of what placing an order needs, used by the request, the action's signature, and its tests (new PlaceOrderData(paymentToken: '...', notes: null)), with nothing HTTP-specific leaking into the action.

Integration with TypeScript for Inertia/Vue projects ​

spatie/laravel-typescript-transformer generates a TypeScript type from every Data class, so a type shared with an Inertia page can't drift out of sync with the PHP that produces it:

bash
composer require spatie/laravel-typescript-transformer
php artisan vendor:publish --tag=typescript-transformer-config

Add Spatie\LaravelData\Support\TypeScriptTransformer\DataTypeScriptTransformer to the transformers array in config/typescript-transformer.php, then mark any Data class that's exposed to the frontend:

php
<?php

namespace App\Data;

use Spatie\LaravelData\Data;
use Spatie\TypeScriptTransformer\Attributes\TypeScript;

#[TypeScript]
class OrderData extends Data
{
    public function __construct(
        public int $id,
        public string $reference,
        public AddressData $shippingAddress,
    ) {}
}
bash
php artisan typescript:transform

This writes a .d.ts file with the matching type:

ts
export type OrderData = {
    id: number;
    reference: string;
    shippingAddress: AddressData;
};

Import it in the Vue component the Inertia page renders, so props are typed against the exact shape the controller sends:

vue
<script setup lang="ts">
import type { OrderData } from '@/types/generated';

defineProps<{
    order: OrderData;
}>();
</script>

Re-run typescript:transform whenever a Data class changes, ideally as a Composer script alongside format/analyse so the generated types don't silently go stale.

Further reading ​