doctrine/orm · error · LogicException
Attempting to change readonly property %s::$%s.
Error message
Attempting to change readonly property %s::$%s.
What it means
ReadonlyAccessor::setValue() permits the initial write to an uninitialized (or PHP 8.4 lazy) readonly property, and permits re-writing the identical value, but throws LogicException when asked to store a different value into an already-initialized readonly property. This keeps hydration from silently breaking readonly semantics.
Source
Thrown at src/Mapping/PropertyAccessors/ReadonlyAccessor.php:43
));
}
}
public function setValue(object $object, mixed $value): void
{
/* For lazy properties, skip the isInitialized() check
because it would trigger the initialization of the whole object. */
if (
PHP_VERSION_ID >= 80400 && $this->reflectionProperty->isLazy($object)
|| ! $this->reflectionProperty->isInitialized($object)
) {
$this->parent->setValue($object, $value);
return;
}
if ($this->parent->getValue($object) !== $value) {
throw new LogicException(sprintf(
'Attempting to change readonly property %s::$%s.',
$this->reflectionProperty->getDeclaringClass()->getName(),
$this->reflectionProperty->getName(),
));
}
}
public function getValue(object $object): mixed
{
return $this->parent->getValue($object);
}
public function getUnderlyingReflector(): ReflectionProperty
{
return $this->reflectionProperty;
}
}
View on GitHub (pinned to d9b9ff7301)
Solutions
- Treat readonly properties as immutable: do not re-assign after initialization; replace the whole entity (remove + persist) when the value must change.
- In custom hydration, skip the write when the property is already initialized, or only write when the new value is identical.
- Remove 'readonly' from the property (and mapping) if the value legitimately changes during the entity lifecycle.
Example fix
// before
$accessor->setValue($entity, $newPrice); // initialized readonly prop + different value -> LogicException
// after
if (! $refl->isInitialized($entity)) {
$accessor->setValue($entity, $newPrice);
} elseif ($accessor->getValue($entity) !== $newPrice) {
throw new DomainException('Cannot modify readonly property after initialization.');
} Defensive patterns
Strategy: try-catch
Validate before calling
if (! $refl->isInitialized($entity) || $accessor->getValue($entity) === $value) {
$accessor->setValue($entity, $value);
} Try / catch
try {
$accessor->setValue($entity, $value);
} catch (\LogicException $e) {
// readonly property already initialized with a different value
throw new DomainException('Cannot update readonly field: ' . $e->getMessage(), 0, $e);
} Prevention
- Model truly immutable values as readonly and never rewrite them during hydration.
- Exclude readonly fields from partial refresh queries.
- In fixtures, construct a fresh entity instead of mutating readonly fields.
When it happens
Trigger: Hydrating or refreshing an already-loaded entity whose readonly property receives different data; custom code calling the property accessor's setValue() with a new value on an initialized readonly property; fixtures/importers that load an entity then rewrite its readonly fields in place.
Common situations: Entity refresh/partial queries returning changed data for readonly fields; data-fixture scripts mutating readonly promoted properties via reflection/accessors; double hydration of the same entity with conflicting values (e.g. different default values per query).
Related errors
- %s::$%s must be readonly property
- hydrateRowData() not implemented by this hydrator.
- {argAlias} does not exist
- This class requires PHP 8.4 or higher.
- %s::$%s must have a type when used with TypedNoDefaultProper
AI-assisted analysis of doctrine/orm@d9b9ff7301 (2026-08-21).
Data as JSON: /api/errors/dfcfc4a91e19c402.
Report an issue: GitHub.