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.
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.
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.
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 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.
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.
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!
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
.
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 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
.
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
.
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!