wintercms / wintercms/winter

PluginTestCase cannot boot a plugin that registers a console command

Open Beginner friendly
#1,539 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

needs review
Dominant language
PHP
Stars
1.5k
Forks
246
Avg merge
19h 2m
Merged PRs (30d)
7

Description

Winter CMS Build

dev-develop

PHP Version

8.4

Database engine

SQLite

Plugins installed

Any plugin that calls registerConsoleCommand() and has a test suite

Issue description

A plugin that registers a console command the documented way cannot be tested with PluginTestCase. Every test in the suite errors during setUp():

Illuminate\Contracts\Container\BindingResolutionException:
Target class [command.myplugin.mycommand] does not exist.

  .../Illuminate/Console/Application.php:280
  .../Illuminate/Support/ServiceProvider.php:404
  .../Illuminate/Foundation/Console/Kernel.php:417
  modules/system/tests/bootstrap/PluginTestCase.php:87   <- Artisan::call('winter:up')

Cause. PluginBase::registerConsoleCommand() binds the command to an alias on the plugin's own application instance, and defers resolution to ServiceProvider::commands():

https://github.com/wintercms/winter/blob/develop/modules/system/classes/PluginBase.php

public function registerConsoleCommand($key, $command)
{
    $key = 'command.'.$key;
    $this->app->singleton($key, $command);   // bound to *this* application
    $this->commands($key);                   // resolved later, by alias
}

ServiceProvider::commands() registers an Artisan::starting() callback, and Illuminate\Console\Application::$bootstrappers is static:

// Illuminate/Support/ServiceProvider.php
public function commands($commands)
{
    Artisan::starting(function ($artisan) use ($commands) {
        $artisan->resolveCommands($commands);
    });
}

// Illuminate/Console/Application.php
protected static $bootstrappers = [];                       // static
public static function starting(Closure $callback) { static::$bootstrappers[] = $callback; }

PluginTestCase builds a fresh application per test and forgets the manager singletons, but nothing clears that static:

https://github.com/wintercms/winter/blob/develop/modules/system/tests/bootstrap/PluginTestCase.php

PluginManager::forgetInstance();
UpdateManager::forgetInstance();

So the callback registered against the previous application survives, and when Artisan::call('winter:up') boots the console on the new application it tries to resolve an alias that was never bound there. The container falls back to treating the alias as a class name, hence Target class [command.…] does not exist.

Nothing is wrong with the plugin — registerConsoleCommand() in register() is exactly what the core plugins do (e.g. Winter.Blocks, Winter.Battlesnake). It only shows up when a plugin has both a console command and a test suite.

Steps to replicate
  1. In any plugin, register a command the documented way:
public function register(): void
{
    $this->registerConsoleCommand('myplugin.mycommand', \My\Plugin\Console\MyCommand::class);
}
  1. Add any test extending System\Tests\Bootstrap\PluginTestCase.
  2. php artisan winter:test -p My.Plugin — every test errors in setUp().
Suggested fix

Clear the static alongside the other resets in PluginTestCase::setUp():

PluginManager::forgetInstance();
UpdateManager::forgetInstance();
Illuminate\Console\Application::forgetBootstrappers();

forgetBootstrappers() is already public in Illuminate. I've confirmed locally that this one line fixes it — with it in place, an unmodified registerConsoleCommand() registration works and the whole suite passes.

The alternative would be for registerConsoleCommand() to pass the class to commands() rather than the alias (keeping the singleton binding for BC), so resolution never depends on which application the alias was bound to. That also works, but it changes a public API's behaviour, whereas the test-case reset is contained and matches what's already done for the two managers.

Happy to open a PR for whichever you'd prefer.

Workaround

Calling it in the plugin's own test before parent::setUp():

public function setUp(): void
{
    \Illuminate\Console\Application::forgetBootstrappers();
    parent::setUp();
}

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in modules/system/tests/bootstrap/PluginTestCase.php, especially setUp() and the existing manager resets. Review how Artisan::call('winter:up') boots the test application, then run php artisan winter:test -p My.Plugin or the affected plugin suite. Done means console-command plugins boot without the command.myplugin.mycommand resolution error and the suite passes.

Written by the indexing model from the issue text.

Assessment

Tech stack
laravel, php
Domain
testing
Issue type
Bug
Difficulty
1/5
Estimated time
Under an hour
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
90/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.