# API Platform Core ## Introduction API Platform Core is a powerful PHP framework for building hypermedia-driven REST and GraphQL APIs. It integrates seamlessly with Symfony and Doctrine ORM, providing a declarative approach to API development using PHP attributes. The framework automatically generates fully-featured APIs with support for JSON-LD, Hydra, OpenAPI, JSON:API, HAL, and other popular formats, eliminating boilerplate code while maintaining flexibility for customization. The framework follows a state-based architecture where resources are declared using attributes, and data flow is controlled through Providers (for reading) and Processors (for writing). It includes built-in support for validation, filtering, pagination, authentication, caching, and comprehensive documentation generation. API Platform emphasizes standards compliance and hypermedia principles, making APIs self-documenting and discoverable while offering extension points at every layer. ## API Resources and Operations ### Declaring a Basic API Resource Define a resource with CRUD operations using the `#[ApiResource]` attribute. ```php id; } } // Test with curl: // curl -X GET http://localhost/books.jsonld?order[id]=desc // curl -X POST -H "Content-Type: application/ld+json" \ // -d '{"title": "API Platform rocks"}' \ // http://localhost/books ``` ## State Management ### Custom Provider for Reading Data Implement a Provider to control how resources are retrieved. ```php id = '1'; $book1->title = 'First Book'; $book2 = new Book(); $book2->id = '2'; $book2->title = 'Second Book'; return [$book1, $book2]; } // Handle single item requests - use URI variables $book = new Book(); $book->id = $uriVariables['id']; $book->title = 'Book ' . $uriVariables['id']; return $book; } } // GET /books.jsonld returns: // { // "@context": "/contexts/Book", // "@type": "hydra:Collection", // "member": [ // {"@id": "/books/1", "@type": "Book", "id": "1", "title": "First Book"}, // {"@id": "/books/2", "@type": "Book", "id": "2", "title": "Second Book"} // ] // } ``` ### Custom Processor for Writing Data Implement a Processor to control how resources are persisted. ```php id; // Persist to JSON file file_put_contents( sprintf('book-%s.json', $id), json_encode($data) ); return $data; } } final class BookProvider implements ProviderInterface { public function provide(Operation $operation, array $uriVariables = [], array $context = []): ?Book { $file = sprintf('book-%s.json', $uriVariables['id']); if (!file_exists($file)) { return null; } $data = json_decode(file_get_contents($file)); return new Book($data->id, $data->title); } } // curl -X POST -H "Content-Type: application/ld+json" \ // -d '{"id": "1", "title": "API Platform rocks"}' \ // http://localhost/books ``` ## Filtering and Search ### Doctrine Search Filter Enable filtering on resource properties with configurable strategies. ```php new QueryParameter(filter: 'app.search_filter'), ] )] #[ORM\Entity] class Book { #[ORM\Id, ORM\Column, ORM\GeneratedValue] public ?int $id = null; #[ORM\Column] public ?string $title = null; #[ORM\Column] public ?string $author = null; } namespace App\DependencyInjection; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; function configure(ContainerConfigurator $configurator): void { $services = $configurator->services(); $services->set('app.search_filter') ->parent('api_platform.doctrine.orm.search_filter') ->args([['author' => 'partial', 'title' => 'partial']]) ->tag('api_platform.filter'); } // curl http://localhost/books.jsonld?author=tolkien // curl http://localhost/books.jsonld?title=lord&author=tolkien // Available strategies: exact, partial, start, end, word_start ``` ### Custom Pagination Implement custom pagination logic with API Platform's Paginator wrapper. ```php createQueryBuilder('b') ->where('b.published = :isPublished') ->setParameter('isPublished', true) ->addCriteria( Criteria::create() ->setFirstResult(($page - 1) * $itemsPerPage) ->setMaxResults($itemsPerPage) ) ); } } namespace App\State; use ApiPlatform\Doctrine\Orm\Paginator; use ApiPlatform\Metadata\Operation; use ApiPlatform\State\Pagination\Pagination; use ApiPlatform\State\ProviderInterface; use App\Repository\BookRepository; class BooksListProvider implements ProviderInterface { public function __construct( private readonly BookRepository $bookRepository, private readonly Pagination $pagination ) {} public function provide(Operation $operation, array $uriVariables = [], array $context = []): Paginator { [$page, , $limit] = $this->pagination->getPagination($operation, $context); return new Paginator($this->bookRepository->getPublishedBooks($page, $limit)); } } // curl http://localhost/books.jsonld // Returns paginated response with view information: // { // "@context": "/contexts/Book", // "@type": "hydra:Collection", // "totalItems": 35, // "member": [...], // "view": { // "@id": "/books.jsonld?page=1", // "@type": "PartialCollectionView", // "first": "/books.jsonld?page=1", // "last": "/books.jsonld?page=2", // "next": "/books.jsonld?page=2" // } // } ``` ## Validation ### Input Validation with Symfony Validator Validate incoming data using Symfony constraints and custom validators. ```php id; } } namespace App\Validator\Constraints; use Symfony\Component\Validator\Constraint; #[\Attribute] class MinimalProperties extends Constraint { public $message = 'The product must have the minimal properties required ("description", "price")'; } 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_key_exists('description', $value) || !\array_key_exists('price', $value)) { $this->context->buildViolation($constraint->message)->addViolation(); } } } // curl -X POST -H "Content-Type: application/ld+json" \ // -d '{"name": "test", "properties": {"description": "foo"}}' \ // http://localhost/products // // Returns 422 Unprocessable Entity: // { // "@context": "/contexts/ConstraintViolationList", // "@type": "ConstraintViolationList", // "title": "An error occurred", // "description": "properties: The product must have the minimal properties required...", // "violations": [ // { // "propertyPath": "properties", // "message": "The product must have the minimal properties required..." // } // ] // } ``` ## Subresources ### Declaring Resource Relationships Define subresources to access related entities through parent resources. ```php new Link(fromClass: Company::class, toProperty: 'company'), 'id' => new Link(fromClass: Employee::class), ], operations: [new Get()] )] #[ApiResource( uriTemplate: '/companies/{companyId}/employees', uriVariables: [ 'companyId' => new Link(fromClass: Company::class, toProperty: 'company'), ], operations: [new GetCollection()] )] #[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; } #[ORM\Entity] #[ApiResource] class Company { #[ORM\Id, ORM\Column, ORM\GeneratedValue] public ?int $id; #[ORM\Column] public string $name; } // curl http://localhost/companies/1/employees // curl http://localhost/companies/1/employees/5 // curl -X POST -H "Content-Type: application/ld+json" \ // -d '{"name": "John Doe", "company": "/companies/1"}' \ // http://localhost/employees ``` ## Documentation ### Extending OpenAPI Documentation Customize OpenAPI specification for operations. ```php new Response(description: 'Ok'), ], summary: 'Add a book to the library.', description: 'My awesome operation', requestBody: new RequestBody( content: new \ArrayObject([ 'application/ld+json' => [ 'schema' => [ 'properties' => [ 'id' => ['type' => 'integer', 'required' => true, 'description' => 'id'], ], ], 'example' => [ 'id' => 12345, ], ], ]) ) ) )] class Book { public int $id; public string $title; } // curl http://localhost/docs.json // OpenAPI spec includes custom documentation ``` ## HTTP Caching ### Custom Cache Tags Customize HTTP cache tags for cache invalidation strategies. ```php title = 'Sample Book'; return $book; } } namespace App\Service; use ApiPlatform\Serializer\TagCollectorInterface; use App\ApiResource\Book; class TagCollector implements TagCollectorInterface { public function collect(array $context = []): void { if (isset($context['property_metadata'])) { return; } $iri = $context['iri'] ?? null; $object = $context['object'] ?? null; if ($object && $object instanceof Book) { $iri = $object->id; } if (!$iri) { return; } $context['resources'][$iri] = $iri; } } namespace App\DependencyInjection; use App\Service\TagCollector; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; function configure(ContainerConfigurator $configurator): void { $services = $configurator->services(); $services->set('api_platform.http_cache.tag_collector', TagCollector::class); } // curl -I http://localhost/books/unique-id-1.jsonld // Response includes: // Cache-Tags: unique-id-1 ``` ## Testing ### API Test Case Test API endpoints with built-in test utilities. ```php request(method: 'GET', url: '/books/1'); $this->assertResponseStatusCodeSame(404); $this->assertJsonContains([ 'detail' => 'Not Found', ]); } public function testGetCollection(): void { $response = static::createClient()->request(method: 'GET', url: '/books'); $this->assertMatchesResourceCollectionJsonSchema(Book::class); $this->assertCount(0, $response->toArray()['member']); } public function testCreateBook(): void { $response = static::createClient()->request(method: 'POST', url: '/books', options: [ 'json' => ['title' => 'New Book'], 'headers' => ['content-type' => 'application/ld+json'], ]); $this->assertResponseStatusCodeSame(201); $this->assertJsonContains([ '@type' => 'Book', 'title' => 'New Book', ]); } } // Run tests: // php bin/phpunit tests/BookTest.php ``` ## Summary API Platform Core streamlines API development by providing a declarative, attribute-based approach to resource definition. The primary use cases include building REST APIs for modern web and mobile applications, creating GraphQL endpoints, exposing database entities through standardized interfaces, and implementing microservices with rich hypermedia support. The framework excels at rapid prototyping while maintaining production-grade quality through built-in features like validation, filtering, pagination, and comprehensive OpenAPI documentation. Integration patterns typically involve declaring resources as PHP classes with attributes, leveraging Doctrine ORM for persistence, implementing custom Providers and Processors for specialized data handling, and extending functionality through Symfony's dependency injection container. The framework supports multiple serialization formats out of the box, enabling seamless content negotiation. Developers can start with zero configuration for simple CRUD operations and progressively customize behavior at any layer - from operation-level hooks to global event listeners - making it suitable for projects ranging from simple REST APIs to complex, standards-compliant hypermedia systems with advanced caching and real-time update capabilities.