Problem

Because DependencySerializationTrait uses get_object_vars() to collect serializable service properties, but that function cannot access private properties. Because of that, the code below fails, because $entityTypeManager becomes null when deserialization happens.

<?php

final class MyForm extends \Drupal\Core\Form\FormBase {

  private $entityTypeManager;

  public function __construct(\Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager) {
    $this->entityTypeManager = $entityTypeManager;
  }

  public static function create(\Symfony\Component\DependencyInjection\ContainerInterface $container) {
    return new static(
      $container->get('entity_type.manager')
    );
  }
}

This seems to be a known limitation for 2 years or so: https://www.drupal.org/project/drupal/issues/2727011#comment-12619886

Proposed solution

Use Reflection or Clousers instead of get_object_vars().

Related

PS.: We are using this fix since patch 1 was submitted here in production without noticeable unexpected sideeffects.

CommentFileSizeAuthor
#68 drupal-core-support-serialization-of-private-properties-3110266-68.patch4.07 KBgergo.bodi
#68 drupal-core-support-serialization-of-private-properties-3110266-68.patch4.07 KBgergo.bodi
#51 3110266-nr-bot.txt2.06 KBneeds-review-queue-bot
#49 drupal-core-support-serialization-of-private-properties-3110266-MR228-4f13d74d-without-phpstan-baseline.patch4.46 KBmxr576
#48 DependencySerializationBench.php_.txt1.95 KBmxr576
#47 DependencySerializationTest.php_.txt2.86 KBmxr576
#43 drupal-core-support-serialization-of-private-properties-3110266-MR1328-9.5.x-port.patch5 KBtamas.hangya
#35 DependencySerializationBench.php_.txt1.97 KBmxr576
#34 phpbench.json_.txt268 bytesmxr576
#34 DependencySerializationBench.php_.txt1.87 KBmxr576
#19 drupal-support-serialization-of-private-properties-3110266-19-test-only.patch2.65 KBmxr576
#17 drupal-support-serialization-of-private-properties-3110266-17-test-only.patch2.65 KBmxr576
#15 drupal-support-serialization-of-private-properties-3110266-15.patch4.12 KBanushrikumari
#6 drupal-support-serialization-of-private-properties-3110266-6.patch5.24 KBmxr576
#6 drupal-support-serialization-of-private-properties-3110266-6-test-only.patch2.65 KBmxr576
#5 drupal-support-serialization-of-private-properties-3110266-5.patch2.25 KBmxr576
#4 drupal-support-serialization-of-private-properties-3110266-4.patch2.07 KBmxr576
#3 drupal-support-serialization-of-private-properties-3110266-3.patch2.08 KBmxr576
#2 drupal_support-serialization-of-private-properties-3110266-2.patch2.14 KBmxr576

Issue fork drupal-3110266

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

mxr576 created an issue. See original summary.

mxr576’s picture

First raw patch by using Reflection, let's see how it performs and if anything fails with it.

mxr576’s picture

Status: Active » Needs work
StatusFileSize
new2.08 KB

Let's combine Closures with Reflections.

mxr576’s picture

StatusFileSize
new2.07 KB
mxr576’s picture

StatusFileSize
new2.25 KB
mxr576’s picture

Did some digging, so #4 was an actual correct fix for the problem and it discovered a missing property issue in ModulesListForm. That caused the 8 failing tests.

1580662686|Drupal\system\Form\ModulesListForm|172.25.0.6|http://webserver/batch?id=5&op=start|http://webserver/admin/modules|1||accessManager property does not exist

mxr576’s picture

Issue tags: +solid

The last submitted patch, 6: drupal-support-serialization-of-private-properties-3110266-6-test-only.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

mxr576’s picture

Monitor and measure performance changes:

If I am reading logs correctly, the code became even faster than it was before
https://dispatcher.drupalci.org/job/drupal8_core_regression_tests/12484/ 1h 5 mins
https://dispatcher.drupalci.org/job/drupal_patches/30560/ 1h 2 mins

andypost’s picture

Issue tags: +needs profiling

It adds overhead, is it viable?

mxr576’s picture

Open for suggestions about how to fix this issue in a different way and how to profile it in a better way. Based on the CI, this fix even improves the speed a little bit. :O

Version: 8.9.x-dev » 9.1.x-dev

Drupal 8.9.0-beta1 was released on March 20, 2020. 8.9.x is the final, long-term support (LTS) minor release of Drupal 8, which means new developments and disruptive changes should now be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 9.1.x-dev » 9.2.x-dev

Drupal 9.1.0-alpha1 will be released the week of October 19, 2020, which means new developments and disruptive changes should now be targeted for the 9.2.x-dev branch. For more information see the Drupal 9 minor version schedule and the Allowed changes during the Drupal 9 release cycle.

mxr576’s picture

Issue tags: +Needs reroll
anushrikumari’s picture

Rerolled patch #6 for 9.2.x

Version: 9.2.x-dev » 9.3.x-dev

Drupal 9.2.0-alpha1 will be released the week of May 3, 2021, which means new developments and disruptive changes should now be targeted for the 9.3.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

mxr576’s picture

mxr576’s picture

yogeshmpawar made their first commit to this issue’s fork.

mxr576’s picture

Issue tags: -Needs reroll

Other than the result in #6 and also comparing the latest test execution times with and without the patch, what else we can do to profile this code. I would appreciate some help with this.

Status: Needs review » Needs work
mxr576’s picture

Status: Needs work » Needs review
mxr576’s picture

(The state of #6 is being used in production since the patch was attached.)

mxr576’s picture

Just by judging based on the test execution time, there is no performance degradation, both took 57 minutes

https://dispatcher.drupalci.org/job/drupal_patches/101597/
https://dispatcher.drupalci.org/job/drupal8_core_regression_tests/44718/

Version: 9.3.x-dev » 9.4.x-dev

Drupal 9.3.0-rc1 was released on November 26, 2021, which means new developments and disruptive changes should now be targeted for the 9.4.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

mxr576’s picture

Having this fixed possibly would be better for everyone than warning for a limitations of the current solution.

https://github.com/mglaman/phpstan-drupal/issues/254

mxr576’s picture

Assigned: Unassigned » mxr576
Status: Needs review » Needs work
mxr576’s picture

Assigned: mxr576 » Unassigned
Status: Needs work » Needs review
mxr576’s picture

Issue summary: View changes
mxr576’s picture

Issue tags: -needs profiling
StatusFileSize
new1.87 KB
new268 bytes
./vendor/bin/phpbench run core/lib/Drupal/Core/DependencyInjection/DependencySerializationBench.php --report=aggregate --ref=original
PHPBench (1.2.3) running benchmarks...
with PHP version 7.4.26, xdebug ❌, opcache ❌
comparing [actual vs. original]

\Drupal\Core\DependencyInjection\DependencySerializationBench

    benchSerialization......................R1 I4 - [Mo5.891μs vs. Mo5.116μs] +15.15% (±1.26%)

Subjects: 1, Assertions: 0, Failures: 0, Errors: 0
+------------------------------+--------------------+-----+------+-----+----------------+-----------------+----------------+
| benchmark                    | subject            | set | revs | its | mem_peak       | mode            | rstdev         |
+------------------------------+--------------------+-----+------+-----+----------------+-----------------+----------------+
| DependencySerializationBench | benchSerialization |     | 1000 | 10  | 3.106mb +0.10% | 5.891μs +15.15% | ±1.26% -59.62% |
+------------------------------+--------------------+-----+------+-----+----------------+-----------------+----------------+

mxr576’s picture

StatusFileSize
new1.97 KB

A more realistic result by excluding the setup part from the benchmark.

    benchSerialization......................I9 - [Mo4.449μs vs. Mo4.497μs] -1.08% (±0.94%)

Subjects: 1, Assertions: 0, Failures: 0, Errors: 0
+------------------------------+--------------------+-----+------+-----+---------------+----------------+----------------+
| benchmark                    | subject            | set | revs | its | mem_peak      | mode           | rstdev         |
+------------------------------+--------------------+-----+------+-----+---------------+----------------+----------------+
| DependencySerializationBench | benchSerialization |     | 1000 | 10  | 1.803mb 0.00% | 4.449μs -1.08% | ±0.94% -11.35% |
+------------------------------+--------------------+-----+------+-----+---------------+----------------+----------------+

mxr576’s picture

Indicates that reflection should perform better than closures, so switched to that.

https://github.com/mark-gerarts/php-private-access-bench

Diff between the previous solution and the current one:

comparing [actual vs. original]

\Drupal\Core\DependencyInjection\DependencySerializationBench

    benchSerialization......................R1 I3 - [Mo8.669μs vs. Mo7.330μs] +18.26% (±1.71%)

Subjects: 1, Assertions: 0, Failures: 0, Errors: 0
+------------------------------+--------------------+-----+------+-----+---------------+-----------------+---------------+
| benchmark                    | subject            | set | revs | its | mem_peak      | mode            | rstdev        |
+------------------------------+--------------------+-----+------+-----+---------------+-----------------+---------------+
| DependencySerializationBench | benchSerialization |     | 1000 | 100 | 1.803mb 0.00% | 8.669μs +18.26% | ±1.71% -3.18% |
+------------------------------+--------------------+-----+------+-----+---------------+-----------------+---------------+
mxr576’s picture

Status: Needs review » Needs work

> d2158784 - Use reflection instead

Meh, this change break this MR, needs rework.

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.0-alpha1 was released on May 6, 2022, which means new developments and disruptive changes should now be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.5.x-dev » 10.1.x-dev

Drupal 9.5.0-beta2 and Drupal 10.0.0-beta2 were released on September 29, 2022, which means new developments and disruptive changes should now be targeted for the 10.1.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 10.1.x-dev » 11.x-dev

Drupal core is moving towards using a “main” branch. As an interim step, a new 11.x branch has been opened, as Drupal.org infrastructure cannot currently fully support a branch named main. New developments and disruptive changes should now be targeted for the 11.x branch, which currently accepts only minor-version allowed changes. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

mxr576’s picture

mxr576’s picture

Status: Needs work » Needs review

MR#2228 resurrected, re-based and currently contains the original Closure based serialization approach.

I have also figured out why the Reflection based serialization approach broke a test. (Spoiler: never unset object properties in runtime...)

However, since Drupal core already uses the Closure based approach in ReverseContainer (since this commit), I am uncertain if I should switch back to the Reflection based serialization approach here or not. Feedback are welcomed!

mxr576’s picture

StatusFileSize
new2.86 KB

okay, so I could not resist, I ran a new PHPBench test suite and it confirmed that Closure should be the winner.

$ ./vendor/bin/phpbench run core/lib/Drupal/Core/DependencyInjection/DependencySerializationBench.php --tag=closure --retry-thresho
ld=5 --report=aggregate --ref=reflection
PHPBench (1.2.14) running benchmarks... #standwithukraine
with configuration file: /mnt/files/local_mount/build/phpbench.json
with PHP version 8.1.12, xdebug ✔, opcache ❌
comparing [closure vs. reflection]

\Drupal\Core\DependencyInjection\DependencySerializationBench

    benchSerialization......................R2 I60 - [Mo15.018μs vs. Mo17.459μs] -13.98% (±1.43%)

Subjects: 1, Assertions: 0, Failures: 0, Errors: 0
Storing results ... OK
Run: 134b37696cb12d58b88f19f2df141c9855120ac2
+------------------------------+--------------------+-----+------+-----+----------------+------------------+----------------+
| benchmark                    | subject            | set | revs | its | mem_peak       | mode             | rstdev         |
+------------------------------+--------------------+-----+------+-----+----------------+------------------+----------------+
| DependencySerializationBench | benchSerialization |     | 1000 | 100 | 2.952mb -0.02% | 15.018μs -13.98% | ±1.43% -10.82% |
+------------------------------+--------------------+-----+------+-----+----------------+------------------+----------------+

mxr576’s picture

StatusFileSize
new1.95 KB
mxr576’s picture

Also leaving a patch file here for those who would like to use the latest state of MR on Drupal core 10.1.x, because the latest MR diff fails to apply due to phpstan-baseline.neon differences.

mxr576’s picture

For data nerds like me, get_object_vars() vs Closure - of course, serializing private properties comes with a relatively small price

PHPBench (1.2.14) running benchmarks... #standwithukraine
with configuration file: /mnt/files/local_mount/build/phpbench.json
with PHP version 8.1.12, xdebug ✔, opcache ❌
comparing [get_object_vars vs. closure]

\Drupal\Core\DependencyInjection\DependencySerializationBench

    benchSerialization......................R1 I79 - [Mo13.995μs vs. Mo14.962μs] -6.47% (±1.55%)

Subjects: 1, Assertions: 0, Failures: 0, Errors: 0
Storing results ... OK
Run: 134b3775801b90c3dfeb141a25a792c5f1705cb2
+------------------------------+--------------------+-----+------+-----+----------------+-----------------+----------------+
| benchmark                    | subject            | set | revs | its | mem_peak       | mode            | rstdev         |
+------------------------------+--------------------+-----+------+-----+----------------+-----------------+----------------+
| DependencySerializationBench | benchSerialization |     | 1000 | 100 | 2.951mb -0.02% | 13.995μs -6.47% | ±1.55% +53.33% |
+------------------------------+--------------------+-----+------+-----+----------------+-----------------+----------------+

needs-review-queue-bot’s picture

Status: Needs review » Needs work
StatusFileSize
new2.06 KB

The Needs Review Queue Bot tested this issue. It fails the Drupal core commit checks. Therefore, this issue status is now "Needs work".

This does not mean that the patch needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.

Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.

mxr576’s picture

Status: Needs work » Needs review
attila.fekete’s picture

Status: Needs review » Reviewed & tested by the community

This looks good.

mxr576’s picture

larowlan’s picture

Status: Reviewed & tested by the community » Needs review

I could be missing something here, or perhaps PHP has moved on.

But in my testing get_object_vars supports private properties.

https://3v4l.org/ApUpq

Or is this about objects extending other objects with private properties?

https://3v4l.org/d6WN9

But if so that feels counter intuitive because access to private properties on extensions isn't possible anyway?

mxr576’s picture

Or is this about objects extending other objects with private properties?

Yes

But if so that feels counter intuitive because access to private properties on extensions isn't possible anyway?

extensions? sub classes? it is true but when an object hierarchy (inheritance tree) is dehydrated then hydrated these properties also needs to be dumped and restored otherwise we end up with malformed objects.

Using private properties is a rather uncommon phenomenon in Drupal core, maybe the situation is better nowadays (have not checked) but when I opened this issue it was. Parent classes do not have expose their internal state to child classes, otherwise they would be harder to refactor or more open for modification rather than extension (O from SOLID).

mxr576’s picture

Also quoting @gabesullice from #3019332-57: Use final to define classes that are NOT extension points

As a community, we shouldn't be afraid of the final and private keywords. They exist in every major OOP language for a reason and we are not above (or below) those reasons. We should use them with good, engineering discretion.

When the usage of private properties would become more common in Drupal core and contrib, this problem would be much of a pain than as of today.

larowlan’s picture

Status: Needs review » Reviewed & tested by the community

That seems reasonable to me

alexpott made their first commit to this issue’s fork.

ghost of drupal past’s picture

When the usage of private properties would become more common in Drupal core and contrib

Let's hope that day never comes.

It would be wonderful to have policy (well, without the TWG I doubt that's possible but maybe?) and/or documentation to only ever use private on final classes -- and even then think twice whether something really is final. If this last twenty years taught us anything it's there is always a use case no one thought of.

alexpott’s picture

Status: Reviewed & tested by the community » Needs work

@Ghost of Drupal Past - an alternate view on the last twenty years is that we're amazing at finding all possible bugs due to side effects.

So this fix is a trade-off - see https://3v4l.org/MNbOi - it sacrifices being able to restore privates from the parent that does the use DependencySerializationTrait; to give us the ability to set it on the child. At the very least we need to document this on the trait and problem inline with the use of the closure.

I think this fix might introduce some quite tricky bugs - consider the case where you have two classes extending each other both with privates with the same name. I think this is what @Ghost of Drupal Past might be getting at with the limitation to only use privates on final classes. OTOH these bugs exist already in different forms - I do wonder about the impact of the behaviour change.

@Ghost of Drupal Past FWIW there is a replacement for the coding standards TWG that potentially will reply and do the work quicker - see https://www.drupal.org/project/coding_standards

mxr576’s picture

@alexpott thanks for your review, yes, that potential edge case exists, but what is your recommendation? You moved the ticket back to "needs work", but maybe the concern you raised only means that this change can be only introduced in a new major version - or with a feature flag in a minor version? - because it changes how private props are serialized?

IMO Closure works as expected for private properties in child classes, get_object_vars() was a buggy solution in that regard as well.

mxr576’s picture

So this fix is a trade-off - see https://3v4l.org/MNbOi - it sacrifices being able to restore privates from the parent that does the use DependencySerializationTrait; to give us the ability to set it on the child.

I tried to wrap my head around it and the answer could be simple... but when this change could become a problem? Private properties are scoped to the class that introduced them: https://3v4l.org/2JvMh

At the very least we need to document this on the trait

I agree that this behavior can be documented on the trait and it can be also documented in a change record.

mxr576’s picture

Issue summary: View changes
alexpott’s picture

In my mind this is a closed won't fix because the trade-offs of the closure are worse than get_object_vars()... it's more often what you want. And if you use a private on a class that extends a class from somewhere else and that class uses the trait... then you can just use the trait. And it'll work. If the class you extend from has a private and uses the trait then you need to be more careful and I'd argue that you can no longer use privates if you want dependency serialization. See https://3v4l.org/3eoXc for more.

mxr576’s picture

Class B
get_object_vars()
Array
(
    [a] => 1
)


Closure
Array
(
)

This is the problematic part, right? You are assuming that after dehydration, the value of the "a" property is going to be missing, am I right? But is it?

Admittedly, I could be blindsided because I really would like to get this issue fixed... Can you pinpoint what is the unexpected outcome in my updated code example: https://3v4l.org/Yfrhp?

I even checked whether the value of the private "a" property properly resolved after serialization/deserialization in different scopes (see scopeCechoA() and scopeCechoD()).

If the class you extend from has a private and uses the trait, then you need to be more careful, and I'd argue that you can no longer use privates if you want dependency serialization.

Well, that would mean you cannot write code that is open for extension and closed for modification in some conditions, can you? And it would also mean that optional configuration introduced in PHPStan Drupal is the only way to save people from some hard-to-debug, DrupalFTW moments.

How do other frameworks handle this problem? Object hydration is quite a basic task nowadays.

alexpott’s picture

With HEAD / get_object_vars

With the way it is currently if you add a private property to a class that uses the serialization trait … you can add the serialization trait too and you’ll get your property serialized… so you have two work arounds… you can either change your property to protected or you can use the trait…

If the thing you are extending has a private and is using the trait you only have 1 work around - you have to change your property to protected - you cannot have a private.

With this MR / closure

If you add a private property and extend then your private property is serialized…. yay… but if the thing you extend has a private property it will not be serialized… and there is no workaround.

Conclusion

Therefore HEAD is a better place to be. Because there are workarounds. Yes you are restricted from what you can do if the base class has privates… but that is more true with the closure.

Unless we can find something that improves the situation by serializing the privates in each class in the inheritance chain I think we’re better off with HEAD.

gergo.bodi’s picture

I tried to update to Drupal 10.3.x and I got error with the latest patch so I fixed it.

gergo.bodi’s picture

gergo.bodi’s picture

nikolay shapovalov’s picture

Issue summary: View changes

Fix IS link

Version: 11.x-dev » main

Drupal core is now using the main branch as the primary development branch. New developments and disruptive changes should now be targeted to the main branch.

Read more in the announcement.