sebastianbergmann/phpunit · error · PropertyCannotBeDoubledException
Trying to double property "%s" of class "%s" with doubleProp
Error message
Trying to double property "%s" of class "%s" with doubleProperties(), but it is not public
What it means
Property doubling works by generating get/set hooks on the mocked class, which is only meaningful for public instance properties — a non-public property's hooks cannot be overridden from the generated subclass. After confirming the property exists, doubleProperties() reflects it and throws PropertyCannotBeDoubledException with reason 'it is not public' when $property->isPublic() is false (TestDoubleBuilder.php:128-132). Protected/private (and PHP 8.1+ readonly, which implies non-public contexts) properties therefore cannot be doubled.
Source
Thrown at src/Framework/MockObject/TestDoubleBuilder.php:131
// @codeCoverageIgnoreStart
} catch (\ReflectionException $e) {
throw new ReflectionException(
$e->getMessage(),
$e->getCode(),
$e,
);
// @codeCoverageIgnoreEnd
}
foreach ($properties as $propertyName) {
if (!$reflector->hasProperty($propertyName)) {
throw new PropertyCannotBeDoubledException($this->type, $propertyName, 'it does not exist');
}
$property = $reflector->getProperty($propertyName);
if (!$property->isPublic()) {
throw new PropertyCannotBeDoubledException($this->type, $propertyName, 'it is not public');
}
if ($property->isStatic()) {
throw new PropertyCannotBeDoubledException($this->type, $propertyName, 'it is static');
}
if ($property->isReadOnly()) {
throw new PropertyCannotBeDoubledException($this->type, $propertyName, 'it is readonly');
}
if ($property->isFinal()) {
throw new PropertyCannotBeDoubledException($this->type, $propertyName, 'it is final');
}
if (!$property->hasType()) {
throw new PropertyCannotBeDoubledException($this->type, $propertyName, 'it does not declare a type');
}
}View on GitHub (pinned to f123cdb2a2)
Solutions
- Expose the value through a method instead: mock the getter/setter method (onlyMethods()) rather than doubling the non-public property.
- If you control the class and the test really needs hook doubling, widen the property to public (weigh whether that damages the design).
- Drop the property from doubleProperties() and assert behavior via the class's public API.
- If the property is private on a parent but public on the mocked class itself, double it on the class where it is declared public.
Example fix
// before
class Order { protected array $items = []; }
$mock = $this->getMockBuilder(Order::class)
->doubleProperties(['items']) // 'it is not public'
->getMock();
// after: double the accessor method instead of the protected property
$mock = $this->getMockBuilder(Order::class)
->onlyMethods(['getItems'])
->getMock(); Defensive patterns
Strategy: validation
Validate before calling
$reflector = new ReflectionClass(Order::class);
foreach ($props as $name) {
$p = $reflector->getProperty($name);
if (!$p->isPublic()) {
self::fail("\${$name} is {$p->getVisibility()} — mock the getter instead of doubling it");
}
} Type guard
/** @return list<non-empty-string> only public, non-static, non-readonly declared props */
function doubleableProperties(string $class): array
{
$r = new ReflectionClass($class);
$doubleable = [];
foreach ($r->getProperties(ReflectionProperty::IS_PUBLIC) as $p) {
if (!$p->isStatic() && !$p->isReadOnly() && !$p->isFinal()) {
$doubleable[] = $p->getName();
}
}
return $doubleable;
} Try / catch
use PHPUnit\Framework\MockObject\PropertyCannotBeDoubledException;
try {
$builder->doubleProperties($names);
} catch (PropertyCannotBeDoubledException $e) {
if (str_contains($e->getMessage(), 'not public')) {
$builder->onlyMethods($getterNames); // fall back to method mocking
} else {
throw $e;
}
} Prevention
- Default to mocking accessor methods; reserve property doubling for public fields that are part of the public API.
- After visibility refactors, re-run property-doubling tests — they are the canary for lost public surface.
- Know the full rule set: public, non-static, non-readonly, non-final, typed properties only.
- Use promoted constructor promotion carefully: private(set)/readonly promoted props fall into this failure.
When it happens
Trigger: $this->getMockBuilder(Order::class)->doubleProperties(['items']) where Order declares protected array $items or private ?float $total: the property is found by hasProperty() but isPublic() returns false, so the 'it is not public' variant of PropertyCannotBeDoubledException is thrown before any mock code is generated.
Common situations: Encapsulation refactors that changed a public property to private/protected while tests still doubled it; entities that expose getters instead of public fields; assuming a promoted constructor property is public when it was promoted as private/private(set); value objects with readonly public properties drifting into the non-public branch via accessor changes.
Related errors
- Trying to double property "%s" of class "%s" with doubleProp
- Trying to double property "%s" of class "%s" with doubleProp
- Trying to configure method "%s" with onlyMethods(), but it d
- Data Provider method %s::%s() is not public
- Comparison method %s::%s() does not exist.
AI-assisted analysis of sebastianbergmann/phpunit@f123cdb2a2 (2026-08-23).
Data as JSON: /api/errors/e19aca575760c053.
Report an issue: GitHub.