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 readonly

What it means

Thrown by PHPUnit's TestDoubleBuilder::doubleProperties() when you ask it to generate property hooks (get/set) for a property that is declared readonly. The test double is generated as a subclass of the mocked type, and PHP forbids redeclaring a readonly property with hooks in a child class, so PHPUnit rejects the request up front via ReflectionProperty::isReadOnly().

Source

Thrown at src/Framework/MockObject/TestDoubleBuilder.php:139

        }

        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');
            }
        }

        $this->doubledProperties = array_merge($this->doubledProperties, $properties);

        return $this;
    }

    /**
     * Specifies the arguments for the constructor.

View on GitHub (pinned to f123cdb2a2)

Solutions

  1. Remove the readonly property from the list passed to doubleProperties()
  2. If you only need a fixed value, keep the property untouched and configure it through the constructor (setConstructorArgs()) instead of doubling it
  3. Restructure the test to assert the real value instead of configuring get/set hook expectations for that property
  4. If the type genuinely needs hook doubling in tests, drop 'readonly' from the property declaration in the production code

Example fix

// before
$double = (new TestDoubleBuilder(Invoice::class))
    ->doubleProperties(['lines', 'total']) // 'total' is readonly
    ->generate();

// after
$double = (new TestDoubleBuilder(Invoice::class))
    ->doubleProperties(['lines']) // only double non-readonly properties
    ->setConstructorArgs([['line 1'], 100])
    ->generate();
Defensive patterns

Strategy: type-guard

Validate before calling

$reflector = new ReflectionClass(Invoice::class);
$doublable = array_filter(
    ['lines', 'total'],
    static fn (string $name) => $reflector->hasProperty($name)
        && $reflector->getProperty($name)->isPublic()
        && !$reflector->getProperty($name)->isStatic()
        && !$reflector->getProperty($name)->isReadOnly(),
);

Type guard

/** @param class-string $class @param list<string> $properties @return list<string> */
function doublableProperties(string $class, array $properties): array
{
    $reflector = new ReflectionClass($class);

    return array_values(array_filter(
        $properties,
        static fn (string $name): bool => $reflector->hasProperty($name)
            && $reflector->getProperty($name)->isPublic()
            && !$reflector->getProperty($name)->isStatic()
            && !$reflector->getProperty($name)->isReadOnly(),
    ));
}

Prevention

When it happens

Trigger: Calling doubleProperties(['someProperty']) (or a builder/wrapper that forwards to it) where someProperty is declared 'readonly' or is a readonly promoted constructor property of the doubled class.

Common situations: Trying to double properties on PHP 8.1+ value objects / DTOs that use readonly promoted constructor properties; upgrading a codebase to readonly and re-running an older test suite that doubled those properties.

Related errors


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