diff --git a/src/Commands/StarterKitCommand.php b/src/Commands/StarterKitCommand.php new file mode 100644 index 00000000..aec6b8a9 --- /dev/null +++ b/src/Commands/StarterKitCommand.php @@ -0,0 +1,268 @@ +acquiaCmsCli = $cli; + $this->installTask = $installTask; + $this->installerQuestions = $installerQuestions; + parent::__construct(); + } + + /** + * {@inheritdoc} + */ + protected function configure(): void { + $this->setName("starterkit") + ->setDescription("Use this command to setup & install a site.") + ->setDefinition([ + new InputArgument('name', NULL, "Name of the starter kit"), + new InputOption('uri', 'l', InputOption::VALUE_OPTIONAL, "Multisite uri to setup drupal site."), + new InputOption('no-install', NULL, InputOption::VALUE_NONE, "Omit Drupal install or database drop operations. Will use an existing database."), + new InputOption('no-composer', NULL, InputOption::VALUE_NONE, "Omit composer operations. Will fail if codebase dependencies are missing."), + ]) + ->setHelp("The starterkit command downloads & setup Drupal site based on user selected use case."); + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output): int { + try { + $name = $input->getArgument('name'); + $args = []; + if ($name) { + $this->validationOptions($name); + $this->acquiaCmsCli->printLogo(); + $this->acquiaCmsCli->printHeadline(); + } + else { + $this->acquiaCmsCli->printLogo(); + $this->acquiaCmsCli->printHeadline(); + $name = $this->askBundleQuestion($input, $output); + } + $args['keys'] = $this->askKeysQuestions($input, $output, $name); + $this->installTask->configure($input, $output, $name); + + $args['drush_install'] = !$input->getOption('no-install'); + $args['use_composer'] = !$input->getOption('no-composer'); + + $this->installTask->run($args); + $this->postSiteInstall($name, $output); + } + catch (AcmsCliException $e) { + $output->writeln("" . $e->getMessage() . ""); + return StatusCodes::ERROR; + } + return StatusCodes::OK; + } + + /** + * Validate all input options/arguments. + * + * @param string $name + * A name of the user selected use-case. + */ + protected function validationOptions(string $name): bool { + $starterKits = array_keys($this->acquiaCmsCli->getStarterKits()); + if (!in_array($name, $starterKits)) { + throw new InvalidArgumentException("Invalid starter kit. It should be from one of the following: " . implode(", ", $starterKits) . "."); + } + return TRUE; + } + + /** + * Providing input to user, asking to select the starter-kit. + */ + protected function askBundleQuestion(InputInterface $input, OutputInterface $output): string { + $helper = $this->getHelper('question'); + $bundles = array_keys($this->acquiaCmsCli->getStarterKits()); + $this->renderStarterKits($output); + $starterKit = "acquia_cms_enterprise_low_code"; + $question = new Question($this->styleQuestion("Please choose bundle from one of the above use case", $starterKit), $starterKit); + $question->setAutocompleterValues($bundles); + $question->setValidator(function ($answer) use ($bundles) { + if (!is_string($answer) || !in_array($answer, $bundles)) { + throw new \RuntimeException( + "Please choose from one of the use case defined above. Ex: acquia_cms_enterprise_low_code." + ); + } + return $answer; + }); + $question->setMaxAttempts(3); + return $helper->ask($input, $output, $question); + } + + /** + * Providing input to user, asking to provide key. + */ + protected function askKeysQuestions(InputInterface $input, OutputInterface $output, string $bundle): array { + // The questions defined in acms.yml file. + $questions = $this->installerQuestions->getQuestions($this->acquiaCmsCli->getInstallerQuestions(), $bundle); + // Get all questions for user selected use-case. + $processedQuestions = $this->installerQuestions->process(array_merge($questions['questionMustAsk'], $questions['questionSkipped'])); + + // Initialize the value with default answer for question, so that + // if any question is dependent on other question which is skipped, + // we can use the value for that question to make sure the cli + // doesn't throw following RunTime exception:"Not able to resolve variable". + // @see AcquiaCMS\Cli\Helpers::shouldAskQuestion(). + $userInputValues = $processedQuestions['default']; + if (isset($processedQuestions['questionToAsk'])) { + foreach ($processedQuestions['questionToAsk'] as $key => $question) { + $userInputValues[$key] = $this->askQuestion($question, $key, $input, $output); + } + foreach ($questions['questionCanAsk'] as $key => $question) { + if ($this->installerQuestions->shouldAskQuestion($question, $userInputValues)) { + $userInputValues[$key] = $this->askQuestion($question, $key, $input, $output); + } + } + } + return array_merge($processedQuestions['default'], $userInputValues); + } + + /** + * Renders the table showing list of all starter kits. + */ + protected function renderStarterKits(OutputInterface $output): void { + $table = new Table($output); + $table->setHeaders(['ID', 'Name', 'Description']); + $starter_kits = $this->acquiaCmsCli->getStarterKits(); + $total = count($starter_kits); + $key = 0; + foreach ($starter_kits as $id => $starter_kit) { + $useCases[$id] = $starter_kit; + $table->addRow([$id, $starter_kit['name'], $starter_kit['description']]); + if ($key + 1 != $total) { + $table->addRow(["", "", ""]); + } + $key++; + } + $table->setColumnMaxWidth(2, 81); + $table->setStyle('box'); + $table->render(); + } + + /** + * Show successful message post site installation. + * + * @param string $bundle + * User selected starter-kit. + * @param \Symfony\Component\Console\Output\OutputInterface $output + * A Symfony console output object. + */ + protected function postSiteInstall(string $bundle, OutputInterface $output): void { + $output->writeln(""); + $formatter = $this->getHelper('formatter'); + $infoMessage = "[OK] Thank you for choosing Acquia CMS. We've successfully setup your project using bundle: `$bundle`."; + $formattedInfoBlock = $formatter->formatBlock($infoMessage, 'fg=black;bg=green', TRUE); + $output->writeln($formattedInfoBlock); + $output->writeln(""); + } + + /** + * Function to ask question to user. + * + * @param array $question + * An array of question. + * @param string $key + * A unique key for question. + * @param \Symfony\Component\Console\Input\InputInterface $input + * A Console input interface object. + * @param \Symfony\Component\Console\Output\OutputInterface $output + * A Console output interface object. + */ + public function askQuestion(array $question, string $key, InputInterface $input, OutputInterface $output): string { + $helper = $this->getHelper('question'); + $isRequired = $question['required'] ?? FALSE; + $defaultValue = $this->installerQuestions->getDefaultValue($question, $key); + $skipOnValue = $question['skip_on_value'] ?? TRUE; + if ($skipOnValue && $defaultValue) { + return $defaultValue; + } + $askQuestion = new Question($this->styleQuestion($question['question'], $defaultValue, $isRequired, TRUE)); + $askQuestion->setValidator(function ($answer) use ($question, $key, $isRequired, $output, $defaultValue) { + if (!is_string($answer) && !$defaultValue) { + if ($isRequired) { + throw new \RuntimeException( + "The `" . $key . "` cannot be left empty." + ); + } + else { + if (isset($question['warning'])) { + $warning = str_replace(PHP_EOL, PHP_EOL . " ", $question['warning']); + $output->writeln($this->style(" " . $warning, 'warning', FALSE)); + } + } + } + if ($answer && isset($question['allowed_values']['options']) && !in_array($answer, $question['allowed_values']['options'])) { + throw new \RuntimeException( + "Invalid value. It should be from one of the following: " . implode(", ", $question['allowed_values']['options']) + ); + } + return $answer ?: $defaultValue; + }); + $askQuestion->setMaxAttempts(3); + if (isset($question['allowed_values']['options'])) { + $askQuestion->setAutocompleterValues($question['allowed_values']['options']); + } + $response = $helper->ask($input, $output, $askQuestion); + return ($response === NULL) ? $defaultValue : $response; + } + +} diff --git a/src/Helpers/Task/InstallTask.php b/src/Helpers/Task/InstallTask.php index 64bea97b..3d7034d1 100644 --- a/src/Helpers/Task/InstallTask.php +++ b/src/Helpers/Task/InstallTask.php @@ -22,7 +22,6 @@ * Executes the task needed to run site:install command. */ class InstallTask { - use StatusMessageTrait; /** @@ -162,7 +161,7 @@ public function __construct(Cli $cli, ContainerInterface $container) { * @poram Symfony\Component\Console\Command\Command $output * The site:install Symfony console command object. */ - public function configure(InputInterface $input, OutputInterface $output, string $bundle) :void { + public function configure(InputInterface $input, OutputInterface $output, string $bundle): void { $this->bundle = $bundle; $this->input = $input; $this->output = $output; @@ -174,28 +173,48 @@ public function configure(InputInterface $input, OutputInterface $output, string * @param array $args * An array of params argument to pass. */ - public function run(array $args) :void { + public function run(array $args): void { $installedDrupalVersion = $this->validateDrupal->execute(); - if (!$installedDrupalVersion) { + if (!$this->validateDrupal->isInComposer()) { $this->print("Looks like, current project is not a Drupal project:", 'warning'); $this->print("Converting the current project to Drupal project:", 'headline'); $this->downloadDrupal->execute(); } else { - $this->print("Seems Drupal is already downloaded. " . + $this->print( + "Seems Drupal is already downloaded. " . "The downloaded Drupal core version is: $installedDrupalVersion. " . - "Skipping downloading Drupal.", 'success' - ); + "Skipping downloading Drupal.", + 'success' + ); } - $this->print("Downloading all packages/modules/themes required by the starter-kit:", 'headline'); - $this->acquiaCmsCli->alterModulesAndThemes($this->starterKits[$this->bundle], $args['keys']); - $this->downloadModules->execute($this->starterKits[$this->bundle]); - $this->print("Installing Site:", 'headline'); - $this->siteInstall->execute([ - 'no-interaction' => $this->input->getOption('no-interaction'), - 'name' => $this->starterKits[$this->bundle]['name'], - ]); + if (!$this->input->hasOption('no-composer') || !$this->input->getOption('no-composer')) { + $this->print("Downloading all packages/modules/themes required by the starter-kit:", 'headline'); + $this->acquiaCmsCli->alterModulesAndThemes($this->starterKits[$this->bundle], $args['keys']); + $this->downloadModules->execute($this->starterKits[$this->bundle]); + } + else { + $this->print("Omitting composer dependency management. Drupal may not have all required dependencies.", 'warning'); + } + + if (!$this->input->hasOption('no-install') || !$this->input->getOption('no-install')) { + $this->print("Installing Site:", 'headline'); + $this->siteInstall->execute([ + 'no-interaction' => $this->input->getOption('no-interaction'), + 'name' => $this->starterKits[$this->bundle]['name'], + ]); + } + else { + $this->print("Omiting Drupal install.", 'notice'); + } + + // Revalidate Drupal is installed. + $this->validateDrupal->execute(); + if (!$this->validateDrupal->isInstalled()) { + $this->print("Drupal is not installed. Discontinuing starter kit initialization.", 'warning'); + return; + } $bundle_modules = $this->starterKits[$this->bundle]['modules']['install'] ?? []; $modules_list = JsonParser::installPackages($bundle_modules); @@ -235,11 +254,12 @@ public function run(array $args) :void { ]); } else { - $this->print("Skipped importing Site Studio Packages." . + $this->print( + "Skipped importing Site Studio Packages." . PHP_EOL . "You can set the key later from: /admin/cohesion/configuration/account-settings & import Site Studio packages.", - "warning", - ); + "warning", + ); } } @@ -279,7 +299,7 @@ public function run(array $args) :void { * @param string $type * Type of styling the message. */ - protected function print(string $message, string $type) :void { + protected function print(string $message, string $type): void { $this->output->writeln($this->style($message, $type)); } diff --git a/src/Helpers/Task/Steps/ValidateDrupal.php b/src/Helpers/Task/Steps/ValidateDrupal.php index 05f82072..ebc1edb5 100644 --- a/src/Helpers/Task/Steps/ValidateDrupal.php +++ b/src/Helpers/Task/Steps/ValidateDrupal.php @@ -3,6 +3,7 @@ namespace AcquiaCMS\Cli\Helpers\Task\Steps; use AcquiaCMS\Cli\Helpers\Process\Commands\Composer; +use AcquiaCMS\Cli\Helpers\Process\Commands\Drush; /** * Provides the class to validate if current project is Drupal project. @@ -14,16 +15,40 @@ class ValidateDrupal { * * @var \AcquiaCMS\Cli\Helpers\Process\Commands\Composer */ - protected $composerCommand; + protected Composer $composerCommand; + + /** + * Flag indicating if Drupal is in composer.lock. + * + * @var bool + */ + protected $isInComposer = FALSE; + + /** + * A drush command object. + * + * @var \AcquiaCMS\Cli\Helpers\Process\Commands\Drush + */ + protected Drush $drushCommand; + + /** + * Flag indicating if Drupal is installed. + * + * @var bool + */ + protected $isInstalled = FALSE; /** * Constructs an object. * * @param \AcquiaCMS\Cli\Helpers\Process\Commands\Composer $composer * Holds the composer command class object. + * @param \AcquiaCMS\Cli\Helpers\Process\Commands\Drush $drush + * Holds the drush command class object. */ - public function __construct(Composer $composer) { + public function __construct(Composer $composer, Drush $drush) { $this->composerCommand = $composer; + $this->drushCommand = $drush; } /** @@ -41,9 +66,35 @@ public function execute(array $args = []) :string { $version = ''; $json_output = json_decode($output); if (json_last_error() === JSON_ERROR_NONE) { + $this->isInComposer = TRUE; $version = implode(', ', $json_output->versions); } + + $statusCommand = [ + "status", + "--format=json", + ]; + $status_information = $this->drushCommand->prepare($statusCommand)->runQuietly([], FALSE); + $json_output = json_decode($status_information, TRUE); + if (json_last_error() === JSON_ERROR_NONE) { + $this->isInstalled = isset($json_output['bootstrap']) && ($json_output['bootstrap'] == 'Successful'); + } + return $version; } + /** + * Indicate if Drupal is available in composer. + */ + public function isInComposer(): bool { + return $this->isInComposer; + } + + /** + * Indicate if Drupal is installed and bootstrapped. + */ + public function isInstalled(): bool { + return $this->isInstalled; + } + }