Issue #2538108 by dawehner, jhedstrom: Add hook_post_update_X() for data value updates to reliably run after data format updates

8.0.x
Nathaniel Catchpole 2015-09-09 18:07:09 +01:00
parent 56993d41d4
commit 7d16f1c6e6
27 changed files with 1345 additions and 86 deletions

View File

@ -1518,6 +1518,12 @@ services:
- { name: placeholder_strategy, priority: -1000 }
email.validator:
class: Egulias\EmailValidator\EmailValidator
update.post_update_registry:
class: Drupal\Core\Update\UpdateRegistry
factory: ['@update.post_update_registry_factory', create]
update.post_update_registry_factory:
class: Drupal\Core\Update\UpdateRegistryFactory
parent: container.trait
response_filter.active_link:
class: Drupal\Core\EventSubscriber\ActiveLinkResponseFilter

View File

@ -182,6 +182,7 @@ function update_do_one($module, $number, $dependency_map, &$context) {
// @TODO We may want to do different error handling for different
// exception types, but for now we'll just log the exception and
// return the message for printing.
// @see https://www.drupal.org/node/2564311
catch (Exception $e) {
watchdog_exception('update', $e);
@ -214,7 +215,66 @@ function update_do_one($module, $number, $dependency_map, &$context) {
drupal_set_installed_schema_version($module, $number);
}
$context['message'] = 'Updating ' . Html::escape($module) . ' module';
$context['message'] = t('Updating @module', ['@module' => $module]);
}
/**
* Executes a single hook_post_update_NAME().
*
* @param string $function
* The function name, that should be executed.
* @param array $context
* The batch context array.
*/
function update_invoke_post_update($function, &$context) {
$ret = [];
// If this update was aborted in a previous step, or has a dependency that was
// aborted in a previous step, go no further.
if (!empty($context['results']['#abort'])) {
return;
}
list($module, $name) = explode('_post_update_', $function, 2);
module_load_include('php', $module, $module . '.post_update');
if (function_exists($function)) {
try {
$ret['results']['query'] = $function($context['sandbox']);
$ret['results']['success'] = TRUE;
\Drupal::service('update.post_update_registry')->registerInvokedUpdates([$function]);
}
// @TODO We may want to do different error handling for different exception
// types, but for now we'll just log the exception and return the message
// for printing.
// @see https://www.drupal.org/node/2564311
catch (Exception $e) {
watchdog_exception('update', $e);
$variables = Error::decodeException($e);
unset($variables['backtrace']);
$ret['#abort'] = [
'success' => FALSE,
'query' => t('%type: @message in %function (line %line of %file).', $variables),
];
}
}
if (isset($context['sandbox']['#finished'])) {
$context['finished'] = $context['sandbox']['#finished'];
unset($context['sandbox']['#finished']);
}
if (!isset($context['results'][$module][$name])) {
$context['results'][$module][$name] = array();
}
$context['results'][$module][$name] = array_merge($context['results'][$module][$name], $ret);
if (!empty($ret['#abort'])) {
// Record this function in the list of updates that were aborted.
$context['results']['#abort'][] = $function;
}
$context['message'] = t('Post updating @module', ['@module' => $module]);
}
/**

View File

@ -90,15 +90,30 @@ class ExtensionDiscovery {
*/
protected $fileCache;
/**
* The site path.
*
* @var string
*/
protected $sitePath;
/**
* Constructs a new ExtensionDiscovery object.
*
* @param string $root
* The app root.
* @param bool $use_file_cache
* Whether file cache should be used.
* @param string[] $profile_directories
* The available profile directories
* @param string $site_path
* The path to the site.
*/
public function __construct($root) {
public function __construct($root, $use_file_cache = TRUE, $profile_directories = NULL, $site_path = NULL) {
$this->root = $root;
$this->fileCache = FileCacheFactory::get('extension_discovery');
$this->fileCache = $use_file_cache ? FileCacheFactory::get('extension_discovery') : NULL;
$this->profileDirectories = $profile_directories;
$this->sitePath = $site_path;
}
/**
@ -172,7 +187,7 @@ class ExtensionDiscovery {
$searchdirs[static::ORIGIN_SITE] = \Drupal::service('site.path');
}
else {
$searchdirs[static::ORIGIN_SITE] = DrupalKernel::findSitePath(Request::createFromGlobals());
$searchdirs[static::ORIGIN_SITE] = $this->sitePath ?: DrupalKernel::findSitePath(Request::createFromGlobals());
}
// Unless an explicit value has been passed, manually check whether we are
@ -180,7 +195,7 @@ class ExtensionDiscovery {
// Test extensions can also be included for debugging purposes by setting a
// variable in settings.php.
if (!isset($include_tests)) {
$include_tests = drupal_valid_test_ua() || Settings::get('extension_discovery_scan_tests');
$include_tests = Settings::get('extension_discovery_scan_tests') || drupal_valid_test_ua();
}
$files = array();
@ -427,7 +442,7 @@ class ExtensionDiscovery {
continue;
}
if ($cached_extension = $this->fileCache->get($fileinfo->getPathName())) {
if ($this->fileCache && $cached_extension = $this->fileCache->get($fileinfo->getPathName())) {
$files[$cached_extension->getType()][$key] = $cached_extension;
continue;
}
@ -467,7 +482,10 @@ class ExtensionDiscovery {
$extension->origin = $dir;
$files[$type][$key] = $extension;
$this->fileCache->set($fileinfo->getPathName(), $extension);
if ($this->fileCache) {
$this->fileCache->set($fileinfo->getPathName(), $extension);
}
}
return $files;
}

View File

@ -260,6 +260,11 @@ class ModuleInstaller implements ModuleInstallerInterface {
}
drupal_set_installed_schema_version($module, $version);
// Ensure that all post_update functions are registered already.
/** @var \Drupal\Core\Update\UpdateRegistry $post_update_registry */
$post_update_registry = \Drupal::service('update.post_update_registry');
$post_update_registry->registerInvokedUpdates($post_update_registry->getModuleUpdateFunctions($module));
// Record the fact that it was installed.
$modules_installed[] = $module;
@ -445,6 +450,10 @@ class ModuleInstaller implements ModuleInstallerInterface {
$schema_store = \Drupal::keyValue('system.schema');
$schema_store->delete($module);
/** @var \Drupal\Core\Update\UpdateRegistry $post_update_registry */
$post_update_registry = \Drupal::service('update.post_update_registry');
$post_update_registry->filterOutInvokedUpdatesByModule($module);
}
\Drupal::service('router.builder')->setRebuildNeeded();
drupal_get_installed_schema_version(NULL, TRUE);

View File

@ -659,6 +659,71 @@ function hook_update_N(&$sandbox) {
return t('All foo bars were updated with the new suffix');
}
/**
* Executes an update which is intended to update data, like entities.
*
* These implementations have to be placed in a MODULE.post_update.php file.
*
* These updates are executed after all hook_update_N() implementations. At this
* stage Drupal is already fully repaired so you can use any API as you wish.
*
* NAME can be arbitrary machine names. In contrast to hook_update_N() the order
* of functions in the file is the only thing which ensures the execution order
* of those functions.
*
* Drupal also ensures to not execute the same hook_post_update_NAME() function
* twice.
*
* @param array $sandbox
* Stores information for batch updates. See above for more information.
*
* @throws \Drupal\Core\Utility\UpdateException|PDOException
* In case of error, update hooks should throw an instance of
* \Drupal\Core\Utility\UpdateException with a meaningful message for the
* user. If a database query fails for whatever reason, it will throw a
* PDOException.
*
* @return string|null
* Optionally, hook_post_update_NAME() hooks may return a translated string
* that will be displayed to the user after the update has completed. If no
* message is returned, no message will be presented to the user.
*
* @ingroup update_api
*
* @see hook_update_N()
*/
function hook_post_update_NAME(&$sandbox) {
// Example of updating some content.
$node = \Drupal\node\Entity\Node::load(123);
$node->setTitle('foo');
$node->save();
$result = t('Node %nid saved', ['%nid' => $node->id()]);
// Example of disabling blocks with missing condition contexts. Note: The
// block itself is in a state which is valid at that point.
// @see block_update_8001()
// @see block_post_update_disable_blocks_with_missing_contexts()
$block_update_8001 = \Drupal::keyValue('update_backup')->get('block_update_8001', []);
$block_ids = array_keys($block_update_8001);
$block_storage = \Drupal::entityManager()->getStorage('block');
$blocks = $block_storage->loadMultiple($block_ids);
/** @var $blocks \Drupal\block\BlockInterface[] */
foreach ($blocks as $block) {
// This block has had conditions removed due to an inability to resolve
// contexts in block_update_8001() so disable it.
// Disable currently enabled blocks.
if ($block_update_8001[$block->id()]['status']) {
$block->setStatus(FALSE);
$block->save();
}
}
return $result;
}
/**
* Return an array of information about module update dependencies.
*

View File

@ -0,0 +1,267 @@
<?php
/**
* @file
* Contains \Drupal\Core\Update\UpdateRegistry.
*/
namespace Drupal\Core\Update;
use Drupal\Core\Extension\Extension;
use Drupal\Core\Extension\ExtensionDiscovery;
use Drupal\Core\KeyValueStore\KeyValueStoreInterface;
/**
* Provides all and missing update implementations.
*
* Note: This registry is specific to a type of updates, like 'post_update' as
* example.
*
* It therefore scans for functions named like the type of updates, so it looks
* like MODULE_UPDATETYPE_NAME() with NAME being a machine name.
*/
class UpdateRegistry {
/**
* The used update name.
*
* @var string
*/
protected $updateType = 'post_update';
/**
* The app root.
*
* @var string
*/
protected $root;
/**
* The filename of the log file.
*
* @var string
*/
protected $logFilename;
/**
* @var string[]
*/
protected $enabledModules;
/**
* The key value storage.
*
* @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface
*/
protected $keyValue;
/**
* Should we respect update functions in tests.
*
* @var bool|null
*/
protected $includeTests = NULL;
/**
* The site path.
*
* @var string
*/
protected $sitePath;
/**
* Constructs a new UpdateRegistry.
*
* @param string $root
* The app root.
* @param string $site_path
* The site path.
* @param string[] $enabled_modules
* A list of enabled modules.
* @param \Drupal\Core\KeyValueStore\KeyValueStoreInterface $key_value
* The key value store.
* @param bool|NULL $include_tests
* (optional) A flag whether to include tests in the scanning of modules.
*/
public function __construct($root, $site_path, array $enabled_modules, KeyValueStoreInterface $key_value, $include_tests = NULL) {
$this->root = $root;
$this->sitePath = $site_path;
$this->enabledModules = $enabled_modules;
$this->keyValue = $key_value;
$this->includeTests = $include_tests;
}
/**
* Gets all available update functions.
*
* @return callable[]
* A list of update functions.
*/
protected function getAvailableUpdateFunctions() {
$regexp = '/^(?<module>.+)_' . $this->updateType . '_(?<name>.+)$/';
$functions = get_defined_functions();
$updates = [];
foreach (preg_grep('/_' . $this->updateType . '_/', $functions['user']) as $function) {
// If this function is a module update function, add it to the list of
// module updates.
if (preg_match($regexp, $function, $matches)) {
if (in_array($matches['module'], $this->enabledModules)) {
$updates[] = $matches['module'] . '_' . $this->updateType . '_' . $matches['name'];
}
}
}
return $updates;
}
/**
* Find all update functions that haven't been executed.
*
* @return callable[]
* A list of update functions.
*/
public function getPendingUpdateFunctions() {
// We need a) the list of active modules (we get that from the config
// bootstrap factory) and b) the path to the modules, we use the extension
// discovery for that.
$this->scanExtensionsAndLoadUpdateFiles();
// First figure out which hook_{$this->updateType}_NAME got executed
// already.
$existing_update_functions = $this->keyValue->get('existing_updates', []);
$available_update_functions = $this->getAvailableUpdateFunctions();
$not_executed_update_functions = array_diff($available_update_functions, $existing_update_functions);
return $not_executed_update_functions;
}
/**
* Loads all update files for a given list of extension.
*
* @param \Drupal\Core\Extension\Extension[] $module_extensions
* The extensions used for loading.
*/
protected function loadUpdateFiles(array $module_extensions) {
// Load all the {$this->updateType}.php files.
foreach ($this->enabledModules as $module) {
if (isset($module_extensions[$module])) {
$this->loadUpdateFile($module_extensions[$module]);
}
}
}
/**
* Loads the {$this->updateType}.php file for a given extension.
*
* @param \Drupal\Core\Extension\Extension $module
* The extension of the module to load its file.
*/
protected function loadUpdateFile(Extension $module) {
$filename = $this->root . '/' . $module->getPath() . '/' . $module->getName() . ".{$this->updateType}.php";
if (file_exists($filename)) {
include_once $filename;
}
}
/**
* Returns a list of all the pending updates.
*
* @return array[]
* An associative array keyed by module name which contains all information
* about database updates that need to be run, and any updates that are not
* going to proceed due to missing requirements.
*
* The subarray for each module can contain the following keys:
* - start: The starting update that is to be processed. If this does not
* exist then do not process any updates for this module as there are
* other requirements that need to be resolved.
* - pending: An array of all the pending updates for the module including
* the description from source code comment for each update function.
* This array is keyed by the update name.
*/
public function getPendingUpdateInformation() {
$functions = $this->getPendingUpdateFunctions();
$ret = [];
foreach ($functions as $function) {
list($module, $update) = explode("_{$this->updateType}_", $function);
// The description for an update comes from its Doxygen.
$func = new \ReflectionFunction($function);
$description = trim(str_replace(array("\n", '*', '/'), '', $func->getDocComment()), ' ');
$ret[$module]['pending'][$update] = $description;
if (!isset($ret[$module]['start'])) {
$ret[$module]['start'] = $update;
}
}
return $ret;
}
/**
* Registers that update fucntions got executed.
*
* @param string[] $function_names
* The executed update functions.
*
* @return $this
*/
public function registerInvokedUpdates(array $function_names) {
$executed_updates = $this->keyValue->get('existing_updates', []);
$executed_updates = array_merge($executed_updates, $function_names);
$this->keyValue->set('existing_updates', $executed_updates);
return $this;
}
/**
* Returns all available updates for a given module.
*
* @param string $module_name
* The module name.
*
* @return callable[]
* A list of update functions.
*/
public function getModuleUpdateFunctions($module_name) {
$this->scanExtensionsAndLoadUpdateFiles();
$all_functions = $this->getAvailableUpdateFunctions();
return array_filter($all_functions, function($function_name) use ($module_name) {
list($function_module_name, ) = explode("_{$this->updateType}_", $function_name);
return $function_module_name === $module_name;
});
}
/**
* Scans all module + profile extensions and load the update files.
*/
protected function scanExtensionsAndLoadUpdateFiles() {
// Scan the module list.
$extension_discovery = new ExtensionDiscovery($this->root, FALSE, [], $this->sitePath);
$module_extensions = $extension_discovery->scan('module');
$profile_extensions = $extension_discovery->scan('profile');
$extensions = array_merge($module_extensions, $profile_extensions);
$this->loadUpdateFiles($extensions);
}
/**
* Filters out already executed update functions by module.
*
* @param string $module
* The module name.
*/
public function filterOutInvokedUpdatesByModule($module) {
$existing_update_functions = $this->keyValue->get('existing_updates', []);
$remaining_update_functions = array_filter($existing_update_functions, function($function_name) use ($module) {
return strpos($function_name, "{$module}_{$this->updateType}_") !== 0;
});
$this->keyValue->set('existing_updates', array_values($remaining_update_functions));
}
}

View File

@ -0,0 +1,30 @@
<?php
/**
* @file
* Contains \Drupal\Core\Update\UpdateRegistryFactory.
*/
namespace Drupal\Core\Update;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerAwareTrait;
/**
* Service factory for the update registry.
*/
class UpdateRegistryFactory implements ContainerAwareInterface {
use ContainerAwareTrait;
/**
* Creates a new UpdateRegistry instance.
*
* @return \Drupal\Core\Update\UpdateRegistry
* The update registry instance.
*/
public function create() {
return new UpdateRegistry($this->container->get('app.root'), $this->container->get('site.path'), array_keys($this->container->get('module_handler')->getModuleList()), $this->container->get('keyvalue')->get('post_update'));
}
}

View File

@ -45,8 +45,9 @@ function block_update_8001() {
}
// Contributed modules should leverage hook_update_dependencies() in order to
// be executed before block_update_8002(), so they can update their context
// mappings, if wanted.
// be executed after block_update_8001(). The blocks are then disabled if the
// contexts are still missing via
// block_post_update_disable_blocks_with_missing_contexts().
$config_factory = \Drupal::configFactory();
$backup_values = $update_backup = [];
@ -95,50 +96,10 @@ function block_update_8001() {
}
/**
* Disable all blocks with missing context IDs in block_update_8001().
* Placeholder for the previous 8002 update.
*/
function block_update_8002() {
$block_update_8001 = \Drupal::keyValue('update_backup')->get('block_update_8001', []);
$block_ids = array_keys($block_update_8001);
$config_factory = \Drupal::configFactory();
/** @var \Drupal\Core\Config\Config[] $blocks */
$blocks = [];
foreach ($block_ids as $block_id) {
$blocks[$block_id] = $block = $config_factory->getEditable('block.block.' . $block_id);
// This block will have an invalid context mapping service and must be
// disabled in order to prevent information disclosure.
// Disable currently enabled blocks.
if ($block_update_8001[$block_id]['status']) {
$block->set('status', FALSE);
$block->save(TRUE);
}
}
// Provides a list of plugin labels, keyed by plugin ID.
$condition_plugin_id_label_map = array_column(\Drupal::service('plugin.manager.condition')->getDefinitions(), 'label', 'id');
// Override with the UI labels we are aware of. Sadly they are not machine
// accessible, see
// \Drupal\node\Plugin\Condition\NodeType::buildConfigurationForm().
$condition_plugin_id_label_map['node_type'] = t('Content types');
$condition_plugin_id_label_map['request_path'] = t('Pages');
$condition_plugin_id_label_map['user_role'] = t('Roles');
if (count($block_ids) > 0) {
$message = t('Encountered an unknown context mapping key coming probably from a contributed or custom module: One or more mappings could not be updated. Please manually review your visibility settings for the following blocks, which are disabled now:');
$message .= '<ul>';
foreach ($blocks as $disabled_block_id => $disabled_block) {
$message .= '<li>' . t('@label (Visibility: @plugin_ids)', array(
'@label' => $disabled_block->get('settings.label'),
'@plugin_ids' => implode(', ', array_intersect_key($condition_plugin_id_label_map, array_flip(array_keys($block_update_8001[$disabled_block_id]['missing_context_ids']))))
)) . '</li>';
}
$message .= '</ul>';
return $message;
}
\Drupal::state()->set('block_update_8002_placeholder', TRUE);
}
/**

View File

@ -0,0 +1,79 @@
<?php
/**
* @file
* Post update functions for Block.
*/
/**
* @addtogroup updates-8.0.0-beta
* @{
*/
/**
* Disable all blocks with missing context IDs in block_update_8001().
*/
function block_post_update_disable_blocks_with_missing_contexts() {
// Don't execute the function if block_update_8002() got executed already,
// which used to do the same. Note: Its okay to check here, because
// update_do_one() does not update the installed schema version until the
// batch is finished.
$module_schema = drupal_get_installed_schema_version('block');
// The state entry 'block_update_8002_placeholder' is used in order to
// indicate that the placeholder block_update_8002() function has been
// executed, so this function needs to be executed as well. If the non
// placeholder version of block_update_8002() got executed already, the state
// won't be set and we skip this update.
if ($module_schema >= 8002 && !\Drupal::state()->get('block_update_8002_placeholder', FALSE)) {
return;
}
// Cleanup the state entry as its no longer needed.
\Drupal::state()->delete('block_update_8002');
$block_update_8001 = \Drupal::keyValue('update_backup')->get('block_update_8001', []);
$block_ids = array_keys($block_update_8001);
$block_storage = \Drupal::entityManager()->getStorage('block');
$blocks = $block_storage->loadMultiple($block_ids);
/** @var $blocks \Drupal\block\BlockInterface[] */
foreach ($blocks as $block) {
// This block has had conditions removed due to an inability to resolve
// contexts in block_update_8001() so disable it.
// Disable currently enabled blocks.
if ($block_update_8001[$block->id()]['status']) {
$block->setStatus(FALSE);
$block->save();
}
}
// Provides a list of plugin labels, keyed by plugin ID.
$condition_plugin_id_label_map = array_column(\Drupal::service('plugin.manager.condition')->getDefinitions(), 'label', 'id');
// Override with the UI labels we are aware of. Sadly they are not machine
// accessible, see
// \Drupal\node\Plugin\Condition\NodeType::buildConfigurationForm().
$condition_plugin_id_label_map['node_type'] = t('Content types');
$condition_plugin_id_label_map['request_path'] = t('Pages');
$condition_plugin_id_label_map['user_role'] = t('Roles');
if (count($block_ids) > 0) {
$message = t('Encountered an unknown context mapping key coming probably from a contributed or custom module: One or more mappings could not be updated. Please manually review your visibility settings for the following blocks, which are disabled now:');
$message .= '<ul>';
foreach ($blocks as $disabled_block_id => $disabled_block) {
$message .= '<li>' . t('@label (Visibility: @plugin_ids)', array(
'@label' => $disabled_block->get('settings')['label'],
'@plugin_ids' => implode(', ', array_intersect_key($condition_plugin_id_label_map, array_flip(array_keys($block_update_8001[$disabled_block_id]['missing_context_ids']))))
)) . '</li>';
}
$message .= '</ul>';
return $message;
}
}
/**
* @} End of "addtogroup updates-8.0.0-beta".
*/

View File

@ -15,6 +15,7 @@ use Drupal\Core\Render\BareHtmlPageRendererInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Site\Settings;
use Drupal\Core\State\StateInterface;
use Drupal\Core\Update\UpdateRegistry;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Response;
@ -74,6 +75,13 @@ class DbUpdateController extends ControllerBase {
*/
protected $root;
/**
* The post update registry.
*
* @var \Drupal\Core\Update\UpdateRegistry
*/
protected $postUpdateRegistry;
/**
* Constructs a new UpdateController.
*
@ -91,8 +99,10 @@ class DbUpdateController extends ControllerBase {
* The current user.
* @param \Drupal\Core\Render\BareHtmlPageRendererInterface $bare_html_page_renderer
* The bare HTML page renderer.
* @param \Drupal\Core\Update\UpdateRegistry $post_update_registry
* The post update registry.
*/
public function __construct($root, KeyValueExpirableFactoryInterface $key_value_expirable_factory, CacheBackendInterface $cache, StateInterface $state, ModuleHandlerInterface $module_handler, AccountInterface $account, BareHtmlPageRendererInterface $bare_html_page_renderer) {
public function __construct($root, KeyValueExpirableFactoryInterface $key_value_expirable_factory, CacheBackendInterface $cache, StateInterface $state, ModuleHandlerInterface $module_handler, AccountInterface $account, BareHtmlPageRendererInterface $bare_html_page_renderer, UpdateRegistry $post_update_registry) {
$this->root = $root;
$this->keyValueExpirableFactory = $key_value_expirable_factory;
$this->cache = $cache;
@ -100,6 +110,7 @@ class DbUpdateController extends ControllerBase {
$this->moduleHandler = $module_handler;
$this->account = $account;
$this->bareHtmlPageRenderer = $bare_html_page_renderer;
$this->postUpdateRegistry = $post_update_registry;
}
/**
@ -113,7 +124,8 @@ class DbUpdateController extends ControllerBase {
$container->get('state'),
$container->get('module_handler'),
$container->get('current_user'),
$container->get('bare_html_page_renderer')
$container->get('bare_html_page_renderer'),
$container->get('update.post_update_registry')
);
}
@ -257,35 +269,52 @@ class DbUpdateController extends ControllerBase {
// Ensure system.module's updates appear first.
$build['start']['system'] = array();
$updates = update_get_update_list();
$starting_updates = array();
$incompatible_updates_exist = FALSE;
foreach ($updates as $module => $update) {
if (!isset($update['start'])) {
$build['start'][$module] = array(
'#type' => 'item',
'#title' => $module . ' module',
'#markup' => $update['warning'],
'#prefix' => '<div class="messages messages--warning">',
'#suffix' => '</div>',
);
$incompatible_updates_exist = TRUE;
continue;
$updates_per_module = [];
foreach (['update', 'post_update'] as $update_type) {
switch ($update_type) {
case 'update':
$updates = update_get_update_list();
break;
case 'post_update':
$updates = $this->postUpdateRegistry->getPendingUpdateInformation();
break;
}
if (!empty($update['pending'])) {
$starting_updates[$module] = $update['start'];
$build['start'][$module] = array(
'#type' => 'hidden',
'#value' => $update['start'],
);
$build['start'][$module . '_updates'] = array(
'#theme' => 'item_list',
'#items' => $update['pending'],
'#title' => $module . ' module',
);
}
if (isset($update['pending'])) {
$count = $count + count($update['pending']);
foreach ($updates as $module => $update) {
if (!isset($update['start'])) {
$build['start'][$module] = array(
'#type' => 'item',
'#title' => $module . ' module',
'#markup' => $update['warning'],
'#prefix' => '<div class="messages messages--warning">',
'#suffix' => '</div>',
);
$incompatible_updates_exist = TRUE;
continue;
}
if (!empty($update['pending'])) {
$updates_per_module += [$module => []];
$updates_per_module[$module] = array_merge($updates_per_module[$module], $update['pending']);
$build['start'][$module] = array(
'#type' => 'hidden',
'#value' => $update['start'],
);
// Store the previous items in order to merge normal updates and
// post_update functions together.
$build['start'][$module] = array(
'#theme' => 'item_list',
'#items' => $updates_per_module[$module],
'#title' => $module . ' module',
);
if ($update_type === 'update') {
$starting_updates[$module] = $update['start'];
}
}
if (isset($update['pending'])) {
$count = $count + count($update['pending']);
}
}
}
@ -415,7 +444,7 @@ class DbUpdateController extends ControllerBase {
if ($module != '#abort') {
$module_has_message = FALSE;
$info_messages = array();
foreach ($updates as $number => $queries) {
foreach ($updates as $name => $queries) {
$messages = array();
foreach ($queries as $query) {
// If there is no message for this update, don't show anything.
@ -439,10 +468,16 @@ class DbUpdateController extends ControllerBase {
if ($messages) {
$module_has_message = TRUE;
if (is_numeric($name)) {
$title = $this->t('Update #@count', ['@count' => $name]);
}
else {
$title = $this->t('Update @name', ['@name' => trim($name, '_')]);
}
$info_messages[] = array(
'#theme' => 'item_list',
'#items' => $messages,
'#title' => $this->t('Update #@count', array('@count' => $number)),
'#title' => $title,
);
}
}
@ -575,6 +610,17 @@ class DbUpdateController extends ControllerBase {
}
}
$post_updates = $this->postUpdateRegistry->getPendingUpdateFunctions();
if ($post_updates) {
// Now we rebuild all caches and after that execute the hook_post_update()
// functions.
$operations[] = ['drupal_flush_all_caches', []];
foreach ($post_updates as $function) {
$operations[] = ['update_invoke_post_update', [$function]];
}
}
$batch['operations'] = $operations;
$batch += array(
'title' => $this->t('Updating'),

View File

@ -53,6 +53,21 @@ class InstallTest extends WebTestBase {
$this->assertTrue($version > 0, 'System module version is > 0.');
$version = drupal_get_installed_schema_version('user', TRUE);
$this->assertTrue($version > 0, 'User module version is > 0.');
$post_update_key_value = \Drupal::keyValue('post_update');
$existing_updates = $post_update_key_value->get('existing_updates', []);
$this->assertTrue(in_array('module_test_post_update_test', $existing_updates));
}
/**
* Ensures that post update functions are removed on uninstall.
*/
public function testUninstallPostUpdateFunctions() {
\Drupal::service('module_installer')->uninstall(['module_test']);
$post_update_key_value = \Drupal::keyValue('post_update');
$existing_updates = $post_update_key_value->get('existing_updates', []);
$this->assertFalse(in_array('module_test_post_update_test', $existing_updates));
}
/**

View File

@ -16,7 +16,10 @@ use Drupal\Core\Logger\RfcLogLevel;
*/
class InstallUninstallTest extends ModuleTestBase {
public static $modules = array('system_test', 'dblog', 'taxonomy');
/**
* {@inheritdoc}
*/
public static $modules = array('system_test', 'dblog', 'taxonomy', 'update_test_postupdate');
/**
* Tests that a fixed set of modules can be installed and uninstalled.
@ -105,6 +108,7 @@ class InstallUninstallTest extends ModuleTestBase {
$this->assertModuleTablesExist($module_to_install);
$this->assertModuleConfig($module_to_install);
$this->assertLogMessage('system', "%module module installed.", array('%module' => $module_to_install), RfcLogLevel::INFO);
$this->assertInstallModuleUpdates($module_to_install);
}
// Uninstall the original module, and check appropriate
@ -188,6 +192,59 @@ class InstallUninstallTest extends ModuleTestBase {
$this->assertModuleTablesDoNotExist($module);
// Check that the module's config files no longer exist.
$this->assertNoModuleConfig($module);
$this->assertUninstallModuleUpdates($module);
}
/**
* Asserts the module post update functions after install.
*
* @param string $module
* The module that got installed.
*/
protected function assertInstallModuleUpdates($module) {
/** @var \Drupal\Core\Update\UpdateRegistry $post_update_registry */
$post_update_registry = \Drupal::service('update.post_update_registry');
$all_update_functions = $post_update_registry->getPendingUpdateFunctions();
$empty_result = TRUE;
foreach ($all_update_functions as $function) {
list($function_module, ) = explode('_post_update_', $function);
if ($module === $function_module) {
$empty_result = FALSE;
break;
}
}
$this->assertTrue($empty_result, 'Ensures that no pending post update functions are available.');
$existing_updates = \Drupal::keyValue('post_update')->get('existing_updates', []);
switch ($module) {
case 'block':
$this->assertFalse(array_diff(['block_post_update_disable_blocks_with_missing_contexts'], $existing_updates));
break;
case 'update_test_postupdate':
$this->assertFalse(array_diff(['update_test_postupdate_post_update_first', 'update_test_postupdate_post_update_second', 'update_test_postupdate_post_update_test1', 'update_test_postupdate_post_update_test0'], $existing_updates));
break;
}
}
/**
* Asserts the module post update functions after uninstall.
*
* @param string $module
* The module that got installed.
*/
protected function assertUninstallModuleUpdates($module) {
/** @var \Drupal\Core\Update\UpdateRegistry $post_update_registry */
$post_update_registry = \Drupal::service('update.post_update_registry');
$all_update_functions = $post_update_registry->getPendingUpdateFunctions();
switch ($module) {
case 'block':
$this->assertFalse(array_intersect(['block_post_update_disable_blocks_with_missing_contexts'], $all_update_functions), 'Asserts that no pending post update functions are available.');
$existing_updates = \Drupal::keyValue('post_update')->get('existing_updates', []);
$this->assertFalse(array_intersect(['block_post_update_disable_blocks_with_missing_contexts'], $existing_updates), 'Asserts that no post update functions are stored in keyvalue store.');
break;
}
}
}

View File

@ -18,6 +18,11 @@ use Drupal\system\SystemRequirements;
*/
class StatusTest extends WebTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['update_test_postupdate'];
/**
* {@inheritdoc}
*/
@ -57,6 +62,23 @@ class StatusTest extends WebTestBase {
$this->assertNoLinkByHref(Url::fromRoute('system.php')->toString());
}
// If a module is fully installed no pending updates exists.
$this->assertNoText(t('Out of date'));
// Set the schema version of update_test_postupdate to a lower version, so
// update_test_postupdate_update_8001() needs to be executed.
drupal_set_installed_schema_version('update_test_postupdate', 8000);
$this->drupalGet('admin/reports/status');
$this->assertText(t('Out of date'));
// Now cleanup the executed post update functions.
drupal_set_installed_schema_version('update_test_postupdate', 8001);
/** @var \Drupal\Core\Update\UpdateRegistry $post_update_registry */
$post_update_registry = \Drupal::service('update.post_update_registry');
$post_update_registry->filterOutInvokedUpdatesByModule('update_test_postupdate');
$this->drupalGet('admin/reports/status');
$this->assertText(t('Out of date'));
$this->drupalGet('admin/reports/status/php');
$this->assertResponse(200, 'The phpinfo page is reachable.');
}

View File

@ -123,6 +123,13 @@ abstract class UpdatePathTestBase extends WebTestBase {
*/
protected $strictConfigSchema = FALSE;
/**
* Fail the test if there are failed updates.
*
* @var bool
*/
protected $checkFailedUpdates = TRUE;
/**
* Constructs an UpdatePathTestCase object.
*
@ -246,7 +253,9 @@ abstract class UpdatePathTestBase extends WebTestBase {
$this->clickLink(t('Apply pending updates'));
// Ensure there are no failed updates.
$this->assertNoRaw('<strong>' . t('Failed:') . '</strong>');
if ($this->checkFailedUpdates) {
$this->assertNoRaw('<strong>' . t('Failed:') . '</strong>');
}
// The config schema can be incorrect while the update functions are being
// executed. But once the update has been completed, it needs to be valid

View File

@ -0,0 +1,53 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Update\UpdatePostUpdateFailingTest.
*/
namespace Drupal\system\Tests\Update;
/**
* Tests hook_post_update() when there are failing update hooks.
*
* @group Update
*/
class UpdatePostUpdateFailingTest extends UpdatePathTestBase {
/**
* {@inheritdoc}
*/
protected function setDatabaseDumpFiles() {
$this->databaseDumpFiles = [
__DIR__ . '/../../../tests/fixtures/update/drupal-8.bare.standard.php.gz',
__DIR__ . '/../../../tests/fixtures/update/drupal-8.update-test-postupdate-failing-enabled.php',
];
}
/**
* Tests hook_post_update_NAME().
*/
public function testPostUpdate() {
// There are expected to be failed updates.
$this->checkFailedUpdates = FALSE;
$this->runUpdates();
// There should be no post update hooks registered as being run.
$this->assertIdentical([], \Drupal::state()->get('post_update_test_execution', []));
$key_value = \Drupal::keyValue('update__post_update');
$this->assertEqual([], $key_value->get('existing_updates'));
}
/**
* {@inheritdoc}
*/
protected function doSelectionTest() {
// First update, should not be run since this module's update hooks fail.
$this->assertRaw('8001 - This update will fail.');
$this->assertRaw('8002 - A further update.');
$this->assertEscaped("First update, should not be run since this module's update hooks fail.");
}
}

View File

@ -0,0 +1,69 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Update\UpdatePostUpdateTest.
*/
namespace Drupal\system\Tests\Update;
/**
* Tests hook_post_update().
*
* @group Update
*/
class UpdatePostUpdateTest extends UpdatePathTestBase {
/**
* {@inheritdoc}
*/
protected function setDatabaseDumpFiles() {
$this->databaseDumpFiles = [
__DIR__ . '/../../../tests/fixtures/update/drupal-8.bare.standard.php.gz',
__DIR__ . '/../../../tests/fixtures/update/drupal-8.update-test-postupdate-enabled.php',
];
}
/**
* {@inheritdoc}
*/
protected function doSelectionTest() {
parent::doSelectionTest();
// Ensure that normal and post_update updates are merged together on the
// selection page.
$this->assertRaw('<ul><li>8001 - Normal update_N() function. </li><li>First update.</li><li>Second update.</li><li>Test1 update.</li><li>Test0 update.</li></ul>');
}
/**
* Tests hook_post_update_NAME().
*/
public function testPostUpdate() {
$this->runUpdates();
$this->assertRaw('<h3>Update first</h3>');
$this->assertRaw('First update');
$this->assertRaw('<h3>Update second</h3>');
$this->assertRaw('Second update');
$this->assertRaw('<h3>Update test1</h3>');
$this->assertRaw('Test1 update');
$this->assertRaw('<h3>Update test0</h3>');
$this->assertRaw('Test0 update');
$updates = [
'update_test_postupdate_post_update_first',
'update_test_postupdate_post_update_second',
'update_test_postupdate_post_update_test1',
'update_test_postupdate_post_update_test0',
];
$this->assertIdentical($updates, \Drupal::state()->get('post_update_test_execution', []));
$key_value = \Drupal::keyValue('post_update');
array_unshift($updates, 'block_post_update_disable_blocks_with_missing_contexts');
$this->assertEqual($updates, $key_value->get('existing_updates'));
$this->drupalGet('update.php/selection');
$this->assertText('No pending updates.');
}
}

View File

@ -598,18 +598,31 @@ function system_requirements($phase) {
);
// Check installed modules.
$has_pending_updates = FALSE;
foreach (\Drupal::moduleHandler()->getModuleList() as $module => $filename) {
$updates = drupal_get_schema_versions($module);
if ($updates !== FALSE) {
$default = drupal_get_installed_schema_version($module);
if (max($updates) > $default) {
$requirements['update']['severity'] = REQUIREMENT_ERROR;
$requirements['update']['value'] = t('Out of date');
$requirements['update']['description'] = t('Some modules have database schema updates to install. You should run the <a href="@update">database update script</a> immediately.', array('@update' => \Drupal::url('system.db_update')));
$has_pending_updates = TRUE;
break;
}
}
}
if (!$has_pending_updates) {
/** @var \Drupal\Core\Update\UpdateRegistry $post_update_registry */
$post_update_registry = \Drupal::service('update.post_update_registry');
$missing_post_update_functions = $post_update_registry->getPendingUpdateFunctions();
if (!empty($missing_post_update_functions)) {
$has_pending_updates = TRUE;
}
}
if ($has_pending_updates) {
$requirements['update']['severity'] = REQUIREMENT_ERROR;
$requirements['update']['value'] = t('Out of date');
$requirements['update']['description'] = t('Some modules have database schema updates to install. You should run the <a href="@update">database update script</a> immediately.', array('@update' => \Drupal::url('system.db_update')));
}
// Verify that no entity updates are pending after running every DB update.
if (!isset($requirements['update']['severity']) && \Drupal::entityDefinitionUpdateManager()->needsUpdates()) {

View File

@ -0,0 +1,40 @@
<?php
/**
* @file
* Partial database to mimic the installation of the update_test_post_update
* module.
*/
use Drupal\Core\Database\Database;
$connection = Database::getConnection();
// Set the schema version.
$connection->merge('key_value')
->condition('collection', 'system.schema')
->condition('name', 'update_test_postupdate')
->fields([
'collection' => 'system.schema',
'name' => 'update_test_postupdate',
'value' => 'i:8000;',
])
->execute();
// Update core.extension.
$extensions = $connection->select('config')
->fields('config', ['data'])
->condition('collection', '')
->condition('name', 'core.extension')
->execute()
->fetchField();
$extensions = unserialize($extensions);
$extensions['module']['update_test_postupdate'] = 8000;
$connection->update('config')
->fields([
'data' => serialize($extensions),
])
->condition('collection', '')
->condition('name', 'core.extension')
->execute();

View File

@ -0,0 +1,39 @@
<?php
/**
* @file
* Partial database to mimic the installation of the update_test_failing module.
*/
use Drupal\Core\Database\Database;
$connection = Database::getConnection();
// Set the schema version.
$connection->merge('key_value')
->condition('collection', 'system.schema')
->condition('name', 'update_test_failing')
->fields([
'collection' => 'system.schema',
'name' => 'update_test_failing',
'value' => 'i:8000;',
])
->execute();
// Update core.extension.
$extensions = $connection->select('config')
->fields('config', ['data'])
->condition('collection', '')
->condition('name', 'core.extension')
->execute()
->fetchField();
$extensions = unserialize($extensions);
$extensions['module']['update_test_failing'] = 8000;
$connection->update('config')
->fields([
'data' => serialize($extensions),
])
->condition('collection', '')
->condition('name', 'core.extension')
->execute();

View File

@ -0,0 +1,7 @@
<?php
/**
* Test post update function.
*/
function module_test_post_update_test() {
}

View File

@ -0,0 +1,6 @@
name: 'Update test failing'
type: module
description: 'Support module for update testing when an update hook is failing.'
package: Testing
version: VERSION
core: 8.x

View File

@ -0,0 +1,22 @@
<?php
/**
* @file
* Contains a failing update hook for testing the update system.
*/
use Drupal\Core\Utility\UpdateException;
/**
* This update will fail.
*/
function update_test_failing_update_8001() {
throw new UpdateException('This update hook is failing.');
}
/**
* A further update.
*/
function update_test_failing_update_8002() {
// This hook won't ever run.
}

View File

@ -0,0 +1,10 @@
<?php
/**
* First update, should not be run since this module's update hooks fail.
*/
function update_test_failing_post_update_first() {
$execution = \Drupal::state()->get('post_update_test_execution', []);
$execution[] = __FUNCTION__;
\Drupal::state()->set('post_update_test_execution', $execution);
}

View File

@ -0,0 +1,3 @@
core: 8.x
name: Update test after
type: module

View File

@ -0,0 +1,7 @@
<?php
/**
* Normal update_N() function.
*/
function update_test_postupdate_update_8001() {
}

View File

@ -0,0 +1,45 @@
<?php
/**
* First update.
*/
function update_test_postupdate_post_update_first() {
$execution = \Drupal::state()->get('post_update_test_execution', []);
$execution[] = __FUNCTION__;
\Drupal::state()->set('post_update_test_execution', $execution);
return 'First update';
}
/**
* Second update.
*/
function update_test_postupdate_post_update_second() {
$execution = \Drupal::state()->get('post_update_test_execution', []);
$execution[] = __FUNCTION__;
\Drupal::state()->set('post_update_test_execution', $execution);
return 'Second update';
}
/**
* Test1 update.
*/
function update_test_postupdate_post_update_test1() {
$execution = \Drupal::state()->get('post_update_test_execution', []);
$execution[] = __FUNCTION__;
\Drupal::state()->set('post_update_test_execution', $execution);
return 'Test1 update';
}
/**
* Test0 update.
*/
function update_test_postupdate_post_update_test0() {
$execution = \Drupal::state()->get('post_update_test_execution', []);
$execution[] = __FUNCTION__;
\Drupal::state()->set('post_update_test_execution', $execution);
return 'Test0 update';
}

View File

@ -0,0 +1,301 @@
<?php
/**
* @file
* Contains \Drupal\Tests\Core\Update\UpdateRegistryTest.
*/
namespace Drupal\Tests\Core\Update;
use Drupal\Core\KeyValueStore\KeyValueStoreInterface;
use Drupal\Core\Site\Settings;
use Drupal\Core\Update\UpdateRegistry;
use Drupal\Tests\UnitTestCase;
use org\bovigo\vfs\vfsStream;
/**
* @coversDefaultClass \Drupal\Core\Update\UpdateRegistry
* @group Update
*
* Note we load code, so we should better run in isolation.
*
* @runTestsInSeparateProcesses
*/
class UpdateRegistryTest extends UnitTestCase {
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$settings = [];
$settings['extension_discovery_scan_tests'] = TRUE;
new Settings($settings);
}
/**
* Sets up some modules with some update functions.
*/
protected function setupBasicModules() {
$info_a = <<<'EOS'
type: module
name: Module A
core: 8.x
EOS;
$info_b = <<<'EOS'
type: module
name: Module B
core: 8.x
EOS;
$module_a = <<<'EOS'
<?php
/**
* Module A update A.
*/
function module_a_post_update_a() {
}
/**
* Module A update B.
*/
function module_a_post_update_b() {
}
EOS;
$module_b = <<<'EOS'
<?php
/**
* Module B update A.
*/
function module_b_post_update_a() {
}
EOS;
vfsStream::setup('drupal');
vfsStream::create([
'sites' => [
'default' => [
'modules' => [
'module_a' => [
'module_a.post_update.php' => $module_a,
'module_a.info.yml' => $info_a
],
'module_b' => [
'module_b.post_update.php' => $module_b,
'module_b.info.yml' => $info_b
],
]
]
],
]);
}
/**
* @covers ::getPendingUpdateFunctions
*/
public function testGetPendingUpdateFunctionsNoExistingUpdates() {
$this->setupBasicModules();
$key_value = $this->prophesize(KeyValueStoreInterface::class);
$key_value->get('existing_updates', [])->willReturn([]);
$key_value = $key_value->reveal();
$update_registry = new UpdateRegistry('vfs://drupal', 'sites/default', [
'module_a',
'module_b'
], $key_value, FALSE);
$this->assertEquals([
'module_a_post_update_a',
'module_a_post_update_b',
'module_b_post_update_a'
], $update_registry->getPendingUpdateFunctions());
}
/**
* @covers ::getPendingUpdateFunctions
*/
public function testGetPendingUpdateFunctionsWithLoadedModulesButNotEnabled() {
$this->setupBasicModules();
$key_value = $this->prophesize(KeyValueStoreInterface::class);
$key_value->get('existing_updates', [])->willReturn([]);
$key_value = $key_value->reveal();
// Preload modules to ensure that ::getAvailableUpdateFunctions filters out
// not enabled modules.
include_once 'vfs://drupal/sites/default/modules/module_a/module_a.post_update.php';
include_once 'vfs://drupal/sites/default/modules/module_b/module_b.post_update.php';
$update_registry = new UpdateRegistry('vfs://drupal', 'sites/default', [
'module_a',
], $key_value, FALSE);
$this->assertEquals([
'module_a_post_update_a',
'module_a_post_update_b',
], $update_registry->getPendingUpdateFunctions());
}
/**
* @covers ::getPendingUpdateFunctions
*/
public function testGetPendingUpdateFunctionsExistingUpdates() {
$this->setupBasicModules();
$key_value = $this->prophesize(KeyValueStoreInterface::class);
$key_value->get('existing_updates', [])->willReturn(['module_a_post_update_a']);
$key_value = $key_value->reveal();
$update_registry = new UpdateRegistry('vfs://drupal', 'sites/default', [
'module_a',
'module_b'
], $key_value, FALSE);
$this->assertEquals(array_values([
'module_a_post_update_b',
'module_b_post_update_a'
]), array_values($update_registry->getPendingUpdateFunctions()));
}
/**
* @covers ::getPendingUpdateInformation
*/
public function testGetPendingUpdateInformation() {
$this->setupBasicModules();
$key_value = $this->prophesize(KeyValueStoreInterface::class);
$key_value->get('existing_updates', [])->willReturn([]);
$key_value = $key_value->reveal();
$update_registry = new UpdateRegistry('vfs://drupal', 'sites/default', [
'module_a',
'module_b'
], $key_value, FALSE);
$expected = [];
$expected['module_a']['pending']['a'] = 'Module A update A.';
$expected['module_a']['pending']['b'] = 'Module A update B.';
$expected['module_a']['start'] = 'a';
$expected['module_b']['pending']['a'] = 'Module B update A.';
$expected['module_b']['start'] = 'a';
$this->assertEquals($expected, $update_registry->getPendingUpdateInformation());
}
/**
* @covers ::getPendingUpdateInformation
*/
public function testGetPendingUpdateInformationWithExistingUpdates() {
$this->setupBasicModules();
$key_value = $this->prophesize(KeyValueStoreInterface::class);
$key_value->get('existing_updates', [])->willReturn(['module_a_post_update_a']);
$key_value = $key_value->reveal();
$update_registry = new UpdateRegistry('vfs://drupal', 'sites/default', [
'module_a',
'module_b'
], $key_value, FALSE);
$expected = [];
$expected['module_a']['pending']['b'] = 'Module A update B.';
$expected['module_a']['start'] = 'b';
$expected['module_b']['pending']['a'] = 'Module B update A.';
$expected['module_b']['start'] = 'a';
$this->assertEquals($expected, $update_registry->getPendingUpdateInformation());
}
/**
* @covers ::getModuleUpdateFunctions
*/
public function testGetModuleUpdateFunctions() {
$this->setupBasicModules();
$key_value = $this->prophesize(KeyValueStoreInterface::class)->reveal();
$update_registry = new UpdateRegistry('vfs://drupal', 'sites/default', [
'module_a',
'module_b'
], $key_value, FALSE);
$this->assertEquals(['module_a_post_update_a', 'module_a_post_update_b'], array_values($update_registry->getModuleUpdateFunctions('module_a')));
$this->assertEquals(['module_b_post_update_a'], array_values($update_registry->getModuleUpdateFunctions('module_b')));
}
/**
* @covers ::registerInvokedUpdates
*/
public function testRegisterInvokedUpdatesWithoutExistingUpdates() {
$this->setupBasicModules();
$key_value = $this->prophesize(KeyValueStoreInterface::class);
$key_value->get('existing_updates', [])->willReturn([]);
$key_value->set('existing_updates', ['module_a_post_update_a'])->willReturn(NULL);
$key_value = $key_value->reveal();
$update_registry = new UpdateRegistry('vfs://drupal', 'sites/default', [
'module_a',
'module_b'
], $key_value, FALSE);
$update_registry->registerInvokedUpdates(['module_a_post_update_a']);
}
/**
* @covers ::registerInvokedUpdates
*/
public function testRegisterInvokedUpdatesWithMultiple() {
$this->setupBasicModules();
$key_value = $this->prophesize(KeyValueStoreInterface::class);
$key_value->get('existing_updates', [])->willReturn([]);
$key_value->set('existing_updates', ['module_a_post_update_a', 'module_a_post_update_b'])->willReturn(NULL);
$key_value = $key_value->reveal();
$update_registry = new UpdateRegistry('vfs://drupal', 'sites/default', [
'module_a',
'module_b'
], $key_value, FALSE);
$update_registry->registerInvokedUpdates(['module_a_post_update_a', 'module_a_post_update_b']);
}
/**
* @covers ::registerInvokedUpdates
*/
public function testRegisterInvokedUpdatesWithExistingUpdates() {
$this->setupBasicModules();
$key_value = $this->prophesize(KeyValueStoreInterface::class);
$key_value->get('existing_updates', [])->willReturn(['module_a_post_update_b']);
$key_value->set('existing_updates', ['module_a_post_update_b', 'module_a_post_update_a'])->willReturn(NULL);
$key_value = $key_value->reveal();
$update_registry = new UpdateRegistry('vfs://drupal', 'sites/default', [
'module_a',
'module_b'
], $key_value, FALSE);
$update_registry->registerInvokedUpdates(['module_a_post_update_a']);
}
/**
* @covers ::filterOutInvokedUpdatesByModule
*/
public function testFilterOutInvokedUpdatesByModule() {
$this->setupBasicModules();
$key_value = $this->prophesize(KeyValueStoreInterface::class);
$key_value->get('existing_updates', [])->willReturn(['module_a_post_update_b', 'module_a_post_update_a', 'module_b_post_update_a']);
$key_value->set('existing_updates', ['module_b_post_update_a'])->willReturn(NULL);
$key_value = $key_value->reveal();
$update_registry = new UpdateRegistry('vfs://drupal', 'sites/default', [
'module_a',
'module_b'
], $key_value, FALSE);
$update_registry->filterOutInvokedUpdatesByModule('module_a');
}
}