J.BizMai
J.BizMai

Reputation: 2787

Get entity with Many-To-Many Relation with API Platform

I have an entity Professionnal with Jobs in API Plateform project 3.2.

When I call host/api/professionnals I got a result like this :

{
    "data": [
        {
            "id": "/api/professionnals/1",
            "type": "Professionnal",
            "attributes": {
                "email": "[email protected]",
                "firstname": "JOHN",
                "lastname": "DOE"
            },
            "relationships": {
                "jobs": {
                    "data": [
                        {
                            "type": "Job",
                            "id": "/api/jobs/1"
                        },
                        {
                            "type": "Job",
                            "id": "/api/jobs/2"
                        }
                    ]
                }
            }
        },
        ...
    ]
}

I would like the job label property but it seems to be more difficult with relation ManyToMany in API Plateform to get it.

Professionnal.php Entity

<?php

namespace App\Entity;

use ...

#[ApiResource(normalizationContext: ['groups' => ['professionnal:read']])]
#[ORM\Entity(repositoryClass: ProfessionnalRepository::class)]
#[ORM\UniqueConstraint(name: 'UNIQ_IDENTIFIER_EMAIL', fields: ['email'])]
class Professionnal implements UserInterface, PasswordAuthenticatedUserInterface {
    
    ...

    #[Assert\NotBlank]
    #[Assert\Email]
    #[ORM\Column(length: 180, unique: true)]
    #[Groups(['professionnal:read'])]
    private ?string $email = null;

    ...
    
    #[ORM\Column(length: 150)]
    #[Groups(['professionnal:read'])]
    private ?string $firstname = null;

    #[ORM\Column(length: 255)]
    #[Groups(['professionnal:read'])]
    private ?string $lastname = null;

    ...

    /**
     * @var Collection<int, Job>
     */
    #[ORM\ManyToMany(targetEntity: Job::class, inversedBy: 'professionnals', fetch: "EAGER")]
    #[ORM\JoinTable(name: 'professionnal_job')]
    #[ORM\JoinColumn(name: 'professionnal_id', referencedColumnName: 'id', nullable: false)]
    #[ORM\InverseJoinColumn(name: 'job_id', referencedColumnName: 'id', nullable: false)]
    #[Groups(['professionnal:read'])]
    private Collection $jobs;


    public function __construct() {
    
        $this->jobs = new ArrayCollection();
    }

    ...
    
    /**
     * @return Job[]
     */
    #[Groups(['professionnal:read'])]
    public function getJobs(): array {
        return $this->jobs->getValues();
    }


}

Job.php Entity

<?php

namespace App\Entity;

...

#[ORM\Entity(repositoryClass: JobRepository::class)]
#[ApiResource()]
class Job {
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 255)]
    #[Groups(['professionnal:read', 'job:read'])]
    private ?string $label = null;


    /**
     * @var Collection<int, Professionnal>
     */
    #[ORM\ManyToMany(targetEntity: Professionnal::class, mappedBy: 'jobs')]
    private Collection $professionnals;

    public function __construct() {
        $this->professionnals = new ArrayCollection();
    }

    public function __toString(): string {
        return (is_null($this->getLabel())) ? '' : $this->getLabel();
    }

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

    public function getLabel(): ?string {
        return $this->label;
    }

    public function setLabel(string $label): static {
        $this->label = $label;

        return $this;
    }


    /**
     * @return Collection<int, Professionnal>
     */
    public function getProfessionnals(): Collection {
        return $this->professionnals;
    }

    ...
}

I tried to add this config code without success :/

api_platform:
    eager_loading:
        max_joins: 100

Someone has got an idea ? Should I find a workaround or there is a proper solution to do this ?

Upvotes: -1

Views: 111

Answers (1)

J.BizMai
J.BizMai

Reputation: 2787

I found a solution based on custom DTO and Provider. Basicaly, API Plateform does not offer an easy way yet to get object properties from an ManyToMany relation.

So...

  1. Make a DTO and Provider

  2. Replace #[ApiResource] with #[GetCollection(output: ProfessionalDTO::class, provider: ProfessionalsProvider::class)]

  3. with jobs property as Collection create a new method.

    public function getJobs(): array { return $this->jobs->getValues(); }

  4. Declare the provider in services.yaml

Professional.php

namespace App\Entity;

use ...
use ApiPlatform\Metadata\GetCollection;

#[GetCollection(output: ProfessionalDTO::class, provider: ProfessionalsProvider::class)]
#[ORM\Entity(repositoryClass: ProfessionalRepository::class)]
#[ORM\UniqueConstraint(name: 'UNIQ_IDENTIFIER_EMAIL', fields: ['email'])]
class Professional implements UserInterface, PasswordAuthenticatedUserInterface {
    
    ...

    #[Assert\NotBlank]
    #[Assert\Email]
    #[ORM\Column(length: 180, unique: true)]
    #[Groups(['professional:read'])]
    private ?string $email = null;

    ...
    
    #[ORM\Column(length: 150)]
    #[Groups(['professional:read'])]
    private ?string $firstname = null;

    #[ORM\Column(length: 255)]
    #[Groups(['professional:read'])]
    private ?string $lastname = null;

    ...

    /**
     * @var Collection<int, Job>
     */
    #[ORM\ManyToMany(targetEntity: Job::class, inversedBy: 'professionals', fetch: "EAGER")]
    #[ORM\JoinTable(name: 'professional_job')]
    #[ORM\JoinColumn(name: 'professional_id', referencedColumnName: 'id', nullable: false)]
    #[ORM\InverseJoinColumn(name: 'job_id', referencedColumnName: 'id', nullable: false)]
    #[Groups(['professional:read'])]
    private Collection $jobs;


    public function __construct() {
    
        $this->jobs = new ArrayCollection();
    }

    ...
    
    /**
     * @return Job[]
     */
    public function getJobs(): array {
        return $this->jobs->getValues();
    }


}

src/Dto/ProfessionalDTO.php

namespace App\Dto;

use App\Entity\Professional;

class ProfessionalDTO {
    public string $id;
    public string $email;
    public string $firstname;
    public string $lastname;
    public array $jobs;

    public function __construct(Professional $professional) {
        $this->id = $professional->getId();
        $this->email = $professional->getEmail();
        $this->firstname = $professional->getFirstname();
        $this->lastname = $professional->getLastname();

        $this->jobs = [];
        foreach ($professional->getJobs() as $job) {
            $this->jobs[] = [
                    'id' => $job->getId(),
                    'label' => $job->getLabel()
            ];
        }
    
    }
}

src/State/ProfessionalsProvider.php

<?php

namespace App\State;

use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use App\Dto\ProfessionalDTO;
use App\Entity\Professional;
use App\Repository\ProfessionalRepository;

class ProfessionalsProvider implements ProviderInterface {

    private ProfessionalRepository $professionalRepository;

    public function __construct(ProfessionalRepository $professionalRepository)
    {
        $this->professionalRepository = $professionalRepository;
    }

    public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null {
        $professionals = $this->professionalRepository->findAll();
        return array_map(
                fn(Professional $professional) => new ProfessionalDTO($professional),
                $professionals
        );
    }
}

services.yaml.

App\State\ProfessionalsProvider:
    arguments:
        $professionalRepository: '@App\Repository\ProfessionalRepository'

Upvotes: 0

Related Questions