Migrazioni Laravel: aggiungere una colonna, annullare, eliminare

Aggiungere colonne o tabelle a mano nel database è un lavoro ingrato e porta quasi sempre a differenze tra un ambiente e l'altro. Le migrazioni Laravel mettono il tuo schema sotto controllo di versione, così ogni persona del team lavora sulla stessa struttura.

Migrazioni Laravel: aggiungere una colonna, annullare, eliminare
Risposta rapida

Una migrazione si elimina cancellandone il file, ma solo se non è mai stata eseguita. Controlla con php artisan migrate:status: se è Ran, lancia prima php artisan migrate:rollback --step=1, poi elimina il file. Per aggiungere una colonna a una tabella esistente non modificare mai la vecchia migrazione: creane una nuova con --table.

Le migrazioni versionano lo schema del database come Git versiona il codice: ogni modifica è un file, rigiocabile su qualsiasi ambiente. Tre operazioni coprono quasi tutto il lavoro quotidiano: aggiungere una colonna a una tabella che esiste già, annullare una migrazione ed eliminare un file che non si sarebbe dovuto creare.

Articolo del percorso Sviluppo web. Tutti i comandi e gli output qui sotto sono stati eseguiti su Laravel 13.30.1 con PHP 8.4.25.

Creare una migrazione

Il comando segue una convenzione di denominazione che determina il contenuto del file generato:

bash
php artisan make:migration create_tasks_table

Il nome inizia con create, seguito dal nome della tabella al plurale, seguito da table. Laravel riconosce questo schema e precompila Schema::create() con id() e timestamps(), la colonna title è aggiunta qui per l’esempio, e i commenti generati sono stati tolti:

database/migrations/2026_09_02_100000_create_tasks_table.php
<?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');
    }
};
Se le tue migrazioni non sono così

Da Laravel 8 le migrazioni sono classi anonime restituite da return new class extends Migration. Le vecchie migrazioni con nome, del tipo class CreateTasksTable extends Migration, continuano a funzionare ma non vengono più generate. Allo stesso modo, $table->bigIncrements('id') ha lasciato il posto a $table->id(), e i metodi up() e down() dichiarano ora un tipo di ritorno void.

Se la tabella deve avere le date di creazione e di modifica, $table->timestamps() le aggiunge: la loro assenza provoca l’errore « Unknown column ‘updated_at’ ».

Eseguire la migrazione crea la tabella:

bash
php artisan migrate

Aggiungere una colonna a una tabella esistente

Una volta pronto lo schema, le query Eloquent di tutti i giorni fanno il resto. Non modificare mai una migrazione già eseguita: i tuoi colleghi e i tuoi server l’hanno già giocata e non la rigiocheranno. Crea una nuova migrazione, indicando la tabella interessata con --table:

bash
php artisan make:migration add_notes_to_tasks_table --table=tasks

Il file generato contiene uno Schema::table() vuoto nei due sensi. Riempili: la colonna in up(), la sua rimozione in down().

database/migrations/2026_09_02_194036_add_notes_to_tasks_table.php
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');
    });
}
bash
php artisan migrate
code
  2026_09_02_194036_add_notes_to_tasks_table .................... 47.43ms DONE

Qui vale la pena ricordare due precauzioni.

Rendi la colonna nullable, oppure dalle un valore predefinito. Su una tabella che contiene già delle righe, una colonna NOT NULL senza valore predefinito fa fallire la migrazione: il database non sa che cosa scrivere nelle righe esistenti.

Scrivi sempre il down(). È lui a rendere reversibile la migrazione. Una migrazione senza down() trasforma il minimo passo indietro in un intervento manuale.

after() non funziona ovunque

Posizionare una colonna con after() è proprio di MySQL e MariaDB. Su SQLite l’istruzione viene accettata senza errore ma ignorata: la colonna finisce in fondo alla tabella. Verificato su SQLite 3.46.1, dove notes si ritrova dopo updated_at nonostante l’after('title'). L’ordine delle colonne non ha alcuna conseguenza funzionale.

Annullare una migrazione già eseguita

Comincia col guardare a che punto sei. migrate:status elenca ogni migrazione con il suo lotto e il suo stato:

bash
php artisan migrate:status
code
  2026_09_02_100000_create_tasks_table ............................... [1] Ran
  2026_09_02_194036_add_notes_to_tasks_table ......................... Pending

Una migrazione Pending non è mai stata giocata: il suo file si può eliminare direttamente. Una migrazione Ran va prima annullata.

bash
# annulla solo l'ultima migrazione
php artisan migrate:rollback --step=1

# annulla tutto l'ultimo lotto
php artisan migrate:rollback

Il rollback esegue il metodo down(). Nell’esempio precedente la colonna notes sparisce davvero dalla tabella e la migrazione torna in Pending. È in quel momento, e non prima, che puoi eliminare il file.

Esistono due comandi vicini, da riservare allo sviluppo:

bash
# annulla tutto e rigioca tutto
php artisan migrate:refresh

# elimina tutte le tabelle, poi rigioca tutto
php artisan migrate:fresh
migrate:fresh cancella i dati

migrate:fresh elimina le tabelle, comprese quelle che nessuna migrazione gestisce. Su un server di produzione il comando distrugge il database senza più alcuna conferma possibile una volta lanciato. Riservalo rigorosamente allo sviluppo locale.

Eliminare una migrazione

Non esiste un make:migration --delete: una migrazione si elimina cancellandone il file, in database/migrations, la cartella restituita da database_path() tra i percorsi dell’applicazione. L’unica domanda è se sia già stata eseguita.

Non è mai stata eseguita (stato Pending): elimina il file, nient’altro.

bash
rm database/migrations/2026_09_02_194036_add_notes_to_tasks_table.php

È già stata eseguita (stato Ran): annullala prima, poi elimina il file.

bash
php artisan migrate:rollback --step=1
rm database/migrations/2026_09_02_194036_add_notes_to_tasks_table.php
Se la migrazione è già partita su un altro ambiente

Eliminare il file non cancella la riga corrispondente nella tabella migrations dei database dove è già stata eseguita. Quegli ambienti conserveranno la colonna, senza più nessuna migrazione che la spieghi. In questo caso non eliminare niente: scrivi una nuova migrazione che disfi la modifica. Il principio è lo stesso di Git, dove a riscrivere una storia già pubblicata si preferisce un commit di annullamento.

Modificare una colonna esistente

Per cambiare un tipo, una lunghezza o la nullabilità, ridichiara la colonna e aggiungi change():

php
public function up(): void
{
    Schema::table('tasks', function (Blueprint $table) {
        $table->string('title', 500)->change();
    });
}

Buona notizia per chi arriva da una versione vecchia: il pacchetto doctrine/dbal, a lungo obbligatorio per questa operazione, non lo è più da Laravel 11. Verificato su Laravel 13.30.1, dove change() funziona senza dipendenze aggiuntive.

change() riscrive tutta la definizione

Gli attributi che non ripeti vanno persi. Una colonna ->nullable()->default('x') modificata con un semplice ->string('title', 500)->change() torna non nullable e senza valore predefinito. Ridichiara ogni volta la definizione completa.

Errori frequenti

Modificare una migrazione già eseguita Gli altri ambienti non la rigiocheranno e terranno il vecchio schema. Crea sempre una nuova migrazione.
Colonna NOT NULL senza valore predefinito Su una tabella che contiene già delle righe la migrazione fallisce: il database non sa che cosa scrivere nell'esistente. Aggiungi nullable() o default().
Dimenticare il metodo down() La migrazione diventa irreversibile e il minimo passo indietro richiede un intervento manuale.
migrate:fresh in produzione Il comando elimina tutte le tabelle, comprese quelle che nessuna migrazione gestisce, senza conferma.
after() ignorato fuori da MySQL Su SQLite la colonna viene aggiunta in fondo alla tabella senza errore. Nessun effetto funzionale, ma lo schema non corrisponde a quello che è scritto.
change() riscrive tutta la definizione Gli attributi non ripetuti vanno persi: una colonna nullable con valore predefinito li perde se non li ridichiari.

LaravelMigrationsPHPSQL

Damien Flandrin Sviluppatore web dal 2010, creatore di Gekkode e di Email Impact. Ogni articolo è testato su un progetto reale prima della pubblicazione. Contatti
Newsletter

I nuovi test, tutorial e progetti, via e-mail.

Test riproducibili, codice versionato, risultati datati. Mai spam.