PluginTestCase cannot boot a plugin that registers a console command
Nobody has claimed this yet.
- 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
- In any plugin, register a command the documented way:
public function register(): void
{
$this->registerConsoleCommand('myplugin.mycommand', \My\Plugin\Console\MyCommand::class);
}
- Add any test extending
System\Tests\Bootstrap\PluginTestCase. php artisan winter:test -p My.Plugin— every test errors insetUp().
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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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