doctrine/orm · error · InvalidArgumentException

%s::$%s must not be nullable when used with TypedNoDefaultPr

Error message

%s::$%s must not be nullable when used with TypedNoDefaultPropertyAccessor

What it means

The second invariant of TypedNoDefaultPropertyAccessor: since its setValue(null) path unsets the property to represent 'uninitialized', a nullable type would make null and uninitialized indistinguishable and break the strategy. The constructor therefore throws InvalidArgumentException when the property type allows null, even if it is typed.

Source

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

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) {
            if ($this->unsetter === null) {
                $propertyName   = $this->reflectionProperty->getName();
                $this->unsetter = function () use ($propertyName): void {
                    unset($this->$propertyName);
                };
            }

            $unsetter = $this->unsetter->bindTo($object, $this->reflectionProperty->getDeclaringClass()->getName());

View on GitHub (pinned to d9b9ff7301)

Solutions

  1. Exclude nullable properties from this accessor; wrap only `public string $x;`-style declarations.
  2. Use PropertyAccessorFactory::createPropertyAccessor() — it already checks allowsNull() before wrapping.
  3. If the property must be nullable, remove the TypedNoDefaultPropertyAccessor wrapper and handle null writes with the parent accessor.

Example fix

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

// after
$accessor = (! $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 constructing TypedNoDefaultPropertyAccessor for a property declared nullable (e.g. `public ?string $name;` or `public string|null $name;`) — typed but allowing null.

Common situations: Custom accessor pipelines wrapping nullable fields; widening a property's type to nullable later while a custom wrapper still targets it; copied factory logic without the allowsNull() branch.

Related errors


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