Upgrade to Pro — share decks privately, control downloads, hide ads and more …

API Platform: From Rest & GraphQL APIs to state...

API Platform: From Rest & GraphQL APIs to state-of-the-art standards in seconds

Talk given at the PHP UK 2026 - London

API Platform is a totally open-source, out-of-the-box framework for making APIs that follow all the Web's best practices (standards, RFCs...) since its release, in 2015.

In this talk, we'll see how in a matter of seconds, thanks to the contributions of around 1,000 developers since the framework was created, we can easily have a robust API with auto-generated documentation, via the Swagger UI, whether for a REST or GraphQL API.

Through concrete examples and feedback, we'll explore how to contribute to API Platform and use it in your projects, whatever the complexity of your APIs. We'll look at extending automatic documentation, adding custom filters, and integrating your own custom data sources.

Avatar for Vincent Amstoutz

Vincent Amstoutz

February 22, 2026
Tweet

More Decks by Vincent Amstoutz

Other Decks in Programming

Transcript

  1. A huge community @vinceAmstoutz An open-source project ➤ 14 000

    GitHub stars ➤ 921 contributors to components and documentation ➤ Laravel and Symfony support ➤
  2. Vincent Amstoutz 💻 Lead Developer @ Les-Tilleuls.coop 🌍 OSS contributions

    to API Platform, FrankenPHP, Symfony and more ... vinceAmstoutz
  3. PHP, JS, GO, Rust, C programming DevOps, SRE Maintaining your

    applications Agile management, UX design, UI design @vinceAmstoutz API, Web & Cloud experts
  4. Integrated Standards @vinceAmstoutz OpenAPI (Auto-generated and extendable documentation) ➤ REST

    HATEOS ➤ GraphQL ➤ Scheme.org support (Search engines, SEO) ➤ Standard formats ➤ And more...
  5. A Full Framework @vinceAmstoutz Front integrations (Next.js, Vue.js, React...) ➤

    Mercure integration ➤ API Platform React Admin ➤ Schema Generator ➤
  6. @vinceAmstoutz RAD / CRUD Several Possible Architectures <?php // src/Entity/Book.php

    // ... #[ORM\Entity] #[ApiResource] class Book { #[ORM\Id, ORM\Column] public ?int $id = null; #[ORM\Column] public string $title; // ... } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
  7. Add a new format @vinceAmstoutz 1. Create a Normalizer 2.

    Create an Encoder 3. Configure it 4. Share it (OSS 🎉)
  8. Global Configuration ⚙ @vinceAmstoutz General information ➤ Validation ➤ Supported

    Formats ➤ Exceptions ➤ Documentation ➤ ... https://api-platform.com/docs/core/configuration/
  9. Native Validation @vinceAmstoutz <?php // api/src/Entity/Contact.php namespace App\Entity; use ApiPlatform\Metadata\ApiResource;

    use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Validator\Constraints as Assert; #[ORM\Entity] #[ApiResource] final class Contact { #[ORM\Id, ORM\Column, ORM\GeneratedValue] private ?int $id = null; #[ORM\Column] #[Assert\NotBlank] #[Assert\Email] public string $email; // ... }
  10. Custom Validation @vinceAmstoutz <?php // api/src/Validator/Constraints/MinimalPropertiesValidator.php namespace App\Validator\Constraints; use Symfony\Component\Validator\Constraint;

    use Symfony\Component\Validator\ConstraintValidator; final class MinimalPropertiesValidator extends ConstraintValidator { public function validate($value, Constraint $constraint): void { if (array_diff(['description', 'price'], $value)) { $this->context ->buildViolation($constraint->message)->addViolation(); } } } https://api-platform.com/docs/core/validation/
  11. Integrated Security @vinceAmstoutz <?php // api/src/ApiResource/PutBook.php namespace App\ApiResource; use ApiPlatform\Metadata\Put;

    #[Put(security: "is_granted('ROLE_ADMIN') or object.owner == user")] final class PutBook { // ... } Secured Operations
  12. Integrated Security @vinceAmstoutz use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authorization\Voter\Vote; use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface; abstract

    class Voter implements VoterInterface { abstract protected function supports(...): bool; abstract protected function voteOnAttribute(...): bool; } Custom Access Controls
  13. Integrated Security @vinceAmstoutz # config/packages/security.yaml security: # ... firewalls: #

    ... access_control: - { path: ^/api/docs, roles: PUBLIC_ACCESS } - { path: ^/api/admin, roles: ROLE_ADMIN } - { path: ^/api, roles: ROLE_USER Global Configuration
  14. @vinceAmstoutz Sample State Provider <?php // src/ApiResource/UserScore.php namespace App\ApiResource; use

    ApiPlatform\Metadata\Get; use App\State\ScoreProvider; #[Get(provider: ScoreProvider::class)] class UserScore { public ?int $id = null; public ?string $username = null; public ?int $points = null; // ... } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <?php // src/State/ScoreProvider.php namespace App\State; use ApiPlatform\State\ProviderInterface; use ApiPlatform\Metadata\Operation; use App\ApiResource\UserScore; class ScoreProvider implements ProviderInterface { public function provide( Operation $operation, array $uriVariables = [], array $context = [] ): object|array|null { $score = new UserScore(); $score->username = 'Joueur1'; $score->points = 950; return $score; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
  15. @vinceAmstoutz ObjectMapper Integration use Symfony\Component\ObjectMapper\Attribute\Map; #[Map(target: BookResource::class)] <?php 1 2

    // src/Entity/Book.php 3 4 namespace App\Entity; 5 6 use App\ApiResource\Book as BookResource; 7 8 // ... 9 10 #[ORM\Entity(repositoryClass: BookRepository::class)] 11 12 class Book 13 { 14 #[ORM\Id] 15 #[ORM\GeneratedValue] 16 #[ORM\Column] 17 private ?int $id = null; 18 19 #[ORM\Column(length: 255)] 20 #[Map(transform: 'ucfirst')] 21 private ?string $title = null; 22 23 // ... 24 } 25 use App\ApiResource\Book as BookResource; use Symfony\Component\ObjectMapper\Attribute\Map; #[Map(target: BookResource::class)] #[ORM\Column(length: 255)] #[Map(transform: 'ucfirst')] private ?string $title = null; <?php 1 2 // src/Entity/Book.php 3 4 namespace App\Entity; 5 6 7 8 // ... 9 10 #[ORM\Entity(repositoryClass: BookRepository::class)] 11 12 class Book 13 { 14 #[ORM\Id] 15 #[ORM\GeneratedValue] 16 #[ORM\Column] 17 private ?int $id = null; 18 19 20 21 22 23 // ... 24 } 25
  16. Pagination Provided @vinceAmstoutz "totalItems": 50, "view": { "@id": "/books?page=1", "@type":

    "PartialCollectionView", "first": "/books?page=1", "last": "/books?page=2", "next": "/books?page=2" } { 1 "@context": "/contexts/Book", 2 "@id": "/books", 3 "@type": "Collection", 4 "member": [ 5 { 6 "@id": "/books/1", 7 "@type": "https://schema.org/Book", 8 "name": "My awesome book" 9 }, 10 { 11 "_": "Other items in the collection..." 12 } 13 ], 14 15 16 17 18 19 20 21 22 } 23
  17. Filtering Resources @vinceAmstoutz { "@context": "/contexts/Book", "@id": "/books", "@type": "Collection",

    "totalItems": 2, "member": [ { "id": 1, "title": "L'Écume des jours", "author": "Boris Vian", "publication": "1947-03-20T00:00:00+00:00", "genre": "Roman" }, { "id": 2, "title": "Dune", "author": "Frank Herbert", "publication": "1965-08-01T00:00:00+00:00", "genre": "Science-Fiction" } ], } GET /books?publication[after]=1950-01-01 { "@context": "/contexts/Book", "@id": "/books", "@type": "Collection", "totalItems": 1, "member": [ { "id": 2, "title": "Dune", "author": "Frank Herbert", "publication": "1965-08-01T00:00:00+00:00", "genre": "Science-Fiction" } ], } GET /books
  18. New Native Filters @vinceAmstoutz Since API Platform > 4.1 IriFilter

    ExactFilter PartialSearchFilter FreeTextQueryFilter OrFilter
  19. Custom Filter @vinceAmstoutz <?php // api/src/Filter/MonthFilter.php namespace App\Filter; use ApiPlatform\Doctrine\Orm\Filter\FilterInterface;

    // ... final class MonthFilter implements FilterInterface { public function apply(...): void { $parameter = $context['parameter']; $monthValue = $parameter->getValue(); $parameterName = $queryNameGenerator->generateParameterName($property); $alias = $queryBuilder->getRootAliases()[0]; $queryBuilder ->andWhere(sprintf('MONTH(%s.%s) = :%s', $alias, $property, $parameterName)) ->setParameter($parameterName, $monthValue); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
  20. OpenApi Extension @vinceAmstoutz On operations #[Post( openapi: new Model\Operation( summary:

    'Create a rabbit picture', requestBody: new Model\RequestBody( content: new \ArrayObject([ 'application/json' => [ 'example' => [ 'name' => 'Mr. Rabbit', 'description' => 'Pink Rabbit' ] ] ]) ), ) )] final class Rabbit {...}
  21. OpenApi Extension @vinceAmstoutz Decorates OpenApiFactory <?php namespace App\OpenApi; use ApiPlatform\OpenApi\Factory\OpenApiFactoryInterface;

    use Symfony\Component\DependencyInjection\Attribute\AsDecorator; #[AsDecorator(decorates: 'api_platform.openapi.factory')] final class OpenApiFactory implements OpenApiFactoryInterface { public function __construct(private OpenApiFactoryInterface $decorated) {} public function __invoke(array $context = []): OpenApi { $openApi = $this->decorated->__invoke($context); return $openApi->withServers([new Model\Server('https://foo.bar')]); } }