From 4bbb25ba2013e09382487ad4854f517298ccfdbc Mon Sep 17 00:00:00 2001 From: gwleuverink Date: Tue, 18 Aug 2026 15:55:54 +0200 Subject: [PATCH 1/3] Register the native database commands under their own names Laravel 13.24 changed FreshCommand and WipeCommand to declare $signature where they used $name. Illuminate\Console\Command::__construct() gives $signature precedence, and the child inherits the parent's, so overriding $name no longer renames anything. Both commands ended up registered under Laravel's names, which breaks artisan list and points migrate:fresh at the NativePHP database. Declaring the signature keeps the native name and carries the parent's options across, which parent::handle() still reads. Co-Authored-By: Claude Opus 5 (1M context) --- src/Commands/FreshCommand.php | 15 ++++++++- src/Commands/WipeDatabaseCommand.php | 9 +++++- tests/Commands/CommandNameTest.php | 47 ++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 tests/Commands/CommandNameTest.php diff --git a/src/Commands/FreshCommand.php b/src/Commands/FreshCommand.php index 7d17b23b..6d2c296b 100644 --- a/src/Commands/FreshCommand.php +++ b/src/Commands/FreshCommand.php @@ -12,7 +12,20 @@ )] class FreshCommand extends BaseFreshCommand { - protected $name = 'native:migrate:fresh'; + // The parent declares its own signature, which wins over $name. + // Inheriting it would register this as a second migrate:fresh, + // so the name and options are spelled out here. + protected $signature = 'native:migrate:fresh + {--database= : The database connection to use} + {--drop-views : Drop all tables and views} + {--drop-types : Drop all tables and types (Postgres only)} + {--force : Force the operation to run when in production} + {--path=* : The path(s) to the migrations files to be executed} + {--realpath : Indicate any provided migration file paths are pre-resolved absolute paths} + {--schema-path= : The path to a schema dump file} + {--seed : Indicates if the seed task should be re-run} + {--seeder= : The class name of the root seeder} + {--step : Force the migrations to be run so they can be rolled back individually}'; protected $description = 'Drop all tables and re-run all migrations in the NativePHP development environment'; diff --git a/src/Commands/WipeDatabaseCommand.php b/src/Commands/WipeDatabaseCommand.php index 1c71884c..4b8d9981 100644 --- a/src/Commands/WipeDatabaseCommand.php +++ b/src/Commands/WipeDatabaseCommand.php @@ -7,7 +7,14 @@ class WipeDatabaseCommand extends BaseWipeCommand { - protected $name = 'native:db:wipe'; + // The parent declares its own signature, which wins over $name. + // Inheriting it would register this as a second db:wipe, + // so the name and options are spelled out here. + protected $signature = 'native:db:wipe + {--database= : The database connection to use} + {--drop-views : Drop all tables and views} + {--drop-types : Drop all tables and types (Postgres only)} + {--force : Force the operation to run when in production}'; protected $description = 'Wipe the database in the NativePHP development environment'; diff --git a/tests/Commands/CommandNameTest.php b/tests/Commands/CommandNameTest.php new file mode 100644 index 00000000..deb19cc1 --- /dev/null +++ b/tests/Commands/CommandNameTest.php @@ -0,0 +1,47 @@ +commands = app(Kernel::class)->all(); +}); + +function optionNames(Command $command): array +{ + return array_keys($command->getDefinition()->getOptions()); +} + +/* +|-------------------------------------------------------------------------- +| Tests +|-------------------------------------------------------------------------- +*/ +it('registers the native commands under their own names', function () { + expect($this->commands)->toHaveKeys(['native:migrate:fresh', 'native:db:wipe']) + ->and($this->commands['native:migrate:fresh'])->toBeInstanceOf(FreshCommand::class) + ->and($this->commands['native:db:wipe'])->toBeInstanceOf(WipeDatabaseCommand::class); +}); + +it('leaves the commands they extend registered under the Laravel names', function () { + expect($this->commands['migrate:fresh'])->toBeInstanceOf(BaseFreshCommand::class) + ->not->toBeInstanceOf(FreshCommand::class) + ->and($this->commands['db:wipe'])->toBeInstanceOf(BaseWipeCommand::class) + ->not->toBeInstanceOf(WipeDatabaseCommand::class); +}); + +it('carries over every option the parent commands define', function () { + expect(optionNames($this->commands['native:migrate:fresh'])) + ->toContain(...optionNames(new BaseFreshCommand(app('migrator')))) + ->and(optionNames($this->commands['native:db:wipe'])) + ->toContain(...optionNames(new BaseWipeCommand)); +}); From 43d2cd512ceafdc3b696c6c54e2e71ab991553c6 Mon Sep 17 00:00:00 2001 From: gwleuverink Date: Tue, 18 Aug 2026 16:16:44 +0200 Subject: [PATCH 2/3] Give the native database commands one convention for names and descriptions --- src/Commands/FreshCommand.php | 5 +--- src/Commands/MigrateCommand.php | 7 ++--- src/Commands/SeedDatabaseCommand.php | 7 ++--- src/Commands/WipeDatabaseCommand.php | 2 ++ tests/Commands/CommandNameTest.php | 42 ++++++++++++++++++++++++---- 5 files changed, 46 insertions(+), 17 deletions(-) diff --git a/src/Commands/FreshCommand.php b/src/Commands/FreshCommand.php index 6d2c296b..12d7f999 100644 --- a/src/Commands/FreshCommand.php +++ b/src/Commands/FreshCommand.php @@ -6,10 +6,7 @@ use Native\Desktop\NativeServiceProvider; use Symfony\Component\Console\Attribute\AsCommand; -#[AsCommand( - name: 'native:migrate:fresh', - description: 'Drop all tables and re-run all migrations in the NativePHP development environment', -)] +#[AsCommand(name: 'native:migrate:fresh')] class FreshCommand extends BaseFreshCommand { // The parent declares its own signature, which wins over $name. diff --git a/src/Commands/MigrateCommand.php b/src/Commands/MigrateCommand.php index 191f56cd..3d9ccc82 100644 --- a/src/Commands/MigrateCommand.php +++ b/src/Commands/MigrateCommand.php @@ -8,12 +8,11 @@ use Native\Desktop\NativeServiceProvider; use Symfony\Component\Console\Attribute\AsCommand; -#[AsCommand( - name: 'native:migrate', - description: 'Run the database migrations in the NativePHP development environment', -)] +#[AsCommand(name: 'native:migrate')] class MigrateCommand extends BaseMigrateCommand { + protected $description = 'Run the database migrations in the NativePHP development environment'; + public function __construct(Migrator $migrator, Dispatcher $dispatcher) { $this->signature = 'native:'.$this->signature; diff --git a/src/Commands/SeedDatabaseCommand.php b/src/Commands/SeedDatabaseCommand.php index f7432a85..f67db3ef 100644 --- a/src/Commands/SeedDatabaseCommand.php +++ b/src/Commands/SeedDatabaseCommand.php @@ -8,14 +8,13 @@ use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputOption; -#[AsCommand( - name: 'native:seed', - description: 'Seed the database in the NativePHP development environment', -)] +#[AsCommand(name: 'native:seed')] class SeedDatabaseCommand extends BaseSeedCommand { protected $signature = 'native:seed'; + protected $description = 'Seed the database in the NativePHP development environment'; + protected function configure(): void { parent::configure(); diff --git a/src/Commands/WipeDatabaseCommand.php b/src/Commands/WipeDatabaseCommand.php index 4b8d9981..8c637666 100644 --- a/src/Commands/WipeDatabaseCommand.php +++ b/src/Commands/WipeDatabaseCommand.php @@ -4,7 +4,9 @@ use Illuminate\Database\Console\WipeCommand as BaseWipeCommand; use Native\Desktop\NativeServiceProvider; +use Symfony\Component\Console\Attribute\AsCommand; +#[AsCommand(name: 'native:db:wipe')] class WipeDatabaseCommand extends BaseWipeCommand { // The parent declares its own signature, which wins over $name. diff --git a/tests/Commands/CommandNameTest.php b/tests/Commands/CommandNameTest.php index deb19cc1..5cd9bd0b 100644 --- a/tests/Commands/CommandNameTest.php +++ b/tests/Commands/CommandNameTest.php @@ -2,9 +2,14 @@ use Illuminate\Contracts\Console\Kernel; use Illuminate\Database\Console\Migrations\FreshCommand as BaseFreshCommand; +use Illuminate\Database\Console\Migrations\MigrateCommand as BaseMigrateCommand; +use Illuminate\Database\Console\Seeds\SeedCommand as BaseSeedCommand; use Illuminate\Database\Console\WipeCommand as BaseWipeCommand; use Native\Desktop\Commands\FreshCommand; +use Native\Desktop\Commands\MigrateCommand; +use Native\Desktop\Commands\SeedDatabaseCommand; use Native\Desktop\Commands\WipeDatabaseCommand; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; /* @@ -12,10 +17,24 @@ | Setup |-------------------------------------------------------------------------- */ +$nativeCommands = [ + 'native:migrate' => [MigrateCommand::class, BaseMigrateCommand::class], + 'native:migrate:fresh' => [FreshCommand::class, BaseFreshCommand::class], + 'native:seed' => [SeedDatabaseCommand::class, BaseSeedCommand::class], + 'native:db:wipe' => [WipeDatabaseCommand::class, BaseWipeCommand::class], +]; + beforeEach(function () { $this->commands = app(Kernel::class)->all(); }); +function attributeName(string $class): ?string +{ + $attribute = (new ReflectionClass($class))->getAttributes(AsCommand::class); + + return empty($attribute) ? null : $attribute[0]->newInstance()->name; +} + function optionNames(Command $command): array { return array_keys($command->getDefinition()->getOptions()); @@ -26,11 +45,10 @@ function optionNames(Command $command): array | Tests |-------------------------------------------------------------------------- */ -it('registers the native commands under their own names', function () { - expect($this->commands)->toHaveKeys(['native:migrate:fresh', 'native:db:wipe']) - ->and($this->commands['native:migrate:fresh'])->toBeInstanceOf(FreshCommand::class) - ->and($this->commands['native:db:wipe'])->toBeInstanceOf(WipeDatabaseCommand::class); -}); +it('registers the native commands under their own names', function (string $name, string $class) { + expect($this->commands)->toHaveKey($name) + ->and($this->commands[$name])->toBeInstanceOf($class); +})->with(array_map(fn ($name, $classes) => [$name, $classes[0]], array_keys($nativeCommands), $nativeCommands)); it('leaves the commands they extend registered under the Laravel names', function () { expect($this->commands['migrate:fresh'])->toBeInstanceOf(BaseFreshCommand::class) @@ -39,6 +57,20 @@ function optionNames(Command $command): array ->not->toBeInstanceOf(WipeDatabaseCommand::class); }); +// Commands are registered lazily under the name in their attribute, but named +// from their signature once built. Any disagreement between the two +// makes Artisan fail with "registered under multiple names". +it('names each native command the same way in its attribute and its signature', function (string $name, string $class) { + expect(attributeName($class))->toBe($name) + ->and($this->commands[$name]->getName())->toBe($name); +})->with(array_map(fn ($name, $classes) => [$name, $classes[0]], array_keys($nativeCommands), $nativeCommands)); + +it('describes the native commands rather than inheriting the Laravel wording', function (string $name, string $parent) { + expect($this->commands[$name]->getDescription()) + ->not->toBe((new ReflectionClass($parent))->getDefaultProperties()['description']) + ->toContain('NativePHP development environment'); +})->with(array_map(fn ($name, $classes) => [$name, $classes[1]], array_keys($nativeCommands), $nativeCommands)); + it('carries over every option the parent commands define', function () { expect(optionNames($this->commands['native:migrate:fresh'])) ->toContain(...optionNames(new BaseFreshCommand(app('migrator')))) From a8f7d4116b8fe097c7d00538eefbbd6274d1dbb9 Mon Sep 17 00:00:00 2001 From: gwleuverink Date: Tue, 18 Aug 2026 16:36:04 +0200 Subject: [PATCH 3/3] Rename the command test to match what it now covers --- .../Commands/{CommandNameTest.php => CommandRegistrationTest.php} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/Commands/{CommandNameTest.php => CommandRegistrationTest.php} (100%) diff --git a/tests/Commands/CommandNameTest.php b/tests/Commands/CommandRegistrationTest.php similarity index 100% rename from tests/Commands/CommandNameTest.php rename to tests/Commands/CommandRegistrationTest.php