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

  1. Treat readonly properties as immutable: do not re-assign after initialization; replace the whole entity (remove + persist) when the value must change.
  2. In custom hydration, skip the write when the property is already initialized, or only write when the new value is identical.
  3. 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

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


AI-assisted analysis of doctrine/orm@d9b9ff7301 (2026-08-21). Data as JSON: /api/errors/dfcfc4a91e19c402. Report an issue: GitHub.