Skip to content
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

[HttpKernel] Add validation groups resolver option to RequestPayloadValueResolver #51233

Conversation

HypeMC
Copy link
Contributor

@HypeMC HypeMC commented Aug 2, 2023

Q A
Branch? 6.4
Bug fix? no
New feature? yes
Deprecations? no
Tickets Fix #51012
License MIT
Doc PR -

This PR introduces a way to configure validation groups for the RequestPayloadValueResolver dynamically, similar to how it can be done in the form component:

class ValidationGroupsResolver
{
    public function __invoke(mixed $payload, Request $request)
    {
        // ...
        return $groups;
    }
}

public function index(
    #[MapRequestPayload(validationGroupsResolver: new ValidationGroupsResolver())] DTO $dto,
): Response {
    // ...
}

The resolver can also be a service, it just needs to be tagged with controller.request_payload.validation_groups_resolver:

#[AutoconfigureTag('controller.request_payload.validation_groups_resolver')]
class ValidationGroupsResolver
{
    public function __construct(SomeDep $someDep)
    {
    }

    public function __invoke(mixed $payload, Request $request)
    {
        // ...
        return $groups;
    }
}

public function index(
    #[MapRequestPayload(validationGroupsResolver: ValidationGroupsResolver::class)] DTO $dto,
): Response {
    // ...
}

Copy link
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.

Would you have a real-life example use case? @MilanKaranovicQuantox maybe?


public function __construct(
public readonly array $serializationContext = [],
public readonly string|GroupSequence|array|null $validationGroups = null,
string $resolver = RequestPayloadValueResolver::class,
public readonly int $validationFailedStatusCode = Response::HTTP_NOT_FOUND,
string|callable $validationGroupsResolver = null,
Copy link
Member

Choose a reason for hiding this comment

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

What about supporting only service ids?

Copy link
Contributor

Choose a reason for hiding this comment

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

What about an array of validation groups to use instead of a resolver (less boilerplate for simple cases)?
We could also have both options.
Another way could be to have a specific interface to implement on the value object directly.

Copy link
Member

@yceruto yceruto Aug 3, 2023

Choose a reason for hiding this comment

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

Determining the validation groups dynamically based on the state of the underlying object is already supported by
#[Assert\GroupSequenceProvider] https://symfony.com/doc/current/validation/sequence_provider.html#group-sequence-providers

I guess the use case the author wants to cover here is determining the groups based on external services through DI (which is not so common but still)

@MilanKaranovicQuantox
Copy link

Would you have a real-life example use case? @MilanKaranovicQuantox maybe?

Hi @nicolas-grekas! I tend to replace Forms with MapRequestPayload on the API endpoints. I want to use Forms only on FE, for actual forms.

I managed to solve my issue as it's suggested in the discussion but I think it would be good to be able to use the service as a group resolver if needed, like Forms are able to.

When I posted the discussion, I had this in mind as potential solution https://api-platform.com/docs/core/validation/#dynamic-validation-groups

@nicolas-grekas
Copy link
Member

@yceruto WDYT? Is this worth the extra bit of complexity? should we improve the doc instead? or is this desired to you?

@yceruto
Copy link
Member

yceruto commented Aug 8, 2023

This example from https://api-platform.com/docs/core/validation/#dynamic-validation-groups shows a valid use case, in my opinion. However, I would prefer for this to be implemented as a validator feature instead, exactly as it's documented here: https://api-platform.com/docs/core/validation/#sequential-validation-groups. This way, it won't be specific to the ValueResolver.

@yceruto
Copy link
Member

yceruto commented Aug 10, 2023

Here we go with another proposal, #51348, but this time from the Validator component instead, covering other advanced scenarios.

@fabpot
Copy link
Member

fabpot commented Oct 20, 2023

Closing in favor of #51348

@fabpot fabpot closed this Oct 20, 2023
fabpot added a commit that referenced this pull request Oct 20, 2023
…tion groups provider outside DTOs (Yonel Ceruto)

This PR was squashed before being merged into the 6.4 branch.

Discussion
----------

[FrameworkBundle][Validator] Allow implementing validation groups provider outside DTOs

| Q             | A
| ------------- | ---
| Branch?       | 6.4
| Bug fix?      | no
| New feature?  | yes
| Deprecations? | no
| Tickets       | -
| License       | MIT
| Doc PR        | symfony/symfony-docs#18744

Alternative to #51233
Inspiration: #51012

Currently, you can determine the sequence of groups to apply dynamically based on the state of your DTO by implementing the `GroupSequenceProviderInterface` in your DTO class. https://symfony.com/doc/current/validation/sequence_provider.html#group-sequence-providers
```php
use Symfony\Component\Validator\GroupSequenceProviderInterface;

#[Assert\GroupSequenceProvider]
class UserDto implements GroupSequenceProviderInterface
{
    // ...

    public function getGroupSequence(): array|GroupSequence
    {
        if ($this->isCompanyType()) {
            return ['User', 'Company'];
        }

        return ['User'];
    }
}
```
It covers most of the common scenarios, but for more advanced ones, it may not be sufficient. Suppose now you need to provide the sequence of groups from an external configuration (or service) which can change its value dynamically:
```php
#[Assert\GroupSequenceProvider]
class UserDto implements GroupSequenceProviderInterface
{
    // ...

    public __constructor(private readonly ConfigService $config)
    {
    }

    public function getGroupSequence(): array|GroupSequence
    {
        if ($this->config->isEnabled()) {
            return ['User', $this->config->getGroup()];
        }

        return ['User'];
    }
}
```
This issue cannot be resolved at present without managing the DTO initialization and manually setting its dependencies. On the other hand, since the state of the DTO is not used at all, the implementation of the `GroupSequenceProviderInterface` becomes less fitting to the DTO responsibility. Further, stricter programming may raise a complaint about a violation of SOLID principles here.

So, the proposal of this PR is to allow configuring the validation groups provider outside of the DTO, while simultaneously enabling the registration of this provider as a service if necessary.

To achieve this, you'll need to implement a new `GroupProviderInterface` in a separate class, and configure it using the new `provider` option within the `GroupSequenceProvider` attribute:
```php
#[Assert\GroupSequenceProvider(provider: UserGroupProvider::class)]
class UserDto
{
    // ...
}

class UserGroupProvider implements GroupProviderInterface
{
    public __constructor(private readonly ConfigService $config)
    {
    }

    public function getGroups(object $object): array|GroupSequence
    {
        if ($this->config->isEnabled()) {
            return ['User', $this->config->getGroup()];
        }

        return ['User'];
    }
}
```
That's all you'll need to do if autowiring is enabled under your custom provider. Otherwise, you can manually tag your service with `validator.group_provider` to collect it and utilize it as a provider service during the validation process.

In conclusion, no more messing with the DTO structure, just use the new `class` option for more advanced use cases.

---

TODO:

- [x] Add tests
- [x] Create doc PR

Commits
-------

a3a089a [FrameworkBundle][Validator] Allow implementing validation groups provider outside DTOs
symfony-splitter pushed a commit to symfony/framework-bundle that referenced this pull request Oct 20, 2023
…tion groups provider outside DTOs (Yonel Ceruto)

This PR was squashed before being merged into the 6.4 branch.

Discussion
----------

[FrameworkBundle][Validator] Allow implementing validation groups provider outside DTOs

| Q             | A
| ------------- | ---
| Branch?       | 6.4
| Bug fix?      | no
| New feature?  | yes
| Deprecations? | no
| Tickets       | -
| License       | MIT
| Doc PR        | symfony/symfony-docs#18744

Alternative to symfony/symfony#51233
Inspiration: symfony/symfony#51012

Currently, you can determine the sequence of groups to apply dynamically based on the state of your DTO by implementing the `GroupSequenceProviderInterface` in your DTO class. https://symfony.com/doc/current/validation/sequence_provider.html#group-sequence-providers
```php
use Symfony\Component\Validator\GroupSequenceProviderInterface;

#[Assert\GroupSequenceProvider]
class UserDto implements GroupSequenceProviderInterface
{
    // ...

    public function getGroupSequence(): array|GroupSequence
    {
        if ($this->isCompanyType()) {
            return ['User', 'Company'];
        }

        return ['User'];
    }
}
```
It covers most of the common scenarios, but for more advanced ones, it may not be sufficient. Suppose now you need to provide the sequence of groups from an external configuration (or service) which can change its value dynamically:
```php
#[Assert\GroupSequenceProvider]
class UserDto implements GroupSequenceProviderInterface
{
    // ...

    public __constructor(private readonly ConfigService $config)
    {
    }

    public function getGroupSequence(): array|GroupSequence
    {
        if ($this->config->isEnabled()) {
            return ['User', $this->config->getGroup()];
        }

        return ['User'];
    }
}
```
This issue cannot be resolved at present without managing the DTO initialization and manually setting its dependencies. On the other hand, since the state of the DTO is not used at all, the implementation of the `GroupSequenceProviderInterface` becomes less fitting to the DTO responsibility. Further, stricter programming may raise a complaint about a violation of SOLID principles here.

So, the proposal of this PR is to allow configuring the validation groups provider outside of the DTO, while simultaneously enabling the registration of this provider as a service if necessary.

To achieve this, you'll need to implement a new `GroupProviderInterface` in a separate class, and configure it using the new `provider` option within the `GroupSequenceProvider` attribute:
```php
#[Assert\GroupSequenceProvider(provider: UserGroupProvider::class)]
class UserDto
{
    // ...
}

class UserGroupProvider implements GroupProviderInterface
{
    public __constructor(private readonly ConfigService $config)
    {
    }

    public function getGroups(object $object): array|GroupSequence
    {
        if ($this->config->isEnabled()) {
            return ['User', $this->config->getGroup()];
        }

        return ['User'];
    }
}
```
That's all you'll need to do if autowiring is enabled under your custom provider. Otherwise, you can manually tag your service with `validator.group_provider` to collect it and utilize it as a provider service during the validation process.

In conclusion, no more messing with the DTO structure, just use the new `class` option for more advanced use cases.

---

TODO:

- [x] Add tests
- [x] Create doc PR

Commits
-------

a3a089a15b8 [FrameworkBundle][Validator] Allow implementing validation groups provider outside DTOs
symfony-splitter pushed a commit to symfony/validator that referenced this pull request Oct 20, 2023
…tion groups provider outside DTOs (Yonel Ceruto)

This PR was squashed before being merged into the 6.4 branch.

Discussion
----------

[FrameworkBundle][Validator] Allow implementing validation groups provider outside DTOs

| Q             | A
| ------------- | ---
| Branch?       | 6.4
| Bug fix?      | no
| New feature?  | yes
| Deprecations? | no
| Tickets       | -
| License       | MIT
| Doc PR        | symfony/symfony-docs#18744

Alternative to symfony/symfony#51233
Inspiration: symfony/symfony#51012

Currently, you can determine the sequence of groups to apply dynamically based on the state of your DTO by implementing the `GroupSequenceProviderInterface` in your DTO class. https://symfony.com/doc/current/validation/sequence_provider.html#group-sequence-providers
```php
use Symfony\Component\Validator\GroupSequenceProviderInterface;

#[Assert\GroupSequenceProvider]
class UserDto implements GroupSequenceProviderInterface
{
    // ...

    public function getGroupSequence(): array|GroupSequence
    {
        if ($this->isCompanyType()) {
            return ['User', 'Company'];
        }

        return ['User'];
    }
}
```
It covers most of the common scenarios, but for more advanced ones, it may not be sufficient. Suppose now you need to provide the sequence of groups from an external configuration (or service) which can change its value dynamically:
```php
#[Assert\GroupSequenceProvider]
class UserDto implements GroupSequenceProviderInterface
{
    // ...

    public __constructor(private readonly ConfigService $config)
    {
    }

    public function getGroupSequence(): array|GroupSequence
    {
        if ($this->config->isEnabled()) {
            return ['User', $this->config->getGroup()];
        }

        return ['User'];
    }
}
```
This issue cannot be resolved at present without managing the DTO initialization and manually setting its dependencies. On the other hand, since the state of the DTO is not used at all, the implementation of the `GroupSequenceProviderInterface` becomes less fitting to the DTO responsibility. Further, stricter programming may raise a complaint about a violation of SOLID principles here.

So, the proposal of this PR is to allow configuring the validation groups provider outside of the DTO, while simultaneously enabling the registration of this provider as a service if necessary.

To achieve this, you'll need to implement a new `GroupProviderInterface` in a separate class, and configure it using the new `provider` option within the `GroupSequenceProvider` attribute:
```php
#[Assert\GroupSequenceProvider(provider: UserGroupProvider::class)]
class UserDto
{
    // ...
}

class UserGroupProvider implements GroupProviderInterface
{
    public __constructor(private readonly ConfigService $config)
    {
    }

    public function getGroups(object $object): array|GroupSequence
    {
        if ($this->config->isEnabled()) {
            return ['User', $this->config->getGroup()];
        }

        return ['User'];
    }
}
```
That's all you'll need to do if autowiring is enabled under your custom provider. Otherwise, you can manually tag your service with `validator.group_provider` to collect it and utilize it as a provider service during the validation process.

In conclusion, no more messing with the DTO structure, just use the new `class` option for more advanced use cases.

---

TODO:

- [x] Add tests
- [x] Create doc PR

Commits
-------

a3a089a15b8 [FrameworkBundle][Validator] Allow implementing validation groups provider outside DTOs
@HypeMC HypeMC deleted the request-payload-resolver-validation-groups-resolver branch October 20, 2023 12:23
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.

None yet

7 participants