Laravel — P28: Migrations

Evolving Your Laravel Projects: Mastering Migrations

Migrations are a way for everyone on the team to easily share database schemas. Once you create a migration, everyone on the team will be able to run it and generate a new table. There is no more messaging others and telling them that they need to add a new column or change the column type. Upload the migrations and if something breaks, the team can run the migrations again to update the tables.

How Do You Create a Migration?

Generating a migration is just another artisan command. I told you that we would be using artisan a lot. The command itself is make:migration.

php artisan make:migration create_tests_table

This new migration will be added to the database/migrations directory. There, you’ll find the migration named and a timestamp prepended to it. We want the files to have a unique name and to be placed in order since the migrations will run from top to bottom. In other words, we may have migrations that depend on other tables that already exist. If we run a migration and that dependency table does not exist, you will get an error. Most of the time it’s because the table is not in the correct order. If you ever happen to find that that’s the case, you can rename the file and add a timestamp that comes before.

The migration creates the following file.

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('tests', function (Blueprint $table) {
            $table->id();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('tests');
    }
};

It’s interesting that Laravel already knows which table we want to create: tests. You can see that in the Schema::create() method. How does it know that? Because that’s how we named our migration: create_tests_table. As long as you follow that convention create_XXXXXXs_table, Laravel will know how to fill out the details.

Everything that you need to fill out will be in the up() method, specifically inside of the Schema::create() method. Laravel has already placed the $table->id() and $table->timestamps() inside of it. We can add other fields that we find necessary. For example, if we wanted to add a typical varchar datatype, we simply use the string() method.

Running the Migration

Let’s add a few datatypes to our tests table.

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('tests', function (Blueprint $table) {
            $table->id();
            $table->string('first_name');
            $table->string('last_name');
            $table->foreignId('user_id')->constrained();
            $table->string('email')->unique();
            $table->text('bio');
            $table->integer('age');
            $table->float('money');
            $table->boolean('is_active');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('tests');
    }
};

Each of the datatypes should be straightforward to read. Let’s run the migration now to actually create our tables.

php artisan migrate
We can see which tables were generated. If you’re using Docker, head over to your mysql container and log into it. Check your .env file for the credentials. Here’s mine, for example.
DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=laravel_tutorials
DB_USERNAME=sail
DB_PASSWORD=password
To log into your container, type:
sh-4.4# mysql -u sail -p 
Enter password: 
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 5464
Server version: 8.0.31 MySQL Community Server - GPL

You’ll then need to switch to the database, which is laravel_tutorials. Running the describe tests command shows us our tests table schema.

mysql> describe tests;
+------------+-----------------+------+-----+---------+----------------+
| Field      | Type            | Null | Key | Default | Extra          |
+------------+-----------------+------+-----+---------+----------------+
| id         | bigint unsigned | NO   | PRI | NULL    | auto_increment |
| first_name | varchar(255)    | NO   |     | NULL    |                |
| last_name  | varchar(255)    | NO   |     | NULL    |                |
| user_id    | bigint unsigned | NO   | MUL | NULL    |                |
| email      | varchar(255)    | NO   | UNI | NULL    |                |
| bio        | text            | NO   |     | NULL    |                |
| age        | int             | NO   |     | NULL    |                |
| money      | double(8,2)     | NO   |     | NULL    |                |
| is_active  | tinyint(1)      | NO   |     | NULL    |                |
| created_at | timestamp       | YES  |     | NULL    |                |
| updated_at | timestamp       | YES  |     | NULL    |                |
+------------+-----------------+------+-----+---------+----------------+
11 rows in set (0.00 sec)

We can see that our id is an unsigned bigint. Our money column is a double(8,2) and our is_active column is a tinyint(1). The timestamps method generated two columns: created_at and updated_at.

Pretty cool. Now you can upload your code and when the next user runs the migration, the same database and tables will be generated.

Here are all the data types:

bigIncrements
bigInteger
binary
boolean
char
dateTimeTz
dateTime
date
decimal
double
enum
float
foreignId
foreignIdFor
foreignUlid
foreignUuid
geometryCollection
geometry
id
increments
integer
ipAddress
json
jsonb
lineString
longText
macAddress
mediumIncrements
mediumInteger
mediumText
morphs
multiLineString
multiPoint
multiPolygon
nullableMorphs
nullableTimestamps
nullableUlidMorphs
nullableUuidMorphs
point
polygon
rememberToken
set
smallIncrements
smallInteger
softDeletesTz
softDeletes
string
text
timeTz
time
timestampTz
timestamp
timestampsTz
timestamps
tinyIncrements
tinyInteger
tinyText
unsignedBigInteger
unsignedDecimal
unsignedInteger
unsignedMediumInteger
unsignedSmallInteger
unsignedTinyInteger
ulidMorphs
uuidMorphs
ulid
uuid
year

Reversing a Migration

I know that you’ve seen it and were wondering what the down() method does. It reverses the migration. Sometimes you just need to roll it back.

To roll back the migration that you just ran, you can use the following command:

php artisan migrate:rollback
Lets say that you ran three migrations and you need to roll them back now. You can use the same command with the --step flag.
php artisan migrate:rollback --step=5
To dump all of your tables and rerun your migrations, you may use:
php artisan migrate:fresh

Just know that this will delete all of your data. We’ll go into seeding more data soon.

Updating Tables

A new requirement just came through and we need to store phone numbers in the tests table. Instead of modifying the original table and rerunning that migration, which will undoubtedly delete all the data, we just need to add a single string column. Let’s run our artisan command.

php artisan make:migration add_phone_to_tests_table

There’s a clear naming convention here as well. We want to add the phone to the tests table. Laravel will know how to piece together some information.

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('tests', function (Blueprint $table) {
            //
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('tests', function (Blueprint $table) {
            //
        });
    }
};

If you look inside of the up() method, you’ll notice a Schema::table() this time. When we were creating our table, we used Schema::create(). The field that we want to add is the phone and we want it to appear after the email column.

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('tests', function (Blueprint $table) {
            $table->string('phone')->after('email');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('tests', function (Blueprint $table) {
            //
        });
    }
};

If we didn’t chain on the after() method, the phone column would have been added to the end of the tests table.

Within the down() method, we want to specify a way to reverse what we just did. Since we added a column, we want to remove it if we run the migration.

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('tests', function (Blueprint $table) {
            $table->string('phone')->after('email');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('tests', function (Blueprint $table) {
            $table->dropColumn('phone');
        });
    }
};

And we’re done. We can now run the php artisan migrate command to run just this migration. If we check our table, we’ll see it in there.

mysql> describe tests;
+------------+-----------------+------+-----+---------+----------------+
| Field      | Type            | Null | Key | Default | Extra          |
+------------+-----------------+------+-----+---------+----------------+
| id         | bigint unsigned | NO   | PRI | NULL    | auto_increment |
| first_name | varchar(255)    | NO   |     | NULL    |                |
| last_name  | varchar(255)    | NO   |     | NULL    |                |
| user_id    | bigint unsigned | NO   | MUL | NULL    |                |
| email      | varchar(255)    | NO   | UNI | NULL    |                |
| phone      | varchar(255)    | NO   |     | NULL    |                |
| bio        | text            | NO   |     | NULL    |                |
| age        | int             | NO   |     | NULL    |                |
| money      | double(8,2)     | NO   |     | NULL    |                |
| is_active  | tinyint(1)      | NO   |     | NULL    |                |
| created_at | timestamp       | YES  |     | NULL    |                |
| updated_at | timestamp       | YES  |     | NULL    |                |
+------------+-----------------+------+-----+---------+----------------+
12 rows in set (0.00 sec)

How does Laravel know which migrations have been run and which are new?

Whenever the command php artisan migrate is run, an entry is added to the migrations table as well, which is the file name of each migration. In your database, run the following command to see all the tables that are present:

mysql> show tables;
+-----------------------------+
| Tables_in_laravel_tutorials |
+-----------------------------+
| failed_jobs                 |
| migrations                  |
| password_resets             |
| personal_access_tokens      |
| tests                       |
| users                       |
+-----------------------------+
6 rows in set (0.00 sec)

One of them is the migrations table. Let’s see what’s in there.

mysql> select * from migrations;
+----+-------------------------------------------------------+-------+
| id | migration                                             | batch |
+----+-------------------------------------------------------+-------+
|  1 | 2014_10_12_000000_create_users_table                  |     1 |
|  2 | 2014_10_12_100000_create_password_resets_table        |     1 |
|  3 | 2019_08_19_000000_create_failed_jobs_table            |     1 |
|  4 | 2019_12_14_000001_create_personal_access_tokens_table |     1 |
|  5 | 2023_01_12_185409_create_tests_table                  |     1 |
|  6 | 2023_01_12_194511_add_phone_to_tests_table            |     2 |
+----+-------------------------------------------------------+-------+
6 rows in set (0.00 sec)

As long as the migration does not exist inside the table, it’s run.

I love migrations and could not live with out them anymore. In the next article(s) we’ll take a look at the Model and then fakers, seeders, and factories.

Laravel Series

Continue your Laravel Learning.

Laravel — P27: Database Intro

Laravel Database Mastery: Starting Your Data Journey

Laravel – P27: Database Intro

Kickstart your Laravel project with a database intro. Unlock the secrets to managing data effectively for powerful, dynamic applications.

Laravel — P28: Migrations

Evolving Your Laravel Projects: Mastering Migrations

Laravel – P28: Migrations

Ensure your Laravel database evolves with your app. Learn migrations for seamless updates and rollback capabilities. Start now!

Laravel — P29: Models Intro

Laravel Models Unveiled: Your Gateway to Data Interaction

Laravel – P29: Models

Dive into Laravel models to interact with database effortlessly. Discover how to utilize models for efficient data handling & retrieval.

Leave a Reply