Skip to content

[13.x] Fix contains/doesnt_contain validation rules using inconsistent comparison strictness - #61492

Closed
iz-ahmad wants to merge 6 commits into
laravel:13.xfrom
iz-ahmad:fix/contains-comparison-mismatch
Closed

iz-ahmad wants to merge 6 commits into
laravel:13.xfrom
iz-ahmad:fix/contains-comparison-mismatch

Conversation

@iz-ahmad

@iz-ahmad iz-ahmad commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Closes #61491: contains was using loose in_array() while doesnt_contain used strict. they're supposed to be logical opposites, but this inconsistency let both pass on the same input - e.g. flags => [true] with contains:1 and doesnt_contain:1 both pass, because "1" == true loosely but "1" !== true strictly (ValidatesAttributes.php: L568-L591). kindly check the issue #61491, I've added more detailed report there.

This was actually fixed once (#61318 made doesnt_contain strict, #61320 tried the same for contains) but #61320 got reverted later. because going strict without normalizing types broke matches like [1] (int) against contains:1 (string param).

Changes I made

in this fix, I've normalize the array values and the rule params to strings, and then compare strictly.. like in rule already uses (#61146 by @crynobone). This closes the gap which #61320 hit, while keeping the strict comparison that #61318 needed (numeric-string collisions like '0e123' vs 0 stay correctly non-matching).

Added tests covering: numeric-string edge cases ('0e123' vs 0) staying non-matching for both rules, non-string-scalar values matching via string-syntax, the same via array-rule syntax (['contains', 1]) - since that path hands parameters without string-casting them, unlike string-rule syntax, non-scalar params (e.g. an array) failing gracefully instead of throwing.

Possible breaking change after this fix: normalizing values to strings means a few loose-comparison matches that worked before, now won't work:

  • [false] with contains:0 - used to pass (false == "0"), now fails ((string) false is "", not "0")
  • [1.0] with contains:1.0 - used to pass, now fails on format mismatch unless the param string matches PHP's exact float-to-string output

Both are edge cases that only worked before because of the loose-comparison bug this PR fixes - anyone relying on them was actually relying on an inconsistency between two rules that are documented as opposites. this PR just fixes the bug. and the alternative (leaving contains and doesnt_contain able to both pass on the same input) is a worse correctness bug than losing this narrow edge-case match.

Still, if this looks problematic to merge into 13.x, I can submit this for the master also. please let me know which one is preferable.

Note: this PR desp and all the changes I made is not AI-made and I've tested+reviewed my changes carefully. so if any further modification needed in this approach or any better approach/suggestion u have in mind for this issue, kindly let me know. thank you.

@iz-ahmad

iz-ahmad commented Sep 8, 2026 •

Copy link
Copy Markdown
Contributor Author

the windows CI failure is unrelated to this my changes. every failing job fails at the setup-php step before any tests run. whereas the Linux/macOS jobs pass. maybe a runner/action issue, re-running may resolve it.

@iz-ahmad

iz-ahmad commented Sep 8, 2026 •

Copy link
Copy Markdown
Contributor Author

Let's review: #61493 instead, my pr is better. take a look

your PR fixes the non-string-scalar gap, but that only patches doesnt_contain and keeps contains still loose, so the same contradiction just comes back through a different door:

$v1 = Validator::make(['x' => ['0e123']], ['x' => 'contains:0']);
$v2 = Validator::make(['x' => ['0e123']], ['x' => 'doesnt_contain:0']);

$v1->passes(); // true, contains is still loose, "0" == "0e123" numerically
$v2->passes(); // true, value gets stringified here, but "0" !== "0e123" strictly

Both pass again, same as the original bug report, just with 0e123 instead of true.. Also worth noting the value-side cast alone doesn't cover array-rule syntax ([['doesnt_contain', 1]]) since the param itself can arrive as a non-string there too and never gets normalized.

and my PR patches both rules symmetrically so this can't reopen from the other side.

@taylorotwell

Copy link
Copy Markdown
Member

Thanks for your pull request to Laravel!

Unfortunately, I'm going to delay merging this code for now. To preserve our ability to adequately maintain the framework, we need to be very careful regarding the amount of code we include.

If applicable, please consider releasing your code as a package so that the community can still take advantage of your contributions!

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.

contains/doesnt_contain validation rules use inconsistent comparison strictness

2 participants