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:rollback… I want to keep the original names
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 all commands (migrate, refresh, reset…)
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:
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
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:
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
/** * 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:
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
First, it will add the
--recursive option. Nothing special here to the
/** * Get the console command options. * * @return array */
/** * Get all of the migration paths. * * @return array */
And finally, the last one,
call(), is only required for the
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 */
I also need to create my own commands for the
StatusCommand. All the implementations are the same thanks to the
RecursiveMigrationCommand trait. For example, my
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.
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!