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
- Only wrap properties where $reflectionProperty->isReadOnly() is true; add that check before constructing.
- Prefer PropertyAccessorFactory::createPropertyAccessor(), which applies the wrapping conditionally.
- 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
- Delegate accessor construction to PropertyAccessorFactory.
- Re-check property modifiers whenever you hand-build accessor chains.
- Treat @internal classes as unstable: wrap them behind your own factory.
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
- Attempting to change readonly property %s::$%s.
- %s::$%s must have a type when used with TypedNoDefaultProper
- %s::$%s must not be nullable when used with TypedNoDefaultPr
- Given property is not readonly.
- The attribute "%s" is repeatable. Call getPropertyAttributeC
AI-assisted analysis of doctrine/orm@d9b9ff7301 (2026-08-21).
Data as JSON: /api/errors/646fde8c620e2b61.
Report an issue: GitHub.