
Una migración se elimina borrando su archivo, pero solo si nunca se ha ejecutado. Compruébalo con php artisan migrate:status: si está en Ran, lanza primero php artisan migrate:rollback --step=1 y borra el archivo después. Para añadir una columna a una tabla existente, no modifiques nunca la migración antigua: crea una nueva con --table.
Las migraciones versionan el esquema de tu base igual que Git versiona tu código: cada cambio es un archivo, reproducible en cualquier entorno. Tres operaciones cubren casi todo el día a día: añadir una columna a una tabla que ya existe, revertir una migración y eliminar un archivo que nunca se debería haber creado.
Artículo del itinerario Desarrollo web. Todos los comandos y las salidas que verás a continuación se han ejecutado en Laravel 13.30.1 con PHP 8.4.25.
Crear una migración
El comando sigue una convención de nombres que determina el contenido del archivo generado:
php artisan make:migration create_tasks_tableEl nombre empieza por create, seguido del nombre de la tabla en plural, seguido de table. Laravel reconoce este patrón y rellena de antemano Schema::create() con id() y timestamps(), la columna title se añade aquí para el ejemplo, y se han quitado los comentarios generados:
<?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');
}
};Desde Laravel 8, las migraciones son clases anónimas devueltas por return new class extends Migration. Las antiguas migraciones con nombre, del tipo class CreateTasksTable extends Migration, siguen funcionando pero ya no se generan. Del mismo modo, $table->bigIncrements('id') ha dejado paso a $table->id(), y los métodos up() y down() declaran ahora un tipo de retorno void.
Si la tabla tiene que llevar fechas de creación y de modificación, $table->timestamps() las añade: sin ellas aparece el error «Unknown column ‘updated_at’».
Ejecutar la migración crea la tabla:
php artisan migrateAñadir una columna a una tabla existente
Una vez montado el esquema, las consultas Eloquent habituales hacen el resto. No modifiques nunca una migración que ya se ha ejecutado: tus compañeros y tus servidores la han lanzado y no la van a volver a lanzar. Crea una migración nueva, indicando la tabla afectada con --table:
php artisan make:migration add_notes_to_tasks_table --table=tasksEl archivo generado contiene un Schema::table() vacío en los dos sentidos. Rellénalos: la columna en up() y su eliminación en 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 DONEAquí conviene recordar dos precauciones.
Haz que la columna sea nullable, o dale un valor por defecto. En una tabla que ya contiene filas, una columna NOT NULL sin valor por defecto hace fallar la migración: la base no sabe qué escribir en las filas existentes.
Escribe siempre el down(). Es lo que hace reversible la migración. Una migración sin down() convierte cualquier marcha atrás en una intervención manual.
Colocar una columna con after() es propio de MySQL y MariaDB. En SQLite la instrucción se acepta sin error, pero se ignora: la columna se añade al final de la tabla. Verificado en SQLite 3.46.1, donde notes acaba después de updated_at pese al after('title'). El orden de las columnas no tiene ninguna consecuencia funcional.
Revertir una migración ya ejecutada
Empieza por mirar en qué punto estás. migrate:status lista cada migración con su lote y su estado:
php artisan migrate:status 2026_09_02_100000_create_tasks_table ............................... [1] Ran
2026_09_02_194036_add_notes_to_tasks_table ......................... PendingUna migración Pending no se ha ejecutado nunca: su archivo se puede borrar directamente. Una migración Ran hay que revertirla antes.
# revierte solo la última migración
php artisan migrate:rollback --step=1
# revierte todo el último lote
php artisan migrate:rollbackEl rollback ejecuta el método down(). En el ejemplo anterior, la columna notes desaparece efectivamente de la tabla y la migración vuelve a Pending. Es en ese momento, y no antes, cuando puedes borrar el archivo.
Existen dos comandos vecinos, reservados al desarrollo:
# revierte todo y lo vuelve a ejecutar todo
php artisan migrate:refresh
# elimina todas las tablas y luego lo vuelve a ejecutar todo
php artisan migrate:freshmigrate:fresh elimina las tablas, incluidas las que no gestiona ninguna migración. En un servidor de producción, el comando destruye la base sin confirmación posible una vez lanzado. Resérvalo estrictamente al desarrollo local.
Eliminar una migración
No existe ningún make:migration --delete: una migración se elimina borrando su archivo, en database/migrations, la carpeta que devuelve database_path() entre las rutas de la aplicación. La única pregunta es si ya se ha ejecutado.
Nunca se ha ejecutado (estado Pending): borra el archivo, y nada más.
rm database/migrations/2026_09_02_194036_add_notes_to_tasks_table.phpYa se ha ejecutado (estado Ran): reviértela primero y luego borra el archivo.
php artisan migrate:rollback --step=1
rm database/migrations/2026_09_02_194036_add_notes_to_tasks_table.phpBorrar el archivo no elimina la fila correspondiente en la tabla migrations de las bases donde ya se ha ejecutado. Esos entornos conservarán la columna, sin ninguna migración que la explique. En ese caso no borres nada: escribe una migración nueva que deshaga el cambio. El principio es el mismo que con Git, donde se prefiere un commit de reversión a reescribir un historial ya publicado.
Modificar una columna existente
Para cambiar un tipo, una longitud o la nulabilidad, vuelve a declarar la columna y añade change():
public function up(): void
{
Schema::table('tasks', function (Blueprint $table) {
$table->string('title', 500)->change();
});
}Buena noticia para quien vuelva de una versión antigua: el paquete doctrine/dbal, obligatorio durante mucho tiempo para esta operación, ya no lo es desde Laravel 11. Verificado en Laravel 13.30.1, donde change() funciona sin ninguna dependencia adicional.
Los atributos que no repitas se pierden. Una columna ->nullable()->default('x') modificada con un simple ->string('title', 500)->change() vuelve a ser no nullable y sin valor por defecto. Vuelve a declarar la definición completa cada vez.
Errores frecuentes
nullable() o default().

