Problem/Motivation

In #3392616: Update to Symfony 6.4 we decided to convert both BookNavigationCacheContext and MenuActiveTrailsCacheContext into lazy services.

Whilst looking into this, I've stumbled over the way core currently handles this: Using a script to create a Proxy class.

Looking at https://symfony.com/doc/current/service_container/lazy_services.html Symfony supports this natively (since 6.2 without additional packages/traits).

Steps to reproduce

Proposed resolution

Replace our proxy lazy service solution with the native one from Symfony itself.

This would get us more "of the island", reduce custom solutions we have to maintain.

Remaining tasks

User interface changes

API changes

Data model changes

Release notes snippet

CommentFileSizeAuthor
#7 do-not-try-this-at-home.patch944 bytesspokje
#7 LazyStateGhost.jpg358.33 KBspokje

Issue fork drupal-3396928

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

Spokje created an issue. See original summary.

spokje’s picture

Assigned: Unassigned » spokje

This needs some TLC tomorrow, but I'm not unhappy so far...

spokje’s picture

Issue summary: View changes
Parent issue: » #3392616: Update to Symfony 6.4
spokje’s picture

Assigned: spokje » Unassigned
Issue summary: View changes
Status: Active » Needs review
longwave’s picture

This seemed too good to be true, and unfortunately I think it is. As per the docs:

To check if your lazy service works you can check the interface of the received object:

dump(class_implements($service));
// the output should include "Symfony\Component\VarExporter\LazyObjectInterface"

With this MR checked out and after a container rebuild:

> class_implements(\Drupal::service('cron'));
= [
    "Drupal\Core\CronInterface" => "Drupal\Core\CronInterface",
  ]

ie. I don't think the ghost objects are working. I suspect our container and/or dumper need to add support for them, somewhere.

spokje’s picture

StatusFileSize
new944 bytes
new358.33 KB

@longwave: Hmm, at the very least #6 points out that this MR is missing test coverage.

ie. I don't think the ghost objects are working. I suspect our container and/or dumper need to add support for them, somewhere.

I'm a tad more optimistic.

The attached (incredibly nasty hacky do-not-try-this-at-home) test patch on top of this MR shows that the following service-IDs implement Symfony\Component\VarExporter\LazyObjectInterface:

1) Drupal\Tests\Core\LazyService\LazyServiceTest::testBuildComplexMethod
page_cache_response_policy
config.installer
cron
module_installer
content_uninstall_validator
required_module_uninstall_validator
module_required_by_themes_uninstall_validator
database_driver_uninstall_validator
plugin.cache_clearer
paramconverter.menu_link
lock.persistent
router.dumper
router.builder
paramconverter.configentity_admin
bare_html_page_renderer
batch.storage
file.mime_type.guesser
file.mime_type.guesser.extension
Drupal\Core\PageCache\ResponsePolicyInterface
Drupal\Core\Config\ConfigInstallerInterface
Drupal\Core\CronInterface
Drupal\Core\Extension\ModuleInstallerInterface
Drupal\Core\Plugin\CachedDiscoveryClearerInterface
Drupal\Core\Routing\MatcherDumperInterface
Drupal\Core\Routing\RouteBuilderInterface
Drupal\Core\Render\BareHtmlPageRendererInterface
Drupal\Core\Batch\BatchStorageInterface

Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-''
+'certainly not equal'

That list matches on first glance what I expected.
Can the difference between your and my findings be explained by looking at different times in the Container lifecycle maybe?

As a sidenote: I also didn't believe this to work, but putting breakpoints in tests using the lazy services indeed shows the presence of LazyObjectState and [ActualClassName]Ghost[hashID] classes. (See https://www.drupal.org/files/issues/2023-10-27/LazyStateGhost.jpg)
Of course, that's no proof at all.

longwave’s picture

In kernel tests, the container is a \Drupal\Core\DependencyInjection\ContainerBuilder - you can see this by dumping $container::class in that sample test. This is a thin layer over Symfony's ContainerBuilder which does support lazy services.

However at runtime the container is a \Drupal\Core\DependencyInjection\Container which is not based on Symfony at all, it only implements their interface. The ContainerBuilder is dumped and stored in cache by the rebuild process, and reloaded from there as a normal Container on subsequent page loads.

IIRC we were bitten by this before when adding service deprecations: #3215611: Service deprecations are only triggered on container build,not ::get()

#2529516: Decouple tests from relying that $container is a container builder would make this distinction better for kernel tests.

longwave’s picture

Not that this should put us off; the fact the kernel tests are passing here are a good sign that these lazy services are working, just they are no longer lazy at runtime or in functional tests. But if we add support for them to the dumper/container then they should be usable at runtime too!

longwave’s picture

spokje’s picture

Assigned: Unassigned » spokje
Status: Needs review » Needs work

Thanks @longwave, that's the reason I always danced around the container bit, never got a firm grasp of it.

The explanation helped, still got hope this is going to work and shouldn't be that hard.
(It seems the actual container is fed the drupal proxy classes instead of the actual services, let's see if that can be altered quickly)
#FamousLastWords
#OnHisWayToTheDrawingBoard

Final question (for now):

With this MR checked out and after a container rebuild:

> class_implements(\Drupal::service('cron'));
= [
"Drupal\Core\CronInterface" => "Drupal\Core\CronInterface",
]

How, what, and where did you do that class_implements?

Also un-parenting, since this isn't directly SF6.4 related.

longwave’s picture

I use DDEV for all Drupal dev work; drush provides a shell that bootstraps Drupal so you can run PHP code interactively:

$ ddev drush cr
 [success] Cache rebuild complete.

$ ddev drush php
Psy Shell v0.11.9 (PHP 8.1.23 — cli) by Justin Hileman
Drupal 10 (Drupal 11.0-dev)
> class_implements(\Drupal::service('cron'));
= [
    "Drupal\Core\CronInterface" => "Drupal\Core\CronInterface",
  ]
spokje’s picture

*hat is tipped*
Much obliged, kind sir.

joachim’s picture

> In kernel tests, the container is a \Drupal\Core\DependencyInjection\ContainerBuilder - you can see this by dumping $container::class in that sample test. This is a thin layer over Symfony's ContainerBuilder which does support lazy services.

Aside: is that documented somewhere?

longwave’s picture

I don't remember seeing it anywhere, we could add some docs to KernelTestBase I guess but really we should fix #2529516: Decouple tests from relying that $container is a container builder

spokje’s picture

spokje’s picture

Assigned: spokje » Unassigned
Status: Needs work » Closed (won't fix)

Sadly SF uses eval for the proxy classes which at least I can't see working with our serialized container solution

bkosborne’s picture

I wonder if the change record can be deleted? It comes up in Google Search and it's not super obvious when looking at a draft CR that it's a draft

alexpott’s picture

@bkosborne I unpublished the CR.