Laravel 10 - Extend existing artisan command?

110 Views Asked by At

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.

1

There are 1 best solutions below

1
miken32 On BEST ANSWER

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.