Who this is for: PHP developers who can read a Laravel route and model and want to understand how database tables are created and changed.
What you will learn: what migrations record, how Laravel's schema builder describes a table, how a later migration adds a column, and how to inspect, run, and cautiously roll back migrations.
Prerequisites: Basic PHP and SQL table concepts. This lesson focuses on Laravel's migration workflow; exact SQL and schema capabilities can vary by database driver.
Tested environment: Laravel Framework 13.16.1 and PHP 8.4.15. The feature-test configuration uses in-memory SQLite. The production database driver may have different DDL, locking, and rollback behavior.
Why keep schema changes in migrations?
A migration is versioned PHP code that describes a database change. Instead of making an undocumented table change directly in a database console, a team can review the migration, apply it in each environment, and inspect which migrations Laravel has recorded as run.
Laravel stores migration names and batch numbers in its migrations table. That history helps Laravel identify pending changes. It is not a backup of table data, and it does not guarantee that a schema change is safe for a live application.
Create a table with the schema builder
TeachyLeaf's initial blog migration creates a blogs table. A shortened version shows the column definitions and database constraint:
Schema::create('blogs', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->string('slug')->unique();
$table->string('excerpt');
$table->text('content');
$table->string('category')->nullable();
$table->string('author')->nullable();
$table->timestamp('published_at')->nullable();
$table->string('status')->default('Draft');
$table->timestamps();
});
Each schema-builder call expresses intent: id() creates the primary key; string() and text() define text columns; nullable() allows a missing value; and default('Draft') supplies a default status. The unique constraint on slug is enforced by the database, not just by a form.
timestamps() adds Laravel's conventional created_at and updated_at columns. The model can cast date columns to date objects for application code.
Evolve the schema with a later migration
TeachyLeaf later added content_updated_at as a separate migration instead of rewriting the original table-creation file:
Schema::table('blogs', function (Blueprint $table) {
$table->timestamp('content_updated_at')->nullable()->after('published_at');
});
This makes the schema history readable: one migration creates the original table, and a later migration adds the content-update timestamp. Changing an already-applied migration does not rerun it on environments where Laravel has recorded it; create a new migration for a new schema change.
The after() column-position modifier is not a portable way to guarantee physical column order across database drivers. Applications should access fields by name and should test migration behavior with the database they deploy.
Inspect the migration history before changing anything
Start by checking the application's configured database and migration state:
php artisan migrate:status
Then create a focused migration for a new column or table. For example:
php artisan make:migration add_summary_to_blogs_table --table=blogs
Open the generated file and review both up() and down(). The up() method applies the intended change. The down() method should reverse it where that can be done without unexpected data loss.
Apply a pending migration
Laravel applies migrations that are not recorded as run against the database selected by the application's configuration:
php artisan migrate
Before running a schema change against production, verify the target environment and database, keep a current restorable backup, review the generated migration, and plan for application compatibility during deployment. A command that succeeds locally does not prove that the production database can apply the same DDL without locking or downtime.
Rollback is not an undo button
To roll back the latest migration batch in a development environment, Laravel provides:
php artisan migrate:rollback
Review the output and migration history first. A rollback calls the migration's down() method; if that method drops a column or table, the data stored there may be lost. Rolling back a batch can also reverse more than one migration. Do not run rollback against production as an experiment.
For a live application, a forward corrective migration is often safer than reversing a deployed change. Backup, rollback, and deployment steps should be planned for the actual database, data volume, and release process.
Verify a schema change in tests
Feature tests can use an isolated test database and reset the schema between tests. TeachyLeaf's PHPUnit configuration uses SQLite in memory, so tests can verify application behavior without changing a developer's or production database.
That is useful, but not a substitute for testing the production database engine. If a migration uses driver-specific DDL, large-table changes, indexes, or constraints, verify it in a representative staging database before deployment.
Try it in this project
- Run
php artisan migrate:statusand inspect which migration files are pending in your local environment. - Read
database/migrations/0001_01_01_000003_create_blogs_table.php, then compare it withdatabase/migrations/2026_11_07_000000_add_content_updated_at_to_blogs_table.php. - Inspect the model's date casts in
app/Models/Blog.phpand compare them with the migration's timestamp columns. - Run
php artisan testto execute tests using the configured in-memory SQLite database.