You are here

class InstallProfile in Helper 8

Helpers related to working with install profiles.

Hierarchy

Expanded class hierarchy of InstallProfile

1 file declares its use of InstallProfile
InstallProfileCommands.php in src/Commands/InstallProfileCommands.php
1 string reference to 'InstallProfile'
helper.services.yml in ./helper.services.yml
helper.services.yml
1 service uses InstallProfile
helper.install_profile in ./helper.services.yml
Drupal\helper\InstallProfile

File

src/InstallProfile.php, line 16

Namespace

Drupal\helper
View source
class InstallProfile {
  use StringTranslationTrait;

  /**
   * The profile extension list service.
   *
   * @var \Drupal\Core\Extension\ExtensionList
   */
  protected $profileList;

  /**
   * The config factory.
   *
   * @var \Drupal\Core\Config\ConfigFactoryInterface
   */
  protected $configFactory;

  /**
   * The state service.
   *
   * @var \Drupal\Core\State\StateInterface
   */
  protected $state;

  /**
   * The module handler.
   *
   * @var \Drupal\Core\Extension\ModuleHandlerInterface
   */
  protected $moduleHandler;

  /**
   * The module schema key/value store.
   *
   * @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface
   */
  protected $schemaKeyValue;

  /**
   * InstallProfileSwitcher constructor.
   *
   * @param \Drupal\Core\Extension\ExtensionList $profileList
   *   The profile extension list service.
   * @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
   *   The config factory.
   * @param \Drupal\Core\State\StateInterface $state
   *   The state service.
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
   *   The module handler.
   * @param \Drupal\Core\KeyValueStore\KeyValueFactoryInterface $key_value_factory
   *   The key/value storage factory.
   */
  public function __construct(ExtensionList $profileList, ConfigFactoryInterface $configFactory, StateInterface $state, ModuleHandlerInterface $module_handler, KeyValueFactoryInterface $key_value_factory) {
    $this->profileList = $profileList;
    $this->configFactory = $configFactory;
    $this->state = $state;
    $this->moduleHandler = $module_handler;
    $this->schemaKeyValue = $key_value_factory
      ->get('system.schema');
  }

  /**
   * Switches the site's install profile.
   *
   * Note the following:
   * - This does not run the new profile's install hooks.
   * - Does not enable any of the profile's dependencies.
   * - This does not check that currently enabled modules are located in the
   *   current profile's code and will no longer be available once switching
   *   to the new profile.
   * - Does not run any uninstallation of the current profile, like removing
   *   any tables defined in hook_schema().
   *
   * @param string $profile
   *   The machine name of the profile to switch to.
   * @param int $schema_version
   *   The schema version to set for the install profile.
   *
   * @author https://www.drupal.org/project/profile_switcher
   */
  public function switch($profile, $schema_version = NULL) {
    $current_profile = \Drupal::installProfile();

    // Forces ExtensionDiscovery to rerun for profiles.
    $this->state
      ->delete('system.profile.files');

    // Set the profile in configuration.
    $extension_config = $this->configFactory
      ->getEditable('core.extension');
    $extension_config
      ->set('profile', $profile);
    $extension_config
      ->save();
    drupal_flush_all_caches();

    // Install profiles are also registered as enabled modules.
    // Remove the old profile and add in the new one.
    $extension_config
      ->clear("module.{$current_profile}");

    // The install profile is always given a weight of 1000 by the core
    // extension system.
    $extension_config
      ->set("module.{$profile}", 1000);
    $extension_config
      ->save();

    // Clear caches again.
    drupal_flush_all_caches();

    // Find the correct schema version of the install profile to set as the
    // current schema version. Assume use the latest schema version if a value
    // has not been provided.
    $schema_version = $schema_version ?: $this
      ->getLatestSchemaVersion($profile);

    // Remove the schema value for the old install profile, and set the schema
    // for the new one.
    $this->schemaKeyValue
      ->delete($current_profile);
    $this->schemaKeyValue
      ->set($profile, $schema_version);

    // Clear caches again.
    drupal_flush_all_caches();
  }

  /**
   * Validates a profile and checks its various requirements.
   *
   * This method performs the following validations:
   * - Does the profile exist?
   * - Does the profile have all of its module dependencies currently enabled?
   * - Does the profile or any of its dependencies fail any hook_requirements()
   *   checks with errors?
   * - Does the current profile have any enabled modules or themes inside of it
   *   that will go "missing" when switching to the new profile?
   *
   * @param string $profile
   *   The profile name.
   *
   * @throws \InvalidArgumentException
   *   If the profile does not exist.
   * @throws \Drupal\Core\Installer\Exception\InstallerException
   *   If the profile failed any validations.
   */
  public function validateProfile($profile) {

    // Ensure the profile exists.
    if (!$this->profileList
      ->exists($profile)) {
      throw new \InvalidArgumentException("The {$profile} profile does not exist.");
    }

    // Ensure that the desired profile is different from the current profile.
    if ($profile === \Drupal::installProfile()) {
      throw new \InvalidArgumentException("The current install profile is already set to {$profile}.");
    }

    // Make sure the installation API is available.
    include_once DRUPAL_ROOT . '/core/includes/install.inc';

    // Check that the profile's dependencies are already enabled.
    $info = install_profile_info($profile);
    if (!empty($info['dependencies']) && ($missing_dependencies = array_diff($info['dependencies'], array_keys($this->moduleHandler
      ->getModuleList())))) {
      sort($missing_dependencies);
      throw new InstallerException("The following module dependencies are not enabled or are missing for the {$profile} profile: " . implode(', ', $missing_dependencies) . '.');
    }

    // Ensure that the profile's .install file and classes are available, since
    // drupal_check_profile only does this for the install profile's
    // dependencies, but not the install profile itself.
    $profile_path = drupal_get_path('profile', $profile);
    $profile_install_file = DRUPAL_ROOT . '/' . $profile_path . "/{$profile}.install";
    \Drupal::service('class_loader')
      ->addPsr4('Drupal\\' . $profile_path . '\\', \Drupal::root() . "/{$profile_path}/src");
    if (is_file($profile_install_file)) {
      require_once $profile_install_file;
    }

    // Ensure that requirements are checked for the new profile.
    $requirements = drupal_check_profile($profile);
    if (is_array($requirements) && drupal_requirements_severity($requirements) === REQUIREMENT_ERROR) {
      $reasons = $this
        ->formatRequirementsReasons($requirements);
      throw new InstallerException("Failed requirements check for the {$profile} profile:\n" . implode("\n", $reasons));
    }

    // Check for any modules or themes inside the profile that are enabled and
    // may not be available in the new profile.
    $this
      ->checkEnabledProfileExtensions($profile, 'module');
    $this
      ->checkEnabledProfileExtensions($profile, 'theme');
  }

  /**
   * Gets the latest schema version for an install profile.
   *
   * @param string $profile
   *   The install profile name.
   *
   * @return int
   *   The schema version.
   */
  protected function getLatestSchemaVersion($profile) {
    module_load_install($profile);
    $versions = drupal_get_schema_versions($profile) ?? [];
    $versions[] = $this->moduleHandler
      ->invoke($profile, 'update_last_removed') ?? \Drupal::CORE_MINIMUM_SCHEMA_VERSION;
    return max($versions);
  }

  /**
   * Convert an array of requirements into a simpler list of reasons.
   *
   * @param array $requirements
   *   An array of requirements from hook_requirements().
   *
   * @return string[]
   *   An array of requirement reasons as strings.
   */
  protected function formatRequirementsReasons(array $requirements) {
    $reasons = [];
    foreach ($requirements as $id => $requirement) {
      if (isset($requirement['severity']) && $requirement['severity'] === REQUIREMENT_ERROR) {
        if (isset($requirement['value']) && $requirement['value']) {
          $reasons[$id] = $this
            ->t('@requirements_message (Currently using @item version @version)', [
            '@requirements_message' => $requirement['description'],
            '@item' => $requirement['title'],
            '@version' => $requirement['value'],
          ]);
        }
        else {
          $reasons[$id] = (string) $requirement['description'];
        }
      }
    }
    return $reasons;
  }

  /**
   * Check for any enabled extensions that reside inside the current profile.
   *
   * @param string $profile
   *   The new desired profile.
   * @param string $type
   *   The extension type. Possible values are module or theme.
   *
   * @throws \Drupal\Core\Installer\Exception\InstallerException
   *   If there are extensions that enabled in the currnet profile that do not
   *   also reside in the new profile.
   */
  protected function checkEnabledProfileExtensions($profile, $type) {
    $current_profile = \Drupal::installProfile();
    $current_profile_path = $this->profileList
      ->getPath($current_profile);
    $new_profile_path = $this->profileList
      ->getPath($profile);

    /** @var \Drupal\Core\Extension\ExtensionList $extensionList */
    $extensionList = \Drupal::service('extension.list.' . $type);
    $extensions = array_keys($extensionList
      ->getAllInstalledInfo());
    $missing_extensions = [];
    foreach ($extensions as $extension) {

      // Skip the current profile (which also acts as a module) since it is not
      // relevant.
      if ($type === 'module' && $extension === $current_profile) {
        continue;
      }

      // If the extension lives inside the current profile but cannot be found
      // in the new profile, then add it to the list.
      // @todo Convert this to use ExtensionDiscovery using the new profile path like drupal_check_profile() does.
      if (strpos($extensionList
        ->getPath($extension), $current_profile_path) !== FALSE && !is_dir($new_profile_path . '/' . $type . '/' . $extension)) {
        $missing_extensions[] = $extension;
      }
    }

    // Throw an exception if there will be any missing extensions.
    if (!empty($missing_extensions)) {
      sort($missing_extensions);
      throw new InstallerException("The following {$type}s are located inside the current {$current_profile} profile and may not be available when switching to the {$profile} profile: " . implode(', ', $missing_extensions) . ".");
    }
  }

}

Members

Namesort descending Modifiers Type Description Overrides
InstallProfile::$configFactory protected property The config factory.
InstallProfile::$moduleHandler protected property The module handler.
InstallProfile::$profileList protected property The profile extension list service.
InstallProfile::$schemaKeyValue protected property The module schema key/value store.
InstallProfile::$state protected property The state service.
InstallProfile::checkEnabledProfileExtensions protected function Check for any enabled extensions that reside inside the current profile.
InstallProfile::formatRequirementsReasons protected function Convert an array of requirements into a simpler list of reasons.
InstallProfile::getLatestSchemaVersion protected function Gets the latest schema version for an install profile.
InstallProfile::switch public function Switches the site's install profile.
InstallProfile::validateProfile public function Validates a profile and checks its various requirements.
InstallProfile::__construct public function InstallProfileSwitcher constructor.
StringTranslationTrait::$stringTranslation protected property The string translation service. 1
StringTranslationTrait::formatPlural protected function Formats a string containing a count of items.
StringTranslationTrait::getNumberOfPlurals protected function Returns the number of plurals supported by a given language.
StringTranslationTrait::getStringTranslation protected function Gets the string translation service.
StringTranslationTrait::setStringTranslation public function Sets the string translation service to use. 2
StringTranslationTrait::t protected function Translates a string to the current language or to a given language.