Een Laravel migration is de manier om je databaseschema te versioneren, precies zoals je je code versioneert. In plaats van tabellen handmatig in een SQL-client aan te maken, beschrijf je elke schemawijziging in een PHP-klasse; die klasse laat iedereen in je team en je server dezelfde structuur opbouwen met één commando. Een seeder is op zijn beurt de officiële manier om dat schema te vullen met realistische testdata. Gebruik je beide samen, dan krimpt het opzetten van het project vanaf nul tot één regel, en verdwijnen de meeste "het werkte op mijn machine"-problemen vanzelf.
Wat een migration is en waarom het beter is dan handgeschreven SQL
Een schema met de hand schrijven kent twee grote problemen: er wordt geen wijzigingsgeschiedenis bijgehouden, en ieders database eindigt net iets anders. Migrations lossen beide op. Elk bestand krijgt een naam met een timestamp, zodat de volgorde ondubbelzinnig is; de up()-methode past de wijziging toe en de down()-methode draait haar terug. Omdat migrations in Git staan, gaat je schemageschiedenis samen met je commitgeschiedenis vooruit.
- Herhaalbaar: hetzelfde commando bouwt op elke machine dezelfde structuur.
- Omkeerbaar: je kunt een foute wijziging terugdraaien met
down(). - Traceerbaar: de
migrations-tabel registreert welk bestand is uitgevoerd, dus het draait nooit twee keer.
Je eerste migration aanmaken
Het Artisan-commando genereert zowel het bestand als het skelet. Gebruik --create voor een gloednieuwe tabel en --table om een bestaande aan te passen:
php artisan make:migration create_posts_table
# Om een kolom aan een bestaande tabel toe te voegen:
php artisan make:migration add_status_to_posts_table --table=posts
Het gegenereerde bestand komt onder database/migrations/ terecht. Een create_posts_table ziet er zo uit:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->string('slug')->unique();
$table->text('body');
$table->boolean('published')->default(false);
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('posts');
}
};
$table->id() maakt een automatisch oplopende primaire sleutel, en timestamps() voegt de kolommen created_at en updated_at toe. Er is een vloeiende API voor de meeste kolomtypen: string, text, integer, boolean, json, decimal en nog veel meer.
Migrations uitvoeren en terugdraaien
Om alle openstaande migrations toe te passen:
php artisan migrate
Dit zijn de commando's die je tijdens het ontwikkelen het vaakst zult gebruiken:
php artisan migrate:rollback— maakt de laatste batch migrations ongedaan.php artisan migrate:rollback --step=2— draait de laatste twee stappen terug.php artisan migrate:fresh— verwijdert alle tabellen en bouwt opnieuw op.php artisan migrate:refresh— draait alles terug en voert het opnieuw uit.php artisan migrate:status— toont welke migrations zijn uitgevoerd.
Let op in productie: migrate:fresh en refresh vernietigen data. Gebruik op een live server alleen php artisan migrate --force; de --force-vlag laat het in productie draaien zonder om bevestiging te vragen.
Relaties en indexen
Je verbindt tabellen met foreign keys, en Laravel biedt daarvoor een korte helper:
Schema::create('comments', function (Blueprint $table) {
$table->id();
$table->foreignId('post_id')
->constrained()
->cascadeOnDelete();
$table->text('body');
$table->timestamps();
});
foreignId('post_id')->constrained() maakt automatisch een foreign key die verwijst naar de id-kolom van de posts-tabel. cascadeOnDelete() zorgt ervoor dat wanneer een post wordt verwijderd, zijn comments meegaan. Vergeet niet om met $table->index('slug') een index toe te voegen aan vaak bevraagde kolommen; op grote tabellen verbetert dat de leessnelheid aanzienlijk.
Testdata genereren met een seeder
Zodra het schema klaar is, schrijf je een seeder om het te vullen. Artisan genereert ook de seeder-klasse:
php artisan make:seeder PostSeeder
De klasse komt onder database/seeders/ en voegt records in binnen de run()-methode:
<?php
namespace Database\Seeders;
use App\Models\Post;
use Illuminate\Database\Seeder;
class PostSeeder extends Seeder
{
public function run(): void
{
Post::create([
'title' => 'Hallo wereld',
'slug' => 'hallo-wereld',
'body' => 'De inhoud van de eerste post.',
'published' => true,
]);
}
}
Om één seeder los uit te voeren, geef je de klassenaam mee:
php artisan db:seed --class=PostSeeder
Om alle seeders aan elkaar te knopen, gebruik je call() in DatabaseSeeder; php artisan db:seed voert die hoofdklasse uit:
public function run(): void
{
$this->call([
UserSeeder::class,
PostSeeder::class,
CommentSeeder::class,
]);
}
Realistische, grootschalige data met factories
Een paar regels met de hand schrijven is prima voor kleine projecten, maar echte tests vragen om honderden records. Hier komt de factory in beeld: die beschrijft hoe één record wordt gegenereerd en laat de Faker-bibliotheek de rest doen.
php artisan make:factory PostFactory --model=Post
public function definition(): array
{
return [
'title' => fake()->sentence(),
'slug' => fake()->unique()->slug(),
'body' => fake()->paragraphs(3, true),
'published' => fake()->boolean(80),
];
}
Daarna produceert één regel in de seeder 50 realistische posts:
Post::factory()->count(50)->create();
Je kunt ook gerelateerde data via factories aan elkaar knopen: Post::factory()->count(10)->hasComments(5)->create() maakt voor elke post vijf comments aan. Deze aanpak voedt demo's en geautomatiseerde tests met consistente maar gevarieerde data.
Een praktische workflow en veelgemaakte fouten
In je dagelijkse werk bouwt de schoonste reset schema en data in één commando opnieuw op:
php artisan migrate:fresh --seed
Een paar praktische regels besparen je problemen:
- Zodra een migration is uitgevoerd en gecommit, bewerk je hem niet meer — schrijf een nieuwe. Bewerken werkt alleen op machines die hem nog niet hebben gedraaid.
- Vul de
down()-methode altijd zo in dat hij het exacte tegenovergestelde vanup()is, en test echt of terugdraaien werkt. updateOrCreatein seeders voorkomt dubbele records wanneer je dezelfde seeder een tweede keer draait.- Zet nooit productiedata hardcoded in een seeder; afgezien van vaste referentiedata (landen, rollen) zijn seeders voor test en ontwikkeling.
Veelgestelde vragen
Wat is het verschil tussen een migration en een seeder?
Een migration definieert en versioneert de structuur van de database (tabellen, kolommen, indexen, relaties). Een seeder voegt data toe aan die structuur. De een bouwt het skelet, de ander vult het met test- of voorbeeldrecords. Ze dienen verschillende doelen maar werken samen.
Verwijdert het uitvoeren van migrations op een live server data?
php artisan migrate past alleen openstaande nieuwe migrations toe en raakt bestaande data nooit aan. De commando's die data vernietigen zijn migrate:fresh en migrate:refresh; gebruik die niet in productie. Geef op een live server altijd de voorkeur aan migrate --force.
Hoe voeg ik later een kolom toe?
Bewerk de oude migration niet. Maak een nieuwe met make:migration add_x_to_y_table --table=y, voeg de kolom in up() toe met bijvoorbeeld $table->string('x')->nullable(), draai het terug in down() met $table->dropColumn('x'), en draai daarna php artisan migrate.
Wil je een schone, testbare databaselaag in je project? Ik ontwerp en implementeer Laravel migration-, seeder- en factory-structuren afgestemd op wat jouw project nodig heeft. Om de details te bespreken, neem contact met me op.