I would not pass Team, Annual through accepted_if as a string — 13.36 added the rest of the Rule builders

I would not pass Team, Annual through accepted_if as a string — 13.36 added the rest of the Rule builders

Laravel 13.36 adds Rule:: methods for the validation rules that were still strings. Rule::acceptedIf keeps a comma inside one parameter. The string form splits it, so a plan named Team, Annual never forces the terms box. Confirm laravel/framework is 13.36.0 or newer before calling Rule::requiredWithout. Packagist is already on 13.38.0.

· Updated · 9 min read #laravel #php #validation #rules

 Rule::requiredWithout is missing on 13.35 and present from 13.36, with 13.38 as the bump

Caption: On 13.35 the new methods are absent and the string rule still splits on the comma. The builders landed in 13.36.0 on 13 Oct 2026. The 13.x line on Packagist this morning is 13.38.0.

Introduction

I keep a throwaway Laravel 13 app for framework bumps. This week the form I care about is a checkout: email or phone, an https website, a device UUID, tags, a confirmed password, and a terms box that is required when the plan is named Team, Annual.

That plan name has a comma in it. I have been writing accepted_if:plan,Team, Annual and assuming the validator saw one value. It does not.

Laravel 13.36.0 shipped 13 Oct 2026. Laravel News covered it 14 Oct 2026 and published a longer tutorial 16 Oct 2026. I am writing this on 21 Oct 2026 (2026-W43). The queue row is week 2026-W42. Nothing was tagged 2026-W43, so this is the lowest-priority undone item.

The latest 13.x on Packagist this morning is v13.38.0 (20 Oct 2026). The builders landed in 13.36.0. I read Rule.php, AcceptedIf.php, RequiredWithout.php, FormatsParameters.php, and ValidationRuleParser.php on tag v13.38.0. I did not boot the throwaway app on the VPS this morning.

About the feature

Illuminate\Validation\Rule already had builders for in, unique, exists, enum, date, email, and file. PR #61833 added a method for every rule that was still string-only. The 13.x validation docs, retrieved 21 Oct 2026, already have a Rule Builders section: Rule::requiredWithout(['phone', 'mobile']), Rule::acceptedIf('account_type', ['personal', 'business']), Rule::url()->protocols(['http', 'https']), and Rule::uuid()->version(4).

Two shapes come back from those methods.

Rules with no parameters return a plain string. Rule::required() is the string required. The docs mix those strings with older rule strings in the same array.

Rules that take field names or comparison values return an object that implements Illuminate\Contracts\Validation\ParameterizedRule. RequiredWithout::toArray() returns ['required_without', ...$fields]. AcceptedIf::toArray() returns ['accepted_if', $anotherField, ...$values]. ValidationRuleParser::parse() sees the interface and uses that array. It does not run the parameter string through str_getcsv.

The string path still does. parseStringRule() splits on the first colon, then parseParameters() calls str_getcsv($parameter, escape: '\\') unless the rule is regex or not_regex. A comma inside a value becomes another parameter. Non-string comparison values are json-encoded inside AcceptedIf::toArray(), so true becomes the parameter true. Casting the object goes through FormatsParameters, which quotes a parameter that contains a comma: (string) Rule::acceptedIf('plan', 'Team, Annual') is accepted_if:plan,"Team, Annual". The object path never builds that string.

Rule::requiredIf() is a different, older method. RequiredIf takes a closure or a boolean. Anything else throws InvalidArgumentException with the message The provided condition must be a callable or boolean. The field-and-value form is acceptedIf, declinedIf, missingIf, presentIf, and the requiredWithout / requiredWith family. There is no Rule::requiredIf('plan', 'pro').

Rule::date()->equals() arrived in 13.36 (absent on tag v13.35.0). Date, Numeric, and StringRule are still cast to strings and split on |. Rule::numeric(strict: true) is 13.37 (PR #62031) and still builds the string numeric:strict.

 String accepted_if splits Team, Annual; AcceptedIf::toArray keeps one value

Caption: str_getcsv turns accepted_if:plan,Team, Annual into the field plan plus two values. Rule::acceptedIf('plan', 'Team, Annual') hands parse() one comparison value, comma included. accepted_if only demands the terms field when that comparison hits.

Why I picked it

The queue item is week 2026-W42. Today is 2026-W43. No newer row was waiting.

Checkout still names a plan Team, Annual and requires terms for that plan. The string rule looks right in a FormRequest, then lets the request through with terms missing, because the condition never sees the real plan name.

Where it can be used

This belongs on a FormRequest whose parameters contain commas or several field names: required_without, accepted_if, url protocols, uuid versions, distinct:ignore_case. Rule::unique(), Rule::exists(), Rule::in(), and Rule::email() were already builders.

The same 13.36 tag also shipped DB::olap() (a no-op callback on MySQL that is not Vitess, PR #61932), Worker::$flushStickyConnections defaulting to false (PR #61915), and #[DeleteWhenMissingModels] on queued mailables (PR #61899).

Benefit

The new methods live in laravel/framework. v13.36.0 and v13.38.0 composer.json both require php: ^8.3 and replace illuminate/validation with self.version. I do not composer require illuminate/validation.

What I get is a rule object the parser will not split, and a string cast I can log when I still want the old format. accepted, required_without, url, and uuid still mean what the docs say they mean.

 The builders sit inside laravel/framework, which replaces illuminate/validation

Caption: Rule::required() is a string. requiredWithout, acceptedIf, url, uuid, and distinct are ParameterizedRule objects. Rule::date()->equals() still compiles to a string. ValidationRuleParser::parse() calls toArray() before str_getcsv.

Practical example from a recent application

Lab shape from the queue notes and tag v13.38.0, not a named client. Confirm composer show laravel/framework is 13.36.0 or newer before calling Rule::requiredWithout(). I would bump the throwaway app to 13.38.0, which is the current 13.x. The token-guard advisory Laravel News published 20 Oct 2026 (GHSA-f9x3-7vq9-cph6) is already fixed in 13.29.0. A 13.35 app has that fix. It does not have these builders.

composer show laravel/framework | sed -n '1,8p'
# name     : laravel/framework
# versions : * v13.38.0   # Packagist 20 Oct 2026. Builders landed in 13.36.0.

If that line still says 13.35.x, stop. Rule::requiredWithout and Rule::acceptedIf are not on the class. PHP raises an undefined-method Error as soon as rules() runs.

namespace App\Http\Requests;

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

class StoreCheckoutRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'email' => [Rule::requiredWithout('phone'), 'email'],
            'phone' => [Rule::requiredWithout('email')],
            'website' => [Rule::nullable(), Rule::url()->protocols(['https'])],
            'device_id' => [Rule::uuid()->version(4)],
            'tags.*' => [Rule::distinct()->ignoreCase()],
            'password' => [Rule::required(), Rule::confirmed()],
            'terms' => [Rule::acceptedIf('plan', 'Team, Annual')],
        ];
    }
}

Rule::nullable() and Rule::required() are strings. The url, uuid, distinct, confirmed, and acceptedIf calls are objects, so explodeExplicitRule() does not pipe-split them.

The comma case, as a Validator::make I can read without the HTTP stack:

use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;

$passes = Validator::make(
    ['plan' => 'Team, Annual', 'terms' => 'yes'],
    ['terms' => [Rule::acceptedIf('plan', 'Team, Annual')]],
)->passes();

$skipped = Validator::make(
    ['plan' => 'Team, Annual'],
    ['terms' => 'accepted_if:plan,Team, Annual'],
)->passes();

yes is one of the accepted values in the docs (yes, on, 1, true). On the builder, a missing terms field fails, because toArray() still holds the single value Team, Annual. On the string, str_getcsv yields plan, then Team, then Annual with a leading space. The plan name equals neither value, so accepted_if does not apply, and a request with no terms key passes.

Failure I hit if I stay on 13.35, keep the string, or grab the old method. On 13.35, rules() dies with an undefined-method Error on requiredWithout or acceptedIf. On 13.36+ with the string still in place, Team, Annual never triggers the terms check. And this call throws before the validator runs:

Rule::requiredIf('plan', 'Team, Annual');
// InvalidArgumentException: The provided condition must be a callable or boolean.

Fix. Bump laravel/framework to 13.36.0 or newer (today that is 13.38.0). Use Rule::acceptedIf('plan', 'Team, Annual') for the plan name. Use Rule::requiredWithout() for the email-or-phone pair. Keep Rule::requiredIf() for a closure or a boolean. Leave Rule::in / unique / exists as they were.

 Checkout rules on the new builders, and the requiredIf call that throws

Caption: The FormRequest mixes string-returning methods with ParameterizedRule objects. Rule::requiredIf('plan', 'Team, Annual') throws InvalidArgumentException. On 13.35, acceptedIf() and requiredWithout() are simply missing.

13.37.0 (16 Oct 2026) adds Rule::numeric(strict: true), queue:failed --queue, multi-id queue:forget, and JPEG XL. 13.38.0 adds #[CountCrashesAsExceptions] on queued listeners, mailables, and notifications (PR #62048). The Laravel News October archive on 21 Oct 2026 still had no 13.37 or 13.38 release note. The latest framework release write-up was the 14 Oct 13.36 post, plus the 16 Oct tutorial and the 19 Oct DB::olap() tutorial. No CVE ships with these builders.

Conclusion

What I would keep: laravel/framework at 13.38.0, Rule::acceptedIf() anywhere a comparison value can contain a comma, Rule::requiredWithout() for the email-or-phone pair, and the older Rule::unique / exists / in builders untouched.

After that bump, parse() takes AcceptedIf::toArray() and the terms box follows the plan name Team, Annual. The string accepted_if:plan,Team, Annual still splits, and Rule::requiredIf('plan', …) still throws. Next I would grep FormRequests for accepted_if: and Rule::requiredIf(' with a field name in the first argument.

Did you hit the same wall?

I got stuck on accepted_if:plan,Team, Annual letting a checkout through with terms missing, and then on Rule::requiredIf('plan', 'Team, Annual') throwing InvalidArgumentException because that method still wants a closure. Did you hit the same thing — a plan or coupon name with a comma, an undefined Rule::requiredWithout on 13.35, or a url / uuid / distinct string you rewrote and then watched the parameters split? Tell me in the comments. I read them.

Need this done on your server?

I deploy and harden Laravel/CodeCanyon apps on cPanel or VPS, and offer monthly Server Watch retainers. Hire for deploy · Care plan

References

  • Laravel validation docs, Rule Builders (retrieved 21 Oct 2026). The section shows requiredWithout, acceptedIf, url()->protocols(), and uuid()->version(). The comma-in-one-value case is the 16 Oct tutorial, not a separate docs chapter: https://laravel.com/docs/13.x/validation
  • Laravel News, 14 Oct 2026, “Rule Builders for Every Validation Rule in Laravel 13.36”: https://laravel-news.com/laravel-13-36-0
  • Laravel News, 16 Oct 2026, “Fluent Rule Builders for All Laravel Validation Rules”: https://laravel-news.com/laravel-validation-rule-builders
  • Laravel News, 19 Oct 2026, DB::olap() tutorial (same 13.36 release): https://laravel-news.com/laravel-planetscale-olap
  • Laravel News, 20 Oct 2026, token guard advisory, patched in 13.29.0 and 12.68.0, separate from these builders: https://laravel-news.com/laravel-token-guard-auth-bypass
  • Framework PR #61833 — builders for the remaining rules, ParameterizedRule, date()->equals()
  • Framework PR #62031 — Rule::numeric(strict: true) in 13.37.0
  • Rule.php, AcceptedIf.php, RequiredIf.php, RequiredWithout.php, FormatsParameters.php, ValidationRuleParser.php at tag v13.38.0 (date()->equals() confirmed on tag v13.36.0 and absent on v13.35.0)
  • laravel/framework v13.36.0 on Packagist, 13 Oct 2026 13:21 UTC: https://packagist.org/packages/laravel/framework#v13.36.0
  • laravel/framework v13.37.0 on Packagist, 16 Oct 2026 04:59 UTC
  • laravel/framework v13.38.0 on Packagist, 20 Oct 2026 10:45 UTC (latest 13.x at write time)
  • v13.36.0 and v13.38.0 composer.json: php: ^8.3, replace.illuminate/validation: self.version
Share:

Get new posts in your inbox

No spam. One short email per new article — practical PHP, Laravel, devops, and AI-assisted workflows.

Comments

Powered by GitHub Discussions via Giscus. A free GitHub account is required.