Skip to content

[PropertyInfo][TypeInfo] Unable to validate doctrine/orm v2 PersistentCollection #60598

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
andyexeter opened this issue May 30, 2025 · 3 comments

Comments

@andyexeter
Copy link
Contributor

andyexeter commented May 30, 2025

Symfony version(s) affected

7.3

Description

The TypeInfo component seems to be unable to parse a Doctrine\ORM\PersistentCollection object when using doctrine/orm v2. I am seeing the following exception thrown when attempting to validate an object with a PersistentCollection as a property:

In StringTypeResolver.php line 88:
                                                                                                                                                                                                                                                            
  [Symfony\Component\TypeInfo\Exception\UnsupportedException]                                                                                                                                                                                               
  Cannot resolve "array{cache?: array, cascade: array<string>, declared?: class-string, fetch: mixed, fieldName: string, id?: bool, inherited?: class-string, indexBy?: string, inversedBy: (string | null), isCascadeRemove: bool, isCascadePersist: bool  
  , isCascadeRefresh: bool, isCascadeMerge: bool, isCascadeDetach: bool, isOnDeleteCascade?: bool, isOwningSide: bool, joinColumns?: array<JoinColumnData>, joinColumnFieldNames?: array<string, string>, joinTable?: array, joinTableColumns?: list<mixed  
  >, mappedBy: (string | null), orderBy?: array, originalClass?: class-string, originalField?: string, orphanRemoval?: bool, relationToSourceKeyColumns?: array, relationToTargetKeyColumns?: array, sourceEntity: class-string, sourceToTargetKeyColumns?  
  : array<string, string>, targetEntity: class-string, targetToSourceKeyColumns?: array<string, string>, type: int, unique?: bool}".                                                                                                                        
                                                                                                                                                                                                                                                            

Exception trace:
  at /app/my_project/vendor/symfony/type-info/TypeResolver/StringTypeResolver.php:88
 Symfony\Component\TypeInfo\TypeResolver\StringTypeResolver->resolve() at /app/my_project/vendor/symfony/type-info/TypeContext/TypeContextFactory.php:207
 Symfony\Component\TypeInfo\TypeContext\TypeContextFactory->collectTypeAliases() at /app/my_project/vendor/symfony/type-info/TypeContext/TypeContextFactory.php:74
 Symfony\Component\TypeInfo\TypeContext\TypeContextFactory->createFromClassName() at /app/my_project/vendor/symfony/type-info/TypeContext/TypeContextFactory.php:217
 Symfony\Component\TypeInfo\TypeContext\TypeContextFactory->collectTypeAliases() at /app/my_project/vendor/symfony/type-info/TypeContext/TypeContextFactory.php:74
 Symfony\Component\TypeInfo\TypeContext\TypeContextFactory->createFromClassName() at /app/my_project/vendor/symfony/property-info/Extractor/PhpStanExtractor.php:209
 Symfony\Component\PropertyInfo\Extractor\PhpStanExtractor->getType() at /app/my_project/vendor/symfony/property-info/PropertyInfoExtractor.php:70
 Symfony\Component\PropertyInfo\PropertyInfoExtractor->getType() at /app/my_project/vendor/symfony/validator/Mapping/Loader/PropertyInfoLoader.php:193
 Symfony\Component\Validator\Mapping\Loader\PropertyInfoLoader->getPropertyTypes() at /app/my_project/vendor/symfony/validator/Mapping/Loader/PropertyInfoLoader.php:70
 Symfony\Component\Validator\Mapping\Loader\PropertyInfoLoader->loadClassMetadata() at /app/my_project/vendor/symfony/validator/Mapping/Loader/LoaderChain.php:48
 Symfony\Component\Validator\Mapping\Loader\LoaderChain->loadClassMetadata() at /app/my_project/vendor/symfony/validator/Mapping/Factory/LazyLoadingMetadataFactory.php:96
 Symfony\Component\Validator\Mapping\Factory\LazyLoadingMetadataFactory->getMetadataFor() at /app/my_project/vendor/symfony/validator/Validator/RecursiveContextualValidator.php:290
 Symfony\Component\Validator\Validator\RecursiveContextualValidator->validateObject() at /app/my_project/vendor/symfony/validator/Validator/RecursiveContextualValidator.php:126
 Symfony\Component\Validator\Validator\RecursiveContextualValidator->validate() at /app/my_project/vendor/symfony/validator/Validator/RecursiveValidator.php:81
 Symfony\Component\Validator\Validator\RecursiveValidator->validate() at /app/my_project/vendor/symfony/validator/Validator/TraceableValidator.php:58
 Symfony\Component\Validator\Validator\TraceableValidator->validate() at /app/my_project/src/Command/ValidateCommand.php:30

How to reproduce

Run the following commands to create a new project:

symfony new --webapp my_project
cd my_project
composer require doctrine/orm:2.20.3 -W
bin/console make:command app:validate

Update the ValidateCommand as follows:

<?php

namespace App\Command;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\PersistentCollection;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;

#[AsCommand(name: 'app:validate')]
class ValidateCommand extends Command
{
    public function __construct(
        private readonly ValidatorInterface     $validator,
        private readonly EntityManagerInterface $entityManager
    )
    {
        parent::__construct();
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $persistentCollection = new PersistentCollection($this->entityManager, new ClassMetadata(''), new ArrayCollection());

        $this->validator->validate($persistentCollection);

        return Command::SUCCESS;
    }
}

Run the following:

bin/console app:validate

And see this error:

In StringTypeResolver.php line 88:
                                                                                                                                                                                                                                                            
  Cannot resolve "array{cache?: array, cascade: array<string>, declared?: class-string, fetch: mixed, fieldName: string, id?: bool, inherited?: class-string, indexBy?: string, inversedBy: (string | null), isCascadeRemove: bool, isCascadePersist: bool  
  , isCascadeRefresh: bool, isCascadeMerge: bool, isCascadeDetach: bool, isOnDeleteCascade?: bool, isOwningSide: bool, joinColumns?: array<JoinColumnData>, joinColumnFieldNames?: array<string, string>, joinTable?: array, joinTableColumns?: list<mixed  
  >, mappedBy: (string | null), orderBy?: array, originalClass?: class-string, originalField?: string, orphanRemoval?: bool, relationToSourceKeyColumns?: array, relationToTargetKeyColumns?: array, sourceEntity: class-string, sourceToTargetKeyColumns?  
  : array<string, string>, targetEntity: class-string, targetToSourceKeyColumns?: array<string, string>, type: int, unique?: bool}".                                                                                                                        
                                                                                                                                                                                                                                                            

In StringTypeResolver.php line 295:
                                          
  Unhandled "JoinColumnData" identifier.                           

app:validate

Possible Solution

No response

Additional Context

No response

@udavka
Copy link

udavka commented Jun 1, 2025

  1. Bug 1: Symfony\Component\TypeInfo\TypeContext\TypeContextFactory::collectTypeAliases() scans locally defined types before imported types.
  2. Bug 2: both imported and locally defined types are not included as aliases into $typeContext, so any in-file dependencies are failed.
  3. An additional problem here is the order of type definitions. Even if all previously scanned type aliases were included in $typeContext, the import could still fail, since there is no requirement that all used types must be defined before use. For instance, this definition is valid, so a simple linear scan wouldn't work:
/**
 * @phpstan-type Type1 int
 * @phpstan-type Type2 array{var1: Type1, var2: Type3}
 * @phpstan-type Type3 int
 */
  1. Bug 3: all resolved types for imports are treated as ObjectType, although other types may be resolved. For example, for imports from classes that implement IteratorAggregate, the CollectionType is resolved. Thus, $this->createFromClassName($importedType->getClassName()) may fail in this case.

@chalasr
Copy link
Member

chalasr commented Jun 1, 2025

cc @mtarld

@simoheinonen
Copy link
Contributor

Pretty sure the issue I'm having is related to this too.

I upgraded Symfony from 7.2 to 7.3 and I'm getting similar error from API Platform when it's doing something with entities that are reusing imported PHPStan types.

https://github.com/simoheinonen/phpstan-bug/blob/master/src/Entity/FooInterface.php
https://github.com/simoheinonen/phpstan-bug/blob/master/src/Entity/FooEntity.php

Full reproducer: simoheinonen/phpstan-bug#2

I was able to narrow it down to this:

// src/Command/FooCommand.php
<?php

namespace App\Command;

use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\TypeInfo\TypeContext\TypeContextFactory;

#[AsCommand(name: 'foo')]
class FooCommand extends Command
{
    public function __construct(
        #[Autowire(service: 'type_info.type_context_factory')]
        private TypeContextFactory $typeContextFactory,
    )
    {
        parent::__construct();
    }


    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $this->typeContextFactory->createFromClassName(BarClass::class, BarClass::class);

        return Command::SUCCESS;
    }
}

/**
 * @phpstan-type FooType string
 */
class FooClass
{
}

/**
 * @phpstan-import-type FooType from FooClass
 * @phpstan-type BarType array{
 *      barKey: FooType
 *  }
 */
class BarClass
{
}

In StringTypeResolver.php line 88:

  [Symfony\Component\TypeInfo\Exception\UnsupportedException]
  Cannot resolve "array{barKey: FooType}".


Exception trace:
  at /Users/simo/Projects/simoheinonen/phpstan-bug/vendor/symfony/type-info/TypeResolver/StringTypeResolver.php:88
 Symfony\Component\TypeInfo\TypeResolver\StringTypeResolver->resolve() at /Users/simo/Projects/simoheinonen/phpstan-bug/vendor/symfony/type-info/TypeContext/TypeContextFactory.php:210
 Symfony\Component\TypeInfo\TypeContext\TypeContextFactory->collectTypeAliases() at /Users/simo/Projects/simoheinonen/phpstan-bug/vendor/symfony/type-info/TypeContext/TypeContextFactory.php:76
 Symfony\Component\TypeInfo\TypeContext\TypeContextFactory->createFromClassName() at /Users/simo/Projects/simoheinonen/phpstan-bug/src/Command/FooCommand.php:26
 App\Command\FooCommand->execute() at /Users/simo/Projects/simoheinonen/phpstan-bug/vendor/symfony/console/Command/Command.php:318
 Symfony\Component\Console\Command\Command->run() at /Users/simo/Projects/simoheinonen/phpstan-bug/vendor/symfony/console/Application.php:1092
 Symfony\Component\Console\Application->doRunCommand() at /Users/simo/Projects/simoheinonen/phpstan-bug/vendor/symfony/framework-bundle/Console/Application.php:123
 Symfony\Bundle\FrameworkBundle\Console\Application->doRunCommand() at /Users/simo/Projects/simoheinonen/phpstan-bug/vendor/symfony/console/Application.php:341
 Symfony\Component\Console\Application->doRun() at /Users/simo/Projects/simoheinonen/phpstan-bug/vendor/symfony/framework-bundle/Console/Application.php:77
 Symfony\Bundle\FrameworkBundle\Console\Application->doRun() at /Users/simo/Projects/simoheinonen/phpstan-bug/vendor/symfony/console/Application.php:192
 Symfony\Component\Console\Application->run() at /Users/simo/Projects/simoheinonen/phpstan-bug/vendor/symfony/runtime/Runner/Symfony/ConsoleApplicationRunner.php:49
 Symfony\Component\Runtime\Runner\Symfony\ConsoleApplicationRunner->run() at /Users/simo/Projects/simoheinonen/phpstan-bug/vendor/autoload_runtime.php:29
 require_once() at /Users/simo/Projects/simoheinonen/phpstan-bug/bin/console:15

In StringTypeResolver.php line 295:

  [DomainException]
  Unhandled "FooType" identifier.


Exception trace:
  at /Users/simo/Projects/simoheinonen/phpstan-bug/vendor/symfony/type-info/TypeResolver/StringTypeResolver.php:295
 Symfony\Component\TypeInfo\TypeResolver\StringTypeResolver->resolveCustomIdentifier() at /Users/simo/Projects/simoheinonen/phpstan-bug/vendor/symfony/type-info/TypeResolver/StringTypeResolver.php:183
 Symfony\Component\TypeInfo\TypeResolver\StringTypeResolver->getTypeFromNode() at /Users/simo/Projects/simoheinonen/phpstan-bug/vendor/symfony/type-info/TypeResolver/StringTypeResolver.php:108
 Symfony\Component\TypeInfo\TypeResolver\StringTypeResolver->getTypeFromNode() at /Users/simo/Projects/simoheinonen/phpstan-bug/vendor/symfony/type-info/TypeResolver/StringTypeResolver.php:86
 Symfony\Component\TypeInfo\TypeResolver\StringTypeResolver->resolve() at /Users/simo/Projects/simoheinonen/phpstan-bug/vendor/symfony/type-info/TypeContext/TypeContextFactory.php:210
 Symfony\Component\TypeInfo\TypeContext\TypeContextFactory->collectTypeAliases() at /Users/simo/Projects/simoheinonen/phpstan-bug/vendor/symfony/type-info/TypeContext/TypeContextFactory.php:76
 Symfony\Component\TypeInfo\TypeContext\TypeContextFactory->createFromClassName() at /Users/simo/Projects/simoheinonen/phpstan-bug/src/Command/FooCommand.php:26
 App\Command\FooCommand->execute() at /Users/simo/Projects/simoheinonen/phpstan-bug/vendor/symfony/console/Command/Command.php:318
 Symfony\Component\Console\Command\Command->run() at /Users/simo/Projects/simoheinonen/phpstan-bug/vendor/symfony/console/Application.php:1092
 Symfony\Component\Console\Application->doRunCommand() at /Users/simo/Projects/simoheinonen/phpstan-bug/vendor/symfony/framework-bundle/Console/Application.php:123
 Symfony\Bundle\FrameworkBundle\Console\Application->doRunCommand() at /Users/simo/Projects/simoheinonen/phpstan-bug/vendor/symfony/console/Application.php:341
 Symfony\Component\Console\Application->doRun() at /Users/simo/Projects/simoheinonen/phpstan-bug/vendor/symfony/framework-bundle/Console/Application.php:77
 Symfony\Bundle\FrameworkBundle\Console\Application->doRun() at /Users/simo/Projects/simoheinonen/phpstan-bug/vendor/symfony/console/Application.php:192
 Symfony\Component\Console\Application->run() at /Users/simo/Projects/simoheinonen/phpstan-bug/vendor/symfony/runtime/Runner/Symfony/ConsoleApplicationRunner.php:49
 Symfony\Component\Runtime\Runner\Symfony\ConsoleApplicationRunner->run() at /Users/simo/Projects/simoheinonen/phpstan-bug/vendor/autoload_runtime.php:29
 require_once() at /Users/simo/Projects/simoheinonen/phpstan-bug/bin/console:15


nicolas-grekas added a commit that referenced this issue Jun 4, 2025
This PR was merged into the 7.3 branch.

Discussion
----------

[TypeInfo] Fix type alias resolving

| Q             | A
| ------------- | ---
| Branch?       | 7.3
| Bug fix?      | yes
| New feature?  | no
| Deprecations? | no
| Issues        | Fixes #60598, fixes #60657
| License       | MIT

Take other type aliases into account when resolving type aliases, no matter the order they're defined.

Commits
-------

b778a80 [TypeInfo] Fix type alias resolving
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