PageRenderTime 62ms CodeModel.GetById 30ms RepoModel.GetById 0ms app.codeStats 1ms

/core/lib/Drupal/Core/Config/ConfigImporter.php

http://github.com/drupal/drupal
PHP | 1075 lines | 483 code | 74 blank | 518 comment | 71 complexity | 7b31eadc0b542b69a09ff457a7bd096d MD5 | raw file
Possible License(s): GPL-2.0, LGPL-2.1
  1. <?php
  2. namespace Drupal\Core\Config;
  3. use Drupal\Core\Config\Importer\MissingContentEvent;
  4. use Drupal\Core\Extension\ModuleExtensionList;
  5. use Drupal\Core\Extension\ModuleHandlerInterface;
  6. use Drupal\Core\Extension\ModuleInstallerInterface;
  7. use Drupal\Core\Extension\ThemeHandlerInterface;
  8. use Drupal\Core\Config\Entity\ImportableEntityStorageInterface;
  9. use Drupal\Core\DependencyInjection\DependencySerializationTrait;
  10. use Drupal\Core\Entity\EntityStorageException;
  11. use Drupal\Core\Lock\LockBackendInterface;
  12. use Drupal\Core\StringTranslation\StringTranslationTrait;
  13. use Drupal\Core\StringTranslation\TranslationInterface;
  14. use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  15. /**
  16. * Defines a configuration importer.
  17. *
  18. * A config importer imports the changes into the configuration system. To
  19. * determine which changes to import a StorageComparer in used.
  20. *
  21. * @see \Drupal\Core\Config\StorageComparerInterface
  22. *
  23. * The ConfigImporter has a identifier which is used to construct event names.
  24. * The events fired during an import are:
  25. * - ConfigEvents::IMPORT_VALIDATE: Events listening can throw a
  26. * \Drupal\Core\Config\ConfigImporterException to prevent an import from
  27. * occurring.
  28. * @see \Drupal\Core\EventSubscriber\ConfigImportSubscriber
  29. * - ConfigEvents::IMPORT: Events listening can react to a successful import.
  30. * @see \Drupal\Core\EventSubscriber\ConfigSnapshotSubscriber
  31. *
  32. * @see \Drupal\Core\Config\ConfigImporterEvent
  33. */
  34. class ConfigImporter {
  35. use StringTranslationTrait;
  36. use DependencySerializationTrait;
  37. /**
  38. * The name used to identify the lock.
  39. */
  40. const LOCK_NAME = 'config_importer';
  41. /**
  42. * The storage comparer used to discover configuration changes.
  43. *
  44. * @var \Drupal\Core\Config\StorageComparerInterface
  45. */
  46. protected $storageComparer;
  47. /**
  48. * The event dispatcher used to notify subscribers.
  49. *
  50. * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
  51. */
  52. protected $eventDispatcher;
  53. /**
  54. * The configuration manager.
  55. *
  56. * @var \Drupal\Core\Config\ConfigManagerInterface
  57. */
  58. protected $configManager;
  59. /**
  60. * The used lock backend instance.
  61. *
  62. * @var \Drupal\Core\Lock\LockBackendInterface
  63. */
  64. protected $lock;
  65. /**
  66. * The typed config manager.
  67. *
  68. * @var \Drupal\Core\Config\TypedConfigManagerInterface
  69. */
  70. protected $typedConfigManager;
  71. /**
  72. * List of configuration file changes processed by the import().
  73. *
  74. * @var array
  75. */
  76. protected $processedConfiguration;
  77. /**
  78. * List of extension changes processed by the import().
  79. *
  80. * @var array
  81. */
  82. protected $processedExtensions;
  83. /**
  84. * List of extension changes to be processed by the import().
  85. *
  86. * @var array
  87. */
  88. protected $extensionChangelist;
  89. /**
  90. * Indicates changes to import have been validated.
  91. *
  92. * @var bool
  93. */
  94. protected $validated;
  95. /**
  96. * The module handler.
  97. *
  98. * @var \Drupal\Core\Extension\ModuleHandlerInterface
  99. */
  100. protected $moduleHandler;
  101. /**
  102. * The theme handler.
  103. *
  104. * @var \Drupal\Core\Extension\ThemeHandlerInterface
  105. */
  106. protected $themeHandler;
  107. /**
  108. * Flag set to import system.theme during processing theme install and uninstalls.
  109. *
  110. * @var bool
  111. */
  112. protected $processedSystemTheme = FALSE;
  113. /**
  114. * A log of any errors encountered.
  115. *
  116. * If errors are logged during the validation event the configuration
  117. * synchronization will not occur. If errors occur during an import then best
  118. * efforts are made to complete the synchronization.
  119. *
  120. * @var array
  121. */
  122. protected $errors = [];
  123. /**
  124. * The total number of extensions to process.
  125. *
  126. * @var int
  127. */
  128. protected $totalExtensionsToProcess = 0;
  129. /**
  130. * The total number of configuration objects to process.
  131. *
  132. * @var int
  133. */
  134. protected $totalConfigurationToProcess = 0;
  135. /**
  136. * The module installer.
  137. *
  138. * @var \Drupal\Core\Extension\ModuleInstallerInterface
  139. */
  140. protected $moduleInstaller;
  141. /**
  142. * The module extension list.
  143. *
  144. * @var \Drupal\Core\Extension\ModuleExtensionList
  145. */
  146. protected $moduleExtensionList;
  147. /**
  148. * Constructs a configuration import object.
  149. *
  150. * @param \Drupal\Core\Config\StorageComparerInterface $storage_comparer
  151. * A storage comparer object used to determine configuration changes and
  152. * access the source and target storage objects.
  153. * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
  154. * The event dispatcher used to notify subscribers of config import events.
  155. * @param \Drupal\Core\Config\ConfigManagerInterface $config_manager
  156. * The configuration manager.
  157. * @param \Drupal\Core\Lock\LockBackendInterface $lock
  158. * The lock backend to ensure multiple imports do not occur at the same time.
  159. * @param \Drupal\Core\Config\TypedConfigManagerInterface $typed_config
  160. * The typed configuration manager.
  161. * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
  162. * The module handler
  163. * @param \Drupal\Core\Extension\ModuleInstallerInterface $module_installer
  164. * The module installer.
  165. * @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
  166. * The theme handler
  167. * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
  168. * The string translation service.
  169. * @param \Drupal\Core\Extension\ModuleExtensionList|null $extension_list_module
  170. * The module extension list. This is left optional for BC reasons, but the
  171. * optional usage is deprecated and will become required in Drupal 9.0.0.
  172. *
  173. * @todo Remove null default value https://www.drupal.org/node/2947083
  174. */
  175. public function __construct(StorageComparerInterface $storage_comparer, EventDispatcherInterface $event_dispatcher, ConfigManagerInterface $config_manager, LockBackendInterface $lock, TypedConfigManagerInterface $typed_config, ModuleHandlerInterface $module_handler, ModuleInstallerInterface $module_installer, ThemeHandlerInterface $theme_handler, TranslationInterface $string_translation, ModuleExtensionList $extension_list_module = NULL) {
  176. if ($extension_list_module === NULL) {
  177. @trigger_error('Invoking the ConfigImporter constructor without the module extension list parameter is deprecated in Drupal 8.8.0 and will no longer be supported in Drupal 9.0.0. The extension list parameter is now required in the ConfigImporter constructor. See https://www.drupal.org/node/2943918', E_USER_DEPRECATED);
  178. $extension_list_module = \Drupal::service('extension.list.module');
  179. }
  180. $this->moduleExtensionList = $extension_list_module;
  181. $this->storageComparer = $storage_comparer;
  182. $this->eventDispatcher = $event_dispatcher;
  183. $this->configManager = $config_manager;
  184. $this->lock = $lock;
  185. $this->typedConfigManager = $typed_config;
  186. $this->moduleHandler = $module_handler;
  187. $this->moduleInstaller = $module_installer;
  188. $this->themeHandler = $theme_handler;
  189. $this->stringTranslation = $string_translation;
  190. foreach ($this->storageComparer->getAllCollectionNames() as $collection) {
  191. $this->processedConfiguration[$collection] = $this->storageComparer->getEmptyChangelist();
  192. }
  193. $this->processedExtensions = $this->getEmptyExtensionsProcessedList();
  194. }
  195. /**
  196. * Logs an error message.
  197. *
  198. * @param string $message
  199. * The message to log.
  200. */
  201. public function logError($message) {
  202. $this->errors[] = $message;
  203. }
  204. /**
  205. * Returns error messages created while running the import.
  206. *
  207. * @return array
  208. * List of messages.
  209. */
  210. public function getErrors() {
  211. return $this->errors;
  212. }
  213. /**
  214. * Gets the configuration storage comparer.
  215. *
  216. * @return \Drupal\Core\Config\StorageComparerInterface
  217. * Storage comparer object used to calculate configuration changes.
  218. */
  219. public function getStorageComparer() {
  220. return $this->storageComparer;
  221. }
  222. /**
  223. * Resets the storage comparer and processed list.
  224. *
  225. * @return $this
  226. * The ConfigImporter instance.
  227. */
  228. public function reset() {
  229. $this->storageComparer->reset();
  230. // Empty all the lists.
  231. foreach ($this->storageComparer->getAllCollectionNames() as $collection) {
  232. $this->processedConfiguration[$collection] = $this->storageComparer->getEmptyChangelist();
  233. }
  234. $this->extensionChangelist = $this->processedExtensions = $this->getEmptyExtensionsProcessedList();
  235. $this->validated = FALSE;
  236. $this->processedSystemTheme = FALSE;
  237. return $this;
  238. }
  239. /**
  240. * Gets an empty list of extensions to process.
  241. *
  242. * @return array
  243. * An empty list of extensions to process.
  244. */
  245. protected function getEmptyExtensionsProcessedList() {
  246. return [
  247. 'module' => [
  248. 'install' => [],
  249. 'uninstall' => [],
  250. ],
  251. 'theme' => [
  252. 'install' => [],
  253. 'uninstall' => [],
  254. ],
  255. ];
  256. }
  257. /**
  258. * Checks if there are any unprocessed configuration changes.
  259. *
  260. * @return bool
  261. * TRUE if there are changes to process and FALSE if not.
  262. */
  263. public function hasUnprocessedConfigurationChanges() {
  264. foreach ($this->storageComparer->getAllCollectionNames() as $collection) {
  265. foreach (['delete', 'create', 'rename', 'update'] as $op) {
  266. if (count($this->getUnprocessedConfiguration($op, $collection))) {
  267. return TRUE;
  268. }
  269. }
  270. }
  271. return FALSE;
  272. }
  273. /**
  274. * Gets list of processed changes.
  275. *
  276. * @param string $collection
  277. * (optional) The configuration collection to get processed changes for.
  278. * Defaults to the default collection.
  279. *
  280. * @return array
  281. * An array containing a list of processed changes.
  282. */
  283. public function getProcessedConfiguration($collection = StorageInterface::DEFAULT_COLLECTION) {
  284. return $this->processedConfiguration[$collection];
  285. }
  286. /**
  287. * Sets a change as processed.
  288. *
  289. * @param string $collection
  290. * The configuration collection to set a change as processed for.
  291. * @param string $op
  292. * The change operation performed, either delete, create, rename, or update.
  293. * @param string $name
  294. * The name of the configuration processed.
  295. */
  296. protected function setProcessedConfiguration($collection, $op, $name) {
  297. $this->processedConfiguration[$collection][$op][] = $name;
  298. }
  299. /**
  300. * Gets a list of unprocessed changes for a given operation.
  301. *
  302. * @param string $op
  303. * The change operation to get the unprocessed list for, either delete,
  304. * create, rename, or update.
  305. * @param string $collection
  306. * (optional) The configuration collection to get unprocessed changes for.
  307. * Defaults to the default collection.
  308. *
  309. * @return array
  310. * An array of configuration names.
  311. */
  312. public function getUnprocessedConfiguration($op, $collection = StorageInterface::DEFAULT_COLLECTION) {
  313. return array_diff($this->storageComparer->getChangelist($op, $collection), $this->processedConfiguration[$collection][$op]);
  314. }
  315. /**
  316. * Gets list of processed extension changes.
  317. *
  318. * @return array
  319. * An array containing a list of processed extension changes.
  320. */
  321. public function getProcessedExtensions() {
  322. return $this->processedExtensions;
  323. }
  324. /**
  325. * Sets an extension change as processed.
  326. *
  327. * @param string $type
  328. * The type of extension, either 'theme' or 'module'.
  329. * @param string $op
  330. * The change operation performed, either install or uninstall.
  331. * @param string $name
  332. * The name of the extension processed.
  333. */
  334. protected function setProcessedExtension($type, $op, $name) {
  335. $this->processedExtensions[$type][$op][] = $name;
  336. }
  337. /**
  338. * Populates the extension change list.
  339. */
  340. protected function createExtensionChangelist() {
  341. // Create an empty changelist.
  342. $this->extensionChangelist = $this->getEmptyExtensionsProcessedList();
  343. // Read the extensions information to determine changes.
  344. $current_extensions = $this->storageComparer->getTargetStorage()->read('core.extension');
  345. $new_extensions = $this->storageComparer->getSourceStorage()->read('core.extension');
  346. // If there is no extension information in sync then exit. This is probably
  347. // due to an empty sync directory.
  348. if (!$new_extensions) {
  349. return;
  350. }
  351. // Get a list of modules with dependency weights as values.
  352. $module_data = $this->moduleExtensionList->getList();
  353. // Set the actual module weights.
  354. $module_list = array_combine(array_keys($module_data), array_keys($module_data));
  355. $module_list = array_map(function ($module) use ($module_data) {
  356. return $module_data[$module]->sort;
  357. }, $module_list);
  358. // Determine which modules to uninstall.
  359. $uninstall = array_keys(array_diff_key($current_extensions['module'], $new_extensions['module']));
  360. // Sort the list of newly uninstalled extensions by their weights, so that
  361. // dependencies are uninstalled last. Extensions of the same weight are
  362. // sorted in reverse alphabetical order, to ensure the order is exactly
  363. // opposite from installation. For example, this module list:
  364. // array(
  365. // 'actions' => 0,
  366. // 'ban' => 0,
  367. // 'options' => -2,
  368. // 'text' => -1,
  369. // );
  370. // will result in the following sort order:
  371. // -2 options
  372. // -1 text
  373. // 0 0 ban
  374. // 0 1 actions
  375. // @todo Move this sorting functionality to the extension system.
  376. array_multisort(array_values($module_list), SORT_ASC, array_keys($module_list), SORT_DESC, $module_list);
  377. $this->extensionChangelist['module']['uninstall'] = array_intersect(array_keys($module_list), $uninstall);
  378. // Determine which modules to install.
  379. $install = array_keys(array_diff_key($new_extensions['module'], $current_extensions['module']));
  380. // Ensure that installed modules are sorted in exactly the reverse order
  381. // (with dependencies installed first, and modules of the same weight sorted
  382. // in alphabetical order).
  383. $module_list = array_reverse($module_list);
  384. $this->extensionChangelist['module']['install'] = array_intersect(array_keys($module_list), $install);
  385. // If we're installing the install profile ensure it comes last. This will
  386. // occur when installing a site from configuration.
  387. $install_profile_key = array_search($new_extensions['profile'], $this->extensionChangelist['module']['install'], TRUE);
  388. if ($install_profile_key !== FALSE) {
  389. unset($this->extensionChangelist['module']['install'][$install_profile_key]);
  390. $this->extensionChangelist['module']['install'][] = $new_extensions['profile'];
  391. }
  392. // Work out what themes to install and to uninstall.
  393. $this->extensionChangelist['theme']['install'] = array_keys(array_diff_key($new_extensions['theme'], $current_extensions['theme']));
  394. $this->extensionChangelist['theme']['uninstall'] = array_keys(array_diff_key($current_extensions['theme'], $new_extensions['theme']));
  395. }
  396. /**
  397. * Gets a list changes for extensions.
  398. *
  399. * @param string $type
  400. * The type of extension, either 'theme' or 'module'.
  401. * @param string $op
  402. * The change operation to get the unprocessed list for, either install
  403. * or uninstall.
  404. *
  405. * @return array
  406. * An array of extension names.
  407. */
  408. public function getExtensionChangelist($type, $op = NULL) {
  409. if ($op) {
  410. return $this->extensionChangelist[$type][$op];
  411. }
  412. return $this->extensionChangelist[$type];
  413. }
  414. /**
  415. * Gets a list of unprocessed changes for extensions.
  416. *
  417. * @param string $type
  418. * The type of extension, either 'theme' or 'module'.
  419. *
  420. * @return array
  421. * An array of extension names.
  422. */
  423. protected function getUnprocessedExtensions($type) {
  424. $changelist = $this->getExtensionChangelist($type);
  425. return [
  426. 'install' => array_diff($changelist['install'], $this->processedExtensions[$type]['install']),
  427. 'uninstall' => array_diff($changelist['uninstall'], $this->processedExtensions[$type]['uninstall']),
  428. ];
  429. }
  430. /**
  431. * Imports the changelist to the target storage.
  432. *
  433. * @return $this
  434. * The ConfigImporter instance.
  435. *
  436. * @throws \Drupal\Core\Config\ConfigException
  437. */
  438. public function import() {
  439. if ($this->hasUnprocessedConfigurationChanges()) {
  440. $sync_steps = $this->initialize();
  441. foreach ($sync_steps as $step) {
  442. $context = [];
  443. do {
  444. $this->doSyncStep($step, $context);
  445. } while ($context['finished'] < 1);
  446. }
  447. }
  448. return $this;
  449. }
  450. /**
  451. * Calls a config import step.
  452. *
  453. * @param string|callable $sync_step
  454. * The step to do. Either a method on the ConfigImporter class or a
  455. * callable.
  456. * @param array $context
  457. * A batch context array. If the config importer is not running in a batch
  458. * the only array key that is used is $context['finished']. A process needs
  459. * to set $context['finished'] = 1 when it is done.
  460. *
  461. * @throws \InvalidArgumentException
  462. * Exception thrown if the $sync_step can not be called.
  463. */
  464. public function doSyncStep($sync_step, &$context) {
  465. if (!is_array($sync_step) && method_exists($this, $sync_step)) {
  466. \Drupal::service('config.installer')->setSyncing(TRUE);
  467. $this->$sync_step($context);
  468. }
  469. elseif (is_callable($sync_step)) {
  470. \Drupal::service('config.installer')->setSyncing(TRUE);
  471. call_user_func_array($sync_step, [&$context, $this]);
  472. }
  473. else {
  474. throw new \InvalidArgumentException('Invalid configuration synchronization step');
  475. }
  476. \Drupal::service('config.installer')->setSyncing(FALSE);
  477. }
  478. /**
  479. * Initializes the config importer in preparation for processing a batch.
  480. *
  481. * @return array
  482. * An array of \Drupal\Core\Config\ConfigImporter method names and callables
  483. * that are invoked to complete the import. If there are modules or themes
  484. * to process then an extra step is added.
  485. *
  486. * @throws \Drupal\Core\Config\ConfigImporterException
  487. * If the configuration is already importing.
  488. */
  489. public function initialize() {
  490. // Ensure that the changes have been validated.
  491. $this->validate();
  492. if (!$this->lock->acquire(static::LOCK_NAME)) {
  493. // Another process is synchronizing configuration.
  494. throw new ConfigImporterException(sprintf('%s is already importing', static::LOCK_NAME));
  495. }
  496. $sync_steps = [];
  497. $modules = $this->getUnprocessedExtensions('module');
  498. foreach (['install', 'uninstall'] as $op) {
  499. $this->totalExtensionsToProcess += count($modules[$op]);
  500. }
  501. $themes = $this->getUnprocessedExtensions('theme');
  502. foreach (['install', 'uninstall'] as $op) {
  503. $this->totalExtensionsToProcess += count($themes[$op]);
  504. }
  505. // We have extensions to process.
  506. if ($this->totalExtensionsToProcess > 0) {
  507. $sync_steps[] = 'processExtensions';
  508. }
  509. $sync_steps[] = 'processConfigurations';
  510. $sync_steps[] = 'processMissingContent';
  511. // Allow modules to add new steps to configuration synchronization.
  512. $this->moduleHandler->alter('config_import_steps', $sync_steps, $this);
  513. $sync_steps[] = 'finish';
  514. return $sync_steps;
  515. }
  516. /**
  517. * Processes extensions as a batch operation.
  518. *
  519. * @param array|\ArrayAccess $context
  520. * The batch context.
  521. */
  522. protected function processExtensions(&$context) {
  523. $operation = $this->getNextExtensionOperation();
  524. if (!empty($operation)) {
  525. $this->processExtension($operation['type'], $operation['op'], $operation['name']);
  526. $context['message'] = t('Synchronizing extensions: @op @name.', ['@op' => $operation['op'], '@name' => $operation['name']]);
  527. $processed_count = count($this->processedExtensions['module']['install']) + count($this->processedExtensions['module']['uninstall']);
  528. $processed_count += count($this->processedExtensions['theme']['uninstall']) + count($this->processedExtensions['theme']['install']);
  529. $context['finished'] = $processed_count / $this->totalExtensionsToProcess;
  530. }
  531. else {
  532. $context['finished'] = 1;
  533. }
  534. }
  535. /**
  536. * Processes configuration as a batch operation.
  537. *
  538. * @param array|\ArrayAccess $context
  539. * The batch context.
  540. */
  541. protected function processConfigurations(&$context) {
  542. // The first time this is called we need to calculate the total to process.
  543. // This involves recalculating the changelist which will ensure that if
  544. // extensions have been processed any configuration affected will be taken
  545. // into account.
  546. if ($this->totalConfigurationToProcess == 0) {
  547. $this->storageComparer->reset();
  548. foreach ($this->storageComparer->getAllCollectionNames() as $collection) {
  549. foreach (['delete', 'create', 'rename', 'update'] as $op) {
  550. $this->totalConfigurationToProcess += count($this->getUnprocessedConfiguration($op, $collection));
  551. }
  552. }
  553. }
  554. $operation = $this->getNextConfigurationOperation();
  555. if (!empty($operation)) {
  556. if ($this->checkOp($operation['collection'], $operation['op'], $operation['name'])) {
  557. $this->processConfiguration($operation['collection'], $operation['op'], $operation['name']);
  558. }
  559. if ($operation['collection'] == StorageInterface::DEFAULT_COLLECTION) {
  560. $context['message'] = $this->t('Synchronizing configuration: @op @name.', ['@op' => $operation['op'], '@name' => $operation['name']]);
  561. }
  562. else {
  563. $context['message'] = $this->t('Synchronizing configuration: @op @name in @collection.', ['@op' => $operation['op'], '@name' => $operation['name'], '@collection' => $operation['collection']]);
  564. }
  565. $processed_count = 0;
  566. foreach ($this->storageComparer->getAllCollectionNames() as $collection) {
  567. foreach (['delete', 'create', 'rename', 'update'] as $op) {
  568. $processed_count += count($this->processedConfiguration[$collection][$op]);
  569. }
  570. }
  571. $context['finished'] = $processed_count / $this->totalConfigurationToProcess;
  572. }
  573. else {
  574. $context['finished'] = 1;
  575. }
  576. }
  577. /**
  578. * Handles processing of missing content.
  579. *
  580. * @param array|\ArrayAccess $context
  581. * Standard batch context.
  582. */
  583. protected function processMissingContent(&$context) {
  584. $sandbox = &$context['sandbox']['config'];
  585. if (!isset($sandbox['missing_content'])) {
  586. $missing_content = $this->configManager->findMissingContentDependencies();
  587. $sandbox['missing_content']['data'] = $missing_content;
  588. $sandbox['missing_content']['total'] = count($missing_content);
  589. }
  590. else {
  591. $missing_content = $sandbox['missing_content']['data'];
  592. }
  593. if (!empty($missing_content)) {
  594. $event = new MissingContentEvent($missing_content);
  595. // Fire an event to allow listeners to create the missing content.
  596. $this->eventDispatcher->dispatch(ConfigEvents::IMPORT_MISSING_CONTENT, $event);
  597. $sandbox['missing_content']['data'] = $event->getMissingContent();
  598. }
  599. $current_count = count($sandbox['missing_content']['data']);
  600. if ($current_count) {
  601. $context['message'] = $this->t('Resolving missing content');
  602. $context['finished'] = ($sandbox['missing_content']['total'] - $current_count) / $sandbox['missing_content']['total'];
  603. }
  604. else {
  605. $context['finished'] = 1;
  606. }
  607. }
  608. /**
  609. * Finishes the batch.
  610. *
  611. * @param array|\ArrayAccess $context
  612. * The batch context.
  613. */
  614. protected function finish(&$context) {
  615. $this->eventDispatcher->dispatch(ConfigEvents::IMPORT, new ConfigImporterEvent($this));
  616. // The import is now complete.
  617. $this->lock->release(static::LOCK_NAME);
  618. $this->reset();
  619. $context['message'] = t('Finalizing configuration synchronization.');
  620. $context['finished'] = 1;
  621. }
  622. /**
  623. * Gets the next extension operation to perform.
  624. *
  625. * @return array|bool
  626. * An array containing the next operation and extension name to perform it
  627. * on. If there is nothing left to do returns FALSE;
  628. */
  629. protected function getNextExtensionOperation() {
  630. foreach (['module', 'theme'] as $type) {
  631. foreach (['install', 'uninstall'] as $op) {
  632. $unprocessed = $this->getUnprocessedExtensions($type);
  633. if (!empty($unprocessed[$op])) {
  634. return [
  635. 'op' => $op,
  636. 'type' => $type,
  637. 'name' => array_shift($unprocessed[$op]),
  638. ];
  639. }
  640. }
  641. }
  642. return FALSE;
  643. }
  644. /**
  645. * Gets the next configuration operation to perform.
  646. *
  647. * @return array|bool
  648. * An array containing the next operation and configuration name to perform
  649. * it on. If there is nothing left to do returns FALSE;
  650. */
  651. protected function getNextConfigurationOperation() {
  652. // The order configuration operations is processed is important. Deletes
  653. // have to come first so that recreates can work.
  654. foreach ($this->storageComparer->getAllCollectionNames() as $collection) {
  655. foreach (['delete', 'create', 'rename', 'update'] as $op) {
  656. $config_names = $this->getUnprocessedConfiguration($op, $collection);
  657. if (!empty($config_names)) {
  658. return [
  659. 'op' => $op,
  660. 'name' => array_shift($config_names),
  661. 'collection' => $collection,
  662. ];
  663. }
  664. }
  665. }
  666. return FALSE;
  667. }
  668. /**
  669. * Dispatches validate event for a ConfigImporter object.
  670. *
  671. * Events should throw a \Drupal\Core\Config\ConfigImporterException to
  672. * prevent an import from occurring.
  673. *
  674. * @throws \Drupal\Core\Config\ConfigImporterException
  675. * Exception thrown if the validate event logged any errors.
  676. */
  677. public function validate() {
  678. if (!$this->validated) {
  679. // Create the list of installs and uninstalls.
  680. $this->createExtensionChangelist();
  681. // Validate renames.
  682. foreach ($this->getUnprocessedConfiguration('rename') as $name) {
  683. $names = $this->storageComparer->extractRenameNames($name);
  684. $old_entity_type_id = $this->configManager->getEntityTypeIdByName($names['old_name']);
  685. $new_entity_type_id = $this->configManager->getEntityTypeIdByName($names['new_name']);
  686. if ($old_entity_type_id != $new_entity_type_id) {
  687. $this->logError($this->t('Entity type mismatch on rename. @old_type not equal to @new_type for existing configuration @old_name and staged configuration @new_name.', ['@old_type' => $old_entity_type_id, '@new_type' => $new_entity_type_id, '@old_name' => $names['old_name'], '@new_name' => $names['new_name']]));
  688. }
  689. // Has to be a configuration entity.
  690. if (!$old_entity_type_id) {
  691. $this->logError($this->t('Rename operation for simple configuration. Existing configuration @old_name and staged configuration @new_name.', ['@old_name' => $names['old_name'], '@new_name' => $names['new_name']]));
  692. }
  693. }
  694. $this->eventDispatcher->dispatch(ConfigEvents::IMPORT_VALIDATE, new ConfigImporterEvent($this));
  695. if (count($this->getErrors())) {
  696. $errors = array_merge(['There were errors validating the config synchronization.'], $this->getErrors());
  697. throw new ConfigImporterException(implode(PHP_EOL, $errors));
  698. }
  699. else {
  700. $this->validated = TRUE;
  701. }
  702. }
  703. return $this;
  704. }
  705. /**
  706. * Processes a configuration change.
  707. *
  708. * @param string $collection
  709. * The configuration collection to process changes for.
  710. * @param string $op
  711. * The change operation.
  712. * @param string $name
  713. * The name of the configuration to process.
  714. *
  715. * @throws \Exception
  716. * Thrown when the import process fails, only thrown when no importer log is
  717. * set, otherwise the exception message is logged and the configuration
  718. * is skipped.
  719. */
  720. protected function processConfiguration($collection, $op, $name) {
  721. try {
  722. $processed = FALSE;
  723. if ($collection == StorageInterface::DEFAULT_COLLECTION) {
  724. $processed = $this->importInvokeOwner($collection, $op, $name);
  725. }
  726. if (!$processed) {
  727. $this->importConfig($collection, $op, $name);
  728. }
  729. }
  730. catch (\Exception $e) {
  731. $this->logError($this->t('Unexpected error during import with operation @op for @name: @message', ['@op' => $op, '@name' => $name, '@message' => $e->getMessage()]));
  732. // Error for that operation was logged, mark it as processed so that
  733. // the import can continue.
  734. $this->setProcessedConfiguration($collection, $op, $name);
  735. }
  736. }
  737. /**
  738. * Processes an extension change.
  739. *
  740. * @param string $type
  741. * The type of extension, either 'module' or 'theme'.
  742. * @param string $op
  743. * The change operation.
  744. * @param string $name
  745. * The name of the extension to process.
  746. */
  747. protected function processExtension($type, $op, $name) {
  748. // Set the config installer to use the sync directory instead of the
  749. // extensions own default config directories.
  750. \Drupal::service('config.installer')
  751. ->setSourceStorage($this->storageComparer->getSourceStorage());
  752. if ($type == 'module') {
  753. $this->moduleInstaller->$op([$name], FALSE);
  754. // Installing a module can cause a kernel boot therefore reinject all the
  755. // services.
  756. $this->reInjectMe();
  757. // During a module install or uninstall the container is rebuilt and the
  758. // module handler is called. This causes the container's instance of the
  759. // module handler not to have loaded all the enabled modules.
  760. $this->moduleHandler->loadAll();
  761. }
  762. if ($type == 'theme') {
  763. // Theme uninstalls possible remove default or admin themes therefore we
  764. // need to import this before doing any. If there are no uninstalls and
  765. // the default or admin theme is changing this will be picked up whilst
  766. // processing configuration.
  767. if ($op == 'uninstall' && $this->processedSystemTheme === FALSE) {
  768. $this->importConfig(StorageInterface::DEFAULT_COLLECTION, 'update', 'system.theme');
  769. $this->configManager->getConfigFactory()->reset('system.theme');
  770. $this->processedSystemTheme = TRUE;
  771. }
  772. \Drupal::service('theme_installer')->$op([$name]);
  773. }
  774. $this->setProcessedExtension($type, $op, $name);
  775. }
  776. /**
  777. * Checks that the operation is still valid.
  778. *
  779. * During a configuration import secondary writes and deletes are possible.
  780. * This method checks that the operation is still valid before processing a
  781. * configuration change.
  782. *
  783. * @param string $collection
  784. * The configuration collection.
  785. * @param string $op
  786. * The change operation.
  787. * @param string $name
  788. * The name of the configuration to process.
  789. *
  790. * @return bool
  791. * TRUE is to continue processing, FALSE otherwise.
  792. *
  793. * @throws \Drupal\Core\Config\ConfigImporterException
  794. */
  795. protected function checkOp($collection, $op, $name) {
  796. if ($op == 'rename') {
  797. $names = $this->storageComparer->extractRenameNames($name);
  798. $target_exists = $this->storageComparer->getTargetStorage($collection)->exists($names['new_name']);
  799. if ($target_exists) {
  800. // If the target exists, the rename has already occurred as the
  801. // result of a secondary configuration write. Change the operation
  802. // into an update. This is the desired behavior since renames often
  803. // have to occur together. For example, renaming a node type must
  804. // also result in renaming its fields and entity displays.
  805. $this->storageComparer->moveRenameToUpdate($name);
  806. return FALSE;
  807. }
  808. return TRUE;
  809. }
  810. $target_exists = $this->storageComparer->getTargetStorage($collection)->exists($name);
  811. switch ($op) {
  812. case 'delete':
  813. if (!$target_exists) {
  814. // The configuration has already been deleted. For example, a field
  815. // is automatically deleted if all the instances are.
  816. $this->setProcessedConfiguration($collection, $op, $name);
  817. return FALSE;
  818. }
  819. break;
  820. case 'create':
  821. if ($target_exists) {
  822. // If the target already exists, use the entity storage to delete it
  823. // again, if is a simple config, delete it directly.
  824. if ($entity_type_id = $this->configManager->getEntityTypeIdByName($name)) {
  825. $entity_storage = $this->configManager->getEntityTypeManager()->getStorage($entity_type_id);
  826. $entity_type = $this->configManager->getEntityTypeManager()->getDefinition($entity_type_id);
  827. $entity = $entity_storage->load($entity_storage->getIDFromConfigName($name, $entity_type->getConfigPrefix()));
  828. $entity->delete();
  829. $this->logError($this->t('Deleted and replaced configuration entity "@name"', ['@name' => $name]));
  830. }
  831. else {
  832. $this->storageComparer->getTargetStorage($collection)->delete($name);
  833. $this->logError($this->t('Deleted and replaced configuration "@name"', ['@name' => $name]));
  834. }
  835. return TRUE;
  836. }
  837. break;
  838. case 'update':
  839. if (!$target_exists) {
  840. $this->logError($this->t('Update target "@name" is missing.', ['@name' => $name]));
  841. // Mark as processed so that the synchronization continues. Once the
  842. // the current synchronization is complete it will show up as a
  843. // create.
  844. $this->setProcessedConfiguration($collection, $op, $name);
  845. return FALSE;
  846. }
  847. break;
  848. }
  849. return TRUE;
  850. }
  851. /**
  852. * Writes a configuration change from the source to the target storage.
  853. *
  854. * @param string $collection
  855. * The configuration collection.
  856. * @param string $op
  857. * The change operation.
  858. * @param string $name
  859. * The name of the configuration to process.
  860. */
  861. protected function importConfig($collection, $op, $name) {
  862. // Allow config factory overriders to use a custom configuration object if
  863. // they are responsible for the collection.
  864. $overrider = $this->configManager->getConfigCollectionInfo()->getOverrideService($collection);
  865. if ($overrider) {
  866. $config = $overrider->createConfigObject($name, $collection);
  867. }
  868. else {
  869. $config = new Config($name, $this->storageComparer->getTargetStorage($collection), $this->eventDispatcher, $this->typedConfigManager);
  870. }
  871. if ($op == 'delete') {
  872. $config->delete();
  873. }
  874. else {
  875. $data = $this->storageComparer->getSourceStorage($collection)->read($name);
  876. $config->setData($data ? $data : []);
  877. $config->save();
  878. }
  879. $this->setProcessedConfiguration($collection, $op, $name);
  880. }
  881. /**
  882. * Invokes import* methods on configuration entity storage.
  883. *
  884. * Allow modules to take over configuration change operations for higher-level
  885. * configuration data.
  886. *
  887. * @todo Add support for other extension types; e.g., themes etc.
  888. *
  889. * @param string $collection
  890. * The configuration collection.
  891. * @param string $op
  892. * The change operation to get the unprocessed list for, either delete,
  893. * create, rename, or update.
  894. * @param string $name
  895. * The name of the configuration to process.
  896. *
  897. * @return bool
  898. * TRUE if the configuration was imported as a configuration entity. FALSE
  899. * otherwise.
  900. *
  901. * @throws \Drupal\Core\Entity\EntityStorageException
  902. * Thrown if the data is owned by an entity type, but the entity storage
  903. * does not support imports.
  904. */
  905. protected function importInvokeOwner($collection, $op, $name) {
  906. // Renames are handled separately.
  907. if ($op == 'rename') {
  908. return $this->importInvokeRename($collection, $name);
  909. }
  910. // Validate the configuration object name before importing it.
  911. // Config::validateName($name);
  912. if ($entity_type = $this->configManager->getEntityTypeIdByName($name)) {
  913. $old_config = new Config($name, $this->storageComparer->getTargetStorage($collection), $this->eventDispatcher, $this->typedConfigManager);
  914. if ($old_data = $this->storageComparer->getTargetStorage($collection)->read($name)) {
  915. $old_config->initWithData($old_data);
  916. }
  917. $data = $this->storageComparer->getSourceStorage($collection)->read($name);
  918. $new_config = new Config($name, $this->storageComparer->getTargetStorage($collection), $this->eventDispatcher, $this->typedConfigManager);
  919. if ($data !== FALSE) {
  920. $new_config->setData($data);
  921. }
  922. $method = 'import' . ucfirst($op);
  923. $entity_storage = $this->configManager->getEntityTypeManager()->getStorage($entity_type);
  924. // Call to the configuration entity's storage to handle the configuration
  925. // change.
  926. if (!($entity_storage instanceof ImportableEntityStorageInterface)) {
  927. throw new EntityStorageException(sprintf('The entity storage "%s" for the "%s" entity type does not support imports', get_class($entity_storage), $entity_type));
  928. }
  929. $entity_storage->$method($name, $new_config, $old_config);
  930. $this->setProcessedConfiguration($collection, $op, $name);
  931. return TRUE;
  932. }
  933. return FALSE;
  934. }
  935. /**
  936. * Imports a configuration entity rename.
  937. *
  938. * @param string $collection
  939. * The configuration collection.
  940. * @param string $rename_name
  941. * The rename configuration name, as provided by
  942. * \Drupal\Core\Config\StorageComparer::createRenameName().
  943. *
  944. * @return bool
  945. * TRUE if the configuration was imported as a configuration entity. FALSE
  946. * otherwise.
  947. *
  948. * @throws \Drupal\Core\Entity\EntityStorageException
  949. * Thrown if the data is owned by an entity type, but the entity storage
  950. * does not support imports.
  951. *
  952. * @see \Drupal\Core\Config\ConfigImporter::createRenameName()
  953. */
  954. protected function importInvokeRename($collection, $rename_name) {
  955. $names = $this->storageComparer->extractRenameNames($rename_name);
  956. $entity_type_id = $this->configManager->getEntityTypeIdByName($names['old_name']);
  957. $old_config = new Config($names['old_name'], $this->storageComparer->getTargetStorage($collection), $this->eventDispatcher, $this->typedConfigManager);
  958. if ($old_data = $this->storageComparer->getTargetStorage($collection)->read($names['old_name'])) {
  959. $old_config->initWithData($old_data);
  960. }
  961. $data = $this->storageComparer->getSourceStorage($collection)->read($names['new_name']);
  962. $new_config = new Config($names['new_name'], $this->storageComparer->getTargetStorage($collection), $this->eventDispatcher, $this->typedConfigManager);
  963. if ($data !== FALSE) {
  964. $new_config->setData($data);
  965. }
  966. $entity_storage = $this->configManager->getEntityTypeManager()->getStorage($entity_type_id);
  967. // Call to the configuration entity's storage to handle the configuration
  968. // change.
  969. if (!($entity_storage instanceof ImportableEntityStorageInterface)) {
  970. throw new EntityStorageException(sprintf("The entity storage '%s' for the '%s' entity type does not support imports", get_class($entity_storage), $entity_type_id));
  971. }
  972. $entity_storage->importRename($names['old_name'], $new_config, $old_config);
  973. $this->setProcessedConfiguration($collection, 'rename', $rename_name);
  974. return TRUE;
  975. }
  976. /**
  977. * Determines if a import is already running.
  978. *
  979. * @return bool
  980. * TRUE if an import is already running, FALSE if not.
  981. */
  982. public function alreadyImporting() {
  983. return !$this->lock->lockMayBeAvailable(static::LOCK_NAME);
  984. }
  985. /**
  986. * Gets all the service dependencies from \Drupal.
  987. *
  988. * Since the ConfigImporter handles module installation the kernel and the
  989. * container can be rebuilt and altered during processing. It is necessary to
  990. * keep the services used by the importer in sync.
  991. */
  992. protected function reInjectMe() {
  993. $this->_serviceIds = [];
  994. $vars = get_object_vars($this);
  995. foreach ($vars as $key => $value) {
  996. if (is_object($value) && isset($value->_serviceId)) {
  997. $this->$key = \Drupal::service($value->_serviceId);
  998. }
  999. }
  1000. }
  1001. }