TeachyLeaf library

What are you looking for?

Search practical tutorials and browser-based tools.

Laravel · PHP · Beginner

Laravel database migrations: create and change tables

Follow the migrations that create TeachyLeaf’s blog table and later add a column, then learn how to inspect and apply schema changes.

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

  1. Run php artisan migrate:status and inspect which migration files are pending in your local environment.
  2. Read database/migrations/0001_01_01_000003_create_blogs_table.php, then compare it with database/migrations/2026_11_07_000000_add_content_updated_at_to_blogs_table.php.
  3. Inspect the model's date casts in app/Models/Blog.php and compare them with the migration's timestamp columns.
  4. Run php artisan test to execute tests using the configured in-memory SQLite database.

References

Continue learning

See how the blog model reads and paginates records, then follow a real Laravel form request. For the SQL concepts behind table constraints and queries, explore the SQL constraints lesson.