Appearance
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-dataUse 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-configAdd 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:transformThis 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
- spatie/laravel-data documentation
- spatie/laravel-typescript-transformer documentation
- Spatie's Laravel Beyond CRUD course also covers data objects in depth. Login details are stored in BitWarden.