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

  1. Expose the value through a method instead: mock the getter/setter method (onlyMethods()) rather than doubling the non-public property.
  2. If you control the class and the test really needs hook doubling, widen the property to public (weigh whether that damages the design).
  3. Drop the property from doubleProperties() and assert behavior via the class's public API.
  4. 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

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


AI-assisted analysis of sebastianbergmann/phpunit@f123cdb2a2 (2026-08-23). Data as JSON: /api/errors/e19aca575760c053. Report an issue: GitHub.