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 final

What it means

Thrown by TestDoubleBuilder::doubleProperties() when the property you want to double is declared final. A final property cannot be redeclared with property hooks in the generated subclass, so PHPUnit refuses the request after detecting ReflectionProperty::isFinal().

Source

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

                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.
     *
     * @param array<mixed> $arguments
     *
     * @return $this

View on GitHub (pinned to f123cdb2a2)

Solutions

  1. Remove the final property from the list passed to doubleProperties()
  2. Supply the property's value via setConstructorArgs() instead of hook expectations
  3. Rewrite the test to exercise the real property behavior rather than doubling it
  4. Remove 'final' from the property declaration if hook doubling in tests is a hard requirement

Example fix

// before
$double = (new TestDoubleBuilder(Order::class))
    ->doubleProperties(['status']) // 'status' is final
    ->generate();

// after
$double = (new TestDoubleBuilder(Order::class))
    ->doubleProperties(['notes'])
    ->generate();
Defensive patterns

Strategy: type-guard

Validate before calling

$property = new ReflectionProperty(Order::class, 'status');
if ($property->isFinal()) {
    // skip doubling 'status', use constructor args instead
}

Type guard

/** @param list<non-empty-string> $properties @return list<non-empty-string> */
function nonFinalProperties(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)->isFinal()
            && $reflector->getProperty($name)->hasType(),
    ));
}

Prevention

When it happens

Trigger: Calling doubleProperties([...]) with a property name that is declared with the 'final' modifier on the class being doubled.

Common situations: Codebases adopting PHP final properties for immutability; tests written before the property was made final that still try to hook it.

Related errors


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