Skip to content

[HttpKernel] Create Attributes #[MapRequestPayload] and #[MapQueryString] to map Request input to typed objects#49138

Merged
nicolas-grekas merged 1 commit intosymfony:6.3from
Koc:feature/mapped-request
Apr 14, 2023
Merged

[HttpKernel] Create Attributes #[MapRequestPayload] and #[MapQueryString] to map Request input to typed objects#49138
nicolas-grekas merged 1 commit intosymfony:6.3from
Koc:feature/mapped-request

Conversation

@Koc
Copy link
Copy Markdown
Contributor

@Koc Koc commented Jan 27, 2023

Q A
Branch? 6.3
Bug fix? no
New feature? yes
Deprecations? no
Tickets #36037, #36093, #45628, #47425, #49002, #49134
License MIT
Doc PR TBD

Yet another variation of how we can map raw Request data to typed objects with validation. We can even build OpenApi Specification based on this DTO classes using NelmioApiDocBundle.

Usage Example 🔧

#[MapRequestPayload]

class PostProductReviewPayload
{
    public function __construct(
        #[Assert\NotBlank]
        #[Assert\Length(min: 10, max: 500)]
        public readonly string $comment,
        #[Assert\GreaterThanOrEqual(1)]
        #[Assert\LessThanOrEqual(5)]
        public readonly int $rating,
    ) {
    }
}

class PostJsonApiController
{
    public function __invoke(
        #[MapRequestPayload] PostProductReviewPayload $payload,
    ): Response {
        // $payload is validated and fully typed representation of the request body $request->getContent() 
        //  or $request->request->all()
    }
}

#[MapQueryString]

class GetOrdersQuery
{
    public function __construct(
        #[Assert\Valid]
        public readonly ?GetOrdersFilter $filter,
        #[Assert\LessThanOrEqual(500)]
        public readonly int $limit = 25,
        #[Assert\LessThanOrEqual(10_000)]
        public readonly int $offset = 0,
    ) {
    }
}

class GetOrdersFilter
{
    public function __construct(
        #[Assert\Choice(['placed', 'shipped', 'delivered'])]
        public readonly ?string $status,
        public readonly ?float $total,
    ) {
    }
}

class GetApiController
{
    public function __invoke(
        #[MapQueryString] GetOrdersQuery $query,
    ): Response {
        // $query is validated and fully typed representation of the query string $request->query->all()
    }
}

Exception handling 💥

  • Validation errors will result in an HTTP 422 response, accompanied by a serialized ConstraintViolationList.
  • Malformed data will be responded to with an HTTP 400 error.
  • Unsupported deserialization formats will be responded to with an HTTP 415 error.

Comparison to another implementations 📑

Differences to #49002:

  • separate Attributes for explicit definition of the used source
  • no need to define which class use to map because Attributes already associated with typed argument
  • used ArgumentValueResolver mechanism instead of Subscribers
  • functional tests

Differences to #49134:

  • it is possible to map whole query string to object, not per parameter
  • there is validation of the mapped object
  • supports both $request->getContent() and $request->request->all() mapping
  • functional tests

Differences to #45628:

  • separate Attributes for explicit definition of the used source
  • supports $request->request->all() and $request->query->all() mapping
  • Exception handling opt-in
  • functional tests

Bonus part 🎁

  • Extracted UnsupportedFormatException which thrown when there is no decoder for a given format

@carsonbot carsonbot added this to the 6.3 milestone Jan 27, 2023
@Koc Koc force-pushed the feature/mapped-request branch 5 times, most recently from 87eb66c to a08be6b Compare January 27, 2023 21:29
@Koc Koc changed the title Create Attributes to map Query String and Request Content to typed objects [HttpKernel] Create Attributes #[MapQueryString and #[MapRequestContent] to map Request input to typed objects Jan 27, 2023
@Koc Koc changed the title [HttpKernel] Create Attributes #[MapQueryString and #[MapRequestContent] to map Request input to typed objects [HttpKernel] Create Attributes #[MapQueryString] and #[MapRequestContent] to map Request input to typed objects Jan 27, 2023
@Koc Koc force-pushed the feature/mapped-request branch 4 times, most recently from 078781e to b3bbe43 Compare January 27, 2023 22:08
@OskarStark
Copy link
Copy Markdown
Contributor

It looks like your committer email is not associated with your Github account

@ruudk
Copy link
Copy Markdown
Contributor

ruudk commented Jan 28, 2023

Was this a coincidence that we both did something similar or did you create this afterwards?

@Koc Koc force-pushed the feature/mapped-request branch from b3bbe43 to 4e8cb8e Compare January 28, 2023 09:38
@Koc
Copy link
Copy Markdown
Contributor Author

Koc commented Jan 28, 2023

@OskarStark thanx, fixed now, please check

@ruudk it is similar but has few differences comparing to your implementation. I've updated PR description to highlight the differences.

BTW anybody know how to fix unrelated fabbot failure?

@ro0NL
Copy link
Copy Markdown
Contributor

ro0NL commented Jan 28, 2023

this looks promising :)

there is validation of the mapped object

what about deserializing errors vs. ValidationFailedException? Does either produce a 400 bad request? is only the latter i18n friendly?

@Koc
Copy link
Copy Markdown
Contributor Author

Koc commented Jan 28, 2023

My idea was start from something like simple implementation in this PR and improve it later before 6.3 release.

  1. Add generic Exception Handler which will catch ValidationFailedException and convert it to http 400 with serialized Constraint Violations. We already have Normalizer for it but anyway it requires some effort.
  2. Handle PartialDenormalizationException in this two Argument Value Resolvers and convert it to ValidationFailedException. We need somehow translate messages about missmatched types

@Koc Koc force-pushed the feature/mapped-request branch from 4e8cb8e to 1b619a2 Compare January 28, 2023 18:40
@ro0NL
Copy link
Copy Markdown
Contributor

ro0NL commented Jan 28, 2023

i like a simple implementation as first iteration, but i like http/i18n compatibility more ;)

@Koc Koc force-pushed the feature/mapped-request branch 3 times, most recently from 19069b1 to 052f625 Compare January 28, 2023 21:35
@y4roc
Copy link
Copy Markdown

y4roc commented Jan 30, 2023

Your PR also makes this one #47425 obsolete.

@Koc Koc force-pushed the feature/mapped-request branch from 052f625 to 25e7d70 Compare January 30, 2023 10:31
@y4roc
Copy link
Copy Markdown

y4roc commented Jan 30, 2023

I would use a separate exception, which builds on the ValidationFailedException. So that your listener does not filter out other ValidationFailedException.

@Koc Koc force-pushed the feature/mapped-request branch 2 times, most recently from cf31004 to a2b5f96 Compare January 30, 2023 11:29
@faizanakram99
Copy link
Copy Markdown
Contributor

Is the naming of attributes final, MapRequestPayload seems a bit mouthful.

What about names suggested here (FromQuery, etc) #49138 (comment)

@renanbr
Copy link
Copy Markdown

renanbr commented Apr 7, 2023

Landing late to the discussion :hi:

This PR overlaps some work I participate currently, so here are some feedbacks:

  • I would rename MapRequestPayload to MapRequestBody because it targets specifically the content (not uploaded files e.g.)
  • It would be great if users could configure the format they want to receive data
    • I didn't get why the proposal uses ->getRequestFormat() to detect the format and not the content-type header
    • I think the resolver should throw a proper exception for any unexpected format
  • It would be great if users could pick the validation group they want to apply, or even disable it if they want
  • It would be great if users could define presets (serialization format + serialization context + validation groups) and let users use them through their endpoints

Most of these ideas are implemented in https://github.com/acsiomatic/http-payload-bundle (ping @renedelima)

All of them can be done in further PR.

I think the current PR presents no blockers regarding these ideas in this comment.

@nicolas-grekas
Copy link
Copy Markdown
Member

nicolas-grekas commented Apr 7, 2023

What about names suggested here (FromQuery, etc)

We have a convention to help discovery that controller-related attributes should start with Map. I submitted symfony/symfony-docs#18176 to clarify this.

rename MapRequestPayload to MapRequestBody

"body" is not a word we use in the request object so this would be MapRequestContent. It's an open topic no my side. "payload" is also new vocabulary so we might prefer "content", which is a term we use already.

targets specifically the content (not uploaded files e.g.)

Which leads to the idea of adding #[MapUploadedFile] ;)

It would be great if users could define presets

My suggestion for this: make the attribute non final. Then a preset is just a child attribute. We do something similar for DI-attributes and it works great.

I left your other questions open for someone else :)

@Koc
Copy link
Copy Markdown
Contributor Author

Koc commented Apr 7, 2023

Mates, can we finally agree on naming, please? 🙏 I've renamed it few times already: MapRequestContent -> MapRequestBody -> MapRequestPayload

image

image

@Koc
Copy link
Copy Markdown
Contributor Author

Koc commented Apr 7, 2023

I think the resolver should throw a proper exception for any unexpected format

@renanbr Have you seen Functional Tests which checks for 415 Unsupported Media Type?

Make class non-final is always easier than vise versa. Let's collect more feedback about all possible "child" implementation first and then remove final flag?

@nicolas-grekas
Copy link
Copy Markdown
Member

nicolas-grekas commented Apr 11, 2023

Make class non-final is always easier than vise versa. Let's collect more feedback about all possible "child" implementation first and then remove final flag?

I'd prefer removing the final now. We do have a use case already, and attributes are not good candidates for final types (they don't define abstractions but semantics, and inheritance is the way to extend semantics.)

Copy link
Copy Markdown
Member

@nicolas-grekas nicolas-grekas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please check tests, some failures look related to me

…String]` to map Request input to typed objects
Copy link
Copy Markdown
Member

@nicolas-grekas nicolas-grekas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nicolas-grekas
Copy link
Copy Markdown
Member

Thank you @Koc.

@OskarStark
Copy link
Copy Markdown
Contributor

giphy

and a big thank you! I really appreciate this new feature! 🎉

@nicolas-grekas
Copy link
Copy Markdown
Member

Note that if there are remaining topics to discuss (naming?), we're still in feature freeze so we can do any changes!

@Koc
Copy link
Copy Markdown
Contributor Author

Koc commented Apr 14, 2023

@nicolas-grekas thanx! I'm fine with MapRequestPayload as it merged or MapRequestContent as it was at the very beginning.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.