Skip to content

Argument matchers

A matcher goes where the argument goes:

php
when(fn () => $repository->find(Arg::any()))->returns($book);

The table

MatcherMatches
Arg::any()anything, including null
Arg::int(min:, max:)an int in range — a numeric string does not match
Arg::float(min:, max:)a float in range — an int does not match
Arg::string(matches:)a string, optionally against a PCRE pattern
Arg::bool()a boolean
Arg::same($v)strict identity; for objects, the same instance
Arg::not($v)negates a literal or another matcher
Arg::allOf(...)everything the operands accept; an operand is a matcher or a literal
Arg::anyOf(...)anything at least one operand accepts, so anyOf('draft', 'review') reads as a set
Arg::instanceOf($class)an instance of the class or interface
Arg::satisfies($fn)whatever the predicate accepts
Arg::containing($entries)an array holding these entries and possibly more
Arg::count(minimum:, maximum:)an array or Countable of that size
Arg::which($method, $value)an object whose getter answers this value
Arg::none()an empty variadic tail — last argument only
Arg::remaining()the whole variadic tail, any length — last argument only
Arg::rest()declared parameters left unspelled — last argument only

There is also Arg::captor(), which matches and records at the same time.

The type matchers are strict on purpose

Arg::int() rejects '5'. Arg::float() rejects 1.

A matcher pins the declared type as much as the value, which is the point in a codebase running under strict_types. A matcher that quietly accepted the other type would hide exactly the bug the declaration exists to prevent.

rest() against remaining()

They look similar and stand for different things:

Stands for
Arg::remaining()the variadic tail the method declares — any length
Arg::rest()"the arguments written here matter, the rest of the arity does not"

rest() is the one matcher that lets a specification stop before the method's required parameters run out:

php
when(fn () => $storage->recordOutcome('svc', Arg::rest()))
    ->throws(new RuntimeException('storage unavailable'));

A specification that stops early without ending in Arg::rest() is refused with the reason, rather than becoming a stub that silently never matches. A later, narrower specification for the same call still wins over the broad prefix stub.

One thing to know

A static analyser reads the shortened call against the contract's arity, so expect a "too few arguments" diagnostic on that line until your analyser knows the idiom. The Psalm plugin and the PHPStan extension teach it.

Arg::which() and a getter that throws

Arg::which() calls only a public, non-static method that needs no arguments.

A getter that throws counts as a mismatch, never as an error. Matching runs while the code under test is executing, and a matcher must not be the thing that breaks it.

A matcher that never reaches a specification

Building a matcher and not passing it to a specification is reported, not ignored — MatcherLeaked. The static analysis packages report the same thing before the test runs, under understudy.matcherLeak.