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

Skip to content

[AbstractObjectNormalizer][PropertyInfo] AbstractObjectNormalizer/PropertyInfoExtractor does not respect constructor type hinting. #30053

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
gusarov112 opened this issue Jan 31, 2019 · 6 comments

Comments

@gusarov112
Copy link

Symfony version(s) affected: v4.2.2 v4.1.11 v4.1.10 v3.4.21

Description
Commit 8741d00

  • \Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer::denormalizeParameter()
  • calls $this->propertyTypeExtractor->getTypes()
  • from \Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface::getTypes()
  • whitch iterates over registered extractors and tries to extract property type.

PropertyInfoExtractor has extractors:

0 => \Symfony\Bridge\Doctrine\PropertyInfo\DoctrineExtractor,
1 => \Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor, 
2 => \Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor

Last two extractors are registered here:
vendor/symfony/framework-bundle/Resources/config/property_info.xml
and here:
\Symfony\Bundle\FrameworkBundle\DependencyInjection\FrameworkExtension::registerPropertyInfoConfiguration
and are tagged as property_info.type_extractor with priorities PhpDocExtractor[-1001], ReflectionExtractor[-1002]

The problem is that PropertyInfoExtractor does not respect constructor type hinting, but this is wrong.
Argument types from constructor are the only types that are valid for the class instantiation. And neither priority of the extractors nor other magic should affect this.

How to reproduce

<?php declare(strict_types=1);

namespace App\Command;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

class Test extends Command
{
    /** @var ContainerInterface */
    private $container;
    public function __construct(ContainerInterface $container)
    {
        $this->container = $container;
        parent::__construct('t:t');
    }
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $serializer = $this->container->get('serializer');
        $obj = $serializer->denormalize(['uuid' => '74a6b770-2ba0-4b35-ae7f-4272ce5d613a'], DTO::class);
        var_dump($obj);
    }
}
interface UuidInterface {}

class DTO {
    /** @var UuidInterface */
    private $uuid;

    public function __construct(string $uuid)
    {
        $this->uuid = new class($uuid) implements UuidInterface {
            private $prop;
            public function __construct($uuid)
            {
                $this->prop = $uuid;
            }
        };
    }
}

Possible Solution
Respect constructor type hinting

Additional context
ErrorMessage:
The type of the "uuid" attribute for class "App\Command\DTO" must be one of "App\Command\UuidInterface" ("string" given).
Stack trace:

PropertyInfoExtractor.php:109, Symfony\Component\PropertyInfo\PropertyInfoExtractor->extract()
PropertyInfoExtractor.php:74, Symfony\Component\PropertyInfo\PropertyInfoExtractor->getTypes()
AbstractObjectNormalizer.php:383, Symfony\Component\Serializer\Normalizer\ObjectNormalizer->denormalizeParameter()
AbstractNormalizer.php:418, Symfony\Component\Serializer\Normalizer\ObjectNormalizer->instantiateObject()
AbstractObjectNormalizer.php:157, Symfony\Component\Serializer\Normalizer\ObjectNormalizer->instantiateObject()
AbstractObjectNormalizer.php:262, Symfony\Component\Serializer\Normalizer\ObjectNormalizer->denormalize()
Serializer.php:191, Symfony\Component\Serializer\Serializer->denormalize()
Serializer.php:142, Symfony\Component\Serializer\Serializer->deserialize()
SymfonySerializerAdapter.php:49, FOS\RestBundle\Serializer\SymfonySerializerAdapter->deserialize()
RequestBodyParamConverter.php:96, FOS\RestBundle\Request\RequestBodyParamConverter->apply()
ParamConverterManager.php:85, Sensio\Bundle\FrameworkExtraBundle\Request\ParamConverter\ParamConverterManager->applyConverter()
ParamConverterManager.php:48, Sensio\Bundle\FrameworkExtraBundle\Request\ParamConverter\ParamConverterManager->apply()
ParamConverterListener.php:78, Sensio\Bundle\FrameworkExtraBundle\EventListener\ParamConverterListener->onKernelController()
WrappedListener.php:111, Symfony\Component\EventDispatcher\Debug\WrappedListener->__invoke()
EventDispatcher.php:212, Symfony\Component\EventDispatcher\EventDispatcher->doDispatch()
EventDispatcher.php:44, Symfony\Component\EventDispatcher\EventDispatcher->dispatch()
TraceableEventDispatcher.php:145, Symfony\Component\HttpKernel\Debug\TraceableEventDispatcher->dispatch()
HttpKernel.php:138, Symfony\Component\HttpKernel\HttpKernel->handleRaw()
HttpKernel.php:67, Symfony\Component\HttpKernel\HttpKernel->handle()
Kernel.php:198, App\Kernel->handle()
index.php:37, {main}()
gusarov112 referenced this issue Jan 31, 2019
@nicolas-grekas
Copy link
Member

@karser could you have a look please?

@karser
Copy link
Contributor

karser commented Jan 31, 2019

Hi folks!

Since ReflectionExtractor takes into account the constructor types (unlike PhpDocExtractor), so you can override ReflectionExtractor definition and increase its priority. @gusarov112 Just place this workaround to your services.yaml. For me, it fixes your issue.

property_info.reflection_extractor:
    class: Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor
    tags:
        - { name: 'property_info.list_extractor', priority: '-1000' }
        - { name: 'property_info.type_extractor', priority: '-1000' } #this used to be -1002
        - { name: 'property_info.access_extractor', priority: '-1000' }
        - { name: 'property_info.initializable_extractor', priority: '-1000' }

Argument types from constructor are the only types that are valid for the class instantiation
It sounds logical but it's weird that the respected constructor argument has type other than the property's one.
@xabbuh @dunglas Should we change it in Symfony? So ReflectionExtractor would have higher priority than PhpDocExtractor. It would be a BC though.

@dunglas
Copy link
Member

dunglas commented Feb 1, 2019

Can we patch PhpDocExtractor to extract the PHPDoc of the constructor argument?
We should use the type of the publicly exposed type when available, here we use the internal one.

@sfortop
Copy link

sfortop commented Feb 1, 2019

It sounds logical but it's weird that the respected constructor argument has type other than the property's one.

It's typical solution when you has type conversion at constructor
e.g our constructor got date as unixtime int, or string but store both as DateTime own property

so you can override ReflectionExtractor definition and increase its priority

Bad idea, because you will get huge BC risk

@karser
Copy link
Contributor

karser commented Feb 1, 2019

I see. I'll try to patch PhpDocExtractor and submit a PR soon.

@karser
Copy link
Contributor

karser commented Feb 9, 2019

@gusarov112 @sfortop Your case is a constructor with a string argument, a private property without getter/setter.
What if it had a getter of UuidInterface type? Or even public property? Here are examples for this question
#30056 (comment)

@xabbuh xabbuh added Feature and removed Bug labels Feb 21, 2019
@fabpot fabpot closed this as completed Aug 26, 2020
fabpot added a commit that referenced this issue Aug 26, 2020
…riority than PhpDocExtractor and ReflectionExtractor (karser)

This PR was merged into the 5.2-dev branch.

Discussion
----------

[PropertyInfo] ConstructorExtractor which has higher priority than PhpDocExtractor and ReflectionExtractor

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | yes
| New feature?  | yes
| BC breaks?    | hopefully no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | #30053
| License       | MIT

Supersedes #30056 #30128

In short, when using PhpDocExtractor, it ignores the constructor argument type, although `argument types from the constructor are the only types that are valid for the class instantiation`.

This PR adds a separate extractor - `ConstructorExtractor` which is called first (-999) and it attempts to extract the type from constructor only, either from PhpDoc or using reflection.
I added `getTypesFromConstructor` to `PhpDocExtractor` and `ReflectionExtractor` - they implement `ConstructorArgumentTypeExtractorInterface` interface. `ConstructorExtractor` aggregates those extractors using compiler pass.

So the flow of control looks like this:
```
PropertyInfoExtractor::getTypes:
    - ConstructorExtractor::getTypes
        - PhpDocExtractor::getTypesFromConstructor
        - ReflectionExtractor::getTypesFromConstructor
    - PhpDocExtractor::getTypes
    - ReflectionExtractor::getTypes
```

Commits
-------

5049e25 Added ConstructorExtractor which has higher priority than PhpDocExtractor and ReflectionExtractor
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

8 participants