Issue #3145501 by alexpott, Spokje, plach, quietone, catch, smustgrave, longwave, larowlan, xjm, mxwright: updb error processMultivalueBaseFieldHandler()

merge-requests/2510/head
catch 2022-09-15 20:41:07 +01:00
parent 2a26adc850
commit 04d7a53390
7 changed files with 632 additions and 9 deletions

View File

@ -4,6 +4,9 @@ namespace Drupal\Core\Config\Entity;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Url;
use Drupal\Core\Utility\Error;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
@ -85,11 +88,17 @@ class ConfigEntityUpdater implements ContainerInjectionInterface {
* validated against schema on save to avoid unexpected errors. If a
* callback is not provided, the default behavior is to update the
* dependencies if required.
* @param bool $continue_on_error
* Set to TRUE to continue updating if an error has occurred.
*
* @see hook_post_update_NAME()
*
* @api
*
* @return \Drupal\Core\StringTranslation\TranslatableMarkup|null
* An error message if $continue_on_error is set to TRUE and an error has
* occurred.
*
* @throws \InvalidArgumentException
* Thrown when the provided entity type ID is not a configuration entity
* type.
@ -97,7 +106,7 @@ class ConfigEntityUpdater implements ContainerInjectionInterface {
* Thrown when used twice in the same update function for different entity
* types. This method should only be called once per update function.
*/
public function update(array &$sandbox, $entity_type_id, callable $callback = NULL) {
public function update(array &$sandbox, $entity_type_id, callable $callback = NULL, bool $continue_on_error = FALSE) {
$storage = $this->entityTypeManager->getStorage($entity_type_id);
if (isset($sandbox[self::SANDBOX_KEY]) && $sandbox[self::SANDBOX_KEY]['entity_type'] !== $entity_type_id) {
@ -111,6 +120,7 @@ class ConfigEntityUpdater implements ContainerInjectionInterface {
$sandbox[self::SANDBOX_KEY]['entity_type'] = $entity_type_id;
$sandbox[self::SANDBOX_KEY]['entities'] = $storage->getQuery()->accessCheck(FALSE)->execute();
$sandbox[self::SANDBOX_KEY]['count'] = count($sandbox[self::SANDBOX_KEY]['entities']);
$sandbox[self::SANDBOX_KEY]['failed_entity_ids'] = [];
}
// The default behavior is to fix dependencies.
@ -125,13 +135,62 @@ class ConfigEntityUpdater implements ContainerInjectionInterface {
/** @var \Drupal\Core\Config\Entity\ConfigEntityInterface $entity */
$entities = $storage->loadMultiple(array_splice($sandbox[self::SANDBOX_KEY]['entities'], 0, $this->batchSize));
foreach ($entities as $entity) {
if (call_user_func($callback, $entity)) {
$entity->trustData();
$entity->save();
try {
if ($continue_on_error) {
// If we're continuing on error silence errors from notices that
// missing indexes.
// @todo consider change this to an error handler that converts such
// notices to exceptions in https://www.drupal.org/node/3309886
@$this->doOne($entity, $callback);
}
else {
$this->doOne($entity, $callback);
}
}
catch (\Throwable $throwable) {
if (!$continue_on_error) {
throw $throwable;
}
$context['%view'] = $entity->id();
$context['%entity_type'] = $entity_type_id;
$context += Error::decodeException($throwable);
\Drupal::logger('update')->error('Unable to update %entity_type %view due to error @message %function (line %line of %file). <pre>@backtrace_string</pre>', $context);
$sandbox[self::SANDBOX_KEY]['failed_entity_ids'][] = $entity->id();
}
}
$sandbox['#finished'] = empty($sandbox[self::SANDBOX_KEY]['entities']) ? 1 : ($sandbox[self::SANDBOX_KEY]['count'] - count($sandbox[self::SANDBOX_KEY]['entities'])) / $sandbox[self::SANDBOX_KEY]['count'];
if (!empty($sandbox[self::SANDBOX_KEY]['failed_entity_ids'])) {
$entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
if (\Drupal::moduleHandler()->moduleExists('dblog')) {
return new TranslatableMarkup("Updates failed for the entity type %entity_type, for %entity_ids. <a href=:url>Check the logs</a>.", [
'%entity_type' => $entity_type->getLabel(),
'%entity_ids' => implode(', ', $sandbox[self::SANDBOX_KEY]['failed_entity_ids']),
':url' => Url::fromRoute('dblog.overview')->toString(),
]);
}
else {
return new TranslatableMarkup("Updates failed for the entity type %entity_type, for %entity_ids. Check the logs.", [
'%entity_type' => $entity_type->getLabel(),
'%entity_ids' => implode(', ', $sandbox[self::SANDBOX_KEY]['failed_entity_ids']),
]);
}
}
}
/**
* Apply the callback an entity and save it if the callback makes changes.
*
* @param \Drupal\Core\Config\Entity\ConfigEntityInterface $entity
* The entity to potentially update.
* @param callable $callback
* The callback to apply.
*/
protected function doOne(ConfigEntityInterface $entity, callable $callback) {
if (call_user_func($callback, $entity)) {
$entity->trustData();
$entity->save();
}
}
}

View File

@ -0,0 +1,35 @@
<?php
/**
* @file
* Text fixture.
*/
use Drupal\Core\Database\Database;
use Drupal\Core\Serialization\Yaml;
$connection = Database::getConnection();
$connection->insert('config')
->fields([
'collection' => '',
'name' => 'views.view.test_user_multi_value',
'data' => serialize(Yaml::decode(file_get_contents(__DIR__ . '/views.view.test_user_multi_value.yml'))),
])
->execute();
$connection->insert('config')
->fields([
'collection' => '',
'name' => 'views.view.test_broken_config_multi_value',
'data' => serialize(Yaml::decode(file_get_contents(__DIR__ . '/views.view.test_broken_config_multi_value.yml'))),
])
->execute();
$connection->insert('config')
->fields([
'collection' => '',
'name' => 'views.view.test_another_broken_config_multi_value',
'data' => serialize(Yaml::decode(file_get_contents(__DIR__ . '/views.view.test_another_broken_config_multi_value.yml'))),
])
->execute();

View File

@ -0,0 +1,239 @@
langcode: en
status: true
dependencies:
module:
- user
id: test_another_broken_config_multi_value
label: test_another_broken_config_multi_value
module: views
description: ''
tag: ''
base_table: users_field_data
base_field: uid
display:
default:
display_plugin: default
id: default
display_title: Default
position: 0
display_options:
access:
type: perm
options:
perm: 'access user profiles'
cache:
type: tag
options: { }
query:
type: views_query
options:
disable_sql_rewrite: false
distinct: false
replica: false
query_comment: ''
query_tags: { }
exposed_form:
type: basic
options:
submit_button: Filter
reset_button: false
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: true
sort_asc_label: Asc
sort_desc_label: Desc
pager:
type: mini
options:
items_per_page: 10
offset: 0
id: 0
total_pages: null
expose:
items_per_page: false
items_per_page_label: 'Items per page'
items_per_page_options: '5, 10, 25, 50'
items_per_page_options_all: false
items_per_page_options_all_label: '- All -'
offset: false
offset_label: Offset
tags:
previous:
next:
style:
type: default
options:
grouping: { }
row_class: ''
default_row_class: true
uses_fields: false
row:
type: fields
options:
inline: { }
separator: ''
hide_empty: false
default_field_elements: true
fields:
roles:
id: roles
table: user__roles
field: roles
relationship: none
group_type: group
admin_label: ''
label: ''
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: false
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: target_id
type: entity_reference_label
settings:
link: true
group_column: target_id
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
entity_type: user
entity_field: roles
plugin_id: field
rendered_entity: null
filters:
roles:
id: roles
table: user__roles
field: roles
relationship: none
group_type: group
admin_label: ''
operator: '='
value: ''
group: 1
exposed: false
expose:
operator_id: ''
label: ''
description: ''
use_operator: false
operator: ''
identifier: ''
required: false
remember: false
multiple: false
remember_roles:
authenticated: authenticated
is_grouped: false
group_info:
label: ''
description: ''
identifier: ''
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items: { }
entity_type: user
entity_field: roles
plugin_id: string
sorts: { }
header: { }
footer: { }
empty: { }
relationships: { }
arguments:
roles:
id: roles
table: user__roles
field: roles
relationship: none
group_type: group
admin_label: ''
default_action: ignore
exception:
value: all
title_enable: false
title: All
title_enable: false
title: ''
default_argument_type: fixed
default_argument_options:
argument: ''
default_argument_skip_url: false
summary_options:
base_path: ''
count: true
items_per_page: 25
override: false
summary:
sort_order: asc
number_of_records: 0
format: default_summary
specify_validation: false
validate:
type: none
fail: 'not found'
validate_options: { }
glossary: false
limit: 0
case: none
path_case: none
transform_dash: false
break_phrase: false
entity_type: user
entity_field: roles
plugin_id: string
display_extenders: { }
cache_metadata:
max-age: -1
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url
- url.query_args
- user.permissions
tags: { }

View File

@ -0,0 +1,240 @@
uuid: d96b5368-de0a-4a84-af12-9535d4ad4c6f
langcode: en
status: true
dependencies:
module:
- user
id: test_broken_config_multi_value
label: test_broken_config_multi_value
module: views
description: ''
tag: ''
base_table: users_field_data
base_field: uid
display:
default:
display_plugin: default
id: default
display_title: Default
position: 0
display_options:
access:
type: perm
options:
perm: 'access user profiles'
cache:
type: tag
options: { }
query:
type: views_query
options:
disable_sql_rewrite: false
distinct: false
replica: false
query_comment: ''
query_tags: { }
exposed_form:
type: basic
options:
submit_button: Filter
reset_button: false
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: true
sort_asc_label: Asc
sort_desc_label: Desc
pager:
type: mini
options:
items_per_page: 10
offset: 0
id: 0
total_pages: null
expose:
items_per_page: false
items_per_page_label: 'Items per page'
items_per_page_options: '5, 10, 25, 50'
items_per_page_options_all: false
items_per_page_options_all_label: '- All -'
offset: false
offset_label: Offset
tags:
previous:
next:
style:
type: default
options:
grouping: { }
row_class: ''
default_row_class: true
uses_fields: false
row:
type: fields
options:
inline: { }
separator: ''
hide_empty: false
default_field_elements: true
fields:
roles:
id: roles
table: user__roles
field: roles
relationship: none
group_type: group
admin_label: ''
label: ''
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: false
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: target_id
type: entity_reference_label
settings:
link: true
group_column: target_id
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
entity_type: user
entity_field: roles
plugin_id: field
rendered_entity: null
filters:
roles:
id: roles
table: user__roles
field: roles
relationship: none
group_type: group
admin_label: ''
operator: '='
value: ''
group: 1
exposed: false
expose:
operator_id: ''
label: ''
description: ''
use_operator: false
operator: ''
identifier: ''
required: false
remember: false
multiple: false
remember_roles:
authenticated: authenticated
is_grouped: false
group_info:
label: ''
description: ''
identifier: ''
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items: { }
entity_type: user
entity_field: roles
plugin_id: string
sorts: { }
header: { }
footer: { }
empty: { }
relationships: { }
arguments:
roles:
id: roles
table: user__roles
field: roles
relationship: none
group_type: group
admin_label: ''
default_action: ignore
exception:
value: all
title_enable: false
title: All
title_enable: false
title: ''
default_argument_type: fixed
default_argument_options:
argument: ''
default_argument_skip_url: false
summary_options:
base_path: ''
count: true
items_per_page: 25
override: false
summary:
sort_order: asc
number_of_records: 0
format: default_summary
specify_validation: false
validate:
type: none
fail: 'not found'
validate_options: { }
glossary: false
limit: 0
case: none
path_case: none
transform_dash: false
break_phrase: false
entity_type: user
entity_field: roles
plugin_id: string
display_extenders: { }
cache_metadata:
max-age: -1
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url
- url.query_args
- user.permissions
tags: { }

View File

@ -14,7 +14,6 @@ description: ''
tag: ''
base_table: users_field_data
base_field: uid
core: 8.x
display:
default:
display_plugin: default

View File

@ -0,0 +1,51 @@
<?php
namespace Drupal\Tests\views\Functional\Update;
use Drupal\FunctionalTests\Update\UpdatePathTestBase;
/**
* Tests the update path base class.
*
* @group Update
* @group legacy
*/
class ViewsMultiValueFieldUpdateTest extends UpdatePathTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['update_test_schema'];
/**
* {@inheritdoc}
*/
protected static $configSchemaCheckerExclusions = [
// This config is broken intentionally.
'views.view.test_broken_config_multi_value',
'views.view.test_another_broken_config_multi_value',
];
/**
* {@inheritdoc}
*/
protected function setDatabaseDumpFiles() {
$this->databaseDumpFiles = [
__DIR__ . '/../../../../../system/tests/fixtures/update/drupal-9.3.0.bare.standard.php.gz',
__DIR__ . '/../../../fixtures/update/multi_value_fields.php',
];
}
/**
* Tests views_post_update_field_names_for_multivalue_fields().
*/
public function testViewsPostUpdateFieldNamesForMultiValueFields() {
$this->runUpdates();
$this->assertSession()->pageTextContainsOnce('Updates failed for the entity type View, for test_another_broken_config_multi_value, test_broken_config_multi_value. Check the logs.');
$this->drupalLogin($this->rootUser);
$this->drupalGet('admin/reports/dblog', ['query' => ['type[]' => 'update']]);
$this->assertSession()->pageTextMatchesCount(2, '/Unable to update view test_broken_config_multi_value/');
}
}

View File

@ -37,13 +37,13 @@ function views_removed_post_updates() {
/**
* Update field names for multi-value base fields.
*/
function views_post_update_field_names_for_multivalue_fields(&$sandbox = NULL) {
function views_post_update_field_names_for_multivalue_fields_followup(&$sandbox = NULL) {
/** @var \Drupal\views\ViewsConfigUpdater $view_config_updater */
$view_config_updater = \Drupal::classResolver(ViewsConfigUpdater::class);
$view_config_updater->setDeprecationsEnabled(FALSE);
\Drupal::classResolver(ConfigEntityUpdater::class)->update($sandbox, 'view', function ($view) use ($view_config_updater) {
return \Drupal::classResolver(ConfigEntityUpdater::class)->update($sandbox, 'view', function ($view) use ($view_config_updater) {
return $view_config_updater->needsMultivalueBaseFieldUpdate($view);
});
}, TRUE);
}
/**
@ -103,5 +103,5 @@ function views_post_update_image_lazy_load(?array &$sandbox = NULL): void {
$view_config_updater = \Drupal::classResolver(ViewsConfigUpdater::class);
\Drupal::classResolver(ConfigEntityUpdater::class)->update($sandbox, 'view', function (ViewEntityInterface $view) use ($view_config_updater): bool {
return $view_config_updater->needsImageLazyLoadFieldUpdate($view);
});
}, TRUE);
}