Skip to content

[DoctrineBridge] #[MapEntity] does not resolve an entity from its interface, aliased using resolve_target_entities #51765

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
siketyan opened this issue Sep 27, 2023 · 2 comments · Fixed by #54545

Comments

@siketyan
Copy link
Contributor

siketyan commented Sep 27, 2023

Symfony version(s) affected

6.3.x

Description

#[MapEntity] attribute does not resolve an entity from its interface names, even it is implemented on the entity and aliased using doctrine.orm.resolve_target_entities configuration.

Error screen says:

Controller "App\Controller::showUser" requires that you provide a value for the "$user" argument. Either the argument is nullable and no null value has been provided, no default value has been provided or there is a non-optional argument after this one.

How to reproduce

config/packages/doctrine.yaml

doctrine:
  orm:
    resolve_target_entities:
      App\Entity\UserInterface: App\Entity\User

src/Entity/UserInterface.php

interface UserInterface
{
    public function getId(): ?int;
}

src/Entity/User.php

#[ORM\Entity]
class User implements UserInterface
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column(type: 'integer')]
    public ?int $id = null;
}

src/Controller/Controller.php

class Controller
{
    #[Route('/users/{id}', methods: ['GET']]
    public function showUser(#[MapEntity] UserInterface $user): Response
    {
        return new JsonResponse($user);
    }
}

Possible Solution

Problem 1: #[MapEntity] does not accept interfaces

MapEntity checks the class existence by class_exists. For interface names, the function will return false even if it exists. We can use class_exists($class) || interface_exists($class) for resolve this.

Problem 2: EntityValueResolver uses ClassMetadataFactory::isTransient and it does not load metadata

To resolve entities aliased in resolve_target_entities, we have to load their metadata before using. ClassMetadataFactory::isTransient does not do so, then it will return true. We should explicitly load metadata to resolve entities.

Additional Context

Specifying the actual entity as #[MapEntity(class: User::class)] do a trick, but we actually separate the interface and the entity in different repository (integrated using Symfony Bundle system).

@trislem
Copy link

trislem commented Oct 20, 2023

+1

For informations, the issue is also reported in the doctrine/persistence repository (doctrine/persistence#63).
I encounter this issue on a Symfony 5.4 project. Since EntityValueResolver was introduced into Symfony 6.2, before that the issue is located into the SensioFrameworkExtraBundle DoctrineParamConverter

@NanoSector
Copy link
Contributor

We've worked around this issue by decorating the EntityValueResolver class and dynamically updating the $class property of the MapEntity attribute.

This consists of two parts; the wrapping ValueResolver, and a container compiler pass to insert the Doctrine configuration into the container:

ValueResolver decorator

Symfony should automatically pick this up and configure the service appropriately if it's placed in your project.

<?php

declare(strict_types=1);

namespace App\Common\Infrastructure\ValueResolver;

use InvalidArgumentException;
use Symfony\Bridge\Doctrine\ArgumentResolver\EntityValueResolver;
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\DependencyInjection\Attribute\AutowireDecorated;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Controller\ValueResolverInterface;
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

/**
 * @internal Works around https://github.com/symfony/symfony/issues/51765
 */
#[AsDecorator('doctrine.orm.entity_value_resolver')]
#[AutoconfigureTag('controller.argument_value_resolver')]
final readonly class AliasingEntityValueResolver implements ValueResolverInterface
{
    public function __construct(
        #[AutowireDecorated]
        private EntityValueResolver $inner,

        /** @var array<class-string, class-string> */
        #[Autowire(param: 'doctrine.orm.resolve_target_entities')]
        private array $doctrineTargetEntityAliases,
    ) {
    }

    /**
     * @return iterable<object>
     * @throws NotFoundHttpException
     * @throws InvalidArgumentException
     */
    public function resolve(Request $request, ArgumentMetadata $argument): iterable
    {
        if (\is_object($request->attributes->get($argument->getName()))) {
            return [];
        }

        $entityMappings = $argument->getAttributes(MapEntity::class, ArgumentMetadata::IS_INSTANCEOF);

        /** @var MapEntity|false $entityMapping */
        $entityMapping = reset($entityMappings);

        if ($entityMapping === false) {
            return [];
        }

        $targetClass = $entityMapping->class
            ?? $argument->getType()
            ?? throw new InvalidArgumentException('MapEntity parameters should specify either a type or a class argument');

        if (\array_key_exists($targetClass, $this->doctrineTargetEntityAliases)) {
            $entityMapping->class = $this->doctrineTargetEntityAliases[$targetClass];
        }

        return $this->inner->resolve($request, $argument);
    }
}

Compiler pass

This does assume the configuration property is always present.

<?php

declare(strict_types=1);

namespace App\Common\Infrastructure\DependencyInjection;

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use RuntimeException;

class DoctrineResolveTargetEntitiesInjectorCompilerPass implements CompilerPassInterface
{
    /**
     * @throws RuntimeException
     */
    public function process(ContainerBuilder $container): void
    {
        $config = $container->getExtensionConfig('doctrine');

        if (
            !isset($config[0]['orm']['resolve_target_entities'])
            || !is_array($mapping = $config[0]['orm']['resolve_target_entities'])
        ) {
            throw new RuntimeException(
                'doctrine.orm.resolve_target_entities is missing from the bundle configuration',
            );
        }

        $container->setParameter('doctrine.orm.resolve_target_entities', $mapping);
    }
}

Register it in your Kernel class:

<?php

declare(strict_types=1);

namespace App;

use App\Common\Infrastructure\DependencyInjection\DoctrineResolveTargetEntitiesInjectorCompilerPass;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;

final class Kernel extends BaseKernel
{
    use MicroKernelTrait;

    // ...

    protected function build(ContainerBuilder $container): void
    {
        $container->addCompilerPass(new DoctrineResolveTargetEntitiesInjectorCompilerPass());
    }

    // ...
}

NanoSector added a commit to NanoSector/symfony that referenced this issue Apr 10, 2024
This allows for fixing symfony#51765; with a consequential Doctrine bundle update, the resolve_target_entities configuration can be injected similarly to ResolveTargetEntityListener in the Doctrine codebase.

Alternatively the config and ValueResolver can be injected using a compiler pass in the Symfony core code, however the value resolver seems to be configured in the Doctrine bundle already.
NanoSector added a commit to NanoSector/symfony that referenced this issue Apr 10, 2024
This allows for fixing symfony#51765; with a consequential Doctrine bundle update, the resolve_target_entities configuration can be injected similarly to ResolveTargetEntityListener in the Doctrine codebase.

Alternatively the config and ValueResolver can be injected using a compiler pass in the Symfony core code, however the value resolver seems to be configured in the Doctrine bundle already.
NanoSector added a commit to NanoSector/symfony that referenced this issue Apr 10, 2024
This allows for fixing symfony#51765; with a consequential Doctrine bundle update, the resolve_target_entities configuration can be injected similarly to ResolveTargetEntityListener in the Doctrine codebase.

Alternatively the config and ValueResolver can be injected using a compiler pass in the Symfony core code, however the value resolver seems to be configured in the Doctrine bundle already.
NanoSector added a commit to NanoSector/symfony that referenced this issue Apr 11, 2024
This allows for fixing symfony#51765; with a consequential Doctrine bundle update, the resolve_target_entities configuration can be injected similarly to ResolveTargetEntityListener in the Doctrine codebase.

Alternatively the config and ValueResolver can be injected using a compiler pass in the Symfony core code, however the value resolver seems to be configured in the Doctrine bundle already.
NanoSector added a commit to NanoSector/DoctrineBundle that referenced this issue Apr 11, 2024
This is an addendum to PR symfony/symfony#51765 in the Symfony Doctrine Bridge, which adds type alias support to EntityValueResolver.

This code injects the doctrine.orm.resolve_target_entities configuration into the EntityValueResolver class.
NanoSector added a commit to NanoSector/DoctrineBundle that referenced this issue Aug 12, 2024
This is an addendum to PR symfony/symfony#51765 in the Symfony Doctrine Bridge, which adds type alias support to EntityValueResolver.

This code injects the doctrine.orm.resolve_target_entities configuration into the EntityValueResolver class.
NanoSector added a commit to NanoSector/symfony that referenced this issue Aug 12, 2024
This allows for fixing symfony#51765; with a consequential Doctrine bundle update, the resolve_target_entities configuration can be injected similarly to ResolveTargetEntityListener in the Doctrine codebase.

Alternatively the config and ValueResolver can be injected using a compiler pass in the Symfony core code, however the value resolver seems to be configured in the Doctrine bundle already.
NanoSector added a commit to NanoSector/DoctrineBundle that referenced this issue Aug 12, 2024
This is an addendum to PR symfony/symfony#51765 in the Symfony Doctrine Bridge, which adds type alias support to EntityValueResolver.

This code injects the doctrine.orm.resolve_target_entities configuration into the EntityValueResolver class.
nicolas-grekas pushed a commit to NanoSector/symfony that referenced this issue Aug 19, 2024
This allows for fixing symfony#51765; with a consequential Doctrine bundle update, the resolve_target_entities configuration can be injected similarly to ResolveTargetEntityListener in the Doctrine codebase.

Alternatively the config and ValueResolver can be injected using a compiler pass in the Symfony core code, however the value resolver seems to be configured in the Doctrine bundle already.
symfony-splitter pushed a commit to symfony/doctrine-bridge that referenced this issue Mar 22, 2025
This allows for fixing symfony/symfony#51765; with a consequential Doctrine bundle update, the resolve_target_entities configuration can be injected similarly to ResolveTargetEntityListener in the Doctrine codebase.

Alternatively the config and ValueResolver can be injected using a compiler pass in the Symfony core code, however the value resolver seems to be configured in the Doctrine bundle already.
symfony-splitter pushed a commit to symfony/doctrine-bridge that referenced this issue Mar 22, 2025
… to set type aliases (NanoSector)

This PR was merged into the 7.3 branch.

Discussion
----------

[DoctrineBridge] Add argument to `EntityValueResolver` to set type aliases

| Q             | A
| ------------- | ---
| Branch?       | 7.2
| Bug fix?      | no
| New feature?  | yes
| Deprecations? | no
| Issues        | Fix #51765
| License       | MIT

<!--
Replace this notice by a description of your feature/bugfix.
This will help reviewers and should be a good start for the documentation.

Additionally (see https://symfony.com/releases):
 - Always add tests and ensure they pass.
 - Bug fixes must be submitted against the lowest maintained branch where they apply
   (lowest branches are regularly merged to upper ones so they get the fixes too).
 - Features and deprecations must be submitted against the latest branch.
 - For new features, provide some code snippets to help understand usage.
 - Changelog entry should follow https://symfony.com/doc/current/contributing/code/conventions.html#writing-a-changelog-entry
 - Never break backward compatibility (see https://symfony.com/bc).
-->

This allows for fixing symfony/symfony#51765; with a consequential Doctrine bundle update, the resolve_target_entities configuration can be injected similarly to ResolveTargetEntityListener in the Doctrine codebase.

Alternatively the config and ValueResolver can be injected using a compiler pass in the Symfony core code, however the value resolver seems to be configured in the Doctrine bundle already.

Commits
-------

3aa64a35550 [DoctrineBridge] Add argument to EntityValueResolver to set type aliases
NanoSector added a commit to NanoSector/DoctrineBundle that referenced this issue Mar 24, 2025
This is an addendum to PR symfony/symfony#51765 in the Symfony Doctrine Bridge, which adds type alias support to EntityValueResolver.

This code injects the doctrine.orm.resolve_target_entities configuration into the EntityValueResolver class.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants