{"record":{"id":"29a926addaf452b7","repo":"sebastianbergmann/phpunit","slug":"trying-to-double-property-s-of-class-s-with-29a926","errorCode":null,"errorMessage":"Trying to double property \"%s\" of class \"%s\" with doubleProperties(), but it is readonly","messagePattern":"Trying to double property \"(.+?)\" of class \"(.+?)\" with doubleProperties\\(\\), but it is readonly","errorType":"exception","errorClass":"PropertyCannotBeDoubledException","httpStatus":null,"severity":"error","filePath":"src/Framework/MockObject/TestDoubleBuilder.php","lineNumber":139,"sourceCode":"        }\n\n        foreach ($properties as $propertyName) {\n            if (!$reflector->hasProperty($propertyName)) {\n                throw new PropertyCannotBeDoubledException($this->type, $propertyName, 'it does not exist');\n            }\n\n            $property = $reflector->getProperty($propertyName);\n\n            if (!$property->isPublic()) {\n                throw new PropertyCannotBeDoubledException($this->type, $propertyName, 'it is not public');\n            }\n\n            if ($property->isStatic()) {\n                throw new PropertyCannotBeDoubledException($this->type, $propertyName, 'it is static');\n            }\n\n            if ($property->isReadOnly()) {\n                throw new PropertyCannotBeDoubledException($this->type, $propertyName, 'it is readonly');\n            }\n\n            if ($property->isFinal()) {\n                throw new PropertyCannotBeDoubledException($this->type, $propertyName, 'it is final');\n            }\n\n            if (!$property->hasType()) {\n                throw new PropertyCannotBeDoubledException($this->type, $propertyName, 'it does not declare a type');\n            }\n        }\n\n        $this->doubledProperties = array_merge($this->doubledProperties, $properties);\n\n        return $this;\n    }\n\n    /**\n     * Specifies the arguments for the constructor.","sourceCodeStart":121,"sourceCodeEnd":157,"githubUrl":"https://github.com/sebastianbergmann/phpunit/blob/f123cdb2a2d49f15025794166cfed8bda8627dd2/src/Framework/MockObject/TestDoubleBuilder.php#L121-L157","documentation":"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().","triggerScenarios":"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.","commonSituations":"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.","solutions":["Remove the readonly property from the list passed to doubleProperties()","If you only need a fixed value, keep the property untouched and configure it through the constructor (setConstructorArgs()) instead of doubling it","Restructure the test to assert the real value instead of configuring get/set hook expectations for that property","If the type genuinely needs hook doubling in tests, drop 'readonly' from the property declaration in the production code"],"exampleFix":"// before\n$double = (new TestDoubleBuilder(Invoice::class))\n    ->doubleProperties(['lines', 'total']) // 'total' is readonly\n    ->generate();\n\n// after\n$double = (new TestDoubleBuilder(Invoice::class))\n    ->doubleProperties(['lines']) // only double non-readonly properties\n    ->setConstructorArgs([['line 1'], 100])\n    ->generate();","handlingStrategy":"type-guard","validationCode":"$reflector = new ReflectionClass(Invoice::class);\n$doublable = array_filter(\n    ['lines', 'total'],\n    static fn (string $name) => $reflector->hasProperty($name)\n        && $reflector->getProperty($name)->isPublic()\n        && !$reflector->getProperty($name)->isStatic()\n        && !$reflector->getProperty($name)->isReadOnly(),\n);","typeGuard":"/** @param class-string $class @param list<string> $properties @return list<string> */\nfunction doublableProperties(string $class, array $properties): array\n{\n    $reflector = new ReflectionClass($class);\n\n    return array_values(array_filter(\n        $properties,\n        static fn (string $name): bool => $reflector->hasProperty($name)\n            && $reflector->getProperty($name)->isPublic()\n            && !$reflector->getProperty($name)->isStatic()\n            && !$reflector->getProperty($name)->isReadOnly(),\n    ));\n}","tryCatchPattern":null,"preventionTips":["Run doublableProperties() over the list before calling doubleProperties() in shared test helpers","Prefer configuring readonly properties through setConstructorArgs() with real values","Review the class's property modifiers before adding new names to a doubling list"],"tags":["phpunit","mock-object","test-double","readonly-property","reflection"],"backgroundTag":"readonly-property-override","analyzedSha":"f123cdb2a2d49f15025794166cfed8bda8627dd2","analyzedAt":"2026-08-23T01:20:58.058Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}