Data Transformers break subresource collections in GraphQL
API Platform version(s) affected: 2.6.8
Description
While using any DataTransformer the subresource collections inside affected objects return all the records from related resource instead of linked ones. The bug reproduces only for GraphQL (REST API works as it's expected)
How to reproduce
Lets create 2 entities and a DataTransformer
Entity Set
<?php
namespace App\Entity;
use ApiPlatform\Core\Annotation\ApiProperty;
use ApiPlatform\Core\Annotation\ApiResource;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
/**
* @ORM\Table(name="sets")
*/
#[ApiResource(
normalizationContext: ['groups' => ['set:read', 'asset:read']],
denormalizationContext: ['groups' => ['set:write']],
collectionOperations: [
'get',
'post'
],
itemOperations: [
'get',
'put',
'delete',
],
attributes: [
'pagination_type' => 'page'
],
output: Set::class
)]
class Set
{
/**
* @var string
* @ORM\Id
* @ORM\Column(type="uuid", unique=true)
* @ORM\GeneratedValue(strategy="CUSTOM")
* @ORM\CustomIdGenerator("doctrine.uuid_generator")
* @ApiProperty(identifier=true)
* @Groups({"set:read"})
*/
private $id;
/**
* @var string
* @ORM\Column(type="string", length=32, nullable=true)
* @Groups({"set:read", "set:write"})
*/
private $name;
/**
* @var Collection|Asset[]
* @ORM\ManyToMany(targetEntity=Asset::class, inversedBy="sets")
* @ORM\JoinTable(name="idp_sets_assets")
* @Groups({"set:read", "set:write"})
*/
private $assets;
public function __construct()
{
$this->assets = new ArrayCollection();
}
public function getId(): ?string
{
return $this->id;
}
public function setId(string $id): self
{
$this->id = $id;
return $this;
}
/**
* @return Collection|Asset[]
*/
public function getAssets(): Collection
{
return $this->assets;
}
public function addAsset(Asset $asset): self
{
if (!$this->assets->contains($asset)) {
$this->assets[] = $asset;
}
return $this;
}
public function removeAsset(Asset $asset): self
{
$this->assets->removeElement($asset);
return $this;
}
/**
* @return string
*/
public function getName(): string
{
return $this->name;
}
/**
* @param string $name
* @return Set
*/
public function setName(string $name): Set
{
$this->name = $name;
return $this;
}
}
Entity Asset
<?php
namespace App\Entity;
use ApiPlatform\Core\Annotation\ApiProperty;
use ApiPlatform\Core\Annotation\ApiResource;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
/**
* @ORM\Table(name="assets")
*/
#[ApiResource(
normalizationContext: ['groups' => ['asset:read', 'set:read']],
denormalizationContext: ['groups' => ['asset:write']],
collectionOperations: [
'get',
'post'
],
itemOperations: [
'get',
'put',
'delete',
],
attributes: [
'pagination_type' => 'page'
],
output: Asset::class
)]
class Asset
{
/**
* @var string
* @ORM\Id
* @ORM\Column(type="uuid", unique=true)
* @ORM\GeneratedValue(strategy="CUSTOM")
* @ORM\CustomIdGenerator("doctrine.uuid_generator")
* @ApiProperty(identifier=true)
* @Groups({"asset:read"})
*/
private $id;
/**
* @var string
* @ORM\Column(type="string", length=32, nullable=true)
* @Groups({"asset:read", "asset:write"})
*/
private $name;
/**
* @var Collection|Set[]
* @ORM\ManyToMany(targetEntity=Set::class, mappedBy="assets")
* @Groups({"asset:read", "asset:write"})
*/
private $sets;
public function __construct()
{
$this->sets = new ArrayCollection();
}
public function getId(): string
{
return $this->id;
}
public function setId(string $id): Asset
{
$this->id = $id;
return $this;
}
/**
* @return Collection|Set[]
*/
public function getSets(): Collection
{
return $this->sets;
}
public function addSet(Set $set): self
{
if (!$this->sets->contains($set)) {
$this->sets[] = $set;
$set->addAsset($this);
}
return $this;
}
public function removeSet(Set $set): self
{
if ($this->sets->removeElement($set)) {
$set->removeAsset($this);
}
return $this;
}
/**
* @return string
*/
public function getName(): string
{
return $this->name;
}
/**
* @param string $name
* @return Asset
*/
public function setName(string $name): Asset
{
$this->name = $name;
return $this;
}
}
And finally a DataTransformer
<?php
namespace App\DataTransformer;
use ApiPlatform\Core\DataTransformer\DataTransformerInterface;
use ReflectionException;
class DummyDataTransformer implements DataTransformerInterface
{
/**
* {@inheritdoc}
*
* @throws ReflectionException
*/
public function transform($data, string $to, array $context = []): object
{
// some code, it's even possible to keep as is
return $data;
}
/**
* @param object $data
*
* @throws ReflectionException
*/
public function supportsTransformation($data, string $to, array $context = []): bool
{
return true;
}
}
Than we have to create for example 2 Assets and link one of them to newly created Set. Could be done using any interface like REST API or GraphQL
Finally request Sets with included Assets collection:
{
sets {
collection {
id
assets {
collection {
id
}
}
}
}
}
In the result you cant see, that there are 2 assets in Asset collection, instead of the one that we have added
Possible Solution
¯_(ツ)_/¯
BTW, the same issue appears if made decoration described there - https://api-platform.com/docs/core/serialization/#decorating-a-serializer-and-adding-extra-data
Having exactly the same problem.
My workaround for now is to add a filter to the related resource to be able to filter by the parent object id. But that's only possible because I fetch the nested relations only on a single parent and can pass down the variable.
But this should be fixed.