Skip to content

[TypeInfo] PhpStan extractor causes failure when @type is combined with @template #61715

@Steveb-p

Description

@Steveb-p

Symfony version(s) affected

7.3.3

Description

We are using JMS Translation bundle to extract translations, which uses Validator to acquire assertion messages. This in turn causes PropertyInfo component to work and extract them from phpdoc's using new TypeInfo.

In our code we use @phpstan-type and @template for some classes. This recently caused our translation extraction to fail, and we investigated.

In TypeContextFactory.php line 275:
                                                         
  [Symfony\Component\TypeInfo\Exception\LogicException]  
  Cannot resolve "TIOHandlersMap" type alias.            
                                                         

Exception trace:
  at /home/stevebpn/PhpstormProjects/ibexa/5.0.x-dev-commerce/vendor/symfony/type-info/TypeContext/TypeContextFactory.php:275
 Symfony\Component\TypeInfo\TypeContext\TypeContextFactory->resolveTypeAliases() at /home/stevebpn/PhpstormProjects/ibexa/5.0.x-dev-commerce/vendor/symfony/type-info/TypeContext/TypeContextFactory.php:234
 Symfony\Component\TypeInfo\TypeContext\TypeContextFactory->collectTypeAliases() at /home/stevebpn/PhpstormProjects/ibexa/5.0.x-dev-commerce/vendor/symfony/type-info/TypeContext/TypeContextFactory.php:74
 Symfony\Component\TypeInfo\TypeContext\TypeContextFactory->createFromClassName() at /home/stevebpn/PhpstormProjects/ibexa/5.0.x-dev-commerce/vendor/symfony/property-info/Extractor/PhpStanExtractor.php:209
 Symfony\Component\PropertyInfo\Extractor\PhpStanExtractor->getType() at /home/stevebpn/PhpstormProjects/ibexa/5.0.x-dev-commerce/vendor/symfony/property-info/PropertyInfoExtractor.php:70
 Symfony\Component\PropertyInfo\PropertyInfoExtractor->getType() at /home/stevebpn/PhpstormProjects/ibexa/5.0.x-dev-commerce/vendor/symfony/validator/Mapping/Loader/PropertyInfoLoader.php:193
 Symfony\Component\Validator\Mapping\Loader\PropertyInfoLoader->getPropertyTypes() at /home/stevebpn/PhpstormProjects/ibexa/5.0.x-dev-commerce/vendor/symfony/validator/Mapping/Loader/PropertyInfoLoader.php:70
 Symfony\Component\Validator\Mapping\Loader\PropertyInfoLoader->loadClassMetadata() at /home/stevebpn/PhpstormProjects/ibexa/5.0.x-dev-commerce/vendor/symfony/validator/Mapping/Loader/LoaderChain.php:48
 Symfony\Component\Validator\Mapping\Loader\LoaderChain->loadClassMetadata() at /home/stevebpn/PhpstormProjects/ibexa/5.0.x-dev-commerce/vendor/symfony/validator/Mapping/Factory/LazyLoadingMetadataFactory.php:96
 Symfony\Component\Validator\Mapping\Factory\LazyLoadingMetadataFactory->getMetadataFor() at /home/stevebpn/PhpstormProjects/ibexa/5.0.x-dev-commerce/vendor/symfony/validator/Validator/RecursiveValidator.php:70
 Symfony\Component\Validator\Validator\RecursiveValidator->getMetadataFor() at /home/stevebpn/PhpstormProjects/ibexa/5.0.x-dev-commerce/vendor/symfony/validator/Validator/TraceableValidator.php:48
 Symfony\Component\Validator\Validator\TraceableValidator->getMetadataFor() at /home/stevebpn/PhpstormProjects/ibexa/5.0.x-dev-commerce/vendor/jms/translation-bundle/Translation/Extractor/File/ValidationExtractor.php:87
 JMS\TranslationBundle\Translation\Extractor\File\ValidationExtractor->enterNode() at /home/stevebpn/PhpstormProjects/ibexa/5.0.x-dev-commerce/vendor/nikic/php-parser/lib/PhpParser/NodeTraverser.php:196
 PhpParser\NodeTraverser->traverseArray() at /home/stevebpn/PhpstormProjects/ibexa/5.0.x-dev-commerce/vendor/nikic/php-parser/lib/PhpParser/NodeTraverser.php:98
 PhpParser\NodeTraverser->traverseNode() at /home/stevebpn/PhpstormProjects/ibexa/5.0.x-dev-commerce/vendor/nikic/php-parser/lib/PhpParser/NodeTraverser.php:227
 PhpParser\NodeTraverser->traverseArray() at /home/stevebpn/PhpstormProjects/ibexa/5.0.x-dev-commerce/vendor/nikic/php-parser/lib/PhpParser/NodeTraverser.php:76
 PhpParser\NodeTraverser->traverse() at /home/stevebpn/PhpstormProjects/ibexa/5.0.x-dev-commerce/vendor/jms/translation-bundle/Translation/Extractor/File/ValidationExtractor.php:109
 JMS\TranslationBundle\Translation\Extractor\File\ValidationExtractor->visitPhpFile() at n/a:n/a

☝️ as you can see, TypeInfo is fed information from PhpStanExtractor. With a specific setup this causes issues.

How to reproduce

To make the issue show up, set up a new Symfony project and install additional packages to trigger PhpStanExtractor to become available for validation metadata.

symfony new test-validator-metadata
symfony composer require validator symfony/property-info phpdocumentor/type-resolver phpstan/phpdoc-parser 

Then create a following class to contain phpdoc-based metadata:

<?php
# src/Test/Collection.php

namespace App\Test;

use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;

/**
 * @template TCollectionType of object
 *
 * @phpstan-type TCollectionTypeMap array<string, TCollectionType>
 */
final class Collection
{
    /**
     * Map of a handler id to a handler service instance.
     *
     * @phpstan-var TCollectionTypeMap
     */
    private array $handlersMap = [];

    /**
     * @phpstan-param TCollectionTypeMap $handlersMap
     */
    public function setHandlersMap(array $handlersMap): void
    {
        $this->handlersMap = $handlersMap;
    }

    /**
     * @phpstan-return TCollectionType
     */
    public function getConfiguredHandler(string $handlerName): object
    {
        if (!isset($this->handlersMap[$handlerName])) {
            throw new InvalidConfigurationException("Unknown handler $handlerName");
        }

        return $this->handlersMap[$handlerName];
    }
}

To trigger the issue I've used a following controller:

<?php
# src/Controller/TestController.php

declare(strict_types=1);

namespace App\Controller;

use App\Test\Collection;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Validator\Mapping\Factory\MetadataFactoryInterface;

final class TestController extends AbstractController
{
    public function __construct(
        #[Autowire(service: 'validator.mapping.class_metadata_factory')]
        private readonly MetadataFactoryInterface $metadataFactory,
    ) {
    }

    #[Route('/test')]
    public function __invoke(): Response
    {
        $this->metadataFactory->getMetadataFor(Collection::class);

        return new Response();
    }
}

Calling this controller will result in an exception when trying to read metadata for Collection:

Image

With the following stacktrace from the innermost exception:

DomainException:
Unhandled "TCollectionType" identifier.

  at vendor/symfony/type-info/TypeResolver/StringTypeResolver.php:342
  at Symfony\Component\TypeInfo\TypeResolver\StringTypeResolver->resolveCustomIdentifier()
     (vendor/symfony/type-info/TypeResolver/StringTypeResolver.php:208)
  at Symfony\Component\TypeInfo\TypeResolver\StringTypeResolver->getTypeFromNode()
     (vendor/symfony/type-info/TypeResolver/StringTypeResolver.php:252)
  at Symfony\Component\TypeInfo\TypeResolver\StringTypeResolver->Symfony\Component\TypeInfo\TypeResolver\{closure}()
  at array_map()
     (vendor/symfony/type-info/TypeResolver/StringTypeResolver.php:252)
  at Symfony\Component\TypeInfo\TypeResolver\StringTypeResolver->getTypeFromNode()
     (vendor/symfony/type-info/TypeResolver/StringTypeResolver.php:88)
  at Symfony\Component\TypeInfo\TypeResolver\StringTypeResolver->resolve()
     (vendor/symfony/type-info/TypeContext/TypeContextFactory.php:262)
  at Symfony\Component\TypeInfo\TypeContext\TypeContextFactory->resolveTypeAliases()
     (vendor/symfony/type-info/TypeContext/TypeContextFactory.php:232)
  at Symfony\Component\TypeInfo\TypeContext\TypeContextFactory->collectTypeAliases()
     (vendor/symfony/type-info/TypeContext/TypeContextFactory.php:74)
  at Symfony\Component\TypeInfo\TypeContext\TypeContextFactory->createFromClassName()
     (vendor/symfony/property-info/Extractor/PhpStanExtractor.php:209)
  at Symfony\Component\PropertyInfo\Extractor\PhpStanExtractor->getType()
     (vendor/symfony/property-info/PropertyInfoExtractor.php:70)
  at Symfony\Component\PropertyInfo\PropertyInfoExtractor->getType()
     (vendor/symfony/validator/Mapping/Loader/PropertyInfoLoader.php:193)
  at Symfony\Component\Validator\Mapping\Loader\PropertyInfoLoader->getPropertyTypes()
     (vendor/symfony/validator/Mapping/Loader/PropertyInfoLoader.php:70)
  at Symfony\Component\Validator\Mapping\Loader\PropertyInfoLoader->loadClassMetadata()
     (vendor/symfony/validator/Mapping/Loader/LoaderChain.php:48)
  at Symfony\Component\Validator\Mapping\Loader\LoaderChain->loadClassMetadata()
     (vendor/symfony/validator/Mapping/Factory/LazyLoadingMetadataFactory.php:96)
  at Symfony\Component\Validator\Mapping\Factory\LazyLoadingMetadataFactory->getMetadataFor()
     (vendor/symfony/validator/Validator/RecursiveValidator.php:70)
  at Symfony\Component\Validator\Validator\RecursiveValidator->getMetadataFor()
     (src/Controller/TestController.php:25)
  at App\Controller\TestController->__invoke()
     (vendor/symfony/http-kernel/HttpKernel.php:183)
  at Symfony\Component\HttpKernel\HttpKernel->handleRaw()
     (vendor/symfony/http-kernel/HttpKernel.php:76)
  at Symfony\Component\HttpKernel\HttpKernel->handle()
     (vendor/symfony/http-kernel/Kernel.php:182)
  at Symfony\Component\HttpKernel\Kernel->handle()
     (vendor/symfony/runtime/Runner/Symfony/HttpKernelRunner.php:35)
  at Symfony\Component\Runtime\Runner\Symfony\HttpKernelRunner->run()
     (vendor/autoload_runtime.php:29)
  at require_once('/home/stevebpn/PhpstormProjects/ibexa/test-validator-metadata/vendor/autoload_runtime.php')
     (public/index.php:5)                

Possible Solution

It seems that Symfony\Component\TypeInfo\TypeContext\TypeContextFactory::resolveTypeAliases() is unaware of any @template declarations that might be present in the same file (extracted by Symfony\Component\TypeInfo\TypeContext\TypeContextFactory::collectTemplates()). It might be possible that types could be properly resolved if resolveTypeAliases was made aware of @template types).

Additional Context

No response

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