class RecipeCommand

Same name and namespace in other branches
  1. 11.x core/lib/Drupal/Core/Recipe/RecipeCommand.php \Drupal\Core\Recipe\RecipeCommand

Applies recipe.

@internal This API is experimental.

Hierarchy

  • class \Drupal\Core\Recipe\RecipeCommand implements \Symfony\Component\Console\Command\Command

Expanded class hierarchy of RecipeCommand

File

core/lib/Drupal/Core/Recipe/RecipeCommand.php, line 30

Namespace

Drupal\Core\Recipe
View source
final class RecipeCommand extends Command {
  
  /**
   * The class loader.
   *
   * @var object
   */
  protected $classLoader;
  
  /**
   * Constructs a new RecipeCommand command.
   *
   * @param object $class_loader
   *   The class loader.
   */
  public function __construct($class_loader) {
    parent::__construct('recipe');
    $this->classLoader = $class_loader;
  }
  
  /**
   * {@inheritdoc}
   */
  protected function configure() : void {
    $this->setDescription('Applies a recipe to a site.')
      ->addArgument('path', InputArgument::REQUIRED, 'The path to the recipe\'s folder to apply');
  }
  
  /**
   * {@inheritdoc}
   */
  protected function execute(InputInterface $input, OutputInterface $output) : int {
    $io = new SymfonyStyle($input, $output);
    $recipe_path = $input->getArgument('path');
    if (!is_string($recipe_path) || !is_dir($recipe_path)) {
      $io->error(sprintf('The supplied path %s is not a directory', $recipe_path));
      return 1;
    }
    // Recipes can only be applied to an already-installed site.
    $container = $this->boot()
      ->getContainer();
    /** @var \Drupal\Core\Config\Checkpoint\CheckpointStorageInterface $checkpoint_storage */
    $checkpoint_storage = $container->get('config.storage.checkpoint');
    $recipe = Recipe::createFromDirectory($recipe_path);
    if ($checkpoint_storage instanceof LoggerAwareInterface) {
      $logger = new ConsoleLogger($output, [
        // The checkpoint storage logs a notice if it decides to not create a
        // checkpoint, and we want to be sure those notices are seen even
        // without additional verbosity.
LogLevel::NOTICE => OutputInterface::VERBOSITY_NORMAL,
      ]);
      $checkpoint_storage->setLogger($logger);
    }
    $backup_checkpoint = $checkpoint_storage->checkpoint("Backup before the '{$recipe->name}' recipe.");
    try {
      $steps = RecipeRunner::toBatchOperations($recipe);
      $progress_bar = $io->createProgressBar();
      $progress_bar->setFormat("%current%/%max% [%bar%]\n%message%\n");
      $progress_bar->setMessage($this->toPlainString(t('Applying recipe')));
      $progress_bar->start(count($steps));
      /** @var array{message?: \Stringable|string, results: array{module?: string[], theme?: string[], content?: string[], recipe?: string[]}} $context */
      $context = [
        'results' => [],
      ];
      foreach ($steps as $step) {
        call_user_func_array($step[0], array_merge($step[1], [
          &$context,
        ]));
        if (isset($context['message'])) {
          $progress_bar->setMessage($this->toPlainString($context['message']));
        }
        unset($context['message']);
        $progress_bar->advance();
      }
      if ($io->isVerbose()) {
        if (!empty($context['results']['module'])) {
          $io->section($this->toPlainString(t('Modules installed')));
          $modules = array_map(fn($module) => \Drupal::service('extension.list.module')->getName($module), $context['results']['module']);
          sort($modules, SORT_NATURAL);
          $io->listing($modules);
        }
        if (!empty($context['results']['theme'])) {
          $io->section($this->toPlainString(t('Themes installed')));
          $themes = array_map(fn($theme) => \Drupal::service('extension.list.theme')->getName($theme), $context['results']['theme']);
          sort($themes, SORT_NATURAL);
          $io->listing($themes);
        }
        if (!empty($context['results']['content'])) {
          $io->section($this->toPlainString(t('Content created for recipes')));
          $io->listing($context['results']['content']);
        }
        if (!empty($context['results']['recipe'])) {
          $io->section($this->toPlainString(t('Recipes applied')));
          $io->listing($context['results']['recipe']);
        }
      }
      $io->success($this->toPlainString(t('%recipe applied successfully', [
        '%recipe' => $recipe->name,
      ])));
      return 0;
    } catch (\Throwable $e) {
      try {
        $this->rollBackToCheckpoint($backup_checkpoint);
      } catch (ConfigImporterException $importer_exception) {
        $io->error($importer_exception->getMessage());
      }
      throw $e;
    }
  }
  
  /**
   * Converts a stringable like TranslatableMarkup to a plain text string.
   *
   * @param \Stringable|string $text
   *   The string to convert.
   *
   * @return string
   *   The plain text string.
   */
  private function toPlainString(\Stringable|string $text) : string {
    return PlainTextOutput::renderFromHtml((string) $text);
  }
  
  /**
   * Rolls config back to a particular checkpoint.
   *
   * @param \Drupal\Core\Config\Checkpoint\Checkpoint $checkpoint
   *   The checkpoint to roll back to.
   */
  private function rollBackToCheckpoint(Checkpoint $checkpoint) : void {
    $container = \Drupal::getContainer();
    /** @var \Drupal\Core\Config\Checkpoint\CheckpointStorageInterface $checkpoint_storage */
    $checkpoint_storage = $container->get('config.storage.checkpoint');
    $checkpoint_storage->setCheckpointToReadFrom($checkpoint);
    $storage_comparer = new StorageComparer($checkpoint_storage, $container->get('config.storage'));
    $storage_comparer->reset();
    $config_importer = new ConfigImporter($storage_comparer, $container->get('event_dispatcher'), $container->get('config.manager'), $container->get('lock'), $container->get('config.typed'), $container->get('module_handler'), $container->get('module_installer'), $container->get('theme_handler'), $container->get('string_translation'), $container->get('extension.list.module'), $container->get('extension.list.theme'));
    $config_importer->import();
  }
  
  /**
   * Boots up a Drupal environment.
   *
   * @return \Drupal\Core\DrupalKernelInterface
   *   The Drupal kernel.
   *
   * @throws \Exception
   *   Exception thrown if kernel does not boot.
   */
  protected function boot() {
    $kernel = new DrupalKernel('prod', $this->classLoader);
    $kernel::bootEnvironment();
    $kernel->setSitePath($this->getSitePath());
    Settings::initialize($kernel->getAppRoot(), $kernel->getSitePath(), $this->classLoader);
    $kernel->boot();
    $kernel->preHandle(Request::createFromGlobals());
    return $kernel;
  }
  
  /**
   * Gets the site path.
   *
   * Defaults to 'sites/default'. For testing purposes this can be overridden
   * using the DRUPAL_DEV_SITE_PATH environment variable.
   *
   * @return string
   *   The site path to use.
   */
  protected function getSitePath() {
    return getenv('DRUPAL_DEV_SITE_PATH') ?: 'sites/default';
  }

}

Members

Title Sort descending Modifiers Object type Summary
RecipeCommand::$classLoader protected property The class loader.
RecipeCommand::boot protected function Boots up a Drupal environment.
RecipeCommand::configure protected function
RecipeCommand::execute protected function
RecipeCommand::getSitePath protected function Gets the site path.
RecipeCommand::rollBackToCheckpoint private function Rolls config back to a particular checkpoint.
RecipeCommand::toPlainString private function Converts a stringable like TranslatableMarkup to a plain text string.
RecipeCommand::__construct public function Constructs a new RecipeCommand command.

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.