diff --git a/bin/cli b/bin/cli
index 66086b23..263df59e 100755
--- a/bin/cli
+++ b/bin/cli
@@ -5,7 +5,4 @@ use Symfony\Component\Console\Application as CliApp;
/** @var ContainerInterface $container */
$container = include __DIR__ . '/../config/container.php';
-
-/** @var CliApp $app */
-$app = $container->get(CliApp::class);
-$app->run();
+$container->get(CliApp::class)->run();
diff --git a/bin/install b/bin/install
index b4147e10..43a07cd3 100755
--- a/bin/install
+++ b/bin/install
@@ -2,6 +2,9 @@
[
Application::class => InstallApplicationFactory::class,
+ Filesystem::class => InvokableFactory::class,
+ QuestionHelper::class => InvokableFactory::class,
]]);
$container->build(Application::class)->run();
diff --git a/bin/update b/bin/update
index 13c2a684..164e20b0 100755
--- a/bin/update
+++ b/bin/update
@@ -2,6 +2,9 @@
[
Application::class => InstallApplicationFactory::class,
+ Filesystem::class => InvokableFactory::class,
+ QuestionHelper::class => InvokableFactory::class,
]]);
$container->build(Application::class, ['isUpdate' => true])->run();
diff --git a/module/CLI/src/Command/Install/InstallCommand.php b/module/CLI/src/Command/Install/InstallCommand.php
index e30785af..641fb552 100644
--- a/module/CLI/src/Command/Install/InstallCommand.php
+++ b/module/CLI/src/Command/Install/InstallCommand.php
@@ -1,9 +1,10 @@
'pdo_mysql',
- 'PostgreSQL' => 'pdo_pgsql',
- 'SQLite' => 'pdo_sqlite',
- ];
- const SUPPORTED_LANGUAGES = ['en', 'es'];
const GENERATED_CONFIG_PATH = 'config/params/generated_config.php';
/**
@@ -46,22 +38,22 @@ class InstallCommand extends Command
* @var ProcessHelper
*/
private $processHelper;
- /**
- * @var string
- */
- private $importedInstallationPath;
/**
* @var WriterInterface
*/
private $configWriter;
- /**
- * @var bool
- */
- private $isUpdate;
/**
* @var Filesystem
*/
private $filesystem;
+ /**
+ * @var ConfigCustomizerPluginManagerInterface
+ */
+ private $configCustomizers;
+ /**
+ * @var bool
+ */
+ private $isUpdate;
/**
* InstallCommand constructor.
@@ -70,18 +62,24 @@ class InstallCommand extends Command
* @param bool $isUpdate
* @throws LogicException
*/
- public function __construct(WriterInterface $configWriter, Filesystem $filesystem, $isUpdate = false)
- {
+ public function __construct(
+ WriterInterface $configWriter,
+ Filesystem $filesystem,
+ ConfigCustomizerPluginManagerInterface $configCustomizers,
+ $isUpdate = false
+ ) {
parent::__construct();
$this->configWriter = $configWriter;
$this->isUpdate = $isUpdate;
$this->filesystem = $filesystem;
+ $this->configCustomizers = $configCustomizers;
}
public function configure()
{
- $this->setName('shlink:install')
- ->setDescription('Installs Shlink');
+ $this
+ ->setName('shlink:install')
+ ->setDescription('Installs or updates Shlink');
}
public function execute(InputInterface $input, OutputInterface $output)
@@ -118,10 +116,16 @@ class InstallCommand extends Command
$config = $this->isUpdate ? $this->importConfig() : new CustomizableAppConfig();
// Ask for custom config params
- $this->askDatabase($config);
- $this->askUrlShortener($config);
- $this->askLanguage($config);
- $this->askApplication($config);
+ foreach ([
+ Plugin\DatabaseConfigCustomizerPlugin::class,
+ Plugin\UrlShortenerConfigCustomizerPlugin::class,
+ Plugin\LanguageConfigCustomizerPlugin::class,
+ Plugin\ApplicationConfigCustomizerPlugin::class,
+ ] as $pluginName) {
+ /** @var Plugin\ConfigCustomizerPluginInterface $configCustomizer */
+ $configCustomizer = $this->configCustomizers->get($pluginName);
+ $configCustomizer->process($input, $output, $config);
+ }
// Generate config params files
$this->configWriter->toFile(self::GENERATED_CONFIG_PATH, $config->getArrayCopy(), false);
@@ -170,10 +174,10 @@ class InstallCommand extends Command
// Ask the user for the older shlink path
$keepAsking = true;
do {
- $this->importedInstallationPath = $this->ask(
+ $config->setImportedInstallationPath($this->ask(
'Previous shlink installation path from which to import config'
- );
- $configFile = $this->importedInstallationPath . '/' . self::GENERATED_CONFIG_PATH;
+ ));
+ $configFile = $config->getImportedInstallationPath() . '/' . self::GENERATED_CONFIG_PATH;
$configExists = $this->filesystem->exists($configFile);
if (! $configExists) {
@@ -194,151 +198,6 @@ class InstallCommand extends Command
return $config;
}
- protected function askDatabase(CustomizableAppConfig $config)
- {
- $this->printTitle('DATABASE');
-
- if ($config->hasDatabase()) {
- $keepConfig = $this->questionHelper->ask($this->input, $this->output, new ConfirmationQuestion(
- 'Do you want to keep imported database config? (Y/n): '
- ));
- if ($keepConfig) {
- // If the user selected to keep DB config and is configured to use sqlite, copy DB file
- if ($config->getDatabase()['DRIVER'] === self::DATABASE_DRIVERS['SQLite']) {
- $this->filesystem->copy(
- $this->importedInstallationPath . '/' . CustomizableAppConfig::SQLITE_DB_PATH,
- CustomizableAppConfig::SQLITE_DB_PATH
- );
- }
-
- return;
- }
- }
-
- // Select database type
- $params = [];
- $databases = array_keys(self::DATABASE_DRIVERS);
- $dbType = $this->questionHelper->ask($this->input, $this->output, new ChoiceQuestion(
- 'Select database type (defaults to ' . $databases[0] . '):',
- $databases,
- 0
- ));
- $params['DRIVER'] = self::DATABASE_DRIVERS[$dbType];
-
- // Ask for connection params if database is not SQLite
- if ($params['DRIVER'] !== self::DATABASE_DRIVERS['SQLite']) {
- $params['NAME'] = $this->ask('Database name', 'shlink');
- $params['USER'] = $this->ask('Database username');
- $params['PASSWORD'] = $this->ask('Database password');
- $params['HOST'] = $this->ask('Database host', 'localhost');
- $params['PORT'] = $this->ask('Database port', $this->getDefaultDbPort($params['DRIVER']));
- }
-
- $config->setDatabase($params);
- }
-
- protected function getDefaultDbPort($driver)
- {
- return $driver === 'pdo_mysql' ? '3306' : '5432';
- }
-
- protected function askUrlShortener(CustomizableAppConfig $config)
- {
- $this->printTitle('URL SHORTENER');
-
- if ($config->hasUrlShortener()) {
- $keepConfig = $this->questionHelper->ask($this->input, $this->output, new ConfirmationQuestion(
- 'Do you want to keep imported URL shortener config? (Y/n): '
- ));
- if ($keepConfig) {
- return;
- }
- }
-
- // Ask for URL shortener params
- $config->setUrlShortener([
- 'SCHEMA' => $this->questionHelper->ask($this->input, $this->output, new ChoiceQuestion(
- 'Select schema for generated short URLs (defaults to http):',
- ['http', 'https'],
- 0
- )),
- 'HOSTNAME' => $this->ask('Hostname for generated URLs'),
- 'CHARS' => $this->ask(
- 'Character set for generated short codes (leave empty to autogenerate one)',
- null,
- true
- ) ?: str_shuffle(UrlShortener::DEFAULT_CHARS)
- ]);
- }
-
- protected function askLanguage(CustomizableAppConfig $config)
- {
- $this->printTitle('LANGUAGE');
-
- if ($config->hasLanguage()) {
- $keepConfig = $this->questionHelper->ask($this->input, $this->output, new ConfirmationQuestion(
- 'Do you want to keep imported language? (Y/n): '
- ));
- if ($keepConfig) {
- return;
- }
- }
-
- $config->setLanguage([
- 'DEFAULT' => $this->questionHelper->ask($this->input, $this->output, new ChoiceQuestion(
- 'Select default language for the application in general (defaults to '
- . self::SUPPORTED_LANGUAGES[0] . '):',
- self::SUPPORTED_LANGUAGES,
- 0
- )),
- 'CLI' => $this->questionHelper->ask($this->input, $this->output, new ChoiceQuestion(
- 'Select default language for CLI executions (defaults to '
- . self::SUPPORTED_LANGUAGES[0] . '):',
- self::SUPPORTED_LANGUAGES,
- 0
- )),
- ]);
- }
-
- protected function askApplication(CustomizableAppConfig $config)
- {
- $this->printTitle('APPLICATION');
-
- if ($config->hasApp()) {
- $keepConfig = $this->questionHelper->ask($this->input, $this->output, new ConfirmationQuestion(
- 'Do you want to keep imported application config? (Y/n): '
- ));
- if ($keepConfig) {
- return;
- }
- }
-
- $config->setApp([
- 'SECRET' => $this->ask(
- 'Define a secret string that will be used to sign API tokens (leave empty to autogenerate one)',
- null,
- true
- ) ?: $this->generateRandomString(32),
- ]);
- }
-
- /**
- * @param string $text
- */
- protected function printTitle($text)
- {
- $text = trim($text);
- $length = strlen($text) + 4;
- $header = str_repeat('*', $length);
-
- $this->output->writeln([
- '',
- '' . $header . '',
- '* ' . strtoupper($text) . ' *',
- '' . $header . '',
- ]);
- }
-
/**
* @param string $text
* @param string|null $default
diff --git a/module/CLI/src/Factory/InstallApplicationFactory.php b/module/CLI/src/Factory/InstallApplicationFactory.php
index 2c5b5236..e4d4beb9 100644
--- a/module/CLI/src/Factory/InstallApplicationFactory.php
+++ b/module/CLI/src/Factory/InstallApplicationFactory.php
@@ -3,10 +3,15 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Factory;
+use Acelaya\ZsmAnnotatedServices\Factory\V3\AnnotatedFactory;
use Interop\Container\ContainerInterface;
use Interop\Container\Exception\ContainerException;
use Shlinkio\Shlink\CLI\Command\Install\InstallCommand;
+use Shlinkio\Shlink\CLI\Install\ConfigCustomizerPluginManager;
+use Shlinkio\Shlink\CLI\Install\Plugin;
+use Shlinkio\Shlink\CLI\Install\Plugin\Factory\DefaultConfigCustomizerPluginFactory;
use Symfony\Component\Console\Application;
+use Symfony\Component\Console\Exception\LogicException;
use Symfony\Component\Filesystem\Filesystem;
use Zend\Config\Writer\PhpArray;
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
@@ -22,6 +27,7 @@ class InstallApplicationFactory implements FactoryInterface
* @param string $requestedName
* @param null|array $options
* @return object
+ * @throws LogicException
* @throws ServiceNotFoundException if unable to resolve the service.
* @throws ServiceNotCreatedException if an exception is raised when
* creating a service.
@@ -32,7 +38,17 @@ class InstallApplicationFactory implements FactoryInterface
$isUpdate = $options !== null && isset($options['isUpdate']) ? (bool) $options['isUpdate'] : false;
$app = new Application();
- $command = new InstallCommand(new PhpArray(), new Filesystem(), $isUpdate);
+ $command = new InstallCommand(
+ new PhpArray(),
+ $container->get(Filesystem::class),
+ new ConfigCustomizerPluginManager($container, ['factories' => [
+ Plugin\DatabaseConfigCustomizerPlugin::class => AnnotatedFactory::class,
+ Plugin\UrlShortenerConfigCustomizerPlugin::class => DefaultConfigCustomizerPluginFactory::class,
+ Plugin\LanguageConfigCustomizerPlugin::class => DefaultConfigCustomizerPluginFactory::class,
+ Plugin\ApplicationConfigCustomizerPlugin::class => DefaultConfigCustomizerPluginFactory::class,
+ ]]),
+ $isUpdate
+ );
$app->add($command);
$app->setDefaultCommand($command->getName());
diff --git a/module/CLI/src/Install/ConfigCustomizerPluginManager.php b/module/CLI/src/Install/ConfigCustomizerPluginManager.php
new file mode 100644
index 00000000..c8f0e7cb
--- /dev/null
+++ b/module/CLI/src/Install/ConfigCustomizerPluginManager.php
@@ -0,0 +1,10 @@
+questionHelper = $questionHelper;
+ }
+
+ /**
+ * @param InputInterface $input
+ * @param OutputInterface $output
+ * @param string $text
+ * @param string|null $default
+ * @param bool $allowEmpty
+ * @return string
+ * @throws RuntimeException
+ */
+ protected function ask(InputInterface $input, OutputInterface $output, $text, $default = null, $allowEmpty = false)
+ {
+ if ($default !== null) {
+ $text .= ' (defaults to ' . $default . ')';
+ }
+ do {
+ $value = $this->questionHelper->ask($input, $output, new Question(
+ '' . $text . ': ',
+ $default
+ ));
+ if (empty($value) && ! $allowEmpty) {
+ $output->writeln('Value can\'t be empty');
+ }
+ } while (empty($value) && $default === null && ! $allowEmpty);
+
+ return $value;
+ }
+
+ /**
+ * @param OutputInterface $output
+ * @param string $text
+ */
+ protected function printTitle(OutputInterface $output, $text)
+ {
+ $text = trim($text);
+ $length = strlen($text) + 4;
+ $header = str_repeat('*', $length);
+
+ $output->writeln([
+ '',
+ '' . $header . '',
+ '* ' . strtoupper($text) . ' *',
+ '' . $header . '',
+ ]);
+ }
+}
diff --git a/module/CLI/src/Install/Plugin/ApplicationConfigCustomizerPlugin.php b/module/CLI/src/Install/Plugin/ApplicationConfigCustomizerPlugin.php
new file mode 100644
index 00000000..78fda68a
--- /dev/null
+++ b/module/CLI/src/Install/Plugin/ApplicationConfigCustomizerPlugin.php
@@ -0,0 +1,46 @@
+printTitle($output, 'APPLICATION');
+
+ if ($appConfig->hasApp()) {
+ $keepConfig = $this->questionHelper->ask($input, $output, new ConfirmationQuestion(
+ 'Do you want to keep imported application config? (Y/n): '
+ ));
+ if ($keepConfig) {
+ return;
+ }
+ }
+
+ $appConfig->setApp([
+ 'SECRET' => $this->ask(
+ $input,
+ $output,
+ 'Define a secret string that will be used to sign API tokens (leave empty to autogenerate one)',
+ null,
+ true
+ ) ?: $this->generateRandomString(32),
+ ]);
+ }
+}
diff --git a/module/CLI/src/Install/Plugin/ConfigCustomizerPluginInterface.php b/module/CLI/src/Install/Plugin/ConfigCustomizerPluginInterface.php
new file mode 100644
index 00000000..2f1c60e1
--- /dev/null
+++ b/module/CLI/src/Install/Plugin/ConfigCustomizerPluginInterface.php
@@ -0,0 +1,17 @@
+ 'pdo_mysql',
+ 'PostgreSQL' => 'pdo_pgsql',
+ 'SQLite' => 'pdo_sqlite',
+ ];
+
+ /**
+ * @var Filesystem
+ */
+ private $filesystem;
+
+ /**
+ * DatabaseConfigCustomizerPlugin constructor.
+ * @param QuestionHelper $questionHelper
+ * @param Filesystem $filesystem
+ *
+ * @DI\Inject({QuestionHelper::class, Filesystem::class})
+ */
+ public function __construct(QuestionHelper $questionHelper, Filesystem $filesystem)
+ {
+ parent::__construct($questionHelper);
+ $this->filesystem = $filesystem;
+ }
+
+ /**
+ * @param InputInterface $input
+ * @param OutputInterface $output
+ * @param CustomizableAppConfig $appConfig
+ * @return void
+ * @throws IOException
+ * @throws RuntimeException
+ */
+ public function process(InputInterface $input, OutputInterface $output, CustomizableAppConfig $appConfig)
+ {
+ $this->printTitle($output, 'DATABASE');
+
+ if ($appConfig->hasDatabase()) {
+ $keepConfig = $this->questionHelper->ask($input, $output, new ConfirmationQuestion(
+ 'Do you want to keep imported database config? (Y/n): '
+ ));
+ if ($keepConfig) {
+ // If the user selected to keep DB config and is configured to use sqlite, copy DB file
+ if ($appConfig->getDatabase()['DRIVER'] === self::DATABASE_DRIVERS['SQLite']) {
+ try {
+ $this->filesystem->copy(
+ $appConfig->getImportedInstallationPath() . '/' . CustomizableAppConfig::SQLITE_DB_PATH,
+ CustomizableAppConfig::SQLITE_DB_PATH
+ );
+ } catch (IOException $e) {
+ $output->writeln('It wasn\'t possible to import the SQLite database');
+ throw $e;
+ }
+ }
+
+ return;
+ }
+ }
+
+ // Select database type
+ $params = [];
+ $databases = array_keys(self::DATABASE_DRIVERS);
+ $dbType = $this->questionHelper->ask($input, $output, new ChoiceQuestion(
+ 'Select database type (defaults to ' . $databases[0] . '):',
+ $databases,
+ 0
+ ));
+ $params['DRIVER'] = self::DATABASE_DRIVERS[$dbType];
+
+ // Ask for connection params if database is not SQLite
+ if ($params['DRIVER'] !== self::DATABASE_DRIVERS['SQLite']) {
+ $params['NAME'] = $this->ask($input, $output, 'Database name', 'shlink');
+ $params['USER'] = $this->ask($input, $output, 'Database username');
+ $params['PASSWORD'] = $this->ask($input, $output, 'Database password');
+ $params['HOST'] = $this->ask($input, $output, 'Database host', 'localhost');
+ $params['PORT'] = $this->ask($input, $output, 'Database port', $this->getDefaultDbPort($params['DRIVER']));
+ }
+
+ $appConfig->setDatabase($params);
+ }
+
+ private function getDefaultDbPort($driver)
+ {
+ return $driver === 'pdo_mysql' ? '3306' : '5432';
+ }
+}
diff --git a/module/CLI/src/Install/Plugin/Factory/DefaultConfigCustomizerPluginFactory.php b/module/CLI/src/Install/Plugin/Factory/DefaultConfigCustomizerPluginFactory.php
new file mode 100644
index 00000000..6e1ea7a0
--- /dev/null
+++ b/module/CLI/src/Install/Plugin/Factory/DefaultConfigCustomizerPluginFactory.php
@@ -0,0 +1,31 @@
+get(QuestionHelper::class));
+ }
+}
diff --git a/module/CLI/src/Install/Plugin/LanguageConfigCustomizerPlugin.php b/module/CLI/src/Install/Plugin/LanguageConfigCustomizerPlugin.php
new file mode 100644
index 00000000..3259883a
--- /dev/null
+++ b/module/CLI/src/Install/Plugin/LanguageConfigCustomizerPlugin.php
@@ -0,0 +1,52 @@
+printTitle($output, 'LANGUAGE');
+
+ if ($appConfig->hasLanguage()) {
+ $keepConfig = $this->questionHelper->ask($input, $output, new ConfirmationQuestion(
+ 'Do you want to keep imported language? (Y/n): '
+ ));
+ if ($keepConfig) {
+ return;
+ }
+ }
+
+ $appConfig->setLanguage([
+ 'DEFAULT' => $this->questionHelper->ask($input, $output, new ChoiceQuestion(
+ 'Select default language for the application in general (defaults to '
+ . self::SUPPORTED_LANGUAGES[0] . '):',
+ self::SUPPORTED_LANGUAGES,
+ 0
+ )),
+ 'CLI' => $this->questionHelper->ask($input, $output, new ChoiceQuestion(
+ 'Select default language for CLI executions (defaults to '
+ . self::SUPPORTED_LANGUAGES[0] . '):',
+ self::SUPPORTED_LANGUAGES,
+ 0
+ )),
+ ]);
+ }
+}
diff --git a/module/CLI/src/Install/Plugin/UrlShortenerConfigCustomizerPlugin.php b/module/CLI/src/Install/Plugin/UrlShortenerConfigCustomizerPlugin.php
new file mode 100644
index 00000000..7c0dec48
--- /dev/null
+++ b/module/CLI/src/Install/Plugin/UrlShortenerConfigCustomizerPlugin.php
@@ -0,0 +1,53 @@
+printTitle($output, 'URL SHORTENER');
+
+ if ($appConfig->hasUrlShortener()) {
+ $keepConfig = $this->questionHelper->ask($input, $output, new ConfirmationQuestion(
+ 'Do you want to keep imported URL shortener config? (Y/n): '
+ ));
+ if ($keepConfig) {
+ return;
+ }
+ }
+
+ // Ask for URL shortener params
+ $appConfig->setUrlShortener([
+ 'SCHEMA' => $this->questionHelper->ask($input, $output, new ChoiceQuestion(
+ 'Select schema for generated short URLs (defaults to http):',
+ ['http', 'https'],
+ 0
+ )),
+ 'HOSTNAME' => $this->ask($input, $output, 'Hostname for generated URLs'),
+ 'CHARS' => $this->ask(
+ $input,
+ $output,
+ 'Character set for generated short codes (leave empty to autogenerate one)',
+ null,
+ true
+ ) ?: str_shuffle(UrlShortener::DEFAULT_CHARS)
+ ]);
+ }
+}
diff --git a/module/CLI/src/Model/CustomizableAppConfig.php b/module/CLI/src/Model/CustomizableAppConfig.php
index a1f48bbd..03836e0f 100644
--- a/module/CLI/src/Model/CustomizableAppConfig.php
+++ b/module/CLI/src/Model/CustomizableAppConfig.php
@@ -23,6 +23,10 @@ final class CustomizableAppConfig implements ArraySerializableInterface
* @var array
*/
private $app;
+ /**
+ * @var string
+ */
+ private $importedInstallationPath;
/**
* @return array
@@ -128,6 +132,32 @@ final class CustomizableAppConfig implements ArraySerializableInterface
return ! empty($this->app);
}
+ /**
+ * @return string
+ */
+ public function getImportedInstallationPath()
+ {
+ return $this->importedInstallationPath;
+ }
+
+ /**
+ * @param string $importedInstallationPath
+ * @return $this|self
+ */
+ public function setImportedInstallationPath($importedInstallationPath)
+ {
+ $this->importedInstallationPath = $importedInstallationPath;
+ return $this;
+ }
+
+ /**
+ * @return bool
+ */
+ public function hasImportedInstallationPath()
+ {
+ return $this->importedInstallationPath !== null;
+ }
+
/**
* Exchange internal values from provided array
*