Skip to content

[PropertyInfo] Bug/BC break? - Getter return type seems to have higher priority than property type #38416

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
rogamoore opened this issue Oct 5, 2020 · 15 comments

Comments

@rogamoore
Copy link
Contributor

rogamoore commented Oct 5, 2020

Symfony version(s) affected: 5.1.6, 5.1.7

Description
Hello!

In one of our projects we have a class similar to this:

final class ExampleDTO
{
    private ?string $example;

    public function __construct(?string $example)
    {
        $this->example = $example;
    }

    public function getExample(): string
    {
        return $this->example ?? 'fallback';
    }
}

It's used in the context of api platform, where incoming data is being deserialized to this object.

Before with Symfony 5.1.5 the example property was detected as type ?string, after upgrading to 5.1.6/5.1.7 it's only detected as type string. Now when the incoming data contains a null value, it will fail with The type of the "example" attribute must be "string", "NULL" given.

I did some tests and was able to track this new behaviour down to the property-info component. I suspect this change to be the reason:
#38041

It looks like now the return type of getExample() is used instead of the property type hint (?string).

So I would like to ask if this is the expected behaviour and if yes, how can I make the old way work again (somehow influence the priorities?). Thanks in advance!

How to reproduce
I created a reproducer here:
https://github.com/rogamoore/property-info-bug

Steps:

^ array:1 [▼
  0 => Symfony\Component\PropertyInfo\Type {#116 ▼
    -builtinType: "string"
    -nullable: true
    -class: null
    -collection: false
    -collectionKeyType: null
    -collectionValueType: null
  }
]
^ array:1 [▼
  0 => Symfony\Component\PropertyInfo\Type {#116 ▼
    -builtinType: "string"
    -nullable: false
    -class: null
    -collection: false
    -collectionKeyType: null
    -collectionValueType: null
  }
]

Additional context
PHP 7.4.10

@dragosprotung
Copy link
Contributor

Because reflection was moved down into the list of guessers all others take precedence not only the getter

@rogamoore
Copy link
Contributor Author

Anything that can be done here? Was the breaking change intentional? Or could we revert the behaviour?

@dpinheiro
Copy link

Any news on this one? I'm also having the same problem. Was this change intentional?

@stof
Copy link
Member

stof commented Nov 25, 2020

I guess this is related to different expectations. The constructor argument is nullable, but the getter is not. Having one type info used for both is what causes the issue.

@rogamoore
Copy link
Contributor Author

@stof But would you agree, that the behaviour should be somehow stable/predictable and not change between minor releases? Looks like there are no tests covering this, otherwise they would have failed when changing it in #38041

Regarding expectations - as this component is called property-info I would argue that the actual type of the class property should come first and any other hints as constructor/getter/setter should be used just as a fallback.

@digilist
Copy link
Contributor

digilist commented Jan 28, 2021

I stumbled upon something similar that is also related to this issue: The PhpDocExtractor has a higher priority than the ReflectionExtractor (at least in the default symfony distribution) and when using the example code from the documentation.
This means, that the type definition through the docblock has a higher priority than the accessor which has a higher priority than the property type. In the overall context, this seems quite confusing then as well.

To clarify this, consider this example (based on the example from @rogamoore):

final class ExampleDTO
{
    /** @var string|null **/
    private ?string $example;

    public function getExample(): string
    {
        return $this->example ?? 'fallback';
    }
}

$propertyInfoExtractor->getTypes(ExampleDTO::class, 'example')

This will now return the nullable string type. If you remove the docblock, it's a non-nullable string again.

@dpinheiro
Copy link

dpinheiro commented Mar 18, 2021

Another situation where this seems to be a problem. Let's say you have a DTO like the following, that to me seems to be a perfect normal use case.

final class Product
{
    private string $currency;

    public function __construct(int $amount, string $currency)
    {
        $this->currency = $currency;
    }

    public function getCurrency(): Currency
    {
        return Currency::fromString($this->currency);
    }
}

final class Currency
{
    private string $currency;

    private function __construct(string $currency)
    {
        $this->currency = $currency;
    }

    public static function fromString(string $currency): self
    {
        return new self($currency);
    }
}

With this last change to the property accessor, if you try to deserialize something into this class there will be an error because it tries to create a Currency instance

@dpinheiro
Copy link

I think I may have found a workaround for this

I discovered this PR #30335 (available for Symfony 5.2) and although there's no documentation yet for it this could be used to give priority to extract the types from constructor, internally this new ConstructorExtractor will call the method getTypesFromConstructor of the other extractors. To use it we just have to declare it has a service and add the proper the tag, something like this:

Symfony\Component\PropertyInfo\Extractor\ConstructorExtractor:
        arguments:
            - { param1: "@property_info.php_doc_extractor", param2: "@property_info.reflection_extractor" }
        tags:
            - { name: "property_info.type_extractor", priority: -999 }

@carsonbot
Copy link

Hey, thanks for your report!
There has not been a lot of activity here for a while. Is this bug still relevant? Have you managed to find a workaround?

@carsonbot
Copy link

Friendly reminder that this issue exists. If I don't hear anything I'll close this.

@digilist
Copy link
Contributor

We have worked around this problem, but I think the issue in general is still valid.

@carsonbot carsonbot removed the Stalled label Nov 29, 2021
@carsonbot
Copy link

Hey, thanks for your report!
There has not been a lot of activity here for a while. Is this bug still relevant? Have you managed to find a workaround?

@carsonbot
Copy link

Could I get an answer? If I do not hear anything I will assume this issue is resolved or abandoned. Please get back to me <3

@carsonbot
Copy link

Hey,

I didn't hear anything so I'm going to close it. Feel free to comment if this is still relevant, I can always reopen!

@IvoPereira
Copy link

In Symfony 6.3 this weird behaviour seems to keep happening.

Is this something that is intended to be fixed or should we rather use a workaround?

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