symfonymany-to-manyapi-platform

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": "foo@bar.com",
                "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 ?


Solution

  • 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'