Problem/Motivation
The following code causes a deprecation in PHP 8.1.
$config_override->set(str_replace(Row::PROPERTY_SEPARATOR, '.', $row->getDestinationProperty('property')), $row->getDestinationProperty('translation'));
It's because $row->getDestinationProperty('property')
returns NULL. So one fix would be to do (string) $row->getDestinationProperty('property')
- which is what happens in HEAD on other versions of PHP. However calling $config_override->set('', 'some value')
does not make any sense so this needs more investigation.
This had lead to the discovery that for many config translation migrations rollback will not work because translations
is not set to TRUE. See CR https://www.drupal.org/node/3242221 for the necessary changes to migrations.
Steps to reproduce
- Migrate Block translations on Drupal 6
- Try to rollback
-> configuration translations will still exist.
Proposed resolution
Use the translations
destination property to alway determine if the migration is for config translations rather than detecting based on keys in the source.
Remaining tasks
User interface changes
API changes
Data model changes
The destination translations
property must be set to true to create (and rollback) configuration translations.
Release notes snippet
Comment | File | Size | Author |
---|---|---|---|
#51 | 3239298-4-51.patch | 21 KB | alexpott |
#51 | 50-51-interdiff.txt | 1.26 KB | alexpott |
#50 | 3239298-4-50.patch | 20.97 KB | alexpott |
#50 | 48-50-interdiff.txt | 4.09 KB | alexpott |
#48 | 3239298-48.patch | 20.09 KB | andypost |
Comments
Comment #2
alexpottLet's investigate where this is happening.
Comment #3
alexpottComment #5
alexpottHere's a potential fix. I'm not sure we should be create config overrides if we don't have a property or translation to set.
Comment #6
daffie CreditAttribution: daffie commentedDo we need this change with the adding of
$row->hasDestinationProperty('property') && $row->hasDestinationProperty('translation') &&
to the if-statement? When we add this hunk, should we also add to the docblock the possibility that a exception can be thrown? Should we add testing for it?Moving this to the migration subsystem.
Comment #7
alexpott@daffie that exception is now unreachable - I left it in #5to prove the change fixes the issue. Here's a patch without the exception.
I have suspicions that this code is still not correct though. It's pulling random info from the destination and doing stuff with it. Can we be sure that
translation
andproperty
always mean what we want them to mean? This code was introduced by #2225717: Add config translation support to migrations and implement for Drupal 6 user profile fieldsComment #8
daffie CreditAttribution: daffie commented$row->hasDestinationProperty('property') && $row->hasDestinationProperty('translation') &&
was added to the if-statement.Shall we create a followup so that the migration experts can fix that?
Comment #9
alexpottWe don;'t need a comment in the code about
$row->hasDestinationProperty('property') && $row->hasDestinationProperty('translation')
- we're adding them because we depend on them in the block below.I think we should try to fix this in a better way here.
Comment #10
daffie CreditAttribution: daffie commentedBack to needs work then.
Comment #11
alexpottOkay this is a bit of a mess. The following code in HEAD
\Drupal\migrate\Plugin\migrate\destination\EntityConfigBase::updateEntity()
IS working out if we are creating a translation... and if it thinks we are - ie. the config entity langcode does not match the config langcode it is going to create a config language override by doing:
This relies on the destination having a 'property' and a 'translation' property.
HOWEVER... elsewhere in the code for example in the caller of
EntityConfigBase::updateEntity()
we do:So we should be calling
$this->isTranslationDestination()
in\Drupal\migrate\Plugin\migrate\destination\EntityConfigBase::updateEntity()
. But this is going to break things because not all migrations for config translations have this set properly. Compare core/modules/config_translation/migrations/d6_field_instance_label_description_translation.yml (has it) with core/modules/config_translation/migrations/d7_field_instance_label_description_translation.yml (does not).This is a bug issue because if
translations: true
is not set correctly we'll be doing unnecessary entity saves and\Drupal\migrate\Plugin\migrate\destination\EntityConfigBase::rollback()
will not work as expected.The patch attached will show us how broken all of this is.
Comment #12
alexpottHere are some more migrations that are not configured correctly and can't be rolled back.
Comment #14
andypostAs this migrations supporting translations it makes sense to add this property but it needs ++ on behavior change (bug fix)
it needs subsystem maintainer's review
Comment #15
quietone CreditAttribution: quietone as a volunteer commentedSorry for forgetting to add
translations: true
to many of the config translation issues.I don't recall a test of rolling back config translation so have added one to d7/MigrateMenuTranslationTest.php. The test fails because the migration does the label and the description in two separate source rows and thus there are two map rows for that language for a single menu. When looping through the id_map table the rollback will delete the entity on the first pass, not just remove the translated property. So, on the second pass the rollback can't find the entity to delete.
There is test of configuration rollback, \Drupal\Tests\migrate\Kernel\MigrateConfigRollbackTest, but that happens to test with two D7 variables which do not have more than one property than can be translated so the problem shown above with the Menu rollback does not occur.
No fix yet, just identify the problem.
Comment #17
andypostLooks strange failure
Comment #18
quietone CreditAttribution: quietone as a volunteer commentedFinally got back to this. getRowByDestination returns an array and since hat can be an empty array we should check the array before using it.
Comment #19
alexpottI think we need to call
$this->setRollbackAction($row->getIdMap(), $rollback_action);
inside theif ($translation) {
bit in \Drupal\migrate\Plugin\migrate\destination\EntityConfigBase::updateEntity()Comment #20
alexpottHmmm that does not fix the problem. But if we don't do this then how can we be sure that the config translation is deleted on rollback...
Comment #21
andypostThank you @quietone! I think it just needs code-comment to explain why map row is empty
It smells behavior change but it looks right
Comment #22
andypostI think improving interface docs
\Drupal\migrate\Plugin\MigrateIdMapInterface::getRowByDestination()
could prevent checking both implementationsComment #23
quietone CreditAttribution: quietone as a volunteer commentedYes, both of the two implementations of getRowByDestination() return an empty array so any usage should be able to handle an empty array. And the only usage in core is in \Drupal\migrate\MigrateExecutable::rollback().
I've tried to make the comment, added in #22, easier to read in this patch.
Comment #24
alexpottThis is looking pretty good now I think. We have way better test coverage we've fixed a bug in rollbacks and tested it. Nice work everyone!
Do we need a CR for this? It's kinda true atm (rollback does not work on config translation migrations without this) but may have been missed if people copied and pasted from many of the core examples.
Comment #25
quietone CreditAttribution: quietone as a volunteer commentedI am not convinced a CR is needed but more information can't hurt so I made one.
Comment #26
alexpott@quietone thanks for the CR.
I'm not sure about
I think that before this change
\Drupal\migrate\Plugin\migrate\destination\EntityConfigBase::rollback()
would not rollback config translations iftranslations: true
was not set because\Drupal\migrate\Plugin\migrate\destination\EntityConfigBase::isTranslationDestination()
would return FALSE if the migration was missing it.Comment #27
quietone CreditAttribution: quietone as a volunteer commented@alexpott, thanks. You are correct and I have modified the CR.
Comment #28
andypostComment #29
alexpott@quietone thanks for confirming. I've tweaked the CR to contain the important information for anyone who has custom configuration translation migrations and to be as clears as possible what needs to be done - while not getting into the weeds. If people want to know the details as to why they can read the discussions on this issue.
I think this issue is ready now :)
Comment #30
andypostI curious about how custom migrations could identify places where to put new line?
Comment #31
andypostI got it - when we use translatable (i18n) to config mapping
Missed this hunk, sorry
Comment #32
quietone CreditAttribution: quietone as a volunteer commentedThere is an issue that has a more comprehensive fix for the undefined array key problem, #3227660: MigrateExecutable::rollback incorrectly assumes MigrateMapInterface::getRowByDestination() returns an array with 'rollback_action' key. In that issue array_key_exists is used to specifically test for 'rollback_action' and there is a new Unit test. It was RTBC and now at NR because I changed a few lines to use defined constants.
Is it within scope to merge those changes in this issue?
Comment #36
alexpottAfter discussing this with @catch I've merged the tests from #3227660: MigrateExecutable::rollback incorrectly assumes MigrateMapInterface::getRowByDestination() returns an array with 'rollback_action' key and slightly improve on both issues logic of checking the result of ... going for:
isset($map_row) will not error for $map_row = FALSE.
Crediting the people who worked on that issue and will mark that one as a duplicate.
Comment #37
huzookaFYI the error/warning can be triggered with PHP 7.4 as well.
But since an underlying sql map interface implementation was fixed weeks ago (and commited into 9.3.x), imho it is enough to check for the array key.
Based on the interface, the migrate sql id map implemetnation should return an array.
Comment #38
huzookaComment #39
alexpott@huzooka the code
checks for an array key - it also happens to be a bit more robust if $map_row happens to be false. There's no need to use
array_key_exists()
here because if$map_row['rollback_action']
is equal toNULL
then.... oh well...const ROLLBACK_DELETE = 0;
and$map_row['rollback_action'] == MigrateIdMapInterface::ROLLBACK_DELETE
. That sucks...$map_row['rollback_action'] = NULL
will return TRUE atm. We need to stop using == everywhere. See https://3v4l.org/628ckPersonally I think we should not care about
$map_row['rollback_action'] = NULL
. The looseness in this code atm is a bug creator.Comment #40
huzooka@alexpott, the sql id map plugin interface declares that getRowByDestination() must return an array.
If it returns something else, then the plugin violates its interface:
https://git.drupalcode.org/project/drupal/-/blob/9.3.x/core/modules/migr...
Comment #41
huzookaIt feels easier and more straightforward solving these separate bugs in their own task...
Comment #42
alexpott@huzooka it's not forced by the interface only by documentation - we have far too many places in core where documentation != reality. If it was
public function getRowByDestination(array $destination_id_values): array;
then sure but something may have extended \Drupal\migrate\Plugin\migrate\id_map\Sql in custom and is still returning FALSE, NULL or whatever.These bugs are intimately linked. We can't fix config translation rollback without fixing the bug in \Drupal\migrate\MigrateExecutable::rollback() as well. And I think not being able to rollback a migration is a way more significant bug than a PHP notice.
Updating the issue summary so we're clear about what is being fixed here.
Comment #43
catchI think we can just ignore the behaviour change for
$map_row['rollback_action'] === NULL
since it's not really supported, just accidental and unlikely to be relied upon. We could have a follow-up to add an assert for a valid value, but prefer the simpler code, explicitly accounting for NULL with array_key_exists() makes it look like a meaningful option when it isn't.Extreme nit: I think this should be 'does not' instead of 'do not', because it's singular.
Comment #44
andypostre-queued for all databases again after #3241308: core/tests/Drupal/Tests/Core/Entity/Sql/DefaultTableMappingTest.php triggers deprecations on PHP 8.1
Comment #45
andypostFix #43-nit
I see no reason in
array_key_exists()
and having no rollback is totally valid caseComment #46
andypostThis way it will be more similar to old behavior
Comment #48
andypostOrder of execution
Comment #49
longwaveThis is confusing to read, and then I learned from earlier in the thread that we are actually testing
if (($var ?? NULL) == 0)
here, which results in TRUE if the var is not set - would prefer this to be explicit, or explained with a comment if we are keeping it like this.Comment #50
alexpott@longwave I agree. Let's make it way more explicit and actually test this behaviour properly.
Comment #51
alexpottBetter prophecy.
Comment #52
alexpottThis is the new version of the contentious if. It will not emit an error if $map_row is an array without a rollback_action or $map_row is FALSE... and it behaves the same way - i.e. if if can't determine a rollback_action it assumes the ->rollback() should be called.
We could add a comment like:
But given this is now
!isset($map_row['rollback_action']) ||
I think it is explicit and obvious enough. Plus we have tests.Comment #53
alexpottHere's evidence that the new formulation works the same way as the old - https://3v4l.org/hGIMP - with we less PHP notices... see https://3v4l.org/Lf6NF for only the new version.
If $map_row is FALSE we will get a deprecation on PHP 8.1 because as pointed out a few times that's against the interface documentation so I think that is okay. It won't be broken and if you have custom code that triggers this you should fix it.
Comment #54
longwave+1 for the additional testing, and happy with the fix to the
if
- as we are now explicit about the!isset
I don't think we need an additional comment.The rest of the changes look fine, so RTBC assuming bot agrees.
Comment #56
catchCommitted c1933e7 and pushed to 9.3.x. Thanks!