Api Platform conference
Register now
Guides Handle doctrine links
API Platform Conference
September 19-20, 2024 | Lille & online

The international conference on the API Platform Framework

Get ready for game-changing announcements for the PHP community!

The API Platform Conference 2024 is happening soon, and it's close to selling out.
API Platform 4, Caddy web server, Xdebug, AI... Enjoy two days of inspiring talks with our friendly community and our amazing speakers.

Only a few tickets left!
Guide

Handle doctrine links

doctrine expert
When using subresources with doctrine, API Platform tries to handle your links, and the algorithm sometimes overcomplicates SQL queries.
// src/App/Entity.php
namespace App\Entity;
use ApiPlatform\Doctrine\Orm\State\Options;
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Link;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\ORM\QueryBuilder;
To get around that, you can hook into the link management with an ORM StateOption
#[GetCollection(
    uriTemplate: '/company/{companyId}/employees',
    uriVariables: ['companyId' => new Link(fromClass: Company::class, toProperty: 'company')],
The handleLinks option takes a service name implementing the LinksHandlerInterface or a callable.
    stateOptions: new Options(handleLinks: [Employee::class, 'handleLinks'])
)]
#[Get('/company/{companyId}/employees/{id}')]
#[ORM\Entity]
class Employee
{
    #[ORM\Id, ORM\Column, ORM\GeneratedValue]
    public ?int $id;
    #[ORM\Column]
    public string $name;
    #[ORM\ManyToOne(targetEntity: Company::class)]
    public ?Company $company;
    public function getId()
    {
        return $this->id;
    }
This function gets called in our generic ItemProvider or CollectionProvider, the idea is to create the WHERE clause to get the correct data. You can also perform joins or whatever SQL clause you need:
    public static function handleLinks(QueryBuilder $queryBuilder, array $uriVariables, QueryNameGeneratorInterface $queryNameGenerator, array $context): void
    {
        $queryBuilder
            ->andWhere($queryBuilder->getRootAliases()[0].'.company = :companyId')
            ->setParameter('companyId', $uriVariables['companyId']);
    }
}
#[ORM\Entity]
#[ApiResource]
class Company
{
    #[ORM\Id, ORM\Column, ORM\GeneratedValue]
    public ?int $id;
    #[ORM\Column]
    public string $name;
}

// src/App/Playground.php
namespace App\Playground;
use Symfony\Component\HttpFoundation\Request;
function request(): Request
{
Persistence is automatic, you can try to create or read data:
    return Request::create('/company/1/employees', 'GET');
}

// src/DoctrineMigrations.php
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Migration extends AbstractMigration
{
    public function up(Schema $schema): void
    {
        $this->addSql('CREATE TABLE company (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name VARCHAR(255) NOT NULL);');
        $this->addSql('CREATE TABLE employee (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, company_id INTEGER DEFAULT NULL, name VARCHAR(255) NOT NULL, CONSTRAINT FK_COMPANY FOREIGN KEY (company_id) REFERENCES company (id) NOT DEFERRABLE INITIALLY IMMEDIATE);
        $this->addSql('CREATE INDEX FK_COMPANY ON employee (company_id)');
    }
}

// src/App/Fixtures.php
namespace App\Fixtures;
use App\Entity\Company;
use App\Entity\Employee;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Persistence\ObjectManager;
use function Zenstruck\Foundry\anonymous;
use function Zenstruck\Foundry\faker;
use function Zenstruck\Foundry\repository;
final class BookFixtures extends Fixture
{
    public function load(ObjectManager $manager): void
    {
        $companyFactory = anonymous(Company::class);
        $companyRepository = repository(Company::class);
        if ($companyRepository->count()) {
            return;
        }
        $companyFactory->many(1)->create(fn () => [
            'name' => faker()->company(),
        ]);
        $employeeFactory = anonymous(Employee::class);
        $employeeFactory->many(10)->create(fn () => [
            'name' => faker()->name(),
            'company' => $companyRepository->first(),
        ]
        );
    }
}

You can also help us improve this guide.

Made with love by

Les-Tilleuls.coop can help you design and develop your APIs and web projects, and train your teams in API Platform, Symfony, Next.js, Kubernetes and a wide range of other technologies.

Learn more

Copyright © 2023 Kévin Dunglas

Sponsored by Les-Tilleuls.coop