Search code examples
phplaravellaravel-artisanlaravel-dusk

Ignore a custom Laravel Artisan command on production


I've written a custom Artisan command (let's call it MyDusk.php) that expands/abstracts some of the functionality of the core Dusk command.

This custom command extends Laravel\Dusk\Console\DuskCommand from within the Dusk package.

The problem is, on production the Dusk package is not installed (it's under require-dev in composer.json)

So when composer is generating its autoload files on production, it errors out when it gets to MyDusk.php because it can't find Laravel\Dusk\Console\DuskCommand.

PHP Fatal error:  Class 'Laravel\Dusk\Console\DuskCommand' not found in app/Console/Commands/Dusk.php on line 10

In Dusk.php line 10:

  Class 'Laravel\Dusk\Console\DuskCommand' not found  

I tried moving the Dusk package to require so it would be available on production (not ideal, I know), but there's a line in the core Dusk service provider that throws an exception when it's run on production preventing this:

# From: vendor/laravel/dusk/src/DuskServiceProvider.php
if ($this->app->environment('production')) {
    throw new Exception('It is unsafe to run Dusk in production.');
}

I'm trying to think of the most elegant solution to allow for my custom Dusk command to be part of the application and accessible locally, without throwing errors on production.

One idea: Write my Dusk command as its own package, that's also only in require-dev.

Any other ideas?


Solution

  • I just took a look at the API, you could do this:

    You could move your command to App\Console\Commmands\Local\DuskCommand.php.

    By default, if you check the commands() method in the Kernel, it's only going to load commands found in App\Console\Commands. This will not include the sub-directories.

    /**
     * Register the commands for the application.
     *
     * @return void
     */
    protected function commands()
    {
        $this->load(__DIR__.'/Commands');
    
        require base_path('routes/console.php');
    }
    

    This is the default commands() method. You could switch this implementation to the one below:

    /**
     * Register the commands for the application.
     *
     * @return void
     */
    protected function commands()
    {
        $paths = [
            __DIR__ . '/Commands'
        ]; 
    
        if(app()->environment('local')) {
            $paths[] = __DIR__ . '/Commands/Local';
        }
    
        $this->load($paths);
    
        require base_path('routes/console.php');
    }
    

    So, in local, we are also going to load commands based in App\Console\Commands\Local.

    I gave it a try and it seems to be working just fine. I thought, I'd try to explain it a bit more. Basically, after doing a composer dump-autoload, Laravel is listening to this event and doing two things:

    "post-autoload-dump": [
        "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
        "@php artisan package:discover --ansi"
    ]
    

    The second one is trying to run the Auto-Package Discovery command and this is where it will fail. The Artisan executable actually boots the application with the Console Kernel.

    $kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
    
    $status = $kernel->handle(
        $input = new Symfony\Component\Console\Input\ArgvInput,
        new Symfony\Component\Console\Output\ConsoleOutput
    );
    

    While resolving the Kernel, it will also try to boot up the Commands that it needs so that they are available to Artisan, and this is where it fails.

    However, like I mentioned above, if you only boot the Commands you need in production, this issue won't happen.