Skip to content

where(): a joint constraint over the whole argument list - #155

Merged
rasuvaeff merged 1 commit into
masterfrom
feat/where-guard
Oct 11, 2026
Merged

rasuvaeff merged 1 commit into
masterfrom
feat/where-guard

Conversation

@rasuvaeff

Copy link
Copy Markdown
Owner

Fixes #151

What

The one Mockery verb with no mapping here now has one: withArgs(closure) → ->where().

when(fn () => $ledger->transfer(Arg::any(), Arg::any()))
    ->where(fn ($from, $to) => $from <= $to)
    ->returns(true);

verify(fn () => $ledger->transfer(Arg::any(), Arg::any()),
    times: 1, where: fn ($from, $to) => $from <= $to);

Variant (2) of the issue — an invocation-level guard — as decided there: a constraint on the call, composing with per-position matchers instead of excluding them.

Shape decisions, and why

  • The closure receives the arguments positionally (fn ($from, $to) => ...), not an Invocation. The guard must be evaluated everywhere matching is — and the by-reference slot walk (DoubleState::hasActionFor()) asks matchesArguments() before any Invocation exists. An Invocation-taking guard would either break the invariant that this walk agrees with dispatch, or require a synthetic Invocation with a fabricated sequence. Positional args keep every walk honest through the one choke point: dispatch, hasActionFor(), and a verify() probe all go through Expectation::matchesArguments().
  • Evaluated only after every position matched; a refused call falls through to broader expectations (the documented layering).
  • One guard per expectation — a second where() is refused with InvalidCallSpecification where it is written, not silently replacing the first.
  • description: names the guard in failure messages (the Arg::satisfies() precedent); default renders where(…). When positions matched but the guard refused, describeAgainst() marks the guard itself — transfer(any(), any()) *where(…)* — rather than blaming a position that accepted. The guard is re-asked defensively while rendering, exactly like a matcher is.
  • The guard is the test's own code and runs inside the code under test; an exception it raises is the test's own error (unlike a matcher, which must survive hostile arguments). No captor accounting, no matcher responsibilities.

Notes for reviewers

  • verify() takes one through a new ?callable $where = null parameter (facade + free function) — an optional trailing parameter, additive per the compatibility policy.
  • Pinned by tests: narrowing, fall-through layering, expect()/verify() forms, both renderings, the strict-refusal mark, the by-reference slot agreement (the guard refusing must look unconfigured to the slot walk), a zero-argument guard, and the variadic tail (fn (string $channel, int ...$values)).
  • mutation-diff: 60 mutants, one survivor — the ReturnRemoval on the zero-arg line, behaviorally equivalent (the fallthrough recomputes the same verdict); covered MSI 98%.
  • Docs: README + README.ru.md + llms.txt + the site guide (stubbing/matchers.md, migrating-from-mockery.md, regenerated MIGRATION.md), make docs-api, docs-claims green; new self-checking examples/where-guard.php in the build's @examples script.

@coderabbitai

coderabbitai Bot commented Oct 11, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: b580e6a4-3617-4ef0-9f37-ea42c85f56fe

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@rasuvaeff
rasuvaeff merged commit a05d04a into master Oct 11, 2026
25 checks passed
@rasuvaeff
rasuvaeff deleted the feat/where-guard branch October 11, 2026 07:19
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.

No way to match several arguments jointly — the missing Mockery withArgs(closure) row (double Argument::all())

1 participant