Search code examples
phplaravellaravel-artisan

Laravel 10 - Extend existing artisan command?


Let's say, for example, I want to add a couple of options to php artisan migrate:

php artisan migrate --core
php artisan migrate --project=0f9ebA2

Is it possible to extend an existing artisan command? I don't want to change any default behavior, just add options. It would be sufficient to have a wrapper class that pre-sets certain options, like changing a config variable or adding a path argument, then passes through everything else to the artisan function as normal.

Stubbed out, php artisan migrate --core would look something like:

public function handle() {
    $this->call('migrate', ['--path' => '/database/migrations/core', ...$this->arguments()]);
}

Stubbed out, php artisan migrate --project=0f9ebA2 would look something like:

public function handle() {
    config(['tenant.key' => $this->argument('--project')]);
    $this->call('migrate', ['--path' => '/database/migrations/project', ...$this->arguments()]);
}

Laravel's documentation is focused solely on authoring custom commands and occasionally invoking other commands with $this->call() - it doesn't seem to cover extending existing functionality, or passing arguments through.


Already Tried / Doesn't Work:

  • Creating a separate custom command like php artisan migrate:core. This will work exactly for the functionality I wish to add, and nothing else - it won't preserve any of migrate's other options, which are also needed. The goal is to set context for migrate's core functionality.
  • Passing arguments through a custom command manually, like the examples above. I can get existing arguments with $this->arguments() and reattach them to the inner artisan call, but each argument also needs to be in the definition of the custom command - trying to pass an argument that isn't in the commands defined arguments causes the command to be rejected. There doesn't seem to be a way to just wildcard passthrough. Sure I can copy-paste migrate's existing argument definitions, but it won't be flexible with any future core changes or 3rd party packages.
  • Creating a new custom command that extends migrate directly. Trying a proof-of-concept alias of the base migrate:
namespace App\Console\Commands;

use Illuminate\Database\Console\Migrations\MigrateCommand;

class MigrateCore extends MigrateCommand {
    protected $signature = 'migrate:core';

    public function handle() {
        parent::handle();
    }
}

Results in a hairy BindingResolutionException from higher up Laravel's core hierarchy:

Target [Illuminate\Database\Migrations\MigrationRepositoryInterface] is not instantiable while building [App\Console\Commands\MigrateCore, Illuminate\Database\Migrations\Migrator]

which, to me, feels like embarking on a path not intended.


Solution

  • Try adding this as app/Console/Commands/MigrateCore.php:

    <?php
    
    namespace App\Console\Commands;
    
    use Illuminate\Database\Console\Migrations\MigrateCommand;
    
    class MigrateCore extends MigrateCommand {
        public function __construct()
        {
            $migrator = app("migrator");
            $dispatcher = app("events");
            $this->signature .= "{--core : Run core migrations}";
            parent::__construct($migrator, $dispatcher);
        }
    
        public function handle(): void
        {
            if ($this->option("core")) {
                $this->input->setOption("path", "database/migrations/core");
            }
            parent::handle();
        }
    }
    

    The error you were getting:

    "Target [Illuminate\Database\Migrations\MigrationRepositoryInterface] is not instantiable while building [App\Console\Commands\MigrateCore, Illuminate\Database\Migrations\Migrator]"

    is fairly cryptic, but if you look into the comments in Illuminate\Container\Container where the error is thrown, it starts to make a bit of sense:

    // If the type is not instantiable, the developer is attempting to resolve
    // an abstract type such as an Interface or Abstract Class and there is
    // no binding registered for the abstractions so we need to bail out.
    

    The constructor for Illuminate\Database\Console\Migrations\MigrateCommand wants to be injected with an instance of Illuminate\Database\Migrations\Migrator which in turn is looking for a Illuminate\Database\Migrations\MigrationRepositoryInterface. But no concrete classes have been bound to that interface yet.

    So, instead of just inheriting the constructor from Illuminate\Database\Console\Migrations\MigrateCommand we initialize those bindings with the app() helper, and then pass them to the constructor.

    Got a bit of help from this Laracasts post.