Published December 15, 2016
I'm currently developing the Quantic Telecom website with Laravel. My app
folder is well-organized as my views
and my resources
but not my migrations because the Laravel php artisan migrate
command doesn't run nested migrations. My package thibaud-dauce/laravel-recursive-migrations
will allow us to put migrations into sub-directories.
In this blog post, I will go through the development of this package. For information, I found two links speaking about this particular problem: on StackOverflow "Laravel running migrations on “app/database/migrations” folder recursively" and this issue on Github "[Proposal/Enhancement] Artisan CLI Migrate command does not go through all sub folders of the migrations folder."
The goal of my package is not to create new commands like submigrate
, submigrate:rollback
… I want to keep the original names migrate
, migrate:rollback
and add an option --recursive
which will look into all sub-directories.
When thinking about this problem, several solutions came into my mind:
- Extending
Illuminate\Database\Migrations\Migrator
- Extending all commands (migrate, refresh, reset…)
Extending Illuminate\Database\Migrations\Migrator
The easiest approach is to extend the Illuminate\Database\Migrations\Migrator
and override the getMigrationFiles()
method. This method currently use the glob()
method of the filesystem to get all files in the migration directory following the "_.php" pattern. Using the allFiles()
recursive method instead should do the trick.
The main benefit of this approach is to act on all commands (migrate, rollback, reset…) with only one change because the Migrator
is used by all of them. But the drawback is that the Migrator
is a low level class, so it can't access the command's flags. So I will be forced to apply migrations recursively all the time.
Extending all commands
Extending the commands' classes will allow me to override some of their methods and modify their behavior. It seems to be a good option for me. The only disadvantage is the fact that I need to create a new class for each of the migrations' commands: MigrateCommand
, RefreshCommand
, ResetCommand
, RollbackCommand
and StatusCommand
. But I can manage the duplicate code with some traits.
So, I will split the work into two traits. The first one will be in charged of fetching subdirectories and will be general (pure and not linked to Laravel or the migration system). The second one, the ugly one, will need to be add to a child of the Illuminate\Database\Console\Migrations\BaseCommand
and will override the getMigrationPaths()
, getOptions()
and call()
methods. I think it's often a bad choice to force a trait into an inheritance tree, but I couldn't find another solution…
My first trait: Subdirectories
This trait will be pure. It will fetch the sub-directories for a path. I could have done a service class injected by the container but I thought it will be over complicated for some business logic that will never change (sub-directories will always be sub-directories). A service class would have given more flexibility to the end user by allowing him to change or extend my implementation.
The first method of this trait will fetch all sub-directories in a path with the help of the Finder
component:
/**
* Fetch all subdirectories from a path.
* The original path is included in the array.
*
* @param string $path
* @return string[]
*/
The second method will flatMap the result of the first method for multiple base paths. flatMap is a function which map all element in an array to an array (you get an array of array) and then merge all arrays into one.
/**
* Fetch all subdirectories from an array of base paths.
* The original paths are included in the array.
*
* @param string[] $paths
* @return string[]
*/
My second trait: RecursiveMigrationCommand
The goal of this trait is to provide new methods for the children of Illuminate\Database\Console\Migrations\BaseCommand
. It will use the previously seen Subdirectories
trait.
First, it will add the --recursive
option. Nothing special here to the getOptions()
method.
/**
* Get the console command options.
*
* @return array
*/
Then, it will add the sub-directories to the migration paths if the flag is set. The allSubdirectories()
method comes from the Subdirectories
trait.
/**
* Get all of the migration paths.
*
* @return array
*/
And finally, the last one, call()
, is only required for the RefreshCommand
. RefreshCommand
does not contain any logic. It's only a wrapper around the ResetCommand
and the MigrateCommand
. Internally, it calls the others commands with the call()
method, passing its arguments like "step", "database" or "path"… But in this implementation, it will not pass the "recursive" flag to the other commands. I need to set it if the command called is a migrate command:
/**
* Call another console command.
*
* @param string $command
* @param array $arguments
* @return int
*/
New Commands
I also need to create my own commands for the MigrateCommand
, RefreshCommand
, ResetCommand
, RollbackCommand
and StatusCommand
. All the implementations are the same thanks to the RecursiveMigrationCommand
trait. For example, my MigrateCommand
:
Laravel integration
Last but not least, I need to create a service provider to bind the new commands in the container. The names of the commands in the container are simple, and I only need to extend the binding.
$this->app;
$this->app;
$this->app;
$this->app;
$this->app;
I didn't found a way to override the first binding with bind()
because the MigrationServiceProvider
is deferred by the ConsoleSupportServiceProvider
so it will register his binding after mines. If I also defer my service provider, I get a "Provider already exists!" exception.
So, I need to use the extend()
method which is not really the correct one I think. The extend method will first resolve the previous binding and give it to my closure. In my service provider, I don't care of the previous instance of the command since I create a new one (a new child) from scratch.
Conclusion
The project is missing PHPUnit tests even if I already try the commands by hand in a fresh Laravel project. But it's available now on Packagist and on Git. Please open an issue if you find anything wrong in it.
I discover a lot about the migration system in Laravel during this little project, and I encourage you to dig in the source code of the framework: you'll learn a lot!