Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DependencyInjection] #[When(env: 'staging')] throws exception when using dev-only classes #51106

Closed
jon-ht opened this issue Jul 25, 2023 · 6 comments

Comments

@jon-ht
Copy link

jon-ht commented Jul 25, 2023

Symfony version(s) affected

5.4, 6.3

Description

When I add #[When(env: 'staging')] attribute to a service that should only be used in staging environment, I got an exception about a missing class.

Error: During class fetch: Uncaught ReflectionException: Class "Nelmio\Alice\IsAServiceTrait" not found while loading "App\OnlyAvailableOnStaging". in /srv/app/vendor/symfony/error-handler/DebugClassLoader.php:293
Stack trace:
#0 /srv/app/vendor/symfony/config/Resource/ClassExistenceResource.php(76): class_exists('App\\OnlyAvailab...')
#1 /srv/app/vendor/symfony/dependency-injection/ContainerBuilder.php(361): Symfony\Component\Config\Resource\ClassExistenceResource->isFresh(0)
#2 /srv/app/vendor/symfony/dependency-injection/Loader/FileLoader.php(275): Symfony\Component\DependencyInjection\ContainerBuilder->getReflectionClass('App\\OnlyAvailab...')
#3 /srv/app/vendor/symfony/dependency-injection/Loader/FileLoader.php(121): Symfony\Component\DependencyInjection\Loader\FileLoader->findClasses('App\\', '../src/', Array, Object(Symfony\Component\DependencyInjection\Compiler\RegisterAutoconfigureAttributesPass), '/srv/app/config...')
// ...

That's totally fair, because the trait referenced here (IsAServiceTrait) is only installed in dev environment.

What I don't understand, is that ReflectionException should be caught here

try {
$r = $this->container->getReflectionClass($class);
} catch (\ReflectionException $e) {
$classes[$class] = $e->getMessage();
continue;
}

But I never reach this catch block and Symfony\Component\ErrorHandler\ErrorHandler::handleFatalError get called instead

public static function handleFatalError(array $error = null): void

How to reproduce

This is the minimal reproducer I can get

<?php

declare(strict_types=1);

namespace App;

use Nelmio\Alice\IsAServiceTrait;
use Symfony\Component\DependencyInjection\Attribute\When;

#[When(env: 'staging')]
class OnlyAvailableOnStaging
{
    use IsAServiceTrait;
}

Nelmio/Alice doesn't need to be in composer.json (even if in my real project, it is). What matters here is to have a conditional service using a class only available when doing composer install and not composer install --no-dev.

Another important thing to note: if I comment use IsAServiceTrait and I want to inject an inexistant service, everything works fine

<?php

declare(strict_types=1);

namespace App;

use Fidry\AliceDataFixtures\LoaderInterface;
use Nelmio\Alice\IsAServiceTrait;
use Symfony\Component\DependencyInjection\Attribute\When;

#[When(env: 'staging')]
class OnlyAvailableOnStaging
{
//    use IsAServiceTrait;

    public function __construct(LoaderInterface $loader)
    {
    }
}

Possible Solution

No response

Additional Context

IsAServiceTrait comes from https://github.com/nelmio/alice/blob/a020c0767e10dbb7bf1c193e16e94710691133d9/src/IsAServiceTrait.php

@nicolas-grekas
Copy link
Member

You might want to see if ClassExistenceResource::throwOnRequiredClass is called when this happens, and if it's called, debug what happens in it.

@jon-ht
Copy link
Author

jon-ht commented Jul 28, 2023

Hi,

As you pointed it out, my exception is handled by ClassExistenceResource::throwOnRequiredClass.

Howerver, condition trait_exists($class, false) is false because the package is not installed.

if (class_exists($class, false) || interface_exists($class, false) || trait_exists($class, false)) {
if (null !== $previous) {
throw $previous;
}
return;
}

The only other case where it can skip the error is based on

switch ($callerFrame['function']) {
case 'get_class_methods':
case 'get_class_vars':
case 'get_parent_class':
case 'is_a':
case 'is_subclass_of':
case 'class_exists':
case 'class_implements':
case 'class_parents':
case 'trait_exists':
case 'defined':
case 'interface_exists':
case 'method_exists':
case 'property_exists':
case 'is_callable':
return;
}

But this occurs during an include, so no return either.

In the end, error is thrown as a ReflectionException and caught by Symfony\Component\ErrorHandler\ErrorHandler::handleFatalError. I can't find why this ReflectionException is converted to FatalError

@jon-ht
Copy link
Author

jon-ht commented Jul 28, 2023

I guess this is a side effect or missing case of php/php-src#4697

I tried to add a dummy service with inexistant trait on PHP 7.3.33, no errors. If I switch to PHP 7.3.7, as mentioned here #32395, I got an exception

During class fetch: Uncaught ReflectionException: Class "App\FooTrait" not found while loading "App\DummyService". in /application/api/vendor/symfony/config/Resource/ClassExistenceResource.php

Switching to a version 7.3.6 doesn't throw an exception

There is also this issue #32995 for PHP 7.4 but was resolved in php-src

@carsonbot
Copy link

Hey, thanks for your report!
There has not been a lot of activity here for a while. Is this bug still relevant? Have you managed to find a workaround?

@carsonbot
Copy link

Hello? This issue is about to be closed if nobody replies.

@carsonbot carsonbot removed the Stalled label Feb 16, 2024
@carsonbot
Copy link

Hey,

I didn't hear anything so I'm going to close it. Feel free to comment if this is still relevant, I can always reopen!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants