{"record":{"id":"bb5b461b65ea645c","repo":"doctrine/orm","slug":"given-property-is-not-readonly","errorCode":null,"errorMessage":"Given property is not readonly.","messagePattern":"Given property is not readonly\\.","errorType":"exception","errorClass":"InvalidArgumentException","httpStatus":null,"severity":"error","filePath":"src/Mapping/ReflectionReadonlyProperty.php","lineNumber":24,"sourceCode":"\nuse InvalidArgumentException;\nuse LogicException;\nuse ReflectionProperty;\n\nuse function assert;\nuse function func_get_args;\nuse function func_num_args;\nuse function is_object;\nuse function sprintf;\n\n/** @internal */\nfinal class ReflectionReadonlyProperty extends ReflectionProperty\n{\n    public function __construct(\n        private readonly ReflectionProperty $wrappedProperty,\n    ) {\n        if (! $wrappedProperty->isReadOnly()) {\n            throw new InvalidArgumentException('Given property is not readonly.');\n        }\n\n        parent::__construct($wrappedProperty->class, $wrappedProperty->name);\n    }\n\n    public function getValue(object|null $object = null): mixed\n    {\n        return $this->wrappedProperty->getValue(...func_get_args());\n    }\n\n    public function setValue(mixed $objectOrValue, mixed $value = null): void\n    {\n        if (func_num_args() < 2 || $objectOrValue === null || ! $this->isInitialized($objectOrValue)) {\n            $this->wrappedProperty->setValue(...func_get_args());\n\n            return;\n        }\n","sourceCodeStart":6,"sourceCodeEnd":42,"githubUrl":"https://github.com/doctrine/orm/blob/d9b9ff73016bf598ae07515f97289ce8074e97a5/src/Mapping/ReflectionReadonlyProperty.php#L6-L42","documentation":"ReflectionReadonlyProperty extends ReflectionProperty to allow exactly one initialization write to a readonly property (setValue passes through while uninitialized; a later differing write throws). Its constructor asserts the wrapped ReflectionProperty is readonly; LegacyReflectionFields only wraps properties where isReadOnly() already returned true, so this InvalidArgumentException signals direct misuse of the class.","triggerScenarios":"Directly calling new ReflectionReadonlyProperty($reflectionProperty) with a property that lacks the readonly modifier; keeping a previously-created wrapper after removing readonly from the property; custom reflection-service implementations wrapping properties unconditionally.","commonSituations":"Custom tooling copying LegacyReflectionFields::getAccessibleProperty() logic without the isReadOnly() branch; refactoring a property from readonly to mutable while cached/custom reflection wrappers persist.","solutions":["Guard the wrapping: only construct ReflectionReadonlyProperty when $reflectionProperty->isReadOnly() is true.","Reuse the library path (LegacyReflectionFields / getClassMetadata()->getReflectionProperty()) instead of constructing the wrapper yourself.","If the property is intentionally mutable, use the plain ReflectionProperty."],"exampleFix":"// before\n$refl = new ReflectionReadonlyProperty($plainRefl); // $plainRefl not readonly -> InvalidArgumentException\n\n// after\n$refl = $plainRefl->isReadOnly()\n    ? new ReflectionReadonlyProperty($plainRefl)\n    : $plainRefl;","handlingStrategy":"validation","validationCode":"$refl = $reflectionProperty->isReadOnly()\n    ? new ReflectionReadonlyProperty($reflectionProperty)\n    : $reflectionProperty;","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Only wrap properties whose isReadOnly() is true.","Use getClassMetadata()->getReflectionProperty() instead of constructing wrappers yourself.","When removing readonly from a property, search for any saved/custom wrappers that reference it."],"tags":["doctrine-orm","reflection","readonly","internal-api"],"backgroundTag":"readonly-property-misuse","analyzedSha":"d9b9ff73016bf598ae07515f97289ce8074e97a5","analyzedAt":"2026-08-21T06:13:15.863Z","schemaVersion":2},"datasetVersion":"2026-08-21T11:28:35.574Z"}