Het aanmaken en wijzigen van model entries is een van de meest voorkomende acties binnen Laravel applicaties. Gebruik je Inertia en Typescript? Dan kan je met Laravel Data het aanmaken en wijzigen van model entries een stuk eenvoudiger laten verlopen. In dit artikel laten we je zien hoe je dat doet.
Stel je hebt een model Artist met de volgende properties name en age. Je Create.vue form kan er dan als volgt uitzien:
<script setup lang="typescript">
import { useForm } from "@inertiajs/vue3";
type ArtistData = {
name: string;
age: number;
};
const form = useForm<ArtistData>();
const submit = form.post('/artist');
</script>
<template @submit.prevent="submit">
<form>
<input type="text" v-model="form.name">
<input type="number" v-model="form.age">
<button type="submit">Create</button>
</form>
</template>
De store controller:
use App\Http\Requests\StoreArtistRequest;
class StoreArtistController extends controller
{
public function __invoke(StoreArtistRequest $request)
{
Artist::create($request->validated());
return redirect()->back();
}
}
De form request:
use Illuminate\Foundation\Http\FormRequest;
class StoreArtistRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(StoreArtistRequest $request)
{
return [
'name' => 'required',
'age' => ['required', 'integer'],
];
}
}
Stel er komt nu een nieuwe property email bij, dan moeten we de volgende acties uitvoeren:
- De type definitie in Create.vue moet worden aangepast
- Een extra input veld moet worden toegevoegd in Create.vue
- In de form request moeten we een extra validatieregel toevoegen.
Dit zijn niet de meest tijdrovende acties maar we kunnen het wel stroomlijnen met het gebruik van Laravel Data. Met Laravel Data kunnen we DTO's aanmaken. Een DTO is bedoeld voor het aanmaken en doorgeven van gestructureerde data binnen je applicatie. Een DTO is een eenvoudige PHP klasse, maar met Laravel Data kunnen we DTO's aanmaken die over veel handige functionaliteiten beschikken.
Als eerste installeren we Laravel Data: composer require spatie/laravel-data
Vervolgens maken we een Artist DTO aan: php artisan make:data Artist. Laravel Data maakt dan ArtistData.php aan in app\data.
En we bouwen de DTO op:
namespace App\Data;
use Spatie\LaravelData\Data;
class ArtistData extends Data
{
public function __construct(
public string $name;
public int $age;
) {}
}
Doordat Laravel Data automatisch validatie regels kan generen op basis van de properties kunnen we de form request als volgt aanpassen:
use Illuminate\Foundation\Http\FormRequest;
class StoreArtistRequest extends FormRequest
{
use WithData;
public function authorize(): bool
{
return true;
}
protected function dataClass(): string
{
return ArtistData::class;
}
}
Vervolgens passen we de controller aan:
use App\Http\Requests\StoreArtistRequest;
class StoreArtistController extends controller
{
public function __invoke(StoreArtistRequest $request)
{
$data = $request->getData(); //<-- validatie wordt uitgevoerd.
Artist::create($data->toArray());
return redirect()->back();
}
}
Nu denk je misschien, ok leuk maar we hebben nu vooral zaken verplaatst en het is er nog niet handiger op geworden. Klopt, maar we zijn nog niet klaar. Dankzij Typescript Transformer kunnen we PHP klassen omzetten naar Typescript types. Ideaal voor onze ArtistData Typescript type waardoor we die niet meer zelf hoeven bij te houden als er een property bijkomt, wijzigt of verdwijnt. Zo gebruik je de typescript transformer:
- Installeer via composer:
composer require spatie/laravel-typescript-transformer - Voer de transformer uit:
php artisan typescript:transform
Je zal zien dat in je resources folder een nieuwe types folder is aangemaakt met daarin generated.d.ts met de inhoud:
declare namespace App.Data {
export type ArtistData = {
name: string;
age: number
};
}
We kunnen nu de gegenereerde type als volgt gebruiken:
<script setup lang="typescript">
import { useForm } from "@inertiajs/vue3";
import { ArtistData } from "/types/generated.d.ts";
const form = useForm<ArtistData>();
const submit = form.post('/artist');
</script>
<template @submit.prevent="submit">
<form >
<input type="text" v-model="form.name">
<input type="number" v-model="form.age">
<button type="submit">Create</button>
</form>
</template>
Waar we voorheen de datastructuur van een artist op meerdere plekken moesten bijhouden (Create form en de form request) gebeurt dat nu nog enkel in ArtistData.php.
Een DTO property toevoegen
Stel, we breiden het Artist model uit met een extra property email. Dan hoeven die enkel in de DTO en Vue form toe te voegen:
namespace App\Data;
use Spatie\LaravelData\Data;
class ArtistData extends Data
{
public function __construct(
public string $name;
public int $age;
public string $email;
) {}
}
Voer vervolgens de transform commando uit: php artisan typescript:transform dit commando is overigens te automatiseren met Vite, dit staat hier beschreven
En pas het formulier aan:
<script setup lang="typescript">
import { useForm } from "@inertiajs/vue3";
import { ArtistData } from "/types/generated.d.ts";
const form = useForm<ArtistData>();
const submit = form.post('/artist');
</script>
<template @submit.prevent="submit">
<form>
<input type="text" v-model="form.name">
<input type="number" v-model="form.age">
<input type="text" v-model="form.email">
<button type="submit">Create</button>
</form>
</template>
Andere Laravel Data features
Nu we Laravel Data volledig hebben ingesteld kunnen meer handige functies gaan gebruiken. We laten je er aan aantal zien.
Validatie regels toevoegen met PHP attributen
Stel, we willen email validatie toevoegen. Dan kunnen dat eenvoudig doen met een PHP attribuut:
namespace App\Data;
use Spatie\LaravelData\Data;
class ArtistData extends Data
{
public function __construct(
public string $name;
public int $age;
#[Email()]
public string $email;
) {}
}
Je kan hier
lezen over alle mogelijkheden met validatie via PHP attributen.
Handmatige validatieregels voor meer controle
Wil je meer controle of wil je complexere validatieregels toevoegen dan kan je ook het standaard formaat aanhouden:
namespace App\Data;
use Spatie\LaravelData\Data;
class ArtistData extends Data
{
public function __construct(
public string $name;
public int $age;
public string $email;
) {}
public static function rules(): array
{
return [
'name' => ['required'],
'age' => ['required', 'integer', 'min:0'],
'email' => ['required', 'emial']
]
}
}
Over handmatige validatie regels kan je hier alles vinden.
DTO casts
Stel dat we de artist model uitbreiden met een extra property primary_instrument dat de volgende structuur heeft:
[
'model' => 'Eastman SB59'
'brand' => 'Goldburst'
'type' => 'guitar'
]
In het model kunnen we aangeven via de cast dat primary_instrument een array is:
protected function casts(): array
{
return ['primary_instrument' => 'array'];
}
Het zou alleen mooi zijn als we via de cast ook een een DTO kunnen specificeren. Dan krijgen we geen platte array terug maar een data object met properties voorzien van de juiste types + autocomplete van de properties in je IDE. Met Laravel Data kan je dit doen.
Eerst maak je een PrimaryIntrumentData DTO aan die er als volgt uit ziet:
namespace App\Data;
use Spatie\LaravelData\Data;
class PrimaryIntrumentData extends Data
{
public function __construct(
public string $model;
public string $brand;
public string $type;
) {}
}
Vervolgens pas je de cast in de model aan:
use App\Data\PrimaryIntrumentData;
protected function casts(): array
{
return ['primary_instrument' => PrimaryIntrumentData::class];
}
Laravel Data zorgt er voor de primary_instrument wordt omgezet naar een DTO als je de model opvraagt. Het zet de DTO ook weer netjes om naar json als deze wordt opgeslagen in de database.
Je kan ook DTO's nesten, je ArtistData DTO kan er bijvoorbeeld zo uitzien:
namespace App\Data;
use Spatie\LaravelData\Data;
class ArtistData extends Data
{
public function __construct(
public string $name;
public int $age;
#[Email()]
public string $email;
public PrimaryInstrumentData $primary_instrument;
) {}
}
Wanneer je een Artist model entry omzet naar een ArtistData DTO met bijvoorbeeld ArtistData::from(Artist::find(1)); Dan wordt de primary_instrument dus automatisch omgezet naar een instance van PrimaryInstrumentData. Dit werkt ook voor relaties bijvoorbeeld https://spatie.be/docs/laravel-data/v4/as-a-data-transfer-object/model-to-data-object#content-relations.
Dit is met name handig bij het bewerken van model entry. In je edit controller kan je de DTO instance meegeven aan je edit form:
use App\Http\Requests\StoreArtistRequest;
class EditArtistController extends controller
{
public function __invoke(Artist $artist)
{
return Inertia::render('EditArtist', [
'artist' => ArtistData::from($artist)
])
}
}
Hierdoor weet je altijd zeker dat de structuur van een artist overeen komt met wat er in Vue wordt verwacht. Dit leidt tot minder fouten waarbij properties tussen wat je mee geeft en PHP en ontvangt in Vue niet overeen komen. Gebruik je dus Inertia en Typescript dan is Laravel Data en de Typescript transformer echt aan te bevelen. Laravel Data is ook geschikt voor Livewire maar valt buiten de scope van dit artikel, hier kan je daar meer over lezen.
Tot slot
Los van de handige features van Laravel Data zijn DTO's in het algemeen ook een ideale vervanging van arrays. Zo geef je data een heldere structuur met duidelijke types. Je IDE snapt DTO's ook beter dan arrays dus het kan autocomplete verzorgen en foutmeldingen tonen in je IDE wanneer je incorrecte properties definieert.
We hebben je laten zien hoe je met Laravel Data de data tussen je backend en frontend met Inertia minder foutgevoelig kan laten verlopen. Daarbij is Laravel Data niet alleen handig in combinatie met Inertia. Ook binnen de backend van je applicatie is het erg fijn om met DTO's op basis van Laravel Data te werken. We hebben slechts het topje van de ijsberg laten zien maar is nog veel meer mogelijk met Laravel Data. Het is daarom zeker aan te raden om is de documentatie te raadplegen.
Typescript transformer automatiseren
Wanneer een DTO verandert kan je de Typescript transfer automatisch laten draaien zodat je de command niet meer handmatig hoeft uit te voeren. Dit doe je als volgt met Vite.
- Installeer vite-plugin-run:
npm install vite-plugin-run --dev - In het plugin gedeelte van je vite config stel je de run plugin als volgt in:
import { defineConfig } from "vite";
import { run } from "vite-plugin-run";
return defineConfig({
plugins: [
run([
{
name: "typescript",
run: ["php", "artisan", "typescript:transform"],
pattern: ["app/{Data}/**/*.php"]
}
])
],
}
- Wanneer je nu
npm run devdraait wordtphp artisan typescript:transformaangeroepen zodra een wijziging wordt gedetecteert in je DTO's.