doctrine/orm · error · InvalidArgumentException

%s::$%s must have a type when used with TypedNoDefaultProper

Error message

%s::$%s must have a type when used with TypedNoDefaultPropertyAccessor

What it means

TypedNoDefaultPropertyAccessor implements the 'typed property without default value' strategy: on setValue(null) it unsets the property so PHP reports it as uninitialized rather than null. That trick only works for declared, non-nullable types, so the constructor requires the ReflectionProperty to have a type and throws InvalidArgumentException otherwise. PropertyAccessorFactory only wraps typed non-nullable properties, so this fires on direct/custom instantiation.

Source

Thrown at src/Mapping/PropertyAccessors/TypedNoDefaultPropertyAccessor.php:22

namespace Doctrine\ORM\Mapping\PropertyAccessors;

use Closure;
use InvalidArgumentException;
use ReflectionProperty;

use function assert;
use function sprintf;

/** @internal */
class TypedNoDefaultPropertyAccessor implements PropertyAccessor
{
    private Closure|null $unsetter = null;

    public function __construct(private PropertyAccessor $parent, private ReflectionProperty $reflectionProperty)
    {
        if (! $this->reflectionProperty->hasType()) {
            throw new InvalidArgumentException(sprintf(
                '%s::$%s must have a type when used with TypedNoDefaultPropertyAccessor',
                $this->reflectionProperty->getDeclaringClass()->getName(),
                $this->reflectionProperty->getName(),
            ));
        }

        if ($this->reflectionProperty->getType()->allowsNull()) {
            throw new InvalidArgumentException(sprintf(
                '%s::$%s must not be nullable when used with TypedNoDefaultPropertyAccessor',
                $this->reflectionProperty->getDeclaringClass()->getName(),
                $this->reflectionProperty->getName(),
            ));
        }
    }

    public function setValue(object $object, mixed $value): void
    {
        if ($value === null) {

View on GitHub (pinned to d9b9ff7301)

Solutions

  1. Only wrap typed, non-nullable properties; add if (! $refl->hasType() || $refl->getType()->allowsNull()) skip.
  2. Prefer PropertyAccessorFactory::createPropertyAccessor(), which applies the wrapping conditionally.
  3. Add a native type declaration to the property if it should participate in this strategy.

Example fix

// before
$accessor = new TypedNoDefaultPropertyAccessor($inner, $refl); // untyped $refl -> InvalidArgumentException

// after
$accessor = ($refl->hasType() && ! $refl->getType()->allowsNull())
    ? new TypedNoDefaultPropertyAccessor($inner, $refl)
    : $inner;
Defensive patterns

Strategy: validation

Validate before calling

$accessor = ($refl->hasType() && ! $refl->getType()->allowsNull())
    ? new TypedNoDefaultPropertyAccessor($inner, $refl)
    : $inner;

Prevention

When it happens

Trigger: Directly calling new TypedNoDefaultPropertyAccessor($parent, $reflectionProperty) for a property declared without a type (legacy `public $foo;`) — the untyped/nullable case is explicitly rejected by the constructor.

Common situations: Custom property-accessor chains that wrap every property unconditionally; legacy entities predating typed properties; code copied from PropertyAccessorFactory without the hasType()/allowsNull() guard.

Related errors


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