Problem/Motivation

I've run into serialization errors when trying to preview a node.

It was a very painful process (https://github.com/EdeMeijer/serialize-debugger helped but I had to make it work with our insane object & array structures first), but I tracked it down to Context objects in $form_state, which use TypedDataTrait.

In a way, this is the same problem as #2893029: EntityType objects cannot be reliably serialized without DependencySerializationTrait, so we could possibly also use the same fix instead of using DependencySerializationTrait. Or both.

Proposed resolution

Add DependencySerializationTrait to the Context class

Remaining tasks

Write tests

User interface changes

N/A

API changes

N/A

Data model changes

N/A

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

Berdir created an issue. See original summary.

Berdir’s picture

Status: Active » Needs review
FileSize
785 bytes
MegaChriz’s picture

@Berdir
What did you need to do to make https://github.com/EdeMeijer/serialize-debugger work? Adjust the library? Adjust code in Drupal?

I get an error when trying the following code in a method called buildPaneForm() where serialize($this) failed:

\EdeMeijer\SerializeDebugger\Debugger::debugPlainText($this);die();

The error:

LogicException: The database connection is not serializable. This probably means you are serializing an object that has an indirect reference to the database connection. Adjust your code so that is not necessary. Alternatively, look at DependencySerializationTrait as a temporary solution. in Drupal\Core\Database\Connection->__sleep() (line 1472 of core/lib/Drupal/Core/Database/Connection.php).

call_user_func(Array) (Line: 38)
EdeMeijer\SerializeDebugger\Resolver\ObjectResolver->getProperties(Object) (Line: 23)
EdeMeijer\SerializeDebugger\Resolver\AbstractResolver->resolve(Object, Object) (Line: 42)
EdeMeijer\SerializeDebugger\Resolver\ChainingResolver->resolve(Object, Object) (Line: 30)
EdeMeijer\SerializeDebugger\Resolver\RecursiveResolver->resolve(Object, Object) (Line: 32)
EdeMeijer\SerializeDebugger\Resolver\RecursiveResolver->resolve(Object, Object) (Line: 32)
EdeMeijer\SerializeDebugger\Resolver\RecursiveResolver->resolve(Object, Object) (Line: 32)
EdeMeijer\SerializeDebugger\Resolver\RecursiveResolver->resolve(Object, Object) (Line: 32)
EdeMeijer\SerializeDebugger\Resolver\RecursiveResolver->resolve(Object, Object) (Line: 32)
EdeMeijer\SerializeDebugger\Resolver\RecursiveResolver->resolve(Object, Object) (Line: 32)
EdeMeijer\SerializeDebugger\Resolver\RecursiveResolver->resolve(Object, Object) (Line: 32)
EdeMeijer\SerializeDebugger\Resolver\RecursiveResolver->resolve(Object, Object) (Line: 32)
EdeMeijer\SerializeDebugger\Resolver\RecursiveResolver->resolve(Object, Object) (Line: 32)
EdeMeijer\SerializeDebugger\Resolver\RecursiveResolver->resolve(Object, Object) (Line: 32)
EdeMeijer\SerializeDebugger\Resolver\RecursiveResolver->resolve(Object, Object) (Line: 32)
EdeMeijer\SerializeDebugger\Resolver\RecursiveResolver->resolve(Object, Object) (Line: 55)
EdeMeijer\SerializeDebugger\Debugger->debug(Object) (Line: 70)
EdeMeijer\SerializeDebugger\Debugger::debugPlaintext(Object) (Line: 75)
Drupal\commerce_promotion\Plugin\Commerce\CheckoutPane\CouponRedemption->buildPaneForm(Array, Object, Array) (Line: 556)
Drupal\commerce_checkout\Plugin\Commerce\CheckoutFlow\CheckoutFlowWithPanesBase->buildForm(Array, Object, 'review') (Line: 46)
Drupal\ukkb_commerce\Plugin\Commerce\CheckoutFlow\Multistep->buildForm(Array, Object, 'review') (Line: 103)
Drupal\ukkbstudy\Plugin\Commerce\CheckoutFlow\UKKBMultistepEvent->buildForm(Array, Object, 'review')
call_user_func_array(Array, Array) (Line: 518)
Drupal\Core\Form\FormBuilder->retrieveForm('commerce_checkout_flow_ukkbstudy_multistep_event', Object) (Line: 275)
Drupal\Core\Form\FormBuilder->buildForm('commerce_checkout_flow_ukkbstudy_multistep_event', Object) (Line: 216)
Drupal\Core\Form\FormBuilder->getForm(Object, 'review') (Line: 94)
Drupal\commerce_checkout\Controller\CheckoutController->formPage(Object)
call_user_func_array(Array, Array) (Line: 123)
Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber->Drupal\Core\EventSubscriber\{closure}() (Line: 582)
Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 124)
Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber->wrapControllerExecutionInRenderContext(Array, Array) (Line: 97)
Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber->Drupal\Core\EventSubscriber\{closure}() (Line: 151)
Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 68)
Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 57)
Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 47)
Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 99)
Drupal\page_cache\StackMiddleware\PageCache->pass(Object, 1, 1) (Line: 78)
Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 47)
Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 52)
Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23)
Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 666)
Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
Berdir’s picture

That is what happens because it manually calls the __sleep(), so this is expected.

The difference is that in serialize(), this recursion happens internally and you can't debug it. Now you have a nice and clear backtrace, all you need to do is set a breakpoint now in Connection::__sleep() and you can look at the object hierarchy that is passed through.

You could also do the same with some print statements in e.g. "RecursiveResolver->resolve" but that will be harder to follow.

MegaChriz’s picture

@Berdir
Thanks for your quick reply. I was expecting that the library would stop on an exception and then print where things went wrong.

In my case \Drupal\Core\Entity\EntityTypeManager apparently is the culprit. When inspecting each \EdeMeijer\SerializeDebugger\Node in the trace, I got a chain of the following objects:

Drupal\Core\Database\Driver\mysql\Connection
Drupal\locale\StringDatabaseStorage
Drupal\locale\LocaleTranslation
array(0)
Drupal\Core\StringTranslation\TranslationManager
Drupal\Core\Entity\EntityTypeManager
Drupal\ukkbaccount\Account\Profile
Drupal\ukkb_commerce\Plugin\Commerce\CheckoutPane\ContactInformation
Drupal\commerce_promotion\Plugin\Commerce\CheckoutPane\CouponRedemption
Berdir’s picture

EntityTypeManager isn't the culprit, that is a service, that already should not be serialized.

> Drupal\ukkbaccount\Account\Profile

What is that object? Is it a service or a custom object you create that has values? Possibly tht one should use the DependencySerializationTrait to drop the reference to EntityTypeManager on serialize.

MegaChriz’s picture

Ah, thus services should generally not be serialized at all.

\Drupal\ukkbaccount\Account\Profile is a custom object with several account related methods. Its constructor asks for the entity type manager and an account. Adding a DependencySerializationTrait there solves the issue in my case! An ajaxified form element now gets properly updated.

Would it be a good idea for a better developer experience to throw exceptions in __sleep() in all service classes as well? This way more developers will be informed that you should avoid serializing these.

phenaproxima’s picture

Status: Needs review » Needs work
Issue tags: +Needs tests

This will need tests...

tim.plunkett’s picture

phenaproxima’s picture

Status: Needs work » Needs review
Issue tags: -Needs tests
FileSize
2.44 KB
3.21 KB

Here, have a patch with a test!

tim.plunkett’s picture

  1. +++ b/core/lib/Drupal/Core/Plugin/Context/Context.php
    index aac6efc0e3..d32d6d5653 100644
    --- a/core/tests/Drupal/Tests/Core/Plugin/Context/ContextTypedDataTest.php
    
    --- a/core/tests/Drupal/Tests/Core/Plugin/Context/ContextTypedDataTest.php
    +++ b/core/tests/Drupal/Tests/Core/Plugin/Context/ContextTypedDataTest.php
    
    +++ b/core/tests/Drupal/Tests/Core/Plugin/Context/ContextTypedDataTest.php
    @@ -16,7 +15,7 @@
    +class ContextTypedDataTest extends KernelTestBase {
    

    If this moves from a unit to kernel test, it should move to core/tests/Drupal/KernelTests/Core/Plugin as well (or just make a new class)

  2. +++ b/core/tests/Drupal/Tests/Core/Plugin/Context/ContextTypedDataTest.php
    @@ -25,6 +24,22 @@ class ContextTypedDataTest extends UnitTestCase {
    +    $this->typedData = new StringData($data_definition);
    

    Weird using an object property for this instead of a local variable.

  3. +++ b/core/tests/Drupal/Tests/Core/Plugin/Context/ContextTypedDataTest.php
    @@ -25,6 +24,22 @@ class ContextTypedDataTest extends UnitTestCase {
    +    // getContextValue() will cause the context to reference the typed data
    +    // manager service. Without DependencySerializationTrait, this will cause
    +    // the context to fail on serialization.
    

    This comment is a little odd because it references how code used to be instead of how things are.

  4. +++ b/core/tests/Drupal/Tests/Core/Plugin/Context/ContextTypedDataTest.php
    @@ -25,6 +24,22 @@ class ContextTypedDataTest extends UnitTestCase {
    +    $context->getContextValue();
    +    serialize($context);
    

    I think we need something asserted here.

phenaproxima’s picture

For the record, we could solve all future occurrences of this problem if we would never make service getters into implicit setters.

function getTypedDataManager() {
  if (!$this->typedDataManager) {
    $this->typedDataManager = \Drupal::typedDataManager();
  }
  return $this->typedDataManager;
}

This is an anti-pattern: it's not a getter, it's a setter. And we wonder why we run into sudden serialization failures. We should reject this anti-pattern forever (in fact, we should deprecate it in Drupal 8, and remove it in Drupal 9). Of course, it'd still be possible to cause a serialization failure by explicitly setting the service (setTypedDataManager()) and serializing the consuming object, but at least then it would be traceable to an explicit method call somewhere, and could be addressed by module authors rather than the onus being on core.

In any case, the method says get, but it actually sets. We need to not do that. Not in this issue, necessarily, but we should not allow any more instances of this anti-pattern to land in core. Looks like I found the hill I'm willing to die on!

phenaproxima’s picture

All feedback from #11 addressed here. I moved the test to the KernelTests namespace and rewrote the mock object to use Prophecy.

The last submitted patch, 10: 2986735-9-FAIL.patch, failed testing. View results

tim.plunkett’s picture

Status: Needs review » Reviewed & tested by the community

Looks great, thanks!

tim.plunkett’s picture

Version: 8.6.x-dev » 8.7.x-dev

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 13: 2986735-13.patch, failed testing. View results

phenaproxima’s picture

Status: Needs work » Reviewed & tested by the community
FileSize
5.59 KB
505 bytes

Whoops! I had the test in the wrong namespace. Kicking directly back to RTBC on the assumption that it will go green this time.

larowlan’s picture

Adding issue credits

  • larowlan committed b1ad4a8 on 8.7.x
    Issue #2986735 by phenaproxima, Berdir, tim.plunkett, MegaChriz:  Drupal...

  • larowlan committed c0c662c on 8.6.x
    Issue #2986735 by phenaproxima, Berdir, tim.plunkett, MegaChriz:  Drupal...
larowlan’s picture

Version: 8.7.x-dev » 8.6.x-dev
Status: Reviewed & tested by the community » Fixed

Committed b1ad4a8 and pushed to 8.7.x. Thanks!

C/p as c0c662c427 and pushed to 8.6.x

Thanks

phenaproxima’s picture

Thanks, @larowlan!

For anyone who's interested, I opened an issue pleading with the framework managers to deprecate the "implicit setter" anti-pattern which caused this bug: #3011620: Deprecate the "implicit setter" anti-pattern

tacituseu’s picture

This introduced test failure on PHP 5.5:
https://www.drupal.org/pift-ci-job/1113676
https://www.drupal.org/pift-ci-job/1113681

1) Drupal\KernelTests\Core\Plugin\ContextTypedDataTest::testGetContextValue
PHPUnit_Framework_Exception: PHP Fatal error:  Call to undefined method Prophecy\Prophecy\MethodProphecy::shouldBeCalledOnce() in /var/www/html/core/tests/Drupal/KernelTests/Core/Plugin/ContextTypedDataTest.php on line 53
Fatal error: Call to undefined method Prophecy\Prophecy\MethodProphecy::shouldBeCalledOnce() in /var/www/html/core/tests/Drupal/KernelTests/Core/Plugin/ContextTypedDataTest.php on line 53
tim.plunkett’s picture

Status: Fixed » Reviewed & tested by the community
FileSize
1012 bytes

That's shouldBeCalledTimes(1) in older PHPSpec.

But it's unnecessary here anyway.

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

Committed and pushed bb9fb66dd7 to 8.7.x and d935fc4871 to 8.6.x. Thanks!

Ran tests locally on PHP5.5,5.6 and 7 and they pass.

  • alexpott committed bb9fb66 on 8.7.x
    Issue #2986735 by tim.plunkett: PHP 5 test fix
    

  • alexpott committed d935fc4 on 8.6.x
    Issue #2986735 by tim.plunkett: PHP 5 test fix
    
    (cherry picked from...

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.