ConfigImporter.php 37 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048
  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. // Work out what themes to install and to uninstall.
  369. $this->extensionChangelist['theme']['install'] = array_keys(array_diff_key($new_extensions['theme'], $current_extensions['theme']));
  370. $this->extensionChangelist['theme']['uninstall'] = array_keys(array_diff_key($current_extensions['theme'], $new_extensions['theme']));
  371. }
  372. /**
  373. * Gets a list changes for extensions.
  374. *
  375. * @param string $type
  376. * The type of extension, either 'theme' or 'module'.
  377. * @param string $op
  378. * The change operation to get the unprocessed list for, either install
  379. * or uninstall.
  380. *
  381. * @return array
  382. * An array of extension names.
  383. */
  384. public function getExtensionChangelist($type, $op = NULL) {
  385. if ($op) {
  386. return $this->extensionChangelist[$type][$op];
  387. }
  388. return $this->extensionChangelist[$type];
  389. }
  390. /**
  391. * Gets a list of unprocessed changes for extensions.
  392. *
  393. * @param string $type
  394. * The type of extension, either 'theme' or 'module'.
  395. *
  396. * @return array
  397. * An array of extension names.
  398. */
  399. protected function getUnprocessedExtensions($type) {
  400. $changelist = $this->getExtensionChangelist($type);
  401. return [
  402. 'install' => array_diff($changelist['install'], $this->processedExtensions[$type]['install']),
  403. 'uninstall' => array_diff($changelist['uninstall'], $this->processedExtensions[$type]['uninstall']),
  404. ];
  405. }
  406. /**
  407. * Imports the changelist to the target storage.
  408. *
  409. * @return \Drupal\Core\Config\ConfigImporter
  410. * The ConfigImporter instance.
  411. *
  412. * @throws \Drupal\Core\Config\ConfigException
  413. */
  414. public function import() {
  415. if ($this->hasUnprocessedConfigurationChanges()) {
  416. $sync_steps = $this->initialize();
  417. foreach ($sync_steps as $step) {
  418. $context = [];
  419. do {
  420. $this->doSyncStep($step, $context);
  421. } while ($context['finished'] < 1);
  422. }
  423. }
  424. return $this;
  425. }
  426. /**
  427. * Calls a config import step.
  428. *
  429. * @param string|callable $sync_step
  430. * The step to do. Either a method on the ConfigImporter class or a
  431. * callable.
  432. * @param array $context
  433. * A batch context array. If the config importer is not running in a batch
  434. * the only array key that is used is $context['finished']. A process needs
  435. * to set $context['finished'] = 1 when it is done.
  436. *
  437. * @throws \InvalidArgumentException
  438. * Exception thrown if the $sync_step can not be called.
  439. */
  440. public function doSyncStep($sync_step, &$context) {
  441. if (!is_array($sync_step) && method_exists($this, $sync_step)) {
  442. \Drupal::service('config.installer')->setSyncing(TRUE);
  443. $this->$sync_step($context);
  444. }
  445. elseif (is_callable($sync_step)) {
  446. \Drupal::service('config.installer')->setSyncing(TRUE);
  447. call_user_func_array($sync_step, [&$context, $this]);
  448. }
  449. else {
  450. throw new \InvalidArgumentException('Invalid configuration synchronization step');
  451. }
  452. \Drupal::service('config.installer')->setSyncing(FALSE);
  453. }
  454. /**
  455. * Initializes the config importer in preparation for processing a batch.
  456. *
  457. * @return array
  458. * An array of \Drupal\Core\Config\ConfigImporter method names and callables
  459. * that are invoked to complete the import. If there are modules or themes
  460. * to process then an extra step is added.
  461. *
  462. * @throws \Drupal\Core\Config\ConfigImporterException
  463. * If the configuration is already importing.
  464. */
  465. public function initialize() {
  466. // Ensure that the changes have been validated.
  467. $this->validate();
  468. if (!$this->lock->acquire(static::LOCK_NAME)) {
  469. // Another process is synchronizing configuration.
  470. throw new ConfigImporterException(sprintf('%s is already importing', static::LOCK_NAME));
  471. }
  472. $sync_steps = [];
  473. $modules = $this->getUnprocessedExtensions('module');
  474. foreach (['install', 'uninstall'] as $op) {
  475. $this->totalExtensionsToProcess += count($modules[$op]);
  476. }
  477. $themes = $this->getUnprocessedExtensions('theme');
  478. foreach (['install', 'uninstall'] as $op) {
  479. $this->totalExtensionsToProcess += count($themes[$op]);
  480. }
  481. // We have extensions to process.
  482. if ($this->totalExtensionsToProcess > 0) {
  483. $sync_steps[] = 'processExtensions';
  484. }
  485. $sync_steps[] = 'processConfigurations';
  486. $sync_steps[] = 'processMissingContent';
  487. // Allow modules to add new steps to configuration synchronization.
  488. $this->moduleHandler->alter('config_import_steps', $sync_steps, $this);
  489. $sync_steps[] = 'finish';
  490. return $sync_steps;
  491. }
  492. /**
  493. * Processes extensions as a batch operation.
  494. *
  495. * @param array|\ArrayAccess $context
  496. * The batch context.
  497. */
  498. protected function processExtensions(&$context) {
  499. $operation = $this->getNextExtensionOperation();
  500. if (!empty($operation)) {
  501. $this->processExtension($operation['type'], $operation['op'], $operation['name']);
  502. $context['message'] = t('Synchronizing extensions: @op @name.', ['@op' => $operation['op'], '@name' => $operation['name']]);
  503. $processed_count = count($this->processedExtensions['module']['install']) + count($this->processedExtensions['module']['uninstall']);
  504. $processed_count += count($this->processedExtensions['theme']['uninstall']) + count($this->processedExtensions['theme']['install']);
  505. $context['finished'] = $processed_count / $this->totalExtensionsToProcess;
  506. }
  507. else {
  508. $context['finished'] = 1;
  509. }
  510. }
  511. /**
  512. * Processes configuration as a batch operation.
  513. *
  514. * @param array|\ArrayAccess $context
  515. * The batch context.
  516. */
  517. protected function processConfigurations(&$context) {
  518. // The first time this is called we need to calculate the total to process.
  519. // This involves recalculating the changelist which will ensure that if
  520. // extensions have been processed any configuration affected will be taken
  521. // into account.
  522. if ($this->totalConfigurationToProcess == 0) {
  523. $this->storageComparer->reset();
  524. foreach ($this->storageComparer->getAllCollectionNames() as $collection) {
  525. foreach (['delete', 'create', 'rename', 'update'] as $op) {
  526. $this->totalConfigurationToProcess += count($this->getUnprocessedConfiguration($op, $collection));
  527. }
  528. }
  529. }
  530. $operation = $this->getNextConfigurationOperation();
  531. if (!empty($operation)) {
  532. if ($this->checkOp($operation['collection'], $operation['op'], $operation['name'])) {
  533. $this->processConfiguration($operation['collection'], $operation['op'], $operation['name']);
  534. }
  535. if ($operation['collection'] == StorageInterface::DEFAULT_COLLECTION) {
  536. $context['message'] = $this->t('Synchronizing configuration: @op @name.', ['@op' => $operation['op'], '@name' => $operation['name']]);
  537. }
  538. else {
  539. $context['message'] = $this->t('Synchronizing configuration: @op @name in @collection.', ['@op' => $operation['op'], '@name' => $operation['name'], '@collection' => $operation['collection']]);
  540. }
  541. $processed_count = 0;
  542. foreach ($this->storageComparer->getAllCollectionNames() as $collection) {
  543. foreach (['delete', 'create', 'rename', 'update'] as $op) {
  544. $processed_count += count($this->processedConfiguration[$collection][$op]);
  545. }
  546. }
  547. $context['finished'] = $processed_count / $this->totalConfigurationToProcess;
  548. }
  549. else {
  550. $context['finished'] = 1;
  551. }
  552. }
  553. /**
  554. * Handles processing of missing content.
  555. *
  556. * @param array|\ArrayAccess $context
  557. * Standard batch context.
  558. */
  559. protected function processMissingContent(&$context) {
  560. $sandbox = &$context['sandbox']['config'];
  561. if (!isset($sandbox['missing_content'])) {
  562. $missing_content = $this->configManager->findMissingContentDependencies();
  563. $sandbox['missing_content']['data'] = $missing_content;
  564. $sandbox['missing_content']['total'] = count($missing_content);
  565. }
  566. else {
  567. $missing_content = $sandbox['missing_content']['data'];
  568. }
  569. if (!empty($missing_content)) {
  570. $event = new MissingContentEvent($missing_content);
  571. // Fire an event to allow listeners to create the missing content.
  572. $this->eventDispatcher->dispatch(ConfigEvents::IMPORT_MISSING_CONTENT, $event);
  573. $sandbox['missing_content']['data'] = $event->getMissingContent();
  574. }
  575. $current_count = count($sandbox['missing_content']['data']);
  576. if ($current_count) {
  577. $context['message'] = $this->t('Resolving missing content');
  578. $context['finished'] = ($sandbox['missing_content']['total'] - $current_count) / $sandbox['missing_content']['total'];
  579. }
  580. else {
  581. $context['finished'] = 1;
  582. }
  583. }
  584. /**
  585. * Finishes the batch.
  586. *
  587. * @param array|\ArrayAccess $context
  588. * The batch context.
  589. */
  590. protected function finish(&$context) {
  591. $this->eventDispatcher->dispatch(ConfigEvents::IMPORT, new ConfigImporterEvent($this));
  592. // The import is now complete.
  593. $this->lock->release(static::LOCK_NAME);
  594. $this->reset();
  595. $context['message'] = t('Finalizing configuration synchronization.');
  596. $context['finished'] = 1;
  597. }
  598. /**
  599. * Gets the next extension operation to perform.
  600. *
  601. * @return array|bool
  602. * An array containing the next operation and extension name to perform it
  603. * on. If there is nothing left to do returns FALSE;
  604. */
  605. protected function getNextExtensionOperation() {
  606. foreach (['module', 'theme'] as $type) {
  607. foreach (['install', 'uninstall'] as $op) {
  608. $unprocessed = $this->getUnprocessedExtensions($type);
  609. if (!empty($unprocessed[$op])) {
  610. return [
  611. 'op' => $op,
  612. 'type' => $type,
  613. 'name' => array_shift($unprocessed[$op]),
  614. ];
  615. }
  616. }
  617. }
  618. return FALSE;
  619. }
  620. /**
  621. * Gets the next configuration operation to perform.
  622. *
  623. * @return array|bool
  624. * An array containing the next operation and configuration name to perform
  625. * it on. If there is nothing left to do returns FALSE;
  626. */
  627. protected function getNextConfigurationOperation() {
  628. // The order configuration operations is processed is important. Deletes
  629. // have to come first so that recreates can work.
  630. foreach ($this->storageComparer->getAllCollectionNames() as $collection) {
  631. foreach (['delete', 'create', 'rename', 'update'] as $op) {
  632. $config_names = $this->getUnprocessedConfiguration($op, $collection);
  633. if (!empty($config_names)) {
  634. return [
  635. 'op' => $op,
  636. 'name' => array_shift($config_names),
  637. 'collection' => $collection,
  638. ];
  639. }
  640. }
  641. }
  642. return FALSE;
  643. }
  644. /**
  645. * Dispatches validate event for a ConfigImporter object.
  646. *
  647. * Events should throw a \Drupal\Core\Config\ConfigImporterException to
  648. * prevent an import from occurring.
  649. *
  650. * @throws \Drupal\Core\Config\ConfigImporterException
  651. * Exception thrown if the validate event logged any errors.
  652. */
  653. public function validate() {
  654. if (!$this->validated) {
  655. // Create the list of installs and uninstalls.
  656. $this->createExtensionChangelist();
  657. // Validate renames.
  658. foreach ($this->getUnprocessedConfiguration('rename') as $name) {
  659. $names = $this->storageComparer->extractRenameNames($name);
  660. $old_entity_type_id = $this->configManager->getEntityTypeIdByName($names['old_name']);
  661. $new_entity_type_id = $this->configManager->getEntityTypeIdByName($names['new_name']);
  662. if ($old_entity_type_id != $new_entity_type_id) {
  663. $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']]));
  664. }
  665. // Has to be a configuration entity.
  666. if (!$old_entity_type_id) {
  667. $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']]));
  668. }
  669. }
  670. $this->eventDispatcher->dispatch(ConfigEvents::IMPORT_VALIDATE, new ConfigImporterEvent($this));
  671. if (count($this->getErrors())) {
  672. throw new ConfigImporterException('There were errors validating the config synchronization.');
  673. }
  674. else {
  675. $this->validated = TRUE;
  676. }
  677. }
  678. return $this;
  679. }
  680. /**
  681. * Processes a configuration change.
  682. *
  683. * @param string $collection
  684. * The configuration collection to process changes for.
  685. * @param string $op
  686. * The change operation.
  687. * @param string $name
  688. * The name of the configuration to process.
  689. *
  690. * @throws \Exception
  691. * Thrown when the import process fails, only thrown when no importer log is
  692. * set, otherwise the exception message is logged and the configuration
  693. * is skipped.
  694. */
  695. protected function processConfiguration($collection, $op, $name) {
  696. try {
  697. $processed = FALSE;
  698. if ($collection == StorageInterface::DEFAULT_COLLECTION) {
  699. $processed = $this->importInvokeOwner($collection, $op, $name);
  700. }
  701. if (!$processed) {
  702. $this->importConfig($collection, $op, $name);
  703. }
  704. }
  705. catch (\Exception $e) {
  706. $this->logError($this->t('Unexpected error during import with operation @op for @name: @message', ['@op' => $op, '@name' => $name, '@message' => $e->getMessage()]));
  707. // Error for that operation was logged, mark it as processed so that
  708. // the import can continue.
  709. $this->setProcessedConfiguration($collection, $op, $name);
  710. }
  711. }
  712. /**
  713. * Processes an extension change.
  714. *
  715. * @param string $type
  716. * The type of extension, either 'module' or 'theme'.
  717. * @param string $op
  718. * The change operation.
  719. * @param string $name
  720. * The name of the extension to process.
  721. */
  722. protected function processExtension($type, $op, $name) {
  723. // Set the config installer to use the sync directory instead of the
  724. // extensions own default config directories.
  725. \Drupal::service('config.installer')
  726. ->setSourceStorage($this->storageComparer->getSourceStorage());
  727. if ($type == 'module') {
  728. $this->moduleInstaller->$op([$name], FALSE);
  729. // Installing a module can cause a kernel boot therefore reinject all the
  730. // services.
  731. $this->reInjectMe();
  732. // During a module install or uninstall the container is rebuilt and the
  733. // module handler is called. This causes the container's instance of the
  734. // module handler not to have loaded all the enabled modules.
  735. $this->moduleHandler->loadAll();
  736. }
  737. if ($type == 'theme') {
  738. // Theme uninstalls possible remove default or admin themes therefore we
  739. // need to import this before doing any. If there are no uninstalls and
  740. // the default or admin theme is changing this will be picked up whilst
  741. // processing configuration.
  742. if ($op == 'uninstall' && $this->processedSystemTheme === FALSE) {
  743. $this->importConfig(StorageInterface::DEFAULT_COLLECTION, 'update', 'system.theme');
  744. $this->configManager->getConfigFactory()->reset('system.theme');
  745. $this->processedSystemTheme = TRUE;
  746. }
  747. $this->themeHandler->$op([$name]);
  748. }
  749. $this->setProcessedExtension($type, $op, $name);
  750. }
  751. /**
  752. * Checks that the operation is still valid.
  753. *
  754. * During a configuration import secondary writes and deletes are possible.
  755. * This method checks that the operation is still valid before processing a
  756. * configuration change.
  757. *
  758. * @param string $collection
  759. * The configuration collection.
  760. * @param string $op
  761. * The change operation.
  762. * @param string $name
  763. * The name of the configuration to process.
  764. *
  765. * @return bool
  766. * TRUE is to continue processing, FALSE otherwise.
  767. *
  768. * @throws \Drupal\Core\Config\ConfigImporterException
  769. */
  770. protected function checkOp($collection, $op, $name) {
  771. if ($op == 'rename') {
  772. $names = $this->storageComparer->extractRenameNames($name);
  773. $target_exists = $this->storageComparer->getTargetStorage($collection)->exists($names['new_name']);
  774. if ($target_exists) {
  775. // If the target exists, the rename has already occurred as the
  776. // result of a secondary configuration write. Change the operation
  777. // into an update. This is the desired behavior since renames often
  778. // have to occur together. For example, renaming a node type must
  779. // also result in renaming its fields and entity displays.
  780. $this->storageComparer->moveRenameToUpdate($name);
  781. return FALSE;
  782. }
  783. return TRUE;
  784. }
  785. $target_exists = $this->storageComparer->getTargetStorage($collection)->exists($name);
  786. switch ($op) {
  787. case 'delete':
  788. if (!$target_exists) {
  789. // The configuration has already been deleted. For example, a field
  790. // is automatically deleted if all the instances are.
  791. $this->setProcessedConfiguration($collection, $op, $name);
  792. return FALSE;
  793. }
  794. break;
  795. case 'create':
  796. if ($target_exists) {
  797. // If the target already exists, use the entity storage to delete it
  798. // again, if is a simple config, delete it directly.
  799. if ($entity_type_id = $this->configManager->getEntityTypeIdByName($name)) {
  800. $entity_storage = $this->configManager->getEntityManager()->getStorage($entity_type_id);
  801. $entity_type = $this->configManager->getEntityManager()->getDefinition($entity_type_id);
  802. $entity = $entity_storage->load($entity_storage->getIDFromConfigName($name, $entity_type->getConfigPrefix()));
  803. $entity->delete();
  804. $this->logError($this->t('Deleted and replaced configuration entity "@name"', ['@name' => $name]));
  805. }
  806. else {
  807. $this->storageComparer->getTargetStorage($collection)->delete($name);
  808. $this->logError($this->t('Deleted and replaced configuration "@name"', ['@name' => $name]));
  809. }
  810. return TRUE;
  811. }
  812. break;
  813. case 'update':
  814. if (!$target_exists) {
  815. $this->logError($this->t('Update target "@name" is missing.', ['@name' => $name]));
  816. // Mark as processed so that the synchronization continues. Once the
  817. // the current synchronization is complete it will show up as a
  818. // create.
  819. $this->setProcessedConfiguration($collection, $op, $name);
  820. return FALSE;
  821. }
  822. break;
  823. }
  824. return TRUE;
  825. }
  826. /**
  827. * Writes a configuration change from the source to the target storage.
  828. *
  829. * @param string $collection
  830. * The configuration collection.
  831. * @param string $op
  832. * The change operation.
  833. * @param string $name
  834. * The name of the configuration to process.
  835. */
  836. protected function importConfig($collection, $op, $name) {
  837. // Allow config factory overriders to use a custom configuration object if
  838. // they are responsible for the collection.
  839. $overrider = $this->configManager->getConfigCollectionInfo()->getOverrideService($collection);
  840. if ($overrider) {
  841. $config = $overrider->createConfigObject($name, $collection);
  842. }
  843. else {
  844. $config = new Config($name, $this->storageComparer->getTargetStorage($collection), $this->eventDispatcher, $this->typedConfigManager);
  845. }
  846. if ($op == 'delete') {
  847. $config->delete();
  848. }
  849. else {
  850. $data = $this->storageComparer->getSourceStorage($collection)->read($name);
  851. $config->setData($data ? $data : []);
  852. $config->save();
  853. }
  854. $this->setProcessedConfiguration($collection, $op, $name);
  855. }
  856. /**
  857. * Invokes import* methods on configuration entity storage.
  858. *
  859. * Allow modules to take over configuration change operations for higher-level
  860. * configuration data.
  861. *
  862. * @todo Add support for other extension types; e.g., themes etc.
  863. *
  864. * @param string $collection
  865. * The configuration collection.
  866. * @param string $op
  867. * The change operation to get the unprocessed list for, either delete,
  868. * create, rename, or update.
  869. * @param string $name
  870. * The name of the configuration to process.
  871. *
  872. * @return bool
  873. * TRUE if the configuration was imported as a configuration entity. FALSE
  874. * otherwise.
  875. *
  876. * @throws \Drupal\Core\Entity\EntityStorageException
  877. * Thrown if the data is owned by an entity type, but the entity storage
  878. * does not support imports.
  879. */
  880. protected function importInvokeOwner($collection, $op, $name) {
  881. // Renames are handled separately.
  882. if ($op == 'rename') {
  883. return $this->importInvokeRename($collection, $name);
  884. }
  885. // Validate the configuration object name before importing it.
  886. // Config::validateName($name);
  887. if ($entity_type = $this->configManager->getEntityTypeIdByName($name)) {
  888. $old_config = new Config($name, $this->storageComparer->getTargetStorage($collection), $this->eventDispatcher, $this->typedConfigManager);
  889. if ($old_data = $this->storageComparer->getTargetStorage($collection)->read($name)) {
  890. $old_config->initWithData($old_data);
  891. }
  892. $data = $this->storageComparer->getSourceStorage($collection)->read($name);
  893. $new_config = new Config($name, $this->storageComparer->getTargetStorage($collection), $this->eventDispatcher, $this->typedConfigManager);
  894. if ($data !== FALSE) {
  895. $new_config->setData($data);
  896. }
  897. $method = 'import' . ucfirst($op);
  898. $entity_storage = $this->configManager->getEntityManager()->getStorage($entity_type);
  899. // Call to the configuration entity's storage to handle the configuration
  900. // change.
  901. if (!($entity_storage instanceof ImportableEntityStorageInterface)) {
  902. throw new EntityStorageException(sprintf('The entity storage "%s" for the "%s" entity type does not support imports', get_class($entity_storage), $entity_type));
  903. }
  904. $entity_storage->$method($name, $new_config, $old_config);
  905. $this->setProcessedConfiguration($collection, $op, $name);
  906. return TRUE;
  907. }
  908. return FALSE;
  909. }
  910. /**
  911. * Imports a configuration entity rename.
  912. *
  913. * @param string $collection
  914. * The configuration collection.
  915. * @param string $rename_name
  916. * The rename configuration name, as provided by
  917. * \Drupal\Core\Config\StorageComparer::createRenameName().
  918. *
  919. * @return bool
  920. * TRUE if the configuration was imported as a configuration entity. FALSE
  921. * otherwise.
  922. *
  923. * @throws \Drupal\Core\Entity\EntityStorageException
  924. * Thrown if the data is owned by an entity type, but the entity storage
  925. * does not support imports.
  926. *
  927. * @see \Drupal\Core\Config\ConfigImporter::createRenameName()
  928. */
  929. protected function importInvokeRename($collection, $rename_name) {
  930. $names = $this->storageComparer->extractRenameNames($rename_name);
  931. $entity_type_id = $this->configManager->getEntityTypeIdByName($names['old_name']);
  932. $old_config = new Config($names['old_name'], $this->storageComparer->getTargetStorage($collection), $this->eventDispatcher, $this->typedConfigManager);
  933. if ($old_data = $this->storageComparer->getTargetStorage($collection)->read($names['old_name'])) {
  934. $old_config->initWithData($old_data);
  935. }
  936. $data = $this->storageComparer->getSourceStorage($collection)->read($names['new_name']);
  937. $new_config = new Config($names['new_name'], $this->storageComparer->getTargetStorage($collection), $this->eventDispatcher, $this->typedConfigManager);
  938. if ($data !== FALSE) {
  939. $new_config->setData($data);
  940. }
  941. $entity_storage = $this->configManager->getEntityManager()->getStorage($entity_type_id);
  942. // Call to the configuration entity's storage to handle the configuration
  943. // change.
  944. if (!($entity_storage instanceof ImportableEntityStorageInterface)) {
  945. throw new EntityStorageException(sprintf("The entity storage '%s' for the '%s' entity type does not support imports", get_class($entity_storage), $entity_type_id));
  946. }
  947. $entity_storage->importRename($names['old_name'], $new_config, $old_config);
  948. $this->setProcessedConfiguration($collection, 'rename', $rename_name);
  949. return TRUE;
  950. }
  951. /**
  952. * Determines if a import is already running.
  953. *
  954. * @return bool
  955. * TRUE if an import is already running, FALSE if not.
  956. */
  957. public function alreadyImporting() {
  958. return !$this->lock->lockMayBeAvailable(static::LOCK_NAME);
  959. }
  960. /**
  961. * Gets all the service dependencies from \Drupal.
  962. *
  963. * Since the ConfigImporter handles module installation the kernel and the
  964. * container can be rebuilt and altered during processing. It is necessary to
  965. * keep the services used by the importer in sync.
  966. */
  967. protected function reInjectMe() {
  968. $this->_serviceIds = [];
  969. $vars = get_object_vars($this);
  970. foreach ($vars as $key => $value) {
  971. if (is_object($value) && isset($value->_serviceId)) {
  972. $this->$key = \Drupal::service($value->_serviceId);
  973. }
  974. }
  975. }
  976. }