ConfigImporter.php 38 KB

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