diff --git a/config/config.php b/config/config.php
index 0ca3f26..1a048c9 100644
--- a/config/config.php
+++ b/config/config.php
@@ -23,6 +23,7 @@
Console\Helper\ProcessHelper::class => Factory\ProcessHelperFactory::class,
Service\InstallationCommandsRunner::class => ConfigAbstractFactory::class,
+ Service\InstallationRunner::class => ConfigAbstractFactory::class,
Service\ShlinkAssetsHandler::class => ConfigAbstractFactory::class,
Config\ConfigGenerator::class => ConfigAbstractFactory::class,
Config\ConfigOptionsManager::class => Config\ConfigOptionsManagerFactory::class,
@@ -209,17 +210,14 @@
PhpExecutableFinder::class,
'config.installer.installation_commands',
],
-
- Command\InstallCommand::class => [
- ConfigWriter::class,
- Service\ShlinkAssetsHandler::class,
- Config\ConfigGenerator::class,
- ],
- Command\UpdateCommand::class => [
+ Service\InstallationRunner::class => [
ConfigWriter::class,
Service\ShlinkAssetsHandler::class,
Config\ConfigGenerator::class,
],
+
+ Command\InstallCommand::class => [Service\InstallationRunner::class],
+ Command\UpdateCommand::class => [Service\InstallationRunner::class],
Command\SetOptionCommand::class => [
ConfigWriter::class,
Service\ShlinkAssetsHandler::class,
diff --git a/src/Command/AbstractInstallCommand.php b/src/Command/AbstractInstallCommand.php
deleted file mode 100644
index 6d3c753..0000000
--- a/src/Command/AbstractInstallCommand.php
+++ /dev/null
@@ -1,92 +0,0 @@
-text([
- 'Welcome to Shlink!!',
- 'This tool will guide you through the installation process.',
- ]);
-
- // Check if a cached config file exists and drop it if so
- $this->assetsHandler->dropCachedConfigIfAny($io);
-
- $importedConfig = $this->resolvePreviousConfig($io);
- if ($this->isUpdate()) {
- $this->assetsHandler->importShlinkAssetsFromPath($io, $importedConfig->importPath);
- }
- $config = $this->configGenerator->generateConfigInteractively($io, $importedConfig->importedConfig);
- $normalizedConfig = Utils::normalizeAndKeepEnvVarKeys($config);
-
- // Generate config params files
- $this->configWriter->toFile(ShlinkAssetsHandler::GENERATED_CONFIG_PATH, $normalizedConfig);
- $io->text('Custom configuration properly generated!');
- $io->newLine();
-
- if (! $this->execInitCommand($io, $importedConfig)) {
- return -1;
- }
-
- $io->success('Installation complete!');
- return 0;
- }
-
- private function resolvePreviousConfig(SymfonyStyle $io): ImportedConfig
- {
- if ($this->isUpdate()) {
- return $this->assetsHandler->resolvePreviousConfig($io);
- }
-
- return ImportedConfig::notImported();
- }
-
- private function execInitCommand(SymfonyStyle $io, ImportedConfig $importedConfig): bool
- {
- $isUpdate = $this->isUpdate();
- $input = [
- InitOption::SKIP_INITIALIZE_DB->asCliFlag() => $isUpdate,
- InitOption::CLEAR_DB_CACHE->asCliFlag() => $isUpdate,
- InitOption::DOWNLOAD_RR_BINARY->asCliFlag() =>
- $isUpdate && $this->assetsHandler->roadRunnerBinaryExistsInPath($importedConfig->importPath),
- ];
-
- if (! $isUpdate) {
- $input[InitOption::INITIAL_API_KEY->asCliFlag()] = null;
- }
-
- $command = $this->getApplication()?->find(InitCommand::NAME);
- $exitCode = $command?->run(new ArrayInput($input), $io);
-
- return $exitCode === 0;
- }
-
- abstract protected function isUpdate(): bool;
-}
diff --git a/src/Command/InstallCommand.php b/src/Command/InstallCommand.php
index 0abc537..6d1edac 100644
--- a/src/Command/InstallCommand.php
+++ b/src/Command/InstallCommand.php
@@ -4,19 +4,24 @@
namespace Shlinkio\Shlink\Installer\Command;
-class InstallCommand extends AbstractInstallCommand
+use Shlinkio\Shlink\Installer\Service\InstallationRunnerInterface;
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Style\SymfonyStyle;
+
+#[AsCommand(InstallCommand::NAME, 'Guides you through the installation process, to get Shlink up and running')]
+class InstallCommand extends Command
{
public const string NAME = 'install';
- protected function configure(): void
+ public function __construct(private readonly InstallationRunnerInterface $installationRunner)
{
- $this
- ->setName(self::NAME)
- ->setDescription('Guides you through the installation process, to get Shlink up and running.');
+ parent::__construct();
}
- protected function isUpdate(): bool
+ public function __invoke(SymfonyStyle $io): int
{
- return false;
+ $initCommand = $this->getApplication()?->find(InitCommand::NAME);
+ return $this->installationRunner->runInstallation($io, $initCommand);
}
}
diff --git a/src/Command/UpdateCommand.php b/src/Command/UpdateCommand.php
index 5aedb54..72cf882 100644
--- a/src/Command/UpdateCommand.php
+++ b/src/Command/UpdateCommand.php
@@ -4,19 +4,24 @@
namespace Shlinkio\Shlink\Installer\Command;
-class UpdateCommand extends AbstractInstallCommand
+use Shlinkio\Shlink\Installer\Service\InstallationRunnerInterface;
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Style\SymfonyStyle;
+
+#[AsCommand(UpdateCommand::NAME, 'Helps you import Shlink\'s config from an older version to a new one')]
+class UpdateCommand extends Command
{
public const string NAME = 'update';
- protected function configure(): void
+ public function __construct(private readonly InstallationRunnerInterface $installationRunner)
{
- $this
- ->setName(self::NAME)
- ->setDescription('Helps you import Shlink\'s config from an older version to a new one.');
+ parent::__construct();
}
- protected function isUpdate(): bool
+ public function __invoke(SymfonyStyle $io): int
{
- return true;
+ $initCommand = $this->getApplication()?->find(InitCommand::NAME);
+ return $this->installationRunner->runUpdate($io, $initCommand);
}
}
diff --git a/src/Model/ImportedConfig.php b/src/Model/ImportedConfig.php
index 5f33e1c..ebb1685 100644
--- a/src/Model/ImportedConfig.php
+++ b/src/Model/ImportedConfig.php
@@ -4,9 +4,9 @@
namespace Shlinkio\Shlink\Installer\Model;
-final class ImportedConfig
+final readonly class ImportedConfig
{
- private function __construct(public readonly string $importPath, public readonly array $importedConfig)
+ private function __construct(public string $importPath, public array $importedConfig)
{
}
diff --git a/src/Service/InstallationRunner.php b/src/Service/InstallationRunner.php
new file mode 100644
index 0000000..db28b92
--- /dev/null
+++ b/src/Service/InstallationRunner.php
@@ -0,0 +1,83 @@
+asCliFlag() => null];
+ return $this->run($io, $initCommand, $initCommandInput, ImportedConfig::notImported());
+ }
+
+ /** @inheritDoc */
+ public function runUpdate(SymfonyStyle $io, Command|null $initCommand): int
+ {
+ $importConfig = $this->assetsHandler->resolvePreviousConfig($io);
+
+ // Check if a cached config file exists and drop it if so
+ $this->assetsHandler->dropCachedConfigIfAny($io);
+ $this->assetsHandler->importShlinkAssetsFromPath($io, $importConfig->importPath);
+
+ $initCommandInput = [
+ InitOption::SKIP_INITIALIZE_DB->asCliFlag() => null,
+ InitOption::CLEAR_DB_CACHE->asCliFlag() => null,
+ ];
+
+ if ($this->assetsHandler->roadRunnerBinaryExistsInPath($importConfig->importPath)) {
+ $initCommandInput[InitOption::DOWNLOAD_RR_BINARY->asCliFlag()] = null;
+ }
+
+ return $this->run($io, $initCommand, $initCommandInput, $importConfig);
+ }
+
+ /**
+ * @return Command::SUCCESS|Command::FAILURE
+ */
+ private function run(
+ SymfonyStyle $io,
+ Command|null $initCommand,
+ array $initCommandInput,
+ ImportedConfig $importedConfig,
+ ): int {
+ $io->text([
+ 'Welcome to Shlink!!',
+ 'This tool will guide you through the installation process.',
+ ]);
+
+ $config = $this->configGenerator->generateConfigInteractively($io, $importedConfig->importedConfig);
+ $normalizedConfig = Utils::normalizeAndKeepEnvVarKeys($config);
+
+ // Generate config params files
+ $this->configWriter->toFile(ShlinkAssetsHandler::GENERATED_CONFIG_PATH, $normalizedConfig);
+ $io->text('Custom configuration properly generated!');
+ $io->newLine();
+
+ $initCommandResult = $initCommand?->run(new ArrayInput($initCommandInput), $io);
+ if ($initCommandResult !== Command::SUCCESS) {
+ return Command::FAILURE;
+ }
+
+ $io->success('Installation complete!');
+ return Command::SUCCESS;
+ }
+}
diff --git a/src/Service/InstallationRunnerInterface.php b/src/Service/InstallationRunnerInterface.php
new file mode 100644
index 0000000..12ba159
--- /dev/null
+++ b/src/Service/InstallationRunnerInterface.php
@@ -0,0 +1,21 @@
+assetsHandler = $this->createMock(ShlinkAssetsHandlerInterface::class);
- $this->assetsHandler->expects($this->once())->method('dropCachedConfigIfAny');
-
- $this->configWriter = $this->createMock(ConfigWriterInterface::class);
-
- $configGenerator = $this->createMock(ConfigGeneratorInterface::class);
- $configGenerator->method('generateConfigInteractively')->willReturn([]);
-
- $app = new Application();
- $command = new InstallCommand(
- $this->configWriter,
- $this->assetsHandler,
- $configGenerator,
- );
- $app->addCommand($command);
-
- $this->initCommand = $this->createMock(Command::class);
- $this->initCommand->method('getName')->willReturn(InitCommand::NAME);
- $this->initCommand->method('isEnabled')->willReturn(true);
- $app->addCommand($this->initCommand);
+ $this->installationRunner = $this->createMock(InstallationRunnerInterface::class);
+ $command = new InstallCommand($this->installationRunner);
$this->commandTester = new CommandTester($command);
}
#[Test]
- public function commandIsExecutedAsExpected(): void
+ #[TestWith([Command::SUCCESS])]
+ #[TestWith([Command::FAILURE])]
+ public function commandIsExecutedAsExpected(int $statusCode): void
{
- $this->initCommand->expects($this->once())->method('run')->with(
- $this->callback(function (ArrayInput $input) {
- Assert::assertEquals(
- '--skip-initialize-db --clear-db-cache --download-rr-binary --initial-api-key',
- $input->__toString(),
- );
- return true;
- }),
- $this->anything(),
- )->willReturn(0);
- $this->assetsHandler->expects($this->never())->method('resolvePreviousConfig');
- $this->assetsHandler->expects($this->never())->method('importShlinkAssetsFromPath');
- $this->configWriter->expects($this->once())->method('toFile')->with($this->anything(), $this->isArray());
-
- $this->commandTester->setInputs(['no']);
+ $this->installationRunner->expects($this->once())->method('runInstallation')->willReturn($statusCode);
$this->commandTester->execute([]);
+
+ self::assertEquals($statusCode, $this->commandTester->getStatusCode());
}
}
diff --git a/test/Command/UpdateCommandTest.php b/test/Command/UpdateCommandTest.php
index cc10921..00ce845 100644
--- a/test/Command/UpdateCommandTest.php
+++ b/test/Command/UpdateCommandTest.php
@@ -4,84 +4,36 @@
namespace ShlinkioTest\Shlink\Installer\Command;
-use PHPUnit\Framework\Assert;
-use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
+use PHPUnit\Framework\Attributes\TestWith;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
-use Shlinkio\Shlink\Installer\Command\InitCommand;
use Shlinkio\Shlink\Installer\Command\UpdateCommand;
-use Shlinkio\Shlink\Installer\Config\ConfigGeneratorInterface;
-use Shlinkio\Shlink\Installer\Model\ImportedConfig;
-use Shlinkio\Shlink\Installer\Service\ShlinkAssetsHandlerInterface;
-use Shlinkio\Shlink\Installer\Util\ConfigWriterInterface;
-use Symfony\Component\Console\Application;
+use Shlinkio\Shlink\Installer\Service\InstallationRunnerInterface;
use Symfony\Component\Console\Command\Command;
-use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Tester\CommandTester;
class UpdateCommandTest extends TestCase
{
private CommandTester $commandTester;
- private MockObject & ConfigWriterInterface $configWriter;
- private MockObject & ShlinkAssetsHandlerInterface $assetsHandler;
- private MockObject & Command $initCommand;
+ private MockObject & InstallationRunnerInterface $installationRunner;
public function setUp(): void
{
- $this->assetsHandler = $this->createMock(ShlinkAssetsHandlerInterface::class);
- $this->assetsHandler->expects($this->once())->method('dropCachedConfigIfAny');
-
- $this->configWriter = $this->createMock(ConfigWriterInterface::class);
-
- $generator = $this->createMock(ConfigGeneratorInterface::class);
- $generator->method('generateConfigInteractively')->willReturn([]);
-
- $app = new Application();
- $command = new UpdateCommand($this->configWriter, $this->assetsHandler, $generator);
- $app->addCommand($command);
-
- $this->initCommand = $this->createMock(Command::class);
- $this->initCommand->method('getName')->willReturn(InitCommand::NAME);
- $this->initCommand->method('isEnabled')->willReturn(true);
- $app->addCommand($this->initCommand);
+ $this->installationRunner = $this->createMock(InstallationRunnerInterface::class);
+ $command = new UpdateCommand($this->installationRunner);
$this->commandTester = new CommandTester($command);
}
- #[Test, DataProvider('provideCommands')]
- public function commandIsExecutedAsExpected(bool $rrBinExists, string $postUpdateCommands): void
+ #[Test]
+ #[TestWith([Command::SUCCESS])]
+ #[TestWith([Command::FAILURE])]
+ public function commandIsExecutedAsExpected(int $statusCode): void
{
- $this->initCommand->expects($this->once())->method('run')->with(
- $this->callback(function (ArrayInput $input) use ($postUpdateCommands) {
- Assert::assertEquals(
- $postUpdateCommands,
- $input->__toString(),
- );
- return true;
- }),
- $this->anything(),
- )->willReturn(0);
- $this->assetsHandler->expects($this->once())->method('roadRunnerBinaryExistsInPath')->willReturn($rrBinExists);
- $this->assetsHandler->expects($this->once())->method('resolvePreviousConfig')->willReturn(
- ImportedConfig::notImported(),
- );
- $this->assetsHandler->expects($this->once())->method('importShlinkAssetsFromPath');
- $this->configWriter->expects($this->once())->method('toFile')->with($this->anything(), $this->isArray());
-
- $this->commandTester->setInputs(['no']);
+ $this->installationRunner->expects($this->once())->method('runUpdate')->willReturn($statusCode);
$this->commandTester->execute([]);
- }
- public static function provideCommands(): iterable
- {
- yield 'no rr binary' => [
- false,
- '--skip-initialize-db=1 --clear-db-cache=1 --download-rr-binary',
- ];
- yield 'rr binary' => [
- true,
- '--skip-initialize-db=1 --clear-db-cache=1 --download-rr-binary=1',
- ];
+ self::assertEquals($statusCode, $this->commandTester->getStatusCode());
}
}
diff --git a/test/Service/InstallationRunnerTest.php b/test/Service/InstallationRunnerTest.php
new file mode 100644
index 0000000..d7c2654
--- /dev/null
+++ b/test/Service/InstallationRunnerTest.php
@@ -0,0 +1,95 @@
+initCommand = $this->createMock(Command::class);
+ $this->assetsHandler = $this->createMock(ShlinkAssetsHandlerInterface::class);
+ $this->configWriter = $this->createMock(ConfigWriterInterface::class);
+
+ $configGenerator = $this->createStub(ConfigGeneratorInterface::class);
+ $configGenerator->method('generateConfigInteractively')->willReturn([]);
+
+ $this->installationRunner = new InstallationRunner($this->configWriter, $this->assetsHandler, $configGenerator);
+ }
+
+ #[Test]
+ public function installationIsExecutedAsExpected(): void
+ {
+ $this->initCommand->expects($this->once())->method('run')->with(
+ $this->callback(function (ArrayInput $input) {
+ Assert::assertEquals('--initial-api-key', $input->__toString());
+ return true;
+ }),
+ $this->anything(),
+ )->willReturn(Command::SUCCESS);
+
+ $this->assetsHandler->expects($this->never())->method('dropCachedConfigIfAny');
+ $this->assetsHandler->expects($this->never())->method('resolvePreviousConfig');
+ $this->assetsHandler->expects($this->never())->method('roadRunnerBinaryExistsInPath');
+ $this->assetsHandler->expects($this->never())->method('importShlinkAssetsFromPath');
+
+ $this->configWriter->expects($this->once())->method('toFile')->with($this->anything(), $this->isArray());
+
+ $this->installationRunner->runInstallation($this->createStub(SymfonyStyle::class), $this->initCommand);
+ }
+
+ #[Test, DataProvider('provideCommands')]
+ public function updateIsExecutedAsExpected(bool $rrBinExists, string $postUpdateCommands): void
+ {
+ $this->initCommand->expects($this->once())->method('run')->with(
+ $this->callback(function (ArrayInput $input) use ($postUpdateCommands) {
+ Assert::assertEquals($postUpdateCommands, $input->__toString());
+ return true;
+ }),
+ $this->anything(),
+ )->willReturn(0);
+
+ $this->assetsHandler->expects($this->once())->method('dropCachedConfigIfAny');
+ $this->assetsHandler->expects($this->once())->method('resolvePreviousConfig')->willReturn(
+ ImportedConfig::notImported(),
+ );
+ $this->assetsHandler->expects($this->once())->method('roadRunnerBinaryExistsInPath')->willReturn($rrBinExists);
+ $this->assetsHandler->expects($this->once())->method('importShlinkAssetsFromPath');
+
+ $this->configWriter->expects($this->once())->method('toFile')->with($this->anything(), $this->isArray());
+
+ $this->installationRunner->runUpdate($this->createStub(SymfonyStyle::class), $this->initCommand);
+ }
+
+ public static function provideCommands(): iterable
+ {
+ yield 'update with no rr binary' => [
+ false,
+ '--skip-initialize-db --clear-db-cache',
+ ];
+ yield 'update with rr binary' => [
+ true,
+ '--skip-initialize-db --clear-db-cache --download-rr-binary',
+ ];
+ }
+}