From 3aab899eaab50256fb2889fef750f243f8868a52 Mon Sep 17 00:00:00 2001 From: Arif Hoque Date: Fri, 28 Aug 2026 21:59:17 +0600 Subject: [PATCH] Prompt for launcher selection in vendor:publish --- .../Console/Commands/VendorPublishCommand.php | 55 +++++++- src/Phaseolies/Console/Schedule/Command.php | 10 ++ tests/Console/CommandBehaviorCoverageTest.php | 121 ++++++++++++++++++ .../Support/CommandTestEnvironment.php | 22 ++++ 4 files changed, 206 insertions(+), 2 deletions(-) diff --git a/src/Phaseolies/Console/Commands/VendorPublishCommand.php b/src/Phaseolies/Console/Commands/VendorPublishCommand.php index 9fcbf5dd..6a140b18 100644 --- a/src/Phaseolies/Console/Commands/VendorPublishCommand.php +++ b/src/Phaseolies/Console/Commands/VendorPublishCommand.php @@ -42,7 +42,7 @@ public function handle(): int return $this->executeWithTiming(function () { $launcher = $this->option('launcher'); $tag = $this->option('tag'); - $force = $this->option('force'); + $force = (bool) $this->option('force'); if ($launcher) { $this->publishLauncher($launcher, $force); @@ -54,10 +54,61 @@ public function handle(): int return Command::SUCCESS; } + return $this->publishInteractively($force); + }); + } + + /** + * Prompt the user to choose which registered launcher to publish, + * falling back to publishing everything in non-interactive environments. + * + * @param bool $force + * @return int + */ + protected function publishInteractively(bool $force): int + { + $launchers = $this->publishableLaunchers(); + + if (empty($launchers)) { + $this->displayWarning('No publishable launchers found.'); + return Command::SUCCESS; + } + + if (!$this->isInteractive()) { $this->publishAll($force); + return Command::SUCCESS; + } + + $choices = array_merge(['All'], $launchers); + + $choice = $this->choice('Which launcher would you like to publish?', $choices, 'All'); + if ($choice === 'All') { + $this->publishAll($force); return Command::SUCCESS; - }); + } + + $this->publishLauncher($choice, $force); + + return Command::SUCCESS; + } + + /** + * Get the class names of registered launchers that have publishable paths. + * + * @return array + */ + protected function publishableLaunchers(): array + { + $launchers = []; + + foreach ($this->app->getLaunchers() as $launcherInstance) { + if (!empty($launcherInstance->pathsToPublish())) { + $launchers[] = get_class($launcherInstance); + } + } + + return $launchers; } protected function publishLauncher(string $launcher, bool $force = false) diff --git a/src/Phaseolies/Console/Schedule/Command.php b/src/Phaseolies/Console/Schedule/Command.php index 1713c4be..41e31bb3 100644 --- a/src/Phaseolies/Console/Schedule/Command.php +++ b/src/Phaseolies/Console/Schedule/Command.php @@ -482,6 +482,16 @@ protected function secret(string $question): string return $helper->ask($this->input, $this->output, $question); } + /** + * Determine if the command is running in an interactive environment. + * + * @return bool + */ + protected function isInteractive(): bool + { + return $this->input->isInteractive(); + } + /** * Give the user a single choice from an array of answers. * diff --git a/tests/Console/CommandBehaviorCoverageTest.php b/tests/Console/CommandBehaviorCoverageTest.php index d0d9fcfd..3eae79af 100644 --- a/tests/Console/CommandBehaviorCoverageTest.php +++ b/tests/Console/CommandBehaviorCoverageTest.php @@ -32,6 +32,7 @@ use Phaseolies\Console\Commands\StorageUnlinkCommand; use Phaseolies\Console\Commands\Tests\UnitTestCommand; use Phaseolies\Console\Commands\VendorPublishCommand; +use Phaseolies\Launchers\ServiceLauncher; use Phaseolies\Console\Commands\ViewCacheCommand; use Phaseolies\Console\Commands\ViewClearCommand; use Phaseolies\Database\Migration\MigrationCreator; @@ -678,6 +679,106 @@ public function testVendorPublishCommandCopiesFilesAndSkipsExistingTargets(): vo $this->assertContains('Skipping: File already exists at ' . $targetFile, $command->capturedWarnings); } + public function testVendorPublishCommandWarnsWhenNoLaunchersArePublishable(): void + { + $appStub = $this->createStub(Application::class); + $appStub->method('getLaunchers')->willReturn([]); + Env::$appInstance = $appStub; + + $command = new class extends VendorPublishCommand + { + use InteractsWithFakeCommandIO; + }; + + $result = $command->handle(); + + $this->assertSame(0, $result); + $this->assertContains('No publishable launchers found.', $command->capturedWarnings); + $this->assertEmpty($command->capturedChoiceQuestions); + } + + public function testVendorPublishCommandFallsBackToPublishingAllWhenNonInteractive(): void + { + $appStub = $this->createStub(Application::class); + $launcher = $this->makeFakePublishableLauncher($appStub, 'alpha'); + $appStub->method('getLaunchers')->willReturn([$launcher]); + Env::$appInstance = $appStub; + + $command = new class extends VendorPublishCommand + { + use InteractsWithFakeCommandIO; + }; + $command->fakeInteractive = false; + + $result = $command->handle(); + + $this->assertSame(0, $result); + $this->assertEmpty($command->capturedChoiceQuestions); + $this->assertContains('Published assets from 1 launchers', $command->capturedSuccesses); + } + + public function testVendorPublishCommandPublishesAllWhenChoiceIsAll(): void + { + $appStub = $this->createStub(Application::class); + $launcherA = $this->makeFakePublishableLauncher($appStub, 'alpha', FakePublishableLauncherAlpha::class); + $launcherB = $this->makeFakePublishableLauncher($appStub, 'beta', FakePublishableLauncherBeta::class); + $appStub->method('getLaunchers')->willReturn([$launcherA, $launcherB]); + Env::$appInstance = $appStub; + + $command = new class extends VendorPublishCommand + { + use InteractsWithFakeCommandIO; + }; + $command->fakeChoiceAnswer = 'All'; + + $result = $command->handle(); + + $this->assertSame(0, $result); + $this->assertCount(1, $command->capturedChoiceQuestions); + $this->assertSame('All', $command->capturedChoiceQuestions[0]['default']); + $this->assertContains(get_class($launcherA), $command->capturedChoiceQuestions[0]['choices']); + $this->assertContains(get_class($launcherB), $command->capturedChoiceQuestions[0]['choices']); + $this->assertContains('Published assets from 2 launchers', $command->capturedSuccesses); + } + + public function testVendorPublishCommandPublishesOnlyTheChosenLauncher(): void + { + $appStub = $this->createStub(Application::class); + $launcherA = $this->makeFakePublishableLauncher($appStub, 'alpha', FakePublishableLauncherAlpha::class); + $launcherB = $this->makeFakePublishableLauncher($appStub, 'beta', FakePublishableLauncherBeta::class); + $appStub->method('getLaunchers')->willReturn([$launcherA, $launcherB]); + $appStub->method('getLauncher')->willReturnCallback( + fn(string $class) => $class === get_class($launcherA) ? $launcherA : null + ); + Env::$appInstance = $appStub; + + $command = new class extends VendorPublishCommand + { + use InteractsWithFakeCommandIO; + }; + $command->fakeChoiceAnswer = get_class($launcherA); + + $result = $command->handle(); + + $this->assertSame(0, $result); + $this->assertContains('Published assets for launcher: ' . get_class($launcherA), $command->capturedSuccesses); + $this->assertNotContains('Published assets for launcher: ' . get_class($launcherB), $command->capturedSuccesses); + } + + private function makeFakePublishableLauncher(object $app, string $label, string $className = FakePublishableLauncherAlpha::class): ServiceLauncher + { + $from = Env::path("vendor/{$label}/config.php"); + $to = Env::path("published/{$label}/config.php"); + + @mkdir(dirname($from), 0755, true); + file_put_contents($from, 'publishes([$from => $to]); + + return $launcher; + } + private function invokeMethod(object $instance, string $method, array $arguments = []): mixed { $reflection = new \ReflectionMethod($instance, $method); @@ -685,3 +786,23 @@ private function invokeMethod(object $instance, string $method, array $arguments return $reflection->invokeArgs($instance, $arguments); } } + +/** + * Distinct launcher fixtures for VendorPublishCommand tests. + * + * These must be separately-named (not anonymous) classes: two anonymous + * class expressions with identical bodies at the same source location + * resolve to the same PHP class, which would make it impossible to tell + * two fake launchers apart by class name in the tests above. + */ +class FakePublishableLauncherAlpha extends ServiceLauncher +{ + public function register() {} + public function launch() {} +} + +class FakePublishableLauncherBeta extends ServiceLauncher +{ + public function register() {} + public function launch() {} +} diff --git a/tests/Console/Support/CommandTestEnvironment.php b/tests/Console/Support/CommandTestEnvironment.php index 9bb51938..b049ac64 100644 --- a/tests/Console/Support/CommandTestEnvironment.php +++ b/tests/Console/Support/CommandTestEnvironment.php @@ -217,6 +217,28 @@ protected function displayInfo(string $message): void $this->capturedInfos[] = $message; } + public bool $fakeInteractive = true; + + public mixed $fakeChoiceAnswer = null; + + public array $capturedChoiceQuestions = []; + + protected function isInteractive(): bool + { + return $this->fakeInteractive; + } + + protected function choice(string $question, array $choices, $default = null): mixed + { + $this->capturedChoiceQuestions[] = [ + 'question' => $question, + 'choices' => $choices, + 'default' => $default, + ]; + + return $this->fakeChoiceAnswer ?? $default; + } + protected function executeWithTiming(callable $callback): int { $result = $callback();