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

  1. Add the missing anchor on the same mock: $mock->expects($this->once())->method('a')->id('first').
  2. Fix the id string so it matches exactly (ids are case-sensitive).
  3. If the ordering constraint is obsolete, delete the ->after('...') call.
  4. 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

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


AI-assisted analysis of sebastianbergmann/phpunit@f123cdb2a2 (2026-08-23). Data as JSON: /api/errors/348c3e2fd02f522c. Report an issue: GitHub.