ConfigManager.php 19 KB

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