Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[DI] wither calls break services when used in certain configurations #48814

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

Closed
cs278 opened this issue Dec 28, 2022 · 7 comments
Closed

[DI] wither calls break services when used in certain configurations #48814

cs278 opened this issue Dec 28, 2022 · 7 comments

Comments

@cs278
Copy link
Contributor

cs278 commented Dec 28, 2022

Symfony version(s) affected

4.4.49, 5.4.17 (those I've reproduced the issue on)

Description

Configuring a service that uses the wither pattern, added in #30212, can break the dumped PHP container with more complex configurations. The inlined service that uses the wither pattern for some reason is written into the container with the service ID of the dependant service, most of the time it is subsequently replaced by the correct implementation but in some configurations this does not happen.

As an example this YAML generates the following method:

services:
    _defaults:
        autoconfigure: true

    App\TestServiceOptions:
        public: false
        calls:
            - withOption: !returns_clone ['foo', 'bar']

    App\TestService:
        public: false
        calls:
            - setOptions: ['@App\TestServiceOptions']

    App\TestCommand:
        arguments:
            - ~
            - '@App\TestService'
/**
 * Gets the private 'App\TestCommand' shared service.
 *
 * @return \App\TestCommand
 */
public static function do($container, $lazyLoad = true)
{
   // include_once ...

    $a = new \App\TestService();

    $b = new \App\TestServiceOptions()
    // Here: inlined wither service is written as App\TestCommand
    $container->privates['App\TestCommand'] = $b = $b->withOption('foo', 'bar');

    $a->setOptions($b);

    // Invalid service is replaced here - fixing the problem.
    $container->privates['App\\TestCommand'] = $instance = new \App\TestCommand(NULL, $a);

    $instance->setName('test');

    return $instance;
}

As I said above in some situations, the correct initialisation of $container->privates['App\\TestCommand'] is done before instead of after which then breaks other services as they get completely the wrong class.

How to reproduce

I've not managed to create a simple reproducer, but I do have one based on the Sentry Symfony bundle which is how I found this problem in the first place. https://github.com/cs278/symfony-issue-48814

git clone https://github.com/cs278/symfony-issue-48814.git
composer install --no-scripts
rm -rf var/cache/dev/
bin/console cache:clear -vvv
grep -h -R -B 10 -A 20 'SentryBeforeSend(' var/cache/dev/Container*/*.php

In this case the following service method is created:

/**
 * Gets the private 'Sentry\State\HubInterface' shared service.
 *
 * @return \Sentry\State\HubInterface
 */
protected function getHubInterfaceService()
{
    $this->privates['Sentry\\State\\HubInterface'] = $instance = \Sentry\State\HubAdapter::getInstance();

    $a = new \App\SentryBeforeSend();
    $this->privates['Sentry\State\HubInterface'] = $a = $a->withTag('foo', 'bar'); // Broken!

    $b = new \Sentry\Options(['before_send' => $a, 'integrations' => [0 => new \Sentry\Integration\IgnoreErrorsIntegration(['ignore_exceptions' => [0 => 'Symfony\\Component\\ErrorHandler\\Error\\FatalError', 1 => 'Symfony\\Component\\Debug\\Exception\\FatalErrorException']]), 1 => new \Sentry\Integration\RequestIntegration(new \Sentry\SentryBundle\Integration\RequestFetcher(($this->services['request_stack'] ?? ($this->services['request_stack'] = new \Symfony\Component\HttpFoundation\RequestStack())), NULL))], 'prefixes' => [0 => \dirname(__DIR__, 4), 1 => '.', 2 => '/usr/share/php'], 'trace_propagation_targets' => [], 'environment' => 'dev', 'release' => 'dev-master@425c01f', 'tags' => [], 'in_app_exclude' => [0 => $this->targetDir.'', 1 => $this->targetDir.'', 2 => (\dirname(__DIR__, 4).'/vendor')], 'in_app_include' => [], 'class_serializers' => [], 'dsn' => $this->getEnv('SENTRY_DSN')]);

    $c = new \Sentry\ClientBuilder($b);
    $c->setSdkIdentifier('sentry.php.symfony');
    $c->setSdkVersion('4.5.0');
    $c->setTransportFactory(new \Sentry\SentryBundle\Transport\TransportFactory(NULL, NULL, NULL, NULL, NULL, NULL));
    $c->setSerializer(new \Sentry\Serializer\Serializer($b));
    $c->setRepresentationSerializer(new \Sentry\Serializer\RepresentationSerializer($b));
    if ($this->has('Psr\\Log\\NullLogger')) {
        $c->setLogger(($this->services['Psr\\Log\\NullLogger'] ?? $this->get('Psr\\Log\\NullLogger', /* ContainerInterface::NULL_ON_INVALID_REFERENCE */ 2)));
    }

    $instance->bindClient($c->getClient());

    return $instance;
}

Possible Solution

I've got a PR incoming with a fix.

Additional Context

Workaround until a fix is released is to make the service that uses the returns_clone tag public.

@julienfastre
Copy link

julienfastre commented Dec 30, 2022

I experience a problem which may be similar, with a complex configuration of EntityManagerInterface.

With symfony/dependency-injection on 5.4.17, the kernel "prepares" the EntityManager like this:

    /**
     * Gets the public 'doctrine.orm.default_entity_manager' shared service.
     *
     * @return \Doctrine\ORM\EntityManager
     */
    protected function getDoctrine_Orm_DefaultEntityManagerService($lazyLoad = true)
    {
        $a = ($this->services['doctrine.dbal.default_connection'] ?? $this->getDoctrine_Dbal_DefaultConnectionService());

        if (isset($this->services['doctrine.orm.default_entity_manager'])) {
            return $this->services['doctrine.orm.default_entity_manager'];
        }
        $b = new \Doctrine\ORM\Configuration();

        $c = new \Doctrine\Persistence\Mapping\Driver\MappingDriverChain();

        $d = ($this->privates['doctrine.orm.default_annotation_metadata_driver'] ?? $this->getDoctrine_Orm_DefaultAnnotationMetadataDriverService());

        $c->addDriver($d, 'App\\Entity');
        $c->addDriver($d, 'Chill\\MainBundle\\Entity');
        $c->addDriver($d, 'Chill\\PersonBundle\\Entity');
        $c->addDriver($d, 'Chill\\CustomFieldsBundle\\Entity');
        $c->addDriver($d, 'Chill\\ActivityBundle\\Entity');
        $c->addDriver($d, 'Chill\\DocStoreBundle\\Entity');
        $c->addDriver($d, 'Chill\\DocGeneratorBundle\\Entity');
        $c->addDriver($d, 'Chill\\AsideActivityBundle\\Entity');
        $c->addDriver($d, 'Chill\\CalendarBundle\\Entity');
        $c->addDriver($d, 'Chill\\TaskBundle\\Entity');
        $c->addDriver($d, 'Chill\\ThirdPartyBundle\\Entity');
        $c->addDriver($d, 'Chill\\BudgetBundle\\Entity');

        // here, an exception is thrown because the configuration ($b) does not contains a mapping driver,
       // which is added later in the function
        $this->services['doctrine.orm.default_entity_manager'] = $instance = new \Doctrine\ORM\EntityManager($a, $b);

        (new \Doctrine\Bundle\DoctrineBundle\ManagerConfigurator([], []))->configure($instance);

        $b->setEntityNamespaces(['App' => 'App\\Entity', 'ChillMainBundle' => 'Chill\\MainBundle\\Entity', 'ChillPersonBundle' => 'Chill\\PersonBundle\\Entity', 'ChillCustomFieldsBundle' => 'Chill\\CustomFieldsBundle\\Entity', 'ChillActivityBundle' => 'Chill\\ActivityBundle\\Entity', 'ChillDocStoreBundle' => 'Chill\\DocStoreBundle\\Entity', 'ChillDocGeneratorBundle' => 'Chill\\DocGeneratorBundle\\Entity', 'ChillAsideActivityBundle' => 'Chill\\AsideActivityBundle\\Entity', 'ChillCalendarBundle' => 'Chill\\CalendarBundle\\Entity', 'ChillTaskBundle' => 'Chill\\TaskBundle\\Entity', 'ChillThirdPartyBundle' => 'Chill\\ThirdPartyBundle\\Entity', 'ChillBudgetBundle' => 'Chill\\BudgetBundle\\Entity']);
        $b->setMetadataCache(new \Symfony\Component\Cache\Adapter\ArrayAdapter());
        $b->setQueryCache(($this->privates['cache.doctrine.orm.default.query'] ?? $this->getCache_Doctrine_Orm_Default_QueryService()));
        $b->setResultCache(($this->privates['cache.doctrine.orm.default.result'] ?? $this->getCache_Doctrine_Orm_Default_ResultService()));

       /// here is added the missing metadata driver
        $b->setMetadataDriverImpl($c);

        $b->setProxyDir(($this->targetDir.''.'/doctrine/orm/Proxies'));
        $b->setProxyNamespace('Proxies');
        $b->setAutoGenerateProxyClasses(true);
        $b->setSchemaIgnoreClasses([]);
        $b->setClassMetadataFactoryName('Doctrine\\ORM\\Mapping\\ClassMetadataFactory');
        $b->setDefaultRepositoryClassName('Doctrine\\ORM\\EntityRepository');
        $b->setNamingStrategy(new \Doctrine\ORM\Mapping\DefaultNamingStrategy());
        $b->setQuoteStrategy(new \Doctrine\ORM\Mapping\DefaultQuoteStrategy());
        $b->setEntityListenerResolver(($this->services['doctrine.orm.default_entity_listener_resolver'] ?? $this->getDoctrine_Orm_DefaultEntityListenerResolverService()));
        $b->setRepositoryFactory( /* etc. etc. */)
        $b->addCustomHydrationMode('chill_flat_hierarchy_list', 'Chill\\MainBundle\\Doctrine\\ORM\\Hydration\\FlatHierarchyEntityHydrator');
        $b->addCustomStringFunction('GET_PERSON_ADDRESS_ADDRESS_ID', 'Chill\\PersonBundle\\Doctrine\\DQL\\AddressPart\\AddressPartAddressId');

        // ...
        // add a lot of other custom function there ...
       // ...

        return $instance;
    }

This cause a bug because the EntityManager instance is created too early:

In MissingMappingDriverImplementation.php line 11:
                                                                                                                       
  [Doctrine\ORM\Exception\MissingMappingDriverImplementation]                                                          
  It's a requirement to specify a Metadata Driver and pass it to Doctrine\ORM\Configuration::setMetadataDriverImpl().  
                                                                                                                       

Exception trace:
  at /var/www/app/vendor/doctrine/orm/lib/Doctrine/ORM/Exception/MissingMappingDriverImplementation.php:11
 Doctrine\ORM\Exception\MissingMappingDriverImplementation::create() at /var/www/app/vendor/doctrine/orm/lib/Doctrine/ORM/EntityManager.php:162
 Doctrine\ORM\EntityManager->__construct() at /var/www/app/var/cache/dev/ContainerXpmlFIp/srcApp_KernelDevDebugContainer.php:4128

If I move those two lines at the end of the method, just before the return $instance;, it does work:

    protected function getDoctrine_Orm_DefaultEntityManagerService($lazyLoad = true)
    {
        // ...
        
        $this->services['doctrine.orm.default_entity_manager'] = $instance = new \Doctrine\ORM\EntityManager($a, $b);

        (new \Doctrine\Bundle\DoctrineBundle\ManagerConfigurator([], []))->configure($instance);

        return $instance;
    }

If I downgrade symfony/dependency-injection to 5.4.16 (composer require symfony/dependency-injection 5.4.16), the container is compiled in this manner, and the problem is fixed.

EDIT 30/12/22 add comment in code block and correct version mismatch

@axi

This comment was marked as off-topic.

@HeinDR
Copy link

HeinDR commented Jan 5, 2023

I also experience a similar problem with the doctrine configuration when I add an extra entity manager. After the update te symfony/dependency-injection 5.4.17 I get this error:
It's a requirement to specify a Metadata Driver and pass it to Doctrine\ORM\Configuration::setMetadataDriverImpl().
when I clear the cache.

I could trace the problem back to commit 58f2988 where in the PhpDumper the edge->isLazy() check is removed in favor of checks further down the foreach.

private function collectCircularReferences(string $sourceId, array $edges, array &$checkedNodes, array &$loops = [], array $path = [], bool $byConstructor = true): void
    {
        $path[$sourceId] = $byConstructor;
        $checkedNodes[$sourceId] = true;
        foreach ($edges as $edge) {
            $node = $edge->getDestNode();
            $id = $node->getId();
            if ($sourceId === $id || !$node->getValue() instanceof Definition || $edge->isWeak()) {
                continue;
            }
       //..
    }

When I add the edge->isLazy() check and manualy delete the cache the error is gone.

private function collectCircularReferences(string $sourceId, array $edges, array &$checkedNodes, array &$loops = [], array $path = [], bool $byConstructor = true): void
    {
        $path[$sourceId] = $byConstructor;
        $checkedNodes[$sourceId] = true;
        foreach ($edges as $edge) {
            $node = $edge->getDestNode();
            $id = $node->getId();
            if ($sourceId === $id || !$node->getValue() instanceof Definition || $edge->isLazy() || $edge->isWeak()) {
                continue;
            }
       //..
    }

@axi

This comment was marked as off-topic.

@cs278
Copy link
Contributor Author

cs278 commented Jan 5, 2023

@julienfastre @axi @HeinDR I'm pretty sure the problem you're talking about is a different issue.

@HeinDR
Copy link

HeinDR commented Jan 5, 2023

@cs278 Ah sorry, I looked at @julienfastre comment and that seems to be the same problem and I also could get my issue sort of fixed with his fix.

@axi
Copy link

axi commented Jan 5, 2023

@julienfastre @axi @HeinDR I'm pretty sure the problem you're talking about is a different issue.

Ouch, sorry, I opened a new issue #48895

nicolas-grekas added a commit that referenced this issue Jan 9, 2023
…-grekas)

This PR was merged into the 5.4 branch.

Discussion
----------

[DependencyInjection] Fix dumping inlined withers

| Q             | A
| ------------- | ---
| Branch?       | 5.4
| Bug fix?      | yes
| New feature?  | no
| Deprecations? | no
| Tickets       | Fix #48814
| License       | MIT
| Doc PR        | -

Commits
-------

455ace0 [DependencyInjection] Fix dumping inlined withers
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

6 participants