I am unable to get subresource POST operations working as expected in API Platform 4.1 with subresources; top-level resources work fine.
Expected: new subresources can be created as needed. Actual: only one subresource can be created; additional attempts simply overwrite the original subresource.
Minimal example
I have entities User and UserEmail in a one-to-many relationship:
User.php:
#[Groups(['user:read'])]
#[ORM\OneToMany(targetEntity: UserEmail::class, mappedBy: 'user', cascade: ['persist', 'remove'])]
private Collection $emails;
UserEmail.php:
#[ApiResource(
uriTemplate: '/users/{userUuid}/emails',
uriVariables: [
'userUuid' => new Link(fromClass: User::class, fromProperty: 'emails'),
],
operations: [
new Post(
normalizationContext: ['groups' => ['userEmail:read']],
security: "is_granted('ROLE_ADMIN') or (is_granted('ROLE_USER') and user.getUuid() == request.attributes.get('userUuid'))",
processor: UserEmailProcessor::class,
),
]
)]
#[ORM\Entity(repositoryClass: UserEmailRepository::class)]
#[ORM\Table(name: 'user_email')]
#[UniqueEntity(fields: ['email'], message: 'This email is already in use.')]
final class UserEmail
{
#[ApiProperty(identifier: false)]
#[ORM\Id]
#[ORM\GeneratedValue(strategy: 'SEQUENCE')]
#[ORM\Column(type: 'integer')]
private int $id;
#[ApiProperty(identifier: true)]
#[Assert\Uuid(versions: [4], groups: ['userEmail:read', 'userEmail:write'])]
#[Groups(['userEmail:read', 'user:read'])]
#[ORM\Column(type: 'string', length: 36, unique: true)]
private string $uuid;
#[ApiProperty]
#[Assert\NotBlank]
#[Assert\Regex(pattern: '/^\d+$/')]
#[Groups(['userEmail:read', 'userEmail:write', 'user:read'])]
#[ORM\Column(type: 'string', length: 20, unique: true)]
private string $email;
#[ApiProperty]
#[Groups(['userEmail:read'])]
#[ORM\ManyToOne(targetEntity: User::class, inversedBy: 'emails')]
#[ORM\JoinColumn(nullable: false)]
private User $user;
public function __construct(?UuidInterface $uuid = null)
{
$this->uuid = $uuid?->toString() ?? Uuid::uuid4()->toString();
}
// ...
}
The problem
Request:
POST /users/00000000-0000-0000-0000-000000000001/emails
{
"email": "[email protected]"
}
{
"title": "An error occurred",
"detail": "An exception occurred while executing a query: SQLSTATE[23502]: Not null violation: 7 ERROR: null value in column \"user_id\" of relation \"user_email\" violates not-null constraint\nDETAIL: Failing row contains (1, 6b63235a-8c25-468c-881f-b4ce80618c56, 2222222, null).",
"status": 500,
"type": "/errors/500"
}
UserEmail::$user is not being set from the URI as expected. However, I can use a state processor to set the UserEmail::$user field manually. I must attach this state processor to the POST operation.
UserEmail.php:
#[ApiResource(
operations: new Post(
processor: UserEmailProcessor::class,
// ...
)
// ...
)]
UserEmailProcessor.php:
final readonly class UserEmailProcessor implements ProcessorInterface
{
public function __construct(
private EntityManagerInterface $entityManager,
private UserRepository $userRepository,
private RequestStack $requestStack,
) {
}
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed
{
if (!$data instanceof UserEmail) {
return $data;
}
$userUuid = $uriVariables['userUuid'] ?? null;
$user = $this->userRepository->findOneBy(['uuid' => $userUuid]);
if (!$user) {
throw new NotFoundHttpException();
}
$data->setUser($user);
$this->entityManager->persist($data);
$this->entityManager->flush();
return $data;
}
}
Now try the same request:
POST /users/00000000-0000-0000-0000-000000000001/emails
{
"email": "[email protected]"
}
201 Created
{
"uuid": "988a50a6-f77f-47aa-b5de-eaa5029fb1f2",
"email": "[email protected]",
"user": "/users/00000000-0000-0000-0000-000000000001"
}
It worked! One more time with a different email:
POST /users/00000000-0000-0000-0000-000000000001/emails
{
"email": "[email protected]"
}
201 Created
{
"uuid": "988a50a6-f77f-47aa-b5de-eaa5029fb1f2",
"email": "[email protected]",
"user": "/users/00000000-0000-0000-0000-000000000001"
}
Wrong. I received a 201 Created response code and the email is correct. However the UUID is the exact same. The previous resource was updated or recreated. If I try this with a different user's UUID it will work the first time, but follow-up attempts show the same problem.
#[ApiProperty(identifier: true)]operations: [new Post(),](i.e. removing normalizationContext, security and processor), what happens ?