Extending Laravel’s migration command to add new options

Written by ankitpokhrel | Published 2017/10/22
Tech Story Tags: laravel | laravel-5 | multitenancy | postgres | php

TLDRvia the TL;DR App

This is the continuation of my previous article where we talked about creating a multi-tenant app using Laravel and Postgres. I recommend you to read that article before moving forward with this one.

Since our multi-tenant app is ready, we want to utilize Laravel’s console commands to handle our migrations. In this article, we will see how we can extend Laravel’s migration implementation to gracefully handle migrations for all schemas of our multi-tenant app.

Overview

Laravel provides easy console commands within migrate namespace to handle migrations. eg:

$ php artisan migrate$ php artisan migrate:rollback...

But, as you might have noticed, these commands will only run migrations in default schema, which in our case needs to be run for all schemas. We will extend commands provided by Laravel to add 2 new options:

  • --all
  • --schema=[SCHEMA]

So, we will be able to:

# Run migrations in all available schemas.$ php artisan migrate --all

# Run migrations in given schemas.$ php artisan migrate --schema=th,vn,ph

# Rollback migrations in all available schemas.$ php artisan migrate:rollback --all

# Rollback migrations in given schemas.$ php artisan migrate:rollback --schema=th,vn,ph

I am using Laravel 5.5 and PostgreSQL 9.5 for my setup.

Laravel Console Commands

Console commands implementation in Laravel is basically an implementation of a command design pattern. Every command has its own handle method which is fired whenever a command is dispatched.

The command pattern is a behavioral design pattern in which an object is used to represent and encapsulate all the information needed to call a method at a later time. This information includes the method name, the object that owns the method and values for the method parameters. — Wikipedia

Database commands in Laravel can be found in Illuminate\Database\Console\Migrations namespace. We just need to override them with our own implementations.

Adding New Options

Create a folder called Migration inside app/Console. We will place all of our migration commands inside this folder. Create a file called MigrateCommand.php.

This class extends Illuminate\Database\Console\Migrations\MigrateCommand which is basically the class that implements migrate command (php artisan migrate). Notice that in the constructor we are extending its signature by appending our options --all and --schema. Then we call the parent constructor with required parameter.

We now need to register it as Laravel’s default migrate command. For this, create a service provider called VentureMigrationServiceProvider inside app/Http/Providers directory and register it inside config/app.php.

Note that this service provider extends Laravel’s migration service provider Illuminate\Database\MigrationServiceProvider instead of the default Illuminate\Support\ServiceProvider.

registerMigrateCommand extends the singleton object available in alias command.migrate and returns our own MigrateCommand. command.migrate alias points to Illuminate\Database\Console\Migrations\MigrateCommand object. So overriding command.migrate will override Laravel’s default MigrateCommand as well.

Run php artisan migrate --help. You can see the two new options we added earlier.

The handle Method

The handle method inside MigrateCommand gets executed whenever we run php artisan migrate. Let’s make it respond to our newly added options.

The handle method is straight forward. If --all option is passed, we run migrations for all ventures available. If --schema option is passed, we run it for given schemas otherwise we will just call default parent handler. The runFor method exists in MigrationTrait which I will explain shortly.

The Migration Trait

Since we will use some common methods quite often when we override commands like migrate:rollback and migrate:status, we will extract some of the common methods inside a trait. Let’s name it MigrationTrait.

Note that you can create an AbstractClass for this as well. How you want to implement this is up to you. I will go with trait in this tutorial.

connectUsingSchema method switches schema based on the parameter passed. First line inside this method overrides global config and the next line will purge current database connection. Disconnecting the database is necessary as Laravel caches the connection and use it all over again. So changing the global config only will not work in this case. Reconnection is made by Laravel itself so we don’t need to reconnect the database in our method. During reconnection Laravel will use schema based on the config which is currently the schema we passed.

getValidSchemas, as the name suggests, will return all valid schemas from the argument passed. Remember we will pass schema from command line in --schema=th,vn,sg format. We first need to explode the arguments with comma and return valid schemas from the list of our ventures.

runFor method loops through all ventures that we pass in the argument and runs the command (migrate, rollback, etc.). First line of the method saves default schema which is used to reset the schema at the end of the method. We then loop through the ventures, switch schema, and run parent handler for current schema.

Testing Migrate Command

At this stage, our MigrateCommand is ready to handle migrations for all ventures. Let’s run some migrations using --all option.

Note that running php artisan migrate will only run the migrations that are in your default schema which can be defined in .env as mentioned in the previous article.

Now, try running migration for specific schemas with php artisan migrate --schema=th,sg,vn. This will run migrations for Thailand, Singapore and Vietnam ventures.

The main advantage of extending migration this way is that we can still chain all other options provided by commands in Laravel migrate namespace. For instance, you can run php artisan migrate --all --seed. Cool, huh!

Rollback Command

Now let’s extend migrate:rollback command. Create a file called RollbackCommand.php inside app/Console. This class will extend Illuminate\Database\Console\Migrations\RollbackCommand, which is Laravel’s implementation of migrate:rollback command.

handle method is same as we discussed earlier.

Unlike MigrateCommand class, we need to override getOptions method in RollbackCommand class to add new options to the list.

You might be thinking why we need to override getOptions in RollbackCommand and not in MigrateCommand. Well, it is implemented that way. Looks like Taylor Otwell changed it in recent version for some reason. Check out this commit. If you any idea on why he did so, please do answer in comments ;)

At this point our rollback command is almost ready. All we need to do now is to tell Laravel to use this class instead of the default one. We can do this in our VentureMigrationServiceProvider that we created earlier.

registerMigrateRollbackCommand extends the singleton object available in alias command.migrate.rollback and returns our own RollbackCommand.

Testing Rollback Command

We can now rollback all with php artisan migrate:rollback --all.

To rollback specific schemas use php artisan migrate:rollback --schema=th,sg,vn.

Status Command

Status command shows the status of your migrations. This command is usually helpful when you want to check which migrations already ran and which one is not executed yet. We can extend StatusCommand in same way as we extended RollbackCommand.

Again, in VentureMigrationServiceProvider, register App\Console\Migration\StatusCommand as Laravel’s default status command.

registerMigrateStatusCommand extends the singleton object available in alias command.migrate.status and returns our own StatusCommand.

Testing Status Command

To view the status of all of our schemas, run php artisan migrate:status --all.

To view the status of specific schemas, run php artisan migrate:status --schema=th,my,vn.

Final Words

In similar way, we can override remaining commands: migrate:fresh, migrate:reset, migrate:refresh and migrate:install. One of the main advantage of extending default migration instead of creating a new one is that we still get the benefit of chaining other useful options like --force, --seed,--database, etc.

Next step: Can you make --path option work to define different migration path for ventures? Let me know your implementation in comments :)

All of the codes used in current and previous tutorial can be found in this Github repo.

That’s all! Happy Coding!


Published by HackerNoon on 2017/10/22