diff --git a/bin/update b/bin/update index 86b7efc9..a10ef3f1 100755 --- a/bin/update +++ b/bin/update @@ -1,6 +1,6 @@ #!/usr/bin/env php add($command); $app->setDefaultCommand($command->getName()); $app->run(); diff --git a/module/CLI/src/Command/Install/AbstractInstallCommand.php b/module/CLI/src/Command/Install/AbstractInstallCommand.php deleted file mode 100644 index f6341627..00000000 --- a/module/CLI/src/Command/Install/AbstractInstallCommand.php +++ /dev/null @@ -1,368 +0,0 @@ - 'pdo_mysql', - 'PostgreSQL' => 'pdo_pgsql', - 'SQLite' => 'pdo_sqlite', - ]; - const SUPPORTED_LANGUAGES = ['en', 'es']; - const GENERATED_CONFIG_PATH = 'config/params/generated_config.php'; - - /** - * @var InputInterface - */ - protected $input; - /** - * @var OutputInterface - */ - protected $output; - /** - * @var QuestionHelper - */ - private $questionHelper; - /** - * @var ProcessHelper - */ - private $processHelper; - /** - * @var WriterInterface - */ - private $configWriter; - - /** - * InstallCommand constructor. - * @param WriterInterface $configWriter - * @throws LogicException - */ - public function __construct(WriterInterface $configWriter) - { - parent::__construct(); - $this->configWriter = $configWriter; - } - - public function configure() - { - $this->setName('shlink:install') - ->setDescription('Installs Shlink'); - } - - public function execute(InputInterface $input, OutputInterface $output) - { - $this->input = $input; - $this->output = $output; - $this->questionHelper = $this->getHelper('question'); - $this->processHelper = $this->getHelper('process'); - $params = []; - - $output->writeln([ - 'Welcome to Shlink!!', - 'This will guide you through the installation process.', - ]); - - // Check if a cached config file exists and drop it if so - if (file_exists('data/cache/app_config.php')) { - $output->write('Deleting old cached config...'); - if (unlink('data/cache/app_config.php')) { - $output->writeln(' Success'); - } else { - $output->writeln( - ' Failed! You will have to manually delete the data/cache/app_config.php file to get' - . ' new config applied.' - ); - return; - } - } - - // If running update command, ask the user to import previous config - if ($this->isUpdate()) { - $this->importConfig(); - } - - // Ask for custom config params - $params['DATABASE'] = $this->askDatabase(); - $params['URL_SHORTENER'] = $this->askUrlShortener(); - $params['LANGUAGE'] = $this->askLanguage(); - $params['APP'] = $this->askApplication(); - - // Generate config params files - $config = $this->buildAppConfig($params); - $this->configWriter->toFile(self::GENERATED_CONFIG_PATH, $config, false); - $output->writeln(['Custom configuration properly generated!', '']); - - // If current command is not update, generate database - if (! $this->isUpdate()) { - $this->output->writeln('Initializing database...'); - if (! $this->runCommand( - 'php vendor/bin/doctrine.php orm:schema-tool:create', - 'Error generating database.' - )) { - return; - } - } - - // Run database migrations - $output->writeln('Updating database...'); - if (! $this->runCommand('php vendor/bin/doctrine-migrations migrations:migrate', 'Error updating database.')) { - return; - } - - // Generate proxies - $output->writeln('Generating proxies...'); - if (! $this->runCommand('php vendor/bin/doctrine.php orm:generate-proxies', 'Error generating proxies.')) { - return; - } - } - - protected function importConfig() - { - // Ask the user if he/she wants to import an older configuration - $importConfig = $this->questionHelper->ask($this->input, $this->output, new ConfirmationQuestion( - 'Do you want to import previous configuration? (Y/n): ' - )); - if (! $importConfig) { - return; - } - - // Ask the user for the older shlink path - $keepAsking = true; - do { - $installationPath = $this->ask('Previous shlink installation path from which to import config'); - $configFile = $installationPath . '/' . self::GENERATED_CONFIG_PATH; - $configExists = file_exists($configFile); - - if (! $configExists) { - $keepAsking = $this->questionHelper->ask($this->input, $this->output, new ConfirmationQuestion( - 'Provided path does not seem to be a valid shlink root path. ' - . 'Do you want to try another path? (Y/n): ' - )); - } - } while (! $configExists && $keepAsking); - - // Read the config file - $previousConfig = include $configFile; - } - - protected function askDatabase() - { - $params = []; - $this->printTitle('DATABASE'); - - // Select database type - $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'])); - } - - return $params; - } - - protected function getDefaultDbPort($driver) - { - return $driver === 'pdo_mysql' ? '3306' : '5432'; - } - - protected function askUrlShortener() - { - $this->printTitle('URL SHORTENER'); - - // Ask for URL shortener params - return [ - '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() - { - $this->printTitle('LANGUAGE'); - - return [ - '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() - { - $this->printTitle('APPLICATION'); - - return [ - '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 - * @param bool $allowEmpty - * @return string - * @throws RuntimeException - */ - protected function ask($text, $default = null, $allowEmpty = false) - { - if ($default !== null) { - $text .= ' (defaults to ' . $default . ')'; - } - do { - $value = $this->questionHelper->ask($this->input, $this->output, new Question( - '' . $text . ': ', - $default - )); - if (empty($value) && ! $allowEmpty) { - $this->output->writeln('Value can\'t be empty'); - } - } while (empty($value) && $default === null && ! $allowEmpty); - - return $value; - } - - /** - * @param array $params - * @return array - */ - protected function buildAppConfig(array $params) - { - // Build simple config - $config = [ - 'app_options' => [ - 'secret_key' => $params['APP']['SECRET'], - ], - 'entity_manager' => [ - 'connection' => [ - 'driver' => $params['DATABASE']['DRIVER'], - ], - ], - 'translator' => [ - 'locale' => $params['LANGUAGE']['DEFAULT'], - ], - 'cli' => [ - 'locale' => $params['LANGUAGE']['CLI'], - ], - 'url_shortener' => [ - 'domain' => [ - 'schema' => $params['URL_SHORTENER']['SCHEMA'], - 'hostname' => $params['URL_SHORTENER']['HOSTNAME'], - ], - 'shortcode_chars' => $params['URL_SHORTENER']['CHARS'], - ], - ]; - - // Build dynamic database config - if ($params['DATABASE']['DRIVER'] === 'pdo_sqlite') { - $config['entity_manager']['connection']['path'] = 'data/database.sqlite'; - } else { - $config['entity_manager']['connection']['user'] = $params['DATABASE']['USER']; - $config['entity_manager']['connection']['password'] = $params['DATABASE']['PASSWORD']; - $config['entity_manager']['connection']['dbname'] = $params['DATABASE']['NAME']; - $config['entity_manager']['connection']['host'] = $params['DATABASE']['HOST']; - $config['entity_manager']['connection']['port'] = $params['DATABASE']['PORT']; - - if ($params['DATABASE']['DRIVER'] === 'pdo_mysql') { - $config['entity_manager']['connection']['driverOptions'] = [ - \PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8', - ]; - } - } - - return $config; - } - - /** - * @param string $command - * @param string $errorMessage - * @return bool - */ - protected function runCommand($command, $errorMessage) - { - $process = $this->processHelper->run($this->output, $command); - if ($process->isSuccessful()) { - $this->output->writeln(' Success!'); - return true; - } - - if ($this->output->isVerbose()) { - return false; - } - - $this->output->writeln( - ' ' . $errorMessage . ' Run this command with -vvv to see specific error info.' - ); - return false; - } - - /** - * @return bool - */ - abstract protected function isUpdate(); -} diff --git a/module/CLI/src/Command/Install/InstallCommand.php b/module/CLI/src/Command/Install/InstallCommand.php index e414bbe0..2ff9e9da 100644 --- a/module/CLI/src/Command/Install/InstallCommand.php +++ b/module/CLI/src/Command/Install/InstallCommand.php @@ -1,13 +1,363 @@ 'pdo_mysql', + 'PostgreSQL' => 'pdo_pgsql', + 'SQLite' => 'pdo_sqlite', + ]; + const SUPPORTED_LANGUAGES = ['en', 'es']; + const GENERATED_CONFIG_PATH = 'config/params/generated_config.php'; + /** + * @var InputInterface + */ + private $input; + /** + * @var OutputInterface + */ + private $output; + /** + * @var QuestionHelper + */ + private $questionHelper; + /** + * @var ProcessHelper + */ + private $processHelper; + /** + * @var WriterInterface + */ + private $configWriter; + /** + * @var bool + */ + private $isUpdate; + + /** + * InstallCommand constructor. + * @param WriterInterface $configWriter + * @param bool $isUpdate + * @throws LogicException + */ + public function __construct(WriterInterface $configWriter, $isUpdate = false) + { + parent::__construct(); + $this->configWriter = $configWriter; + $this->isUpdate = $isUpdate; + } + + public function configure() + { + $this->setName('shlink:install') + ->setDescription('Installs Shlink'); + } + + public function execute(InputInterface $input, OutputInterface $output) + { + $this->input = $input; + $this->output = $output; + $this->questionHelper = $this->getHelper('question'); + $this->processHelper = $this->getHelper('process'); + + $output->writeln([ + 'Welcome to Shlink!!', + 'This will guide you through the installation process.', + ]); + + // Check if a cached config file exists and drop it if so + if (file_exists('data/cache/app_config.php')) { + $output->write('Deleting old cached config...'); + if (unlink('data/cache/app_config.php')) { + $output->writeln(' Success'); + } else { + $output->writeln( + ' Failed! You will have to manually delete the data/cache/app_config.php file to get' + . ' new config applied.' + ); + return; + } + } + + // If running update command, ask the user to import previous config + $config = $this->isUpdate ? $this->importConfig() : new CustomizableAppConfig(); + + // Ask for custom config params + $this->askDatabase($config); + $this->askUrlShortener($config); + $this->askLanguage($config); + $this->askApplication($config); + + // Generate config params files + $this->configWriter->toFile(self::GENERATED_CONFIG_PATH, $config->getArrayCopy(), false); + $output->writeln(['Custom configuration properly generated!', '']); + + // If current command is not update, generate database + if (! $this->isUpdate) { + $this->output->writeln('Initializing database...'); + if (! $this->runCommand( + 'php vendor/bin/doctrine.php orm:schema-tool:create', + 'Error generating database.' + )) { + return; + } + } + + // Run database migrations + $output->writeln('Updating database...'); + if (! $this->runCommand('php vendor/bin/doctrine-migrations migrations:migrate', 'Error updating database.')) { + return; + } + + // Generate proxies + $output->writeln('Generating proxies...'); + if (! $this->runCommand('php vendor/bin/doctrine.php orm:generate-proxies', 'Error generating proxies.')) { + return; + } + } + + /** + * @return CustomizableAppConfig + * @throws RuntimeException + */ + protected function importConfig() + { + $config = new CustomizableAppConfig(); + + // Ask the user if he/she wants to import an older configuration + $importConfig = $this->questionHelper->ask($this->input, $this->output, new ConfirmationQuestion( + 'Do you want to import previous configuration? (Y/n): ' + )); + if (! $importConfig) { + return $config; + } + + // Ask the user for the older shlink path + $keepAsking = true; + do { + $installationPath = $this->ask('Previous shlink installation path from which to import config'); + $configFile = $installationPath . '/' . self::GENERATED_CONFIG_PATH; + $configExists = file_exists($configFile); + + if (! $configExists) { + $keepAsking = $this->questionHelper->ask($this->input, $this->output, new ConfirmationQuestion( + 'Provided path does not seem to be a valid shlink root path. ' + . 'Do you want to try another path? (Y/n): ' + )); + } + } while (! $configExists && $keepAsking); + + // If after some retries the user has chosen not to test another path, return + if (! $configExists) { + return $config; + } + + // Read the config file + $config->exchangeArray(include $configFile); + 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) { + 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 + * @param bool $allowEmpty + * @return string + * @throws RuntimeException + */ + protected function ask($text, $default = null, $allowEmpty = false) + { + if ($default !== null) { + $text .= ' (defaults to ' . $default . ')'; + } + do { + $value = $this->questionHelper->ask($this->input, $this->output, new Question( + '' . $text . ': ', + $default + )); + if (empty($value) && ! $allowEmpty) { + $this->output->writeln('Value can\'t be empty'); + } + } while (empty($value) && $default === null && ! $allowEmpty); + + return $value; + } + + /** + * @param string $command + * @param string $errorMessage * @return bool */ - protected function isUpdate() + protected function runCommand($command, $errorMessage) { + $process = $this->processHelper->run($this->output, $command); + if ($process->isSuccessful()) { + $this->output->writeln(' Success!'); + return true; + } + + if ($this->output->isVerbose()) { + return false; + } + + $this->output->writeln( + ' ' . $errorMessage . ' Run this command with -vvv to see specific error info.' + ); return false; } } diff --git a/module/CLI/src/Command/Install/UpdateCommand.php b/module/CLI/src/Command/Install/UpdateCommand.php deleted file mode 100644 index 425dc06a..00000000 --- a/module/CLI/src/Command/Install/UpdateCommand.php +++ /dev/null @@ -1,13 +0,0 @@ -database; + } + + /** + * @param array $database + * @return $this + */ + public function setDatabase(array $database) + { + $this->database = $database; + return $this; + } + + /** + * @return bool + */ + public function hasDatabase() + { + return ! empty($this->database); + } + + /** + * @return array + */ + public function getUrlShortener() + { + return $this->urlShortener; + } + + /** + * @param array $urlShortener + * @return $this + */ + public function setUrlShortener(array $urlShortener) + { + $this->urlShortener = $urlShortener; + return $this; + } + + /** + * @return bool + */ + public function hasUrlShortener() + { + return ! empty($this->urlShortener); + } + + /** + * @return array + */ + public function getLanguage() + { + return $this->language; + } + + /** + * @param array $language + * @return $this + */ + public function setLanguage(array $language) + { + $this->language = $language; + return $this; + } + + /** + * @return bool + */ + public function hasLanguage() + { + return ! empty($this->language); + } + + /** + * @return array + */ + public function getApp() + { + return $this->app; + } + + /** + * @param array $app + * @return $this + */ + public function setApp(array $app) + { + $this->app = $app; + return $this; + } + + /** + * @return bool + */ + public function hasApp() + { + return ! empty($this->app); + } + + /** + * Exchange internal values from provided array + * + * @param array $array + * @return void + */ + public function exchangeArray(array $array) + { + + } + + /** + * Return an array representation of the object + * + * @return array + */ + public function getArrayCopy() + { + $config = [ + 'app_options' => [ + 'secret_key' => $this->app['SECRET'], + ], + 'entity_manager' => [ + 'connection' => [ + 'driver' => $this->database['DRIVER'], + ], + ], + 'translator' => [ + 'locale' => $this->language['DEFAULT'], + ], + 'cli' => [ + 'locale' => $this->language['CLI'], + ], + 'url_shortener' => [ + 'domain' => [ + 'schema' => $this->urlShortener['SCHEMA'], + 'hostname' => $this->urlShortener['HOSTNAME'], + ], + 'shortcode_chars' => $this->urlShortener['CHARS'], + ], + ]; + + // Build dynamic database config based on selected driver + if ($this->database['DRIVER'] === 'pdo_sqlite') { + $config['entity_manager']['connection']['path'] = 'data/database.sqlite'; + } else { + $config['entity_manager']['connection']['user'] = $this->database['USER']; + $config['entity_manager']['connection']['password'] = $this->database['PASSWORD']; + $config['entity_manager']['connection']['dbname'] = $this->database['NAME']; + $config['entity_manager']['connection']['host'] = $this->database['HOST']; + $config['entity_manager']['connection']['port'] = $this->database['PORT']; + + if ($this->database['DRIVER'] === 'pdo_mysql') { + $config['entity_manager']['connection']['driverOptions'] = [ + \PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8', + ]; + } + } + + return $config; + } +} diff --git a/module/CLI/test/Command/Install/InstallCommandTest.php b/module/CLI/test/Command/Install/InstallCommandTest.php index ebfb7e29..4b49d7ec 100644 --- a/module/CLI/test/Command/Install/InstallCommandTest.php +++ b/module/CLI/test/Command/Install/InstallCommandTest.php @@ -104,6 +104,7 @@ CLI_INPUT 'shortcode_chars' => 'abc123BCA', ], ], false)->shouldBeCalledTimes(1); + $this->commandTester->execute([ 'command' => 'shlink:install', ]); diff --git a/module/Common/src/Util/StringUtilsTrait.php b/module/Common/src/Util/StringUtilsTrait.php index 648dbfcb..9680aa49 100644 --- a/module/Common/src/Util/StringUtilsTrait.php +++ b/module/Common/src/Util/StringUtilsTrait.php @@ -9,7 +9,7 @@ trait StringUtilsTrait $charactersLength = strlen($characters); $randomString = ''; for ($i = 0; $i < $length; $i++) { - $randomString .= $characters[rand(0, $charactersLength - 1)]; + $randomString .= $characters[mt_rand(0, $charactersLength - 1)]; } return $randomString;