-
-
Notifications
You must be signed in to change notification settings - Fork 960
Description
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.