Skip to content

[HttpKernel][DependencyInjection][ProxyManager] Class loading error with inline factories  #43956

Closed
@digilist

Description

@digilist

Symfony version(s) affected

5.3

Description

The dependency injection container is usually updated automatically whenever a change has been made that requires an update to the container cache (when the debug mode is enabled).

However, when the service container is dumped into a single file (via the container.dumper.inline_factories parameter) and lazy services are used this does not work under some conditions.

In our case, we have been seeing this error whenever we deleted a class (and often implicitly by switching branches):

Attempted to load class "EntityManager_9a5be93" from the global namespace. Did you forget a "use" statement?

The only way to fix this was to manually delete the cache. It's probably an edge case, but this bug has been hunting us for months.

How to reproduce

I've created a little demo project that is based on the Symfony demo application: https://github.com/digilist/symfony-class-loading-bug

These are basically the changes (composer.lock and symfony.lock ignored):
diff --git a/composer.json b/composer.json
index c00de53..2e38c71 100644
--- a/composer.json
+++ b/composer.json
@@ -28,8 +28,9 @@
         "symfony/mailer": "^5.3",
         "symfony/monolog-bundle": "^3.1",
         "symfony/polyfill-intl-messageformatter": "^1.12",
-        "symfony/security-bundle": "^5.3",
+        "symfony/proxy-manager-bridge": "5.3.*",
         "symfony/runtime": "^5.3",
+        "symfony/security-bundle": "^5.3",
         "symfony/string": "^5.3",
         "symfony/translation": "^5.3",
         "symfony/twig-pack": "^1.0",
diff --git a/config/services.yaml b/config/services.yaml
index 2c46868..3f102ef 100644
--- a/config/services.yaml
+++ b/config/services.yaml
@@ -8,6 +8,7 @@ parameters:
     # This parameter defines the codes of the locales (languages) enabled in the application
     app_locales: ar|en|fr|de|es|cs|nl|ru|uk|ro|pt_BR|pl|it|ja|id|ca|sl|hr|zh_CN|bg|tr|lt|bs|sr_Cyrl|sr_Latn
     app.notifications.email_sender: anonymous@example.com
+    container.dumper.inline_factories: true
 
 services:
     # default configuration for services in *this* file
@@ -29,6 +30,11 @@ services:
             - '../src/Kernel.php'
             - '../src/Tests/'
 
+    App\Utils\:
+        resource: '../src/Utils'
+        public: true # prevent dropping unused dependencies
+        lazy: true
+
     # when the service definition only contains arguments, you can omit the
     # 'arguments' key and define the arguments just below the service class
     App\EventSubscriber\CommentNotificationSubscriber:
diff --git a/src/Utils/SomeDependency.php b/src/Utils/SomeDependency.php
new file mode 100644
index 0000000..6c61fc8
--- /dev/null
+++ b/src/Utils/SomeDependency.php
@@ -0,0 +1,10 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Utils;
+
+class SomeDependency
+{
+
+}

After applying the changes and opening the application (i.e. warming up the cache) you need to delete the SomeDependency class to see this error on the next request:

Attempted to load class "EntityManager_9a5be93" from the global namespace.
Did you forget a "use" statement?

Possible Solution

No response

Additional Context

The App_KernelDevDebugContainer.php file is generated like this:

/**
 * @internal This class has been auto-generated by the Symfony Dependency Injection Component.
 */
class App_KernelDevDebugContainer extends Container
{
    // [...]
}

class SomeDependency_91c8bfe extends \App\Utils\SomeDependency implements \ProxyManager\Proxy\VirtualProxyInterface
{
    // [...]
}

if (!\class_exists('SomeDependency_91c8bfe', false)) {
    \class_alias(__NAMESPACE__.'\\SomeDependency_91c8bfe', 'SomeDependency_91c8bfe', false);
}


class EntityManager_9a5be93 extends \Doctrine\ORM\EntityManager implements \ProxyManager\Proxy\VirtualProxyInterface
{
    // [...]
}

if (!\class_exists('EntityManager_9a5be93', false)) {
    \class_alias(__NAMESPACE__.'\\EntityManager_9a5be93', 'EntityManager_9a5be93', false);
}

When the SomeDependency class is deleted the cache file results in an error when it is included by the Kernel class: Class "App\Utils\SomeDependency" not found

The container cache file is included here:

try {
if (is_file($cachePath) && \is_object($this->container = include $cachePath)
&& (!$this->debug || (self::$freshCache[$cachePath] ?? $cache->isFresh()))
) {
self::$freshCache[$cachePath] = true;
$this->container->set('kernel', $this);
error_reporting($errorLevel);
return;
}
} catch (\Throwable $e) {
}
$oldContainer = \is_object($this->container) ? new \ReflectionClass($this->container) : $this->container = null;

As you can see, the include in line 453 is wrapped in a try..catch block that catches any error and therefore, the error of the missing class is ignored.

Then, in line 465 is checked if the container class has been defined. This is actually the case, because the container class is defined at the very top of the file.

I am not entirely sure about the code that follows in the next block, but I think it considers the container valid and doesn't consider a new cache warmup, because it returns in line 483:

$oldContainer = \is_object($this->container) ? new \ReflectionClass($this->container) : $this->container = null;
try {
is_dir($buildDir) ?: mkdir($buildDir, 0777, true);
if ($lock = fopen($cachePath.'.lock', 'w')) {
flock($lock, \LOCK_EX | \LOCK_NB, $wouldBlock);
if (!flock($lock, $wouldBlock ? \LOCK_SH : \LOCK_EX)) {
fclose($lock);
$lock = null;
} elseif (!is_file($cachePath) || !\is_object($this->container = include $cachePath)) {
$this->container = null;
} elseif (!$oldContainer || \get_class($this->container) !== $oldContainer->name) {
flock($lock, \LOCK_UN);
fclose($lock);
$this->container->set('kernel', $this);
return;
}
}
} catch (\Throwable $e) {
} finally {
error_reporting($errorLevel);
}

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions