{"record":{"id":"3a16dfa2769821d5","repo":"doctrine/orm","slug":"attempting-to-change-readonly-property-s-s-3a16df","errorCode":null,"errorMessage":"Attempting to change readonly property %s::$%s.","messagePattern":"Attempting to change readonly property (.+?)::\\$(.+?)\\.","errorType":"exception","errorClass":"LogicException","httpStatus":null,"severity":"error","filePath":"src/Mapping/ReflectionReadonlyProperty.php","lineNumber":46,"sourceCode":"    }\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\n        assert(is_object($objectOrValue));\n\n        if (parent::getValue($objectOrValue) !== $value) {\n            throw new LogicException(sprintf('Attempting to change readonly property %s::$%s.', $this->class, $this->name));\n        }\n    }\n}\n","sourceCodeStart":28,"sourceCodeEnd":50,"githubUrl":"https://github.com/doctrine/orm/blob/d9b9ff73016bf598ae07515f97289ce8074e97a5/src/Mapping/ReflectionReadonlyProperty.php#L28-L50","documentation":"Doctrine wraps PHP readonly properties in ReflectionReadonlyProperty (legacy reflection mode, installed by LegacyReflectionFields) so hydration can initialize them once. setValue() only bypasses PHP's readonly restriction while the property is uninitialized, or when the new value is identical (===) to the current one (idempotent re-set during re-hydration/flush). Writing a DIFFERENT value to an already-initialized readonly property is rejected with this LogicException.","triggerScenarios":"flush() attempts to write a changed value to a readonly property: a #[Column] readonly field combined with #[Version] (version increments), a generated column value written back that differs, refresh()/re-hydration of a managed entity whose stored row changed (trigger/other process), or user code mutating the property via reflection before flush.","commonSituations":"Making all constructor-promoted entity properties readonly while still expecting updates; readonly + @Version columns; entities re-hydrated from rows modified by DB triggers or concurrent requests; refresh() after another process changed the row.","solutions":["Remove `readonly` from any property that can legitimately change (version fields, updatable or generated columns)","If the value is unchanged, ensure you assign the identical value (=== strict comparison; int vs string type mismatch counts as different)","Never declare #[Version] fields readonly","Audit DB triggers/row-level security if refresh()/flush re-hydration sees different stored values"],"exampleFix":"// before\nfinal class User {\n    public function __construct(\n        #[Column] public readonly string $name,\n    ) {}\n}\n$u->name = 'new'; // throws on flush\n\n// after\nfinal class User {\n    public function __construct(\n        #[Column] public string $name,\n    ) {}\n}","handlingStrategy":"validation","validationCode":"$rp = new ReflectionProperty($entity, $field);\nif ($rp->isReadOnly() && $rp->isInitialized($entity) && $rp->getValue($entity) !== $newValue) {\n    // refuse: would throw on flush\n}","typeGuard":"function wouldOverwriteInitializedReadonly(object $entity, string $field, mixed $value): bool\n{\n    $rp = new ReflectionProperty($entity, $field);\n    return $rp->isReadOnly() && $rp->isInitialized($entity) && $rp->getValue($entity) !== $value;\n}","tryCatchPattern":"try { $em->flush(); } catch (LogicException $e) { if (str_starts_with($e->getMessage(), 'Attempting to change readonly property')) { /* drop the change or make the property mutable */ } throw $e; }","preventionTips":["Never mark versioned, updatable or generated columns readonly","Keep entity integration tests that flush changed entities","Treat readonly entity properties as truly immutable in application code"],"tags":["php-readonly","orm-mapping","flush","hydration"],"backgroundTag":"readonly-property-write","analyzedSha":"d9b9ff73016bf598ae07515f97289ce8074e97a5","analyzedAt":"2026-08-21T06:13:15.863Z","schemaVersion":2},"datasetVersion":"2026-08-21T11:28:35.574Z"}