InstallCommand.php
Same filename in other branches
Namespace
Drupal\Core\CommandFile
-
core/
lib/ Drupal/ Core/ Command/ InstallCommand.php
View source
<?php
namespace Drupal\Core\Command;
use Drupal\Component\Utility\Crypt;
use Drupal\Core\Database\ConnectionNotDefinedException;
use Drupal\Core\Database\Database;
use Drupal\Core\DrupalKernel;
use Drupal\Core\Extension\ExtensionDiscovery;
use Drupal\Core\Extension\InfoParserDynamic;
use Drupal\Core\Site\Settings;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
/**
* Installs a Drupal site for local testing/development.
*
* @internal
* This command makes no guarantee of an API for Drupal extensions.
*/
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-or-recipe', InputArgument::OPTIONAL, 'Install profile or recipe directory from which to install the site.')
->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')
->addUsage('core/recipes/standard --site-name RecipeBuiltSite');
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_or_recipe = $input->getArgument('install-profile-or-recipe');
if (!$install_profile_or_recipe) {
// User did not provide a recipe or install profile.
$install_profile = $this->selectProfile($io);
}
elseif ($this->validateProfile($install_profile_or_recipe)) {
// User provided an install profile.
$install_profile = $install_profile_or_recipe;
}
elseif ($this->validateRecipe($install_profile_or_recipe)) {
// User provided a recipe.
$recipe = $install_profile_or_recipe;
}
else {
$error_msg = sprintf("'%s' is not a valid install profile or recipe.", $install_profile_or_recipe);
// If it does not look like a path make suggestions based upon available
// profiles.
if (!str_contains('/', $install_profile_or_recipe)) {
$alternatives = [];
foreach (array_keys($this->getProfiles(TRUE, FALSE)) as $profile_name) {
$lev = levenshtein($install_profile_or_recipe, $profile_name);
if ($lev <= strlen($profile_name) / 4 || str_contains($profile_name, $install_profile_or_recipe)) {
$alternatives[] = $profile_name;
}
}
if (!empty($alternatives)) {
$error_msg .= sprintf(" Did you mean '%s'?", implode("' or '", $alternatives));
}
}
$io->getErrorStyle()
->error($error_msg);
return 1;
}
return $this->install($this->classLoader, $io, $install_profile ?? '', $input->getOption('langcode'), $this->getSitePath(), $input->getOption('site-name'), $recipe ?? '');
}
/**
* 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) {
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.
* @param string $recipe
* The recipe to use for installing.
*
* @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, string $recipe) {
$sqliteDriverNamespace = 'Drupal\\sqlite\\Driver\\Database\\sqlite';
$password = Crypt::randomBytesBase64(12);
$parameters = [
'interactive' => FALSE,
'site_path' => $site_path,
'parameters' => [
'profile' => $profile,
'langcode' => $langcode,
],
'forms' => [
'install_settings_form' => [
'driver' => $sqliteDriverNamespace,
$sqliteDriverNamespace => [
'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,
],
],
];
if ($recipe) {
$parameters['parameters']['recipe'] = $recipe;
}
// 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.
*
* @return bool
* TRUE if the profile is valid, FALSE if not.
*/
protected function validateProfile($install_profile) : bool {
// Allow people to install hidden and non-distribution profiles if they
// supply the argument.
return array_key_exists($install_profile, $this->getProfiles(TRUE, FALSE));
}
/**
* Validates a user provided recipe.
*
* @param string $recipe
* The path to the recipe to validate.
*
* @return bool
* TRUE if the recipe exists, FALSE if not.
*/
protected function validateRecipe(string $recipe) : bool {
// It is impossible to validate a recipe fully at this point because that
// requires a container.
if (!is_dir($recipe) || !is_file($recipe . '/recipe.yml')) {
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;
}
}
Classes
Title | Deprecated | Summary |
---|---|---|
InstallCommand | Installs a Drupal site for local testing/development. |
Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.