
Migrację usuwa się, kasując jej plik, ale tylko wtedy, gdy nigdy się nie wykonała. Sprawdź to komendą php artisan migrate:status: jeśli stan to Ran, uruchom najpierw php artisan migrate:rollback --step=1, a dopiero potem skasuj plik. Żeby dodać kolumnę do istniejącej tabeli, nigdy nie ruszaj starej migracji: utwórz nową z opcją --table.
Migracje wersjonują schemat bazy tak, jak Git wersjonuje kod: każda zmiana to jeden plik, odtwarzalny na dowolnym środowisku. Codzienną pracę pokrywają trzy operacje: dodanie kolumny do istniejącej tabeli, wycofanie migracji i usunięcie pliku, który nigdy nie powinien powstać.
Artykuł ze ścieżki Programowanie webowe. Wszystkie komendy i ich wyniki uruchomiono na Laravelu 13.30.1 z PHP 8.4.25.
Tworzenie migracji
Komenda trzyma się konwencji nazewnictwa, od której zależy zawartość wygenerowanego pliku:
php artisan make:migration create_tasks_tableNazwa zaczyna się od create, po którym następuje nazwa tabeli w liczbie mnogiej i table. Laravel rozpoznaje ten wzorzec i wstępnie wypełnia Schema::create() metodami id() i timestamps(), kolumna title została tu dodana dla przykładu, a wygenerowane komentarze usunięte:
<?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('tasks', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('tasks');
}
};Od Laravela 8 migracje są klasami anonimowymi, zwracanymi przez return new class extends Migration. Stare migracje z nazwaną klasą, w rodzaju class CreateTasksTable extends Migration, nadal działają, ale nikt ich już nie generuje. Podobnie $table->bigIncrements('id') ustąpiło miejsca $table->id(), a metody up() i down() deklarują dziś typ zwracany void.
Jeśli tabela ma trzymać daty utworzenia i modyfikacji, dodaje je $table->timestamps(): ich brak wywołuje błąd „Unknown column 'updated_at’”.
Uruchomienie migracji tworzy tabelę:
php artisan migrateDodanie kolumny do istniejącej tabeli
Kiedy schemat już stoi, resztę robią typowe zapytania Eloquenta. Nigdy nie zmieniaj migracji, która już się wykonała: koledzy z zespołu i serwery ją odtworzyli i drugi raz tego nie zrobią. Utwórz nową migrację i wskaż tabelę opcją --table:
php artisan make:migration add_notes_to_tasks_table --table=tasksWygenerowany plik zawiera puste Schema::table() w obie strony. Uzupełnij oba: kolumnę w up(), jej usunięcie w down().
public function up(): void
{
Schema::table('tasks', function (Blueprint $table) {
$table->text('notes')->nullable()->after('title');
});
}
public function down(): void
{
Schema::table('tasks', function (Blueprint $table) {
$table->dropColumn('notes');
});
}php artisan migrate 2026_09_02_194036_add_notes_to_tasks_table .................... 47.43ms DONEWarto przypomnieć tu o dwóch środkach ostrożności.
Zrób kolumnę nullowalną albo nadaj jej wartość domyślną. W tabeli, w której są już wiersze, kolumna NOT NULL bez wartości domyślnej wywala migrację: baza nie wie, co wpisać w istniejących wierszach.
Zawsze pisz down(). To ona sprawia, że migracja jest odwracalna. Migracja bez down() zamienia najdrobniejsze cofnięcie w ręczną robotę.
Ustawianie pozycji kolumny przez after() to specjalność MySQL-a i MariaDB. SQLite przyjmuje tę instrukcję bez błędu, ale ją ignoruje: kolumna ląduje na końcu tabeli. Sprawdzone na SQLite 3.46.1, gdzie notes trafia za updated_at mimo after('title'). Kolejność kolumn nie ma żadnego znaczenia funkcjonalnego.
Wycofanie migracji, która już się wykonała
Zacznij od sprawdzenia, na czym stoisz. migrate:status wypisuje każdą migrację razem z jej partią i stanem:
php artisan migrate:status 2026_09_02_100000_create_tasks_table ............................... [1] Ran
2026_09_02_194036_add_notes_to_tasks_table ......................... PendingMigracja w stanie Pending nigdy się nie wykonała: jej plik możesz skasować od razu. Migrację Ran trzeba najpierw wycofać.
# cofa tylko ostatnią migrację
php artisan migrate:rollback --step=1
# cofa całą ostatnią partię
php artisan migrate:rollbackRollback uruchamia metodę down(). W powyższym przykładzie kolumna notes faktycznie znika z tabeli, a migracja wraca do stanu Pending. Dopiero wtedy, nie wcześniej, wolno usunąć plik.
Istnieją jeszcze dwie pokrewne komendy, zarezerwowane dla środowiska deweloperskiego:
# cofa wszystko i odtwarza wszystko od nowa
php artisan migrate:refresh
# usuwa wszystkie tabele, a potem odtwarza wszystko od nowa
php artisan migrate:freshmigrate:fresh usuwa tabele, także te, którymi nie zarządza żadna migracja. Na serwerze produkcyjnym komenda niszczy bazę i raz uruchomiona o nic już nie pyta. Trzymaj ją wyłącznie na lokalnym środowisku.
Usuwanie migracji
Nie ma czegoś takiego jak make:migration --delete: migrację usuwa się, kasując jej plik w database/migrations, czyli w katalogu zwracanym przez database_path() spośród ścieżek aplikacji. Jedyne pytanie brzmi: czy ta migracja już się wykonała.
Nigdy się nie wykonała (stan Pending): usuń plik i tyle.
rm database/migrations/2026_09_02_194036_add_notes_to_tasks_table.phpJuż się wykonała (stan Ran): najpierw ją wycofaj, potem usuń plik.
php artisan migrate:rollback --step=1
rm database/migrations/2026_09_02_194036_add_notes_to_tasks_table.phpSkasowanie pliku nie usuwa odpowiadającego mu wiersza w tabeli migrations tych baz, na których migracja już się wykonała. Te środowiska zachowają kolumnę, której nie tłumaczy już żadna migracja. W takiej sytuacji nie usuwaj niczego: napisz nową migrację, która cofa zmianę. Zasada jest ta sama co w Gicie, gdzie zamiast przepisywać opublikowaną historię, robi się commit odwracający.
Zmiana istniejącej kolumny
Żeby zmienić typ, długość albo nullowalność, zadeklaruj kolumnę ponownie i dopisz change():
public function up(): void
{
Schema::table('tasks', function (Blueprint $table) {
$table->string('title', 500)->change();
});
}Dobra wiadomość dla wracających ze starszej wersji: pakiet doctrine/dbal, przez lata obowiązkowy przy tej operacji, od Laravela 11 nie jest już potrzebny. Sprawdzone na Laravelu 13.30.1, gdzie change() działa bez dodatkowej zależności.
Atrybuty, których nie powtórzysz, przepadają. Kolumna ->nullable()->default('x') zmieniona zwykłym ->string('title', 500)->change() znów jest nienullowalna i bez wartości domyślnej. Za każdym razem deklaruj pełną definicję.
Częste błędy
nullable() albo default().

