sebastianbergmann/phpunit · error · MatchBuilderNotFoundException
No builder found for match builder identification <%s>
Error message
No builder found for match builder identification <%s>
What it means
Thrown by Matcher::invoked() when an expectation ordered with ->after($id) fires (its method was actually invoked) but no matcher with that id exists on the same double. The lookup goes through $invocation->object()->__phpunit_getInvocationHandler()->lookupMatcher($id), so the referenced id must have been registered earlier with ->id($id) on the very same mock object.
Source
Thrown at src/Framework/MockObject/Runtime/Matcher.php:130
* @throws Exception
* @throws ExpectationFailedException
* @throws MatchBuilderNotFoundException
* @throws MethodNameNotConfiguredException
* @throws RuntimeException
*/
public function invoked(Invocation $invocation): mixed
{
if ($this->methodNameRule === null) {
throw new MethodNameNotConfiguredException;
}
if ($this->afterMatchBuilderId !== null) {
$matcher = $invocation->object()
->__phpunit_getInvocationHandler()
->lookupMatcher($this->afterMatchBuilderId);
if ($matcher === null) {
throw new MatchBuilderNotFoundException($this->afterMatchBuilderId);
}
}
$this->invocationRule->invoked($invocation);
try {
$this->parametersRule?->apply($invocation);
} catch (ExpectationFailedException $e) {
throw new ExpectationFailedException(
sprintf(
"Expectation for %s failed.\n%s",
$this->methodNameRule->failureDescription($this->className),
$e->getMessage(),
),
$e->getComparisonFailure(),
);
}
View on GitHub (pinned to f123cdb2a2)
Solutions
- Add the missing anchor on the same mock: $mock->expects($this->once())->method('a')->id('first').
- Fix the id string so it matches exactly (ids are case-sensitive).
- If the ordering constraint is obsolete, delete the ->after('...') call.
- If the anchor lives on another mock, move both expectations onto one mock or drop the cross-mock ordering (it is not supported).
Example fix
// before
$mock->expects($this->once())->method('save')->after('load-step');
// no matcher registered with id 'load-step'
// after
$mock->expects($this->once())->method('load')->id('load-step');
$mock->expects($this->once())->method('save')->after('load-step'); Defensive patterns
Strategy: validation
Validate before calling
// register the anchor id on the same mock before any after() references it
$mock->expects($this->once())->method('load')->id('load-step');
$mock->expects($this->once())->method('save')->after('load-step'); Type guard
function afterIdIsRegistered(string $id, object $mock): bool
{
return $mock->__phpunit_getInvocationHandler()->lookupMatcher($id) !== null;
} Try / catch
try {
$subject->run($mock);
} catch (PHPUnit\Framework\MockObject\MatchBuilderNotFoundException $e) {
// the after() id has no matching id() — add the anchor expectation
} Prevention
- Always write the ->id() line first, then the ->after() line that references it.
- Ids are per-mock-object: never reference an id registered on a different instance.
- Delete both halves together when removing ordering constraints.
When it happens
Trigger: $mock->expects($this->once())->method('b')->after('first'); where no ->method('a')->id('first') exists; the id was registered on a different mock instance (e.g. after recreating the mock); typo in the id string; the id registration line was deleted while the after() remained.
Common situations: Refactoring tests and dropping the ->id() call that anchored the ordering; copying an after()-based expectation into another test that never set the id; using ids across two mocks expecting them to share a registry (each mock has its own InvocationHandler).
Related errors
- Matcher with id <%s> is already registered
- Cannot double method with invalid name "%s"
- Cannot double using a method list that contains duplicates:
- Cannot use "%s" as the name of a test double class because i
- The name "%s" is already in use
AI-assisted analysis of sebastianbergmann/phpunit@f123cdb2a2 (2026-08-23).
Data as JSON: /api/errors/348c3e2fd02f522c.
Report an issue: GitHub.