sebastianbergmann/phpunit · error · MatcherAlreadyRegisteredException
Matcher with id <%s> is already registered
Error message
Matcher with id <%s> is already registered
What it means
Thrown by InvocationHandler::registerMatcher() when an expectation id is registered twice on the same double. Matcher ids are per-mock-object keys in matcherMap (used by ->after($id) ordering); registering the same id again would silently overwrite the first matcher, so PHPUnit refuses it.
Source
Thrown at src/Framework/MockObject/Runtime/InvocationHandler.php:109
* @param non-empty-string $id
*/
public function lookupMatcher(string $id): ?Matcher
{
return $this->matcherMap[$id] ?? null;
}
/**
* Registers a matcher with the identification $id. The matcher can later be
* looked up using lookupMatcher() to figure out if it has been invoked.
*
* @param non-empty-string $id
*
* @throws MatcherAlreadyRegisteredException
*/
public function registerMatcher(string $id, Matcher $matcher): void
{
if (isset($this->matcherMap[$id])) {
throw new MatcherAlreadyRegisteredException($id);
}
$this->matcherMap[$id] = $matcher;
}
/**
* @throws TestDoubleSealedException
*/
public function expects(InvocationOrder $rule): InvocationMocker|InvocationStubber
{
if ($this->sealed) {
throw new TestDoubleSealedException;
}
$matcher = new Matcher($rule, $this->className);
$this->addMatcher($matcher);
if ($this->isMockObject) {View on GitHub (pinned to f123cdb2a2)
Solutions
- Make ids unique per expectation on the same mock, e.g. 'step-1', 'step-2' or include the method name in the id.
- Delete the ->id() call on expectations that are never referenced by ->after().
- If ids come from a generator, ensure the generator state is not shared/reset between expectations.
Example fix
// before
$mock->expects($this->once())->method('load')->id('step');
$mock->expects($this->once())->method('save')->id('step');
// after
$mock->expects($this->once())->method('load')->id('load-step');
$mock->expects($this->once())->method('save')->id('save-step'); Defensive patterns
Strategy: validation
Validate before calling
$usedIds = [];
$id = 'step';
for ($i = 1; isset($usedIds[$id]); $i++) {
$id = 'step_' . $i;
}
$usedIds[$id] = true;
$mock->expects($this->once())->method('a')->id($id); Type guard
// ids are plain strings; guard against reuse per mock object
function uniqueId(string $base, array &$used): string
{
$id = $base;
$i = 1;
while (isset($used[$id])) {
$id = $base . '_' . $i++;
}
return $used[$id] = $id;
} Try / catch
try {
$mock->expects($this->once())->method('b')->id($id);
} catch (PHPUnit\Framework\MockObject\MatcherAlreadyRegisteredException $e) {
// choose a different id or drop ->id() if nothing references it
} Prevention
- Only assign ->id() to expectations that a later ->after() actually references.
- Derive ids from the method name ('save-step') so copy-paste collisions are obvious.
- Keep paired id()/after() calls adjacent in the test for reviewability.
When it happens
Trigger: $mock->expects($this->once())->method('a')->id('step'); $mock->expects($this->once())->method('b')->id('step'); — the second ->id('step') triggers it. The lookup table lives on the specific mock object, so the same id on a different mock is fine.
Common situations: Copy-pasted expectation blocks in tests where the id string was not updated; ids generated from counters that reset (e.g. 'call' . ($i - $i)); test helpers that tag every expectation with a constant id like 'target'.
Related errors
- No builder found for match builder identification <%s>
- 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/02dfc78fe583f960.
Report an issue: GitHub.