Chris@0: 'entity.manager']; Chris@18: Chris@18: /** Chris@18: * The entity type manager. Chris@0: * Chris@18: * @var \Drupal\Core\Entity\EntityTypeManagerInterface Chris@0: */ Chris@18: protected $entityTypeManager; Chris@18: Chris@18: /** Chris@18: * The entity repository. Chris@18: * Chris@18: * @var \Drupal\Core\Entity\EntityRepositoryInterface Chris@18: */ Chris@18: protected $entityRepository; Chris@0: Chris@0: /** Chris@0: * The configuration factory. Chris@0: * Chris@0: * @var \Drupal\Core\Config\ConfigFactoryInterface Chris@0: */ Chris@0: protected $configFactory; Chris@0: Chris@0: /** Chris@0: * The typed config manager. Chris@0: * Chris@0: * @var \Drupal\Core\Config\TypedConfigManagerInterface Chris@0: */ Chris@0: protected $typedConfigManager; Chris@0: Chris@0: /** Chris@0: * The active configuration storage. Chris@0: * Chris@0: * @var \Drupal\Core\Config\StorageInterface Chris@0: */ Chris@0: protected $activeStorage; Chris@0: Chris@0: /** Chris@0: * The event dispatcher. Chris@0: * Chris@0: * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface Chris@0: */ Chris@0: protected $eventDispatcher; Chris@0: Chris@0: /** Chris@0: * The configuration collection info. Chris@0: * Chris@0: * @var \Drupal\Core\Config\ConfigCollectionInfo Chris@0: */ Chris@0: protected $configCollectionInfo; Chris@0: Chris@0: /** Chris@0: * The configuration storages keyed by collection name. Chris@0: * Chris@0: * @var \Drupal\Core\Config\StorageInterface[] Chris@0: */ Chris@0: protected $storages; Chris@0: Chris@0: /** Chris@0: * Creates ConfigManager objects. Chris@0: * Chris@18: * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager Chris@18: * The entity type manager. Chris@0: * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory Chris@0: * The configuration factory. Chris@0: * @param \Drupal\Core\Config\TypedConfigManagerInterface $typed_config_manager Chris@0: * The typed config manager. Chris@0: * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation Chris@0: * The string translation service. Chris@0: * @param \Drupal\Core\Config\StorageInterface $active_storage Chris@0: * The active configuration storage. Chris@0: * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher Chris@0: * The event dispatcher. Chris@18: * @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository Chris@18: * The entity repository. Chris@0: */ Chris@18: public function __construct(EntityTypeManagerInterface $entity_type_manager, ConfigFactoryInterface $config_factory, TypedConfigManagerInterface $typed_config_manager, TranslationInterface $string_translation, StorageInterface $active_storage, EventDispatcherInterface $event_dispatcher, EntityRepositoryInterface $entity_repository = NULL) { Chris@18: if ($entity_type_manager instanceof EntityManagerInterface) { Chris@18: @trigger_error('Passing the entity.manager service to ConfigManager::__construct() is deprecated in Drupal 8.7.0 and will be removed before Drupal 9.0.0. Pass the new dependencies instead. See https://www.drupal.org/node/2549139.', E_USER_DEPRECATED); Chris@18: $this->entityTypeManager = \Drupal::entityTypeManager(); Chris@18: } Chris@18: else { Chris@18: $this->entityTypeManager = $entity_type_manager; Chris@18: } Chris@0: $this->configFactory = $config_factory; Chris@0: $this->typedConfigManager = $typed_config_manager; Chris@0: $this->stringTranslation = $string_translation; Chris@0: $this->activeStorage = $active_storage; Chris@0: $this->eventDispatcher = $event_dispatcher; Chris@18: if ($entity_repository) { Chris@18: $this->entityRepository = $entity_repository; Chris@18: } Chris@18: else { Chris@18: @trigger_error('The entity.repository service must be passed to ConfigManager::__construct(), it is required before Drupal 9.0.0. See https://www.drupal.org/node/2549139.', E_USER_DEPRECATED); Chris@18: $this->entityRepository = \Drupal::service('entity.repository'); Chris@18: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function getEntityTypeIdByName($name) { Chris@18: $entities = array_filter($this->entityTypeManager->getDefinitions(), function (EntityTypeInterface $entity_type) use ($name) { Chris@0: return ($entity_type instanceof ConfigEntityTypeInterface && $config_prefix = $entity_type->getConfigPrefix()) && strpos($name, $config_prefix . '.') === 0; Chris@0: }); Chris@0: return key($entities); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function loadConfigEntityByName($name) { Chris@0: $entity_type_id = $this->getEntityTypeIdByName($name); Chris@0: if ($entity_type_id) { Chris@18: $entity_type = $this->entityTypeManager->getDefinition($entity_type_id); Chris@0: $id = substr($name, strlen($entity_type->getConfigPrefix()) + 1); Chris@18: return $this->entityTypeManager->getStorage($entity_type_id)->load($id); Chris@0: } Chris@0: return NULL; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function getEntityManager() { Chris@18: @trigger_error('ConfigManagerInterface::getEntityManager() is deprecated in Drupal 8.7.0 and will be removed before Drupal 9.0.0. Use ::getEntityTypeManager() instead. See https://www.drupal.org/node/2549139.', E_USER_DEPRECATED); Chris@18: return \Drupal::service('entity.manager'); Chris@18: } Chris@18: Chris@18: /** Chris@18: * {@inheritdoc} Chris@18: */ Chris@18: public function getEntityTypeManager() { Chris@18: return $this->entityTypeManager; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function getConfigFactory() { Chris@0: return $this->configFactory; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function diff(StorageInterface $source_storage, StorageInterface $target_storage, $source_name, $target_name = NULL, $collection = StorageInterface::DEFAULT_COLLECTION) { Chris@0: if ($collection != StorageInterface::DEFAULT_COLLECTION) { Chris@0: $source_storage = $source_storage->createCollection($collection); Chris@0: $target_storage = $target_storage->createCollection($collection); Chris@0: } Chris@0: if (!isset($target_name)) { Chris@0: $target_name = $source_name; Chris@0: } Chris@0: // The output should show configuration object differences formatted as YAML. Chris@0: // But the configuration is not necessarily stored in files. Therefore, they Chris@0: // need to be read and parsed, and lastly, dumped into YAML strings. Chris@0: $source_data = explode("\n", Yaml::encode($source_storage->read($source_name))); Chris@0: $target_data = explode("\n", Yaml::encode($target_storage->read($target_name))); Chris@0: Chris@0: // Check for new or removed files. Chris@0: if ($source_data === ['false']) { Chris@0: // Added file. Chris@0: // Cast the result of t() to a string, as the diff engine doesn't know Chris@0: // about objects. Chris@0: $source_data = [(string) $this->t('File added')]; Chris@0: } Chris@0: if ($target_data === ['false']) { Chris@0: // Deleted file. Chris@0: // Cast the result of t() to a string, as the diff engine doesn't know Chris@0: // about objects. Chris@0: $target_data = [(string) $this->t('File removed')]; Chris@0: } Chris@0: Chris@0: return new Diff($source_data, $target_data); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function createSnapshot(StorageInterface $source_storage, StorageInterface $snapshot_storage) { Chris@18: self::replaceStorageContents($source_storage, $snapshot_storage); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function uninstall($type, $name) { Chris@0: $entities = $this->getConfigEntitiesToChangeOnDependencyRemoval($type, [$name], FALSE); Chris@0: // Fix all dependent configuration entities. Chris@0: /** @var \Drupal\Core\Config\Entity\ConfigEntityInterface $entity */ Chris@0: foreach ($entities['update'] as $entity) { Chris@0: $entity->save(); Chris@0: } Chris@0: // Remove all dependent configuration entities. Chris@0: foreach ($entities['delete'] as $entity) { Chris@0: $entity->setUninstalling(TRUE); Chris@0: $entity->delete(); Chris@0: } Chris@0: Chris@0: $config_names = $this->configFactory->listAll($name . '.'); Chris@0: foreach ($config_names as $config_name) { Chris@0: $this->configFactory->getEditable($config_name)->delete(); Chris@0: } Chris@0: Chris@0: // Remove any matching configuration from collections. Chris@0: foreach ($this->activeStorage->getAllCollectionNames() as $collection) { Chris@0: $collection_storage = $this->activeStorage->createCollection($collection); Chris@0: $collection_storage->deleteAll($name . '.'); Chris@0: } Chris@0: Chris@0: $schema_dir = drupal_get_path($type, $name) . '/' . InstallStorage::CONFIG_SCHEMA_DIRECTORY; Chris@0: if (is_dir($schema_dir)) { Chris@0: // Refresh the schema cache if uninstalling an extension that provides Chris@0: // configuration schema. Chris@0: $this->typedConfigManager->clearCachedDefinitions(); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function getConfigDependencyManager() { Chris@0: $dependency_manager = new ConfigDependencyManager(); Chris@0: // Read all configuration using the factory. This ensures that multiple Chris@0: // deletes during the same request benefit from the static cache. Using the Chris@0: // factory also ensures configuration entity dependency discovery has no Chris@0: // dependencies on the config entity classes. Assume data with UUID is a Chris@0: // config entity. Only configuration entities can be depended on so we can Chris@0: // ignore everything else. Chris@0: $data = array_map(function ($config) { Chris@0: $data = $config->get(); Chris@0: if (isset($data['uuid'])) { Chris@0: return $data; Chris@0: } Chris@0: return FALSE; Chris@0: }, $this->configFactory->loadMultiple($this->activeStorage->listAll())); Chris@0: $dependency_manager->setData(array_filter($data)); Chris@0: return $dependency_manager; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function findConfigEntityDependents($type, array $names, ConfigDependencyManager $dependency_manager = NULL) { Chris@0: if (!$dependency_manager) { Chris@0: $dependency_manager = $this->getConfigDependencyManager(); Chris@0: } Chris@0: $dependencies = []; Chris@0: foreach ($names as $name) { Chris@0: $dependencies = array_merge($dependencies, $dependency_manager->getDependentEntities($type, $name)); Chris@0: } Chris@0: return $dependencies; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function findConfigEntityDependentsAsEntities($type, array $names, ConfigDependencyManager $dependency_manager = NULL) { Chris@0: $dependencies = $this->findConfigEntityDependents($type, $names, $dependency_manager); Chris@0: $entities = []; Chris@18: $definitions = $this->entityTypeManager->getDefinitions(); Chris@0: foreach ($dependencies as $config_name => $dependency) { Chris@0: // Group by entity type to efficient load entities using Chris@0: // \Drupal\Core\Entity\EntityStorageInterface::loadMultiple(). Chris@0: $entity_type_id = $this->getEntityTypeIdByName($config_name); Chris@0: // It is possible that a non-configuration entity will be returned if a Chris@0: // simple configuration object has a UUID key. This would occur if the Chris@0: // dependents of the system module are calculated since system.site has Chris@0: // a UUID key. Chris@0: if ($entity_type_id) { Chris@0: $id = substr($config_name, strlen($definitions[$entity_type_id]->getConfigPrefix()) + 1); Chris@0: $entities[$entity_type_id][] = $id; Chris@0: } Chris@0: } Chris@0: $entities_to_return = []; Chris@0: foreach ($entities as $entity_type_id => $entities_to_load) { Chris@18: $storage = $this->entityTypeManager->getStorage($entity_type_id); Chris@0: // Remove the keys since there are potential ID clashes from different Chris@0: // configuration entity types. Chris@0: $entities_to_return = array_merge($entities_to_return, array_values($storage->loadMultiple($entities_to_load))); Chris@0: } Chris@0: return $entities_to_return; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function getConfigEntitiesToChangeOnDependencyRemoval($type, array $names, $dry_run = TRUE) { Chris@0: $dependency_manager = $this->getConfigDependencyManager(); Chris@0: Chris@12: // Store the list of dependents in three separate variables. This allows us Chris@12: // to determine how the dependency graph changes as entities are fixed by Chris@12: // calling the onDependencyRemoval() method. Chris@12: Chris@12: // The list of original dependents on $names. This list never changes. Chris@12: $original_dependents = $this->findConfigEntityDependentsAsEntities($type, $names, $dependency_manager); Chris@12: Chris@12: // The current list of dependents on $names. This list is recalculated when Chris@12: // calling an entity's onDependencyRemoval() method results in the entity Chris@12: // changing. This list is passed to each entity's onDependencyRemoval() Chris@12: // method as the list of affected entities. Chris@12: $current_dependents = $original_dependents; Chris@12: Chris@12: // The list of dependents to process. This list changes as entities are Chris@12: // processed and are either fixed or deleted. Chris@12: $dependents_to_process = $original_dependents; Chris@12: Chris@12: // Initialize other variables. Chris@12: $affected_uuids = []; Chris@0: $return = [ Chris@0: 'update' => [], Chris@0: 'delete' => [], Chris@0: 'unchanged' => [], Chris@0: ]; Chris@0: Chris@12: // Try to fix the dependents and find out what will happen to the dependency Chris@12: // graph. Entities are processed in the order of most dependent first. For Chris@12: // example, this ensures that Menu UI third party dependencies on node types Chris@12: // are fixed before processing the node type's other dependents. Chris@12: while ($dependent = array_pop($dependents_to_process)) { Chris@0: /** @var \Drupal\Core\Config\Entity\ConfigEntityInterface $dependent */ Chris@0: if ($dry_run) { Chris@0: // Clone the entity so any changes do not change any static caches. Chris@0: $dependent = clone $dependent; Chris@0: } Chris@0: $fixed = FALSE; Chris@12: if ($this->callOnDependencyRemoval($dependent, $current_dependents, $type, $names)) { Chris@0: // Recalculate dependencies and update the dependency graph data. Chris@0: $dependent->calculateDependencies(); Chris@0: $dependency_manager->updateData($dependent->getConfigDependencyName(), $dependent->getDependencies()); Chris@12: // Based on the updated data rebuild the list of current dependents. Chris@12: // This will remove entities that are no longer dependent after the Chris@12: // recalculation. Chris@12: $current_dependents = $this->findConfigEntityDependentsAsEntities($type, $names, $dependency_manager); Chris@12: // Rebuild the list of entities that we need to process using the new Chris@12: // list of current dependents and removing any entities that we've Chris@12: // already processed. Chris@12: $dependents_to_process = array_filter($current_dependents, function ($current_dependent) use ($affected_uuids) { Chris@12: return !in_array($current_dependent->uuid(), $affected_uuids); Chris@0: }); Chris@12: // Ensure that the dependent has actually been fixed. It is possible Chris@12: // that other dependencies cause it to still be in the list. Chris@0: $fixed = TRUE; Chris@12: foreach ($dependents_to_process as $key => $entity) { Chris@0: if ($entity->uuid() == $dependent->uuid()) { Chris@0: $fixed = FALSE; Chris@12: unset($dependents_to_process[$key]); Chris@0: break; Chris@0: } Chris@0: } Chris@0: if ($fixed) { Chris@12: $affected_uuids[] = $dependent->uuid(); Chris@0: $return['update'][] = $dependent; Chris@0: } Chris@0: } Chris@0: // If the entity cannot be fixed then it has to be deleted. Chris@0: if (!$fixed) { Chris@12: $affected_uuids[] = $dependent->uuid(); Chris@0: // Deletes should occur in the order of the least dependent first. For Chris@0: // example, this ensures that fields are removed before field storages. Chris@0: array_unshift($return['delete'], $dependent); Chris@0: } Chris@0: } Chris@12: // Use the list of affected UUIDs to filter the original list to work out Chris@12: // which configuration entities are unchanged. Chris@12: $return['unchanged'] = array_filter($original_dependents, function ($dependent) use ($affected_uuids) { Chris@12: return !(in_array($dependent->uuid(), $affected_uuids)); Chris@0: }); Chris@0: Chris@0: return $return; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function getConfigCollectionInfo() { Chris@0: if (!isset($this->configCollectionInfo)) { Chris@0: $this->configCollectionInfo = new ConfigCollectionInfo(); Chris@0: $this->eventDispatcher->dispatch(ConfigEvents::COLLECTION_INFO, $this->configCollectionInfo); Chris@0: } Chris@0: return $this->configCollectionInfo; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Calls an entity's onDependencyRemoval() method. Chris@0: * Chris@0: * A helper method to call onDependencyRemoval() with the correct list of Chris@0: * affected entities. This list should only contain dependencies on the Chris@0: * entity. Configuration and content entity dependencies will be converted Chris@0: * into entity objects. Chris@0: * Chris@0: * @param \Drupal\Core\Config\Entity\ConfigEntityInterface $entity Chris@0: * The entity to call onDependencyRemoval() on. Chris@0: * @param \Drupal\Core\Config\Entity\ConfigEntityInterface[] $dependent_entities Chris@0: * The list of dependent configuration entities. Chris@0: * @param string $type Chris@0: * The type of dependency being checked. Either 'module', 'theme', 'config' Chris@0: * or 'content'. Chris@0: * @param array $names Chris@0: * The specific names to check. If $type equals 'module' or 'theme' then it Chris@0: * should be a list of module names or theme names. In the case of 'config' Chris@0: * or 'content' it should be a list of configuration dependency names. Chris@0: * Chris@0: * @return bool Chris@0: * TRUE if the entity has changed as a result of calling the Chris@0: * onDependencyRemoval() method, FALSE if not. Chris@0: */ Chris@0: protected function callOnDependencyRemoval(ConfigEntityInterface $entity, array $dependent_entities, $type, array $names) { Chris@0: $entity_dependencies = $entity->getDependencies(); Chris@0: if (empty($entity_dependencies)) { Chris@0: // No dependent entities nothing to do. Chris@0: return FALSE; Chris@0: } Chris@0: Chris@0: $affected_dependencies = [ Chris@0: 'config' => [], Chris@0: 'content' => [], Chris@0: 'module' => [], Chris@0: 'theme' => [], Chris@0: ]; Chris@0: Chris@0: // Work out if any of the entity's dependencies are going to be affected. Chris@0: if (isset($entity_dependencies[$type])) { Chris@0: // Work out which dependencies the entity has in common with the provided Chris@0: // $type and $names. Chris@0: $affected_dependencies[$type] = array_intersect($entity_dependencies[$type], $names); Chris@0: Chris@0: // If the dependencies are entities we need to convert them into objects. Chris@0: if ($type == 'config' || $type == 'content') { Chris@0: $affected_dependencies[$type] = array_map(function ($name) use ($type) { Chris@0: if ($type == 'config') { Chris@0: return $this->loadConfigEntityByName($name); Chris@0: } Chris@0: else { Chris@0: // Ignore the bundle. Chris@0: list($entity_type_id,, $uuid) = explode(':', $name); Chris@18: return $this->entityRepository->loadEntityByConfigTarget($entity_type_id, $uuid); Chris@0: } Chris@0: }, $affected_dependencies[$type]); Chris@0: } Chris@0: } Chris@0: Chris@0: // Merge any other configuration entities into the list of affected Chris@0: // dependencies if necessary. Chris@0: if (isset($entity_dependencies['config'])) { Chris@0: foreach ($dependent_entities as $dependent_entity) { Chris@0: if (in_array($dependent_entity->getConfigDependencyName(), $entity_dependencies['config'])) { Chris@0: $affected_dependencies['config'][] = $dependent_entity; Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: // Key the entity arrays by config dependency name to make searching easy. Chris@0: foreach (['config', 'content'] as $dependency_type) { Chris@0: $affected_dependencies[$dependency_type] = array_combine( Chris@0: array_map(function ($entity) { Chris@0: return $entity->getConfigDependencyName(); Chris@0: }, $affected_dependencies[$dependency_type]), Chris@0: $affected_dependencies[$dependency_type] Chris@0: ); Chris@0: } Chris@0: Chris@0: // Inform the entity. Chris@0: return $entity->onDependencyRemoval($affected_dependencies); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function findMissingContentDependencies() { Chris@0: $content_dependencies = []; Chris@0: $missing_dependencies = []; Chris@0: foreach ($this->activeStorage->readMultiple($this->activeStorage->listAll()) as $config_data) { Chris@0: if (isset($config_data['dependencies']['content'])) { Chris@0: $content_dependencies = array_merge($content_dependencies, $config_data['dependencies']['content']); Chris@0: } Chris@0: if (isset($config_data['dependencies']['enforced']['content'])) { Chris@0: $content_dependencies = array_merge($content_dependencies, $config_data['dependencies']['enforced']['content']); Chris@0: } Chris@0: } Chris@0: foreach (array_unique($content_dependencies) as $content_dependency) { Chris@0: // Format of the dependency is entity_type:bundle:uuid. Chris@0: list($entity_type, $bundle, $uuid) = explode(':', $content_dependency, 3); Chris@18: if (!$this->entityRepository->loadEntityByUuid($entity_type, $uuid)) { Chris@0: $missing_dependencies[$uuid] = [ Chris@0: 'entity_type' => $entity_type, Chris@0: 'bundle' => $bundle, Chris@0: 'uuid' => $uuid, Chris@0: ]; Chris@0: } Chris@0: } Chris@0: return $missing_dependencies; Chris@0: } Chris@0: Chris@0: }