api-platform icon indicating copy to clipboard operation
api-platform copied to clipboard

ApiProperty identifier are not followed for item requests

Open NotionCommotion opened this issue 4 years ago • 0 comments

API Platform version(s) affected: 2.6.7

Description

EntityA and EntityB use a different identifier than Doctrine. EntityAB use EntityA's and EntityBs identifier as its identifier.

When making an item request to EntityAB, the provided identifiers are used as the child entities primary keys and not their ApiProperty identifiers.

How to reproduce

Create the following four entities:

<?php
namespace App\Entity\Testing;

use Doctrine\ORM\Mapping as ORM;
use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Annotation\ApiProperty;

#[ORM\Entity]
#[ORM\UniqueConstraint(columns: ['public_id', 'tenant_entity_id'])]
#[ApiResource]
class EntityA
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column(type: 'integer')]
    #[ApiProperty(identifier: false)]
    private ?int $id = null;

    // Will be set by a listner
    #[SerializedName('id')]
    #[ApiProperty(identifier: true)]
    #[ORM\Column(type: 'integer')]
    private ?int $publicId = null;

    #[ORM\ManyToOne(targetEntity: TenantEntity::class)]
    #[ORM\JoinColumn(nullable: false)]
    private ?TenantEntity $tenantEntity = null;

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getPublicId(): ?int
    {
        return $this->publicId;
    }

    public function setPublicId(int $publicId): self
    {
        $this->publicId = $publicId;

        return $this;
    }

    public function getTenantEntity(): ?TenantEntity
    {
        return $this->tenantEntity;
    }

    public function setTenantEntity(TenantEntity $tenantEntity): self
    {
        $this->tenantEntity = $tenantEntity;

        return $this;
    }
}
<?php
namespace App\Entity\Testing;

use Doctrine\ORM\Mapping as ORM;
use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Annotation\ApiProperty;

#[ORM\Entity]
#[ORM\UniqueConstraint(columns: ['public_id', 'tenant_entity_id'])]
#[ApiResource]
class EntityB
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column(type: 'integer')]
    #[ApiProperty(identifier: false)]
    private ?int $id = null;

    // Will be set by a listner
    #[SerializedName('id')]
    #[ApiProperty(identifier: true)]
    #[ORM\Column(type: 'integer')]
    private ?int $publicId = null;

    #[ORM\ManyToOne(targetEntity: TenantEntity::class)]
    #[ORM\JoinColumn(nullable: false)]
    private ?TenantEntity $tenantEntity = null;

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getPublicId(): ?int
    {
        return $this->publicId;
    }

    public function setPublicId(int $publicId): self
    {
        $this->publicId = $publicId;

        return $this;
    }

    public function getTenantEntity(): ?TenantEntity
    {
        return $this->tenantEntity;
    }

    public function setTenantEntity(TenantEntity $tenantEntity): self
    {
        $this->tenantEntity = $tenantEntity;

        return $this;
    }
}
<?php
namespace App\Entity\Testing;

use Doctrine\ORM\Mapping as ORM;
use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Annotation\ApiProperty;

#[ORM\Entity]
#[ORM\UniqueConstraint(columns: ['entity_a_id', 'entity_b_id'])]
#[ApiResource]
class EntityAB
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column(type: 'integer')]
    #[ApiProperty(identifier: false)]
    private ?int $id = null;

    #[ORM\ManyToOne(targetEntity: EntityA::class)]
    #[ORM\JoinColumn(nullable: false)]
    #[ApiProperty(identifier: true)]
    private ?EntityA $entityA = null;

    #[ORM\ManyToOne(targetEntity: EntityB::class)]
    #[ORM\JoinColumn(nullable: false)]
    #[ApiProperty(identifier: true)]
    private ?EntityB $entityB = null;

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getEntityA(): ?EntityA
    {
        return $this->entityA;
    }

    public function setEntityA(EntityA $entityA): self
    {
        $this->entityA = $entityA;

        return $this;
    }

    public function getEntityB(): ?EntityB
    {
        return $this->entityB;
    }

    public function setEntityB(EntityB $entityB): self
    {
        $this->entityB = $entityB;

        return $this;
    }
}
<?php
namespace App\Entity\Testing;

use Doctrine\ORM\Mapping as ORM;
use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Annotation\ApiProperty;

#[ORM\Entity]
#[ApiResource]
class TenantEntity
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column(type: 'integer')]
    private ?int $id = null;

    public function getId(): ?int
    {
        return $this->id;
    }
}

POST for all entities works as expected (only one shown).

{
  "@context": "/contexts/EntityA",
  "@id": "/entity_as/10",
  "@type": "EntityA",
  "id": 1,
  "publicId": 10,
  "tenantEntity": "/tenant_entities/1"
}

GET collection for EntityAB works as expected.

{
  "@context": "/contexts/EntityAB",
  "@id": "/entity_a_bs",
  "@type": "hydra:Collection",
  "hydra:member": [
    {
      "@id": "/entity_a_bs/entityA=10;entityB=10",
      "@type": "EntityAB",
      "id": 1,
      "entityA": "/entity_as/10",
      "entityB": "/entity_bs/10"
    },
    {
      "@id": "/entity_a_bs/entityA=20;entityB=10",
      "@type": "EntityAB",
      "id": 2,
      "entityA": "/entity_as/20",
      "entityB": "/entity_bs/10"
    },
    {
      "@id": "/entity_a_bs/entityA=20;entityB=20",
      "@type": "EntityAB",
      "id": 3,
      "entityA": "/entity_as/20",
      "entityB": "/entity_bs/20"
    },
    {
      "@id": "/entity_a_bs/entityA=10;entityB=20",
      "@type": "EntityAB",
      "id": 4,
      "entityA": "/entity_as/10",
      "entityB": "/entity_bs/20"
    }
  ],
  "hydra:totalItems": 4
}

GET item for EntityAB, however, returns a 404 Not Found error.

curl -X 'GET' \
  'https://beat-the-heat.net/entity_a_bs/entityA%3D10%3BentityB%3D10' \
  -H 'accept: application/ld+json' \

SQL executed is the following which incorrectly uses the provided identifiers as the DB ID and not the ApiProperty identifier:

SELECT e0_.id AS id_0, e0_.entity_a_id AS entity_a_id_1, e0_.entity_b_id AS entity_b_id_2
FROM entity_ab e0_
WHERE e0_.entity_a_id = ? AND e0_.entity_b_id = ? ["10","10"] []

Possible Solution

SQL should have been:

SELECT e0_.id AS id_0, ea.id AS ea_id, ea.public_id, eb.id AS eb_id, eb.public_id
FROM entity_ab e0_
INNER JOIN entity_a ea ON ea.id=e0_.entity_a_id
INNER JOIN entity_b eb ON eb.id=e0_.entity_b_id
WHERE ea.public_id = ? AND eb.public_id = ? ["10","10"] []

Before generating the query, API-Platform must check to see if the properties have the ApiProperty identifier attribute/annotation on them. Not really sure where to fix it but it might be related to ApiPlatform\Core\Api\IdentifiersExtractor::getIdentifiersFromResourceClass() or ApiPlatform\Core\Identifier\IdentifierConverter::getIdentifierType()

Additional Context

NotionCommotion avatar Jan 23 '22 20:01 NotionCommotion