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

Skip to content

[HttpKernel] Conversion of type in #[MapQueryString] constructor is no longer accepted since Symfony 7.1.10 #59353

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

Open
ostrolucky opened this issue Jan 3, 2025 · 5 comments

Comments

@ostrolucky
Copy link
Contributor

ostrolucky commented Jan 3, 2025

Symfony version(s) affected

7.1.10

Description

Since #59134, DTO that changes type in a constructor is no longer accepted.

Trigger of this seem to be that in a given PR, disable_type_enforcement no longer defaults to true. However, this might be a deeper issue, as thanks to type juggling in constructor there should not be any type mismatch in the first place. Notice that in reproducer, although (private) property is declared to be an array, constructor accepts string. Hence, denormalization should accept string.

I bet this is another issue of a kind in #46918, since serializer has no way to distinguish class property named same way as constructor parameter

How to reproduce

Query DTO:

readonly class ScimQueryInput
{
    /** @var list<string> */
    private array $attributes;

    public function __construct(string $attributes = '') {
        $this->attributes = $attributes ? explode(',', $attributes) : [];
    }

    public function isAttributeAllowed(string $attribute): bool
    {
        return !\in_array($attribute, $this->attributes, true);
    }
}

Controller:

class GetUsersController
{
    #[Route('/api/datasync/scim/{connectorId}/Users')]
    public function __invoke(#[MapQueryString] ScimQueryInput $queryInput = new ScimQueryInput()): Response 
    {
        return new Response();
    }
}

Call: /api/datasync/scim/Users?attributes=displayName,userName

Response:

      Current response status code is 404, but 200 expected. (Behat\Mink\Exception\ExpectationException)
    │
    │  {
    │      "type": "https://symfony.com/errors/validation",
    │      "title": "Validation Failed",
    │      "detail": "attributes: This value should be of type array.",
    │      "violations": [
    │          {
    │              "propertyPath": "attributes",
    │              "title": "This value should be of type array.",
    │              "template": "This value should be of type {{ type }}.",
    │              "parameters": {
    │                  "{{ type }}": "array"
    │              }
    │          },
    │      ]
    │  }

Possible Solution

No response

Additional Context

No response

@fashxp
Copy link

fashxp commented Jan 3, 2025

we came across the same problem at Pimcore

@ovidiuenache
Copy link
Contributor

ovidiuenache commented Jan 5, 2025

@ostrolucky

I installed a fresh Symfony 7.1.10 locally and required symfony/serializer-pack.

My composer.json file looks like this:

"require": {
    "php": ">=8.2",
    "ext-ctype": "*",
    "ext-iconv": "*",
    "phpdocumentor/reflection-docblock": "^5.6",
    "phpstan/phpdoc-parser": "^2.0",
    "symfony/console": "7.1.*",
    "symfony/dotenv": "7.1.*",
    "symfony/flex": "^2",
    "symfony/framework-bundle": "7.1.*",
    "symfony/property-access": "7.1.*",
    "symfony/property-info": "7.1.*",
    "symfony/runtime": "7.1.*",
    "symfony/serializer": "7.1.*",
    "symfony/yaml": "7.1.*"
}

The reason why you get a type mismatch here is because eventually, the code execution ends up in the extract method of the Symfony\Component\PropertyInfo\PropertyInfoExtractor to fetch the type of the attributes property. Here, we have the following $extractors in this order:

  1. Symfony\Component\PropertyInfo\Extractor\PhpStanExtractor;
  2. Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
  3. Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor.

You can check the order by running bin/console debug:container --tag=property_info.type_extractor:

Service ID                           Priority                                                   
 ------------------------------------ ----------
  property_info.phpstan_extractor      -1000     
  property_info.php_doc_extractor      -1001    
  property_info.reflection_extractor   -1002

Since you are using the @var list<string> annotation for your property it means that the Symfony\Component\PropertyInfo\Extractor\PhpStanExtractor class can extract its type (array) and the remaining extractors are ignored. Later, the array type returned here is compared to the given string type. Since we no longer disable type enforcement by default we get the error you mentioned.

If we remove the annotation then we get the following behavior:

  1. Symfony\Component\PropertyInfo\Extractor\PhpStanExtractor returns null;
  2. Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor returns null;
  3. Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor returns string as the property type because it infers it from the constructor (see the getType method in Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor).

Depending on your context, the solutions are the following:

  • manually set disable_type_enforcement to true when using MapQueryString in the controller;
  • provide the query parameter as an array: ?attributes[]=displayName&attributes[]=userName;
  • remove the @var annotation from the attributes property;
  • disable the PhpStanExtractor or change the priorities but I am not sure what is the impact of such a change and I wouldn't do it unless I am fully informed about the consequences;
  • some other options that I might have missed. However, I don't see an issue in the current behavior given that in the context you described we are enforcing types, we specify that the property is an array and then provide a string via the query parameter.

@ostrolucky
Copy link
Contributor Author

Thanks for detailed explanation!

I don't see an issue in the current behavior given that in the context you described we are enforcing types, we specify that the property is an array and then provide a string via the query parameter.

Well issue I see is that for successful denormalization, attributes query parameter has to match type in constructor, not a type of class property. From what you wrote, it seems PhpStanExtractor incorrectly extracts type of class property instead of class element that's actually going to be used for denormalization (constructor parameter). If it did extract type of constructor parameter, validator would realize that it is string, so same as query parameter.

@ovidiuenache
Copy link
Contributor

ovidiuenache commented Jan 6, 2025

Indeed. The Symfony\Component\PropertyInfo\Extractor\PhpStanExtractor checks the doc block of the property or of the accessor or mutator methods related to the property. It does not look into the constructor. The Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor class does. I can't really tell whether this is an issue or intended behavior.

Maybe @Korbeil can shed some light here.

@andyexeter
Copy link
Contributor

We're seeing the same issue in 6.4. Our workaround for now has been to rename the class property so it doesn't have the same name as the constructor argument, e.g.:

readonly class ScimQueryInput
{
    /** @var list<string> */
    private array $attributesArray;

    public function __construct(string $attributes = '') {
        $this->attributesArray = $attributes ? explode(',', $attributes) : [];
    }

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

5 participants