{"record":{"id":"76d0349e56ccf8a6","repo":"sebastianbergmann/phpunit","slug":"class-s-is-declared-final-and-cannot-be-doubl","errorCode":null,"errorMessage":"Class \"%s\" is declared \"final\" and cannot be doubled","messagePattern":"Class \"(.+?)\" is declared \"final\" and cannot be doubled","errorType":"exception","errorClass":"ClassIsFinalException","httpStatus":null,"severity":"error","filePath":"src/Framework/MockObject/Generator/Generator.php","lineNumber":394,"sourceCode":"\n        if (class_exists($_mockClassName['fullClassName'])) {\n            $isClass = true;\n        } elseif (interface_exists($_mockClassName['fullClassName'])) {\n            $isInterface = true;\n        }\n\n        $class = $this->reflectClass($_mockClassName['fullClassName']);\n\n        if ($class->isAnonymous()) {\n            throw new ClassIsAnonymousException($_mockClassName['fullClassName']);\n        }\n\n        if ($class->isEnum()) {\n            throw new ClassIsEnumerationException($_mockClassName['fullClassName']);\n        }\n\n        if ($class->isFinal()) {\n            throw new ClassIsFinalException($_mockClassName['fullClassName']);\n        }\n\n        if ($class->isReadOnly()) {\n            $isReadonly = true;\n        }\n\n        // @see https://github.com/sebastianbergmann/phpunit/issues/2995\n        if ($isInterface && $class->implementsInterface(Throwable::class)) {\n            $actualClassName        = Exception::class;\n            $additionalInterfaces[] = $class->getName();\n            $isInterface            = false;\n            $class                  = $this->reflectClass($actualClassName);\n\n            foreach ($this->userDefinedInterfaceMethods($_mockClassName['fullClassName']) as $method) {\n                $methodName = $method->getName();\n\n                if ($class->hasMethod($methodName)) {\n                    $classMethod = $class->getMethod($methodName);","sourceCodeStart":376,"sourceCodeEnd":412,"githubUrl":"https://github.com/sebastianbergmann/phpunit/blob/f123cdb2a2d49f15025794166cfed8bda8627dd2/src/Framework/MockObject/Generator/Generator.php#L376-L412","documentation":"Test doubles are implemented as generated subclasses of the mocked type, so a class declared final cannot be extended; ReflectionClass::isFinal() on the target triggers ClassIsFinalException before any code generation. This guard exists to make the attempt fail loudly instead of producing a fatal error.","triggerScenarios":"createMock(FinalClass::class), createStub(FinalClass::class), or MockBuilder usage targeting any class declared with the final keyword; also classes that are final via @final annotation only will NOT trigger this (only real final), but readonly classes and many framework utility classes are genuinely final.","commonSituations":"Trying to mock final framework/vendor classes (e.g. Symfony's final utility classes, Carbon periods); mocking final value objects; production code marked final during a 'final by default' policy while tests still mock it; upgrading a dependency that newly marked a class final.","solutions":["Remove the final modifier from the class (preferred when tests are a legitimate reason; consider final-by-default exceptions for this class)","Mock an interface the final class implements, and type-hint the interface in the SUT so the mock can be injected","Extract the behavior you need to fake into a non-final collaborator class and mock that","As a last resort for third-party final classes, wrap them in your own adapter and mock the adapter"],"exampleFix":"// before\nfinal class PaymentGateway { public function charge(int $cents): bool { ... } }\n$gateway = $this->createMock(PaymentGateway::class); // final -> exception\n\n// after\ninterface PaymentGateway { public function charge(int $cents): bool; }\nfinal class StripeGateway implements PaymentGateway { ... }\n$gateway = $this->createMock(PaymentGateway::class);","handlingStrategy":"validation","validationCode":"if ((new ReflectionClass($type))->isFinal()) {\n    $type = class_implements($type)[0]\n        ?? throw new RuntimeException(\"Cannot double final class {$type} without an interface\");\n}\n$mock = $this->createMock($type);","typeGuard":"function isMockableType(string $type): bool\n{\n    if (!class_exists($type) && !interface_exists($type)) {\n        return false;\n    }\n    $r = new ReflectionClass($type);\n    return !$r->isAnonymous() && !$r->isEnum() && !$r->isFinal();\n}","tryCatchPattern":null,"preventionTips":["Type-hint interfaces in SUT collaborators so mocks never need the concrete (possibly final) class","Reserve final for value objects you never need to double; drop final from services you must mock","Pin a dependency-review check for newly-final classes in vendor upgrades"],"tags":["php","phpunit","mockbuilder","final-class","createmock"],"backgroundTag":"unmockable-type","analyzedSha":"f123cdb2a2d49f15025794166cfed8bda8627dd2","analyzedAt":"2026-08-23T01:20:58.058Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}