{"record":{"id":"02dfc78fe583f960","repo":"sebastianbergmann/phpunit","slug":"matcher-with-id-s-is-already-registered","errorCode":null,"errorMessage":"Matcher with id <%s> is already registered","messagePattern":"Matcher with id <(.+?)> is already registered","errorType":"exception","errorClass":"MatcherAlreadyRegisteredException","httpStatus":null,"severity":"error","filePath":"src/Framework/MockObject/Runtime/InvocationHandler.php","lineNumber":109,"sourceCode":"     * @param non-empty-string $id\n     */\n    public function lookupMatcher(string $id): ?Matcher\n    {\n        return $this->matcherMap[$id] ?? null;\n    }\n\n    /**\n     * Registers a matcher with the identification $id. The matcher can later be\n     * looked up using lookupMatcher() to figure out if it has been invoked.\n     *\n     * @param non-empty-string $id\n     *\n     * @throws MatcherAlreadyRegisteredException\n     */\n    public function registerMatcher(string $id, Matcher $matcher): void\n    {\n        if (isset($this->matcherMap[$id])) {\n            throw new MatcherAlreadyRegisteredException($id);\n        }\n\n        $this->matcherMap[$id] = $matcher;\n    }\n\n    /**\n     * @throws TestDoubleSealedException\n     */\n    public function expects(InvocationOrder $rule): InvocationMocker|InvocationStubber\n    {\n        if ($this->sealed) {\n            throw new TestDoubleSealedException;\n        }\n\n        $matcher = new Matcher($rule, $this->className);\n        $this->addMatcher($matcher);\n\n        if ($this->isMockObject) {","sourceCodeStart":91,"sourceCodeEnd":127,"githubUrl":"https://github.com/sebastianbergmann/phpunit/blob/f123cdb2a2d49f15025794166cfed8bda8627dd2/src/Framework/MockObject/Runtime/InvocationHandler.php#L91-L127","documentation":"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.","triggerScenarios":"$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.","commonSituations":"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'.","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."],"exampleFix":"// before\n$mock->expects($this->once())->method('load')->id('step');\n$mock->expects($this->once())->method('save')->id('step');\n\n// after\n$mock->expects($this->once())->method('load')->id('load-step');\n$mock->expects($this->once())->method('save')->id('save-step');","handlingStrategy":"validation","validationCode":"$usedIds = [];\n$id = 'step';\nfor ($i = 1; isset($usedIds[$id]); $i++) {\n    $id = 'step_' . $i;\n}\n$usedIds[$id] = true;\n$mock->expects($this->once())->method('a')->id($id);","typeGuard":"// ids are plain strings; guard against reuse per mock object\nfunction uniqueId(string $base, array &$used): string\n{\n    $id = $base;\n    $i = 1;\n\n    while (isset($used[$id])) {\n        $id = $base . '_' . $i++;\n    }\n\n    return $used[$id] = $id;\n}","tryCatchPattern":"try {\n    $mock->expects($this->once())->method('b')->id($id);\n} catch (PHPUnit\\Framework\\MockObject\\MatcherAlreadyRegisteredException $e) {\n    // choose a different id or drop ->id() if nothing references it\n}","preventionTips":["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."],"tags":["phpunit","mock-object","matcher-id","duplicate-id"],"backgroundTag":"duplicate-id-registration","analyzedSha":"f123cdb2a2d49f15025794166cfed8bda8627dd2","analyzedAt":"2026-08-23T01:20:58.058Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}