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