Skip to content

fix: length rules reject a valid length of 0 - #17

Merged
csemazharul merged 6 commits into
bit-apps-pro:mainfrom
shakurShirajul:fix/between-rule
Sep 16, 2026
Merged

csemazharul merged 6 commits into
bit-apps-pro:mainfrom
shakurShirajul:fix/between-rule

Conversation

@shakurShirajul

@shakurShirajul shakurShirajul commented Aug 6, 2026 •

Copy link
Copy Markdown
Contributor

Summary

The min, max, and between rules rejected any value whose length is 0. An empty string, an empty array, and the numbers 0 / 0.0 all failed validation even when the configured bounds explicitly allowed a length of zero — min:0, max:5, and between:0,5 would each reject ''.

The cause is a truthiness check on a length value:

$length = $this->getValueLength($value);

if ($length) {          // 0 is falsy, so a valid length of 0 falls through
    return $length >= $min;
}

return false;           // ...and is reported as a failure

getValueLength() returns an integer length, or false when the value has no measurable length at all (bool, null, object). Because 0 and false are both falsy, the guard could not tell "this value is legitimately empty" from "this value cannot be measured", and collapsed both into a failure.

This PR distinguishes the two, and moves the distinction into the shared helper so it is made in exactly one place.

Behaviour change

Only values with a length of 0 are affected. Verified before/after:

Value min:0 max:5 between:0,5
'' false → true false → true false → true
[] false → true false → true false → true
0 false → true false → true false → true
0.0 false → true false → true false → true

Everything else is untouched. Non-zero bounds behave identically to before (min:3 still accepts 'abc' and rejects 'ab'), and values with no measurable length — true, null, new stdClass() — still fail, as they should.

Note this only changes whether an empty value passes a length rule. It does not make empty values pass validation overall: required is still the rule that rejects them, which is the correct separation of concerns. A field declared min:0 was always meant to accept an empty value.

Changes

Fix — added Helpers::lengthWithin() next to getValueLength() in the trait all three rules already use. It decodes the false sentinel once and applies the bounds, with null meaning "no bound on this side":

protected function lengthWithin($value, $min = null, $max = null): bool

MinRule, MaxRule, and BetweenRule now each delegate to it in a single line rather than repeating the same guard three times. The original fix applied the corrected check separately in all three classes; consolidating it means a future length-based rule cannot reintroduce the bug by writing if ($length) again, and there is one definition of what a measurable length is.

Tests — added coverage to MinTest, MaxTest, and BetweenTest for the three distinct cases the old code conflated:

  • normal boundary behaviour (lengths above/below/at the bound)
  • a length of 0 is accepted, for strings, arrays, and numbers
  • values with no measurable length (true, null, stdClass) are still rejected

The previously commented-out array assertion in MaxTest is now enabled.

Developer tooling — added composer test:watch (bin/test-watch), which re-runs the suite when a PHP file under src/ or tests/ changes. It delegates to the existing test:unit script so there is a single definition of the test command, and uses plain polling so it needs no extra dependencies. Added /bin export-ignore to .gitattributes so this dev-only script is not shipped in the distributed package, and documented the two commands briefly under Contributing.

Testing

composer test:unit — 36 tests, 123 assertions, all passing.

Before/after behaviour in the table above was verified by running both revisions of the rules directly against each value.

Compatibility

No API changes. lengthWithin() is a new protected trait method; getValueLength() keeps its existing signature and return contract, so any code extending Rule or using Helpers continues to work unchanged. PHP 7.2+ compatible, consistent with the package's existing constraint.

One caveat worth calling out for reviewers: if a project currently relies on min:0 or between:0,N to reject empty values, that behaviour was a bug and this PR ends it. Such a field should use required instead.

Copilot AI lite review requested due to automatic review settings August 6, 2026 06:28

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Fixes an edge case in WPValidator’s length-based rules where a valid computed length of 0 was treated as falsy and incorrectly rejected. The change ensures only “unmeasurable” values (where getValueLength() returns false) short-circuit validation, while a real length/value of 0 proceeds through comparisons.

Changes:

  • Update BetweenRule, MinRule, and MaxRule to guard on $length === false rather than truthiness.
  • Expand rule test coverage to include zero-length/value cases (0, 0.0, '', []) and explicitly unmeasurable cases (true, null, stdClass).
  • Add boundary-negative assertions in existing min/max/between tests to validate rejection beyond configured limits.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated no comments.

Show a summary per file
File Description
src/Rules/MinRule.php Treat 0 as a valid measurable length/value; only false (unmeasurable) fails fast.
src/Rules/MaxRule.php Same fix as MinRule for max comparisons (accepts 0).
src/Rules/BetweenRule.php Same fix as Min/Max for range comparisons (accepts 0).
tests/Rules/MinTest.php Adds coverage for zero-length acceptance, unmeasurable rejection, and boundary negatives.
tests/Rules/MaxTest.php Adds coverage for zero-length acceptance, unmeasurable rejection, and boundary negatives.
tests/Rules/BetweenTest.php Adds coverage for zero-length acceptance, unmeasurable rejection, and boundary negatives.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

…tor Between, Max, and Min rules to utilize new method; add test-watch script and update README
Copilot AI review requested due to automatic review settings August 6, 2026 10:56

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 10 out of 11 changed files in this pull request and generated no new comments.

Copilot AI review requested due to automatic review settings September 15, 2026 06:27

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

Reject non-numeric floats such as NAN and resolve the documented watcher-tooling and guidance inconsistencies.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (1)

src/Helpers.php:40

  • Because getValueLength() accepts every float, this also receives NAN. For a min-only call, NAN < $min is false and the open-ended max causes the final expression to return true, whereas the previous implementation rejected it (NAN >= $min is false). Reject non-numeric float values such as NAN before applying the bounds so this fix only changes legitimate zero lengths.
        if ($length === false) {
            return false;
  • Files reviewed: 8/9 changed files
  • Comments generated: 2
  • Review effort level: Lite

Comment thread CLAUDE.md
Comment on lines +14 to +18
composer test:unit # full suite (Pest, --testdox, excludes group 'db')
./vendor/bin/pest tests/Rules/BetweenTest.php # single file
./vendor/bin/pest --filter='between accepts a length of zero' # single test by name
composer compat # PHPCompatibility check against PHP 7.2
composer rector # Rector dry-run (never auto-applies)
Comment thread CLAUDE.md
- `isEmpty()` treats `'0'`, `0`, `0.0`, and `false` as **non**-empty.
- `getValueLength()` returns `mb_strlen` for strings, `count` for arrays, the number itself for ints/floats, and `false` for anything unmeasurable (bool, null, object). `false` is distinct from length `0` — a length of 0 is valid.

`lengthWithin($value, $min, $max)` wraps both and is what `between`, `min`, `max`, and `size` should use (`null` for an open bound) rather than comparing lengths by hand.
Copilot AI review requested due to automatic review settings September 15, 2026 06:30

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 Needs a closer look

Unresolved issues include a NAN regression, inaccurate SizeRule guidance, and missing advertised test-watch tooling.

Review details

Suppressed comments (3)

CLAUDE.md:69

  • This guidance says size should use lengthWithin, but SizeRule::validate() still implements its own string/int/array comparisons and never calls this helper. That makes the documented invariant false and can mislead future rule changes; either migrate SizeRule with compatibility tests or remove size from this sentence.
`lengthWithin($value, $min, $max)` wraps both and is what `between`, `min`, `max`, and `size` should use (`null` for an open bound) rather than comparing lengths by hand.

CLAUDE.md:14

  • The PR description says this change adds composer test:watch, bin/test-watch, and documentation for it, but this checkout has no bin/test-watch and composer.json still defines only test:unit, compat, and rector. The advertised developer tooling is therefore not delivered; please add the missing files/script/docs or remove that scope from the description.
composer test:unit                                  # full suite (Pest, --testdox, excludes group 'db')

src/Helpers.php:47

  • Because the lower-bound check only rejects $length < $min, a NAN float (which getValueLength() accepts as a float) skips that check; with $max === null, the final expression then returns true. The previous MinRule comparison ($length >= $min) rejected NAN, so this is a regression. Use the positive lower-bound comparison or explicitly reject non-finite lengths.
        if ($min !== null && $length < $min) {
            return false;
        }

        return $max === null || $length <= $max;
  • Files reviewed: 8/9 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@csemazharul
csemazharul merged commit c2d390b into bit-apps-pro:main Sep 16, 2026
3 of 5 checks passed
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.

3 participants