You are here

FeaturesManager.php in Features 8.4

Same filename and directory in other branches
  1. 8.3 src/FeaturesManager.php

Namespace

Drupal\features

File

src/FeaturesManager.php
View source
<?php

namespace Drupal\features;

use Drupal\Core\Config\ImmutableConfig;
use Drupal;
use Drupal\Component\Serialization\Yaml;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\ConfigManagerInterface;
use Drupal\Core\Config\InstallStorage;
use Drupal\Core\Config\StorageInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Extension\Dependency;
use Drupal\Core\Extension\Extension;
use Drupal\Core\Extension\ExtensionDiscovery;
use Drupal\Core\Extension\ModuleExtensionList;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\config_update\ConfigRevertInterface;

/**
 * The FeaturesManager provides helper functions for building packages.
 */
class FeaturesManager implements FeaturesManagerInterface {
  use StringTranslationTrait;

  /**
   * The entity type manager.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;

  /**
   * The target storage.
   *
   * @var \Drupal\Core\Config\StorageInterface
   */
  protected $configStorage;

  /**
   * The extension storages.
   *
   * @var \Drupal\features\FeaturesExtensionStoragesByDirectoryInterface
   */
  protected $extensionStorages;

  /**
   * The configuration manager.
   *
   * @var \Drupal\Core\Config\ConfigManagerInterface
   */
  protected $configManager;

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

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

  /**
   * The config reverter.
   *
   * @var \Drupal\config_update\ConfigRevertInterface
   */
  protected $configReverter;

  /**
   * The module extension list service.
   *
   * @var \Drupal\Core\Extension\ModuleExtensionList
   */
  protected $moduleExtensionList;

  /**
   * The Features settings.
   *
   * @var array
   */
  protected $settings;

  /**
   * The app root.
   *
   * @var string
   */
  protected $root;

  /**
   * The configuration present on the site.
   *
   * @var \Drupal\features\ConfigurationItem[]
   */
  private $configCollection;

  /**
   * The packages to be generated.
   *
   * @var \Drupal\features\Package[]
   */
  protected $packages;

  /**
   * Whether the packages have been assigned a bundle prefix.
   *
   * @var bool
   */
  protected $packagesPrefixed;

  /**
   * The package assigner.
   *
   * @var \Drupal\features\FeaturesAssigner
   */
  protected $assigner;

  /**
   * Cache module.features.yml data keyed by module name.
   *
   * @var array
   */
  protected $featureInfoCache;

  /**
   * Constructs a FeaturesManager object.
   *
   * @param string $root
   *   The app root.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The entity type manager.
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
   *   The configuration factory.
   * @param \Drupal\Core\Config\StorageInterface $config_storage
   *   The target storage.
   * @param \Drupal\Core\Config\ConfigManagerInterface $config_manager
   *   The configuration manager.
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
   *   The module handler.
   * @param \Drupal\config_update\ConfigRevertInterface $config_reverter
   *   The config revert interface.
   * @param \Drupal\Core\Extension\ModuleExtensionList $module_extension_list
   *   The module extension list service.
   */
  public function __construct($root, EntityTypeManagerInterface $entity_type_manager, ConfigFactoryInterface $config_factory, StorageInterface $config_storage, ConfigManagerInterface $config_manager, ModuleHandlerInterface $module_handler, ConfigRevertInterface $config_reverter, ModuleExtensionList $module_extension_list) {
    $this->root = $root;
    $this->entityTypeManager = $entity_type_manager;
    $this->configStorage = $config_storage;
    $this->configManager = $config_manager;
    $this->moduleHandler = $module_handler;
    $this->configFactory = $config_factory;
    $this->configReverter = $config_reverter;
    $this->moduleExtensionList = $module_extension_list;
    $this->settings = $config_factory
      ->getEditable('features.settings');
    $this->extensionStorages = new FeaturesExtensionStoragesByDirectory($this->configStorage);
    $this->extensionStorages
      ->addStorage(InstallStorage::CONFIG_INSTALL_DIRECTORY);
    $this->extensionStorages
      ->addStorage(InstallStorage::CONFIG_OPTIONAL_DIRECTORY);
    $this->packages = [];
    $this->packagesPrefixed = FALSE;
    $this->configCollection = [];
  }

  /**
   * {@inheritdoc}
   */
  public function setRoot($root) {
    $this->root = $root;

    // Clear cache.
    $this->featureInfoCache = [];
  }

  /**
   * {@inheritdoc}
   */
  public function getActiveStorage() {
    return $this->configStorage;
  }

  /**
   * {@inheritdoc}
   */
  public function getExtensionStorages() {
    return $this->extensionStorages;
  }

  /**
   * {@inheritdoc}
   */
  public function getFullName($type, $name) {
    if ($type == FeaturesManagerInterface::SYSTEM_SIMPLE_CONFIG || !$type) {
      return $name;
    }
    $definition = $this->entityTypeManager
      ->getDefinition($type);
    $prefix = $definition
      ->getConfigPrefix() . '.';
    return $prefix . $name;
  }

  /**
   * {@inheritdoc}
   */
  public function getConfigType($fullname) {
    $result = [
      'type' => '',
      'name_short' => '',
    ];
    $prefix = FeaturesManagerInterface::SYSTEM_SIMPLE_CONFIG . '.';
    if (strpos($fullname, $prefix) !== FALSE) {
      $result['type'] = FeaturesManagerInterface::SYSTEM_SIMPLE_CONFIG;
      $result['name_short'] = substr($fullname, strlen($prefix));
    }
    else {
      foreach ($this->entityTypeManager
        ->getDefinitions() as $entity_type => $definition) {
        if ($definition
          ->entityClassImplements('Drupal\\Core\\Config\\Entity\\ConfigEntityInterface')) {
          $prefix = $definition
            ->getConfigPrefix() . '.';
          if (strpos($fullname, $prefix) === 0) {
            $result['type'] = $entity_type;
            $result['name_short'] = substr($fullname, strlen($prefix));
          }
        }
      }
    }
    return $result;
  }

  /**
   * {@inheritdoc}
   */
  public function reset() {
    $this->packages = [];

    // Don't use getConfigCollection because reset() may be called in
    // cases where we don't need to load config.
    foreach ($this->configCollection as $config) {
      $config
        ->setPackage(NULL);
    }
  }

  /**
   * {@inheritdoc}
   */
  public function getConfigCollection($reset = FALSE) {
    $this
      ->initConfigCollection($reset);
    return $this->configCollection;
  }

  /**
   * {@inheritdoc}
   */
  public function setConfigCollection(array $config_collection) {
    $this->configCollection = $config_collection;
  }

  /**
   * {@inheritdoc}
   */
  public function getPackages() {
    return $this->packages;
  }

  /**
   * {@inheritdoc}
   */
  public function setPackages(array $packages) {
    $this->packages = $packages;
  }

  /**
   * {@inheritdoc}
   */
  public function getPackage($machine_name) {
    if (isset($this->packages[$machine_name])) {
      return $this->packages[$machine_name];
    }
    return NULL;
  }

  /**
   * {@inheritdoc}
   */
  public function findPackage($machine_name) {
    $result = $this
      ->getPackage($machine_name);
    if (!isset($result)) {

      // Didn't find direct match, but now go through and look for matching
      // full name (bundle_machinename)
      foreach ($this->packages as $name => $package) {
        if ($package
          ->getFullName() == $machine_name) {
          return $this->packages[$name];
        }
      }
    }
    return $result;
  }

  /**
   * {@inheritdoc}
   */
  public function setPackage(Package $package) {
    if ($package
      ->getMachineName()) {
      $this->packages[$package
        ->getMachineName()] = $package;
    }
  }

  /**
   * {@inheritdoc}
   */
  public function loadPackage($module_name, $any = FALSE) {
    $package = $this
      ->getPackage($module_name);

    // Load directly from module if packages are not loaded or
    // if we want to include ANY module regardless of its a feature.
    if ((empty($this->packages) || $any) && !isset($package)) {
      $module_list = $this->moduleHandler
        ->getModuleList();
      if (!empty($module_list[$module_name])) {
        $extension = $module_list[$module_name];
        $package = $this
          ->initPackageFromExtension($extension);
        $config = $this
          ->listExtensionConfig($extension);
        $package
          ->setConfigOrig($config);
        $package
          ->setStatus(FeaturesManagerInterface::STATUS_INSTALLED);
      }
    }
    return $package;
  }

  /**
   * {@inheritdoc}
   */
  public function filterPackages(array $packages, $namespace = '', $only_exported = FALSE) {
    $result = [];

    /** @var \Drupal\features\Package $package */
    foreach ($packages as $key => $package) {

      // A package matches the namespace if:
      // - it's prefixed with the namespace, or
      // - it's assigned to a bundle named for the namespace, or
      // - the namespace is the default bundle and it has an empty bundle, and
      // - we're not removing only exported packages, or
      // - we are removing only exported packages and it's not exported.
      if ((strpos($package
        ->getMachineName(), $namespace . '_') === 0 || $package
        ->getBundle() && $package
        ->getBundle() === $namespace || $namespace === FeaturesBundleInterface::DEFAULT_BUNDLE && empty($package
        ->getBundle())) && (!$only_exported || $package
        ->getStatus() === FeaturesManagerInterface::STATUS_NO_EXPORT)) {
        $result[$key] = $package;
      }
    }
    return $result;
  }

  /**
   * {@inheritdoc}
   */
  public function getAssigner() {
    if (empty($this->assigner)) {
      $this
        ->setAssigner(\Drupal::service('features_assigner'));
    }
    return $this->assigner;
  }

  /**
   * {@inheritdoc}
   */
  public function setAssigner(FeaturesAssignerInterface $assigner) {
    $this->assigner = $assigner;
    $this
      ->reset();
  }

  /**
   * {@inheritdoc}
   */
  public function getGenerator() {
    return $this->generator;
  }

  /**
   * {@inheritdoc}
   */
  public function setGenerator(FeaturesGeneratorInterface $generator) {
    $this->generator = $generator;
  }

  /**
   * {@inheritdoc}
   */
  public function getExportSettings() {
    return $this->settings
      ->get('export');
  }

  /**
   * {@inheritdoc}
   */
  public function getSettings() {
    return $this->settings;
  }

  /**
   * {@inheritdoc}
   */
  public function getExtensionInfo(Extension $extension) {
    return Yaml::decode(file_get_contents($this->root . '/' . $extension
      ->getPathname()));
  }

  /**
   * {@inheritdoc}
   */
  public function isFeatureModule(Extension $module, FeaturesBundleInterface $bundle = NULL) {
    if ($features_info = $this
      ->getFeaturesInfo($module)) {

      // If no bundle was requested, it's enough that this is a feature.
      if (is_null($bundle)) {
        return TRUE;
      }
      elseif ($bundle
        ->isDefault()) {
        return !isset($features_info['bundle']);
      }
      else {
        return isset($features_info['bundle']) && $features_info['bundle'] == $bundle
          ->getMachineName();
      }
    }
    return FALSE;
  }

  /**
   * {@inheritdoc}
   */
  public function listPackageDirectories(array $machine_names = [], FeaturesBundleInterface $bundle = NULL) {
    if (empty($machine_names)) {
      $machine_names = array_keys($this
        ->getPackages());
    }

    // If the bundle is a profile, then add the profile's machine name.
    if (isset($bundle) && $bundle
      ->isProfile() && !in_array($bundle
      ->getProfileName(), $machine_names)) {
      $machine_names[] = $bundle
        ->getProfileName();
    }

    // If we are checking the default bundle, return all features.
    if (isset($bundle) && $bundle
      ->isDefault()) {
      $bundle = NULL;
    }
    $modules = $this
      ->getFeaturesModules($bundle);

    // Filter to include only the requested packages.
    $modules = array_filter($modules, function ($module) use ($bundle, $machine_names) {
      return in_array($module
        ->getName(), $machine_names);
    });
    $directories = [];
    foreach ($modules as $module) {
      $directories[$module
        ->getName()] = $module
        ->getPath();
    }
    return $directories;
  }

  /**
   * {@inheritdoc}
   */
  public function getAllModules() {
    static $modules;
    if (!isset($modules)) {

      // ModuleHandler::getModuleDirectories() returns data only for installed
      // modules. system_rebuild_module_data() includes only the site's install
      // profile directory, while we may need to include a custom profile.
      // @see _system_rebuild_module_data().
      $listing = new ExtensionDiscovery($this->root);
      $profile_directories = $listing
        ->setProfileDirectoriesFromSettings()
        ->getProfileDirectories();
      $installed_profile = $this
        ->drupalGetProfile();
      if (isset($bundle) && $bundle
        ->isProfile()) {
        $profile_directory = 'profiles/' . $bundle
          ->getProfileName();
        if ($bundle
          ->getProfileName() != $installed_profile && is_dir($profile_directory)) {
          $profile_directories[] = $profile_directory;
        }
      }
      $listing
        ->setProfileDirectories($profile_directories);

      // Find modules.
      $modules = $listing
        ->scan('module');

      // Find installation profiles.
      $profiles = $listing
        ->scan('profile');
      foreach ($profiles as $key => $profile) {
        $modules[$key] = $profile;
      }
    }
    return $modules;
  }

  /**
   * {@inheritdoc}
   */
  public function getFeaturesModules(FeaturesBundleInterface $bundle = NULL, $installed = FALSE) {
    $modules = $this
      ->getAllModules();

    // Filter by bundle.
    $features_manager = $this;
    $modules = array_filter($modules, function ($module) use ($features_manager, $bundle) {
      return $features_manager
        ->isFeatureModule($module, $bundle);
    });

    // Filtered by installed status.
    if ($installed) {
      $features_manager = $this;
      $modules = array_filter($modules, function ($extension) use ($features_manager) {
        return $features_manager
          ->extensionEnabled($extension);
      });
    }
    return $modules;
  }

  /**
   * {@inheritdoc}
   */
  public function extensionEnabled(Extension $extension) {
    return $this->moduleHandler
      ->moduleExists($extension
      ->getName());
  }

  /**
   * {@inheritdoc}
   */
  public function initPackage($machine_name, $name = NULL, $description = '', $type = 'module', FeaturesBundleInterface $bundle = NULL, Extension $extension = NULL) {
    if (isset($this->packages[$machine_name])) {
      return $this->packages[$machine_name];
    }
    elseif (isset($bundle) && isset($this->packages[$bundle
      ->getFullName($machine_name)])) {
      return $this->packages[$bundle
        ->getFullName($machine_name)];
    }
    return $this->packages[$machine_name] = $this
      ->getPackageObject($machine_name, $name, $description, $type, $bundle, $extension);
  }

  /**
   * {@inheritdoc}
   */
  public function initPackageFromExtension(Extension $extension) {
    $info = $this
      ->getExtensionInfo($extension);
    $features_info = $this
      ->getFeaturesInfo($extension);
    $bundle = $this
      ->getAssigner()
      ->findBundle($info, $features_info);

    // Use the full extension name as the short_name.  Important to allow
    // multiple modules with different namespaces such as oa_media, test_media.
    $short_name = $extension
      ->getName();
    return $this
      ->initPackage($short_name, $info['name'], !empty($info['description']) ? $info['description'] : '', $info['type'], $bundle, $extension);
  }

  /**
   * Helper function to update dependencies array for a specific config item.
   *
   * @param \Drupal\features\ConfigurationItem $config
   *   a config item.
   * @param array $module_list
   *
   * @return array $dependencies
   */
  protected function getConfigDependency(ConfigurationItem $config, array $module_list = []) {
    $dependencies = [];
    $type = $config
      ->getType();

    // For configuration in the InstallStorage::CONFIG_INSTALL_DIRECTORY
    // directory, set any dependencies of the configuration item as package
    // dependencies.
    // As its name implies, the core-provided
    // InstallStorage::CONFIG_OPTIONAL_DIRECTORY should not create
    // dependencies.
    if ($config
      ->getSubdirectory() === InstallStorage::CONFIG_INSTALL_DIRECTORY) {
      if ($type === FeaturesManagerInterface::SYSTEM_SIMPLE_CONFIG) {
        $dependencies[] = strtok($config
          ->getName(), '.');
      }
      else {
        $dependencies[] = $this->entityTypeManager
          ->getDefinition($type)
          ->getProvider();
      }
      if (isset($config
        ->getData()['dependencies']['module'])) {
        $dependencies = array_merge($dependencies, $config
          ->getData()['dependencies']['module']);
      }

      // Only return dependencies for installed modules and not, for example,
      // 'core'.
      $dependencies = array_intersect($dependencies, array_keys($module_list));
    }
    return $dependencies;
  }

  /**
   * {@inheritdoc}
   */
  public function assignConfigPackage($package_name, array $item_names, $force = FALSE) {
    $config_collection = $this
      ->getConfigCollection();
    $module_list = $this->moduleHandler
      ->getModuleList();
    $current_bundle = $this->assigner
      ->getBundle();
    $packages =& $this->packages;
    if (isset($packages[$package_name])) {
      $package =& $packages[$package_name];
    }
    elseif (isset($current_bundle) && isset($this->packages[$current_bundle
      ->getFullName($package_name)])) {
      $package =& $this->packages[$current_bundle
        ->getFullName($package_name)];
    }
    else {
      throw new \Exception($this
        ->t('Failed to package @package_name. Package not found.', [
        '@package_name' => $package_name,
      ]));
    }
    foreach ($item_names as $item_name) {
      if (isset($config_collection[$item_name])) {

        // Add to the package if:
        // - force is set or
        //   - the item hasn't already been assigned elsewhere, and
        //   - the package hasn't been excluded.
        // - and the item isn't already in the package.
        $item =& $config_collection[$item_name];
        $already_assigned = !empty($item
          ->getPackage());

        // If this is the profile package, we can reassign extension-provided
        // configuration.
        $package_bundle = $this
          ->getAssigner()
          ->getBundle($package
          ->getBundle());
        $is_profile_package = isset($package_bundle) ? $package_bundle
          ->isProfilePackage($package_name) : FALSE;

        // An item is assignable if:
        // - it is not provider excluded or this is the profile package, and
        // - it is not flagged as excluded.
        $assignable = (!$item
          ->isProviderExcluded() || $is_profile_package) && !$item
          ->isExcluded();

        // An item is assignable if it was provided by the current package.
        $assignable = $assignable || $item
          ->getProvider() == $package
          ->getMachineName();
        $excluded_from_package = in_array($package_name, $item
          ->getPackageExcluded());
        $already_in_package = in_array($item_name, $package
          ->getConfig());
        if (($force || !$already_assigned && $assignable && !$excluded_from_package) && !$already_in_package) {

          // Add the item to the package's config array.
          $package
            ->appendConfig($item_name);

          // Mark the item as already assigned.
          $item
            ->setPackage($package_name);
          $module_dependencies = $this
            ->getConfigDependency($item, $module_list);
          $package
            ->setDependencies($this
            ->mergeUniqueItems($package
            ->getDependencies(), $module_dependencies));
        }

        // Return memory.
        unset($item);
      }
    }
    $this
      ->setConfigCollection($config_collection);
  }

  /**
   * {@inheritdoc}
   */
  public function assignConfigByPattern(array $patterns) {

    // Regular expressions for items that are likely to generate false
    // positives when assigned by pattern.
    $false_positives = [
      // Blocks with the page title should not be assigned to a 'page' package.
      '/block\\.block\\..*_page_title/',
    ];
    $config_collection = $this
      ->getConfigCollection();

    // Sort by key so that specific package will claim items before general
    // package. E.g., event_registration and registration_event will claim
    // before event.
    uksort($patterns, function ($a, $b) {

      // Count underscores to determine specificity of the package.
      return (int) (substr_count($a, '_') <= substr_count($b, '_'));
    });
    foreach ($patterns as $pattern => $machine_name) {
      if (isset($this->packages[$machine_name])) {
        foreach ($config_collection as $item_name => $item) {

          // Test for and skip false positives.
          foreach ($false_positives as $false_positive) {
            if (preg_match($false_positive, $item_name)) {
              continue 2;
            }
          }
          if (!$item
            ->getPackage() && preg_match('/(\\.|-|_|^)' . $pattern . '(\\.|-|_|$)/', $item
            ->getShortName())) {
            try {
              $this
                ->assignConfigPackage($machine_name, [
                $item_name,
              ]);
            } catch (\Exception $exception) {
              \Drupal::logger('features')
                ->error($exception
                ->getMessage());
            }
          }
        }
      }
    }
  }

  /**
   * {@inheritdoc}
   */
  public function assignConfigDependents(array $item_names = NULL, $package = NULL) {
    $config_collection = $this
      ->getConfigCollection();
    if (empty($item_names)) {
      $item_names = array_keys($config_collection);
    }
    foreach ($item_names as $item_name) {

      // Make sure the extension provided item exists in the active
      // configuration storage.
      if (isset($config_collection[$item_name]) && $config_collection[$item_name]
        ->getPackage()) {
        foreach ($config_collection[$item_name]
          ->getDependents() as $dependent_item_name) {
          if (isset($config_collection[$dependent_item_name]) && (!empty($package) || empty($config_collection[$dependent_item_name]
            ->getPackage()))) {
            try {
              $package_name = !empty($package) ? $package : $config_collection[$item_name]
                ->getPackage();
              $this
                ->assignConfigPackage($package_name, [
                $dependent_item_name,
              ]);
            } catch (\Exception $exception) {
              \Drupal::logger('features')
                ->error($exception
                ->getMessage());
            }
          }
        }
      }
    }
  }

  /**
   * {@inheritdoc}
   */
  public function setPackageBundleNames(FeaturesBundleInterface $bundle, array &$package_names = []) {
    $this->packagesPrefixed = TRUE;
    if (!$bundle
      ->isDefault()) {
      $new_package_names = [];

      // Assign the selected bundle to the exports.
      $packages = $this
        ->getPackages();
      if (empty($package_names)) {
        $package_names = array_keys($packages);
      }
      foreach ($package_names as $package_name) {

        // Rename package to use bundle prefix.
        $package = $packages[$package_name];

        // The install profile doesn't need renaming.
        if ($package
          ->getType() != 'profile') {
          unset($packages[$package_name]);
          $package
            ->setMachineName($bundle
            ->getFullName($package
            ->getMachineName()));
          $packages[$package
            ->getMachineName()] = $package;
        }

        // Set the bundle machine name.
        $packages[$package
          ->getMachineName()]
          ->setBundle($bundle
          ->getMachineName());
        $new_package_names[] = $package
          ->getMachineName();
      }
      $this
        ->setPackages($packages);
      $package_names = $new_package_names;
    }
  }

  /**
   * {@inheritdoc}
   */
  public function assignPackageDependencies(Package $package = NULL) {
    if (is_null($package)) {
      $packages = $this
        ->getPackages();
    }
    else {
      $packages = [
        $package,
      ];
    }
    $module_list = $this->moduleHandler
      ->getModuleList();
    $config_collection = $this
      ->getConfigCollection();
    foreach ($packages as $package) {
      $module_dependencies = [];
      foreach ($package
        ->getConfig() as $item_name) {
        if (isset($config_collection[$item_name])) {
          $dependencies = $this
            ->getConfigDependency($config_collection[$item_name], $module_list);
          $module_dependencies = array_merge($module_dependencies, $dependencies);
        }
      }
      $package
        ->setDependencies($this
        ->mergeUniqueItems($package
        ->getDependencies(), $module_dependencies));
    }
  }

  /**
   * {@inheritdoc}
   */
  public function assignInterPackageDependencies(FeaturesBundleInterface $bundle, array &$packages) {
    if (!$this->packagesPrefixed) {
      throw new \Exception($this
        ->t('The packages have not yet been prefixed with a bundle name.'));
    }
    $config_collection = $this
      ->getConfigCollection();
    $module_list = $this->moduleHandler
      ->getModuleList();

    /** @var \Drupal\features\Package[] $packages */
    foreach ($packages as $package) {
      foreach ($package
        ->getConfig() as $item_name) {
        if (!empty($config_collection[$item_name]
          ->getData()['dependencies']['config'])) {
          foreach ($config_collection[$item_name]
            ->getData()['dependencies']['config'] as $dependency_name) {
            if (isset($config_collection[$dependency_name]) && $config_collection[$dependency_name]
              ->getSubdirectory() === InstallStorage::CONFIG_INSTALL_DIRECTORY) {

              // If the required item is assigned to one of the packages, add
              // a dependency on that package.
              $dependency_set = FALSE;
              if ($dependency_package = $config_collection[$dependency_name]
                ->getPackage()) {
                $package_name = $bundle
                  ->getFullName($dependency_package);
                if ($package_name && array_key_exists($package_name, $packages) && isset($module_list[$package_name])) {
                  $package
                    ->appendDependency($package_name);
                  $dependency_set = TRUE;
                }
              }

              // Otherwise, if the dependency is provided by an existing
              // feature, add a dependency on that feature.
              if (!$dependency_set && ($extension_name = $config_collection[$dependency_name]
                ->getProvider())) {

                // No extension should depend on the install profile.
                $package_name = $bundle
                  ->getFullName($package
                  ->getMachineName());
                if ($extension_name != $package_name && $extension_name != $this
                  ->drupalGetProfile() && isset($module_list[$extension_name])) {
                  $package
                    ->appendDependency($extension_name);
                }
              }
            }
          }
        }
      }
    }

    // Unset the $package pass by reference.
    unset($package);
  }

  /**
   * Gets the name of the currently active installation profile.
   *
   * @return string|null
   *   The name of the installation profile or NULL if no installation profile is
   *   currently active. This is the case for example during the first steps of
   *   the installer or during unit tests.
   */
  protected function drupalGetProfile() {
    return \Drupal::installProfile();
  }

  /**
   * Merges a set of new item into an array.
   *
   * Only unique values are retained.
   *
   * @param array $items
   *   An array of items.
   * @param array $new_items
   *   An array of new items to be merged in.
   *
   * @return array
   *   The merged and unique items.
   */
  protected function mergeUniqueItems(array $items, array $new_items) {
    $items = array_unique(array_merge($items, $new_items));
    return $items;
  }

  /**
   * Initializes and returns a package or profile array.
   *
   * @param string $machine_name
   *   Machine name of the package.
   * @param string $name
   *   (optional) Human readable name of the package.
   * @param string $description
   *   (optional) Description of the package.
   * @param string $type
   *   (optional) Type of project.
   * @param \Drupal\features\FeaturesBundleInterface $bundle
   *   (optional) Bundle to use to add profile directories to the scan.
   * @param \Drupal\Core\Extension\Extension $extension
   *   (optional) An Extension object.
   *
   * @return \Drupal\features\Package
   *   An array of package properties; see
   *   FeaturesManagerInterface::getPackages().
   */
  protected function getPackageObject($machine_name, $name = NULL, $description = '', $type = 'module', FeaturesBundleInterface $bundle = NULL, Extension $extension = NULL) {
    if (!isset($bundle)) {
      $bundle = $this
        ->getAssigner()
        ->getBundle();
    }
    $package = new Package($machine_name, [
      'name' => isset($name) ? $name : ucwords(str_replace([
        '_',
        '-',
      ], ' ', $machine_name)),
      'description' => $description,
      'type' => $type,
      'status' => FeaturesManagerInterface::STATUS_DEFAULT,
      'state' => FeaturesManagerInterface::STATE_DEFAULT,
      'bundle' => $bundle
        ->isDefault() ? '' : $bundle
        ->getMachineName(),
    ]);

    // If no extension was passed in, look for a match.
    if (!isset($extension)) {
      $module_list = $this
        ->getFeaturesModules($bundle);
      $module_name = $package
        ->getMachineName();
      if (isset($module_list[$module_name])) {
        $extension = $module_list[$module_name];
      }
    }

    // If there is an extension, set extension-specific properties.
    if (isset($extension)) {
      $info = $this
        ->getExtensionInfo($extension);
      $features_info = $this
        ->getFeaturesInfo($extension);
      $package
        ->setExtension($extension);
      $package
        ->setInfo($info);
      $package
        ->setFeaturesInfo($features_info);
      $package
        ->setConfigOrig($this
        ->listExtensionConfig($extension));
      $package
        ->setStatus($this
        ->extensionEnabled($extension) ? FeaturesManagerInterface::STATUS_INSTALLED : FeaturesManagerInterface::STATUS_UNINSTALLED);
      $package
        ->setVersion(isset($info['version']) ? $info['version'] : '');
    }
    return $package;
  }

  /**
   * Convert a Dependency object back to a string value.
   *
   * @param \Drupal\Core\Extension\Dependency $dependency
   *   The dependency to convert.
   * @return string
   *   The normalized "project:name (constraint)" string.
   */
  protected function getDependencyString(Dependency $dependency) {
    $dependency_name = $dependency
      ->getName();
    $project = $dependency
      ->getProject();
    if (empty($project)) {
      $path = $this->moduleExtensionList
        ->getPath($dependency_name);

      // Handle project for core modules.
      $project = strpos($path, 'core/modules') === 0 ? 'drupal' : $dependency_name;

      // Override with project key for contrib or custom modules.
      $info = $this->moduleExtensionList
        ->getExtensionInfo($dependency_name);
      $project = isset($info['project']) ? $info['project'] : $project;
    }
    $new_dependency = $project . ':' . $dependency_name;
    if ($constraint_string = $dependency
      ->getConstraintString()) {
      $new_dependency = $new_dependency . ' (' . $constraint_string . ')';
    }
    return $new_dependency;
  }

  /**
   * Generates and adds .info.yml files to a package.
   *
   * @param \Drupal\features\Package $package
   *   The package.
   */
  protected function addInfoFile(Package $package) {
    $info = [
      'name' => $package
        ->getName(),
      'description' => $package
        ->getDescription(),
      'type' => $package
        ->getType(),
      'core_version_requirement' => $package
        ->getCoreVersionRequirement(),
      'dependencies' => $package
        ->getDependencies(),
      'themes' => $package
        ->getThemes(),
      'version' => $package
        ->getVersion(),
    ];
    $features_info = [];

    // Assign to a "package" named for the profile.
    if ($package
      ->getBundle()) {
      $bundle = $this
        ->getAssigner()
        ->getBundle($package
        ->getBundle());
    }

    // Save the current bundle in the info file so the package
    // can be reloaded later by the AssignmentPackages plugin.
    if (isset($bundle) && !$bundle
      ->isDefault()) {
      $info['package'] = $bundle
        ->getName();
      $features_info['bundle'] = $bundle
        ->getMachineName();
    }
    else {
      unset($features_info['bundle']);
    }
    if ($package
      ->getConfig()) {
      foreach ([
        'excluded',
        'required',
      ] as $constraint) {
        if (!empty($package
          ->{'get' . $constraint}())) {
          $features_info[$constraint] = $package
            ->{'get' . $constraint}();
        }
        else {
          unset($features_info[$constraint]);
        }
      }
      if (empty($features_info)) {
        $features_info = TRUE;
      }
    }

    // The name and description need to be cast as strings from the
    // TranslatableMarkup objects returned by t() to avoid raising an
    // InvalidDataTypeException on Yaml serialization.
    foreach ([
      'name',
      'description',
    ] as $key) {
      $info[$key] = (string) $info[$key];
    }

    // Add profile-specific info data.
    if ($info['type'] == 'profile') {

      // Set the distribution name.
      $info['distribution'] = [
        'name' => $info['name'],
      ];
    }
    $package
      ->appendFile([
      'filename' => $package
        ->getMachineName() . '.info.yml',
      'subdirectory' => NULL,
      // Filter to remove any empty keys, e.g., an empty themes array.
      'string' => Yaml::encode(array_filter($info)),
    ], 'info');
    $package
      ->appendFile([
      'filename' => $package
        ->getMachineName() . '.features.yml',
      'subdirectory' => NULL,
      'string' => Yaml::encode($features_info),
    ], 'features');
  }

  /**
   * Generates and adds files to a given package or profile.
   */
  protected function addPackageFiles(Package $package) {
    $config_collection = $this
      ->getConfigCollection();

    // Always add .info.yml and .features.yml files.
    $this
      ->addInfoFile($package);

    // Only add files if there is at least one piece of configuration present.
    if ($package
      ->getConfig()) {

      // Add configuration files.
      foreach ($package
        ->getConfig() as $name) {
        $config = $config_collection[$name];
        $package
          ->appendFile([
          'filename' => $config
            ->getName() . '.yml',
          'subdirectory' => $config
            ->getSubdirectory(),
          'string' => Yaml::encode($config
            ->getData()),
        ], $name);
      }
    }
  }

  /**
   * {@inheritdoc}
   */
  public function mergeInfoArray(array $info1, array $info2, array $keys = []) {

    // If keys were specified, use only those.
    if (!empty($keys)) {
      $info2 = array_intersect_key($info2, array_fill_keys($keys, NULL));
    }
    $info = NestedArray::mergeDeep($info1, $info2);

    // Merge dependencies. Preserve constraints.
    // Handle cases of mixed "project:module" and "module" dependencies.
    $dependencies = [];

    // First collect dependency list from info1.
    if (!empty($info1['dependencies'])) {
      foreach ($info1['dependencies'] as $dependency_string) {
        $dependency = Dependency::createFromString($dependency_string);
        $dependencies[$dependency
          ->getName()] = $this
          ->getDependencyString($dependency);
      }
    }

    // Now merge dependencies from info2.
    if (!empty($info2['dependencies'])) {
      foreach ($info2['dependencies'] as $dependency_string) {
        $dependency = Dependency::createFromString($dependency_string);
        $dependency_name = $dependency
          ->getName();
        if (isset($dependencies[$dependency_name])) {

          // Dependency already in list, so only overwrite if there is a constraint.
          if ($dependency
            ->getConstraintString()) {
            $dependencies[$dependency_name] = $this
              ->getDependencyString($dependency);
          }
        }
        else {

          // Wasn't in info1, so merge it into list.
          $dependencies[$dependency_name] = $this
            ->getDependencyString($dependency);
        }
      }
    }
    if (!empty($dependencies)) {
      $info['dependencies'] = array_values($dependencies);
      sort($info['dependencies']);
    }

    // Process themes keys.
    $keys = [
      'themes',
    ];
    foreach ($keys as $key) {
      if (isset($info[$key]) && is_array($info[$key])) {

        // NestedArray::mergeDeep() may produce duplicate values.
        $info[$key] = array_unique($info[$key]);
        sort($info[$key]);
      }
    }
    return $info;
  }

  /**
   * {@inheritdoc}
   */
  public function listConfigTypes($bundles_only = FALSE) {
    $definitions = [];
    foreach ($this->entityTypeManager
      ->getDefinitions() as $entity_type => $definition) {
      if ($definition
        ->entityClassImplements('Drupal\\Core\\Config\\Entity\\ConfigEntityInterface')) {
        if (!$bundles_only || $definition
          ->getBundleOf()) {
          $definitions[$entity_type] = $definition;
        }
      }
    }
    $entity_types = array_map(function (EntityTypeInterface $definition) {
      return $definition
        ->getLabel();
    }, $definitions);

    // Sort the entity types by label, then add the simple config to the top.
    uasort($entity_types, 'strnatcasecmp');
    return $bundles_only ? $entity_types : [
      FeaturesManagerInterface::SYSTEM_SIMPLE_CONFIG => $this
        ->t('Simple configuration'),
    ] + $entity_types;
  }

  /**
   * {@inheritdoc}
   */
  public function listExtensionConfig(Extension $extension) {
    return $this->extensionStorages
      ->listExtensionConfig($extension);
  }

  /**
   * {@inheritdoc}
   */
  public function listExistingConfig($installed = FALSE, FeaturesBundleInterface $bundle = NULL) {
    $config = [];
    $existing = $this
      ->getFeaturesModules($bundle, $installed);
    foreach ($existing as $extension) {

      // Keys are configuration item names and values are providing extension
      // name.
      $new_config = array_fill_keys($this
        ->listExtensionConfig($extension), $extension
        ->getName());
      $config = array_merge($config, $new_config);
    }
    return $config;
  }

  /**
   * {@inheritdoc}
   */
  public function listConfigByType($config_type) {

    // For a given entity type, load all entities.
    if ($config_type && $config_type !== FeaturesManagerInterface::SYSTEM_SIMPLE_CONFIG) {
      $entity_storage = $this->entityTypeManager
        ->getStorage($config_type);
      $names = [];
      foreach ($entity_storage
        ->loadMultiple() as $entity) {
        $entity_id = $entity
          ->id();
        $label = $entity
          ->label() ?: $entity_id;
        $names[$entity_id] = $label;
      }
    }
    else {
      $definitions = [];
      foreach ($this->entityTypeManager
        ->getDefinitions() as $entity_type => $definition) {
        if ($definition
          ->entityClassImplements('Drupal\\Core\\Config\\Entity\\ConfigEntityInterface')) {
          $definitions[$entity_type] = $definition;
        }
      }

      // Gather the config entity prefixes.
      $config_prefixes = array_map(function (EntityTypeInterface $definition) {
        return $definition
          ->getConfigPrefix() . '.';
      }, $definitions);

      // Find all config, and then filter our anything matching a config prefix.
      $names = $this->configStorage
        ->listAll();
      $names = array_combine($names, $names);
      foreach ($names as $item_name) {
        foreach ($config_prefixes as $config_prefix) {
          if (strpos($item_name, $config_prefix) === 0) {
            unset($names[$item_name]);
          }
        }
      }
    }
    return $names;
  }

  /**
   * Creates a high performant version of the ConfigDependencyManager.
   *
   * @return \Drupal\features\FeaturesConfigDependencyManager
   *   A high performant version of the ConfigDependencyManager.
   *
   * @see \Drupal\Core\Config\Entity\ConfigDependencyManager
   */
  protected function getFeaturesConfigDependencyManager() {
    $dependency_manager = new FeaturesConfigDependencyManager();

    // Read all configuration using the factory. This ensures that multiple
    // deletes during the same request benefit from the static cache. Using the
    // factory also ensures configuration entity dependency discovery has no
    // dependencies on the config entity classes. Assume data with UUID is a
    // config entity. Only configuration entities can be depended on so we can
    // ignore everything else.
    $data = array_map(function (ImmutableConfig $config) {
      $data = $config
        ->get();
      if (isset($data['uuid'])) {
        return $data;
      }
      return FALSE;
    }, $this->configFactory
      ->loadMultiple($this->configStorage
      ->listAll()));
    $dependency_manager
      ->setData(array_filter($data));
    return $dependency_manager;
  }

  /**
   * Loads configuration from storage into a property.
   */
  protected function initConfigCollection($reset = FALSE) {
    if ($reset || empty($this->configCollection)) {
      $config_collection = [];
      $config_types = $this
        ->listConfigTypes();
      $dependency_manager = $this
        ->getFeaturesConfigDependencyManager();

      // List configuration provided by installed features.
      $existing_config = $this
        ->listExistingConfig(NULL);
      $existing_config_by_directory = $this->extensionStorages
        ->listAllByDirectory();
      foreach (array_keys($config_types) as $config_type) {
        $config = $this
          ->listConfigByType($config_type);
        foreach ($config as $item_name => $label) {
          $name = $this
            ->getFullName($config_type, $item_name);
          $data = $this->configStorage
            ->read($name);
          $config_collection[$name] = new ConfigurationItem($name, $data, [
            'shortName' => $item_name,
            'label' => $label,
            'type' => $config_type,
            'dependents' => array_keys($dependency_manager
              ->getDependentEntities('config', $name)),
            // Default to the install directory.
            'subdirectory' => isset($existing_config_by_directory[$name]) ? $existing_config_by_directory[$name] : InstallStorage::CONFIG_INSTALL_DIRECTORY,
            'package' => '',
            'providerExcluded' => NULL,
            'provider' => isset($existing_config[$name]) ? $existing_config[$name] : NULL,
            'packageExcluded' => [],
          ]);
        }
      }
      $this
        ->setConfigCollection($config_collection);
    }
  }

  /**
   * {@inheritdoc}
   */
  public function prepareFiles(array $packages) {
    foreach ($packages as $package) {
      $this
        ->addPackageFiles($package);
    }
  }

  /**
   * {@inheritdoc}
   */
  public function getExportInfo(Package $package, FeaturesBundleInterface $bundle = NULL) {
    $full_name = isset($bundle) ? $bundle
      ->getFullName($package
      ->getMachineName()) : $package
      ->getMachineName();
    $path = '';

    // Adjust export directory to be in profile.
    if (isset($bundle) && $bundle
      ->isProfile()) {
      $path .= 'profiles/' . $bundle
        ->getProfileName();
    }

    // If this is not the profile package, nest the directory.
    if (!isset($bundle) || !$bundle
      ->isProfilePackage($package
      ->getMachineName())) {
      $path .= empty($path) ? 'modules' : '/modules';
      $export_settings = $this
        ->getExportSettings();
      if (!empty($export_settings['folder'])) {
        $path .= '/' . $export_settings['folder'];
      }
    }

    // Use the same path of a package to override it.
    if ($extension = $package
      ->getExtension()) {
      $extension_path = $extension
        ->getPath();
      $path = dirname($extension_path);
    }
    return [
      $full_name,
      $path,
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function detectOverrides(Package $feature, $include_new = FALSE) {

    /** @var \Drupal\config_update\ConfigDiffInterface $config_diff */
    $config_diff = \Drupal::service('config_update.config_diff');
    $different = [];
    foreach ($feature
      ->getConfig() as $name) {
      $active = $this->configStorage
        ->read($name);
      $extension = $this->extensionStorages
        ->read($name);
      $extension = !empty($extension) ? $extension : [];
      if (($include_new || !empty($extension)) && !$config_diff
        ->same($extension, $active)) {
        $different[] = $name;
      }
    }
    if (!empty($different)) {
      $feature
        ->setState(FeaturesManagerInterface::STATE_OVERRIDDEN);
    }
    return $different;
  }

  /**
   * {@inheritdoc}
   */
  public function detectNew(Package $feature) {
    $result = [];
    foreach ($feature
      ->getConfig() as $name) {
      $extension = $this->extensionStorages
        ->read($name);
      if (empty($extension)) {
        $result[] = $name;
      }
    }
    return $result;
  }

  /**
   * {@inheritdoc}
   */
  public function detectMissing(Package $feature) {
    $config = $this
      ->getConfigCollection();
    $result = [];
    foreach ($feature
      ->getConfigOrig() as $name) {
      if (!isset($config[$name])) {
        $result[] = $name;
      }
    }
    return $result;
  }

  /**
   * {@inheritdoc}
   */
  public function reorderMissing(array $missing) {
    $list = [];
    $result = [];
    foreach ($missing as $full_name) {
      $this
        ->addConfigList($full_name, $list);
    }
    foreach ($list as $full_name) {
      if (in_array($full_name, $missing)) {
        $result[] = $full_name;
      }
    }
    return $result;
  }

  /**
   * {@inheritDoc}
   */
  protected function addConfigList($full_name, &$list) {
    $index = array_search($full_name, $list);
    if ($index !== FALSE) {
      unset($list[$index]);
    }
    array_unshift($list, $full_name);
    $value = $this->extensionStorages
      ->read($full_name);
    if (isset($value['dependencies']['config'])) {
      foreach ($value['dependencies']['config'] as $config_name) {
        $this
          ->addConfigList($config_name, $list);
      }
    }
  }

  /**
   * {@inheritdoc}
   */
  public function statusLabel($status) {
    switch ($status) {
      case FeaturesManagerInterface::STATUS_NO_EXPORT:
        return $this
          ->t('Not exported');
      case FeaturesManagerInterface::STATUS_UNINSTALLED:
        return $this
          ->t('Uninstalled');
      case FeaturesManagerInterface::STATUS_INSTALLED:
        return $this
          ->t('Installed');
    }
  }

  /**
   * {@inheritdoc}
   */
  public function stateLabel($state) {
    switch ($state) {
      case FeaturesManagerInterface::STATE_DEFAULT:
        return $this
          ->t('Default');
      case FeaturesManagerInterface::STATE_OVERRIDDEN:
        return $this
          ->t('Changed');
    }
  }

  /**
   * {@inheritdoc}
   */
  public function getFeaturesInfo(Extension $extension) {
    $module_name = $extension
      ->getName();
    if (isset($this->featureInfoCache[$module_name])) {
      return $this->featureInfoCache[$module_name];
    }
    $features_info = NULL;
    $filename = $this->root . '/' . $extension
      ->getPath() . '/' . $module_name . '.features.yml';
    if (file_exists($filename)) {
      $features_info = Yaml::decode(file_get_contents($filename));
    }
    $this->featureInfoCache[$module_name] = $features_info;
    return $features_info;
  }

  /**
   * {@inheritdoc}
   */
  public function createConfiguration(array $config_to_create) {
    $existing_config = $this
      ->getConfigCollection();

    // If config data is not specified, load it from the extension storage.
    foreach ($config_to_create as $name => $item) {
      if (empty($item)) {
        $config = $this->configReverter
          ->getFromExtension('', $name);

        // For testing purposes, if it couldn't load from a module, get config
        // from the cached Config Collection.
        if (empty($config) && isset($existing_config[$name])) {
          $config = $existing_config[$name]
            ->getData();
        }
        $config_to_create[$name] = $config;
      }
    }

    // Determine which config is new vs existing.
    $existing = array_intersect_key($config_to_create, $existing_config);
    $new = array_diff_key($config_to_create, $existing);

    // The FeaturesConfigInstaller exposes the normally protected
    // createConfiguration
    // function from Core ConfigInstaller than handles the creation of new
    // config or the changing of existing config.

    /** @var \Drupal\features\FeaturesConfigInstaller $config_installer */
    $config_installer = \Drupal::service('features.config.installer');
    $config_installer
      ->createConfiguration(StorageInterface::DEFAULT_COLLECTION, $config_to_create);

    // Collect results for new and updated config.
    $new_config = $this
      ->getConfigCollection(TRUE);
    $result['updated'] = array_intersect_key($new_config, $existing);
    $result['new'] = array_intersect_key($new_config, $new);
    return $result;
  }

  /**
   * {@inheritdoc}
   */
  public function import($modules, $any = FALSE) {
    $result = [];
    foreach ($modules as $module_name) {
      $package = $this
        ->loadPackage($module_name, $any);
      $components = isset($package) ? $package
        ->getConfigOrig() : [];
      if (empty($components)) {
        continue;
      }
      $result[$module_name] = $this
        ->createConfiguration(array_fill_keys($components, []));
    }
    return $result;
  }

}

Classes

Namesort descending Description
FeaturesManager The FeaturesManager provides helper functions for building packages.