sebastianbergmann/phpunit · error · ClassIsFinalException
Class "%s" is declared "final" and cannot be doubled
Error message
Class "%s" is declared "final" and cannot be doubled
What it means
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.
Source
Thrown at src/Framework/MockObject/Generator/Generator.php:394
if (class_exists($_mockClassName['fullClassName'])) {
$isClass = true;
} elseif (interface_exists($_mockClassName['fullClassName'])) {
$isInterface = true;
}
$class = $this->reflectClass($_mockClassName['fullClassName']);
if ($class->isAnonymous()) {
throw new ClassIsAnonymousException($_mockClassName['fullClassName']);
}
if ($class->isEnum()) {
throw new ClassIsEnumerationException($_mockClassName['fullClassName']);
}
if ($class->isFinal()) {
throw new ClassIsFinalException($_mockClassName['fullClassName']);
}
if ($class->isReadOnly()) {
$isReadonly = true;
}
// @see https://github.com/sebastianbergmann/phpunit/issues/2995
if ($isInterface && $class->implementsInterface(Throwable::class)) {
$actualClassName = Exception::class;
$additionalInterfaces[] = $class->getName();
$isInterface = false;
$class = $this->reflectClass($actualClassName);
foreach ($this->userDefinedInterfaceMethods($_mockClassName['fullClassName']) as $method) {
$methodName = $method->getName();
if ($class->hasMethod($methodName)) {
$classMethod = $class->getMethod($methodName);View on GitHub (pinned to f123cdb2a2)
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
Example fix
// before
final class PaymentGateway { public function charge(int $cents): bool { ... } }
$gateway = $this->createMock(PaymentGateway::class); // final -> exception
// after
interface PaymentGateway { public function charge(int $cents): bool; }
final class StripeGateway implements PaymentGateway { ... }
$gateway = $this->createMock(PaymentGateway::class); Defensive patterns
Strategy: validation
Validate before calling
if ((new ReflectionClass($type))->isFinal()) {
$type = class_implements($type)[0]
?? throw new RuntimeException("Cannot double final class {$type} without an interface");
}
$mock = $this->createMock($type); Type guard
function isMockableType(string $type): bool
{
if (!class_exists($type) && !interface_exists($type)) {
return false;
}
$r = new ReflectionClass($type);
return !$r->isAnonymous() && !$r->isEnum() && !$r->isFinal();
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Class "%s" is an anonymous class and cannot be doubled
- Class "%s" is an enumeration and cannot be doubled
- Class or interface "%s" does not exist
- At least two interfaces must be specified
- Interface "%s" does not exist
AI-assisted analysis of sebastianbergmann/phpunit@f123cdb2a2 (2026-08-23).
Data as JSON: /api/errors/76d0349e56ccf8a6.
Report an issue: GitHub.