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
12 changes: 5 additions & 7 deletions config/config.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
92 changes: 0 additions & 92 deletions src/Command/AbstractInstallCommand.php

This file was deleted.

19 changes: 12 additions & 7 deletions src/Command/InstallCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
19 changes: 12 additions & 7 deletions src/Command/UpdateCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
4 changes: 2 additions & 2 deletions src/Model/ImportedConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
}

Expand Down
83 changes: 83 additions & 0 deletions src/Service/InstallationRunner.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<?php

declare(strict_types=1);

namespace Shlinkio\Shlink\Installer\Service;

use Shlinkio\Shlink\Installer\Command\Model\InitOption;
use Shlinkio\Shlink\Installer\Config\ConfigGeneratorInterface;
use Shlinkio\Shlink\Installer\Model\ImportedConfig;
use Shlinkio\Shlink\Installer\Util\ConfigWriterInterface;
use Shlinkio\Shlink\Installer\Util\Utils;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Style\SymfonyStyle;

readonly class InstallationRunner implements InstallationRunnerInterface
{
public function __construct(
private ConfigWriterInterface $configWriter,
private ShlinkAssetsHandlerInterface $assetsHandler,
private ConfigGeneratorInterface $configGenerator,
) {
}

/** @inheritDoc */
public function runInstallation(SymfonyStyle $io, Command|null $initCommand): int
{
$initCommandInput = [InitOption::INITIAL_API_KEY->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([
'<info>Welcome to Shlink!!</info>',
'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('<info>Custom configuration properly generated!</info>');
$io->newLine();

$initCommandResult = $initCommand?->run(new ArrayInput($initCommandInput), $io);
if ($initCommandResult !== Command::SUCCESS) {
return Command::FAILURE;
}

$io->success('Installation complete!');
return Command::SUCCESS;
}
}
21 changes: 21 additions & 0 deletions src/Service/InstallationRunnerInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace Shlinkio\Shlink\Installer\Service;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Style\SymfonyStyle;

interface InstallationRunnerInterface
{
/**
* @return Command::SUCCESS|Command::FAILURE
*/
public function runInstallation(SymfonyStyle $io, Command|null $initCommand): int;

/**
* @return Command::SUCCESS|Command::FAILURE
*/
public function runUpdate(SymfonyStyle $io, Command|null $initCommand): int;
}
57 changes: 11 additions & 46 deletions test/Command/InstallCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,71 +4,36 @@

namespace ShlinkioTest\Shlink\Installer\Command;

use PHPUnit\Framework\Assert;
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\InstallCommand;
use Shlinkio\Shlink\Installer\Config\ConfigGeneratorInterface;
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 InstallCommandTest 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);

$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());
}
}
Loading