Skip to content

PersistProcessor::handleLazyObjectRelations() crashes on uninitialized typed properties #7735

@marius-swfy

Description

@marius-swfy

API Platform version(s) affected

4.2.15 (introduced by #7521)

Description

PersistProcessor::handleLazyObjectRelations() iterates all entity properties via ReflectionProperty::getValue() without first checking ReflectionProperty::isInitialized(). This causes a fatal error for typed properties that are only set during Doctrine @PrePersist lifecycle callbacks, since handleLazyObjectRelations() runs before EntityManager::persist().

The issue only affects operations where $operation->canMap() returns true (i.e. ObjectMapper-enabled resources).

How to reproduce

Given a simple entity with typed properties initialized via @PrePersist:

#[ORM\Entity]
#[ORM\HasLifecycleCallbacks]
#[Map(target: BookResource::class)]
class Book
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    public int $id;

    #[ORM\Column(length: 255)]
    public string $title;

    #[ORM\Column(length: 50)]
    public string $status;

    #[ORM\Column]
    public int $version;

    #[ORM\Column(type: Types::DATETIME_IMMUTABLE)]
    public DateTimeInterface $createdAt;

    #[ORM\PrePersist]
    public function initializeDefaults(): void
    {
        $this->status = 'draft';
        $this->version = 1;
        $this->createdAt = new DateTimeImmutable();
    }
}

And an API resource using ObjectMapper:

#[ApiResource(
    shortName: 'books',
    operations: [new Post()],
    stateOptions: new Options(entityClass: Book::class),
)]
#[Map(target: Book::class)]
final class BookResource
{
    #[ApiProperty(identifier: true)]
    public ?int $id = null;

    #[Assert\NotBlank]
    public ?string $title = null;
}

Sending a POST request:

{
    "data": {
        "type": "books",
        "attributes": {
            "title": "My Book"
        }
    }
}

Results in:

Typed property Book::$status must not be accessed before initialization

Stack trace:

PersistProcessor.php:171  ReflectionProperty->getValue()
PersistProcessor.php:107  PersistProcessor->handleLazyObjectRelations()

Possible fix

Add an isInitialized() check before accessing the property value in handleLazyObjectRelations():

 foreach ($reflectionProperties as $propertyName => $reflectionProperty) {
     if ($classMetadata->isIdentifier($propertyName)) {
         continue;
     }

+    if (!$reflectionProperty->isInitialized($data)) {
+        continue;
+    }
+
     $value = $reflectionProperty->getValue($data);

     if (!\is_object($value)) {
         continue;
     }

Since the method's purpose is to find lazy-loaded object relations, skipping uninitialized properties is safe — they cannot contain a lazy proxy object.

Additional Context

This pattern of typed properties without defaults + @PrePersist initialization is common and well-documented in Doctrine. The issue is specifically caused by handleLazyObjectRelations() running at line 107 (before $manager->persist($data) at line 111), so @PrePersist callbacks have not yet fired.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions