You are here

class InstallCommand in Drupal 10

Same name and namespace in other branches
  1. 8 core/lib/Drupal/Core/Command/InstallCommand.php \Drupal\Core\Command\InstallCommand
  2. 9 core/lib/Drupal/Core/Command/InstallCommand.php \Drupal\Core\Command\InstallCommand

Installs a Drupal site for local testing/development.

@internal This command makes no guarantee of an API for Drupal extensions.

Hierarchy

  • class \Drupal\Core\Command\InstallCommand extends \Symfony\Component\Console\Command\Command

Expanded class hierarchy of InstallCommand

File

core/lib/Drupal/Core/Command/InstallCommand.php, line 25

Namespace

Drupal\Core\Command
View source
class InstallCommand extends Command {

  /**
   * The class loader.
   *
   * @var object
   */
  protected $classLoader;

  /**
   * Constructs a new InstallCommand command.
   *
   * @param object $class_loader
   *   The class loader.
   */
  public function __construct($class_loader) {
    parent::__construct('install');
    $this->classLoader = $class_loader;
  }

  /**
   * {@inheritdoc}
   */
  protected function configure() {
    $this
      ->setName('install')
      ->setDescription('Installs a Drupal demo site. This is not meant for production and might be too simple for custom development. It is a quick and easy way to get Drupal running.')
      ->addArgument('install-profile', InputArgument::OPTIONAL, 'Install profile to install the site in.')
      ->addOption('langcode', NULL, InputOption::VALUE_OPTIONAL, 'The language to install the site in.', 'en')
      ->addOption('site-name', NULL, InputOption::VALUE_OPTIONAL, 'Set the site name.', 'Drupal')
      ->addUsage('demo_umami --langcode fr')
      ->addUsage('standard --site-name QuickInstall');
    parent::configure();
  }

  /**
   * {@inheritdoc}
   */
  protected function execute(InputInterface $input, OutputInterface $output) : int {
    $io = new SymfonyStyle($input, $output);
    if (!extension_loaded('pdo_sqlite')) {
      $io
        ->getErrorStyle()
        ->error('You must have the pdo_sqlite PHP extension installed. See core/INSTALL.sqlite.txt for instructions.');
      return 1;
    }

    // Change the directory to the Drupal root.
    chdir(dirname(__DIR__, 5));

    // Check whether there is already an installation.
    if ($this
      ->isDrupalInstalled()) {

      // Do not fail if the site is already installed so this command can be
      // chained with ServerCommand.
      $output
        ->writeln('<info>Drupal is already installed.</info> If you want to reinstall, remove sites/default/files and sites/default/settings.php.');
      return 0;
    }
    $install_profile = $input
      ->getArgument('install-profile');
    if ($install_profile && !$this
      ->validateProfile($install_profile, $io)) {
      return 1;
    }
    if (!$install_profile) {
      $install_profile = $this
        ->selectProfile($io);
    }
    return $this
      ->install($this->classLoader, $io, $install_profile, $input
      ->getOption('langcode'), $this
      ->getSitePath(), $input
      ->getOption('site-name'));
  }

  /**
   * Returns whether there is already an existing Drupal installation.
   *
   * @return bool
   */
  protected function isDrupalInstalled() {
    try {
      $kernel = new DrupalKernel('prod', $this->classLoader, FALSE);
      $kernel::bootEnvironment();
      $kernel
        ->setSitePath($this
        ->getSitePath());
      Settings::initialize($kernel
        ->getAppRoot(), $kernel
        ->getSitePath(), $this->classLoader);
      $kernel
        ->boot();
    } catch (ConnectionNotDefinedException $e) {
      return FALSE;
    }
    return !empty(Database::getConnectionInfo());
  }

  /**
   * Installs Drupal with specified installation profile.
   *
   * @param object $class_loader
   *   The class loader.
   * @param \Symfony\Component\Console\Style\SymfonyStyle $io
   *   The Symfony output decorator.
   * @param string $profile
   *   The installation profile to use.
   * @param string $langcode
   *   The language to install the site in.
   * @param string $site_path
   *   The path to install the site to, like 'sites/default'.
   * @param string $site_name
   *   The site name.
   *
   * @throws \Exception
   *   Thrown when failing to create the $site_path directory or settings.php.
   *
   * @return int
   *   The command exit status.
   */
  protected function install($class_loader, SymfonyStyle $io, $profile, $langcode, $site_path, $site_name) {
    $password = Crypt::randomBytesBase64(12);
    $parameters = [
      'interactive' => FALSE,
      'site_path' => $site_path,
      'parameters' => [
        'profile' => $profile,
        'langcode' => $langcode,
      ],
      'forms' => [
        'install_settings_form' => [
          'driver' => 'sqlite',
          'sqlite' => [
            'database' => $site_path . '/files/.sqlite',
          ],
        ],
        'install_configure_form' => [
          'site_name' => $site_name,
          'site_mail' => 'drupal@localhost',
          'account' => [
            'name' => 'admin',
            'mail' => 'admin@localhost',
            'pass' => [
              'pass1' => $password,
              'pass2' => $password,
            ],
          ],
          'enable_update_status_module' => TRUE,
          // \Drupal\Core\Render\Element\Checkboxes::valueCallback() requires
          // NULL instead of FALSE values for programmatic form submissions to
          // disable a checkbox.
          'enable_update_status_emails' => NULL,
        ],
      ],
    ];

    // Create the directory and settings.php if not there so that the installer
    // works.
    if (!is_dir($site_path)) {
      if ($io
        ->isVerbose()) {
        $io
          ->writeln("Creating directory: {$site_path}");
      }
      if (!mkdir($site_path, 0775)) {
        throw new \RuntimeException("Failed to create directory {$site_path}");
      }
    }
    if (!file_exists("{$site_path}/settings.php")) {
      if ($io
        ->isVerbose()) {
        $io
          ->writeln("Creating file: {$site_path}/settings.php");
      }
      if (!copy('sites/default/default.settings.php', "{$site_path}/settings.php")) {
        throw new \RuntimeException("Copying sites/default/default.settings.php to {$site_path}/settings.php failed.");
      }
    }
    require_once 'core/includes/install.core.inc';
    $progress_bar = $io
      ->createProgressBar();
    install_drupal($class_loader, $parameters, function ($install_state) use ($progress_bar) {
      static $started = FALSE;
      if (!$started) {
        $started = TRUE;

        // We've already done 1.
        $progress_bar
          ->setFormat("%current%/%max% [%bar%]\n%message%\n");
        $progress_bar
          ->setMessage(t('Installing @drupal', [
          '@drupal' => drupal_install_profile_distribution_name(),
        ]));
        $tasks = install_tasks($install_state);
        $progress_bar
          ->start(count($tasks) + 1);
      }
      $tasks_to_perform = install_tasks_to_perform($install_state);
      $task = current($tasks_to_perform);
      if (isset($task['display_name'])) {
        $progress_bar
          ->setMessage($task['display_name']);
      }
      $progress_bar
        ->advance();
    });
    $success_message = t('Congratulations, you installed @drupal!', [
      '@drupal' => drupal_install_profile_distribution_name(),
      '@name' => 'admin',
      '@pass' => $password,
    ], [
      'langcode' => $langcode,
    ]);
    $progress_bar
      ->setMessage('<info>' . $success_message . '</info>');
    $progress_bar
      ->display();
    $progress_bar
      ->finish();
    $io
      ->writeln('<info>Username:</info> admin');
    $io
      ->writeln("<info>Password:</info> {$password}");
    return 0;
  }

  /**
   * 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';
  }

  /**
   * Selects the install profile to use.
   *
   * @param \Symfony\Component\Console\Style\SymfonyStyle $io
   *   Symfony style output decorator.
   *
   * @return string
   *   The selected install profile.
   *
   * @see _install_select_profile()
   * @see \Drupal\Core\Installer\Form\SelectProfileForm
   */
  protected function selectProfile(SymfonyStyle $io) {
    $profiles = $this
      ->getProfiles();

    // If there is a distribution there will be only one profile.
    if (count($profiles) == 1) {
      return key($profiles);
    }

    // Display alphabetically by human-readable name, but always put the core
    // profiles first (if they are present in the filesystem).
    natcasesort($profiles);
    if (isset($profiles['minimal'])) {

      // If the expert ("Minimal") core profile is present, put it in front of
      // any non-core profiles rather than including it with them
      // alphabetically, since the other profiles might be intended to group
      // together in a particular way.
      $profiles = [
        'minimal' => $profiles['minimal'],
      ] + $profiles;
    }
    if (isset($profiles['standard'])) {

      // If the default ("Standard") core profile is present, put it at the very
      // top of the list. This profile will have its radio button pre-selected,
      // so we want it to always appear at the top.
      $profiles = [
        'standard' => $profiles['standard'],
      ] + $profiles;
    }
    reset($profiles);
    return $io
      ->choice('Select an installation profile', $profiles, current($profiles));
  }

  /**
   * Validates a user provided install profile.
   *
   * @param string $install_profile
   *   Install profile to validate.
   * @param \Symfony\Component\Console\Style\SymfonyStyle $io
   *   Symfony style output decorator.
   *
   * @return bool
   *   TRUE if the profile is valid, FALSE if not.
   */
  protected function validateProfile($install_profile, SymfonyStyle $io) {

    // Allow people to install hidden and non-distribution profiles if they
    // supply the argument.
    $profiles = $this
      ->getProfiles(TRUE, FALSE);
    if (!isset($profiles[$install_profile])) {
      $error_msg = sprintf("'%s' is not a valid install profile.", $install_profile);
      $alternatives = [];
      foreach (array_keys($profiles) as $profile_name) {
        $lev = levenshtein($install_profile, $profile_name);
        if ($lev <= strlen($profile_name) / 4 || FALSE !== strpos($profile_name, $install_profile)) {
          $alternatives[] = $profile_name;
        }
      }
      if (!empty($alternatives)) {
        $error_msg .= sprintf(" Did you mean '%s'?", implode("' or '", $alternatives));
      }
      $io
        ->getErrorStyle()
        ->error($error_msg);
      return FALSE;
    }
    return TRUE;
  }

  /**
   * Gets a list of profiles.
   *
   * @param bool $include_hidden
   *   (optional) Whether to include hidden profiles. Defaults to FALSE.
   * @param bool $auto_select_distributions
   *   (optional) Whether to only return the first distribution found.
   *
   * @return string[]
   *   An array of profile descriptions keyed by the profile machine name.
   */
  protected function getProfiles($include_hidden = FALSE, $auto_select_distributions = TRUE) {

    // Build a list of all available profiles.
    $listing = new ExtensionDiscovery(getcwd(), FALSE);
    $listing
      ->setProfileDirectories([]);
    $profiles = [];
    $info_parser = new InfoParserDynamic(getcwd());
    foreach ($listing
      ->scan('profile') as $profile) {
      $details = $info_parser
        ->parse($profile
        ->getPathname());

      // Don't show hidden profiles.
      if (!$include_hidden && !empty($details['hidden'])) {
        continue;
      }

      // Determine the name of the profile; default to the internal name if none
      // is specified.
      $name = $details['name'] ?? $profile
        ->getName();
      $description = $details['description'] ?? $name;
      $profiles[$profile
        ->getName()] = $description;
      if ($auto_select_distributions && !empty($details['distribution'])) {
        return [
          $profile
            ->getName() => $description,
        ];
      }
    }
    return $profiles;
  }

}

Members