doctrine/orm · error · InvalidArgumentException

%s::$%s must be readonly property

Error message

%s::$%s must be readonly property

What it means

ReadonlyAccessor decorates another PropertyAccessor to allow hydrating readonly properties once (initialization) while preventing later overwrites. Its constructor asserts the wrapped ReflectionProperty is declared readonly; wrapping a non-readonly property is a programming error and throws InvalidArgumentException. PropertyAccessorFactory only wraps actually-readonly properties, so this fires on direct/custom instantiation.

Source

Thrown at src/Mapping/PropertyAccessors/ReadonlyAccessor.php:21

declare(strict_types=1);

namespace Doctrine\ORM\Mapping\PropertyAccessors;

use InvalidArgumentException;
use LogicException;
use ReflectionProperty;

use function sprintf;

use const PHP_VERSION_ID;

/** @internal */
class ReadonlyAccessor implements PropertyAccessor
{
    public function __construct(private PropertyAccessor $parent, private ReflectionProperty $reflectionProperty)
    {
        if (! $this->reflectionProperty->isReadOnly()) {
            throw new InvalidArgumentException(sprintf(
                '%s::$%s must be readonly property',
                $this->reflectionProperty->getDeclaringClass()->getName(),
                $this->reflectionProperty->getName(),
            ));
        }
    }

    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;

View on GitHub (pinned to d9b9ff7301)

Solutions

  1. Only wrap properties where $reflectionProperty->isReadOnly() is true; add that check before constructing.
  2. Prefer PropertyAccessorFactory::createPropertyAccessor(), which applies the wrapping conditionally.
  3. If you removed the readonly modifier from the property intentionally, also remove the ReadonlyAccessor wrapper from your chain.

Example fix

// before
$accessor = new ReadonlyAccessor($inner, $refl); // $refl is not readonly -> InvalidArgumentException

// after
$accessor = $refl->isReadOnly()
    ? new ReadonlyAccessor($inner, $refl)
    : $inner;
Defensive patterns

Strategy: validation

Validate before calling

$accessor = $reflectionProperty->isReadOnly()
    ? new ReadonlyAccessor($inner, $reflectionProperty)
    : $inner;

Prevention

When it happens

Trigger: Directly calling new ReadonlyAccessor($parentAccessor, $reflectionProperty) where the property lacks the readonly modifier; custom property-accessor chains that wrap every property unconditionally; code copied from PropertyAccessorFactory without the isReadOnly() branch.

Common situations: Custom bundles building their own accessor pipelines; removing 'readonly' from an entity property while a cached/custom accessor chain still wraps it; experiment code that assumes the guard is optional.

Related errors


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