We found that after migrating our Drupal content management system to PHP 7.3, we experienced some issue related to the unserialize failure of a serialized input batch object. This batch object comes from a multi-step input form, which has following data structure
batch (array)
'sets'=> array,
...
'form_state' => Drupal\Core\Form\FormState
....
'complete_form'=>....
'values' => .....
....//Other fields
In above example, the Batch is an array of objects, inside it has a "form_state" object (Drupal FormState), inside "form_state", it has another array of the "complete_form", which encapsulate all the request form from UI.
To simple test, you can just run
unserialize(serialize($batch)
it will return false in above case.
Following are findings by playing around the unserialize(serialize($batch)
1: The reason "unserialize(serialize($batch)" return false is because "unserialize(serialize($batch['form_state'])" return false
2: If we create an empty FormState, like "$batch['form_state'] = new FormState()", unserialize(serialize($batch) will return as an array
3: If we assign some nested array of object inside 'form_state', like the "complete_form" above inside, keep the original nested structure of the $batch object as above, then unserialize(serialize($batch) return false
4: If we run "unserialize(serialize($batch['form_state'])", it will return correctly with a nested array of objects
This makes us to workaround the serialize/unserialize issue of $Batchby combing 3 and 4 above into an array, and then serialize()/unserialize it in in the BatchStorage to make sure it workaround the problem we have. But this change of data structure of the input batch object, is just an additional catch of the problem we have. This issue should be addressed in the Drupal core in a more generic solution, for example:
1: Make sure the input batch object is always serializable/deserialize correctly, which may not be easy since the input form data can be configured based on specific request, we need to make sure each nested object inside the batch object has correctly serialization implemented (either through the Serializable interface or method pair of __sleep() and __awake().
2: Add some validation in the batch processing, or change the logic of batch processing not using serialize/unserialize pair, instead, use something like json_encode/json_decode (need to reconfig the PHP object from the decode), or
3: Add certain validation before persisting to database, and validate the return after "unserialize" from database return.
Comment | File | Size | Author |
---|---|---|---|
#20 | drupal-batchstorage-unserialize-formstate-3055287-19.patch | 1.17 KB | Henry Tran |
#14 | drupal-batchstorage-unserialize-formstate-3055287-14.patch | 1.02 KB | chOP |
Comments
Comment #2
wangchunzhang CreditAttribution: wangchunzhang commentedComment #3
ndrake86 CreditAttribution: ndrake86 at Workday, Inc. commentedAttaching the patch to a comment so the test bot can run it. This is the same patch as uploaded in the body.
Comment #4
ndrake86 CreditAttribution: ndrake86 at Workday, Inc. commentedUploading a patch for @Wangchun he is having issues because of his account:
Comment #5
ndrake86 CreditAttribution: ndrake86 at Workday, Inc. commentedTestbot doesn't have an issue w/ it moving to needs review.
Comment #7
kevin.dutra CreditAttribution: kevin.dutra at Workday, Inc. commentedThere are a few problems here:
FormState
class.Also, please read the issue tagging guidlines before adding tags to issues. Thanks!
Comment #8
ndrake86 CreditAttribution: ndrake86 at Workday, Inc. commented1. Yes, a test would be great. The test we have that identified the issue is all based on custom code, one of the main drivers to putting the ticket on D.O was for the test bot in core to run the tests around batch and hopefully to spark some conversation that anyone else saw around serialization of the form_state. I will see if I can carve out some time to write a test based only on core code that reproduces the issue.
2. Yes, this is a super specific patch. Luckily the other workd-arounds for for PHP 73. I actually think this post shows something similarly wrong w/ batch -> https://www.drupal.org/project/drupal/issues/3011684
3. Its a known PHP change, they "fixed" a bug in 7.3 that made unserialize stricter, from the changelog (https://www.php.net/ChangeLog-7.php):
there isn't an issue associated w/ that change so don't really know what spawned the change. If you read through some of the other issues here about serialize and unserialize you can see find a link to the conversation on the PHP email list where the PHP maintainers explains some of the issues w/ serialize unserialize:
Comment #9
wangchunzhang CreditAttribution: wangchunzhang commentedThe correct fix, based on my investigation, can be let the input batch go through some Temp Store (like SharedTempStore) before the real serialize/unserialize call in BatchStorage, I noticed that in Drupal core, the BatchStorage is in a position equal to PHPSerialize. but a lot of calls into PHPSerialize comes from object in SharedTempStore & PrivateTempStore, while BatchStorage doesn't, it directly come from input form state, for example, the FormState in this case.
There is a workaround applied in SharedTempStore (https://www.drupal.org/files/issues/2019-04-23/3049437-6.patch) to workaround PHP7.3 serialize/unserialize issue. Also, workaround is already in Drupal Core (8.6.16) for PrivateTempStore, etc. this is the reason for above fix suggestion.
Of course, the ultimate fix should also include the fix to make the PHP serialize/unserialize more robust. Based on the discussion, it is obvious they are not as good as it should be.
Comment #10
daffie CreditAttribution: daffie commentedCan we do it more like:
And use the value of $batch_serialized in the insert/update query.
The method testSerialization() should not do the inserting or updating.
Let the method serialization() do everything that testSerialization() without the insert/update.
Comment #11
Henry Tran CreditAttribution: Henry Tran at Deloitte Digital commentedUpdate patch to 8.8.x
Comment #12
chOP CreditAttribution: chOP at Deloitte Digital commentedComment #13
chOP CreditAttribution: chOP at Deloitte Digital commentedTagging against topical issue tag for PHP 7.3 as this issue impacts sites running PHP 7.3.0 or later. Updating to version to 8.8.x-dev
We may also want to link this solution to other PHP 7.3 Workarounds #3001920: Investigate PHP 7.3 workarounds
The first step should be to write a test that can reproduce the problem.
The solution patch provided so far reads as too specific and hacky. It might be better to follow the patterns used when solving this in #3049437: SharedTempStore not serializable on PHP 7.3 and #3011684: Properly serialize \Drupal\Core\TempStore\PrivateTempStore where they leverage the DependencySerializationTrait to serialize/unserialize
Comment #14
chOP CreditAttribution: chOP at Deloitte Digital commentedFind attached a new solution that is allows FormState to be serialized intact, so it can be used in batch Form submissions. This solution implements the Drupal\Core\DependencyInjection\DependencySerializationTrait
When returning the list of properties to serialize, we remove the build_info as this contains Symfony Routes and Requests and other services that shouldn't be serialized and unserialized again. By removing them for serialization it seems to have fixed the problem.
I've yet to find a simple way to write a test for this. Was looking at how FormState unit tests are set-up for clues.
Comment #16
philltran CreditAttribution: philltran at Symmetri Technology commented@chOP
Thanks for patch #14. It seems to solve the issue I was having with runnint batch processes from a form on PHP 7.3.
However, previewing a node errors out with:
The form object has not yet been stored on the form state by the form builder
Comment #17
philltran CreditAttribution: philltran at Symmetri Technology commentedRealized that the error message I mentioned was from a patch on Issue 2897557
Here is a patch for this issue that fixed the preview error on my local. Please test it.
Thanks.
Comment #18
philltran CreditAttribution: philltran at Symmetri Technology commentedUpdated patch to use array_diff instead of unset-ing. Aka going back to what @chOP had in their patch.
Comment #20
Henry Tran CreditAttribution: Henry Tran at Deloitte Digital commentedThank you Chopper.
I've updated the patch for fixing error in preview content
Cheer
Comment #22
larowlanComment #23
larowlanThanks for working on this, as its a bug, we need a new test demonstrating the bug
shouldn't we be using array_filter with 'is_object' on $obj_vars['build_info']['args'] in case the second, third or subsequent argument is an object?
no need for the else here as we return earlier
Comment #24
hoanns CreditAttribution: hoanns commentedHello,
this is the simplest form I could write to reproduce the problem. Maybe someone can write a test with that. Or this isn't correlated and is a bug in requestStack?
This works when you use the form normally in the browser, but when you serialize it won't work.
To fix serialize for this form you could use either \Drupal::request() or set $oRequest to private. But I just wrote this form to help reproduce the problem. Hope someone more knowledgeable can understand the underlying problem :)
Comment #26
refman1073 CreditAttribution: refman1073 commentedI discovered that injecting the logger.factory service into a class also causes batch to fail because of serialization. Digging a bit deeper, logger uses the RequestStack, which is at the root of the problem.
In looking at the serialized data in the batch table, the unserialize is failing because the request parameter _access_result value is r:xxx where the xxx reference is not defined (I don't fully understand the r syntax so I cannot give greater detail.
I hope this helps.
Comment #28
alexdoma CreditAttribution: alexdoma as a volunteer and commentedConfirm that removing logger.factory service fix the problem
Comment #29
andrewsuth CreditAttribution: andrewsuth at World Food Programme commentedJust to confirm that the patch from #20 works.
Comment #30
InspiredNotion CreditAttribution: InspiredNotion commentedI have just installed a fresh latest version of drupal 9.3X andi am getting this message on opening and logging into the site. anyone know why this is still here in the latest version and is there a fix. thanks
Daniel
Comment #32
hidabe CreditAttribution: hidabe commentedThe last path (#20) not is working for me,
I have need add another condition for remove build_info field, so:
In Drupal 9 all work ok about this issue.
Comment #33
sergiurRE: #26 also confirming that's the cause -- removing the logger factory service fixed my issue as well. I've been struggling with it for longer than I care to admit, thank you @refman1073.
With the logger factory in (and since PHP7.3 and up) the form_state on my Configurable Action (buildConfigurationForm) errors on serialization. Without it, no errors!
Comment #35
belatrix CreditAttribution: belatrix as a volunteer commentedLast patch (#20) with changes from comment #32 worked for me on project with Drupal 9.
Also decision from #33 worked, looks like problem in logger factory.