Problem/Motivation
#3573856: SiteConfigureForm properties need to be reset after module install in submitForm() addressed a bug in the installer for Standard (and Umami), but a similar bug persists in the site installer UI when installing Umami.

This is occurring because when the site configure form is submitted, the update module is installed, which leads to Drupal building a new container. However, the another submit handler (DemoUmamiHooks::installConfigureSubmit() is added in DemoUmamiHooks::formInstallConfigureFormAlter(). (DemoUmamiHooks::installConfigureSubmit() calls setUserPasswords, which uses an entity type manager object from the old container to load user entities.
Steps to reproduce
Install Umami via the UI at /core/install.php, and observe the error in the screenshot in Problem/Motivation after submitting the site configuration form with "Check for updates automatically" checked.
The message in the screenshot is
The website encountered an unexpected error. Try again later.
Symfony\Component\DependencyInjection\Exception\RuntimeException: You have requested a synthetic service ("kernel"). The DIC does not know how to construct this service. in Symfony\Component\DependencyInjection\ContainerBuilder->createService() (line 1095 of vendor/symfony/dependency-injection/ContainerBuilder.php).
(The error message is the summary of a <details> element. If you expand it, you are treated to a stack trace.)
Proposed resolution
Change DemoUmamiHooks::installConfigureSubmit() and DemoUmamiHooks::setUserPasswords to static methods and use `\Drupal::entityTypeManager()` instead of a class property.
Remaining tasks
Add test.
User interface changes
Introduced terminology
API changes
Data model changes
Release notes snippet
Issue fork drupal-3581958
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
godotislateComment #4
godotislatehttps://git.drupalcode.org/project/drupal/-/merge_requests/15242/ is up, but needs tests.
Comment #5
benjifisherFWIW, that comes from #3045966: Set author and editor passwords to the admin password in the demo to make demoing easier.
@godotislate:
It looks like the second commit adds a test and reverts the first commit. Is that on purpose? Is the "Test-only changes" CI job broken?
Comment #6
nicxvan commentedThank you for finding that!
Comment #7
godotislate@benjifisher: It was a bad commit. I was essentially running test-only locally by reverting the fix, and then accidentally committed.
Test only failure: https://git.drupalcode.org/project/drupal/-/jobs/9127609
I do have an somewhat unrelated but general concern about the use of service object callables in form submit handlers, etc. I'm not completely sure about this, but it might be possible on multistep forms or AJAX forms (or any form that gets cached), that when the form is retrieved from cache and unserialized, the submit handler object is stale. And DependencySerializationTrait doesn't help us, because it's not a class property.
Comment #8
godotislateThis isn't critical. It was just cloned that way from #3573856: SiteConfigureForm properties need to be reset after module install in submitForm().
Comment #9
benjifisherI reproduced the error using the
mainbranch. I am adding the error message as text to the issue summary: that makes it easier for screen readers and search engines.If I ignore the error, then visiting various admin pages (
/en/admin/modulesfor one) leads to a WSOD with the error messageand a stack trace.
Comment #10
benjifisherI repeated the test using the feature branch. I did not see the error in the installer. Instead, I was logged in to the site, redirected to the home page.
I visited a few admin pages, cleared caches, and checked recent log messages. There are some known warnings generated when installing Umami, a couple of warnings about trying to start cron when it is already running, and nothing else of note.
Comment #11
benjifisherTest coverage
I reviewed the changes to the tests. It makes sense, and the test-only CI job fails with the same error message that I get in manual testing.
I have a tiny reservation that we no longer have the original version of the test: we are modifying the test instead of adding a new one. But it is already a Functional test marked
#[Group('#slow')]. Also, I assume the other installer tests do not enable theupdatemodule, so we still have test coverage of that code path.TL;DR: LGTM
Code changes
The fix makes sense:
staticmethods for callbacks (like form submit functions).\Drupal::service(...)instead of an injected service.To illustrate, here is an important hunk from the change introduced for this issue:
This function is made
static, so it has to use\Drupal::entityTypeManager()instead of the injected$this->entityTypeManager.The breaking change
I used
git bisectto find the issue that caused this problem: #3516264: CKEditor 5 loads all plugin translations on AJAX operations. (I am now very efficient at installing Umami through the admin UI.) It includes these changes:In other words, the exact opposite of the fix for this issue: replacing a call to
\Drupal::moduleHandler()with an injected service.That is as close as I get to explaining how that issue caused this one. Is it some interaction between the module handler and the entity type manager from the old service container? (shrug)
I cannot help pointing out that there is a Remaining task on that issue:
Maybe now we are ready to check that off. ;)
Needs work: more documentation
@godotislate, I have to admit that I do not understand the interaction of dependency injection (DI), serialization, and container rebuilds as well as you do. (I could not have figured out a fix for this issue in five times as long as you took. Maybe ten times.) For years, I have been following the rule that we should use DI and avoid invoking the
\Drupalclass in OO code. Now, this issue shows me that there are exceptions, but I still do not know what the exceptions are.We need some guidelines, and we need some documentation.
Side note: just last week, I was doing a code review at work. Oh, no, someone was injecting the whole container instead of individual services. I know that’s the wrong thing to do! But I could not find a documentation page that said so. I looked hard for such a page.
I am setting this issue to NW: not for code changes but for a more complete explanation, something that we can add to the developer documentation.
Comment #12
godotislateTo clarify, the issue here is not about serialization. I have a concern about serialization that is not directly related here, but I'll get into that in a bit.
This issue is closely related to #3573856: SiteConfigureForm properties need to be reset after module install in submitForm(), and the problem is this: If the "Check for updates automatically" on the final step of the site installation form is checked, then on submit,
Drupal\Core\Installer\Form\SiteConfigureForm::submitForm()will install the Update module. When the module is installed, the container is rebuilt and is set to a new object. After the container is rebuilt, all the form's service object properties are stale, because they were instantiated from the old container object. Any usage of those service properties in subsequent submit handler processing can run into errors because of outdated references. The solution in #3573856: SiteConfigureForm properties need to be reset after module install in submitForm() was to repopulate those service properties from the new container right after the update module is installed.The issue here is that DemoUmamiHooks alters the SiteConfigureForm build array to add another submit handler. The submit handler is the callable
[$this, 'installConfigureSubmit'], and$thisis theDemoUmamiHooksobject, which is a service instantiated from the original container. In addition, the entityTypeManager property onDemoUmamiHooksis also instantiated from the original container. So wheninstallConfigureSubmit()is invoked (after the container rebuild), and subsequently callssetUserPasswords(), using the outdated entityTypeManager object there causes the error. Since there's no real way to repopulate the entityTypeManger from the new container object other than using the\Drupalglobal object, we might as well just make the methods static. I don't know that there's much learning to take from this, because having to deal with container rebuilds in runtime is somewhat unusual.My concern with serialization is this: Assume that some Hook class similarly alters a different form and adds a submit handler in the same way. If the form is serialized because it is cached, then the Hook object that is unserialized is not instantiated from the current container. Though if I think about it, this might not be a problem if the Hooks class itself uses
DependendencySerializationTrait. While the Hook object would not be instantiated from the current container, all the Hook class's service properties would be, viaDependendencySerializationTrait::__wakeup(). So it's probably fine.Comment #13
benjifisher@godotislate:
Thanks. I think I understand what is going on.
To summarize: if a class requests an object from the service container, the class should be careful not to use that object after a container rebuild.
It feels like we are playing Whack-a-mole and fixing bugs when that rule is broken. For this issue, when
DemoUmamiHooksaltersSiteConfigureForm::submitForm(), it has to realize that its objects might be used after a rebuild, so it has to pass astaticmethod. I am not sure that this counts as "tightly coupled" code, but it is coupled.I guess the saving grace is that "having to deal with container rebuilds in runtime is somewhat unusual."
I wish there were a way to handle this at the container level during a rebuild. I expect that the old container "knows" which of its services have been instantiated. If we could update those objects and hand them off to the new container, then we would never have stale service objects (and we could remove all the code for services from
DependencySerializationTrait). Something like this, for each instantiated service:Unfortunately, after the first line,
$foo = $new_container->get('foo')does not do what we want and$foo = clone new_container->get('foo')does the opposite of what we want.For this issue, can you add a code comment before the line
explaining why we are not using DI? I know: if anyone tries to "fix" that, then the new test will fail. But let’s use "defense in depth" and save some future developer a cycle of code-test-recode.
I am pretty sure that the switch to DI was not required to fix #3516264: CKEditor 5 loads all plugin translations on AJAX operations. Someone was just cleaning up a call to
\Drupal::moduleHandler()and using DI.Comment #14
godotislateAdded the comment requested in #13. Also added `@internal` to the submit handler functions, since they shouldn't be considered API.
Comment #15
benjifisherI already reviewed (Comment #11) and tested (Comments #9 and #10). Since then, @godotislate added my requested change (a code comment) and marked two methods
@internal. I do not think I need to re-test.Thanks for the updates. I am happy with this version.
Comment #16
dries commentedThis looks great. One small inaccuracy: the comment in
setUserPasswords()referencesSiteConfigureForm::submit()but the actual method isSiteConfigureForm::submitForm().Comment #17
nicxvan commentedFound another comment similar to 16, once it's green I think it's ready!
Comment #18
nicxvan commentedRTBC plus 1
Both comments have been addressed and test only fails as expected!
Comment #21
catchCommitted/pushed to main and 11.x, thanks!