diff --git a/CHANGELOG.md b/CHANGELOG.md index b3cc413..e55a996 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Changed + +- `laranail::captcha.install` now extends laranail/package-tools' `InstallCommand` and takes + laranail/console's display API and run lifecycle from its `InteractsWithConsoleServices` and + `InteractsWithConsoleWriter` traits instead of its `Command` base. Name, option, description, + listing visibility, output and exit codes are unchanged and pinned by a new contract test. The + command is bound in the container because the new base takes the `Package` in its constructor. + ## [0.1.0] - 2026-08-15 ### Added diff --git a/src/Commands/InstallCommand.php b/src/Commands/InstallCommand.php index ff3597d..50783e9 100644 --- a/src/Commands/InstallCommand.php +++ b/src/Commands/InstallCommand.php @@ -4,9 +4,11 @@ namespace Simtabi\Laranail\Captcha\Commands; +use Simtabi\Laranail\Package\Tools\Package; use Simtabi\Laranail\Captcha\Enums\Provider; -use Simtabi\Laranail\Console\Tools\Commands\Command; -use Simtabi\Laranail\Console\Tools\Commands\Concerns\SupportsNamespacedNames; +use Simtabi\Laranail\Console\Tools\Commands\Concerns\InteractsWithConsoleWriter; +use Simtabi\Laranail\Console\Tools\Commands\Concerns\InteractsWithConsoleServices; +use Simtabi\Laranail\Package\Tools\Commands\InstallCommand as PackageToolsInstallCommand; /** * Publishes what an application needs, and nothing it does not. @@ -15,16 +17,37 @@ * self-hosted math provider needs no keys, no account and no config file — so `install` exists for * applications that want to change something, not as a step between installing and being * protected. + * + * The base is package-tools' install command, which carries the `::` name support. laranail/console's + * display API (`$this->services`) and managed run lifecycle come from its two traits rather than its + * base class, so neither package has to depend on the other. `handle()` is this command's own: the + * base's generic publish pipeline would print different steps, and the output here is the contract. */ -final class InstallCommand extends Command +final class InstallCommand extends PackageToolsInstallCommand { - use SupportsNamespacedNames; + use InteractsWithConsoleServices; + use InteractsWithConsoleWriter; + + public const string SIGNATURE = 'laranail::captcha.install {--migrations : Also publish the optional settings-table migration}'; - protected $name = 'laranail::captcha.install'; + public const string DESCRIPTION = 'Publish the captcha config file, and optionally the settings migration.'; - protected $signature = 'laranail::captcha.install {--migrations : Also publish the optional settings-table migration}'; + public function __construct(Package $package) + { + // Listed in `php artisan list`, as it always has been: the base hides install commands by + // default, so visibility is passed explicitly rather than inherited. + parent::__construct($package, self::SIGNATURE, hidden: false); - protected $description = 'Publish the captcha config file, and optionally the settings migration.'; + // The base writes `Install {package}` as the description during construction; restore the + // one this command has always shown. Both the property and Symfony's copy are set, because + // the parent constructor has already pushed the property through setDescription(). + $this->description = self::DESCRIPTION; + $this->setDescription(self::DESCRIPTION); + + // Booted eagerly, as console's own base does, so `$this->services` exists straight after + // construction rather than only once run() has been entered. + $this->bootConsoleSupport(); + } public function handle(): int { diff --git a/src/Providers/CaptchaServiceProvider.php b/src/Providers/CaptchaServiceProvider.php index 28701bc..630fde6 100644 --- a/src/Providers/CaptchaServiceProvider.php +++ b/src/Providers/CaptchaServiceProvider.php @@ -86,6 +86,11 @@ public function configurePackage(Package $package): void #[Override] public function packageRegistered(): void { + // The install command's package-tools base takes the Package in its constructor, which the + // container cannot autowire. Binding it here keeps the command registered by class name, + // like the other three, so it is still only constructed when Artisan resolves it. + $this->app->bind(InstallCommand::class, fn (): InstallCommand => new InstallCommand($this->package)); + $config = $this->app->make(Repository::class); // Resolved once, here, rather than on every lookup. It is also the value the production diff --git a/tests/Feature/InstallCommandContractTest.php b/tests/Feature/InstallCommandContractTest.php new file mode 100644 index 0000000..827d8e5 --- /dev/null +++ b/tests/Feature/InstallCommandContractTest.php @@ -0,0 +1,81 @@ +toBeInstanceOf(InstallCommand::class); + + /** @var Command $command */ + return $command; +} + +it('keeps its name, aliases, description and listing visibility', function (): void { + $command = captchaInstallCommand(); + + expect($command->getName())->toBe('laranail::captcha.install') + ->and($command->getAliases())->toBe([]) + ->and($command->getDescription())->toBe('Publish the captcha config file, and optionally the settings migration.') + ->and($command->isHidden())->toBeFalse(); +}); + +it('keeps exactly one option of its own, and no arguments', function (): void { + $definition = captchaInstallCommand()->getNativeDefinition(); + + expect(array_keys($definition->getOptions()))->toBe(['migrations']) + ->and($definition->getArguments())->toBe([]); + + $option = $definition->getOption('migrations'); + + expect($option->acceptValue())->toBeFalse() + ->and($option->getDescription())->toBe('Also publish the optional settings-table migration'); +}); + +it('publishes the config and names the active provider', function (): void { + $this->artisan('laranail::captcha.install') + ->expectsOutputToContain('Published config/laranail/captcha.php') + ->expectsOutputToContain('Active provider: math.') + ->doesntExpectOutputToContain('captcha_settings migration') + ->assertExitCode(0); +}); + +it('publishes the settings migration only when asked', function (): void { + $this->artisan('laranail::captcha.install', ['--migrations' => true]) + ->expectsOutputToContain('Published config/laranail/captcha.php') + ->expectsOutputToContain('Published the captcha_settings migration.') + ->assertExitCode(0); +}); + +it('reports an unknown provider rather than failing', function (): void { + config()->set('laranail.captcha.provider', 'not-a-provider'); + + $this->artisan('laranail::captcha.install') + ->expectsOutputToContain('Active provider: unknown.') + ->assertExitCode(0); +}); + +it('extends the package-tools install base and keeps both console traits', function (): void { + expect(is_subclass_of(InstallCommand::class, PackageToolsInstallCommand::class))->toBeTrue(); + + $traits = class_uses(InstallCommand::class); + + expect($traits)->toHaveKey(InteractsWithConsoleServices::class) + ->and($traits)->toHaveKey(InteractsWithConsoleWriter::class); +});