ConfigManager.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525
  1. <?php
  2. namespace Drupal\Core\Config;
  3. use Drupal\Component\Diff\Diff;
  4. use Drupal\Core\Config\Entity\ConfigDependencyManager;
  5. use Drupal\Core\Config\Entity\ConfigEntityInterface;
  6. use Drupal\Core\Config\Entity\ConfigEntityTypeInterface;
  7. use Drupal\Core\DependencyInjection\DeprecatedServicePropertyTrait;
  8. use Drupal\Core\Entity\EntityManagerInterface;
  9. use Drupal\Core\Entity\EntityRepositoryInterface;
  10. use Drupal\Core\Entity\EntityTypeInterface;
  11. use Drupal\Core\Entity\EntityTypeManagerInterface;
  12. use Drupal\Core\Serialization\Yaml;
  13. use Drupal\Core\StringTranslation\StringTranslationTrait;
  14. use Drupal\Core\StringTranslation\TranslationInterface;
  15. use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  16. /**
  17. * The ConfigManager provides helper functions for the configuration system.
  18. */
  19. class ConfigManager implements ConfigManagerInterface {
  20. use StringTranslationTrait;
  21. use DeprecatedServicePropertyTrait;
  22. use StorageCopyTrait;
  23. /**
  24. * {@inheritdoc}
  25. */
  26. protected $deprecatedProperties = ['entityManager' => 'entity.manager'];
  27. /**
  28. * The entity type manager.
  29. *
  30. * @var \Drupal\Core\Entity\EntityTypeManagerInterface
  31. */
  32. protected $entityTypeManager;
  33. /**
  34. * The entity repository.
  35. *
  36. * @var \Drupal\Core\Entity\EntityRepositoryInterface
  37. */
  38. protected $entityRepository;
  39. /**
  40. * The configuration factory.
  41. *
  42. * @var \Drupal\Core\Config\ConfigFactoryInterface
  43. */
  44. protected $configFactory;
  45. /**
  46. * The typed config manager.
  47. *
  48. * @var \Drupal\Core\Config\TypedConfigManagerInterface
  49. */
  50. protected $typedConfigManager;
  51. /**
  52. * The active configuration storage.
  53. *
  54. * @var \Drupal\Core\Config\StorageInterface
  55. */
  56. protected $activeStorage;
  57. /**
  58. * The event dispatcher.
  59. *
  60. * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
  61. */
  62. protected $eventDispatcher;
  63. /**
  64. * The configuration collection info.
  65. *
  66. * @var \Drupal\Core\Config\ConfigCollectionInfo
  67. */
  68. protected $configCollectionInfo;
  69. /**
  70. * The configuration storages keyed by collection name.
  71. *
  72. * @var \Drupal\Core\Config\StorageInterface[]
  73. */
  74. protected $storages;
  75. /**
  76. * Creates ConfigManager objects.
  77. *
  78. * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
  79. * The entity type manager.
  80. * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
  81. * The configuration factory.
  82. * @param \Drupal\Core\Config\TypedConfigManagerInterface $typed_config_manager
  83. * The typed config manager.
  84. * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
  85. * The string translation service.
  86. * @param \Drupal\Core\Config\StorageInterface $active_storage
  87. * The active configuration storage.
  88. * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
  89. * The event dispatcher.
  90. * @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
  91. * The entity repository.
  92. */
  93. 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) {
  94. if ($entity_type_manager instanceof EntityManagerInterface) {
  95. @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);
  96. $this->entityTypeManager = \Drupal::entityTypeManager();
  97. }
  98. else {
  99. $this->entityTypeManager = $entity_type_manager;
  100. }
  101. $this->configFactory = $config_factory;
  102. $this->typedConfigManager = $typed_config_manager;
  103. $this->stringTranslation = $string_translation;
  104. $this->activeStorage = $active_storage;
  105. $this->eventDispatcher = $event_dispatcher;
  106. if ($entity_repository) {
  107. $this->entityRepository = $entity_repository;
  108. }
  109. else {
  110. @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);
  111. $this->entityRepository = \Drupal::service('entity.repository');
  112. }
  113. }
  114. /**
  115. * {@inheritdoc}
  116. */
  117. public function getEntityTypeIdByName($name) {
  118. $entities = array_filter($this->entityTypeManager->getDefinitions(), function (EntityTypeInterface $entity_type) use ($name) {
  119. return ($entity_type instanceof ConfigEntityTypeInterface && $config_prefix = $entity_type->getConfigPrefix()) && strpos($name, $config_prefix . '.') === 0;
  120. });
  121. return key($entities);
  122. }
  123. /**
  124. * {@inheritdoc}
  125. */
  126. public function loadConfigEntityByName($name) {
  127. $entity_type_id = $this->getEntityTypeIdByName($name);
  128. if ($entity_type_id) {
  129. $entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
  130. $id = substr($name, strlen($entity_type->getConfigPrefix()) + 1);
  131. return $this->entityTypeManager->getStorage($entity_type_id)->load($id);
  132. }
  133. return NULL;
  134. }
  135. /**
  136. * {@inheritdoc}
  137. */
  138. public function getEntityManager() {
  139. @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);
  140. return \Drupal::service('entity.manager');
  141. }
  142. /**
  143. * {@inheritdoc}
  144. */
  145. public function getEntityTypeManager() {
  146. return $this->entityTypeManager;
  147. }
  148. /**
  149. * {@inheritdoc}
  150. */
  151. public function getConfigFactory() {
  152. return $this->configFactory;
  153. }
  154. /**
  155. * {@inheritdoc}
  156. */
  157. public function diff(StorageInterface $source_storage, StorageInterface $target_storage, $source_name, $target_name = NULL, $collection = StorageInterface::DEFAULT_COLLECTION) {
  158. if ($collection != StorageInterface::DEFAULT_COLLECTION) {
  159. $source_storage = $source_storage->createCollection($collection);
  160. $target_storage = $target_storage->createCollection($collection);
  161. }
  162. if (!isset($target_name)) {
  163. $target_name = $source_name;
  164. }
  165. // The output should show configuration object differences formatted as YAML.
  166. // But the configuration is not necessarily stored in files. Therefore, they
  167. // need to be read and parsed, and lastly, dumped into YAML strings.
  168. $source_data = explode("\n", Yaml::encode($source_storage->read($source_name)));
  169. $target_data = explode("\n", Yaml::encode($target_storage->read($target_name)));
  170. // Check for new or removed files.
  171. if ($source_data === ['false']) {
  172. // Added file.
  173. // Cast the result of t() to a string, as the diff engine doesn't know
  174. // about objects.
  175. $source_data = [(string) $this->t('File added')];
  176. }
  177. if ($target_data === ['false']) {
  178. // Deleted file.
  179. // Cast the result of t() to a string, as the diff engine doesn't know
  180. // about objects.
  181. $target_data = [(string) $this->t('File removed')];
  182. }
  183. return new Diff($source_data, $target_data);
  184. }
  185. /**
  186. * {@inheritdoc}
  187. */
  188. public function createSnapshot(StorageInterface $source_storage, StorageInterface $snapshot_storage) {
  189. self::replaceStorageContents($source_storage, $snapshot_storage);
  190. }
  191. /**
  192. * {@inheritdoc}
  193. */
  194. public function uninstall($type, $name) {
  195. $entities = $this->getConfigEntitiesToChangeOnDependencyRemoval($type, [$name], FALSE);
  196. // Fix all dependent configuration entities.
  197. /** @var \Drupal\Core\Config\Entity\ConfigEntityInterface $entity */
  198. foreach ($entities['update'] as $entity) {
  199. $entity->save();
  200. }
  201. // Remove all dependent configuration entities.
  202. foreach ($entities['delete'] as $entity) {
  203. $entity->setUninstalling(TRUE);
  204. $entity->delete();
  205. }
  206. $config_names = $this->configFactory->listAll($name . '.');
  207. foreach ($config_names as $config_name) {
  208. $this->configFactory->getEditable($config_name)->delete();
  209. }
  210. // Remove any matching configuration from collections.
  211. foreach ($this->activeStorage->getAllCollectionNames() as $collection) {
  212. $collection_storage = $this->activeStorage->createCollection($collection);
  213. $collection_storage->deleteAll($name . '.');
  214. }
  215. $schema_dir = drupal_get_path($type, $name) . '/' . InstallStorage::CONFIG_SCHEMA_DIRECTORY;
  216. if (is_dir($schema_dir)) {
  217. // Refresh the schema cache if uninstalling an extension that provides
  218. // configuration schema.
  219. $this->typedConfigManager->clearCachedDefinitions();
  220. }
  221. }
  222. /**
  223. * {@inheritdoc}
  224. */
  225. public function getConfigDependencyManager() {
  226. $dependency_manager = new ConfigDependencyManager();
  227. // Read all configuration using the factory. This ensures that multiple
  228. // deletes during the same request benefit from the static cache. Using the
  229. // factory also ensures configuration entity dependency discovery has no
  230. // dependencies on the config entity classes. Assume data with UUID is a
  231. // config entity. Only configuration entities can be depended on so we can
  232. // ignore everything else.
  233. $data = array_map(function ($config) {
  234. $data = $config->get();
  235. if (isset($data['uuid'])) {
  236. return $data;
  237. }
  238. return FALSE;
  239. }, $this->configFactory->loadMultiple($this->activeStorage->listAll()));
  240. $dependency_manager->setData(array_filter($data));
  241. return $dependency_manager;
  242. }
  243. /**
  244. * {@inheritdoc}
  245. */
  246. public function findConfigEntityDependents($type, array $names, ConfigDependencyManager $dependency_manager = NULL) {
  247. if (!$dependency_manager) {
  248. $dependency_manager = $this->getConfigDependencyManager();
  249. }
  250. $dependencies = [];
  251. foreach ($names as $name) {
  252. $dependencies = array_merge($dependencies, $dependency_manager->getDependentEntities($type, $name));
  253. }
  254. return $dependencies;
  255. }
  256. /**
  257. * {@inheritdoc}
  258. */
  259. public function findConfigEntityDependentsAsEntities($type, array $names, ConfigDependencyManager $dependency_manager = NULL) {
  260. $dependencies = $this->findConfigEntityDependents($type, $names, $dependency_manager);
  261. $entities = [];
  262. $definitions = $this->entityTypeManager->getDefinitions();
  263. foreach ($dependencies as $config_name => $dependency) {
  264. // Group by entity type to efficient load entities using
  265. // \Drupal\Core\Entity\EntityStorageInterface::loadMultiple().
  266. $entity_type_id = $this->getEntityTypeIdByName($config_name);
  267. // It is possible that a non-configuration entity will be returned if a
  268. // simple configuration object has a UUID key. This would occur if the
  269. // dependents of the system module are calculated since system.site has
  270. // a UUID key.
  271. if ($entity_type_id) {
  272. $id = substr($config_name, strlen($definitions[$entity_type_id]->getConfigPrefix()) + 1);
  273. $entities[$entity_type_id][] = $id;
  274. }
  275. }
  276. $entities_to_return = [];
  277. foreach ($entities as $entity_type_id => $entities_to_load) {
  278. $storage = $this->entityTypeManager->getStorage($entity_type_id);
  279. // Remove the keys since there are potential ID clashes from different
  280. // configuration entity types.
  281. $entities_to_return = array_merge($entities_to_return, array_values($storage->loadMultiple($entities_to_load)));
  282. }
  283. return $entities_to_return;
  284. }
  285. /**
  286. * {@inheritdoc}
  287. */
  288. public function getConfigEntitiesToChangeOnDependencyRemoval($type, array $names, $dry_run = TRUE) {
  289. $dependency_manager = $this->getConfigDependencyManager();
  290. // Store the list of dependents in three separate variables. This allows us
  291. // to determine how the dependency graph changes as entities are fixed by
  292. // calling the onDependencyRemoval() method.
  293. // The list of original dependents on $names. This list never changes.
  294. $original_dependents = $this->findConfigEntityDependentsAsEntities($type, $names, $dependency_manager);
  295. // The current list of dependents on $names. This list is recalculated when
  296. // calling an entity's onDependencyRemoval() method results in the entity
  297. // changing. This list is passed to each entity's onDependencyRemoval()
  298. // method as the list of affected entities.
  299. $current_dependents = $original_dependents;
  300. // The list of dependents to process. This list changes as entities are
  301. // processed and are either fixed or deleted.
  302. $dependents_to_process = $original_dependents;
  303. // Initialize other variables.
  304. $affected_uuids = [];
  305. $return = [
  306. 'update' => [],
  307. 'delete' => [],
  308. 'unchanged' => [],
  309. ];
  310. // Try to fix the dependents and find out what will happen to the dependency
  311. // graph. Entities are processed in the order of most dependent first. For
  312. // example, this ensures that Menu UI third party dependencies on node types
  313. // are fixed before processing the node type's other dependents.
  314. while ($dependent = array_pop($dependents_to_process)) {
  315. /** @var \Drupal\Core\Config\Entity\ConfigEntityInterface $dependent */
  316. if ($dry_run) {
  317. // Clone the entity so any changes do not change any static caches.
  318. $dependent = clone $dependent;
  319. }
  320. $fixed = FALSE;
  321. if ($this->callOnDependencyRemoval($dependent, $current_dependents, $type, $names)) {
  322. // Recalculate dependencies and update the dependency graph data.
  323. $dependent->calculateDependencies();
  324. $dependency_manager->updateData($dependent->getConfigDependencyName(), $dependent->getDependencies());
  325. // Based on the updated data rebuild the list of current dependents.
  326. // This will remove entities that are no longer dependent after the
  327. // recalculation.
  328. $current_dependents = $this->findConfigEntityDependentsAsEntities($type, $names, $dependency_manager);
  329. // Rebuild the list of entities that we need to process using the new
  330. // list of current dependents and removing any entities that we've
  331. // already processed.
  332. $dependents_to_process = array_filter($current_dependents, function ($current_dependent) use ($affected_uuids) {
  333. return !in_array($current_dependent->uuid(), $affected_uuids);
  334. });
  335. // Ensure that the dependent has actually been fixed. It is possible
  336. // that other dependencies cause it to still be in the list.
  337. $fixed = TRUE;
  338. foreach ($dependents_to_process as $key => $entity) {
  339. if ($entity->uuid() == $dependent->uuid()) {
  340. $fixed = FALSE;
  341. unset($dependents_to_process[$key]);
  342. break;
  343. }
  344. }
  345. if ($fixed) {
  346. $affected_uuids[] = $dependent->uuid();
  347. $return['update'][] = $dependent;
  348. }
  349. }
  350. // If the entity cannot be fixed then it has to be deleted.
  351. if (!$fixed) {
  352. $affected_uuids[] = $dependent->uuid();
  353. // Deletes should occur in the order of the least dependent first. For
  354. // example, this ensures that fields are removed before field storages.
  355. array_unshift($return['delete'], $dependent);
  356. }
  357. }
  358. // Use the list of affected UUIDs to filter the original list to work out
  359. // which configuration entities are unchanged.
  360. $return['unchanged'] = array_filter($original_dependents, function ($dependent) use ($affected_uuids) {
  361. return !(in_array($dependent->uuid(), $affected_uuids));
  362. });
  363. return $return;
  364. }
  365. /**
  366. * {@inheritdoc}
  367. */
  368. public function getConfigCollectionInfo() {
  369. if (!isset($this->configCollectionInfo)) {
  370. $this->configCollectionInfo = new ConfigCollectionInfo();
  371. $this->eventDispatcher->dispatch(ConfigEvents::COLLECTION_INFO, $this->configCollectionInfo);
  372. }
  373. return $this->configCollectionInfo;
  374. }
  375. /**
  376. * Calls an entity's onDependencyRemoval() method.
  377. *
  378. * A helper method to call onDependencyRemoval() with the correct list of
  379. * affected entities. This list should only contain dependencies on the
  380. * entity. Configuration and content entity dependencies will be converted
  381. * into entity objects.
  382. *
  383. * @param \Drupal\Core\Config\Entity\ConfigEntityInterface $entity
  384. * The entity to call onDependencyRemoval() on.
  385. * @param \Drupal\Core\Config\Entity\ConfigEntityInterface[] $dependent_entities
  386. * The list of dependent configuration entities.
  387. * @param string $type
  388. * The type of dependency being checked. Either 'module', 'theme', 'config'
  389. * or 'content'.
  390. * @param array $names
  391. * The specific names to check. If $type equals 'module' or 'theme' then it
  392. * should be a list of module names or theme names. In the case of 'config'
  393. * or 'content' it should be a list of configuration dependency names.
  394. *
  395. * @return bool
  396. * TRUE if the entity has changed as a result of calling the
  397. * onDependencyRemoval() method, FALSE if not.
  398. */
  399. protected function callOnDependencyRemoval(ConfigEntityInterface $entity, array $dependent_entities, $type, array $names) {
  400. $entity_dependencies = $entity->getDependencies();
  401. if (empty($entity_dependencies)) {
  402. // No dependent entities nothing to do.
  403. return FALSE;
  404. }
  405. $affected_dependencies = [
  406. 'config' => [],
  407. 'content' => [],
  408. 'module' => [],
  409. 'theme' => [],
  410. ];
  411. // Work out if any of the entity's dependencies are going to be affected.
  412. if (isset($entity_dependencies[$type])) {
  413. // Work out which dependencies the entity has in common with the provided
  414. // $type and $names.
  415. $affected_dependencies[$type] = array_intersect($entity_dependencies[$type], $names);
  416. // If the dependencies are entities we need to convert them into objects.
  417. if ($type == 'config' || $type == 'content') {
  418. $affected_dependencies[$type] = array_map(function ($name) use ($type) {
  419. if ($type == 'config') {
  420. return $this->loadConfigEntityByName($name);
  421. }
  422. else {
  423. // Ignore the bundle.
  424. list($entity_type_id,, $uuid) = explode(':', $name);
  425. return $this->entityRepository->loadEntityByConfigTarget($entity_type_id, $uuid);
  426. }
  427. }, $affected_dependencies[$type]);
  428. }
  429. }
  430. // Merge any other configuration entities into the list of affected
  431. // dependencies if necessary.
  432. if (isset($entity_dependencies['config'])) {
  433. foreach ($dependent_entities as $dependent_entity) {
  434. if (in_array($dependent_entity->getConfigDependencyName(), $entity_dependencies['config'])) {
  435. $affected_dependencies['config'][] = $dependent_entity;
  436. }
  437. }
  438. }
  439. // Key the entity arrays by config dependency name to make searching easy.
  440. foreach (['config', 'content'] as $dependency_type) {
  441. $affected_dependencies[$dependency_type] = array_combine(
  442. array_map(function ($entity) {
  443. return $entity->getConfigDependencyName();
  444. }, $affected_dependencies[$dependency_type]),
  445. $affected_dependencies[$dependency_type]
  446. );
  447. }
  448. // Inform the entity.
  449. return $entity->onDependencyRemoval($affected_dependencies);
  450. }
  451. /**
  452. * {@inheritdoc}
  453. */
  454. public function findMissingContentDependencies() {
  455. $content_dependencies = [];
  456. $missing_dependencies = [];
  457. foreach ($this->activeStorage->readMultiple($this->activeStorage->listAll()) as $config_data) {
  458. if (isset($config_data['dependencies']['content'])) {
  459. $content_dependencies = array_merge($content_dependencies, $config_data['dependencies']['content']);
  460. }
  461. if (isset($config_data['dependencies']['enforced']['content'])) {
  462. $content_dependencies = array_merge($content_dependencies, $config_data['dependencies']['enforced']['content']);
  463. }
  464. }
  465. foreach (array_unique($content_dependencies) as $content_dependency) {
  466. // Format of the dependency is entity_type:bundle:uuid.
  467. list($entity_type, $bundle, $uuid) = explode(':', $content_dependency, 3);
  468. if (!$this->entityRepository->loadEntityByUuid($entity_type, $uuid)) {
  469. $missing_dependencies[$uuid] = [
  470. 'entity_type' => $entity_type,
  471. 'bundle' => $bundle,
  472. 'uuid' => $uuid,
  473. ];
  474. }
  475. }
  476. return $missing_dependencies;
  477. }
  478. }