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

[12.x] Introduce Rule::oneOf() for Validating Against Multiple Rule Sets (#54880) #54946

Open
wants to merge 4 commits into
base: 12.x
Choose a base branch
from

Conversation

brianferri
Copy link

@brianferri brianferri commented Mar 8, 2025

Note

Non-Breaking Change
This PR does not modify any existing validation rules—it only introduces a new rule.

This PR introduces a new validation rule, Rule::oneOf(), allowing a field to be validated against multiple predefined rule sets, ensuring that at least one set fully passes. This feature is particularly useful for validating tagged unions, discriminator-based validation (as seen in OpenAPI specs), or alternative input structures in FormRequest validation.

Some keypoints on how this would benefit end users:

  • Simplifies Complex Validation: Instead of manually writing conditional rules like chained required_if, sometimes, or custom logic, oneOf() provides an expressive way to define alternative validation paths using objects which don't need to be necessarily flat maps of validation rules.
  • OpenAPI Compliance: Aligns Laravel validation with OpenAPI’s Discriminator Object, useful for API validation.
  • Improves Maintainability, Validation generation: Developers can declare validation rules in a structured way.

A practical example is the one I've mentioned in my discussion, with some additional attempts to implement this as a custom rule as well.

A sort of "emergent behavior" from this implementation is also the possibility of nested validations using nested OneOf rules as well, which would allow for deep parsing of body parameters in FormRequests

I look forward to receiving feedback and fixing, implementing or improving the implementation in the event that the current one is not deemed worthy.

I realize that the current implementation might be more akin to AnyOf instead, if that name fits better I will commit it and push it to the PR

Example Usage

Regular rule declaration (Base case for oneOf)

The p1 value effectively determines what the "shape" of the object to be validated should be

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;

class StoreWorkflowServices extends FormRequest
{
    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
     */
    public function rules(): array
    {
        return [
            'params' => [
                'required',
                Rule::oneOf([
                    [
                        'p1' => ['required', Rule::in([ArrayKeysBacked::key_1])],
                        'p2' => ['required'],
                        'p3' => ['required', 'url:http,https'],
                        'p4' => ['sometimes', 'required'],
                    ],
                    [
                        'p1' => ['required', Rule::in([ArrayKeysBacked::key_2])],
                        'p2' => ['required', 'email:rfc'],
                    ]
                ])
            ]
        ];
    }
}

Regular rule declaration using dynamic sets from OpenAPI Spec [Discussion Example]

<?php

namespace App\Http\Requests;

use App\Rules\OneOf;
use App\Util\OpenAPIGenerator;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
use OpenAPI\Client\Model\ModelServices;
use OpenAPI\Client\Model\Params;

class StoreWorkflowServices extends FormRequest
{
    /**
     * @return ValidationRule[]
     */
    public static function list(): array
    {
        $modelServiceValues = array_map(fn($case) => $case->value, ModelServices::cases());
        /** @var ValidationRule[] */
        $classCases = [];
        foreach ($modelServiceValues as $modelService) {
            $paramsRules = [];
            $modelClass = '\\OpenAPI\\Client\\Model\\' . $modelService . 'Params';
            /** @var Params */
            $params = new $modelClass();
            $formats = $params->openAPIFormats();
            foreach ($params->openAPITypes() as $key => $type) {
                $paramsRules[$key] = ['required',  match ($key) {
                    Params::DISCRIMINATOR => Rule::in($params->getServiceAllowableValues()),
                    default => OpenAPIGenerator::mapTypeToRule($type, $formats[$key], $params->isNullable($key))
                }];
            }
            array_push($classCases, $paramsRules);
        }
        return $classCases;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
     */
    public function rules(): array
    {
        return [
            'params' => ['required', Rule::oneOf($this::list())]
        ];
    }
}

Nested rules (Which becomes emergent from the nature of the passes() method in OneOf validation)

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;

class StoreWorkflowServices extends FormRequest
{
    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
     */
    public function rules(): array
    {
        return [
            'params' => ['required', Rule::oneOf([
                [
                    'p1' => ['required', Rule::oneOf([
                        [
                            'p2' => ['required', 'string'],
                            'p3' => ['required', Rule::oneOf([[
                                'p4' => ['nullable', 'string'],
                            ]])],
                        ],
                    ])],
                ],
            ])]
        ];
    }
}

Copy link

github-actions bot commented Mar 8, 2025

Thanks for submitting a PR!

Note that draft PR's are not reviewed. If you would like a review, please mark your pull request as ready for review in the GitHub user interface.

Pull requests that are abandoned in draft may be closed due to inactivity.

@brianferri brianferri changed the title [12.x] Introduce Rule::oneOf() for Validating Against Multiple Rule Sets (https://github.com/laravel/framework/discussions/54880) [12.x] Introduce Rule::oneOf() for Validating Against Multiple Rule Sets (#54880) Mar 8, 2025
@brianferri brianferri marked this pull request as ready for review March 9, 2025 12:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant