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
- https://www.lambda-out-loud.com/posts/accessing-private-properties-php/
- https://github.com/mglaman/phpstan-drupal/issues/730 user level mitigation was introduced to avoid potential side effects (foot guns) of the lack of serialization support for private properties
PS.: We are using this fix since patch 1 was submitted here in production without noticeable unexpected sideeffects.
| Comment | File | Size | Author |
|---|---|---|---|
| #51 | 3110266-nr-bot.txt | 2.06 KB | needs-review-queue-bot |
| #49 | drupal-core-support-serialization-of-private-properties-3110266-MR228-4f13d74d-without-phpstan-baseline.patch | 4.46 KB | mxr576 |
| #48 | DependencySerializationBench.php_.txt | 1.95 KB | mxr576 |
| #34 | phpbench.json_.txt | 268 bytes | mxr576 |
Issue fork drupal-3110266
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
Comment #2
mxr576First raw patch by using Reflection, let's see how it performs and if anything fails with it.
Comment #3
mxr576Let's combine Closures with Reflections.
Comment #4
mxr576Comment #5
mxr576Comment #6
mxr576Did 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 existComment #7
mxr576Comment #9
mxr576If 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
Comment #10
andypostIt adds overhead, is it viable?
Comment #11
mxr576Open 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
Comment #14
mxr576Comment #15
anushrikumari commentedRerolled patch #6 for 9.2.x
Comment #17
mxr576rerolling patches...
Comment #19
mxr576Comment #21
mxr576Other 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.
Comment #23
mxr576Comment #24
mxr576(The state of #6 is being used in production since the patch was attached.)
Comment #25
mxr576Just 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/
Comment #30
mxr576Having 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
Comment #31
mxr576Comment #32
mxr576Comment #33
mxr576Comment #34
mxr576Comment #35
mxr576A more realistic result by excluding the setup part from the benchmark.
Comment #36
mxr576Indicates 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:
Comment #37
mxr576> d2158784 - Use reflection instead
Meh, this change break this MR, needs rework.
Comment #43
tamas.hangya commentedComment #44
mxr576Comment #46
mxr576MR#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!
Comment #47
mxr576okay, so I could not resist, I ran a new PHPBench test suite and it confirmed that Closure should be the winner.
Comment #48
mxr576Comment #49
mxr576Also 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.
Comment #50
mxr576For data nerds like me, get_object_vars() vs Closure - of course, serializing private properties comes with a relatively small price
Comment #51
needs-review-queue-bot commentedThe 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.
Comment #52
mxr576Comment #53
attila.fekete commentedThis looks good.
Comment #54
mxr576FTR, opened #3400379: Do not unset class properties because it causes un-initialization as a follow on #46
Comment #55
larowlanI 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?
Comment #56
mxr576Yes
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).
Comment #57
mxr576Also quoting @gabesullice from #3019332-57: Use final to define classes that are NOT extension points
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.
Comment #58
larowlanThat seems reasonable to me
Comment #60
ghost of drupal pastLet'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.
Comment #61
alexpott@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
Comment #62
mxr576@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.
Comment #63
mxr576I 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
I agree that this behavior can be documented on the trait and it can be also documented in a change record.
Comment #64
mxr576Comment #65
alexpottIn 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.
Comment #66
mxr576This 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()andscopeCechoD()).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.
Comment #67
alexpottWith 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.
Comment #68
gergo.bodi commentedI tried to update to Drupal 10.3.x and I got error with the latest patch so I fixed it.
Comment #69
gergo.bodi commentedComment #70
gergo.bodi commentedComment #71
nikolay shapovalov commentedFix IS link