Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 53 additions & 2 deletions src/Phaseolies/Console/Commands/VendorPublishCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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<int, string>
*/
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)
Expand Down
10 changes: 10 additions & 0 deletions src/Phaseolies/Console/Schedule/Command.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
121 changes: 121 additions & 0 deletions tests/Console/CommandBehaviorCoverageTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -678,10 +679,130 @@ 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, '<?php return [];');

$launcher = new $className($app);
$launcher->publishes([$from => $to]);

return $launcher;
}

private function invokeMethod(object $instance, string $method, array $arguments = []): mixed
{
$reflection = new \ReflectionMethod($instance, $method);

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() {}
}
22 changes: 22 additions & 0 deletions tests/Console/Support/CommandTestEnvironment.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Loading