Using instances of classes defined by modules in service container parameters causes fatal errors

Created on 2 May 2025, 10 days ago

Problem/Motivation

Drupal core is unable to serialise/unserialise objects defined by extensions (modules, etc) in the service container, especially requests loading the warm (cached) container.

I've tested this only with service parameters, there may be other parts of the service container affected by this problem.

I believe service definitions currently get around this because they are double serialised, once by `\Drupal\Component\DependencyInjection\Dumper\OptimizedPhpArrayDumper::getServiceDefinitions`, then again by the whole container cache in \Drupal\Core\DrupalKernel::cacheDrupalContainer. There is then a point where unserialisation happens in \Drupal\Component\DependencyInjection\Container::get.

Rationale for this comes from the Pinto project and ecosystem, where a variety of things happen at the container compilation level with compiler passes.

Steps to reproduce

  1. Create a plain object in a module. Ensure it has the typical `Drupal\my_module` namespace. E.g `\Drupal\my_module\MyObject`
  2. Create a compiler pass (and associated service provider class).
  3. In the compiler pass, create an instance of the above object as a parameter. E.g $container->setParameter('paramname', new MyObject())
  4. Clear caches, then reload the page so the warm container is used.
  5. On a controller page, create an instance of the object:

Result:

The website encountered an unexpected error. Try again later.

Error: Class "\Drupal\my_module\MyObject" not found in Drupal\my_module\Controller\Sandbox->__invoke() (line XX of XXXXX).

call_user_func_array() (Line: 123)

This happens because unserialise happens before the namespaces are added to the the class loader. The class loader has a cache/memo of missing classess at \Composer\Autoload\ClassLoader::$missingClasses. When the class is instantiated in the controller code, the class loader knows the class is missing, so the fatal error is thrown.

Proposed resolution

My current suggestion is to serialise the module list, so it may be unpacked and relevant PSR namespaces added to the autoloader before the container is unserialised in `\Drupal\Core\DrupalKernel::getCachedContainerDefinition`. This could be done with a sibling cache entry in `cache.container` with the same container cache key (\Drupal\Core\DrupalKernel::getContainerCacheKey) as the whole container.

A secondary suggestion is to serialise all parameters, so they are double serialised, before then again being serialised by the cache system. But this would still rely on the above module list serialisation process.

That, or also serialise/unserialise parameters indivually as they enter the container cache. We can safely unserialise the known module list parameter, then unserialise the rest after autoloader has been populated with module namespaces.

----

I think it is completely reasonable to expect we should be able to add classes owned by modules as parameters. You'll notice that instances of classes defined in vendor/std PHP library do not suffer from this problem. It's likely we havn't hit this issue before/often because custom code can just workaround with double-serialisation. But thats ugly and something that should be handled by core.

Remaining tasks

User interface changes

Nil

Introduced terminology

Nil

API changes

Nil.

Data model changes

Internal changes to serialised containter.

Release notes snippet

๐Ÿ› Bug report
Status

Active

Version

11.1 ๐Ÿ”ฅ

Component

base system

Created by

๐Ÿ‡ฆ๐Ÿ‡บAustralia dpi Perth, Australia

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Merge Requests

Comments & Activities

  • Issue created by @dpi
  • ๐Ÿ‡ฆ๐Ÿ‡บAustralia dpi Perth, Australia
  • ๐Ÿ‡ฆ๐Ÿ‡บAustralia dpi Perth, Australia

    Critical points:

    DrupalKernel

    if (empty($this->moduleList) && !$this->containerNeedsRebuild) {
      $container_definition = $this->getCachedContainerDefinition();
    }
    

    and further down, where module PSR4 namespaces are added:

    $this->attachSynthetic($container);
    
  • ๐Ÿ‡ณ๐Ÿ‡ฟNew Zealand quietone
  • ๐Ÿ‡ฆ๐Ÿ‡บAustralia dpi Perth, Australia

    The result is similarly catastrophic for _simpler_ data types stored in the container, like enums.

    Enums are a valid return type of getParameter() in Symfony, but Drupal has not been updated to support them.

  • ๐Ÿ‡บ๐Ÿ‡ธUnited States cmlara

    IIRX Iโ€™ve hit something similar, in the past.

    IIRC I was trying to use a decorator with autowiring and an โ€œignore if service doesnโ€™t existโ€ attribute for an optional core service. However since the core module wasnโ€™t loaded IIRC the references to the class caused similar faults.

    Maybe thatโ€™s not in scope here though I wonder if this will solve that too.

    I wonder is this is a sign that Drupal should consider adopting a โ€œmore standardโ€ composer layout of having modules always register their namespaces into the composer autoloader instead of the Kernel registering enabled modules?

    Obviously adjusting the Kernel would make more sense here as an immediate fix, Iโ€™m just spitballing โ€œlong termโ€ if it might be time to let composer handle more of this.

  • ๐Ÿ‡ฌ๐Ÿ‡งUnited Kingdom longwave UK

    This does feel like another unfortunate side effect of Drupal making a distinction between a module being available on disk and a module that is actually installed.

  • ๐Ÿ‡ฆ๐Ÿ‡บAustralia dpi Perth, Australia

    I believe there is an issues somewhere..?

    It would be nice to have code always-available, and simplify what "install" means. As in an "enabled" module (etc) adds it do discovery and namespaces, and which hooks are executed.

  • ๐Ÿ‡บ๐Ÿ‡ธUnited States nicxvan

    Would this be related ๐Ÿ’ฌ Drupal 11.2 upgrade causes \Drupal::$container is not initialized yet error Active
    We're having trouble reproducing the issue.

  • ๐Ÿ‡ณ๐Ÿ‡ฟNew Zealand quietone

    When I changed the version earlier, I did not add a comment. Doing that now.

    If this problem was discovered on a version of Drupal that is not 11.x, add that information in the issue summary and leave the version at 11.x. In Drupal core changes are made on on 11.x (our main development branch) first, and are then back ported as needed according to the Core change policies โ†’ . This is also mentioned on the version โ†’ section of the list of issue fields documentation.

  • ๐Ÿ‡บ๐Ÿ‡ธUnited States cmlara

    I wonder if this will solve that too.

    Thinking deeper my issue would likely not be solved by this solution. The module would still be disabled so code would still not exist in the auto loader even if the process occurred sooner.

    I believe there is an issue(s) somewhere..?

    I thought I saw something in the past, however on a quick search the only issue I turn up is #2536610: Ensure all modules are autoloaded with PSR-4 only if enabled โ†’ advocating for the exact opposite.

  • Pipeline finished with Success
    7 days ago
    Total: 485s
    #489358
  • ๐Ÿ‡ฆ๐Ÿ‡บAustralia dpi Perth, Australia

    Pushed !12038

    ---

    I've pushed up a PR which seems to fix the problem.

    I'm at a loss for how to test this, as core/tests/bootstrap.php always adds all modules to the autoloader. I've tried a bunch of workarounds but I cant seem to get far. I'd appreciate if anyone can give that a go...

    The flow of DrupalKernel has been moved around a bit, and the nearby type handling has been improved to satisfy PHPStan better, for even PHPStan levels core doesn't enforce yet. Especially with how core is passing around this semi-endorsed "container definition array" to various subsystems, for which I've specifically added a @phpstan-type for.

    The way the PR currently works is there is a new cache entry in cache.container cache bin with a cache key with a different prefix to the entry with the entire container definition array.

    A new ContainerDiscovery class is implemented to hold onto discovery related data. Objects beat arbitrary arrays. And we can extend it later if we need other discovery related things added to it.

    The flow of where classLoaderAddMultiplePsr4</code. is called has been changed up. It isnt evident to me why <code>classLoaderAddMultiplePsr4 is currently in attachSynthetic, and it doesnt look like its relevant to that method as state and namespaces are not modified there. Perhaps it was more relevant many refactors ago. So it has been moved out of attachSynthetic, up into initializeContainer, so I can track and prevent doubling up of calls to classLoaderAddMultiplePsr4

    Some parts of DrupalKernel have been changed to be more concise especially when a "discovery" edition of a code block is introduced alongside an existing "container definition" block.

  • I'm at a loss for how to test this, as core/tests/bootstrap.php always adds all modules to the autoloader. I've tried a bunch of workarounds but I cant seem to get far. I'd appreciate if anyone can give that a go...

    See Drupal\Tests\migrate_drupal\Kernel\I18nQueryTraitTest::disablePsr4ForUninstalledModules() for an idea on how this can be done, but it's only been used for a very limited test use case.

  • ๐Ÿ‡ฆ๐Ÿ‡บAustralia dpi Perth, Australia

    Thanks for the suggestion @godotislate, however I don't think thats works.

    Problem is I need to add an enum or class reference to the container then get a new kernel either by a new PHP process or programmatically _rebooting_ it, so DrupalKernel uses the warm container cache. However since the test is all the same process, PHP already knows the class exists and caches it (AFAICT), so modifying the autoloader after the fact wont work.

    Perhaps I will need a web test since I can rely on fresh PHP processes...

  • However since the test is all the same process, PHP already knows the enum/class exists after I created instances of test enum/classes and caches it (AFAICT), so modifying the autoloader after the fact wont work.

    Ah, right. Once the class/enum is loaded by PHP into memory, it stays there.

  • Pipeline finished with Success
    5 days ago
    #491177
  • ๐Ÿ‡ฆ๐Ÿ‡บAustralia dpi Perth, Australia

    Converting what should be a Kernel test to a Functional test in order to get different PHP process is working out! ๐ŸŽ‰ Still open to ideas if anyone else knows better.

    Coverage

    Critically, an error is shown when the new classLoaderAddMultiplePsr4 call in !12038 is removed:

    Exception: Warning: unserialize(): Class 'Drupal\autoloader_test\AutoloaderTestEnum' not found
    Drupal\Component\Serialization\PhpSerialize::decode()() (Line: 21)

    When restored, the test pass once again.

    NR

  • ๐Ÿ‡ฆ๐Ÿ‡บAustralia dpi Perth, Australia

    I did find ๐Ÿ“Œ Extension System, Part IV: Properly register all installed extensions/namespaces during container generation Needs work , which is more about comprehensive/unified namespaces for everything. E.g themes are added to autoloader in a crazy place (\Drupal\Core\Extension\ThemeHandler::addTheme). Distinctly different to this issue.

  • ๐Ÿ‡ฆ๐Ÿ‡บAustralia dpi Perth, Australia
  • ๐Ÿ‡บ๐Ÿ‡ธUnited States nicxvan

    I've been eyeing ๐Ÿ“Œ Extension System, Part IV: Properly register all installed extensions/namespaces during container generation Needs work because it's likely on the road to getting oop hooks in themes which will let us clean up a fair bit around preprocess.

  • ๐Ÿ‡บ๐Ÿ‡ธUnited States nicxvan

    I took a look, in general it looks like this is going in an interesting direction. I ran the test only job and it fails as expected.

    I did not review the test.

    Had a few questions.

Production build 0.71.5 2024