# API Platform documentation (4.3) Source: https://api-platform.com/docs/ --- # Getting Started With API Platform with Symfony Source: https://api-platform.com/docs/symfony/ # Getting Started With API Platform with Symfony ![The welcome page](images/api-platform-3.0-welcome.png) > _API Platform_ is the most advanced API platform, in any framework or language. > > —Fabien Potencier (creator of Symfony) [API Platform](https://api-platform.com) is a powerful yet easy-to-use **full stack** framework dedicated to API-driven projects and implementing the [Jamstack](https://jamstack.org/) architecture. ## Introduction API Platform contains [a **PHP** library (Core)](../core/index.md) to create fully featured hypermedia (or [GraphQL](../core/graphql.md)) web APIs supporting industry-leading standards: [JSON-LD](https://json-ld.org) with [Hydra](https://www.hydra-cg.com), [OpenAPI](../core/openapi.md)... API Platform also provides ambitious **JavaScript** tools to create web and mobile applications based on the most popular frontend technologies in a snap. These tools parse the documentation of the API (or of any other API supporting Hydra or OpenAPI). API Platform is shipped with **[Docker](../deployment/docker-compose.md)** and **[Kubernetes](../deployment/kubernetes.md)** definitions, to develop and deploy instantly on the cloud. The easiest and most powerful way to get started is to use the **`api-platform` installer**, a command-line tool that scaffolds a new project for you. Depending on the options you select, it can generate: - the API skeleton, including [the Core library](../core/index.md), [the Symfony framework](https://symfony.com/) ([optional](../core/bootstrap.md)) and [the Doctrine ORM](https://www.doctrine-project.org/projects/orm.html) ([optional](../core/extending.md)) - [a beautiful admin interface](../admin/index.md), built on top of React Admin, dynamically created by parsing the API documentation - a [Next.js](../create-client/nextjs.md) Progressive Web App, ready to welcome the code generated by [the client scaffolding tool](../create-client/index.md) ([Nuxt](https://nuxt.com/), [Vue](https://vuejs.org/), [React Native](https://reactnative.dev/), [Quasar](https://quasar.dev/) and [Vuetify](https://vuetifyjs.com/) are also supported) - all you need to [create real-time and async APIs using the Mercure protocol](../core/mercure.md) - a [Docker](../deployment/docker-compose.md) definition to start a working development environment in a single command, providing containers for the API and the Next.js web application ## A Bookshop API To discover how the framework works, we will create an API to manage a bookshop. To create a fully featured API, an admin interface, and a Progressive Web App using Next.js, all you need is to design **the public data model of our API** and handcraft it as _Plain Old PHP Objects_. API Platform uses these model classes to expose and document a web API having a bunch of built-in features: - creating, retrieving, updating, and deleting (CRUD) resources - data validation - pagination - filtering - sorting - hypermedia/[HATEOAS](https://en.wikipedia.org/wiki/HATEOAS) and content negotiation support ([JSON-LD](https://json-ld.org) and [Hydra](https://www.hydra-cg.com/), [JSON:API](https://jsonapi.org/), [HAL](https://tools.ietf.org/html/draft-kelly-json-hal-08)...) - [GraphQL support](../core/graphql.md) - Nice UI and machine-readable documentations ([Swagger UI/OpenAPI](https://swagger.io), [GraphiQL](https://github.com/graphql/graphiql)...) - authentication ([Basic HTTP](https://en.wikipedia.org/wiki/Basic_access_authentication), cookies as well as [JWT](../core/jwt.md) and [OAuth](https://oauth.net) through extensions) - [CORS headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS) - security checks and headers (tested against [OWASP recommendations](https://www.owasp.org/index.php/REST_Security_Cheat_Sheet)) - [invalidation-based HTTP caching](../core/performance.md) - and basically everything needed to build modern APIs. One more thing, before we start: as the generated project is built on top of [the Symfony framework](https://symfony.com), it is compatible with most [Symfony bundles](https://symfony.com/bundles) (plugins) and benefits from [the numerous extension points](../core/extending.md) provided by this rock-solid foundation (events, Dependency Injection Container...). Adding features like custom or service-oriented API endpoints, JWT or OAuth authentication, HTTP caching, mail sending or asynchronous jobs to your APIs is straightforward. ## Installing the Framework ### Using the API Platform Installer (Recommended) The `api-platform` installer is a command-line tool that scaffolds a new project for you. #### Installing the CLI Download the latest release for your platform from the [Releases page](https://github.com/api-platform/api-platform/releases/latest) and move the binary somewhere on your `$PATH`: ```console curl -L https://github.com/api-platform/api-platform/releases/latest/download/api-platform-linux-x86_64 -o /usr/local/bin/api-platform chmod +x /usr/local/bin/api-platform ``` Alternatively, if you already have PHP and [Composer](https://getcomposer.org/) installed, you can install the installer globally with Composer: ```console composer global require api-platform/installer ``` The `api-platform` binary will then be available in `~/.composer/vendor/bin` (make sure this directory is in your `$PATH`). #### Scaffolding the Project This tutorial builds a full stack project (API, admin and a Next.js Progressive Web App), so we generate it with the `--with-pwa` and `--with-admin` options. Run: ```console api-platform bookshop-api --framework=symfony --with-docker --with-pwa --with-admin ``` > [!TIP] > > Run `api-platform` without any argument to start the interactive wizard, which lets you pick the > framework, the API formats, the documentation UI, and whether to include Docker, the admin and the > PWA. When the PWA and the admin are enabled, the installer creates a project directory containing an `api/` subdirectory (the Symfony API) alongside a `pwa/` directory (the Next.js application) and an `admin/` directory (the React-admin SPA). The rest of this tutorial assumes this layout. API Platform is shipped with a [Docker](https://docker.com) definition that makes it easy to get a containerized development environment up and running. If you do not already have Docker on your computer, it's the right time to [install it](https://docs.docker.com/get-docker/). **Note**: On Mac, only [Docker for Mac](https://docs.docker.com/docker-for-mac/) is supported. Similarly, on Windows, only [Docker for Windows](https://docs.docker.com/docker-for-windows/) is supported. Docker Machine **is not** supported out of the box. Open a terminal and navigate to the generated `api/` directory. Run the following command to start all services using [Docker Compose](https://docs.docker.com/compose/): ```console cd bookshop-api/api docker compose up --wait ``` > [!TIP] > > Be sure that the ports `80`, `443`, and `5432` of the host are not already in use. The usual > offenders are Apache, NGINX, and Postgres. If they are running, stop them and run > `docker compose up --wait` again. > > Alternatively, run the following command to start the web server on port `8080` with HTTPS > disabled: > > ```console > SERVER_NAME=localhost:80 HTTP_PORT=8080 TRUSTED_HOSTS=localhost docker compose up --wait > ``` The `SERVER_NAME` is used by Caddy server, specify `localhost:8080` if you want any other address or to diable https. This starts the following services: | Name | Description | | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | php | The API powered by [FrankenPHP](https://frankenphp.dev) (a modern application server for PHP built on top of [Caddy web server](caddy.md) and with native support for [Mercure realtime](../core/mercure.md), [Vulcain relations preloading](https://vulcain.rocks), and [XDebug](debugging.md)), Composer, and sensitive configs | | database | PostgreSQL database server | When generated with `--with-pwa`, the Next.js application lives in the sibling `pwa/` directory. It is **not** part of the Docker Compose stack: you run it separately with its own development server (see [A Next.js Web App](#a-nextjs-web-app) below). The following components are available: | URL | Path | Language | Description | | ------------------------- | ------ | ---------- | --------------------------------------------- | | `https://localhost/docs/` | `api/` | PHP | The API | | `http://localhost:3000/` | `pwa/` | TypeScript | The Next.js application (run with `pnpm dev`) | To see the container's logs, run: ```console docker compose logs -f ``` The `-f` option is to follow the logs. Project files are automatically shared between your local host machine and the container thanks to a pre-configured [Docker volume](https://docs.docker.com/engine/tutorials/dockervolumes/). It means that you can edit files of your project locally using your preferred IDE or code editor, they will be transparently taken into account in the container. Speaking about IDEs, our favorite software to develop API Platform apps is [PhpStorm](https://www.jetbrains.com/phpstorm/) with its awesome [Symfony](https://confluence.jetbrains.com/display/PhpStorm/Getting+Started+-+Symfony+Development+using+PhpStorm) and [Php Inspections](https://plugins.jetbrains.com/plugin/7622-php-inspections-ea-extended-) plugins. Give them a try, you'll get auto-completion for almost everything and awesome quality analysis. [PHP Intelephense for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=bmewburn.vscode-intelephense-client) also works well, and is free and open source. The installer creates a dummy API resource for test purposes: `api/src/ApiResource/Greetings.php`. We will remove it later. The installer also requires the [Doctrine ORM](https://www.doctrine-project.org/projects/orm.html) bridge (`api-platform/doctrine-orm`), the industry-leading PHP persistence system, so you can start mapping your own entities right away. Doctrine ORM is the easiest way to persist and query data in an API Platform project thanks to this bridge, but it's also entirely optional, and [you may prefer to plug your own persistence system](../core/design.md). The Doctrine Bridge is optimized for performance and development convenience. For instance, when using Doctrine, API Platform is able to automatically optimize the generated SQL queries by adding the appropriate `JOIN` clauses. It also provides a lot of powerful [built-in filters](../core/filters.md). Doctrine ORM and its bridge support the most popular RDBMS including PostgreSQL, MySQL, MariaDB, SQL Server, Oracle and SQLite. There is also a shipped [Doctrine MongoDB ODM](https://www.doctrine-project.org/projects/mongodb-odm.html) optional support. That being said, keep in mind that API Platform is 100% independent of the persistence system. You can use the one(s) that best suit(s) your needs (including NoSQL databases or remote web services) by implementing the [right interfaces](../core/state-providers.md). API Platform even supports using several persistence systems together in the same project. > [!TIP] The `php` container is where your API app stands. Prefixing a command by > `docker compose exec php` allows executing the given command in this container. You may want > [to create an alias](https://www.linfo.org/alias.html) to make your life easier. So, for example, > you could run a command like this: `docker compose exec php `. ### Using Symfony CLI Alternatively, the API Platform server component can also be installed directly on a local machine. **This method is recommended only for users who want full control over the directory structure and the installed dependencies.** [For a good introduction, watch how to install API Platform manually on SymfonyCasts](https://symfonycasts.com/screencast/api-platform/install?cid=apip). The rest of this tutorial assumes that you have scaffolded your project with the `api-platform` installer. Go straight to the next section if it's your case. API Platform has an official Symfony Flex recipe. It means that you can easily install it from any Symfony application using [the Symfony binary](https://symfony.com/download): Create a new Symfony project: ```console symfony new bookshop-api ``` Enter the project directory: ```console cd bookshop-api ``` Install the API Platform's server component in this skeleton: ```console symfony composer require api ``` Then, create the database and its schema: ```console symfony console doctrine:database:create symfony console doctrine:schema:create ``` And start the built-in PHP server: ```console symfony serve ``` All TypeScript components are also [available as standalone libraries](https://github.com/api-platform?language=typescript) installable with npm (or any other package manager). **Note:** when installing API Platform this way, the API will be exposed at the `/api/` path. You need to open `http://localhost:8000/api/` to see the API documentation. If you are deploying API Platform directly on an Apache or NGINX webserver and getting a 404 error on opening this link, you will need to enable the [rewriting rules](https://symfony.com/doc/current/setup/web_server_configuration.html) for your specific webserver software. ## It's Ready Open `https://localhost` in your favorite web browser: ![The welcome page](images/api-platform-3.0-welcome.png) You'll need to add a security exception in your browser to accept the self-signed TLS certificate that has been generated for this container when installing the framework. If you generated the project without `--with-pwa`, you can skip this welcome screen and build your Next.js application separately later. When the PWA is included, it runs as a standalone Next.js app in the sibling `pwa/` directory (don't remove it now, we'll use it later in this tutorial). Click on the "API" button, or go to `https://localhost/docs/`: ![The API](images/api-platform-2.6-api.png) API Platform exposes a description of the API in the [OpenAPI](https://www.openapis.org/) format (formerly known as Swagger). It also integrates a customized version of [Swagger UI](https://swagger.io/swagger-ui/), a nice interface rendering the OpenAPI documentation. Click on an operation to display its details. You can also send requests to the API directly from the UI. Try to fetch the _Greetings_ collection using the `GET` operation, then fetch a single item. If you access any API URL with the `.html` extension appended, API Platform displays the corresponding API request in the UI. Try it yourself by browsing to `https://localhost/greetings.html`. If no extension is present, API Platform will use the `Accept` header to select the format to use. By default, a JSON-LD response is sent ([configurable behavior](../core/content-negotiation.md)). So, if you want to access the raw data, you have two alternatives: - Add the correct `Accept` header (or don't set any `Accept` header at all if you don't care about security) - preferred when writing API clients - Add the format you want as the extension of the resource - for debug purpose only For instance, go to `https://localhost/greetings.jsonld` to retrieve the list of `Greeting` resources in JSON-LD. Of course, you can also use your favorite HTTP client to query the API. We are fond of [Hoppscotch](https://hoppscotch.com), a free and open source API client with good support of API Platform. ## Keep Your Docker Setup in Sync The Docker and [FrankenPHP](https://frankenphp.dev/) configuration scaffolded by the installer comes from the [Symfony Docker](https://github.com/dunglas/symfony-docker) project. To benefit from the latest enhancements introduced upstream since you created your project, you can use the Git based [_template-sync_ project](https://github.com/coopTilleuls/template-sync). Run the following command from the `api/` directory to import the changes since your last update: ```console curl -sSL https://raw.githubusercontent.com/coopTilleuls/template-sync/main/template-sync.sh | sh -s -- https://github.com/dunglas/symfony-docker ``` Resolve potential conflicts, run `git cherry-pick --continue` and you are done! For more details, refer to the [coopTilleuls/template-sync documentation](https://github.com/coopTilleuls/template-sync/blob/main/README.md) ## Bringing your Own Model Your API Platform project is now 100% functional. Let's expose our own data model. Our bookshop API will start simple. It will be composed of a `Book` resource type and a `Review` one. Books have an ID, an ISBN, a title, a description, an author, a publication date and are related to a list of reviews. Reviews have an ID, a rating (between 0 and 5), a body, an author, a publication date and are related to one book. Let's describe this data model as a set of Plain Old PHP Objects (POPO): ```php reviews = new ArrayCollection(); } public function getId(): ?int { return $this->id; } } ``` ```php id; } } ``` We created two typical PHP objects with the corresponding PHPDoc, both marked with the `#[ApiResource]` attribute. For convenience, we also used the Doctrine Collection library (that is independent from Doctrine ORM), but it's not mandatory. Reload `https://localhost/docs/`: API Platform used these classes to generate an OpenAPI documentation (a Hydra documentation is also exposed), and registered for us [the typical REST routes](../core/operations.md). ![The bookshop API](images/api-platform-2.6-bookshop-api.png) Operations available for our 2 resource types appear in the UI. We can also see the awesome [Web Debug Toolbar](https://symfonycasts.com/screencast/symfony/profiler?cid=apip). Note that the entities' and properties' descriptions are present in the API documentation, and that API Platform uses PHP types to generate the appropriate JSON Schemas. ![Bookshop JSON Schemas](images/api-platform-2.6-bookshop-json-schemas.png) The framework also uses these metadata to serialize and deserialize data from JSON (and other formats) to PHP objects (back and forth)! For the sake of simplicity, in this example we used public properties (except for the ID, see below). API Platform (as well as Symfony and Doctrine) also supports accessor methods (getters/setters), use them if you want to. We used a private property and a getter for the ID to enforce the fact that it is read only (we will let the DBMS generate it). API Platform also has first-grade support for UUIDs v7. In some cases it is preferable use them instead of auto-incremented IDs. Because API Platform provides all the infrastructure for us, our API is almost ready! The only remaining task to have a working API is to be able to query and persist data. ## Plugging the Persistence System To retrieve and save data, API Platform proposes two main options (and we can mix them): 1. Writing our own [state providers](../core/state-providers.md) and [state processors](../core/state-processors.md) to fetch and save data in any persistence system and trigger our custom business logic. This is what we recommend if you want to separate the public data model exposed by the API from the internal one, and to implement a layered architecture such as Clean Architecture or Hexagonal Architecture; 2. Using one of the various existing state providers and processors allowing to automatically fetch and persist data using popular persistence libraries. Out of the box, state providers and processors are provided for [Doctrine ORM](https://www.doctrine-project.org/projects/orm.html) and [Doctrine MongoDB ODM](../core/mongodb.md). A state provider (but no processor yet) is also available for [Elasticsearch](../core/elasticsearch.md). [Pomm](https://github.com/pomm-project/pomm-api-platform) and [PHP Extended SQL](https://github.com/soyuka/esql#api-platform-bridge) also provides state providers and processors for API Platform. We recommend this approach for Rapid Application Development. Be sure to read the [General Design Considerations](../core/design.md) document to learn more about the architecture of API Platform and how to choose between these two approaches. Here, we will use the built-in Doctrine ORM state provider in the rest of this tutorial. Modify the classes to map them to database tables using the attributes provided by the Doctrine ORM. Modify these files as described in these patches: `api/src/Entity/Book.php` ```diff use ApiPlatform\Metadata\ApiResource; use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\ORM\Mapping as ORM; /** A book. */ +#[ORM\Entity] #[ApiResource] class Book { /** The ID of this book. */ + #[ORM\Id, ORM\Column, ORM\GeneratedValue] private ?int $id = null; /** The ISBN of this book (or null if doesn't have one). */ + #[ORM\Column(nullable: true)] public ?string $isbn = null; /** The title of this book. */ + #[ORM\Column] public string $title = ''; /** The description of this book. */ + #[ORM\Column(type: 'text')] public string $description = ''; /** The author of this book. */ + #[ORM\Column] public string $author = ''; /** The publication date of this book. */ + #[ORM\Column] public ?\DateTimeImmutable $publicationDate = null; /** @var Review[] Available reviews for this book. */ + #[ORM\OneToMany(targetEntity: Review::class, mappedBy: 'book', cascade: ['persist', 'remove'])] public iterable $reviews; public function __construct() ``` `api/src/Entity/Review.php` ```diff namespace App\Entity; use ApiPlatform\Metadata\ApiResource; +use Doctrine\ORM\Mapping as ORM; /** A review of a book. */ +#[ORM\Entity] #[ApiResource] class Review { /** The ID of this review. */ + #[ORM\Id, ORM\Column, ORM\GeneratedValue] private ?int $id = null; /** The rating of this review (between 0 and 5). */ + #[ORM\Column(type: 'smallint')] public int $rating = 0; /** The body of the review. */ + #[ORM\Column(type: 'text')] public string $body = ''; /** The author of the review. */ + #[ORM\Column] public string $author = ''; /** The date of publication of this review.*/ + #[ORM\Column] public ?\DateTimeImmutable $publicationDate = null; /** The book this review is about. */ + #[ORM\ManyToOne(inversedBy: 'reviews')] public ?Book $book = null; public function getId(): ?int ``` **Tip**: You can use Symfony [MakerBundle](https://symfonycasts.com/screencast/symfony-fundamentals/maker-command?cid=apip) to generate a Doctrine entity that is also a resource thanks to the `--api-resource` option: ```console docker compose exec php bin/console make:entity --api-resource ``` For more information on the available makers see [Maker documentation](./maker.md). Doctrine's [attributes](https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/attributes-reference.html) map these entities to tables in the database. Mapping through [annotations](https://www.doctrine-project.org/projects/doctrine-annotations/en/current/index.html) is still supported for backward compatibility, but they are considered deprecated and attributes are now the recommended approach. Both methods are convenient as they allow grouping the code and the configuration but, if you want to decouple classes from their metadata, you can switch to XML or YAML mappings. They are supported as well. Learn more about how to map entities with the Doctrine ORM in [the project's official documentation](https://docs.doctrine-project.org/projects/doctrine-orm/en/current/reference/association-mapping.html) or in Kévin's book "[Persistence in PHP with the Doctrine ORM](https://www.amazon.fr/gp/product/B00HEGSKYQ/ref=as_li_tl?ie=UTF8&camp=1642&creative=6746&creativeASIN=B00HEGSKYQ&linkCode=as2&tag=kevidung-21)". Now, delete the file `api/src/Entity/Greeting.php`. This demo entity isn't useful anymore. Finally, generate a new database migration using [Doctrine Migrations](https://symfony.com/doc/current/doctrine.html#migrations-creating-the-database-tables-schema) and apply it: ```console docker compose exec php bin/console doctrine:migrations:diff docker compose exec php bin/console doctrine:migrations:migrate ``` **We now have a working API with read and write capabilities!** In Swagger UI, click on the `POST` operation of the `Book` resource type, click on "Try it out" and send the following JSON document as request body: ```json { "isbn": "9781782164104", "title": "Persistence in PHP with the Doctrine ORM", "description": "This book is designed for PHP developers and architects who want to modernize their skills through better understanding of Persistence and ORM.", "author": "Kévin Dunglas", "publicationDate": "2013-12-01" } ``` You just saved a new book resource through the bookshop API! API Platform automatically transforms the JSON document to an instance of the corresponding PHP entity class and uses Doctrine ORM to persist it in the database. By default, the API supports `GET` (retrieve, on collections and items), `POST` (create), `PATCH` (partial update) and `DELETE` (self-explanatory) HTTP methods. Don't forget to [disable the ones you don't want](../core/operations.md#enabling-and-disabling-operations)! The `PUT` (replace or create) method is also supported, but is not enabled by default. Try the `GET` operation on the collection. The book we added appears. When the collection contains more than 30 items, the pagination will automatically show up, [and this is entirely configurable](../core/pagination.md). You may be interested in [adding some filters and adding sorts to the collection](../core/filters.md) as well. You may have noticed that some keys start with the `@` symbol in the generated JSON response (`@id`, `@type`, `@context`...)? API Platform comes with a full support of the [JSON-LD](https://json-ld.org/) format (and its [Hydra](https://www.hydra-cg.com/) extension). It allows to build smart clients, with auto-discoverability capabilities such as the API Platform Admin that we will discover in a few lines. It is useful for open data, SEO and interoperability, especially when [used with open vocabularies such as Schema.org](http://blog.schema.org/2013/06/schemaorg-and-json-ld.html) and allows to [give access to Google to your structured data](https://developers.google.com/search/docs/guides/intro-structured-data) or to query your APIs in [SPARQL](https://en.wikipedia.org/wiki/SPARQL) using [Apache Jena](https://jena.apache.org/documentation/io/#formats)). We think that JSON-LD is the best default format for a new API. However, API Platform natively [supports many other formats](../core/content-negotiation.md) including [GraphQL](https://graphql.org/) (we'll get to it), [JSON:API](https://jsonapi.org/), [HAL](https://github.com/zircote/Hal), raw [JSON](https://www.json.org/), [XML](https://www.w3.org/XML/) (experimental) and even [YAML](https://yaml.org/) and [CSV](https://en.wikipedia.org/wiki/Comma-separated_values). You can also easily [add support for other formats](../core/content-negotiation.md) and it's up to you to choose which format to enable and to use by default. Now, add a review for this book using the `POST` operation for the `Review` resource: ```json { "book": "/books/1", "rating": 5, "body": "Interesting book!", "author": "Kévin", "publicationDate": "September 21, 2016" } ``` **Note:** If you have installed API Platform in an existing project using `composer`, the content of the key `book` must be `"/api/books/1"` There are two interesting things to mention about this request: First, we learned how to work with relations. In a hypermedia API, every resource is identified by a (unique) [IRI](https://en.wikipedia.org/wiki/Internationalized_Resource_Identifier). A URL is a valid IRI, and it's what API Platform uses. The `@id` property of every JSON-LD document contains the IRI identifying it. You can use this IRI to reference this document from other documents. In the previous request, we used the IRI of the book we created earlier to link it with the `Review` we were creating. API Platform is smart enough to deal with IRIs. By the way, you may want to [embed documents](../core/serialization.md) instead of referencing them (e.g. to reduce the number of HTTP requests). You can even [let the client select only the properties it needs](../core/filters.md#property-filter). The other interesting thing is how API Platform handles dates (the `publicationDate` property). API Platform understands [any date format supported by PHP](https://www.php.net/manual/en/datetime.formats.date.php). In production we strongly recommend using the format specified by the [RFC 3339](https://tools.ietf.org/html/rfc3339), but, as you can see, most common formats including `September 21, 2016` can be used. To summarize, if you want to expose any entity you just have to: 1. Put it under the `App\Entity\` namespace 2. Write your data providers and persisters, or if you use Doctrine, map it with the database 3. Mark it with the `#[ApiPlatform\Metadata\ApiResource]` attribute Could it be any easier?! ## Validating Data Now try to add another book by issuing a `POST` request to `/books` with the following body: ```json { "isbn": "2815840053", "description": "Hello", "author": "Me", "publicationDate": "today" } ``` The book is successfully created but there is a problem; we did not give it a title. It makes no sense to create a book record without a title so we really should have some validation measures in place to prevent this from being possible. API Platform comes with a bridge with [the Symfony Validator Component](https://symfony.com/doc/current/validation.html). Adding some of [its numerous validation constraints](https://symfony.com/doc/current/validation.html#supported-constraints) (or [creating custom ones](https://symfony.com/doc/current/validation/custom_constraint.html)) to our entities is enough to validate user-submitted data. Let's add some validation rules to our data model. Modify the following files as described in these patches: `api/src/Entity/Book.php` ```diff use ApiPlatform\Metadata\ApiResource; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Validator\Constraints as Assert; #[ORM\Column(nullable: true)] + #[Assert\Isbn] public ?string $isbn = null; #[ORM\Column] + #[Assert\NotBlank] public string $title = ''; #[ORM\Column(type: 'text')] + #[Assert\NotBlank] public string $description = ''; #[ORM\Column] + #[Assert\NotBlank] public string $author = ''; #[ORM\Column] + #[Assert\NotNull] public ?\DateTimeImmutable $publicationDate = null; ``` `api/src/Entity/Review.php` ```diff use ApiPlatform\Metadata\ApiResource; use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Validator\Constraints as Assert; #[ORM\Column(type: 'smallint')] + #[Assert\Range(min: 0, max: 5)] public int $rating = 0; #[ORM\Column(type: 'text')] + #[Assert\NotBlank] public string $body = ''; #[ORM\Column] + #[Assert\NotBlank] public string $author = ''; #[ORM\Column] + #[Assert\NotNull] public ?\DateTimeImmutable $publicationDate = null; #[ORM\ManyToOne(inversedBy: 'reviews')] + #[Assert\NotNull] public ?Book $book = null; public function getId(): ?int ``` After updating the entities by adding those `#[Assert\*]` attributes (as with Doctrine, you can also use XML or YAML), try again the previous `POST` request. ```json { "@context": "/contexts/ConstraintViolationList", "@type": "ConstraintViolationList", "title": "An error occurred", "description": "isbn: This value is neither a valid ISBN-10 nor a valid ISBN-13.\ntitle: This value should not be blank.", "violations": [ { "propertyPath": "isbn", "message": "This value is neither a valid ISBN-10 nor a valid ISBN-13." }, { "propertyPath": "title", "message": "This value should not be blank." } ] } ``` You now get proper validation error messages, always serialized using the Hydra error format ([RFC 7807](https://tools.ietf.org/html/rfc7807) is also supported). Those errors are easy to parse client-side. By adding the proper validation constraints, we also noticed that the provided ISBN isn't valid... ## Adding GraphQL Support Isn't API Platform a REST **and** GraphQL framework? That's true! GraphQL support isn't enabled by default. To add it we need to install the [graphql-php](https://webonyx.github.io/graphql-php/) library. Run the following command: ```console docker compose exec php composer require api-platform/graphql ``` You now have a GraphQL API! Open `https://localhost/graphql` (or `https://localhost/api/graphql` if you used Symfony Flex to install API Platform) to play with it using the nice [GraphiQL](https://github.com/graphql/graphiql) UI that is shipped with API Platform: ![GraphQL endpoint](images/api-platform-2.6-graphql.png) Try it out by creating a book: ```graphql mutation { createBook( input: { isbn: "9781782164104" title: "Persistence in PHP with the Doctrine ORM" description: "This book is designed for PHP developers and architects who want to modernize their skills through better understanding of Persistence and ORM." author: "Kévin Dunglas" publicationDate: "2013-12-01" } ) { book { id title } } } ``` And by reading out the book: ```graphql { book(id: "/books/2") { id title _id } } ``` You can also try things a bit more complex: ```graphql { books { totalCount edges { node { id title reviews { totalCount edges { node { author rating } } } } } } } ``` The GraphQL implementation supports [queries](https://graphql.org/learn/queries/), [mutations](https://graphql.org/learn/queries/#mutations), [100% of the Relay server specification](https://relay.dev/docs/guides/graphql-server-specification/), pagination, [filters](../core/filters.md) and [access control rules](../core/security.md). You can use it with the popular [RelayJS](https://relay.dev) and [Apollo](https://www.apollographql.com/docs/react/) clients. ## The Admin Wouldn't it be nice to have an administration backend to manage the data exposed by your API? Wait... You already have one! When generated with `--with-admin`, the React-admin SPA lives in the sibling `admin/` directory. Start its development server: ```console cd ../admin npm run dev ``` Open `http://localhost:5173/` in your browser: ![The admin](images/api-platform-2.6-admin.png) This [Material Design](https://material.io/guidelines/) admin is a [Progressive Web App](https://developers.google.com/web/progressive-web-apps/) built with [API Platform Admin](../admin/index.md) ([React Admin](https://marmelab.com/react-admin/) inside!). It is powerful and fully customizable. Refer to its documentation to learn more. It leverages the Hydra documentation exposed by the API component to build itself. It's 100% dynamic - **no code generation occurs**. ## A Next.js Web App API Platform also has an awesome [client generator](../create-client/index.md) able to scaffold fully working [Next.js](../create-client/nextjs.md), [Nuxt.js](../create-client/nuxt.md), [React/Redux](../create-client/react.md), [Vue.js](../create-client/vuejs.md), [Quasar](../create-client/quasar.md), and [Vuetify](../create-client/vuetify.md) Progressive Web Apps/Single Page Apps that you can easily tune and customize. The generator also supports [React Native](../create-client/react-native.md) if you prefer to leverage all capabilities of mobile devices. When generated with `--with-pwa`, the installer scaffolds a [Next.js](https://nextjs.org/) application in the `pwa/` directory, ready to welcome the generated code. To bootstrap your app, run the client generator from that directory, then start the development server: ```console cd ../pwa pnpm create @api-platform/client pnpm dev ``` Open `http://localhost:3000/books/` in your browser: ![The Next.js Progressive Web App](images/api-platform-2.6-pwa-react.png) You can also choose to generate the code for a specific resource with the `--resource` argument (example: `pnpm create @api-platform/client --resource books`). The generated code contains a list (including pagination), a delete button, a creation and an edit form. It also includes [Tailwind CSS](https://tailwindcss.com) classes and [ARIA roles](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA) to make the app usable by people with disabilities. If you prefer to generate a PWA built on top of another frontend stack, read [the dedicated documentation](../create-client/index.md). ## Hooking Your Own Business Logic Now that you learned the basics, be sure to read [the general design considerations](../core/design.md) and [how to extend API Platform](../core/extending.md) to understand how API Platform is designed, and how to hook your custom business logic! ## Other Features First, you may want to learn [how to deploy your application](../deployment/index.md) in the cloud using [the built-in Kubernetes integration](../deployment/kubernetes.md). Then, there are many more features to learn! Read [the full documentation](../core/index.md) to discover how to use them and how to extend API Platform to fit your needs. API Platform is incredibly efficient for prototyping and Rapid Application Development (RAD), but the framework is mostly designed to create complex API-driven projects, far beyond simple CRUD apps. It benefits from [**strong extension points**](../core/extending.md) and it is **continuously optimized for [performance](../core/performance.md).** It powers numerous high-traffic websites. API Platform has a built-in HTTP cache invalidation system which allows making API Platform apps blazing fast using [Varnish](https://varnish-cache.org/). Read more in the chapter [API Platform Core Library: Enabling the Built-in HTTP Cache Invalidation System](../core/performance.md#enabling-the-built-in-http-cache-invalidation-system). Keep in mind that you can use your favorite client-side technology: API Platform provides generators for popular JavaScript frameworks, but you can also use your preferred client-side technology including Angular, Ionic, and Swift directly. Any language able to send HTTP requests is OK (even COBOL can do that). To go further, the API Platform team maintains a demo application showing more advanced use cases like leveraging serialization groups, user management, or JWT and OAuth authentication. [Check out the demo code source on GitHub](https://github.com/api-platform/demo) and [browse it online](https://demo.api-platform.com). ## Screencasts

SymfonyCasts, API Platform screencasts

The easiest and funniest way to learn how to use API Platform is to watch [the more than 60 screencasts available on SymfonyCasts](https://symfonycasts.com/tracks/rest?cid=apip#api-platform-3)! --- # Validation with Symfony Source: https://api-platform.com/docs/symfony/validation/ # Validation with Symfony API Platform takes care of validating the data sent to the API by the client (usually user data entered through forms). By default, the framework relies on [the powerful Symfony Validator Component](https://symfony.com/doc/current/validation.html) for this task, but you can replace it with your preferred validation library such as [the PHP filter extension](https://www.php.net/manual/en/intro.filter.php) if you want to.

Validation screencast
Watch the Validation screencast

## Validating Submitted Data Validating submitted data is as simple as adding [Symfony's built-in constraints](https://symfony.com/doc/current/reference/constraints.html) or [custom constraints](https://symfony.com/doc/current/validation/custom_constraint.html) directly in classes marked with the `#[ApiResource]` attribute: ```php context->buildViolation($constraint->message)->addViolation(); } } } ``` If the data submitted by the client is invalid, the HTTP status code will be set to `422 Unprocessable Entity` and the response's body will contain the list of violations serialized in a format compliant with the requested one. For instance, a validation error will look like the following if the requested format is JSON-LD (the default): ```json { "@context": "/contexts/ConstraintViolationList", "@type": "ConstraintViolationList", "title": "An error occurred", "description": "properties: The product must have the minimal properties required (\"description\", \"price\")", "violations": [ { "propertyPath": "properties", "message": "The product must have the minimal properties required (\"description\", \"price\")" } ] } ``` Take a look at the [Errors Handling guide](../core/errors.md) to learn how API Platform converts PHP exceptions like validation errors to HTTP errors. ## Using Validation Groups Without specific configuration, the default validation group is always used, but this behavior is customizable: the framework is able to leverage Symfony's [validation groups](https://symfony.com/doc/current/validation/groups.html). You can configure the groups you want to use when the validation occurs directly through the `ApiResource` attribute: ```php ['a', 'b']])] class Book { #[Assert\NotBlank(groups: ['a'])] public string $name; #[Assert\NotNull(groups: ['b'])] public string $author; // ... } ``` With the previous configuration, the validation groups `a` and `b` will be used when validation is performed. Like for [serialization groups](../core/serialization.md#using-serialization-groups-per-operation), you can specify validation groups globally or on a per-operation basis. Of course, you can use XML or YAML configuration format instead of attributes if you prefer. You may also pass in a [group sequence](https://symfony.com/doc/current/validation/sequence_provider.html) in place of the array of group names. ## Using Validation Groups on Operations You can have different validation for each [operation](../core/operations.md) related to your resource. ```php ['Default', 'putValidation']])] #[GetCollection] #[Post(validationContext: ['groups' => ['Default', 'postValidation']])] class Book { #[Assert\Uuid] private $id; #[Assert\NotBlank(groups: ['postValidation'])] public $name; #[Assert\NotNull] #[Assert\Length(min: 2, max: 50, groups: ['postValidation'])] #[Assert\Length(min: 2, max: 70, groups: ['putValidation'])] public $author; // ... } ``` With this configuration, there are three validation groups: `Default` contains the constraints that belong to no other group. `postValidation` contains the constraints on the name and author (length from 2 to 50) fields only. `putValidation` contains the constraints on the author (length from 2 to 70) field only. ## Validation Groups and the Generated Schema Validation groups also affect the [JSON Schema](../core/json-schema.md) and [OpenAPI](../core/openapi.md) output that API Platform generates. When a constraint declares an explicit `groups:` option, it leaves the `Default` group (standard Symfony semantics). Schema generation is per-operation: if the operation does not declare `validationContext`, it falls back to the `Default` group only, so grouped constraints are not reflected in the generated schema. For example, with the following resource: ```php ['Default', 'postValidation']])] ``` Include `'Default'` in the list if the resource also has ungrouped constraints that should still apply. Omitting `'Default'` drops all constraints that do not belong to a named group, which is standard Symfony group behavior. ### Callable Validation Groups When `validationContext` uses a callable (a closure or an invokable service) to resolve groups dynamically at runtime, those groups **cannot** be reflected in the generated schema. The schema is built once per operation at documentation generation time, before any request object is available, so the callable is never invoked during schema generation. The schema falls back to the `Default` group only. If accurate OpenAPI / JSON Schema output matters for a given operation, prefer a static group list over a callable. ## Dynamic Validation Groups If you need to dynamically determine which validation groups to use for an entity in different scenarios, just pass in a [callable](https://www.php.net/manual/en/language.types.callable.php). The callback will receive the entity object as its first argument, and should return an array of group names or a [group sequence](https://symfony.com/doc/current/validation/sequence_provider.html). In the following example, we use a static method to return the validation groups: ```php [Book::class, 'validationGroups']] )] class Book { /** * Return dynamic validation groups. * * @param self $book Contains the instance of Book to validate. * * @return string[] */ public static function validationGroups(self $book) { return ['a']; } #[Assert\NotBlank(groups: ['a'])] public $name; #[Assert\NotNull(groups: ['b'])] public $author; // ... } ``` Alternatively, you can use a service to retrieve the groups to use: ```php authorizationChecker = $authorizationChecker; } public function __invoke($book): array { assert($book instanceof Book); return $this->authorizationChecker->isGranted('ROLE_ADMIN', $book) ? ['a', 'b'] : ['a']; } } ``` This class selects the groups to apply based on the role of the current user: if the current user has the `ROLE_ADMIN` role, groups `a` and `b` are returned. In other cases, just `a` is returned. This class is automatically registered as a service thanks to [the autowiring feature of the Symfony DependencyInjection component](https://symfony.com/doc/current/service_container/autowiring.html). Then, configure the entity class to use this service to retrieve validation groups: ```php AdminGroupsGenerator::class]) class Book { #[Assert\NotBlank(groups: ['a'])] public $name; #[Assert\NotNull(groups: ['b'])] public $author; // ... } ``` ## Sequential Validation Groups If you need to specify the order in which your validation groups must be tested against, you can use a [group sequence](https://symfony.com/doc/current/validation/sequence_provider.html). First, you need to create your sequenced group. ```php MySequencedGroup::class])] class Greeting { #[ORM\Id, ORM\Column, ORM\GeneratedValue] private ?int $id = null; /** * @var A nice person * * I want this "second" validation to be executed after the "first" one even though I wrote them in this order. */ #[One(groups: ['second'])] #[Two(groups: ['first'])] #[ORM\Column] public string $name = ''; public function getId(): int { return $this->id; } } ``` ## Validating Delete Operations By default, validation rules that are specified on the API resource are not evaluated during DELETE operations. You need to trigger the validation in your code, if needed. Assume that you have the following entity that uses a custom delete validator: ```php ['deleteValidation']], processor: MyEntityRemoveProcessor::class) ] )] #[AssertCanDelete(groups: ['deleteValidation'])] class MyEntity { #[ORM\Id, ORM\Column, ORM\GeneratedValue] private ?int $id = null; #[ORM\Column] public string $name = ''; } ``` Create a processor, which receives the default processor, where you will trigger the validation: ```php validator->validate($data, ['groups' => ['deleteValidation']]); $this->doctrineProcessor->process($data, $operation, $uriVariables, $context); } } ``` ## Error Levels and Payload Serialization As stated in the [Symfony documentation](https://symfony.com/doc/current/validation/severity.html), you can use the payload field to define error levels. You can retrieve the payload field by setting the `serialize_payload_fields` to an empty `array` in the API Platform config: ```yaml # api/config/packages/api_platform.yaml api_platform: validator: serialize_payload_fields: ~ ``` Then, the serializer will return all payload values in the error response. If you want to serialize only some payload fields, define them in the config like this: ```yaml # api/config/packages/api_platform.yaml api_platform: validator: serialize_payload_fields: [severity, anotherPayloadField] ``` In this example, only `severity` and `anotherPayloadField` will be serialized. ## Validation on Collection Relations Use the [Valid](https://symfony.com/doc/current/reference/constraints/Valid.html) constraint. Note: this is related to the [collection relation denormalization](../core/serialization.md#collection-relation-using-doctrine). You may have an issue when trying to validate a relation representing a Doctrine's `ArrayCollection` (`toMany`). Fix the denormalization using the property getter. Return an `array` instead of an `ArrayCollection` with `$collectionRelation->getValues()`. Then, define your validation on the getter instead of the property. For example: ```xml ``` ```php cars = new ArrayCollection(); } #[Assert\Valid] public function getCars() { return $this->cars->getValues(); } } ``` ## Open Vocabulary Generated from Validation Metadata API Platform automatically detects Symfony's built-in validators and generates schema.org IRI metadata accordingly. This allows for rich clients such as the Admin component to infer the field types for most basic use cases. The following validation constraints are covered: | Constraints | Vocabulary | | ------------------------------------------------------------------------------------- | ---------------------------------- | | [`Url`](https://symfony.com/doc/current/reference/constraints/Url.html) | `https://schema.org/url` | | [`Email`](https://symfony.com/doc/current/reference/constraints/Email.html) | `https://schema.org/email` | | [`Uuid`](https://symfony.com/doc/current/reference/constraints/Uuid.html) | `https://schema.org/identifier` | | [`CardScheme`](https://symfony.com/doc/current/reference/constraints/CardScheme.html) | `https://schema.org/identifier` | | [`Bic`](https://symfony.com/doc/current/reference/constraints/Bic.html) | `https://schema.org/identifier` | | [`Iban`](https://symfony.com/doc/current/reference/constraints/Iban.html) | `https://schema.org/identifier` | | [`Date`](https://symfony.com/doc/current/reference/constraints/Date.html) | `https://schema.org/Date` | | [`DateTime`](https://symfony.com/doc/current/reference/constraints/DateTime.html) | `https://schema.org/DateTime` | | [`Time`](https://symfony.com/doc/current/reference/constraints/Time.html) | `https://schema.org/Time` | | [`Image`](https://symfony.com/doc/current/reference/constraints/Image.html) | `https://schema.org/image` | | [`File`](https://symfony.com/doc/current/reference/constraints/File.html) | `https://schema.org/MediaObject` | | [`Currency`](https://symfony.com/doc/current/reference/constraints/Currency.html) | `https://schema.org/priceCurrency` | | [`Isbn`](https://symfony.com/doc/current/reference/constraints/Isbn.html) | `https://schema.org/isbn` | | [`Issn`](https://symfony.com/doc/current/reference/constraints/Issn.html) | `https://schema.org/issn` | ## Specification Property Restrictions API Platform generates specification property restrictions based on Symfony’s built-in validator. For example, from [`Regex`](https://symfony.com/doc/4.4/reference/constraints/Regex.html) constraint API Platform builds [`pattern`](https://swagger.io/docs/specification/data-models/data-types/#pattern) restriction. For building custom property schema based on custom validation constraints you can create a custom class for generating property scheme restriction. To create property schema, you have to implement the [`PropertySchemaRestrictionMetadataInterface`](https://github.com/api-platform/core/blob/main/src/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaRestrictionMetadataInterface.php). This interface defines only 2 methods: - `create`: to create property schema - `supports`: to check whether the property and constraint is supported Here is an implementation example: ```php // api/src/PropertySchemaRestriction/CustomPropertySchemaRestriction.php namespace App\PropertySchemaRestriction; use ApiPlatform\Metadata\ApiProperty; use Symfony\Component\Validator\Constraint; use App\Validator\CustomConstraint; final class CustomPropertySchemaRestriction implements PropertySchemaRestrictionMetadataInterface { public function supports(Constraint $constraint, ApiProperty $propertyMetadata): bool { return $constraint instanceof CustomConstraint; } public function create(Constraint $constraint, ApiProperty $propertyMetadata): array { // your logic to create property schema restriction based on constraint return $restriction; } } ``` If you use a custom dependency injection configuration, you need to register the corresponding service and add the `api_platform.metadata.property_schema_restriction` tag. The `priority` attribute can be used for service ordering. ```yaml # api/config/services.yaml services: # ... 'App\PropertySchemaRestriction\CustomPropertySchemaRestriction': ~ # Uncomment only if autoconfiguration is disabled #tags: [ 'api_platform.metadata.property_schema_restriction' ] ``` ## Collecting Denormalization Errors When submitting data you can collect denormalization errors using the [COLLECT_DENORMALIZATION_ERRORS option](https://symfony.com/doc/current/components/serializer.html#collecting-type-errors-while-denormalizing). It can be done directly in the `#[ApiResource]` attribute (or in the operations): ```php Security screencast
Watch the Security screencast

```php Resource signature can be modified at the property level as well: ```php In this example: - The user must be logged in to interact with `Book` resources (configured at the resource level) - Only users having [the role](https://symfony.com/doc/current/security.html#roles) `ROLE_ADMIN` can create a new resource (configured on the `post` operation) - Only users having the `ROLE_ADMIN` or owning the current object can replace an existing book (configured on the `put` operation) - Only users having the `ROLE_ADMIN` can view or modify the `adminOnlyProperty` property. Only users having the `ROLE_ADMIN` can create a new resource specifying `adminOnlyProperty` value. - Only users that are granted the `UPDATE` attribute on the book (via a voter) can write to the field Available variables are: - `user`: the current logged in object, if any - `object`: the current resource class during denormalization, the current resource during normalization, or collection of resources for collection operations - `previous_object`: (`securityPostDenormalize` only) a clone of `object`, before modifications were made - this is `null` for create operations - `request` (only at the resource level): the current request Access control checks in the `security` attribute are always executed before the [denormalization step](../core/serialization.md). It means that for `PUT` or `PATCH` requests, `object` doesn't contain the value submitted by the user, but values currently stored in [the persistence layer](../core/state-processors.md). ## Executing Access Control Rules After Denormalization In some cases, it might be useful to execute a security after the denormalization step. To do so, use the `securityPostDenormalize` attribute: ```php This time, the `object` variable contains data that have been extracted from the HTTP request body during the denormalization process. However, the object is not persisted yet. Additionally, in some cases you need to perform security checks on the original data. For example here, only the actual owner should be allowed to edit their book. In these cases, you can use the `previous_object` variable which contains the object that was read from the state provider. The value in the `previous_object` variable is cloned from the original object. Note that, by default, this clone is not a deep one (it doesn't clone relationships, relationships are references). To make a deep clone, [implement `__clone` method](https://www.php.net/manual/en/language.oop5.cloning.php) in the concerned resource class.i ## Controlling the response on `securityPostDenormalize` By default, when a request for a write operation is made that doesn't meet the `securityPostDenormalize` requirements (i.e. the expression returns `false`), the values of those protected properties in the request data are silently discarded and not set on the object. Any properties the user does have permission to update will be updated and the request succeeds. You can optionally instruct API Platform to instead return a 403 Access Denied response in such cases, by adding `throw_on_access_denied` as an extra property with a value of `true`: ```php true] )] class Book { // ... } ``` ```yaml # api/config/api_platform/resources.yaml resources: App\Entity\Book: operations: ApiPlatform\Metadata\Get: ~ ApiPlatform\Metadata\GetCollectionPut: securityPostDenormalize: "is_granted('ROLE_ADMIN') or (object.owner == user and previous_object.owner == user)" extraProperties: throw_on_access_denied: true # ... ``` ```xml is_granted('ROLE_ADMIN') or (object.owner == user and previous_object.owner == user) ``` ## Hooking Custom Permission Checks Using Voters The easiest and recommended way to hook custom access control logic is [to write Symfony Voter classes](https://symfony.com/doc/current/security/voters.html). Your custom voters will automatically be used in security expressions through the `is_granted()` function. In order to give the current `object` to your voter, use the expression `is_granted('READ', object)` For example: ```php Please note that if you use both `security: "..."` and then `"post" => ["securityPostDenormalize" => "..."]`, the `security` on top level is called first, and after `securityPostDenormalize`. This could lead to unwanted behaviour, so avoid using both of them simultaneously. If you need to use `securityPostDenormalize`, consider adding `security` for the other operations instead of the global one. Create a _BookVoter_ with the `bin/console make:voter` command: ```php security = $security; } protected function supports($attribute, $subject): bool { $supportsAttribute = in_array($attribute, ['BOOK_CREATE', 'BOOK_READ', 'BOOK_EDIT', 'BOOK_DELETE']); $supportsSubject = $subject instanceof Book; return $supportsAttribute && $supportsSubject; } /** * @param string $attribute * @param Book $subject * @param TokenInterface $token * @return bool */ protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool { /** ... check if the user is anonymous ... **/ switch ($attribute) { case 'BOOK_CREATE': if ( $this->security->isGranted(Role::ADMIN) ) { return true; } // only admins can create books break; case 'BOOK_READ': /** ... other authorization rules ... **/ } return false; } } ``` _Note 1: When using Voters on POST methods: The voter needs an `$attribute` and `$subject` as input parameter, so you have to use the `securityPostDenormalize` (i.e. `"post" = { "securityPostDenormalize" = "is_granted('BOOK_CREATE', object)" }` ) because the object does not exist before denormalization (it is not created, yet.)_ _Note 2: You can't use Voters on the collection GET method, use [Collection Filters](https://api-platform.com/docs/core/security/#filtering-collection-according-to-the-current-user-permissions) instead._ ## Configuring the Access Control Error Message By default when API requests are denied, you will get the "Access Denied" message. You can change it by configuring the `securityMessage` attribute or the `securityPostDenormalizeMessage` attribute. For example: ```php ## Filtering Collection According to the Current User Permissions Filtering collections according to the role or permissions of the current user must be done directly at [the state provider](../core/state-providers.md) level. For instance, when using the built-in adapters for Doctrine ORM, MongoDB and ElasticSearch, removing entries from a collection should be done using [extensions](../core/extensions.md). Extensions allow to customize the generated DQL/Mongo/Elastic/... query used to retrieve the collection (e.g. add `WHERE` clauses depending of the currently connected user) instead of using access control expressions. As extensions are services, you can [inject the Symfony `Security` class](https://symfony.com/doc/current/security.html#b-fetching-the-user-from-a-service) into them to access to current user's roles and permissions. If you use [custom state providers](../core/state-providers.md), you'll have to implement the filtering logic according to the persistence layer you rely on. ## Disabling Operations To completely disable some operations from your application, refer to the [disabling operations](../core/operations.md#enabling-and-disabling-operations) section. ## Changing Serialization Groups Depending of the Current User See [how to dynamically change](../core/serialization.md#changing-the-serialization-context-dynamically) the current Serializer context according to the current logged-in user. --- # Testing the API with Symfony Source: https://api-platform.com/docs/symfony/testing/ # Testing the API with Symfony For an introduction to testing using API Platform, refer to the [Core Testing Documentation](../core/testing.md), or access the [Laravel Testing Guide](../laravel/testing.md). Let's learn how to use tests with Symfony!

Tests and Assertions screencast
Watch the Tests & Assertions screencast

In this article you'll learn how to use: - [PHPUnit](https://phpunit.de), a testing framework to cover your classes with unit tests and to write API-oriented functional tests thanks to its API Platform and [Symfony](https://symfony.com/doc/current/testing.html) integrations. - [DoctrineFixturesBundle](https://symfony.com/bundles/DoctrineFixturesBundle/current/index.html), a bundle to load data fixtures in the database. - [Foundry](https://github.com/zenstruck/foundry), an expressive fixtures generator to write data fixtures. ## Creating Data Fixtures Before creating your functional tests, you will need a dataset to pre-populate your API and be able to test it. First, install [Foundry](https://github.com/zenstruck/foundry) and [Doctrine/DoctrineFixturesBundle](https://github.com/doctrine/DoctrineFixturesBundle): ```console composer require --dev foundry orm-fixtures ``` Thanks to Symfony Flex, [DoctrineFixturesBundle](https://github.com/doctrine/DoctrineFixturesBundle) and [Foundry](https://github.com/zenstruck/foundry) are ready to use! Then, create some factories for [the bookstore API you created in the tutorial](index.md): ```console bin/console make:factory 'App\Entity\Book' bin/console make:factory 'App\Entity\Review' ``` Improve the default values: ```php // src/Factory/BookFactory.php // ... protected function getDefaults(): array { return [ 'author' => self::faker()->name(), 'description' => self::faker()->text(), 'isbn' => self::faker()->isbn13(), 'publication_date' => \DateTimeImmutable::createFromMutable(self::faker()->dateTime()), 'title' => self::faker()->sentence(4), ]; } ``` ```php // src/Factory/ReviewFactory.php // ... use function Zenstruck\Foundry\lazy; // ... protected function getDefaults(): array { return [ 'author' => self::faker()->name(), 'body' => self::faker()->text(), 'book' => lazy(fn() => BookFactory::randomOrCreate()), 'publicationDate' => \DateTimeImmutable::createFromMutable(self::faker()->dateTime()), 'rating' => self::faker()->numberBetween(0, 5), ]; } ``` Create some stories: ```console bin/console make:story 'DefaultBooks' bin/console make:story 'DefaultReviews' ``` ```php // src/Story/DefaultBooksStory.php namespace App\Story; use App\Factory\BookFactory; use Zenstruck\Foundry\Story; final class DefaultBooksStory extends Story { public function build(): void { BookFactory::createMany(100); } } ``` ```php // src/Story/DefaultReviewsStory.php namespace App\Story; use App\Factory\ReviewFactory; use Zenstruck\Foundry\Story; final class DefaultReviewsStory extends Story { public function build(): void { ReviewFactory::createMany(200); } } ``` Edit your Fixtures: ```php //src/DataFixtures/AppFixtures.php namespace App\DataFixtures; use App\Story\DefaultBooksStory; use App\Story\DefaultReviewsStory; use Doctrine\Bundle\FixturesBundle\Fixture; use Doctrine\Persistence\ObjectManager; class AppFixtures extends Fixture { public function load(ObjectManager $manager): void { DefaultBooksStory::load(); DefaultReviewsStory::load(); } } ``` You can now load your fixtures in the database with the following command: ```console bin/console doctrine:fixtures:load ``` To learn more about fixtures, take a look at the documentation of [Foundry](https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html). The list of available generators as well as a cookbook explaining how to create custom generators can be found in the documentation of [Faker](https://github.com/fakerphp/faker), the library used by Foundry under the hood. ## Writing Functional Tests Now that you have some data fixtures for your API, you are ready to write functional tests with [PHPUnit](https://phpunit.de). The API Platform test client implements the interfaces of the [Symfony HttpClient](https://symfony.com/doc/current/components/http_client.html). HttpClient is shipped with the API Platform Symfony variant. The [Symfony test pack](https://github.com/symfony/test-pack/blob/main/composer.json), which includes PHPUnit as well as Symfony components useful for testing, is also included. Run `composer require --dev symfony/test-pack symfony/http-client` to install the testing tools (when using the API Platform Symfony variant they're already installed). Install [DAMADoctrineTestBundle](https://github.com/dmaicher/doctrine-test-bundle) to reset the database automatically before each test: ```console composer require --dev dama/doctrine-test-bundle ``` And activate it in the `phpunit.xml.dist` file: ```xml ``` Optionally, you can install [JSON Schema for PHP](https://github.com/justinrainbow/json-schema) if you want to use the [JSON Schema](https://json-schema.org) test assertions provided by API Platform: ```console composer require --dev justinrainbow/json-schema ``` Your API is now ready to be functionally tested. Create your test classes under the `tests/` directory. Here is an example of functional tests specifying the behavior of [the bookstore API you created in the tutorial](index.md): ```php request('GET', '/books'); $this->assertResponseIsSuccessful(); // Asserts that the returned content type is JSON-LD (the default) $this->assertResponseHeaderSame('content-type', 'application/ld+json; charset=utf-8'); // Asserts that the returned JSON is a superset of this one $this->assertJsonContains([ '@context' => '/contexts/Book', '@id' => '/books', '@type' => 'Collection', 'totalItems' => 100, 'view' => [ '@id' => '/books?page=1', '@type' => 'PartialCollectionView', 'first' => '/books?page=1', 'last' => '/books?page=4', 'next' => '/books?page=2', ], ]); // Because test fixtures are automatically loaded between each test, you can assert on them $this->assertCount(30, $response->toArray()['member']); // Asserts that the returned JSON is validated by the JSON Schema generated for this resource by API Platform // This generated JSON Schema is also used in the OpenAPI spec! $this->assertMatchesResourceCollectionJsonSchema(Book::class); } public function testCreateBook(): void { $response = static::createClient()->request('POST', '/books', ['json' => [ 'isbn' => '0099740915', 'title' => 'The Handmaid\'s Tale', 'description' => 'Brilliantly conceived and executed, this powerful evocation of twenty-first century America gives full rein to Margaret Atwood\'s devastating irony, wit and astute perception.', 'author' => 'Margaret Atwood', 'publicationDate' => '1985-07-31T00:00:00+00:00', ]]); $this->assertResponseStatusCodeSame(201); $this->assertResponseHeaderSame('content-type', 'application/ld+json; charset=utf-8'); $this->assertJsonContains([ '@context' => '/contexts/Book', '@type' => 'Book', 'isbn' => '0099740915', 'title' => 'The Handmaid\'s Tale', 'description' => 'Brilliantly conceived and executed, this powerful evocation of twenty-first century America gives full rein to Margaret Atwood\'s devastating irony, wit and astute perception.', 'author' => 'Margaret Atwood', 'publicationDate' => '1985-07-31T00:00:00+00:00', 'reviews' => [], ]); $this->assertMatchesRegularExpression('~^/books/\d+$~', $response->toArray()['@id']); $this->assertMatchesResourceItemJsonSchema(Book::class); } public function testCreateInvalidBook(): void { static::createClient()->request('POST', '/books', ['json' => [ 'isbn' => 'invalid', ]]); $this->assertResponseStatusCodeSame(422); $this->assertResponseHeaderSame('content-type', 'application/ld+json; charset=utf-8'); $this->assertJsonContains([ '@context' => '/contexts/ConstraintViolationList', '@type' => 'ConstraintViolationList', 'title' => 'An error occurred', 'description' => 'isbn: This value is neither a valid ISBN-10 nor a valid ISBN-13. title: This value should not be blank. description: This value should not be blank. author: This value should not be blank. publicationDate: This value should not be null.', ]); } public function testUpdateBook(): void { // Only create the book we need with a given ISBN BookFactory::createOne(['isbn' => '9781344037075']); $client = static::createClient(); // findIriBy allows to retrieve the IRI of an item by searching for some of its properties. $iri = $this->findIriBy(Book::class, ['isbn' => '9781344037075']); // Use the PATCH method here to do a partial update $client->request('PATCH', $iri, [ 'json' => [ 'title' => 'updated title', ], 'headers' => [ 'Content-Type' => 'application/merge-patch+json', ] ]); $this->assertResponseIsSuccessful(); $this->assertJsonContains([ '@id' => $iri, 'isbn' => '9781344037075', 'title' => 'updated title', ]); } public function testDeleteBook(): void { // Only create the book we need with a given ISBN BookFactory::createOne(['isbn' => '9781344037075']); $client = static::createClient(); $iri = $this->findIriBy(Book::class, ['isbn' => '9781344037075']); $client->request('DELETE', $iri); $this->assertResponseStatusCodeSame(204); $this->assertNull( // Through the container, you can access all your services from the tests, including the ORM, the mailer, remote API clients... static::getContainer()->get('doctrine')->getRepository(Book::class)->findOneBy(['isbn' => '9781344037075']) ); } } ``` As you can see, the example uses the [trait `ResetDatabase`](https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#database-reset) from [Foundry](https://github.com/zenstruck/foundry) which will, at the beginning of each test, purge the database, begin a transaction, and, at the end of each test, roll back the transaction previously begun. Because of this, you can run your tests without worrying about fixtures. There is one caveat though: in some tests, it is necessary to perform multiple requests in one test, for example when creating a user via the API and checking that a subsequent login using the same password works. However, the client will by default reboot the kernel, which will reset the database. You can prevent this by adding `$client->disableReboot();` to such tests. All you have to do now is to run your tests: ```console bin/phpunit ``` If everything is working properly, you should see `OK (5 tests, 17 assertions)`. Your REST API is now properly tested! Check out the [API Test Assertions section](#api-test-assertions-with-symfony) to discover the full range of assertions and other features provided by API Platform's test utilities. ## Writing Unit Tests In addition to integration tests written using the helpers provided by `ApiTestCase`, all the classes of your project should be covered by [unit tests](https://en.wikipedia.org/wiki/Unit_testing). To do so, learn how to write unit tests with [PHPUnit](https://phpunit.de/) and [its Symfony/API Platform integration](https://symfony.com/doc/current/testing.html). ## Continuous Integration, Continuous Delivery and Continuous Deployment Running your test suite in your [CI/CD pipeline](https://en.wikipedia.org/wiki/Continuous_integration) is important to ensure good quality and delivery time. The API Platform Demo is [shipped with a GitHub Actions workflow](https://github.com/api-platform/demo/tree/main/.github/workflows) that builds the Docker images, does a [smoke test]() to check that the application's entrypoint is accessible, and runs PHPUnit. The API Platform Demo [contains a CD workflow](https://github.com/api-platform/demo/tree/main/.github/workflows) that uses [a Helm chart](../deployment/kubernetes.md) to deploy the app on a Kubernetes cluster. ## Additional and Alternative Testing Tools You may also be interested in these alternative testing tools (not included by default): - [Hoppscotch](https://docs.hoppscotch.io/), create functional [test](https://docs.hoppscotch.io/documentation/getting-started/rest/tests) for your API Platform project using a nice UI, benefit from its Swagger integration and run tests in the CI using [the command-line tool](https://docs.hoppscotch.io/documentation/clients/cli/overview); - [Behat](https://behat.org), a [behavior-driven development (BDD)](https://en.wikipedia.org/wiki/Behavior-driven_development) framework to write the API specification as user stories and in natural language then execute these scenarios against the application to validate its behavior; - [Blackfire Player](https://blackfire.io/player), a nice DSL to crawl HTTP services, assert responses, and extract data from HTML/XML/JSON responses; - [PHP Matcher](https://github.com/coduo/php-matcher), the Swiss Army knife of JSON document testing. ## End-to-End Testing If you would like to verify that your stack (including services such as the DBMS, web server, [Varnish](https://varnish-cache.org/)) works, you need [end-to-end testing](https://wiki.c2.com/?EndToEndPrinciple). To do so, we recommend using [Playwright](https://playwright.dev) if you use have PWA/JavaScript-heavy app, or [Symfony Panther](https://github.com/symfony/panther) if you mostly use Twig. Usually, end-to-end testing should be done with a production-like setup. For your convenience, you may [run our Docker Compose setup for production locally](../deployment/docker-compose.md#deploying-with-docker-compose). ## Testing Utilities for Symfony API Platform provides a set of useful utilities dedicated to API testing. For an overview of how to test an API Platform app, be sure to read [the testing part first](#testing-the-api-with-symfony).

Test and Assertions screencast
Watch the API Tests & Assertions screencast

### The Test HttpClient API Platform provides its own implementation of the [Symfony HttpClient](https://symfony.com/doc/current/components/http_client.html)'s interfaces, tailored to be used directly in [PHPUnit](https://phpunit.de/) test classes. While all the convenient features of Symfony HttpClient are available and usable directly, under the hood the API Platform implementation manipulates [the Symfony HttpKernel](https://symfony.com/doc/current/components/http_kernel.html) directly to simulate HTTP requests and responses. This approach results in a huge performance boost compared to triggering real network requests. It also allows access to the [Symfony HttpKernel](https://symfony.com/doc/current/components/http_kernel.html) and to all your services via the [Dependency Injection Container](https://symfony.com/doc/current/testing.html#accessing-the-container). Reuse them to run, for instance, SQL queries or requests to external APIs directly from your tests. Install the `symfony/http-client` and `symfony/browser-kit` packages to enable the API Platform test client: ```console composer require symfony/browser-kit symfony/http-client ``` To use the testing client, your test class must extend the `ApiTestCase` class: ```php request('GET', '/books'); // your assertions here... } } ``` Refer to [the Symfony HttpClient documentation](https://symfony.com/doc/current/components/http_client.html) to discover all the features of the client (custom headers, JSON encoding and decoding, HTTP Basic and Bearer authentication and cookies support, among other things). Note that you can create your own test case class extending the ApiTestCase. For example to set up a Json Web Token authentication: ```php getToken(); return static::createClient([], ['headers' => ['authorization' => 'Bearer '.$token]]); } /** * Use other credentials if needed. */ protected function getToken($body = []): string { if ($this->token) { return $this->token; } $response = static::createClient()->request('POST', '/login', ['json' => $body ?: [ 'username' => 'admin@example.com', 'password' => '$3cr3t', ]]); $this->assertResponseIsSuccessful(); $data = $response->toArray(); $this->token = $data['token']; return $data['token']; } } ``` Use it by extending the `AbstractTest` class. For example this class tests the `/users` resource accessibility where only the admin can retrieve the collection: ```php createClientWithCredentials()->request('GET', '/users'); $this->assertResponseIsSuccessful(); } public function testLoginAsUser() { $token = $this->getToken([ 'username' => 'user@example.com', 'password' => '$3cr3t', ]); $response = $this->createClientWithCredentials($token)->request('GET', '/users'); $this->assertJsonContains(['description' => 'Access Denied.']); $this->assertResponseStatusCodeSame(403); } } ``` ### API Test Assertions with Symfony In addition to [the built-in ones](https://phpunit.readthedocs.io/en/11.4/assertions.html), API Platform provides convenient PHPUnit assertions dedicated to API testing: ```php request(...); // Asserts that the returned JSON is equal to the passed one $this->assertJsonEquals(/* a JSON document as an array or as a string */); // Asserts that the returned JSON is a superset of the passed one $this->assertJsonContains(/* a JSON document as an array or as a string */); // justinrainbow/json-schema must be installed to use the following assertions // Asserts that the returned JSON matches the passed JSON Schema $this->assertMatchesJsonSchema(/* a JSON Schema as an array or as a string */); // Asserts that the returned JSON is validated by the JSON Schema generated for this resource by API Platform // For collections $this->assertMatchesResourceCollectionJsonSchema(YourApiResource::class); // And for items $this->assertMatchesResourceItemJsonSchema(YourApiResource::class); } } ``` There is also a method to find the IRI matching a given resource and some criteria: ```php findIriBy(Book::class, ['isbn' => '9780451524935']); static::createClient()->request('GET', $iri); $this->assertResponseIsSuccessful(); } } ``` ### HTTP Test Assertions All test assertions provided by Symfony (assertions for status codes, headers, cookies, XML documents...) can be used out of the box with the API Platform test client: ```php request('GET', '/books'); $this->assertResponseIsSuccessful(); $this->assertResponseHeaderSame('content-type', 'application/ld+json; charset=utf-8'); } } ``` [Check out the dedicated Symfony documentation entry](https://symfony.com/doc/current/testing/functional_tests_assertions.html). --- # Debugging with Symfony Source: https://api-platform.com/docs/symfony/debugging/ # Debugging with Symfony

API Platform debugging screencast
Watch the Debugging API Platform screencast

## Xdebug For development purposes such as debugging tests or remote API requests, [Xdebug](https://xdebug.org/) is shipped by default with the Docker setup of the API Platform Symfony variant. To enable it, run: ```console XDEBUG_MODE=debug XDEBUG_SESSION=1 docker compose up --wait ``` ## Using Xdebug with PhpStorm First, [create a PHP debug remote server configuration](https://www.jetbrains.com/help/phpstorm/creating-a-php-debug-server-configuration.html): 1. In the `Settings/Preferences` dialog, go to `PHP | Servers` 2. Create a new server: - Name: `api` (or whatever you want to use for the variable `PHP_IDE_CONFIG`) - Host: `localhost` (or the one defined using the `SERVER_NAME` environment variable) - Port: `443` - Debugger: `Xdebug` - Check `Use path mappings` - Map the local `api/` directory to the `/app` absolute path on the server You can now use the debugger! 1. In PhpStorm, open the `Run` menu and click on `Start Listening for PHP Debug Connections` 2. Add the `XDEBUG_SESSION=PHPSTORM` query parameter to the URL of the page you want to debug or use [other available triggers](https://xdebug.org/docs/step_debug#activate_debugger). Alternatively, you can use [the Xdebug extension](https://xdebug.org/docs/step_debug#browser-extensions) for your preferred web browser. 3. On the command-line, we might need to tell PhpStorm which [path mapping configuration](https://www.jetbrains.com/help/phpstorm/zero-configuration-debugging-cli.html#configure-path-mappings) should be used, set the value of the PHP_IDE_CONFIG environment variable to `serverName=api`, where `api` is the name of the debug server configured higher. Example: ```console XDEBUG_SESSION=1 PHP_IDE_CONFIG="serverName=api" php bin/console ... ``` ## Using Xdebug With Visual Studio Code If you are using Visual Studio Code, use the following `launch.json` to debug. Note that this configuration includes the path mappings for the Docker image. ```json { "version": "0.2.0", "configurations": [ { "name": "Listen for Xdebug", "type": "php", "request": "launch", "port": 9003, "log": true, "pathMappings": { "/app": "${workspaceFolder}/api" } } ] } ``` > [!NOTE] > > On Linux, the `client_host` setting of `host.docker.internal` may not work. In this case you will > need the actual local IP address of your computer. ## Troubleshooting Inspect the installation with the following command. The requested Xdebug version should be displayed in the output. ```console $ docker compose exec php \ php --version PHP … with Xdebug v…, Copyright (c) 2002-2021, by Derick Rethans … ``` --- # Configuring the Caddy Web Server with Symfony Source: https://api-platform.com/docs/symfony/caddy/ # Configuring the Caddy Web Server with Symfony [The API Platform Symfony variant](index.md), when generated with Docker, is shipped with [the Caddy web server](https://caddyserver.com). The build contains the [Mercure](../core/mercure.md) and the [Vulcain](https://vulcain.rocks) Caddy modules. Caddy is positioned in front of the web API and of the Progressive Web App (PWA). It routes requests to either service depending on the value of the `Accept` HTTP header or the path of the request. Using the same domain to serve the API and the PWA [improves performance by preventing unnecessary CORS preflight requests and encourages embracing the REST principles](https://dunglas.fr/2022/01/preventing-cors-preflight-requests-using-content-negotiation/). ## Why `route {}` Is Required Caddy processes directives in a [predefined global order](https://caddyserver.com/docs/caddyfile/directives#directive-order), not in the order they appear in the Caddyfile. In that order, `rewrite` runs **before** `reverse_proxy`. Without explicit ordering, a browser request to `/` would match the `@phpRoute` rewrite condition and be rewritten to `index.php` before Caddy ever evaluated whether the request should be proxied to Next.js. Wrapping the directives in a `route {}` block enforces **strict first-match-wins evaluation in file order**. The first directive that matches a request wins, and Caddy stops evaluating the rest. This is what makes the `@pwa` proxy check run before the PHP rewrite: ```caddy route { # 1. Check @pwa first — proxy to Next.js if matched reverse_proxy @pwa http://{$PWA_UPSTREAM} # 2. Only if @pwa did not match, rewrite to index.php @phpRoute { not path /.well-known/mercure*; not file {path} } rewrite @phpRoute index.php # 3. Run PHP for index.php @frontController path index.php php @frontController # 4. Serve remaining static files file_server { hide *.php } } ``` ## The `@pwa` Matcher The `@pwa` named matcher is a [CEL (Common Expression Language) expression](https://caddyserver.com/docs/caddyfile/matchers#expression) that decides which requests are forwarded to the Next.js application: ```caddy @pwa expression `( header({'Accept': '*text/html*'}) && !path( '/docs*', '/graphql*', '/bundles*', '/contexts*', '/_profiler*', '/_wdt*', '*.json*', '*.html', '*.csv', '*.yml', '*.yaml', '*.xml' ) ) || path('/favicon.ico', '/manifest.json', '/robots.txt', '/sitemap*', '/_next*', '/__next*') || query({'_rsc': '*'})` ``` The expression has three independent clauses joined by `||`. A request matches `@pwa` if **any** clause is true. ### Clause 1: HTML requests that are not API paths A browser navigating to any URL sends `Accept: text/html, */*`. This clause forwards those requests to Next.js unless the path is known to be served by the API or carries an extension that API Platform handles through [content negotiation](../core/content-negotiation.md). Paths excluded from Next.js (handled by PHP instead): | Pattern | Reason | | -------------------------------------------------------- | --------------------------------------------------- | | `/docs*` | Swagger UI and OpenAPI documentation | | `/graphql*` | GraphQL endpoint | | `/bundles*` | Symfony bundle assets published by `assets:install` | | `/contexts*` | JSON-LD context documents | | `/_profiler*`, `/_wdt*` | Symfony Web Debug Toolbar and Profiler | | `*.json*`, `*.html`, `*.csv`, `*.yml`, `*.yaml`, `*.xml` | Content-negotiated formats served by the API | ### Clause 2: Next.js static assets and well-known files ```caddy path('/favicon.ico', '/manifest.json', '/robots.txt', '/sitemap*', '/_next*', '/__next*') ``` These paths are forwarded to Next.js unconditionally, regardless of the `Accept` header. `/_next/*` and `/__next/*` are the internal asset paths used by the Next.js runtime for JavaScript chunks, CSS, images, and hot module replacement updates in development. ### Clause 3: React Server Components requests ```caddy query({'_rsc': '*'}) ``` Next.js uses the `_rsc` query parameter internally for [React Server Components](https://nextjs.org/docs/app/building-your-application/rendering/server-components) data fetching. These requests do not carry `text/html` in their `Accept` header, so they would miss clause 1 without this dedicated check. ## The `Link` Header ```caddy header ?Link `; rel="http://www.w3.org/ns/hydra/core#apiDocumentation", ; rel="mercure"` ``` This directive is placed at the **site block level**, outside the `route {}` block, so it applies to every response regardless of whether it came from PHP or Next.js. The `?` prefix means the header is only set when not already present in the response — a PHP response that sets its own `Link` header is not overwritten. Setting this at the Caddy level serves two purposes: 1. **API discoverability**: every response advertises the Hydra API documentation URL, allowing clients to auto-discover the API. 2. **Mercure subscription**: every response advertises the Mercure hub URL, so clients can subscribe to real-time updates without any application code. The Next.js application does not need to set these headers itself — they arrive on every response automatically. ## The `PWA_UPSTREAM` Environment Variable ```caddy reverse_proxy @pwa http://{$PWA_UPSTREAM} ``` `PWA_UPSTREAM` is resolved at runtime from the container environment. In `compose.yaml` it is set to `pwa:3000`, where `pwa` is the Docker Compose service name and `3000` is the default port of the Next.js server. When the `pwa` service is not running (for example in an API-only project), Caddy returns a `502 Bad Gateway` for any request matching `@pwa`. To run without a Next.js frontend, comment out that line in the Caddyfile: ```caddy route { # Comment the following line if you don't want Next.js to catch requests for HTML documents. # In this case, they will be handled by the PHP app. # reverse_proxy @pwa http://{$PWA_UPSTREAM} @phpRoute { not path /.well-known/mercure*; not file {path} } rewrite @phpRoute index.php @frontController path index.php php @frontController file_server { hide *.php } } ``` ## Adjusting the Routing Rules ### Routing an admin path to PHP If you use EasyAdmin, SonataAdmin, or a custom Symfony controller that serves HTML pages, add the path prefix to the exclusion list inside clause 1 so those requests bypass Next.js: ```caddy @pwa expression `( header({'Accept': '*text/html*'}) && !path( '/admin*', '/docs*', '/graphql*', '/bundles*', '/contexts*', '/_profiler*', '/_wdt*', '*.json*', '*.html', '*.csv', '*.yml', '*.yaml', '*.xml' ) ) || path('/favicon.ico', '/manifest.json', '/robots.txt', '/sitemap*', '/_next*', '/__next*') || query({'_rsc': '*'})` ``` You can use [any CEL expression](https://caddyserver.com/docs/caddyfile/matchers#expression) supported by Caddy. ### Adding a custom API prefix If your API is mounted under a prefix such as `/api`, add it to the exclusion list: ```caddy && !path( '/api*', '/docs*', '/graphql*', ... ) ``` --- # JWT Authentication with Symfony Source: https://api-platform.com/docs/symfony/jwt/ # JWT Authentication with Symfony > [!NOTE] While solutions like `LexikJWTAuthenticationBundle` (Symfony) or `tymondesigns/jwt-auth` > (Laravel) are popular, **we recommend adopting open standards such as > [OpenID Connect (OIDC)](https://openid.net/connect/)** for robust, scalable, and interoperable > authentication.

JWT screencast
Watch the LexikJWTAuthenticationBundle screencast

## Installing LexikJWTAuthenticationBundle > [!NOTE] API Platform makes it easy to add JWT-based authentication to your API using > [LexikJWTAuthenticationBundle](https://github.com/lexik/LexikJWTAuthenticationBundle). We begin by installing the bundle: ```console composer require lexik/jwt-authentication-bundle ``` Then we need to generate the public and private keys used for signing JWT tokens. You can generate them by using this command: ```console php bin/console lexik:jwt:generate-keypair ``` Or if you're using the [API Platform Symfony variant](../symfony/index.md) with Docker, you may run this from the project's `api/` directory: ```console docker compose exec php sh -c ' set -e apt-get install openssl php bin/console lexik:jwt:generate-keypair setfacl -R -m u:www-data:rX -m u:"$(whoami)":rwX config/jwt setfacl -dR -m u:www-data:rX -m u:"$(whoami)":rwX config/jwt ' ``` Note that the `setfacl` command relies on the `acl` package. This is installed by default in the Docker setup of the API Platform Symfony variant but may need to be installed in your working environment in order to execute the `setfacl` command. This takes care of keypair creation (including using the correct passphrase to encrypt the private key), and setting the correct permissions on the keys allowing the web server to read them. If you want the keys to be auto generated in `dev` environment, see an example in the [docker-entrypoint script of api-platform/demo](https://github.com/api-platform/demo/blob/a03ce4fb1f0e072c126e8104e42a938bb840bffc/api/docker/php/docker-entrypoint.sh#L16-L17). Since these keys are created by the `root` user from a container, your host user will not be able to read them during the `docker compose build caddy` process. Add the `config/jwt/` folder to the `api/.dockerignore` file so that they are skipped from the result image. The keys should not be checked in to the repository (i.e. it's in `api/.gitignore`). However, note that a JWT token could only pass signature validation against the same pair of keys it was signed with. This is especially relevant in a production environment, where you don't want to accidentally invalidate all your clients' tokens at every deployment. For more information, refer to [the bundle's documentation](https://github.com/lexik/LexikJWTAuthenticationBundle/blob/2.x/Resources/doc/index.rst) or read a [general introduction to JWT here](https://jwt.io/introduction/). We're not done yet! Let's move on to configuring the Symfony SecurityBundle for JWT authentication. ## Configuring the Symfony SecurityBundle It is necessary to configure a user provider. You can either use the [Doctrine entity user provider](https://symfony.com/doc/current/security/user_provider.html#entity-user-provider) provided by Symfony (recommended), [create a custom user provider](https://symfony.com/doc/current/security/user_provider.html#creating-a-custom-user-provider) or use [API Platform's FOSUserBundle integration](../symfony/fosuser-bundle.md) (**not recommended**). If you choose to use the Doctrine entity user provider, start by [creating your `User` class](https://symfony.com/doc/current/security.html#a-create-your-user-class). Then update the security configuration: ```yaml # api/config/packages/security.yaml security: # https://symfony.com/doc/current/security.html#c-hashing-passwords password_hashers: App\Entity\User: "auto" # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers providers: # used to reload user from session & other features (e.g. switch_user) users: entity: class: App\Entity\User property: email # mongodb: # class: App\Document\User # property: email firewalls: dev: pattern: ^/_(profiler|wdt) security: false main: stateless: true provider: users json_login: check_path: /auth # The name in routes.yaml is enough for mapping username_path: email password_path: password success_handler: lexik_jwt_authentication.handler.authentication_success failure_handler: lexik_jwt_authentication.handler.authentication_failure jwt: ~ access_control: - { path: ^/$, roles: PUBLIC_ACCESS } # Allows accessing the Swagger UI - { path: ^/docs, roles: PUBLIC_ACCESS } # Allows accessing the Swagger UI docs - { path: ^/contexts, roles: PUBLIC_ACCESS } # Allows accessing the Swagger UI contexts - { path: ^/auth, roles: PUBLIC_ACCESS } - { path: ^/, roles: IS_AUTHENTICATED_FULLY } ``` You must also declare the route used for `/auth`: ```yaml # api/config/routes.yaml auth: path: /auth methods: ["POST"] ``` If you want to avoid loading the `User` entity from database each time a JWT token needs to be authenticated, you may consider using the [database-less user provider](https://github.com/lexik/LexikJWTAuthenticationBundle/blob/2.x/Resources/doc/8-jwt-user-provider.rst) provided by LexikJWTAuthenticationBundle. However, it means you will have to fetch the `User` entity from the database yourself as needed (probably through the Doctrine EntityManager). Refer to the section on [Security](security.md) to learn how to control access to API resources and operations. You may also want to [configure Swagger UI for JWT authentication](#documenting-the-authentication-mechanism-with-swaggeropen-api). ### Adding Authentication to an API Which Uses a Path Prefix If your API uses a [path prefix](https://symfony.com/doc/current/routing/external_resources.html#route-groups-and-prefixes), the security configuration would look something like this instead: ```yaml # api/config/packages/security.yaml security: # https://symfony.com/doc/current/security.html#c-hashing-passwords password_hashers: App\Entity\User: "auto" # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers providers: # used to reload user from session & other features (e.g. switch_user) users: entity: class: App\Entity\User property: email firewalls: dev: pattern: ^/_(profiler|wdt) security: false api: pattern: ^/api/ stateless: true provider: users jwt: ~ main: json_login: check_path: /auth # The name in routes.yaml is enough for mapping username_path: email password_path: password success_handler: lexik_jwt_authentication.handler.authentication_success failure_handler: lexik_jwt_authentication.handler.authentication_failure access_control: - { path: ^/$, roles: PUBLIC_ACCESS } # Allows accessing the Swagger UI - { path: ^/docs, roles: PUBLIC_ACCESS } # Allows accessing API documentations and Swagger UI docs - { path: ^/contexts, roles: PUBLIC_ACCESS } # Allows accessing the Swagger UI contexts - { path: ^/auth, roles: PUBLIC_ACCESS } - { path: ^/, roles: IS_AUTHENTICATED_FULLY } ``` ### Be sure to have lexik_jwt_authentication configured on your user_identity_field ```yaml # api/config/packages/lexik_jwt_authentication.yaml lexik_jwt_authentication: secret_key: "%env(resolve:JWT_SECRET_KEY)%" public_key: "%env(resolve:JWT_PUBLIC_KEY)%" pass_phrase: "%env(JWT_PASSPHRASE)%" ``` ## Documenting the Authentication Mechanism with Swagger/Open API Want to test the routes of your JWT-authentication-protected API? ### Configuring API Platform ```yaml # api/config/packages/api_platform.yaml api_platform: swagger: api_keys: JWT: name: Authorization type: header ``` The "Authorize" button will automatically appear in Swagger UI. ![Screenshot of API Platform with Authorize button](../core/images/JWTAuthorizeButton.png) ### Adding a New API Key All you have to do is configure the API key in the `value` field. By default, [only the authorization header mode is enabled](https://github.com/lexik/LexikJWTAuthenticationBundle/blob/2.x/Resources/doc/index.rst#2-use-the-token) in LexikJWTAuthenticationBundle. You must set the [JWT token](https://github.com/lexik/LexikJWTAuthenticationBundle/blob/2.x/Resources/doc/index.rst#1-obtain-the-token) as below and click on the "Authorize" button. `Bearer MY_NEW_TOKEN` ![Screenshot of API Platform with the configuration API Key](../core/images/JWTConfigureApiKey.png) ### Adding endpoint to SwaggerUI to retrieve a JWT token LexikJWTAuthenticationBundle has an integration with API Platform to automatically add an OpenAPI endpoint to conveniently retrieve the token in Swagger UI. If you need to modify the default configuration, you can do it in the dedicated configuration file: ```yaml # config/packages/lexik_jwt_authentication.yaml lexik_jwt_authentication: # ... api_platform: check_path: /auth username_path: email password_path: password ``` You will see something like this in Swagger UI: ![API Endpoint to retrieve JWT Token from SwaggerUI](../core/images/jwt-token-swagger-ui.png) ## Testing To test your authentication with `ApiTestCase`, you can write a method as below: ```php setEmail('test@example.com'); $user->setPassword( $container->get('security.user_password_hasher')->hashPassword($user, '$3CR3T') ); $manager = $container->get('doctrine')->getManager(); $manager->persist($user); $manager->flush(); // retrieve a token $response = $client->request('POST', '/auth', [ 'headers' => ['Content-Type' => 'application/json'], 'json' => [ 'email' => 'test@example.com', 'password' => '$3CR3T', ], ]); $json = $response->toArray(); $this->assertResponseIsSuccessful(); $this->assertArrayHasKey('token', $json); // test not authorized $client->request('GET', '/greetings'); $this->assertResponseStatusCodeSame(401); // test authorized $client->request('GET', '/greetings', ['auth_bearer' => $json['token']]); $this->assertResponseIsSuccessful(); } } ``` Refer to [Testing the API](../symfony/testing.md) for more information about testing API Platform. ### Improving Tests Suite Speed Since now we have a `JWT` authentication, functional tests require us to log in each time we want to test an API endpoint. This is where [Password Hashers](https://symfony.com/doc/current/security/passwords.html) come into play. Hashers are used for 2 reasons: 1. To generate a hash for a raw password (`$container->get('security.user_password_hasher')->hashPassword($user, '$3CR3T')`) 2. To verify a password during authentication While hashing and verifying 1 password is quite a fast operation, doing it hundreds or even thousands of times in a tests suite becomes a bottleneck, because reliable hashing algorithms are slow by their nature. To significantly improve the test suite speed, we can use more simple password hasher specifically for the `test` environment. ```yaml # override in api/config/packages/test/security.yaml for test env security: password_hashers: App\Entity\User: algorithm: md5 encode_as_base64: false iterations: 0 ``` --- # Symfony Messenger Integration: CQRS and Async Message Processing Source: https://api-platform.com/docs/symfony/messenger/ # Symfony Messenger Integration: CQRS and Async Message Processing API Platform provides an integration with the [Symfony Messenger Component](https://symfony.com/doc/current/messenger.html). This feature allows to implement the [Command Query Responsibility Segregation (CQRS)](https://martinfowler.com/bliki/CQRS.html) pattern in a convenient way. It also makes it easy to send messages through the web API that will be consumed asynchronously. Many transports are supported to dispatch messages to async consumers, including RabbitMQ, Apache Kafka, Amazon SQS and Google Pub/Sub. ## Installing Symfony Messenger To enable the support of Messenger, install the library: ```console composer require symfony/messenger ``` ## Dispatching a Resource through the Message Bus Set the `messenger` attribute to `true`, and API Platform will automatically dispatch the API Resource instance as a message using the message bus provided by the Messenger Component. The following example allows you to create a new `Person` in an asynchronous manner: ```php Because the `messenger` attribute is `true`, when a `POST` is handled by API Platform, the corresponding instance of the `Person` will be dispatched. For this example, only the `POST` operation is enabled. If the resource does not have any `Get` operation, API Platform [automatically adds an operation to help generating an IRI identify the resource](../core/operations/#enabling-and-disabling-operations). We use the `status` attribute to configure API Platform to return a [202 Accepted HTTP status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/202). It indicates that the request has been received and will be treated later, without giving an immediate return to the client. Finally, the `output` attribute is set to `false`, so the HTTP response that will be generated by API Platform will be empty, and the [serialization process](../core/serialization.md) will be skipped. **Note:** when using `messenger=true` ApiResource attribute in a Doctrine entity, the Doctrine Processor is not called. If you want the Doctrine Processor to be called, you should [decorate a built-in state processor](../core/state-processors.md#creating-a-custom-state-processor) and implement your own logic. ## Registering a Message Handler To process the message that will be dispatched, [a handler](https://symfony.com/doc/current/messenger.html#registering-handlers) must be created: ```php ['Default', 'user:create']]), new Get(), new Put(processor: UserPasswordHasher::class), new Patch(processor: UserPasswordHasher::class), new Delete(), ], normalizationContext: ['groups' => ['user:read']], denormalizationContext: ['groups' => ['user:create', 'user:update']], )] #[ORM\Entity(repositoryClass: UserRepository::class)] #[ORM\Table(name: '`user`')] #[UniqueEntity('email')] class User implements UserInterface, PasswordAuthenticatedUserInterface { #[Groups(['user:read'])] #[ORM\Id] #[ORM\Column(type: 'integer')] #[ORM\GeneratedValue] private ?int $id = null; #[Assert\NotBlank] #[Assert\Email] #[Groups(['user:read', 'user:create', 'user:update'])] #[ORM\Column(length: 180, unique: true)] private ?string $email = null; #[ORM\Column] private ?string $password = null; #[Assert\NotBlank(groups: ['user:create'])] #[Groups(['user:create', 'user:update'])] private ?string $plainPassword = null; #[ORM\Column(type: 'json')] private array $roles = []; public function getId(): ?int { return $this->id; } public function getEmail(): ?string { return $this->email; } public function setEmail(string $email): self { $this->email = $email; return $this; } /** * @see PasswordAuthenticatedUserInterface */ public function getPassword(): string { return $this->password; } public function setPassword(string $password): self { $this->password = $password; return $this; } public function getPlainPassword(): ?string { return $this->plainPassword; } public function setPlainPassword(?string $plainPassword): self { $this->plainPassword = $plainPassword; return $this; } /** * @see UserInterface */ public function getRoles(): array { $roles = $this->roles; $roles[] = 'ROLE_USER'; return array_unique($roles); } public function setRoles(array $roles): self { $this->roles = $roles; return $this; } /** * A visual identifier that represents this user. * * @see UserInterface */ public function getUserIdentifier(): string { return (string) $this->email; } /** * @see UserInterface * * Required until Symfony 8.0, where eraseCredentials() will be removed from the interface. * No-op since plainPassword is cleared manually in the password processor. */ public function eraseCredentials(): void { // Intentionally left blank } } ``` The repository is same as generated by Symfony. For completeness: ```php * * @method User|null find($id, $lockMode = null, $lockVersion = null) * @method User|null findOneBy(array $criteria, array $orderBy = null) * @method User[] findAll() * @method User[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) */ class UserRepository extends ServiceEntityRepository implements PasswordUpgraderInterface { public function __construct(ManagerRegistry $registry) { parent::__construct($registry, User::class); } public function save(User $entity, bool $flush = false): void { $this->getEntityManager()->persist($entity); if ($flush) { $this->getEntityManager()->flush(); } } public function remove(User $entity, bool $flush = false): void { $this->getEntityManager()->remove($entity); if ($flush) { $this->getEntityManager()->flush(); } } /** * Used to upgrade (rehash) the user's password automatically over time. */ public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void { if (!$user instanceof User) { throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', \get_class($user))); } $user->setPassword($newHashedPassword); $this->save($user, true); } } ``` ## Creating and Updating User Password There's no built-in way for hashing the plain password on `POST`, `PUT` or `PATCH`. Happily you can use the API Platform [state processors](../core/state-processors.md) for auto-hashing plain passwords. First create a new state processor: ```php */ final readonly class UserPasswordHasher implements ProcessorInterface { public function __construct( private ProcessorInterface $processor, private UserPasswordHasherInterface $passwordHasher ) { } /** * @param User $data */ public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): User { if (!$data->getPlainPassword()) { return $this->processor->process($data, $operation, $uriVariables, $context); } $hashedPassword = $this->passwordHasher->hashPassword( $data, $data->getPlainPassword() ); $data->setPassword($hashedPassword); // To avoid leaving sensitive data like the plain password in memory or logs, we manually clear it after hashing. $data->setPlainPassword(null); return $this->processor->process($data, $operation, $uriVariables, $context); } } ``` Then bind it to the ORM persist processor: ```yaml # api/config/services.yaml services: # ... App\State\UserPasswordHasher: bind: $processor: "@api_platform.doctrine.orm.state.persist_processor" ``` You may have wondered about the following lines in our entity file we created before: ```php operations: [ ... new Post(processor: UserPasswordHasher::class), new Put(processor: UserPasswordHasher::class), new Patch(processor: UserPasswordHasher::class), ... ], ``` This just means we want to run the new created state processor to these specific operations. So we're done. Create a new user, change the password and enjoy! --- # Handling File Upload with Symfony Source: https://api-platform.com/docs/symfony/file-upload/ # Handling File Upload with Symfony As common a problem as it may seem, handling file upload requires a custom implementation in your app. This page will guide you in handling file upload in your API, with the help of [VichUploaderBundle](https://github.com/dustin10/VichUploaderBundle). It is recommended you [read the documentation of VichUploaderBundle](https://github.com/dustin10/VichUploaderBundle/blob/master/docs/index.md) before proceeding. It will help you get a grasp on how the bundle works, and why we use it. > [!NOTE] Uploading files won't work in `PUT` or `PATCH` requests, you must use `POST` method to > upload files. See [the related issue on Symfony](https://github.com/symfony/symfony/issues/9226) > and [the related bug in PHP](https://bugs.php.net/bug.php?id=55815) talking about this behavior. Enable the multipart format globally in order to use it as the input format of your resource: ```yaml api_platform: formats: jsonld: ["application/ld+json"] multipart: ["multipart/form-data"] ``` ## Installing VichUploaderBundle Install the bundle with the help of Composer: ```console composer require vich/uploader-bundle ``` This will create a new configuration file that you will need to slightly change to make it look like this. ```yaml # api/config/packages/vich_uploader.yaml vich_uploader: db_driver: orm metadata: type: attribute mappings: media_object: uri_prefix: /media upload_destination: "%kernel.project_dir%/public/media" # Will rename uploaded files using a uniqueid as a suffix. namer: Vich\UploaderBundle\Naming\SmartUniqueNamer ``` ## Uploading to a Dedicated Resource In this example, we will create a `MediaObject` API resource. We will post files to this resource endpoint, and then link the newly created resource to another resource (in our case: `Book`). ### Configuring the Resource Receiving the Uploaded File The `MediaObject` resource is implemented like this: ```php ['media_object:read']], types: ['https://schema.org/MediaObject'], outputFormats: ['jsonld' => ['application/ld+json']], operations: [ new Get(), new GetCollection(), new Post( inputFormats: ['multipart' => ['multipart/form-data']], openapi: new Model\Operation( requestBody: new Model\RequestBody( content: new \ArrayObject([ 'multipart/form-data' => [ 'schema' => [ 'type' => 'object', 'properties' => [ 'file' => [ 'type' => 'string', 'format' => 'binary' ] ] ] ] ]) ) ) ) ] )] class MediaObject { #[ORM\Id, ORM\Column, ORM\GeneratedValue] private ?int $id = null; #[ApiProperty(types: ['https://schema.org/contentUrl'], writable: false)] #[Groups(['media_object:read'])] public ?string $contentUrl = null; #[Vich\UploadableField(mapping: 'media_object', fileNameProperty: 'filePath')] #[Assert\NotNull] public ?File $file = null; #[ApiProperty(writable: false)] #[ORM\Column(nullable: true)] public ?string $filePath = null; public function getId(): ?int { return $this->id; } } ``` Note: From V3.3 onwards, `'multipart/form-data'` must either be including in the global API-Platform config, either in `formats` or `defaults->inputFormats`, or defined as an `inputFormats` parameter on an operation by operation basis. ### Resolving the File URL Returning the plain file path on the filesystem where the file is stored is not useful for the client, which needs a URL to work with. A [normalizer](../core/serialization.md#normalization) could be used to set the `contentUrl` property: ```php contentUrl = $this->storage->resolveUri($object, 'file'); return $this->normalizer->normalize($object, $format, $context); } public function supportsNormalization($data, ?string $format = null, array $context = []): bool { if (isset($context[self::ALREADY_CALLED])) { return false; } return $data instanceof MediaObject; } public function getSupportedTypes(?string $format): array { return [ MediaObject::class => true, ]; } } ``` ### Handling the Multipart Deserialization By default, Symfony is not able to decode `multipart/form-data`-encoded data. We need to create our own decoder to do it: ```php requestStack->getCurrentRequest(); if (!$request) { return null; } return array_map(static function (string $element) { // Multipart form values will be encoded in JSON. return json_decode($element, true, flags: \JSON_THROW_ON_ERROR); }, $request->request->all()) + $request->files->all(); } public function supportsDecoding(string $format): bool { return self::FORMAT === $format; } } ``` If you're not using `autowiring` and `autoconfiguring`, don't forget to register the service and tag it as `serializer.encoder`. We also need to make sure the field containing the uploaded file is not denormalized: ```php true, ]; } } ``` If you're not using `autowiring` and `autoconfiguring`, don't forget to register the service and tag it as `serializer.normalizer`. ### Making a Request to the `/media_objects` Endpoint Your `/media_objects` endpoint is now ready to receive a `POST` request with a file. This endpoint accepts standard `multipart/form-data`-encoded data, but not JSON data. You will need to format your request accordingly. After posting your data, you will get a response looking like this: ```json { "@type": "https://schema.org/MediaObject", "@id": "/media_objects/", "contentUrl": "" } ``` ### Accessing Your Media Objects Directly You will need to modify your `Caddyfile` to allow the above `contentUrl` to be accessed directly. If you followed the above configuration for the VichUploaderBundle, that will be in `api/public/media`. Add your folder to the list of path matches, e.g. `|^/media/|`: ```patch # Matches requests for HTML documents, for static files and for Next.js files, # except for known API paths and paths with extensions handled by API Platform @pwa expression `( header({'Accept': '*text/html*'}) && !path( - '/docs*', '/graphql*', '/bundles*', '/contexts*', '/_profiler*', '/_wdt*', + '/media*', '/docs*', '/graphql*', '/bundles*', '/contexts*', '/_profiler*', '/_wdt*', '*.json*', '*.html', '*.csv', '*.yml', '*.yaml', '*.xml' ) ) || path('/favicon.ico', '/manifest.json', '/robots.txt', '/_next*', '/sitemap*')` ``` ### Linking a MediaObject Resource to Another Resource We now need to update our `Book` resource, so that we can link a `MediaObject` to serve as the book cover. We first need to edit our Book resource, and add a new property called `image`. ```php " } ``` Voilà! You can now send files to your API, and link them to any other resource in your app. ### Testing To test your upload with `ApiTestCase`, you can write a method as below: ```php request('POST', 'http://localhost:8888/api/media_objects', [ 'headers' => ['Content-Type' => 'multipart/form-data'], 'extra' => [ // If you have additional fields in your MediaObject entity, use the parameters. 'parameters' => [ // 'title' => 'title' ], 'files' => [ 'file' => $file, ], ] ]); $this->assertResponseIsSuccessful(); $this->assertMatchesResourceItemJsonSchema(MediaObject::class); $this->assertJsonContains([ // 'title' => 'My file uploaded', ]); } } ``` ## Uploading to an Existing Resource with its Fields In this example, the file will be included in an existing resource (in our case: `Book`). The file and the resource fields will be posted to the resource endpoint. This example will use a custom `multipart/form-data` decoder to deserialize the resource instead of a custom controller. > [!WARNING] Make sure to encode the fields in JSON before sending them. For instance, you could do something like this: ```js async function uploadBook(file) { const bookMetadata = { title: "API Platform Best Practices", genre: "Programming", }; const formData = new FormData(); for (const [name, value] of Object.entries(bookMetadata)) { formData.append(name, JSON.stringify(value)); } formData.append("file", file); const response = await fetch("https://my-api.com/books", { method: "POST", body: formData, }); const result = await response.json(); return result; } ``` ### Configuring the Existing Resource Receiving the Uploaded File The `Book` resource needs to be modified like this: ```php ['book:read']], denormalizationContext: ['groups' => ['book:write']], types: ['https://schema.org/Book'], operations: [ new GetCollection(), new Post( outputFormats: ['jsonld' => ['application/ld+json']], inputFormats: ['multipart' => ['multipart/form-data']] ) ] )] class Book { // ... #[ApiProperty(types: ['https://schema.org/contentUrl'])] #[Groups(['book:read'])] public ?string $contentUrl = null; #[Vich\UploadableField( mapping: 'media_object', fileNameProperty: 'filePath', )] #[Groups(['book:write'])] public ?File $file = null; #[ORM\Column(nullable: true)] public ?string $filePath = null; // ... } ``` --- # Creating Custom Operations and Symfony Controllers Source: https://api-platform.com/docs/symfony/controllers/ # Creating Custom Operations and Symfony Controllers > [!NOTE] Using custom Symfony controllers with API Platform is **discouraged**. Also, GraphQL is > **not supported**. > [For most use cases, better extension points, working both with REST and GraphQL, are available](../core/design.md). > We recommend to use > [System providers and processors](../core/extending.md#system-providers-and-processors) to extend > API Platform internals. API Platform can leverage the Symfony routing system to register custom operations related to custom controllers. Such custom controllers can be any valid [Symfony controller](https://symfony.com/doc/current/controller.html), including standard Symfony controllers extending the [`Symfony\Bundle\FrameworkBundle\Controller\AbstractController`](https://symfony.com/doc/current/controller.html#the-base-controller-class-services) helper class. To enable this feature use `use_symfony_listeners: true` in your `api_platform` configuration file: ```yaml api_platform: title: "My Dummy API" description: | This is a test API. Made with love use_symfony_listeners: true ``` However, API Platform recommends to use **action classes** instead of typical Symfony controllers. Internally, API Platform implements the [Action-Domain-Responder](https://github.com/pmjones/adr) pattern (ADR), a web-specific refinement of [MVC](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller). The API Platform Symfony variant also eases the implementation of the ADR pattern: it automatically registers action classes stored in `api/src/Controller` as autowired services. Thanks to the [autowiring](https://symfony.com/doc/current/service_container/autowiring.html) feature of the Symfony Dependency Injection container, services required by an action can be type-hinted in its constructor, it will be automatically instantiated and injected, without having to declare it explicitly. In the following examples, the built-in `GET` operation is registered as well as a custom operation called `post_publication`. By default, API Platform uses the first `Get` operation defined to generate the IRI of an item and the first `GetCollection` operation to generate the IRI of a collection. If your resource does not have any `Get` operation, API Platform automatically adds an operation to help generating this IRI. If your resource has any identifier, this operation will look like `/books/{id}`. But if your resource doesn't have any identifier, API Platform will use the Skolem format `/.well-known/genid/{id}`. Those routes are not exposed from any documentation (for instance OpenAPI), but are anyway declared on the Symfony routing and always return a HTTP 404. If you create a custom operation, you will probably want to properly document it. See the [OpenAPI](../core/openapi.md) part of the documentation to do so. First, let's create your custom operation: ```php bookPublishingHandler->handle($book); return $book; } } ``` This custom operation behaves exactly like the built-in operation: it returns a JSON-LD document corresponding to the ID passed in the URL. Here we consider that [autowiring](https://symfony.com/doc/current/service_container/autowiring.html) is enabled for controller classes (the default when using the API Platform Symfony variant). This action will be automatically registered as a service (the service name is the same as the class name: `App\Controller\CreateBookPublication`). API Platform automatically retrieves the appropriate PHP entity using the state provider then deserializes user data in it, and for `POST`, `PUT` and `PATCH` requests updates the entity with state provided by the user. The entity is retrieved in the `__invoke` method thanks to a dedicated argument resolver. When using `GET`, the `__invoke()` method parameter will receive the identifier and should be called the same as the resource identifier. So for the path `/user/{uuid}/bookmarks`, you must use `__invoke(string $uuid)`. Services (`$bookPublishingHandler` here) are automatically injected thanks to the autowiring feature. You can type-hint any service you need and it will be autowired too. The `__invoke` method of the action is called when the matching route is hit. It can return either an instance of `Symfony\Component\HttpFoundation\Response` (that will be displayed to the client immediately by the Symfony kernel) or, like in this example, an instance of an entity mapped as a resource (or a collection of instances for collection operations). In this case, the entity will pass through [all built-in event listeners](../core/events.md#built-in-event-listeners) of API Platform. It will be automatically validated, persisted and serialized in JSON-LD. Then the Symfony kernel will send the resulting document to the client. The routing has not been configured yet because we will add it at the resource configuration level: ```php ``` It is mandatory to set the `method`, `uriTemplate` and `controller` attributes. They allow API Platform to configure the routing path and the associated controller respectively. ## Using the PlaceholderAction Complex use cases may lead you to create multiple custom operations. In such a case, you will probably create the same amount of custom controllers while you may not need to perform custom logic inside. To avoid that, API Platform provides the `ApiPlatform\Action\PlaceholderAction` which behaves the same when using the [built-in operations](../core/operations.md#operations). You just need to set the `controller` attribute with this class. Here, the previous example updated: ```php // api/src/Entity/Book.php namespace App\Entity; use ApiPlatform\Action\PlaceholderAction; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\Post; #[ApiResource(operations: [ new Get(), new Post( name: 'publication', uriTemplate: '/books/{id}/publication', controller: PlaceholderAction::class ) ])] class Book { // ... } ``` ```yaml # api/config/api_platform/resources.yaml resources: App\Entity\Book: operations: ApiPlatform\Metadata\Get: ~ post_publication: class: ApiPlatform\Metadata\Post method: POST uriTemplate: /books/{id}/publication controller: ApiPlatform\Action\PlaceholderAction ``` ```xml ``` ## Using Serialization Groups You may want different serialization groups for your custom operations. Just configure the proper `normalizationContext` and/or `denormalizationContext` in your operation: ```php ['publication']], ) ])] class Book { // ... #[Groups(['publication'])] public $isbn; // ... } ``` ```yaml # api/config/api_platform/resources.yaml resources: App\Entity\Book: operations: ApiPlatform\Metadata\Get: ~ post_publication: class: ApiPlatform\Metadata\Get uriTemplate: /books/{id}/publication controller: App\Controller\CreateBookPublication normalizationContext: groups: ["publication"] ``` ```xml publication ``` ## Retrieving the Entity If you want to bypass the automatic retrieval of the entity in your custom operation, you can set `read: false` in the operation attribute: ```php ``` This way, it will skip the `ReadListener`. You can do the same for some other built-in listeners. See [Built-in Event Listeners](../core/events.md#built-in-event-listeners) for more information. In your custom controller, the `__invoke()` method parameter should be called the same as the entity identifier. So for the path `/user/{uuid}/bookmarks`, you must use `__invoke(string $uuid)`. ## Alternative Method There is another way to create a custom operation. However, we do not encourage its use. Indeed, this one disperses the configuration at the same time in the routing and the resource configuration. The `post_publication` operation references the Symfony route named `book_post_publication`. Since version 2.3, you can also use the route name as operation name by convention, as shown in the following example for `book_post_discontinuation` when neither `method` nor `routeName` attributes are specified. First, let's create your resource configuration: ```php ``` API Platform will automatically map this `post_publication` operation to the route `book_post_publication`. Let's create a custom action and its related route using attributes: ```php Book::class, '_api_operation_name' => '_api_/books/{id}/publication_post', ], )] public function __invoke(Book $book): Book { $this->bookPublishingHandler->handle($book); return $book; } } ``` It is mandatory to set `_api_resource_class` and `_api_operation_name` in the parameters of the route (`defaults` key). It allows API Platform to work with the Symfony routing system. Alternatively, you can also use a traditional Symfony controller and YAML or XML route declarations. The following example does the same thing as the previous example: ```php handle($book); } } ``` ```yaml # api/config/routes.yaml book_post_publication: path: /books/{id}/publication methods: ["POST"] defaults: _controller: App\Controller\BookController::createPublication _api_resource_class: App\Entity\Book _api_operation_name: post_publication ``` --- # NelmioApiDocBundle Integration with Symfony Source: https://api-platform.com/docs/symfony/nelmio-api-doc/ # NelmioApiDocBundle Integration with Symfony > [!WARNING] For new projects, prefer using the built-in Swagger support and/or NelmioApiDoc 3. NelmioApiDoc provides an alternative to [the native Swagger/Open API support](../core/openapi.md) provided by API Platform. As NelmioApiDocBundle 3+ has built-in support for API Platform, this documentation is only relevant for people using NelmioApiDocBundle between version 2.9 and 3.0. ![Screenshot of API Platform integrated with NelmioApiDocBundle](images/NelmioApiDocBundle.png) [NelmioApiDocBundle](https://github.com/nelmio/NelmioApiDocBundle) is supported by API Platform since version 2.9. To enable the NelmioApiDoc integration, copy the following configuration: ```yaml # api/config/packages/api_platform.yaml api_platform: # ... enable_nelmio_api_doc: true nelmio_api_doc: sandbox: accept_type: "application/json" body_format: formats: ["json"] default_format: "json" request_format: formats: json: "application/json" ``` Please note that NelmioApiDocBundle has a sandbox limitation where you cannot pass a JSON array as parameter, so you cannot use it to deserialize nested objects. --- # Migrate From FOSRestBundle with Symfony Source: https://api-platform.com/docs/symfony/migrate-from-fosrestbundle/ # Migrate From FOSRestBundle with Symfony [FOSRestBundle](https://github.com/FriendsOfSymfony/FOSRestBundle) is a popular bundle to rapidly develop RESTful APIs with Symfony. This page provides a guide to help developers migrate from FOSRestBundle to API Platform. > [!IMPORTANT] > Since [2021](https://x.com/lsmith/status/1440216817876627459), the creators of FOSRestBundle have > recommended transitioning to **API Platform** as the preferred solution **for building modern > APIs**. ## Features Comparison The table below provides a list of the main features you can find in FOSRestBundle 3.1, and their equivalents in API Platform. ### Make CRUD endpoints #### In FOSRestBundle (CRUD endpoints) Create a controller extending the `AbstractFOSRestController` abstract class, make your magic manually in your methods, and return responses through the `handleView()` provided by FOSRest's `ControllerTrait`. See [The view layer](https://github.com/FriendsOfSymfony/FOSRestBundle/blob/3.x/Resources/doc/2-the-view-layer.rst). #### In API Platform (CRUD endpoints) Add the `ApiResource` attribute to your entities, and enable the operations you desire inside. By default, every operation is activated. See [Operations](../core/operations.md). ### Make custom controllers #### In FOSRestBundle (Custom controllers) Same as above. #### In API Platform (Custom controllers) Even though this is not recommended, API Platform allows you to [create custom controllers](controllers.md) and declare them in your entity's `ApiResource` attribute. You can use them as you migrate from FOSRestBundle, but you should consider [switching to Symfony Messenger](messenger.md) as it will give you more benefits, such as compatibility with both REST and GraphQL and better performances of your API on big tasks. See [General Design Considerations](../core/design.md). ### Routing system (with native documentation support) #### In FOSRestBundle (Routing) Annotate your controllers with FOSRest's route annotations that are the most suitable to your needs. See [Full default annotations](https://github.com/FriendsOfSymfony/FOSRestBundle/blob/3.x/Resources/doc/annotations-reference.rst). #### In API Platform (Routing) Use the `ApiResource` attribute to activate the HTTP methods you need for your entity. By default, all the methods are enabled. See [Operations](../core/operations.md). ### Hook into the handling of the requests #### In FOSRestBundle (Request handling) Listen to FOSRest's events to modify the requests before they come into your controllers and the responses after they come out of them. See [Listener support](https://github.com/FriendsOfSymfony/FOSRestBundle/blob/3.x/Resources/doc/3-listener-support.rst). #### In API Platform (Request handling) API Platform provides a lot of ways to customize the behavior of your API, depending on what you exactly want to do. See [Extending API Platform](../core/extending.md) for more details. ### Customize the formats of the requests and the responses #### In FOSRestBundle (Formats) Only the request body's format can be customized. Use body listeners to use either FOSRest's own decoders or your own ones. FOSRestBundle provides native support for JSON and XML. See [Body Listener](https://github.com/FriendsOfSymfony/FOSRestBundle/blob/3.x/Resources/doc/body_listener.rst). #### In API Platform (Formats) Both the request and the response body's format can be customized. You can configure the formats of the API either globally or in specific resources or operations. API Platform provides native support for multiple formats including JSON, XML, CSV, YAML, etc. See [Content negotiation](../core/content-negotiation.md). ### Name conversion #### In FOSRestBundle (Name conversion) Only request bodies can be converted before entering into your controller. FOSRest provides two native normalizers for converting the names of your JSON keys to camelCase. You can create your own ones by implementing the `ArrayNormalizerInterface`. See [Body Listeners](https://github.com/FriendsOfSymfony/FOSRestBundle/blob/3.x/Resources/doc/body_listener.rst). #### In API Platform (Name conversion) Both request and response bodies can be converted. API Platform uses [name converters](https://symfony.com/doc/current/components/serializer.html#component-serializer-converting-property-names-when-serializing-and-deserializing) included in the Serializer component of Symfony. You can create your own by implementing the `NameConverterInterface` provided by Symfony. See [_Name Conversion_ in The Serialization Process](../core/serialization.md#name-conversion-for-symfony). ### Handle errors #### In FOSRestBundle (Error handling) Map the exceptions to HTTP statuses in the `fos_rest.exception` parameter. See [ExceptionController support](https://github.com/FriendsOfSymfony/FOSRestBundle/blob/3.x/Resources/doc/4-exception-controller-support.rst). #### In API Platform (Error handling) Map the exceptions to HTTP statuses in the `api_platform.exception_to_status` parameter. See [Errors Handling](../core/errors.md). ### Security #### In FOSRestBundle (Security) Use [Symfony's Security component](https://symfony.com/doc/current/security) to control your API access. #### In API Platform (Security) Use the `security` attribute in the `ApiResource` and `ApiProperty` attributes. It is an [Expression language](https://symfony.com/doc/current/components/expression_language.md) string describing who can access your resources or who can see the properties of your resources. By default, everything is accessible without authentication. Note you can also use the `security.yml` file if you only need to limit access to specific roles. See [Security](../core/security.md). ### API versioning #### In FOSRestBundle (API versioning) FOSRestBundle provides a way to provide versions to your APIs in a way users have to specify which one they want to use. See [API versioning](https://github.com/FriendsOfSymfony/FOSRestBundle/blob/3.x/Resources/doc/versioning.rst). #### In API Platform (API versioning) API Platform has no native support for API versioning, but instead provides an approach consisting of deprecating resources when needed. It allows a smoother upgrade for clients, as they need to change their code only when it is necessary. See [Deprecating Resources and Properties](../core/deprecations.md). --- # FOSUserBundle Integration with Symfony Source: https://api-platform.com/docs/symfony/fosuser-bundle/ # FOSUserBundle Integration with Symfony > [!WARNING] The use of FOSUserBundle is no longer recommended for better flexibility and security. > It is advised to switch to the > [Doctrine entity user provider](https://symfony.com/doc/current/security/user_provider.html#entity-user-provider) > (recommended) or consider > [creating a custom user provider](https://symfony.com/doc/current/security/user_provider.html#creating-a-custom-user-provider). ## Installing the Bundle The installation procedure of the FOSUserBundle is described [in the FOSUserBundle documentation](https://github.com/FriendsOfSymfony/FOSUserBundle/blob/master/docs/index.rst). You can: - Skip [step 3 (Create your User class)](https://github.com/FriendsOfSymfony/FOSUserBundle/blob/master/docs/index.rst#step-3-create-your-user-class) and use the class provided in the next paragraph to set up serialization groups the correct way - Skip [step 4 (Configure your application's security.yml)](https://github.com/FriendsOfSymfony/FOSUserBundle/blob/master/docs/index.rst#step-4-configure-your-applications-securityyml) if you are planning to [use a JWT-based authentication using `LexikJWTAuthenticationBundle`](../core/jwt.md) If you are using the API Platform Standard Edition, you will need to enable the form services in the symfony framework configuration options: ```yaml # api/config/packages/framework.yaml framework: form: { enabled: true } ``` ## Creating a `User` Entity with Serialization Groups Here's an example of declaration of a [Doctrine ORM User class](https://github.com/FriendsOfSymfony/FOSUserBundle/blob/master/docs/index.rst#a-doctrine-orm-user-class). There's also an example for a [Doctrine MongoDB ODM](https://github.com/FriendsOfSymfony/FOSUserBundle/blob/master/docs/index.rst#b-mongodb-user-class). You need to use serialization groups to hide some properties like `plainPassword` (only in read) and `password`. The properties shown are handled with [`normalizationContext`](../core/serialization.md#normalization), while the properties you can modify are handled with [`denormalizationContext`](../core/serialization.md#denormalization). Create your User entity with serialization groups: ```php ['user']], denormalizationContext: ['groups' => ['user', 'user:write']], )] class User extends BaseUser { #[ORM\Id, ORM\Column, ORM\GeneratedValue] protected ?int $id = null; #[Groups("user")] protected string $email; #[ORM\Column(nullable: true)] #[Groups("user")] protected string $fullname; #[Groups("user:write")] protected string $plainPassword; #[Groups("user")] protected string $username; public function setFullname(?string $fullname): void { $this->fullname = $fullname; } public function getFullname(): ?string { return $this->fullname; } public function isUser(?UserInterface $user = null): bool { return $user instanceof self && $user->id === $this->id; } } ``` --- # API Platform for Laravel Projects Source: https://api-platform.com/docs/laravel/ # API Platform for Laravel Projects API Platform is **the easiest way** to create **state-of-the-art** web APIs using Laravel! ![Basic REST API](images/basic-rest.png) With API Platform, you can: - [expose your Eloquent](#exposing-a-model) models in minutes as: - a REST API implementing the industry-leading standards, formats and best practices: [JSON-LD](https://en.wikipedia.org/wiki/JSON-LD)/[RDF](https://en.wikipedia.org/wiki/Resource_Description_Framework), [JSON:API](https://jsonapi.org), [HAL](https://stateless.group/hal_specification.html), and many RFCs... - a [GraphQL](#enabling-graphql) API - or both at the same time, with the same code! - automatically expose an [OpenAPI](https://www.openapis.org) specification (formerly Swagger), dynamically generated from your Eloquent models and always up to date - automatically expose nice UIs and playgrounds to develop using your API ([Swagger UI](https://swagger.io/tools/swagger-ui/) and [GraphiQL](https://github.com/graphql/graphiql)) - automatically paginate your collections - add validation logic using Laravel [Form Request Validation](#write-operations-authorization-and-validation) - add authorization logic using [gates and policies](#authorization) ([compatible with Sanctum, Passport, Socialite...](#authentication)) - add [filtering logic](#adding-filters) - push changed data to the clients in real-time using Laravel Broadcast and [Mercure](https://mercure.rocks) (a popular WebSockets alternative, created by Kévin Dunglas, the original author of API Platform) and receive them using Laravel Echo--> - benefits from the API Platform JavaScript tools: [admin](../admin/index.md) and [create client](../create-client/index.md) (supports Next/React, Nuxt/Vue.js, Quasar, Vuetify and more!) - benefits from native HTTP cache (with automatic invalidation) - boost your app with [Octane](https://laravel.com/docs/octane) and [FrankenPHP](https://frankenphp.dev) (the default Octane engine, also created by Kévin) - [decouple your API from your models](../core/state-providers.md) and implement patterns such as CQRS - test your API using convenient ad-hoc assertions that work with Pest and PHPUnit Let's discover how to use API Platform with Laravel! ## Scaffolding a New Project (Recommended) The fastest way to start a new project is the **`api-platform` installer**, a command-line tool that creates a ready-to-use Laravel project with API Platform pre-installed. Download the latest release for your platform from the [Releases page](https://github.com/api-platform/api-platform/releases/latest) and move the binary somewhere on your `$PATH`: ```console curl -L https://github.com/api-platform/api-platform/releases/latest/download/api-platform-linux-x86_64 -o /usr/local/bin/api-platform chmod +x /usr/local/bin/api-platform ``` Alternatively, if you already have PHP and [Composer](https://getcomposer.org/) installed, install it globally with `composer global require api-platform/installer`. Then scaffold the project: ```console api-platform my-api-platform-laravel-app --framework=laravel ``` The installer creates the Laravel application, requires `api-platform/laravel`, and runs `php artisan api-platform:install` for you. Once it finishes, start the server with `php artisan serve` and open `http://127.0.0.1:8000/api/`. If you prefer to install API Platform manually, or you want to add it to an existing Laravel project, follow the sections below instead. ## Installing Laravel API Platform can be installed easily on new and existing Laravel projects. If you already have an existing project, skip directly to the next section. API Platform 4.2 supports **Laravel 11 and Laravel 12** (`laravel/framework ^11.0 || ^12.0`). For Laravel 13 support, use API Platform 4.3. If you don't have an existing Laravel project, [create one](https://laravel.com/docs/installation). All Laravel installation methods are supported. For instance, you can use Composer: ```console composer create-project laravel/laravel my-api-platform-laravel-app cd my-api-platform-laravel-app ``` ## Installing API Platform In your Laravel project, install the API Platform integration for Laravel: ```console composer require api-platform/laravel ``` After installing API Platform, publish its assets and config: ```console php artisan api-platform:install ``` If it's not already done, start the built-in web server: ```console php artisan serve ``` Open `http://127.0.0.1:8000/api/`, your API is already active and documented... but empty! ![Empty docs](images/empty-docs.png) ## Publishing the Config File and Assets After installing API Platform, you can publish its assets and config using the `api-platform:install` Artisan command. ```console php artisan api-platform:install ``` ## Creating an Eloquent Model To discover how API Platform framework works, we will create an API to manage a bookshop. Let's start by creating a `Book` model: ```console php artisan make:model Book ``` By default, Laravel uses SQLite. You can open the `database/database.sqlite` file with your preferred SQLite client (PhpStorm works like a charm), create a table named `books`, and add some columns, Eloquent and API Platform will detect these columns automatically. But there is a better alternative: using a migration class. ### Creating a Migration First, create a migration class for the `books` table: ```console php artisan make:migration create_books_table ``` Open the generated migration class (`database/migrations/_create_books_table.php`) and add some columns: ```patch public function up(): void { Schema::create('books', function (Blueprint $table) { $table->id(); + $table->string('isbn')->nullable(); + $table->string('title'); + $table->text('description'); + $table->string('author'); + $table->date('publication_date')->nullable(); $table->timestamps(); }); } ``` Finally, execute the migration: ```console php artisan migrate ``` The table and columns have been created for you! ## Exposing A Model Open `app/Models/Book.php` that we generated in the previous step and mark the class it contains with the `#[ApiResource]` attribute: ```patch namespace App\Models; +use ApiPlatform\Metadata\ApiResource; use Illuminate\Database\Eloquent\Model; +#[ApiResource] class Book extends Model { } ``` Open `http://127.0.0.1:8000/api/`, tadam, your API is ready and **entirely functional** 🎉: ![Basic REST API](images/basic-rest.png) You can play with your API with the sandbox provided by SwaggerUI. Under the hood, API Platform: 1. Registered the standard REST routes in Laravel's router and a controller that implements a state-of-the-art, fully-featured, and secure API endpoint using the services provided by the [API Platform Core library](../core/index.md) 2. Used its built-in Eloquent [state provider](../core/state-providers.md) to introspect the database and gather metadata about all columns to expose through the API 3. Generated machine-readable documentations of the API in the [OpenAPI (formerly known as Swagger)](../core/openapi.md) (available at `http://127.0.0.1:8000/api/docs.json`) and [JSON-LD](https://json-ld.org)/[Hydra](https://www.hydra-cg.com) formats using this metadata 4. Generated nice human-readable documentation and a sandbox for the API with [SwaggerUI](https://swagger.io/tools/swagger-ui/) (Redoc is also available out-of-the-box) Imagine doing it all again, properly, by hand? How much time have you saved? Weeks, months? And you've seen nothing yet! ## Playing With The API ![View of an item](images/books-collection.png) If you access any API URL with the `.html` extension appended, API Platform displays the corresponding API request in the UI. Try it yourself by browsing to `http://127.0.0.1:8000/api/books.html`. If no extension is present, API Platform will use the `Accept` header to select the format to use. So, if you want to access the raw data, you have two alternatives: - Add the correct `Accept` header (or don't set any `Accept` header at all if you don't care about security) - preferred when writing API clients - Add the format you want as the extension of the resource - for debug purposes only For instance, go to `http://127.0.0.1:8000/api/books.jsonld` to retrieve the list of `Book` resources in JSON-LD. > [!NOTE] Documentation for Eloquent "API resources" encourages using the JSON:API community format. > While we recommend preferring JSON-LD when possible, JSON:API is also supported by API Platform, > read the [Content Negotiation](#content-negotiation) section to learn how to enable it. Of course, you can also use your favorite HTTP client to query the API. We are fond of [Hoppscotch](https://hoppscotch.com), a free and open source API client with good support of API Platform. ## Using Data Transfer Objects and Hooking Custom Logic While exposing directly the data in the database is convenient for Rapid Application Development, using different classes for the internal data and the public data is a good practice for more complex projects. As explained in our [general design considerations](../core/design.md), API Platform allows us to use the data source of our choice using a [provider](../core/state-providers.md) and Data Transfer Objects (DTOs) are first-class citizens! Let's create our DTO: ```php [ app_path('ApiResource'), app_path('Models'), ], // ... ]; ``` Then we can create the logic to retrieve the state of our `Book` DTO: ```php id, title: $book->title); } } ``` Apply the provider to your operation: ```php [ 'jsonld' => ['application/ld+json'], 'jsonapi' => ['application/vnd.api+json'], 'csv' => ['text/csv'], ], 'patch_formats' => [ 'json' => ['application/merge-patch+json'], ], 'docs_formats' => [ 'jsonld' => ['application/ld+json'], 'jsonapi' => ['application/vnd.api+json'], 'jsonopenapi' => ['application/vnd.openapi+json'], 'html' => ['text/html'], ], // ... ]; ``` ## Enabling GraphQL ![GraphQL in Laravel](images/graphql.png) By default, only the REST endpoints are enabled, but API Platform also [supports GraphQL](../core/graphql.md)! Install the GraphQL support package: ```console composer require api-platform/graphql:^4 ``` Then, enable GraphQL in `config/api-platform.php`: ```patch 'graphql' => [ - 'enabled' => false, + 'enabled' => true, ``` Then open `http://127.0.0.1:8000/api/graphql` and replace the default GraphQL query example with: ```graphql { books(first: 3) { edges { node { title author publicationDate } } } } ``` You now have a REST and a GraphQL API with the same code! As you can see, a nice UI ([GraphiQL](https://github.com/graphql/graphiql)) is also available. The documentation is automatically generated using the GraphQL introspection endpoint. ## Hiding Fields API Platform allows to control which fields will be publicly exposed by the API using [the same syntax as Eloquent serialization](https://laravel.com/docs/eloquent-serialization#hiding-attributes-from-json): ```php namespace App\Models; use ApiPlatform\Metadata\ApiResource; use Illuminate\Database\Eloquent\Model; #[ApiResource] class Book extends Model { /** * The attributes that should be hidden (deny list). * * @var array */ protected $hidden = ['isbn']; } ``` ```php namespace App\Models; use ApiPlatform\Metadata\ApiResource; use Illuminate\Database\Eloquent\Model; #[ApiResource] class Book extends Model { /** * The attributes that should be visible (allow list). * * @var array */ protected $visible = ['title', 'description']; } ``` ## Relations and Nested Resources Let's replace our author column with a relation to a new `author` table: ```patch public function up(): void { Schema::create('books', function (Blueprint $table) { $table->id(); $table->string('description'); - $table->string('author'); + $table->integer('author_id')->unsigned(); + $table->foreign('author_id')->references('id')->on('authors'); $table->timestamps(); }); + Schema::create('authors', function (Blueprint $table): void { + $table->id(); + $table->string('name'); + $table->timestamps(); + }); } ``` By doing so, API Platform will automatically handle links to that relation using your preferred format (JSON:API, JSON-LD, etc) and when we request a Book we obtain: ```json { "@context": "/api/contexts/Book", "@id": "/api/books/1", "@type": "Book", "name": "Miss Nikki Senger V", "isbn": "9784291624633", "publicationDate": "1971-09-04", "author": "/api/authors/1" } ``` To create a Book related to an author, you should use IRIs to reference the relation: ```http PATCH /api/books/1 Content-Type: application/merge-patch+json { "author": "/api/authors/2" } ``` There's a powerful mechanism inside API Platform to create routes using relation (e.g.: `/api/authors/2/books`), read more about [subresources here](../core/subresources.md). If you need to embed data, you can use [serialization groups](/core/serialization.md). Note that when you apply groups on Eloquent models they don't have properties therefore you need to specify groups using `#[ApiProperty(property: 'title')]`. Here's an example to embed the `author`: ```php namespace App\Models; use ApiPlatform\Metadata\ApiResource; use Illuminate\Database\Eloquent\Model; #[ApiResource(normalizationContext: ['groups' => ['book:read']])] #[ApiProperty(serialize: new Groups(['book:read']), property: 'title')] #[ApiProperty(serialize: new Groups(['book:read']), property: 'description')] #[ApiProperty(serialize: new Groups(['book:read']), property: 'author')] class Book extends Model { public function author(): BelongsTo { return $this->belongsTo(Author::class); } } ``` If you need a group on every properties use the `Group` attribute on the class (note that we use the same group as specified on the Book's normalizationContext): ```php namespace App\Models; use ApiPlatform\Metadata\ApiResource; use Illuminate\Database\Eloquent\Model; #[ApiResource)] #[Groups(['book:read'])] class Author extends Model { } ``` You'll see: ```json { "@context": "/api/contexts/Book", "@id": "/api/books/1", "@type": "Book", "name": "Miss Nikki Senger V", "isbn": "9784291624633", "publicationDate": "1971-09-04", "author": { "@id": "/api/author/1", "name": "Homer" } } ``` ## Paginating Data A must have feature for APIs is pagination. Without pagination, collection responses quickly become huge and slow, and can even lead to crashes (Out of Memory, timeouts...). Fortunately, the Eloquent state provider provided by API Platform automatically paginates data! To test this feature, let's inject some fake data into the database. ### Seeding the Database Instead of manually creating the data you need to test your API, it can be convenient to automatically insert fake data in the database. Laravel provides a convenient way to do that: [Eloquent Factories](https://laravel.com/docs/eloquent-factories). First, create a factory class for our `Book` model: ```console php artisan make:factory BookFactory ``` Then, edit `database/factories/BookFactory.php` to specify which generator to use for each property of the model: ```patch namespace Database\Factories; use Illuminate\Database\Eloquent\Factories\Factory; /** * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Book> */ class BookFactory extends Factory { /** * Define the model's default state. * * @return array */ public function definition(): array { return [ - // + 'title' => mb_convert_case(fake()->words(4, true), MB_CASE_TITLE), + 'isbn' => fake()->isbn13(), + 'description' => fake()->text(), + 'author' => fake()->name(), + 'publication_date' => fake()->date(), ]; } } ``` Then, update the `app/Models/Book.php` to hint Eloquent that it has an associated factory: ```patch namespace App\Models; use ApiPlatform\Metadata\ApiResource; +use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; #[ApiResource] class Book extends Model { + use HasFactory; } ``` Reference this factory in the seeder (`database/seeder/DatabaseSeeder.php`): ```patch namespace Database\Seeders; +use App\Models\Book; use App\Models\User; // use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Database\Seeder; class DatabaseSeeder extends Seeder { /** * Seed the application's database. */ public function run(): void { // User::factory(10)->create(); User::factory()->create([ 'name' => 'Test User', 'email' => 'test@example.com', ]); + Book::factory(100)->create(); } } ``` Finally, seed the database: ```console php artisan db:seed ``` > [!NOTE] The `fake()` helper provided by Laravel lets you generate different types of random data > for testing and seeding purposes. It uses [the Faker library](https://fakerphp.org), which has > been created by François Zaninotto. François is also a member of the API Platform Core Team. He > maintains [API Platform Admin](../admin/index.md), a tool built on top of his popular > [React-Admin](https://marmelab.com/react-admin/) library that makes creating admin interfaces > consuming your API data super easy. What a small world! ### Configuring The Pagination Send a `GET` request on `http://127.0.0.1:8000/api/books`. By default, API Platform paginates collections by slices of 30 items. This is configurable, to change to 10 items per page, change `app/Models/Book.php` like this: ```patch namespace App\Models; use ApiPlatform\Metadata\ApiResource; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; -#[ApiResource] +#[ApiResource( + paginationItemsPerPage: 10, +)] class Book extends Model { use HasFactory; } ``` Read the [pagination documentation](../core/pagination.md) to learn all you can do! ## Customizing the API API Platform has a ton of knobs and gives you full control over what is exposed. For instance, here is how to make your API read-only by enabling only the `GET` [operations](../core/operations.md): ```patch // app/Models/Book.php namespace App\Models; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; use Illuminate\Database\Eloquent\Model; -#[ApiResource] #[ApiResource( paginationItemsPerPage: 10, + operations: [ + new GetCollection(), + new Get(), + ], )] class Book extends Model { } ``` ![Read-only](images/read-only.png) We'll use configuration options provided by API Platform all along this getting started guide, but there are tons of features! A good way to discover them is to inspect the properties of the `ApiResource` and `ApiProperty` attributes and, of course, to [read the core library documentation](../core/index.md). You can change the default configuration (for instance, which operations are enabled by default) in the config (`config/api-platform.php`). For the rest of this tutorial, we'll assume that at least all default operations are enabled (you can also enable `PUT` if you want to support upsert operations). ## Adding Filters API Platform provides an easy shortcut to some [useful filters](./filters.md), for starters you can enable a `PartialSearchFilter` the title property: ```patch // app/Models/Book.php namespace App\Models; use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\QueryParameter; +use ApiPlatform\Laravel\Eloquent\Filter\PartialSearchFilter; use Illuminate\Database\Eloquent\Model; #[ApiResource] +#[QueryParameter(key: 'title', filter: PartialSearchFilter::class)] class Book extends Model { } ``` ![Title filter](images/title-filter.png) It's also possible to enable filters on every exposed property: ```patch // app/Models/Book.php namespace App\Models; use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\QueryParameter; +use ApiPlatform\Laravel\Eloquent\Filter\PartialSearchFilter; +use ApiPlatform\Laravel\Eloquent\Filter\OrderFilter; use Illuminate\Database\Eloquent\Model; #[ApiResource] +#[QueryParameter(key: ':property', filter: PartialSearchFilter::class)] +#[QueryParameter(key: 'sort[:property]', filter: OrderFilter::class)] class Book extends Model { } ``` The `OrderFilter` allows us to sort the collection. The `:property` placeholder gives the ability to create a parameter for each exposed property. These filters will be automatically documented: ![Filters documentation](images/filters-documentation.png) On top of that, some validation rules are automatically added based on the given JSON Schema. You can customize the set of rules inside the `constraints` option of a `QueryParameter`. API Platform comes with several filters dedicated to Laravel, [check them out](filters.md)! ## Authentication API Platform hooks into the native [Laravel authentication mechanism](https://laravel.com/docs/authentication). It also natively supports: - [Laravel Sanctum](https://laravel.com/docs/sanctum), an authentication system for SPAs (single page applications), mobile applications, and simple, token-based APIs - [Laravel Passport](https://laravel.com/docs/passport), a full OAuth 2 server - [Laravel Socialite](https://laravel.com/docs/socialite), OAuth providers including Facebook, X, LinkedIn, Google, GitHub, GitLab, Bitbucket, and Slack Follow the official instructions for the tool(s) you want to use. ### Login With Swagger UI In Swagger UI, you can authenticate your requests using the `Authorize` button in the top right corner. To use it, you need to add some configuration in the `config/api-platform.php` file. Here is an example of how to configure API key authentication: ```php // config/api-platform.php 'swagger_ui' => [ 'enabled' => true, 'apiKeys' => [ 'api' => [ 'type' => 'header', 'name' => 'X-API-Key' ] ] ] ``` Or if you are using Laravel Passport (or any other OAuth server): ```php // config/api-platform.php 'swagger_ui' => [ 'enabled' => true, 'oauth' => [ 'enabled' => true, 'type' => 'oauth2', 'flow' => 'authorizationCode', 'tokenUrl' => '', 'authorizationUrl' =>'', 'refreshUrl' => '', 'scopes' => ['scope' => 'Description of the scope'], 'pkce' => true, ] ] ``` A combination of both is also possible. For more information, you can also check the [Swagger UI documentation](https://swagger.io/docs/specification/authentication/). ### Middlewares It's sometimes convenient to enforce the use of middleware for all API routes. In the following example, we enable the Laravel Sanctum middleware for all API routes: ```php // config/api-platform.php return [ // .. 'defaults' => [ 'middleware' => 'auth:sanctum', ], ]; ``` ## Write Operations Authorization and Validation ![Form Request](images/form-request.png) To authorize write operations (`POST`, `PATCH`, `PUT`) and validate user input, you may generate a [Form Request class](https://laravel.com/docs/validation#creating-form-requests): ```console php artisan make:request BookFormRequest ``` Then, add validation rules to the generated class (`app/Http/Requests/BookFormRequest.php` in our example): ```patch namespace App\Http\Requests; use Illuminate\Foundation\Http\FormRequest; class BookFormRequest extends FormRequest { /** * Determine if the user is authorized to make this request. */ public function authorize(): bool { - return false; + return user()->isAdmin(); } /** * Get the validation rules that apply to the request. * * @return array|string> */ public function rules(): array { return [ - // + 'title' => 'required|unique:books|max:255', + 'description' => 'required', + 'author' => 'required|max:100', ]; } } ``` In this example, we only authorize admin users to do write operations, and we add some validation rules. If the standard Laravel conventions are followed, the Form Request class is autodetected and used automatically. Otherwise, reference it explicitly in the `rules` parameter: ```patch // app/Models/Book.php namespace App\Models; +use App\Http\Requests\BookFormRequest; use Illuminate\Database\Eloquent\Model; -#[ApiResource] +#[ApiResource( + rules: BookFormRequest::class, +)] class Book extends Model { } ``` API Platform will transform any exception in the [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807) (Problem Details for HTTP APIs) format. You can create your own `Error` resource following [this guide](https://api-platform.com/docs/guides/error-resource/). Read the detailed documentation about [Laravel data validation in API Platform](validation.md). ### Authorization To protect an operation and ensure that only authorized users can access it, start by creating a Laravel [policy](https://laravel.com/docs/authorization#creating-policies): ```console php artisan make:policy BookPolicy --model=Book ``` Laravel will automatically detect your new policy and use it when manipulating a Book. Read the detailed documentation about using [Laravel gates and policies with API Platform](security.md). ## Using the JavaScript Tools ### The Admin Wouldn't it be nice to have an administration backend to manage the data exposed by your API? Checkout [API Platform Admin](../admin/index.md)! ![The admin](../symfony/images/api-platform-2.6-admin.png) This [Material Design](https://material.io/guidelines/) admin is a Single Page App built with [React Admin](https://marmelab.com/react-admin/). It is powerful and fully customizable. It leverages the Hydra documentation exposed by the API component to build itself. It's 100% dynamic - **no code generation occurs**. ### SPA/PWA Scaffolding ![The Next.js Progressive Web App](../symfony/images/api-platform-2.6-pwa-react.png) API Platform also has an awesome [client generator](../create-client/index.md) able to scaffold fully working [Next.js](../create-client/nextjs.md), [Nuxt.js](../create-client/nuxt.md), [React/Redux](../create-client/react.md), [Vue.js](../create-client/vuejs.md), [Quasar](../create-client/quasar.md), and [Vuetify](../create-client/vuetify.md) Progressive Web Apps/Single Page Apps that you can easily tune and customize. The generator also supports [React Native](../create-client/react-native.md) if you prefer to leverage all capabilities of mobile devices. The generated code contains a list (including pagination), a delete button, a creation and an edit form. It also includes [Tailwind CSS](https://tailwindcss.com) classes and [ARIA roles](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA) to make the app usable by people with disabilities. Checkout [the dedicated documentation](../create-client/index.md). ## Caching API Platform supports Caching Metadata out of the box. It uses the Laravel cache system to store that information. Caching is automatically enabled in production environments (when `APP_DEBUG` is set to `false`). Calling `php artisan optimize` will cache the metadata and improve the performance of your API drastically. To clear the cache, use `php artisan optimize:clear`. ## Hooking Your Own Business Logic Now that you learned the basics, be sure to read [the general design considerations](../core/design.md) and [how to extend API Platform](../core/extending.md) to understand how API Platform is designed, and how to hook your custom business logic! ## Using The `IsApiResourceTrait` Instead of Attributes While attributes (introduced in PHP 8) are the preferred way to configure your API Platform resources, it's also possible to use a trait instead. These two classes are strictly equivalent: ```php // Attributes namespace App\Models; use ApiPlatform\Metadata\ApiResource; use Illuminate\Database\Eloquent\Model; #[ApiResource] class Book extends Model { } ``` ```php // Trait namespace App\Models; use ApiPlatform\Metadata\IsApiResource; use Illuminate\Database\Eloquent\Model; class Book extends Model { use IsApiResource; } ``` When using the `IsApiResourceTrait`, it's also possible to return advanced configuration by defining an `apiResource()` static method. These two classes are strictly equivalent: ```php // Attributes namespace App\Models; use ApiPlatform\Metadata\ApiResource; use Illuminate\Database\Eloquent\Model; #[ApiResource( paginationItemsPerPage: 10, operations: [ new GetCollection(), new Get(), ], )] class Book extends Model { } ``` ```php // Trait namespace App\Models; use ApiPlatform\Metadata\IsApiResource; use Illuminate\Database\Eloquent\Model; class Book extends Model { use IsApiResource; public static function apiResource(): ApiResource { return new ApiResource( paginationItemsPerPage: 10, operations: [ new GetCollection(), new Get(), ], ); } } ``` It's quite common to define multiple `ApiResource`, `ApiProperty`, and `Filter` attributes on the same class. To mimic this behavior, the `apiResource()` function can return an array instead of a single instance of metadata class. These two classes are strictly equivalent: ```php // Attributes namespace App\Models; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\QueryParameter; use ApiPlatform\Laravel\Eloquent\Filter\PartialSearchFilter; use App\Http\Requests\BookFormRequest; use Illuminate\Database\Eloquent\Model; #[ApiResource( paginationEnabled: true, paginationItemsPerPage: 5, rules: BookFormRequest::class, operations: [ new Put(), new Patch(), new Get(), new Post(), new Delete(), new GetCollection(), ] )] #[QueryParameter(key: ':property', filter: PartialSearchFilter::class)] class Book extends Model { } ``` ```php // Trait namespace App\Models; use ApiPlatform\Metadata\IsApiResource; use ApiPlatform\Metadata\QueryParameter; use ApiPlatform\Laravel\Eloquent\Filter\PartialSearchFilter; use App\Http\Requests\BookFormRequest; use Illuminate\Database\Eloquent\Model; class Book extends Model { use IsApiResource; public static function apiResource(): array { return [ new ApiResource( paginationEnabled: true, paginationItemsPerPage: 5, rules: BookFormRequest::class, operations: [ new Put(), new Patch(), new Get(), new Post(), new Delete(), new GetCollection(), ] ), new QueryParameter(key: ':property', filter: PartialSearchFilter::class), ]; } } ``` --- # Testing the API with Laravel Source: https://api-platform.com/docs/laravel/testing/ # Testing the API with Laravel For an introduction to testing using API Platform, refer to the [Core Testing Documentation](../core/testing.md), or access the [Symfony Testing Guide](../symfony/testing.md). Let's learn how to use tests with Laravel! In this article, you'll learn how to use: - **[Pest](https://pestphp.com/)**: A testing framework that enables you to write unit tests for your classes and create API-oriented functional tests, thanks to its integrations with API Platform and [Laravel](https://laravel.com/docs/testing). - **[PHPUnit](https://phpunit.de)**: A testing framework for writing unit tests for your classes and conducting API-oriented functional tests, with support for API Platform and [Laravel](https://laravel.com/docs/testing). > [!TIP] Pest is built on top of PHPUnit and introduces additional features along with a syntax > inspired by Ruby's RSpec and the Jest testing APIs. ## Tests with Pest > [!TIP] Even if you are using Pest, you can also use PHPUnit's assertion API, which can be useful > if you're already familiar with PHPUnit's assertion API or if you need to perform more complex > assertions that aren't available in Pest's expectation API. For more information see the > [Pest Assertion API](https://pestphp.com/docs/writing-tests#content-assertion-api) documentation. ### Installing Pest By default, when using Laravel, Pest is pre-configured through the Composer plugin `pestphp/pest-plugin`. You can find this plugin listed in the `allow-plugins` section of your `composer.json` file. To check the Pest installation, run the following command: ```console php artisan test ``` If for some reason, Pest is not installed refer to the [Pest Installation Guide](https://pestphp.com/docs/installation). In that case, you can run Pest using: ```console ./vendor/bin/pest ``` ### Writing Functional Tests with Pest #### Generate the Factory Using Laravel, you can efficiently test databases by combining seeding with model factories. Model factories allow you to generate large amounts of test data quickly, while seeding ensures your database is pre-populated with the necessary records. To create a factory for your model, you can use [Laravel Artisan](https://laravel.com/docs/artisan) command. For example, to create a factory for a Book model, run: ```console php artisan make:factory BookFactory ``` For advanced customization and configuration, refer to the [Defining model Factories Laravel Guide](https://laravel.com/docs/eloquent-factories#defining-model-factories). Then, you can now use your factory in tests to quickly generate model instances. #### Writing Pest tests Here’s an example of tests, which use the Factory: ```php test(function () { // Create 100 books using the factory Book::factory()->count(100)->create(); // Send a GET request to the collection endpoint $response = $this->getJson('/api/books'); // Assert that the response is successful (200 OK) $response->assertStatus(200); // Check the Content-Type header $response->assertHeader('Content-Type', 'application/json'); // Assert the returned JSON contains the expected structure using assertJsonContains from the trait $this->assertJsonContains([ '@context' => '/contexts/Book', '@id' => '/books', '@type' => 'Collection', 'totalItems' => 100, 'view' => [ '@id' => '/books?page=1', '@type' => 'PartialCollectionView', 'first' => '/books?page=1', 'last' => '/books?page=4', 'next' => '/books?page=2', ], ], $response->json()); // Assert that 30 items are returned in the response $this->assertCount(30, $response->json('data')); }); it('creates a valid book') ->test(function () { // Send a POST request to create a book $response = $this->postJson('/api/books', [ 'isbn' => '0099740915', 'title' => 'The Handmaid\'s Tale', 'description' => 'Brilliantly conceived and executed, this powerful evocation of twenty-first century America...', 'author' => 'Margaret Atwood', 'publication_date' => '1985-07-31', ]); // Assert that the book was created successfully (201) $response->assertStatus(201); // Check the Content-Type header $response->assertHeader('Content-Type', 'application/json'); // Assert the returned JSON contains the expected book information using assertJsonContains $this->assertJsonContains([ '@context' => '/contexts/Book', '@type' => 'Book', 'isbn' => '0099740915', 'title' => 'The Handmaid\'s Tale', 'author' => 'Margaret Atwood', 'publication_date' => '1985-07-31', 'reviews' => [], ], $response->json()); // Assert that the URI of the created resource matches the expected format $this->assertMatchesRegularExpression('~^/api/books/\d+$~', $response->json('@id')); }); it('creates an invalid book and validates error handling') ->test(function () { // Send a POST request with invalid data $response = $this->postJson('/api/books', [ 'isbn' => 'invalid', ]); // Assert that the response status is 422 Unprocessable Entity $response->assertStatus(422); // Check the Content-Type header $response->assertHeader('Content-Type', 'application/json'); // Assert the JSON response contains the validation errors using assertJsonContains $this->assertJsonContains([ '@context' => '/contexts/ConstraintViolationList', '@type' => 'ConstraintViolationList', 'title' => 'An error occurred', 'description' => [ 'isbn' => 'This value is neither a valid ISBN-10 nor a valid ISBN-13.', 'title' => 'This value should not be blank.', 'description' => 'This value should not be blank.', 'author' => 'This value should not be blank.', 'publication_date' => 'This value should not be null.', ], ], $response->json()); }); it('updates a book') ->test(function () { // Create a book using the factory $book = Book::factory()->create(['isbn' => '9781344037075']); // Get the IRI of the book using getIriFromResource from the trait $iri = $this->getIriFromResource($book); // Send a PATCH request to update the book's title $response = $this->patchJson($iri, [ 'title' => 'Updated Title', ]); // Assert that the response is successful (200 OK) $response->assertStatus(200); // Assert the JSON response contains the updated book information using assertJsonContains $this->assertJsonContains([ '@id' => $iri, 'isbn' => '9781344037075', 'title' => 'Updated Title', ], $response->json()); }); it('deletes a book') ->test(function () { // Create a book using the factory $book = Book::factory()->create(['isbn' => '9781344037075']); // Get the IRI of the book using getIriFromResource from the trait $iri = $this->getIriFromResource($book); // Send a DELETE request to remove the book $response = $this->deleteJson($iri); // Assert that the response status is 204 No Content $response->assertStatus(204); // Assert that the book is no longer in the database $this->assertDatabaseMissing('books', ['id' => $book->id]); }); ``` In the example above, the [RefreshDatabase Trait](https://laravel.com/docs/database-testing#resetting-the-database-after-each-test) is used to ensure that the database is automatically reset between test runs. This guarantees that each test starts with a clean database state, avoiding conflicts from residual data and ensuring test isolation. This trait is especially useful when testing operations that modify the database, as it rolls back any changes made during the test. As a result, your test environment remains reliable and consistent across multiple test executions. #### Run Pest tests If everything is working properly, you should see `Tests: 5 passed (15 assertions)`. Your REST API is now properly tested! Check out the [API Test Assertions section](#api-test-assertions-with-laravel) to discover the full range of assertions and other features provided by API Platform's test utilities. ### Migrating from PHPUnit to Pest If you want to migrate from PHPUnit to Pest, refer to [Migrating from PHPUnit Guide](https://pestphp.com/docs/migrating-from-phpunit-guide) and [Installation Guide](https://pestphp.com/docs/installation). ## Tests with PHPUnit ### Installing PHPUnit By default, with Laravel, PHPUnit is already a dependency in your project. You may see `phpunit/phpunit` in the `require-dev` section of your `composer.json`. You can test the PHPUnit installation by running: ```console ./vendor/bin/phpunit --version ``` If for some reason, PHPUnit is not installed refer to the [PHPUnit Installation Guide](https://docs.phpunit.de/en/11.4/installation.html#installing-phpunit-with-composer). ### Writing Functional Tests with PHPUnit For instructions on generating the factory, please refer to the [Generate The Factory section](#generate-the-factory). #### Writing PHPUnit tests Here’s an example of a test class, which use the Factory: ```php count(100)->create(); // Send a GET request to the collection endpoint $response = $this->getJson('/api/books'); // Assert that the response is successful (200 OK) $response->assertStatus(200); // Check the Content-Type header $response->assertHeader('Content-Type', 'application/json'); // Assert the returned JSON contains the expected structure using assertJsonContains from the trait $this->assertJsonContains([ '@context' => '/contexts/Book', '@id' => '/books', '@type' => 'Collection', 'totalItems' => 100, 'view' => [ '@id' => '/books?page=1', '@type' => 'PartialCollectionView', 'first' => '/books?page=1', 'last' => '/books?page=4', 'next' => '/books?page=2', ], ], $response->json()); // Assert that 30 items are returned in the response $this->assertCount(30, $response->json('data')); } /** * Test to create a valid book. */ public function testCreateBook(): void { // Send a POST request to create a book $response = $this->postJson('/api/books', [ 'isbn' => '0099740915', 'title' => 'The Handmaid\'s Tale', 'description' => 'Brilliantly conceived and executed, this powerful evocation of twenty-first century America...', 'author' => 'Margaret Atwood', 'publication_date' => '1985-07-31', ]); // Assert that the book was created successfully (201) $response->assertStatus(201); // Check the Content-Type header $response->assertHeader('Content-Type', 'application/json'); // Assert the returned JSON contains the expected book information using assertJsonContains $this->assertJsonContains([ '@context' => '/contexts/Book', '@type' => 'Book', 'isbn' => '0099740915', 'title' => 'The Handmaid\'s Tale', 'author' => 'Margaret Atwood', 'publication_date' => '1985-07-31', 'reviews' => [], ], $response->json()); // Assert that the URI of the created resource matches the expected format $this->assertMatchesRegularExpression('~^/api/books/\d+$~', $response->json('@id')); } /** * Test to create an invalid book and validate error handling. */ public function testCreateInvalidBook(): void { // Send a POST request with invalid data $response = $this->postJson('/api/books', [ 'isbn' => 'invalid', ]); // Assert that the response status is 422 Unprocessable Entity $response->assertStatus(422); // Check the Content-Type header $response->assertHeader('Content-Type', 'application/json'); // Assert the JSON response contains the validation errors using assertJsonContains $this->assertJsonContains([ '@context' => '/contexts/ConstraintViolationList', '@type' => 'ConstraintViolationList', 'title' => 'An error occurred', 'description' => [ 'isbn' => 'This value is neither a valid ISBN-10 nor a valid ISBN-13.', 'title' => 'This value should not be blank.', 'description' => 'This value should not be blank.', 'author' => 'This value should not be blank.', 'publication_date' => 'This value should not be null.', ], ], $response->json()); } /** * Test to update a book. */ public function testUpdateBook(): void { // Create a book using the factory $book = Book::factory()->create(['isbn' => '9781344037075']); // Get the IRI of the book using getIriFromResource from the trait $iri = $this->getIriFromResource($book); // Send a PATCH request to update the book's title $response = $this->patchJson($iri, [ 'title' => 'Updated Title', ]); // Assert that the response is successful (200 OK) $response->assertStatus(200); // Assert the JSON response contains the updated book information using assertJsonContains $this->assertJsonContains([ '@id' => $iri, 'isbn' => '9781344037075', 'title' => 'Updated Title', ], $response->json()); } /** * Test to delete a book. */ public function testDeleteBook(): void { // Create a book using the factory $book = Book::factory()->create(['isbn' => '9781344037075']); // Get the IRI of the book using getIriFromResource from the trait $iri = $this->getIriFromResource($book); // Send a DELETE request to remove the book $response = $this->deleteJson($iri); // Assert that the response status is 204 No Content $response->assertStatus(204); // Assert that the book is no longer in the database $this->assertDatabaseMissing('books', ['id' => $book->id]); } } ``` In the example above, the [RefreshDatabase Trait](https://laravel.com/docs/database-testing#resetting-the-database-after-each-test) is used to ensure that the database is automatically reset between test runs. This guarantees that each test starts with a clean database state, avoiding conflicts from residual data and ensuring test isolation. This trait is especially useful when testing operations that modify the database, as it rolls back any changes made during the test. As a result, your test environment remains reliable and consistent across multiple test executions. #### Run PHPUnit tests If everything is working properly, you should see `OK (5 tests, 15 assertions)`. Your REST API is now properly tested! Check out the [API Test Assertions section](#api-test-assertions-with-laravel) to discover the full range of assertions and other features provided by API Platform's test utilities. ## Writing Unit Tests In addition to integration tests written using the helpers provided by Pest and PHPUnit, all the classes of your project should be covered by [unit tests](https://en.wikipedia.org/wiki/Unit_testing). To do so, learn how to write unit tests with [Pest](https://pestphp.com), [PHPUnit](https://phpunit.de/) and [Laravel Creating Tests Guide](https://laravel.com/docs/11.x/testing#creating-tests). ## Continuous Integration, Continuous Delivery and Continuous Deployment Running your test suite in your [CI/CD pipeline](https://en.wikipedia.org/wiki/Continuous_integration) is important to ensure good quality and delivery time. The API Platform Demo is [shipped with a GitHub Actions workflow](https://github.com/api-platform/demo/tree/main/.github/workflows) that builds the Docker images, does a [smoke test]() to check that the application's entrypoint is accessible, and runs PHPUnit. The API Platform Demo [contains a CD workflow](https://github.com/api-platform/demo/tree/main/.github/workflows) that uses [a Helm chart](../deployment/kubernetes.md) to deploy the app on a Kubernetes cluster. ## Additional and Alternative Testing Tools You may also be interested in these alternative testing tools (not included by default): - [Hoppscotch](https://docs.hoppscotch.io/features/tests), create functional test for your API - [Behat](https://behat.org), a [behavior-driven development (BDD)](https://en.wikipedia.org/wiki/Behavior-driven_development) framework to write the API specification as user stories and in natural language then execute these scenarios against the application to validate its behavior; - [Playwright](https://playwright.dev) is recommended if you use have PWA/JavaScript-heavy app. ## Testing Utilities for Laravel ### API Test Assertions with Laravel In addition to [the built-in ones](https://phpunit.readthedocs.io/en/main/assertions.html), API Platform provides convenient PHPUnit assertions dedicated to API testing: ```php get('/'); // Asserts that an array has a specified subset. $this->assertArraySubset(/* An array or an iterable */); // Asserts that the returned JSON is a superset of the passed one $this->assertJsonContains(/* a JSON document as an array or as a string */); } } ``` There is also a method to find the IRI matching a given resource: ```php getIriFromResource($book); $response = $this->get($iri); $response->assertStatus(200); } } ``` --- # Parameters and Filters Source: https://api-platform.com/docs/laravel/filters/ # Parameters and Filters API Platform is great for Rapid Application Development and provides lots of functionalities out of the box such as collection filtering with Eloquent. Most of the filtering is done using query parameters, which are automatically documented and validated. If needed you can use [state providers](../core/state-providers.md) or a [Links Handler] to provide data. ## Parameters A filter is usually used via a `ApiPlatform\Metadata\QueryParameter` and is also available through `ApiPlatform\Metadata\HeaderParameter`. For example, let's declare an `EqualsFilter` on our `Book` to be able to query an exact match using `/books?name=Animal Farm. A Fairy Story`: ```php // app/Models/Book.php use ApiPlatform\Laravel\Eloquent\Filter\EqualsFilter; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\QueryParameter; #[ApiResource] #[QueryParameter(key: 'name', filter: EqualsFilter::class)] class Book extends Model { } ``` The `key` option specifies the query parameter and the `filter` applies the given value to a where clause: ```php namespace ApiPlatform\Laravel\Eloquent\Filter; use ApiPlatform\Metadata\Parameter; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; final class EqualsFilter implements FilterInterface { /** * @param Builder $builder * @param array $context */ public function apply(Builder $builder, mixed $values, Parameter $parameter, array $context = []): Builder { return $builder->where($parameter->getProperty(), $values); } } ``` You can create your own filters by implementing the `ApiPlatform\Laravel\Eloquent\Filter\FilterInterface`. API Platform provides several eloquent filters for a RAD approach. ### Parameter for Specific Operations To defines a parameter for only a `GetCollection` operation, you can do the following: ```php // app/Models/Book.php use ApiPlatform\Laravel\Eloquent\Filter\EqualsFilter; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\QueryParameter; #[ApiResource] #[GetCollection(parameters: ['name' => new QueryParameter(key: 'name', filter: EqualsFilter::class)])] class Book extends Model { } ``` ### Parameter Validation You can add [validation rules](https://laravel.com/docs/validation) to parameters within the `constraints` attribute: ```php // app/Models/Book.php use ApiPlatform\Laravel\Eloquent\Filter\PartialSearchFilter; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\QueryParameter; #[ApiResource] #[QueryParameter(key: 'name', filter: PartialSearchFilter::class, constraints: 'min:2')] class Book extends Model { } ``` ### The `:property` Placeholder When programming APIs you may need to apply a filter on many properties at once. For example, we're allowing to sort on every property of our ApiResource with a partial search filter: ```php // app/Models/Book.php use ApiPlatform\Laravel\Eloquent\Filter\PartialSearchFilter; use ApiPlatform\Laravel\Eloquent\Filter\OrderFilter; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\QueryParameter; #[ApiResource] #[QueryParameter(key: 'sort[:property]', filter: OrderFilter::class)] #[QueryParameter(key: ':property', filter: PartialSearchFilter::class)] class Book extends Model { } ``` The documentation will output a query parameter per property that applies the `PartialSearchFilter` and also gives the ability to sort by name and ID using: `/books?name=search&order[id]=asc&order[name]=desc`. ### Filtering on Specific Properties Only To enable partial search filtering and sorting on specific properties like `name` and `description`: ```php // app/Models/Book.php use ApiPlatform\Laravel\Eloquent\Filter\PartialSearchFilter; use ApiPlatform\Laravel\Eloquent\Filter\OrderFilter; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\QueryParameter; #[ApiResource] #[QueryParameter(key: 'sort[:property]', filter: OrderFilter::class, properties: ['name', 'description'])] #[QueryParameter(key: ':property', filter: PartialSearchFilter::class, properties: ['name', 'description'])] class Book extends Model { } ``` ## Filters ### Text As shown above the following search filters are available: - `ApiPlatform\Laravel\Eloquent\Filter\PartialSearchFilter` queries `LIKE %term%` - `ApiPlatform\Laravel\Eloquent\Filter\EqualsFilter` queries `= term` - `ApiPlatform\Laravel\Eloquent\Filter\StartSearchFilter` queries `LIKE term%` - `ApiPlatform\Laravel\Eloquent\Filter\EndSearchFilter` queries `LIKE %term` ### Date The `DateFilter` allows to filter dates with an operator (`eq`, `lt`, `gt`, `lte`, `gte`): - `eq` - equals (exact match) - `gt` - greater than (strictly after) - `gte` - greater than or equal (after or on) - `lt` - less than (strictly before) - `lte` - less than or equal (before or on) - `after` - alias for `gte` - `before` - alias for `lte` - `strictly_after` - alias for `gt` - `strictly_before` - alias for `lt` Usage Examples With the `DateFilter` applied, you can now filter dates between 2024-01-01 and 2024-01-31 using these API calls: #### Option 1: Using gte and lte operators ```http GET /api/your_entities?createdAt[gte]=2024-01-01&createdAt[lte]=2024-01-31 ``` #### Option 2: Using after and before operators ```http GET /api/your_entities?createdAt[after]=2024-01-01&createdAt[before]=2024-01-31 ``` #### Option 3: Using strictly_after and strictly_before ```http GET /api/your_entities?createdAt[strictly_after]=2023-12-31&createdAt[strictly_before]=2024-02-01 ``` ```php // app/Models/Book.php use ApiPlatform\Laravel\Eloquent\Filter\DateFilter; #[ApiResource] #[QueryParameter(key: 'publicationDate', filter: DateFilter::class, filterContext: ['include_nulls' => true])] class Book extends Model { use HasUlids; public function author(): BelongsTo { return $this->belongsTo(Author::class); } } ``` Our default strategy is to exclude null values, just remove the `filterContext` if you want to exclude nulls. ### Or The `OrFilter` allows to filter using an `OR WHERE` clause: ```php // app/Models/Book.php use ApiPlatform\Laravel\Eloquent\Filter\DateFilter; #[ApiResource] #[QueryParameter( key: 'q', filter: new OrFilter(new EqualsFilter()), property: 'isbn' )] class Book extends Model { use HasUlids; public function author(): BelongsTo { return $this->belongsTo(Author::class); } } ``` This allows to query multiple `isbn` values with a `q` query parameter: `/books?q[]=9781784043735&q[]=9780369406361`. ### BooleanFilter The `BooleanFilter` allows to filter using an `WHERE` clause on a boolean field with (`true`, `false`, `0`, `1`): ```php // app/Models/Book.php use ApiPlatform\Laravel\Eloquent\Filter\BooleanFilter; #[ApiResource] #[QueryParameter(key: 'published', filter: BooleanFilter::class)] class Book extends Model { use HasUlids; public function author(): BelongsTo { return $this->belongsTo(Author::class); } } ``` Examples: - `/books?published=true` - `/books?published=1` - `/books?published=false` - `/books?published=0` ### PropertyFilter Note: We strongly recommend using [Vulcain](https://vulcain.rocks) instead of this filter. Vulcain is faster, allows a better hit rate, and is supported out of the box in the API Platform Symfony variant. The property filter adds the possibility to select the properties to serialize (sparse fieldsets). ```php // app/Models/Book.php use ApiPlatform\Laravel\Eloquent\Filter\DateFilter; use ApiPlatform\Serializer\Filter\PropertyFilter; #[ApiResource] #[QueryParameter(key: 'properties', filter: PropertyFilter::class)] class Book extends Model { use HasUlids; public function author(): BelongsTo { return $this->belongsTo(Author::class); } } ``` A few `filterContext` options are available to configure the filter: - `override_default_properties` allows to override the default serialization properties (default `false`) Using `true` is dangerous, use carefully this can expose unwanted data! - `whitelist` properties whitelist to avoid uncontrolled data exposure (default `null` to allow all properties) Given that the collection endpoint is `/books`, you can filter the serialization properties with the following query: `/books?properties[]=title&properties[]=author`. ### Creating Custom Filters (API Platform >= 4.2) #### Generating the Laravel Eloquent Filter Skeleton To get started, API Platform includes a very handy make command to generate the basic structure of an Laravel Eloquent filter: ```console bin/console make:filter ``` Then, provide the name of your filter, for example `MonthFilter`, or pass it directly as an argument: ```console make:filter MyCustomFilter ``` You will get a file at `app/Filter/MonthFilter.php` with the following content: ```php $builder * @param array $context */ public function apply(Builder $builder, mixed $values, Parameter $parameter, array $context = []): Builder { // TODO: make your awesome query using the $builder // return $builder-> } } ``` #### Implementing a Custom Laravel Eloquent Filter Let's create a concrete filter that allows fetching entities based on the month of a date field (e.g., `createdAt`). The goal is to be able to call a URL like `GET /invoices?createdAtMonth=7` to get all invoices created in July. Here is the complete and corrected code for the filter: ```php $builder * @param array $context */ public function apply(Builder $builder, mixed $values, Parameter $parameter, array $context = []): Builder { return $builder->->whereMonth($parameter->getProperty(), $values); } } ``` We can now use it in our resources and model like other filters, for example, as follows: ```php [ // .... 'middleware' => 'auth:sanctum', ], ]; ``` --- # Validation with Laravel Source: https://api-platform.com/docs/laravel/validation/ # Validation with Laravel API Platform simplifies the validation of data sent by clients to the API, typically user inputs submitted through forms. You can add [validation rules](https://laravel.com/docs/validation) within the `rules` option: ```php // app/Models/Book.php use ApiPlatform\Metadata\ApiResource; #[ApiResource( rules: [ 'title' => 'required', ] )] class Book extends Model { } ``` --- # JWT Authentication with Laravel Source: https://api-platform.com/docs/laravel/jwt/ # JWT Authentication with Laravel > [!NOTE] While solutions like `tymondesigns/jwt-auth` (Laravel) or `LexikJWTAuthenticationBundle` > (Symfony) are popular, **we recommend adopting open standards such as > [OpenID Connect (OIDC)](https://openid.net/connect/)** for robust, scalable, and interoperable > authentication. For comprehensive details on authentication, refer to our [Laravel Authentication documentation](../laravel/index.md#authentication). ## Setup Instructions 1. **Install** Follow the official installation guide of [Laravel Passport](https://laravel.com/docs/passport#installation) to implement OpenID Connect (OIDC) standards in your Laravel application. Alternatively, if you prefer an ad-hoc solution, you can use [tymondesigns/jwt-auth](https://github.com/tymondesigns/jwt-auth) to set up JWT authentication in your Laravel project. 2. **Configure Authentication** Refer to the [Authentication section](../laravel/index.md#authentication) of our documentation to properly configure and secure your API with JWT tokens. > [!TIP] Use [Laravel middlewares with API Platform](../laravel/index.md#middlewares) such as > `auth:api` to restrict access to certain endpoints, ensuring only authenticated users can access > them. By following these steps, you can set up a secure and scalable JWT-based authentication system in your Laravel application. ## Testing To verify your authentication setup using `ApiTestCase`, you can write a test method tailored to your preferred testing framework. Here's how you can approach it for both **Pest** and **PHPUnit**: > [!NOTE] Ensure your routes (/api/auth) and authentication mechanisms are configured to match your > application's implementation. ### Test with Pest ```php create([ 'email' => 'test@example.com', 'password' => bcrypt('$3CR3T'), // Hash the password ]); // Retrieve a token $response = $this->postJson('/api/auth', [ 'email' => 'test@example.com', 'password' => '$3CR3T', ]); $response->assertStatus(200) ->assertJsonStructure(['token']); $token = $response->json('token'); // Test not authorized $this->getJson('/api/greetings') ->assertStatus(401); // Test authorized $this->withHeader('Authorization', "Bearer $token") ->getJson('/api/greetings') ->assertStatus(200); }); ``` ### Test with PHPUnit ```php create([ 'email' => 'test@example.com', 'password' => bcrypt('$3CR3T'), // Hash the password ]); // Retrieve a token $response = $this->postJson('/api/auth', [ 'email' => 'test@example.com', 'password' => '$3CR3T', ]); $response->assertStatus(200) ->assertJsonStructure(['token']); $token = $response->json('token'); // Test not authorized $this->getJson('/api/greetings') ->assertStatus(401); // Test authorized $this->withHeader('Authorization', "Bearer $token") ->getJson('/api/greetings') ->assertStatus(200); } } ``` --- # The API Platform Core Library Source: https://api-platform.com/docs/core/ # The API Platform Core Library API Platform Core is an easy-to-use and powerful library for creating [hypermedia-driven REST APIs](https://en.wikipedia.org/wiki/HATEOAS). It is a component of the [API Platform framework](https://api-platform.com). It embraces [JSON for Linked Data (JSON-LD)](https://json-ld.org/) and [Hydra Core Vocabulary](https://www.hydra-cg.com/) web standards but also supports [OpenAPI (formerly known as Swagger)](https://www.openapis.org/), [JSON:API](https://jsonapi.org/), [HAL](https://stateless.co/hal_specification.html), XML, JSON, CSV and YAML. Build a working and fully featured CRUD API in minutes. Leverage the awesome features of the tool to develop complex and high-performance API-first projects. If you are starting a new project, the easiest way to get API Platform up is to install API Platform using [API Platform for Symfony](../symfony/index.md) or [API Platform for Laravel](../laravel/index.md). Alternatively, it's possible to [bootstrap the API Platform core library manually](../core/bootstrap.md). ![Screenshot](../symfony/images/swagger-ui-1.png) ## Features Here is the fully featured REST API you'll get in minutes: - [Automatic CRUD](operations.md) - Hypermedia (JSON-LD and HAL) - Machine-readable documentation of the API in the Hydra and [Swagger/Open API](openapi.md) formats, guessed from PHPDoc, Serializer, Validator, and Doctrine ORM / MongoDB ODM metadata - Nice human-readable documentation built with Swagger UI (including a sandbox) and/or ReDoc - [Pagination](pagination.md) - A bunch of [filters](filters.md) - [Ordering](default-order.md) - [Validation](validation.md) using the Symfony Validator Component (with group support) - Advanced [authentication and authorization](security.md) rules - Errors serialization (Hydra and the [RFC 7807](https://tools.ietf.org/html/rfc7807) are supported) - Advanced [serialization](serialization.md) thanks to the Symfony Serializer Component (groups support, relation embedding, max depth...) - Automatic route registration - Automatic entry point generation giving access to all resources - [User Management using Symfony](../symfony/user.md) - [JWT](jwt.md) and [OAuth](https://oauth.net/) support - Files and `\DateTime` and serialization and deserialization Everything is fully customizable through a powerful [event system](events.md) and strong OOP. This bundle is extensively tested (unit and functional). The [`Fixtures/` directory](https://github.com/api-platform/core/tree/main/tests/Fixtures) contains a working app covering all library features. ## Symfony Screencasts

SymfonyCasts, API Platform screencasts

The easiest and funniest way to learn how to use API Platform for Symfony is to watch [the more than 60 screencasts available on SymfonyCasts](https://symfonycasts.com/tracks/rest?cid=apip#api-platform-3)! --- # Getting started Source: https://api-platform.com/docs/core/getting-started/ # Getting started ## Installing API Platform You can choose your preferred stack between Symfony, Laravel, or bootstrapping the API Platform core library manually. > [!CAUTION] If you are migrating from an older version of API Platform, make sure you read the > [Upgrade Guide](upgrade-guide.md). ### Symfony If you are starting a new project, the easiest way to get API Platform up is to install [API Platform for Symfony](../symfony/index.md). It comes with the API Platform core library integrated with [the Symfony framework](https://symfony.com), [the schema generator](../schema-generator/index.md), [Doctrine ORM](https://www.doctrine-project.org), [NelmioCorsBundle](https://github.com/nelmio/NelmioCorsBundle) and [test assertions dedicated to APIs](../symfony/testing.md). [MongoDB](mongodb.md) and [Elasticsearch](elasticsearch.md) can also be easily enabled. Basically, it is a Symfony edition packaged with the best tools to develop a REST and GraphQL APIs and sensible default settings. Alternatively, you can use [Composer](https://getcomposer.org/) to install the standalone bundle in an existing Symfony Flex project: ```console composer require api ``` There are no mandatory configuration options although [many settings are available](configuration.md). ### Migrating from FOSRestBundle If you plan to migrate from FOSRestBundle, you might want to read [this guide](../symfony/migrate-from-fosrestbundle.md) to get started with API Platform. ### Laravel API Platform can be installed on any new or existing Laravel project using [API Platform for Laravel](../laravel/index.md). It comes with integrations from the Laravel ecosystem, including [Eloquent](https://laravel.com/docs/eloquent), [Validation](https://laravel.com/docs/validation), [Authorization](https://laravel.com/docs/authorization), [Octane](https://laravel.com/docs/octane), [Pest](https://pestphp.com)... ### Bootstrapping the Core Library While more complex, the core library [can also be installed in vanilla PHP projects and other frameworks](../core/bootstrap.md). ## Before Reading this Documentation If you haven't read it already, take a look at [the Laravel Getting Started guide](../laravel/index.md) or [the Symfony Getting Started guide](../symfony/index.md). These tutorials cover basic concepts required to understand how API Platform works including how it implements the REST architectural style and what [JSON-LD](https://json-ld.org/) and [Hydra](https://www.hydra-cg.com/) formats are. ## Mapping the Entities ### Symfony with Doctrine

Create an API Resource screencast
Watch the Create an API Resource screencast

API Platform can automatically expose entities mapped as "API resources" through a REST API supporting CRUD operations. To expose your entities, you can use attributes, XML, and YAML configuration files. Here is an example of entities mapped using attributes that will be exposed through a REST API: ```php offers = new ArrayCollection(); // Initialize $offers as a Doctrine collection } public function getId(): ?int { return $this->id; } // Adding both an adder and a remover as well as updating the reverse relation is mandatory // if you want Doctrine to automatically update and persist (thanks to the "cascade" option) the related entity public function addOffer(Offer $offer): void { $offer->product = $this; $this->offers->add($offer); } public function removeOffer(Offer $offer): void { $offer->product = null; $this->offers->removeElement($offer); } // ... } ``` ```php id; } } ``` It is the minimal configuration required to expose `Product` and `Offer` entities as JSON-LD documents through an hypermedia web API. If you are familiar with the Symfony ecosystem, you noticed that entity classes are also mapped with Doctrine ORM attributes and validation constraints from [the Symfony Validator Component](https://symfony.com/doc/current/validation.html). This isn't mandatory. You can use [your preferred persistence](state-providers.md) and [validation](validation.md) systems. However, API Platform has built-in support for those libraries and is able to use them without requiring any specific code or configuration to automatically persist and validate your data. They are a good default option and we encourage you to use them unless you know what you are doing. Thanks to the mapping done previously, API Platform will automatically register the following REST [operations](operations.md) for resources of the product type: ### Product API using Symfony | Method | URL | Description | | ------ | -------------- | ----------------------------------------- | | GET | /products | Retrieve the (paginated) collection | | POST | /products | Create a new product | | GET | /products/{id} | Retrieve a product | | PATCH | /products/{id} | Apply a partial modification to a product | | DELETE | /products/{id} | Delete a product | > [!NOTE] > > `PUT` (replace or create) isn't registered automatically, but is entirely supported by API > Platform and can be added explicitly. The same operations are available for the offer method > (routes will start with the `/offers` pattern). Route prefixes are built by pluralizing the name > of the mapped entity class. It is also possible to override the naming convention using > [operation path namings](operation-path-naming.md). As an alternative to attributes, you can map entity classes using YAML or XML: ```yaml # api/config/api_platform/resources.yaml resources: App\Entity\Product: ~ App\Entity\Offer: shortName: "Offer" # optional description: "An offer from my shop" # optional types: ["https://schema.org/Offer"] # optional paginationItemsPerPage: 25 # optional ``` ```xml description="An offer from my shop" > https://schema.org/Offer ``` If you prefer to use YAML or XML files instead of attributes, you must configure API Platform to load the appropriate files: ```yaml # api/config/packages/api_platform.yaml api_platform: mapping: paths: - "%kernel.project_dir%/src/Entity" # default configuration for attributes - "%kernel.project_dir%/config/api_platform" # yaml or xml directory configuration ``` If you want to serialize only a subset of your data, please refer to the [Serialization documentation](serialization.md). **You're done!** You now have a fully featured API exposing your entities. Run the Symfony app with the [Symfony Local Web Server](https://symfony.com/doc/current/setup/symfony_server.html) (`symfony server:start`) and browse the API entrypoint at `http://localhost:8000/api`. ### Laravel with Eloquent API Platform introspects the database (column names, types, constraints, types, constraints...) to populate API Platform metadata. Serialization, OpenAPI, and hydra docs are generated from these metadata directly. #### Example First, create a migration class for the `products` table: ```console php artisan make:migration create_products_table ``` Open the generated migration class (`database/migrations/_create_products_table.php`) and add some columns: ```patch public function up(): void { Schema::create('products', function (Blueprint $table) { $table->id(); + $table->string('name'); + $table->decimal('price', 8, 2); + $table->text('description'); + $table->boolean('is_active')->default(true); + $table->date('created_date')->nullable(); $table->timestamps(); }); } ``` Finally, execute the migration: ```console php artisan migrate ``` And after that, just adding the `#[ApiResource]` attribute as follows onto your model: ```patch markReadOnly()`) no longer expose PUT and PATCH operations. Clients sending PUT/PATCH to these resources will receive a 404. If you need write operations on readonly entities, explicitly define them in your `ApiResource` attribute. #### JSON-LD `@type` with `output` and `itemUriTemplate` When using `output` with `itemUriTemplate` on a collection operation, the JSON-LD `@type` now uses the resource class name instead of the output DTO class name for semantic consistency with `itemUriTemplate` behavior. Update any client code that relies on the DTO class name in `@type`. ### Behavioral Changes #### `isGranted` Evaluated Before Provider Security expressions are now evaluated before the state provider runs. Expressions that do not reference the `object` variable will be checked at the `pre_read` stage, improving security by preventing unnecessary database queries on unauthorized requests. Expressions that reference `object` still wait for the provider to resolve the entity. Review any security expressions that relied on provider side-effects running before authorization. #### Hydra Class `@id` Now Always Uses `#ShortName` Hydra documentation classes now consistently use `#ShortName` as their `@id` instead of schema.org type URIs (e.g. `schema:Product`). Semantic types configured via `types` are now exposed through `rdfs:subClassOf`. Clients should expect class `@id` and property range changes in the Hydra documentation if resources had custom `types` configured. #### LDP-Compliant Response Headers API responses now include `Allow` and `Accept-Post` headers per the Linked Data Platform specification. These are informational headers that help clients discover API capabilities and should not break existing integrations. ## API Platform 3.4 Remove the `keep_legacy_inflector`, the `event_listeners_backward_compatibility_layer` and the `rfc_7807_compliant_errors` flag: ```diff api_platform: - event_listeners_backward_compatibility_layer: false - keep_legacy_inflector: false extra_properties: - standard_put: true - rfc_7807_compliant_errors: true ``` If you use a custom normalizer for validation exception use: ```yaml api_platform: validator: legacy_validation_exception: true ``` Indeed, we will throw another validation class in API Platform 4 we will throw `ApiPlatform\Validator\Exception\ValidationException` instead of `ApiPlatform\Symfony\Validator\Exception\ValidationException` It's really important to add the `use_symfony_listeners` flag, set to `true` if you use Symfony listeners or controllers: ```yaml api_platform: use_symfony_listeners: false ``` The `keep_legacy_inflector` flag will be removed from API Platform 4, you need to fix your issues first. In API Platform 3.4, the Inflector is available as a service that you can configure through: ```yaml api_platform: inflector: api_platform.metadata.inflector ``` Implement the `ApiPlatform\Metadata\InflectorInterface` if you need to tweak its behavior. We added an `hydra_prefix` configuration as the `hydra:` prefix will be removed by default in API Platform 4: ```yaml api_platform: serializer: hydra_prefix: false ``` Standard PUT is now `true` by default, you can change its value using: ```yaml api_platform: defaults: extra_properties: standard_put: true ``` We recommend using the standalone API Platform packages instead of the Core monolithic repository. Update your `composer.json` like that: ```patch { "require": { - "api-platform/core": "^3", + "api-platform/symfony": "^3 || ^4" + // also add the extra packages you need, like "api-platform/doctrine-orm" } } ``` ## API Platform 3.1/3.2 This is the recommended configuration for API Platform 3.2. We review each of these changes in this document. ```yaml api_platform: title: Hello API Platform version: 1.0.0 formats: jsonld: ["application/ld+json"] docs_formats: jsonld: ["application/ld+json"] jsonopenapi: ["application/vnd.openapi+json"] html: ["text/html"] defaults: stateless: true cache_headers: vary: ["Content-Type", "Authorization", "Origin"] extra_properties: standard_put: true rfc_7807_compliant_errors: true event_listeners_backward_compatibility_layer: false keep_legacy_inflector: false ``` ### Formats We noticed that API Platform was enabling `json` by default because of our OpenAPI support. We introduced the new `application/vnd.openapi+json`. Therefore if you want `json` you need to explicitly handle it: ```yaml formats: json: ["application/json"] ``` You can also remove documentations you're not using via the new `docs_formats`. A new option `error_formats` is also used for content negotiation. ### Event listeners For new users we recommend to use ```yaml event_listeners_backward_compatibility_layer: false ``` This allows API Platform to not use http kernel event listeners. It also allows you to force options like `read: true` or `validate: true`. This simplifies use cases like [validating a delete operation](https://api-platform.com/docs/v3.2/guides/delete-operation-with-validation/) Event listeners will not get removed and are not deprecated, they'll use our providers and processors in a future version. ### Inflector We're switching to `symfony/string` [inflector](https://symfony.com/doc/current/components/string.html#inflector), to keep using `doctrine/inflector` use: ```yaml keep_legacy_inflector: true ``` We strongly recommend that you use your own inflector anyways with a [PathSegmentNameGenerator](https://github.com/api-platform/core/blob/f776f11fd23e5397a65c1355a9ebcbb20afac9c2/src/Metadata/Operation/UnderscorePathSegmentNameGenerator.php). ### Errors ```yaml defaults: extra_properties: rfc_7807_compliant_errors: true ``` As this is an `extraProperties` it's configurable per resource/operation. This is improving the compatibility of Hydra errors with JSON problem. It also enables new extension points on [Errors](https://api-platform.com/docs/v3.2/core/errors/) such as [Error provider](https://api-platform.com/docs/v3.2/guides/error-provider/) and [Error Resource](https://api-platform.com/docs/v3.2/guides/error-resource/). ### OpenApi context You may want to convert your openApiContext to openapi, doing so is quite fastidious, @lyrixx created a rector script to help if needed: [https://github.com/lyrixx/rector-apip-openapi](https://github.com/lyrixx/rector-apip-openapi) --- # General Design Considerations Source: https://api-platform.com/docs/core/design/ # General Design Considerations Since you only need to describe the structure of the data to expose, API Platform is both [a "design-first" and "code-first"](https://swagger.io/blog/api-design/design-first-or-code-first-api-development/) API framework. However, the "design-first" methodology is strongly recommended: first you design the **public shape** of API endpoints. To do so, you have to write a plain old PHP object (POPO) representing the input and output of your endpoint. This is the class that is [marked with the `#[ApiResource]` attribute](../symfony/index.md). This class **doesn't have** to be mapped with Doctrine ORM, or any other persistence system. It must be simple (it's usually just a data structure with no or minimal behaviors) and will be automatically converted to [Hydra](extending-jsonld-context.md), [OpenAPI](openapi.md) and [GraphQL](graphql.md) documentations or schemas by API Platform (there is a 1-1 mapping between this class and those docs). Then, it's up to the developer to feed API Platform with an hydrated instance of this API resource object by implementing the [`ProviderInterface`](state-providers.md). Basically, the state provider will query the persistence system (RDBMS, document or graph DB, external API...), and must hydrate and return the POPO that has been designed as mentioned above. When updating a state (`POST`, `PUT`, `PATCH`, `DELETE` HTTP methods), it's up to the developer to properly persist the data provided by API Platform's resource object [hydrated by the serializer](serialization.md). To do so, there is another interface to implement: [`ProcessorInterface`](state-processors.md). This class will read the API resource object (the one marked with `#[ApiResource]`) and: - persist it directly in the database; - or hydrate a DTO then trigger a command; - or populate an event store; - or persist the data in any other useful way. The logic of state processors is the responsibility of application developers, and is **out of the API Platform's scope**. For [Rapid Application Development](https://en.wikipedia.org/wiki/Rapid_application_development), convenience and prototyping, **if and only if the class marked with `#[ApiResource]` is also a Doctrine entity**, the developer can use the Doctrine ORM's state provider and processor implementations shipped with API Platform. In this case, the public (`#[ApiResource]`) and internal (Doctrine entity) data models are shared. Then, API Platform will be able to query, filter, paginate and persist data automatically. This approach is super-convenient and efficient, but is probably **not a good idea** for non-[CRUD](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete) and/or large systems. Again, it's up to the developers to use, or to not use these built-in state providers/processors depending on the business logic they are dealing with. API Platform makes it easy to create custom state providers and processors. It also makes it easy to implement patterns such as [CQS](https://www.martinfowler.com/bliki/CommandQuerySeparation.html) or [CQRS](https://martinfowler.com/bliki/CQRS.html) thanks to [the Messenger Component integration](../symfony/messenger.md) and the [DTO support](dto.md). Last but not least, to create [Event Sourcing](https://martinfowler.com/eaaDev/EventSourcing.html)-based systems, a convenient approach is: - to persist data in an event store using a Messenger handler or a custom [state processor](state-processors.md) - to create projections in standard RDBMS (PostgreSQL, MariaDB...) tables or views - to map those projections with read-only Doctrine entity classes **and** to mark those classes with `#[ApiResource]` You can then benefit from the built-in Doctrine filters, sorting, pagination, auto-joins and all of [the extension points](extending.md) provided by API Platform. --- # Extending API Platform Source: https://api-platform.com/docs/core/extending/ # Extending API Platform Because it handles the complex, tedious and repetitive task of creating an API infrastructure for you, API Platform lets you focus on what matters the most for the end user: the business logic. To do so, API Platform provides a lot of extension points you can use to hook your own code. Those extensions points are taken into account both by the REST and [GraphQL](graphql.md) subsystems. The following tables summarizes which extension point to use depending on what you want to do: | Extension Point | Usage | | ---------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [State Providers](state-providers.md) | adapters for custom persistence layers, virtual fields, custom hydration | | [Denormalizers](serialization.md) | post-process objects created from the payload sent in the HTTP request body | | [Symfony Voters](../symfony/security.md#hooking-custom-permission-checks-using-voters) | custom authorization logic | | [Laravel Policies](../laravel/security.md#policies) | custom authorization logic | | [Validation constraints](validation.md) | custom validation logic | | [State Processors](state-processors) | custom business logic and computations to trigger before or after persistence (ex: mail, call to an external API...) | | [Normalizers](serialization.md#changing-the-serialization-context-dynamically) | customize the resource sent to the client (add fields in JSON documents, encode codes, dates...) | | [Filters](filters.md) | create filters for collections and automatically document them (OpenAPI, GraphQL, Hydra) | | [Serializer Context Builders](serialization.md#changing-the-serialization-context-dynamically) | change the Serialization context (e.g. groups) dynamically | | [Messenger Handlers](../symfony/messenger.md) | create 100% custom, RPC, async, service-oriented endpoints (should be used in place of custom controllers because the messenger integration is compatible with both REST and GraphQL, while custom controllers only work with REST) | | [DTOs](dto.md) | use a specific class to represent the input or output data structure related to an operation | | [Kernel Events](events.md) | customize the HTTP request or response (REST only, other extension points must be preferred when possible) | | [Operations and Resources](operations.md) | use mutators to dynamically alter metadata (works for third party API endpoints) | ## Doctrine Specific Extension Points | Extension Point | Usage | | ------------------------------ | -------------------------------------------------------------------------------------------------- | | [Extensions](extensions.md) | Access to the query builder to change the DQL query | | [Filters](doctrine-filters.md) | Add filters documentations (OpenAPI, GraphQL, Hydra) and automatically apply them to the DQL query | ## Leveraging the Built-in Infrastructure Using Composition While most API Platform classes are marked as `final`, built-in services are straightforward to reuse and customize [using composition](https://en.wikipedia.org/wiki/Composition_over_inheritance). For instance, if you want to send a mail after a resource has been persisted, but still want to benefit from the native Doctrine ORM [state processor](state-processors.md), use [the decorator design pattern](https://en.wikipedia.org/wiki/Decorator_pattern#PHP) to wrap the native state processor in your own class sending the mail, as demonstrated in [this example](../core/state-processors.md#creating-a-custom-state-processor). To replace existing API Platform services with your decorators, [check out how to decorate services](https://symfony.com/doc/current/service_container/service_decoration.html).

Service Decoration screencast
Watch the Service Decoration screencast

## System Providers and Processors The system is based on a workflow composed of **state providers** and **state processors**. The schema below describes them: ```mermaid --- title: System providers and processors --- flowchart TB C1(ContentNegotiationProvider) --> C2(ReadProvider) C2 --> C3(AccessCheckerProvider) C3 --> C4(DeserializeProvider) C4 --> C5(ParameterProvider) C5 --> C6(ValidateProcessor) C6 --> C7(WriteProcessor) C7 --> C8(SerializeProcessor) ``` ### Symfony Access Checker Provider When using Symfony, the access checker provider is used at three different stages: - `api_platform.state_provider.access_checker.post_validate` decorates the `ValidateProvider` - `api_platform.state_provider.access_checker.post_deserialize` decorates the `DeserializeProvider` - `api_platform.state_provider.access_checker` decorates the `ReadProvider` > [!NOTE] For graphql use: `api_platform.graphql.state_provider.access_checker.post_deserialize`, > `api_platform.graphql.state_provider.access_checker.post_validate`, > `api_platform.graphql.state_provider.validate` and > `api_platform.graphql.state_provider.access_checker.after_resolver` ### Decoration Example Here is an example of the decoration of the RespondProcessor: Starts by creating your `CustomRespondProcessor`: ```php processor->process($data, $operation, $uriVariables, $context); // You can add post-write code here. return $writtenObject; } } ``` Now decorate the `RespondProcessor` with the `CustomRespondProcessor` using Symfony or Laravel: ### Symfony Processor Decoration With Symfony you can simply do that by adding the `#[AsDecorator]` attribute as following: ```php namespace App\State; use ApiPlatform\State\ProcessorInterface; #[AsDecorator(decorates: 'api_platform.state.processor.respond_processor')] final class CustomRespondProcessor implements ProcessorInterface { // ... } ``` or in the `services.yaml` by defining: ```yaml # api/config/services.yaml services: # ... App\State\CustomRespondProcessor: decorates: api_platform.state.processor.respond_processor ``` And that's it! ### Laravel Processor Decoration ```php app->extend(RespondProcessor::class, function (RespondProcessor $respondProcessor) { return new CustomRespondProcessor($respondProcessor); }); } } ``` --- # Testing the API Source: https://api-platform.com/docs/core/testing/ # Testing the API Once your API is up and running, it's crucial to write tests to ensure it is bug-free and to prevent future regressions. A good practice is to follow a [Test-Driven Development (TDD)](https://martinfowler.com/bliki/TestDrivenDevelopment.html) approach, where tests are written before the production code. API Platform provides a set of helpful testing utilities to write unit tests, functional tests, and to create [test fixtures](https://en.wikipedia.org/wiki/Test_fixture#Software). ## Testing Documentations - If you are using API Platform with Symfony, refer to the [Testing the API with Symfony](/symfony/testing.md) documentation. - If you are using API Platform with Laravel, refer to the [Testing the API with Laravel](/laravel/testing.md) documentation. --- # Operations Source: https://api-platform.com/docs/core/operations/ # Operations API Platform relies on the concept of operations. Operations can be applied to a resource exposed by the API. From an implementation point of view, an operation is a link between a resource, a route and its related controller.

Operations screencast
Watch the Operations screencast

API Platform automatically registers typical [CRUD](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete) operations and describes them in the exposed documentation (Hydra and Swagger). It also creates and registers routes for these operations in the Symfony routing system, if available, or in the Laravel routing system, should that be the case. The behavior of built-in operations is briefly presented in the [Getting started](getting-started.md#mapping-the-entities) guide. The list of enabled operations can be configured on a per-resource basis. Creating custom operations on specific routes is also possible. There are two types of operations: collection operations and item operations. Collection operations act on a collection of resources. By default two operations are implemented: `POST` and `GET`. Item operations act on an individual resource. Three default operation are defined: `GET`, `DELETE` and `PATCH`. `PATCH` is supported with [JSON Merge Patch (RFC 7396)](https://www.rfc-editor.org/rfc/rfc7386), or [using the JSON:API format](https://jsonapi.org/format/#crud-updating), as required by the specification. The `PUT` operation is also supported, but is not registered by default. When the `ApiPlatform\Metadata\ApiResource` annotation is applied to an entity class, the following built-in CRUD operations are automatically enabled: Collection operations: | Method | Mandatory | Description | Registered by default | | ------ | --------- | ----------------------------------------- | --------------------- | | `GET` | yes | Retrieve the (paginated) list of elements | yes | | `POST` | no | Create a new element | yes | Item operations: | Method | Mandatory | Description | Registered by default | | -------- | --------- | ------------------------------------------ | --------------------- | | `GET` | yes | Retrieve an element | yes | | `PUT` | no | Replace an element | no | | `PATCH` | no | Apply a partial modification to an element | yes | | `DELETE` | no | Delete an element | yes | > [!NOTE] The `PATCH` method must be enabled explicitly in the configuration, refer to the > [Content Negotiation](content-negotiation.md) section for more information. --- > [!NOTE] With JSON Merge Patch, the > [null values will be skipped](https://symfony.com/doc/current/components/serializer.html#skipping-null-values) > in the response. --- > [!NOTE] Current `PUT` implementation behaves more or less like the `PATCH` method. Existing > properties not included in the payload are **not** removed, their current values are preserved. To > remove an existing property, its value must be explicitly set to `null`. ## Enabling and Disabling Operations If no operation is specified, all default CRUD operations are automatically registered. It is also possible - and recommended for large projects - to define operations explicitly. Keep in mind that once you explicitly set up an operation, the automatically registered CRUD will no longer be. If you declare even one operation manually, such as `#[GET]`, you must declare the others manually as well if you need them. Operations can be configured using attributes, XML or YAML. In the following examples, we enable only the built-in operation for the `GET` method for both `collection` and `item` to create a readonly endpoint. If the operation's name matches a supported HTTP method (`GET`, `POST`, `PUT`, `PATCH` or `DELETE`), the corresponding `method` property will be automatically added. --- > [!NOTE] In Symfony we use the term “entities”, while the following documentation is mostly for > Laravel “models”. ```php ``` The previous example can also be written with an explicit method definition: ```php ``` API Platform is smart enough to automatically register the applicable Symfony route referencing a built-in CRUD action just by specifying the method name as key, or by checking the explicitly configured HTTP method. By default, API Platform uses the first `Get` operation defined to generate the IRI of an item and the first `GetCollection` operation to generate the IRI of a collection. If your resource does not have any `Get` operation, API Platform automatically adds an operation to help generating this IRI. If your resource has any identifier, this operation will look like `/books/{id}`. But if your resource doesn’t have any identifier, API Platform will use the Skolem format `/.well-known/genid/{id}`. Those routes are not exposed from any documentation (for instance OpenAPI), but are anyway declared on the routing system and always return a HTTP 404. ## Configuring Operations The URL, the method and the default status code (among other options) can be configured per operation. In the next example, both `GET` and `POST` operations are registered with custom URLs. Those will override the URLs generated by default. In addition to that, we require the `id` parameter in the URL of the `GET` operation to be an integer, and we configure the status code generated after successful `POST` request to be `301`: ```php '\d+'], defaults: ['color' => 'brown'], options: ['my_option' => 'my_option_value'], schemes: ['https'], host: '{subdomain}.api-platform.com' ), new Post( uriTemplate: '/grimoire', status: 301 ) ])] class Book { //... } ``` ```yaml # api/config/api_platform/resources.yaml resources: App\Entity\Book: operations: ApiPlatform\Metadata\Post: uriTemplate: "/grimoire" status: 301 ApiPlatform\Metadata\Get: uriTemplate: "/grimoire/{id}" requirements: id: '\d+' defaults: color: "brown" host: "{subdomain}.api-platform.com" schemes: ["https"] options: my_option: "my_option_value" ``` ```xml \d+ brown https brown ``` When you do not want to allow access to the resource item (i.e. you don't want a `GET` item operation), instead of omitting the resource item altogether, you can explicitly specify the IRI of the resource item by declaring a `GET` item operation that returns HTTP 404 (Not Found). For example: ```php ``` ## Prefixing All Routes of All Operations Sometimes it's also useful to put a whole resource into its own "namespace" regarding the URI. Let's say you want to put everything that's related to a `Book` into the `library` so that URIs become `library/book/{id}`. In that case you don't need to override all the operations to set the path but configure the `routePrefix` attribute for the whole entity instead: ```php ``` ## Defining Which Operation to Use to Generate the IRI Using multiple operations on your resource, you may want to specify which operation to use to generate the IRI, instead of letting API Platform use the first one it finds. Let's say you have 2 resources in relationship: `Company` and `User`, where a company has multiple users. You can declare the following routes: - `/users` - `/users/{id}` - `/companies/{companyId}/users` - `/companies/{companyId}/users/{id}` The first routes (`/users...`) are only accessible by the admin, and the others by regular users. Calling `/companies/{companyId}/users` should return IRIs matching `/companies/{companyId}/users/{id}` to not expose an admin route to regular users. To do so, use the `itemUriTemplate` option only available on `GetCollection` and `Post` operations: ```php ``` API Platform will find the operation matching this `itemUriTemplate` and use it to generate the IRI. If this option is not set, the first `Get` operation is used to generate the IRI. ## Expose a Model Without Any Routes Sometimes, you may want to expose a model, but want it to be used through subrequests only, and never through item or collection operations. Because the OpenAPI standard requires at least one route to be exposed to make your models consumable, let's see how you can manage this kind of issue. Let's say you have the following entities in your project: ```php getOperations(); if (null === $operations) { return $resource; } foreach ($operations as $name => $operation) { // add route prefix to each resource operation $prefixedOperation = $operation->withRoutePrefix($this->prefix); $operations->add($name, $prefixedOperation); } return $resource->withOperations($operations); } } ``` ### Operation Mutator The operation mutator will modify a specific operation's metadata, by using the attribute and passing the operation name: ```php getNormalizationContext() ?? []; // add another group to normalization group $context['groups'][] = 'review:list:read'; return $operation->withNormalizationContext($context); } } ``` > [!NOTE] Operation mutators are executed during metadata loading, the result is stored in cache so > runtime logic is prohibited. --- --- # GraphQL Support Source: https://api-platform.com/docs/core/graphql/ # GraphQL Support [GraphQL](https://graphql.org/) is a query language made to communicate with an API and therefore is an alternative to REST. It has some advantages compared to REST: it solves the over-fetching or under-fetching of data, is strongly typed, and is capable of retrieving multiple and nested data in one go, but it also comes with drawbacks. For example it creates overhead depending on the request. API Platform creates a REST API by default. But you can choose to enable GraphQL as well. Once enabled, you have nothing to do: your schema describing your API is automatically built and your GraphQL endpoint is ready to go! ## Enabling GraphQL To enable GraphQL and its IDE (GraphiQL and GraphQL Playground) in your API, simply require the `api-platform/graphql` package using Composer: ```console composer require api-platform/graphql ``` You can now use GraphQL at the endpoint: `https://localhost/graphql`. > [!NOTE] If you used > [the Symfony Variant thanks to Symfony Flex](../symfony/index.md#installing-the-framework) or the > Laravel variant, the default GraphQL endpoint will be available at a relative URL like `/graphql`. > For example: `https://localhost/graphql`. ## Changing Location of the GraphQL Endpoint Sometimes you may want to have the GraphQL endpoint at a different location. This can be done by manually configuring the GraphQL controller. ### Symfony Routes Using the Symfony variant we can do this modification by adding the following code: ```yaml # api/config/routes.yaml api_graphql_entrypoint: path: /graphql controller: api_platform.graphql.action.entrypoint # ... ``` Change `/graphql` to the URI you wish the GraphQL endpoint to be accessible on. ### Laravel Routes Using the Laravel variant we can do this modification by adding the following code: ```php // routes/web.php use Illuminate\Support\Facades\Route; use ApiPlatform\GraphQL\Action\EntrypointAction; Route::post('/graphql', EntrypointAction::class) ->name('api_graphql_entrypoint'); ``` Change `/graphql` to the URI you wish the GraphQL endpoint to be accessible on. ## GraphiQL Go to the GraphQL endpoint with your browser, you will see a nice interface provided by GraphiQL to interact with your API. The GraphiQL IDE can also be found at `/graphql/graphiql`. If you need to disable it, it can be done in the configuration: ### Disabling GraphiQL with Symfony ```yaml # api/config/packages/api_platform.yaml api_platform: graphql: graphiql: enabled: false # ... ``` ### Disabling GraphiQL with Laravel ```php [ 'graphiql' => [ 'enabled' => false, ] ], ]; ``` ### Add another Location for GraphiQL Sometimes you may want to have the GraphiQL at a different location. This can be done by manually configuring the GraphiQL controller. ### Symfony config routes for GraphiQL If you want to add a different location besides `/graphql/graphiql`, you can do it like this if you are using the Symfony variant: ```yaml # app/config/routes.yaml graphiql: path: /docs/graphiql controller: api_platform.graphql.action.graphiql ``` ### Laravel config routes for GraphiQL If you want to add a different location besides `/graphql/graphiql`, you can do it like this if you are using the Laravel variant: ```php // routes/web.php use Illuminate\Support\Facades\Route; use ApiPlatform\GraphQL\Action\GraphiQlAction; Route::post('/docs/graphiql', GraphiQlAction::class) ->name('graphiql'); ``` ## GraphQL Playground Another IDE is by default included in API Platform: GraphQL Playground. It can be found at `/graphql/graphql_playground`. You can disable it if you want in the configuration. ### Disable GraphQL Playground with Symfony ```yaml # api/config/packages/api_platform.yaml api_platform: graphql: graphql_playground: enabled: false # ... ``` ### Disable GraphQL Playground with Laravel > [!WARNING] This is not yet available with Laravel, you're welcome to contribute > [on GitHub](https://github.com/api-platform/core) ### Add another Location for GraphQL Playground You can add a different location besides `/graphql/graphql_playground`. ### Symfony config routes for GraphQL Playground Using the Symfony variant we can do this modification by adding the following code: ```yaml # app/config/routes.yaml graphql_playground: path: /docs/graphql_playground controller: api_platform.graphql.action.graphql_playground ``` ### Laravel config routes for GraphQL Playground Using the Laravel variant we can do this modification by adding the following code: ```php // routes/web.php use Illuminate\Support\Facades\Route; use ApiPlatform\GraphQL\Action\GraphQlPlaygroundAction; Route::post('/docs/graphql_playground', GraphQlPlaygroundAction::class) ->name('graphql_playground'); ``` ## Modifying or Disabling the Default IDE When going to the GraphQL endpoint, you can choose to launch the IDE you want. ### Symfony config to modifying the default IDE ```yaml # api/config/packages/api_platform.yaml api_platform: graphql: # Choose between graphiql or graphql-playground default_ide: graphql-playground # ... ``` ### Laravel config to modifying the default IDE ```php [ // Choose between graphiql or graphql-playground 'default_ide' => 'graphql-playground', ], ]; ``` You can also disable this feature by setting the configuration value to `false`. ### Symfony config to disable default IDE ```yaml # api/config/packages/api_platform.yaml api_platform: graphql: default_ide: false # ... ``` ### Laravel config to disable default IDE ```php [ 'default_ide' => false, ], ]; ``` ## Disabling the Introspection Query For security reason, the introspection query should be disabled to not expose the GraphQL schema. ### Symfony config to disable the Introspection Query If you need to disable it, it can be done in the configuration: ```yaml # api/config/packages/api_platform.yaml api_platform: graphql: introspection: false # ... ``` ### Laravel config to disable the Introspection Query If you need to disable it, it can be done in the configuration: ```php [ 'introspection' => false, ], ]; ``` ## Request with `application/graphql` Content-Type If you wish to send a [POST request using the `application/graphql` Content-Type](https://graphql.org/learn/serving-over-http/#post-request), you need to enable it in the [allowed formats of API Platform](content-negotiation.md#configuring-formats-globally): ### Symfony config for GraphQL Content-Type ```yaml # api/config/packages/api_platform.yaml api_platform: formats: # ... graphql: ["application/graphql"] ``` ### Laravel config for GraphQL Content-Type ```php [ 'graphql' => [ 'application/graphql', ], ], ]; ``` ## Operations > [!NOTE] In Symfony we use the term “entities”, while the following documentation is mostly for > Laravel “models”. To understand what an operation is, please refer to the [operations documentation](operations.md). For GraphQL, the operations are defined by using the `Query`, `QueryCollection`, `Mutation`, `DeleteMutation` and `Subscription` attributes. By default, the following operations are enabled: - `Query` - `QueryCollection` - `Mutation(name: 'create')` - `Mutation(name: 'update')` - `DeleteMutation(name: 'delete')` You can of course disable or configure these operations. For instance, in the following example, only the query of an item and the create mutation are enabled: ```php ``` ## Queries If you don't know what queries are yet, please [read the documentation about them](https://graphql.org/learn/queries/). For each resource, two queries are available: one for retrieving an item and the other one for the collection. For example, if you have a `Book` resource, the queries `book` and `books` can be used. ### Global Object Identifier When querying an item, you need to pass an identifier as argument. Following the [GraphQL Global Object Identification Specification](https://relay.dev/graphql/objectidentification.htm), the identifier needs to be globally unique. In API Platform, this argument is represented as an [IRI (Internationalized Resource Identifier)](https://www.w3.org/TR/ld-glossary/#internationalized-resource-identifier). For example, to query a book having as identifier `89`, you have to run the following: ```graphql { book(id: "/books/89") { title isbn } } ``` Note that in this example, we're retrieving two fields: `title` and `isbn`. ### Custom Queries To create a custom query, first of all you need to create its resolver. If you want a custom query for a collection, create a class like this: ```php $collection * * @return iterable */ public function __invoke(iterable $collection, array $context): iterable { // Query arguments are in $context['args']. foreach ($collection as $book) { // Do something with the book. } return $collection; } } ``` ### Custom Queries config for Symfony If you use autoconfiguration (the default Symfony configuration) in your application, then you are done! Else, you need to tag your resolver like this if you are using Symfony without autoconfiguration : ```yaml # api/config/services.yaml services: # ... App\Resolver\BookCollectionResolver: tags: - { name: api_platform.graphql.query_resolver } ``` ### Custom Queries config for Laravel If you are using Laravel tag your resolver with: ```php app->tag([BookCollectionResolver::class], QueryItemResolverInterface::class); } public function boot(): void { } } ``` The resolver for an item is very similar: ```php ```php ['type' => 'ID!'], 'log' => ['type' => 'Boolean!', 'description' => 'Is logging activated?'], 'logDate' => ['type' => 'DateTime'] ] ), new QueryCollection(name: 'collectionQuery', resolver: BookCollectionResolver::class), ])] class Book { // ... } ``` ```yaml #The YAML syntax is only supported for Symfony resources: App\Entity\Book: graphQlOperations: - class: ApiPlatform\Metadata\GraphQl\Query - class: ApiPlatform\Metadata\GraphQl\QueryCollection - class: ApiPlatform\Metadata\GraphQl\Mutation name: create - class: ApiPlatform\Metadata\GraphQl\Mutation name: update - class: ApiPlatform\Metadata\GraphQl\Mutation name: delete - class: ApiPlatform\Metadata\GraphQl\Query name: retrievedQuery resolver: App\Resolver\BookResolver - class: ApiPlatform\Metadata\GraphQl\Query name: notRetrievedQuery resolver: App\Resolver\BookResolver args: [] - class: ApiPlatform\Metadata\GraphQl\Query name: withDefaultArgsNotRetrievedQuery resolver: App\Resolver\BookResolver read: false - class: ApiPlatform\Metadata\GraphQl\Query name: withCustomArgsQuery resolver: App\Resolver\BookResolver args: id: type: "ID!" log: type: "Boolean!" description: "Is logging activated?" logDate: type: "DateTime" - class: ApiPlatform\Metadata\GraphQl\QueryCollection name: collectionQuery resolver: App\Resolver\BookCollectionResolver ``` ```xml ID! Boolean! Is logging activated? DateTime ```
Note that you need to explicitly add the auto-generated queries and mutations if they are needed when configuring custom queries, like it's done for the [operations](#operations). As you can see, it's possible to define your own arguments for your custom queries. They are following the GraphQL type system. If you don't define the `args` property, it will be the default ones (for example `id` for an item). You can also use the `extraArgs` property if you want to add more arguments than the generated ones. If you don't want API Platform to retrieve the item for you, disable the `read` provider. Some other providers and processors [can be disabled](#disabling-system-providers-and-processors). Another option would be to make sure there is no `id` argument. This is the case for `notRetrievedQuery` (empty args). Conversely, if you need to add custom arguments, make sure `id` is added among the arguments if you need the item to be retrieved automatically. Note also that: - If you have added your [own custom types](#custom-types), you can use them directly for your arguments types (it's the case here for `DateTime`). - You can also add a custom description for your custom arguments. You can see the [field arguments documentation](https://webonyx.github.io/graphql-php/type-system/object-types/#field-arguments) for more options. The arguments you have defined or the default ones and their value will be in `$context['args']` of your resolvers. Your custom queries will be available like this: ```graphql { retrievedQueryBook(id: "/books/56") { title } notRetrievedQueryBook { title } withDefaultArgsNotRetrievedQueryBook(id: "/books/56") { title } withCustomArgsQueryBook(id: "/books/23", log: true, logDate: "2019-12-20") { title } collectionQueryBooks { edges { node { title } } } } ``` ## Mutations If you don't know what mutations are yet, the documentation about them is [in the GraphQL documentation for mutations](https://graphql.org/learn/queries/#mutations). For each resource, three mutations are available: - `Mutation(name: 'create')` for creating a new resource - `Mutation(name: 'update')` for updating an existing resource - `DeleteMutation(name: 'delete')` for deleting an existing resource When updating or deleting a resource, you need to pass the **IRI** of the resource as argument. See [Global Object Identifier](#global-object-identifier) for more information. ### Client Mutation ID Following the [Relay Input Object Mutations Specification](https://github.com/facebook/relay/blob/v7.1.0/website/spec/Mutations.md#relay-input-object-mutations-specification), you can pass a `clientMutationId` as argument and can ask its value as a field. For example, if you delete a book: ```graphql mutation DeleteBook($id: ID!, $clientMutationId: String!) { deleteBook(input: { id: $id, clientMutationId: $clientMutationId }) { clientMutationId } } ``` ### Custom Mutations Creating custom mutations is comparable to creating [custom queries](#custom-queries). Create your resolver: ```php ```php ```
### Subscribe Doing a subscription is very similar to doing a query: ```graphql { subscription { updateBookSubscribe(input: { id: "/books/1", clientSubscriptionId: "myId" }) { book { title isbn } mercureUrl clientSubscriptionId } } } ``` As you can see, you need to pass the **IRI** of the resource as argument. See [Global Object Identifier](#global-object-identifier) for more information. You can also pass `clientSubscriptionId` as argument and can ask its value as a field. In the payload of the subscription, the given fields of the resource will be the fields you subscribe to: if any of these fields is updated, you will be pushed their updated values. The `mercureUrl` field is the Mercure URL you need to use to [subscribe to the updates](https://mercure.rocks/docs/getting-started#subscribing) on the client-side. ### Receiving an Update On the client-side, you will receive the pushed updated data like you would receive the updated data if you did an `update` mutation. For instance, you could receive a JSON payload like this: ```json { "book": { "title": "Updated title", "isbn": "978-6-6344-4051-1" } } ``` ### Subscriptions Cache Internally, API Platform stores the subscriptions in a cache, using the [Symfony Cache](https://symfony.com/doc/current/cache.html) if you're using the Symfony variant or API Platform uses [Laravel cache](https://laravel.com/docs/cache) if you're using the Laravel variant. The cache is named `api_platform.graphql.cache.subscription` and the subscription keys are generated from the subscription payload by using a SHA-256 hash. It's recommended to use an adapter like Redis for this cache. ## Workflow of the Resolvers API Platform resolves the queries and mutations by using its own **resolvers**. Even if you create your [custom queries](#custom-queries) or your [custom mutations](#custom-mutations), these resolvers will be used and yours will be called at the right time. See the [Extending API Platform](extending.md) documentation for more information. ### Disabling system providers and processors If you need to, you can disable some states providers and state processors, for instance if you don't want your data to be validated. The following table lists the system states providers and states processors you can disable in your resource configuration. | Attribute | Type | Default | Description | | -------------------------- | ------ | ------- | ----------------------------------------- | | `query_parameter_validate` | `bool` | `true` | Enables or disables `QueryParameter` | | `read` | `bool` | `true` | Enables or disables `ReadProvider` | | `deserialize` | `bool` | `true` | Enables or disables `DeserializeProvider` | | `validate` | `bool` | `true` | Enables or disables `ValidateProcessor` | | `write` | `bool` | `true` | Enables or disables `WriteProcessor` | | `serialize` | `bool` | `true` | Enables or disables `SerializeProcessor` | A provider or a processor can be disabled at the operation level: ```php ``` Or at the resource attributes level (will be also applied in REST and for all operations): ```php ``` ## Events No events are sent by the resolvers in API Platform. If you want to add your custom logic, [extending API Platform](extending.md) is the recommended way to do it. ## Filters Filters are supported out-of-the-box. Follow the [filters](filters.md) documentation and your filters will be available as arguments of queries. However, you don't necessarily have the same needs for your GraphQL endpoint as for your REST one. In the `QueryCollection` attribute, you can choose to decorrelate the GraphQL filters. In order to keep the default behavior (possibility to fetch, delete, update or create), define all the auto-generated operations (`Query` ,`QueryCollection`, `DeleteMutation`, and the `update` and `create` `Mutation`). For example, this entity will have a search filter for REST and a date filter for GraphQL: ```php offer.search_filter offer.date_filter ``` ### Syntax for Filters with a List of Key / Value Arguments Some filters like the [exists filter](doctrine-filters.md#exists-filter) or the [order filter](doctrine-filters.md#order-filter-sorting) take a list of key / value as arguments. The first syntax coming to mind to use them is to write: ```graphql { offers(order: { id: "ASC", name: "DESC" }) { edges { node { id name } } } } ``` However this syntax has a problematic issue: it doesn't keep the order of the arguments. These filters usually need a proper order to give results as expected. That's why this syntax needs to be used instead: ```graphql { offers(order: [{ id: "ASC" }, { name: "DESC" }]) { edges { node { id name } } } } ``` Since a list is used for the arguments, the order is preserved. ### Filtering on Nested Properties Unlike for REST, all built-in filters support nested properties using the underscore (`_`) syntax instead of the dot (`.`) syntax, e.g.: ```php 'exact'])] class Offer { // ... } ``` The above allows you to find offers by their respective product's color like for the REST API. You can then filter using the following syntax: ```graphql { offers(product_color: "red") { edges { node { id product { name color } } } } } ``` Or order your results like: ```graphql { offers(order: [{ product_releaseDate: "DESC" }]) { edges { node { id product { name color } } } } } ``` Another difference with the REST API filters is that the keyword `_list` must be used instead of the traditional `[]` to filter over multiple values. For example, if you want to search the offers with a green or a red product you can use the following syntax: ```graphql { offers(product_color_list: ["red", "green"]) { edges { node { id product { name color } } } } } ``` ## Pagination API Platform natively enables a cursor-based pagination for collections. It supports [GraphQL's Complete Connection Model](https://graphql.org/learn/pagination/#complete-connection-model) and is compatible with [GraphQL Cursor Connections Specification](https://relay.dev/graphql/connections.htm). A page-based pagination can also be enabled per resource or per operation. ### Using the Cursor-based Pagination Here is an example query leveraging the pagination system: ```graphql { offers(first: 10, after: "cursor") { totalCount pageInfo { endCursor hasNextPage } edges { cursor node { id } } } } ``` Two pairs of parameters work with the query: - `first` and `after`; - `last` and `before`. More precisely: - `first` corresponds to the items per page starting from the beginning; - `after` corresponds to the `cursor` from which the items are returned. - `last` corresponds to the items per page starting from the end; - `before` corresponds to the `cursor` from which the items are returned, from a backwards point of view. The current page always has a `startCursor` and an `endCursor`, present in the `pageInfo` field. To get the next page, you would add the `endCursor` from the current page as the `after` parameter. ```graphql { offers(first: 10, after: "endCursor") { } } ``` For the previous page, you would add the `startCursor` from the current page as the `before` parameter. ```graphql { offers(last: 10, before: "startCursor") { } } ``` How do you know when you have reached the last page? It is the aim of the property `hasNextPage` or `hasPreviousPage` in `pageInfo`. When it is false, you know it is the last page and moving forward or backward will give you an empty result. ### Using the Page-based Pagination In order to use the page-based pagination, you need to enable it in the resource. For instance at the operation level: ```php ``` Or if you want to do it at the resource level: ```php ``` Once enabled, a `page` filter will be available in the collection query (its name [can be changed in the configuration](pagination.md)) and an `itemsPerPage` filter will be available too if [client-side-pagination](pagination.md#changing-the-number-of-items-per-page-for-a-specific-resource) is enabled. A `paginationInfo` field can be queried to obtain the following information: - `itemsPerPage`: the number of items per page. To change it, follow the [pagination documentation](pagination.md#changing-the-number-of-items-per-page). - `lastPage`: the last page of the collection. - `totalCount`: the total number of items in the collection. - `hasNextPage`: does the current collection offers a next page. The collection items data are available in the `collection` field. An example of a query: ```graphql { offers(page: 3, itemsPerPage: 15) { collection { id } paginationInfo { itemsPerPage lastPage totalCount hasNextPage } } } ``` ### Disabling the Pagination See also the [pagination documentation](pagination.md#disabling-the-pagination). #### Globally The pagination can be disabled for all GraphQL resources using this configuration: ##### Disable pagination for all GraphQL resources with Symfony ```yaml # api/config/packages/api_platform.yaml api_platform: graphql: collection: pagination: enabled: false ``` ##### Disable pagination for all GraphQL resources with Laravel ```php [ 'collection' => [ 'pagination' => => [ 'enabled' => false, ], ] ], ]; ``` #### For a Specific Resource It can also be disabled for a specific resource (REST and GraphQL): ```php ``` #### For a Specific Resource Collection Operation You can also disable the pagination for a specific collection operation: ```php ``` ### Partial Pagination [Partial pagination](pagination.md#partial-pagination) is possible with GraphQL. When enabled, backwards pagination will not be possible, and the `hasNextPage` information will be always `false`. ## Security To add a security layer to your queries and mutations, follow the [security](security.md) documentation. The REST security configuration and the GraphQL one are **not** correlated. If you have only some parts differing between REST and GraphQL, you have to redefine the common parts anyway. In the example below, we want the same security rules as we have in REST, but we also want to allow an admin to delete a book only in GraphQL. Please note that, it's not possible to update a book in GraphQL because the `update` operation is not defined. ```php ``` ### Securing Properties (Including Associations) You may want to limit access to certain resource properties with a security expression. This can be done with the `ApiProperty` `security` attribute. Note: adding the `ApiProperty` `security` expression to a GraphQL property will automatically make the GraphQL property type nullable (if it wasn't already). This is because `null` is returned as the property value if access is denied via the `security` expression. In GraphQL, it's possible to expose associations - allowing nested querying. For example, associations can be made with Doctrine ORM's `OneToMany`, `ManyToOne`, `ManyToMany`, etc. It's important to note that the security defined on resource operations applies only to the exposed query/mutation endpoints (e.g. `Query.users`, `Mutation.updateUser`, etc.). Resource operation security is defined via the `security` attribute for each operation defined on the resource. This security is _not_ applied to exposed associations. Associations can instead be secured with the `ApiProperty` `security` attribute. This provides the flexibility to have different security depending on where an association is exposed. To prevent traversal attacks, you should ensure that any exposed associations are secured appropriately. A traversal attack is where a user can gain unintended access to a resource by querying nested associations, gaining access to a resource that prevents direct access (via the query endpoint). For example, a user may be denied using `Query.getUser` to get a user, but is able to access the user through an association on an object that they do have access to (e.g. `document.createdBy`). The following example shows how associations can be secured: ```php ``` ```php ``` The above example only allows admins to see the full collection of each resource (`QueryCollection`). Users must be granted the `VIEW` attribute on a resource to be able to query it directly (`Query`) - which would use a `Voter` to make this decision. Similar to `Query`, all associations are secured, requiring `VIEW` access on the parent object (_not_ on the association). This means that a user with `VIEW` access to a `Document` is able to see all users who are in the `viewers` collection, as well as the `createdBy` association. This may be a little too open, so you could instead do a role check here to only allow admins to access these fields, or check for a different attribute that could be implemented in the voter (e.g. `VIEW_CREATED_BY`.) Alternatively, you could still expose the users, but limit the visible fields by limiting access with `ApiProperty` `security` (such as the `User::$email` property above) or with [dynamic serializer groups](serialization.md#changing-the-serialization-context-dynamically). ## Serialization Groups You may want to restrict some resource's attributes to your GraphQL clients. As described in the [serialization process](serialization.md) documentation, you can use serialization groups to expose only the attributes you want in queries or in mutations. If the (de)normalization context between GraphQL and REST is different, use the `(de)normalizationContext` key to change it in each query and mutations. Note that: - A **query** is only using the normalization context. - A **mutation** is using the denormalization context for its input and the normalization context for its output. The following example shows you what can be done: ```php ['read']], denormalizationContext: ['groups' => ['write']], graphQlOperations: [ new Query(normalizationContext: ['groups' => ['query']]), new QueryCollection(normalizationContext: ['groups' => ['query_collection']]), new Mutation( name: 'create', normalizationContext: ['groups' => ['query_collection']], denormalizationContext: ['groups' => ['mutation']] ) ] )] class Book { // ... #[Groups(['read', 'write', 'query', 'query_collection'])] public $title; #[Groups(['read', 'mutation', 'query'])] public $author; // ... } ``` ```yaml #The YAML syntax is only supported for Symfony resources: App\Entity\Book: normalizationContext: groups: ["read"] denormalizationContext: groups: ["write"] graphQlOperations: ApiPlatform\Metadata\GraphQl\Query: normalizationContext: groups: ["query"] ApiPlatform\Metadata\GraphQl\QueryCollection: normalizationContext: groups: ["query_collection"] ApiPlatform\Metadata\GraphQl\Mutation: name: create normalizationContext: groups: ["query_collection"] denormalizationContext: groups: ["mutation"] ``` ```xml read write query query_collection query_collection mutation ``` In this case, the REST endpoint will be able to get the two attributes of the book and to modify only its title. The GraphQL endpoint will be able to query the title and author of an item. It will be able to query the title of the items in the collection. It will only be able to create a book with an author. When doing this mutation, the author of the created book will not be returned (the title will be instead). ### Different Types when Using Different Serialization Groups When you use different serialization groups, it will create different types in your schema. Make sure you understand the implications when doing this: having different types means breaking the cache features in some GraphQL clients (in [Apollo Client](https://www.apollographql.com/docs/react/caching/cache-configuration/#automatic-cache-updates) for example). For instance: - If you use a different `normalizationContext` for a mutation, a `MyResourcePayloadData` type with the restricted fields will be generated and used instead of `MyResource` (the query type). - If you use a different `normalizationContext` for the query of an item (`Query` attribute) and for the query of a collection (`QueryCollection` attribute), two types `MyResourceItem` and `MyResourceCollection` with the restricted fields will be generated and used instead of `MyResource` (the query type). ### Embedded Relation Input (Creation of Relation in Mutation) By default, creating a relation when using a `create` or `update` mutation is not possible. Indeed, the mutation expects an IRI for the relation in the input, so you need to use an existing relation. For instance if you have the following resource: ```php ``` Creating a book with its author will be done like this, where `/authors/32` is the IRI of an existing resource: ```graphql { mutation { createBook(input: { title: "The Name of the Wind", author: "/authors/32" }) { book { title author { name } } } } } ``` In order to create an author as the same time as a book, you need to use the denormalization context and groups on the book and the author (see also [the dedicated part in the serialization documentation](serialization.md#denormalization): ```php ['book:create']])])] class Book { // ... #[Groups(['book:create'])] public string $title; #[Groups(['book:create'])] public ?Author $author; // ... } ``` ```yaml #The YAML syntax is only supported for Symfony resources: App\Entity\Book: graphQlOperations: ApiPlatform\Metadata\GraphQl\Mutation: name: create denormalizationContext: groups: ["book:create"] ``` ```xml book:create ``` And in the author resource: ```php defaultErrorHandler = $defaultErrorHandler; } public function __invoke(array $errors, callable $formatter): array { // Log or filter the errors. return ($this->defaultErrorHandler)($errors, $formatter); } } ``` Then register the service: #### Register the Error handler using Symfony ```yaml # api/config/services.yaml services: # ... App\Error\ErrorHandler: decorates: api_platform.graphql.error_handler ``` ```xml ``` ```php services(); $services->set(ErrorHandler::class) ->decorate('api_platform.graphql.error_handler'); }; ``` #### Register the Error handler using Laravel ```php app->extend(ErrorHandler::class, function (ErrorHandler $errorHandler) { return new ErrorHandlerDecorated($errorHandler); }); } } ``` ### Formatting Exceptions and Errors By default, if an exception is sent when resolving a query or a mutation or if there are GraphQL errors, they are normalized following the [GraphQL specification](https://github.com/graphql/graphql-spec/blob/master/spec/Section%207%20--%20Response.md#errors). It means an `errors` entry will be returned in the response, containing the following entries: `message`, `extensions`, `locations` and `path`. For more information, please [refer to the documentation in graphql-php](https://webonyx.github.io/graphql-php/error-handling/#default-error-formatting). In `prod` mode, the displayed message will be a generic one, excepted for a `RuntimeException` (and all exceptions inherited from it) for which it will be its actual message. This behavior is different from what is described in the [graphql-php documentation](https://webonyx.github.io/graphql-php/error-handling). It's because a built-in [custom exception normalizer](#custom-exception-normalizer) is used to normalize the `RuntimeException` and change the default behavior. If you are in `dev` mode, more entries will be added in the response: `debugMessage` (containing the actual exception message, for instance in the case of a `LogicException`) and `trace` (the formatted exception trace). For some specific exceptions, built-in [custom exception normalizers](#custom-exception-normalizer) are also used to add more information. It's the case for a `HttpException` for which the `status` entry will be added under `extensions` and for a `ValidationException` for which `status` (by default 422) and `violations` entries will be added. #### Custom Exception Normalizer If you want to add more specific behaviors depending on the exception or if you want to change the behavior of the built-in ones, you can do so by creating your own normalizer. Please follow the [Symfony documentation to create a custom normalizer](https://symfony.com/doc/current/serializer/custom_normalizer.html). The code should look like this: ```php getPrevious(); $error = FormattedError::createFromException($object); // Add your logic here and add your specific data in the $error array (in the 'extensions' entry to follow the GraphQL specification). // $error['extensions']['yourEntry'] = ...; return $error; } public function supportsNormalization($data, $format = null): bool { return $data instanceof Error && $data->getPrevious() instanceof MyException; } } ``` You can see that, in the `normalize` method, you should add a call to `FormattedError::createFromException` in order to have the same behavior as the other normalizers. When registering your custom normalizer, you can add a priority to order your normalizers between themselves. If you use a positive priority (or no priority), your normalizer will always be called before the built-in normalizers. For instance, you can register a custom normalizer like this: ```yaml # api/config/services.yaml services: App\Serializer\Exception\MyExceptionNormalizer: tags: - { name: "serializer.normalizer", priority: 12 } ``` ## Name Conversion You can modify how the property names of your resources are converted into field and filter names of your GraphQL schema. By default, the property name will be used without conversion. If you want to apply a name converter, follow the [Name Conversion documentation](serialization.md#name-conversion). For instance, your resource can have properties in camelCase: ```php 'partial'])] class Book { // ... public $publicationDate; // ... } ``` By default, with the search filter, the query to retrieve a collection will be: ```graphql { books(publicationDate: "2010") { edges { node { publicationDate } } } } ``` But if you use the `CamelCaseToSnakeCaseNameConverter`, it will be: ```graphql { books(publication_date: "2010") { edges { node { publication_date } } } } ``` ### Nesting Separator If you use snake_case, you can wonder how to make the difference between an underscore and the separator of the nested fields in the filter names, by default an underscore too. For instance if you have this resource: ```php 'exact'])] class Book { // ... public $title; #[ORM\OneToMany(targetEntity: Book::class)] public $relatedBooks; // ... } ``` You would need to use the search filter like this: ```graphql { books(related_books_title: "The Fitz and the Fool") { edges { node { title } } } } ``` To avoid this issue, you can configure the nesting separator to use, for example, `__` instead of `_`: #### Modifying nesting separator for GraphQL with Symfony ```yaml # api/config/packages/api_platform.yaml api_platform: graphql: nesting_separator: __ # ... ``` In this case, your query will be: ```graphql { books(related_books__title: "The Fitz and the Fool") { edges { node { title } } } } ``` #### Modifying nesting separator for GraphQL with Laravel ```php [ 'nesting_separator' => '__' ], ]; ``` In this case, your query will be: ```graphql { books(related_books__title: "The Fitz and the Fool") { edges { node { title } } } } ``` Much better, isn't it? ## Custom Types You might need to add your own types to your GraphQL application. Create your type class by implementing the interface `ApiPlatform\GraphQl\Type\Definition\TypeInterface`. You should extend the `GraphQL\Type\Definition\ScalarType` class too to take advantage of its useful methods. For instance, to create a custom `DateType`: ```php name = 'DateTime'; $this->description = 'The `DateTime` scalar type represents time data.'; parent::__construct(); } public function getName(): string { return $this->name; } public function serialize($value) { // Already serialized. if (\is_string($value)) { return (new \DateTime($value))->format('Y-m-d'); } if (!($value instanceof \DateTime)) { throw new Error(sprintf('Value must be an instance of DateTime to be represented by DateTime: %s', Utils::printSafe($value))); } return $value->format(\DateTime::ATOM); } public function parseValue($value) { if (!\is_string($value)) { throw new Error(sprintf('DateTime cannot represent non string value: %s', Utils::printSafeJson($value))); } if (false === \DateTime::createFromFormat(\DateTime::ATOM, $value)) { throw new Error(sprintf('DateTime cannot represent non date value: %s', Utils::printSafeJson($value))); } // Will be denormalized into a \DateTime. return $value; } public function parseLiteral($valueNode, ?array $variables = null) { if ($valueNode instanceof StringValueNode && false !== \DateTime::createFromFormat(\DateTime::ATOM, $valueNode->value)) { return $valueNode->value; } // Intentionally without message, as all information already in wrapped Exception throw new \Exception(); } } ``` You can also check the documentation of [graphql-php](https://webonyx.github.io/graphql-php/type-definitions/scalars/#writing-custom-scalar-types). The big difference in API Platform is that the value is already serialized when it's received in your type class. Similarly, you would not want to denormalize your parsed value since it will be done by API Platform later. ### Custom Types config for Symfony If you use autoconfiguration (the default Symfony configuration) in your application, then you are done! Else, you need to tag your type class like this, if you're using Symfony : ```yaml # api/config/services.yaml services: # ... App\Type\Definition\DateTimeType: tags: - { name: api_platform.graphql.type } ``` Your custom type is now registered and is available in the `TypesContainer`. To use it please [modify the extracted types](#modify-the-extracted-types) or use it directly in [custom queries](#custom-queries) or [custom mutations](#custom-mutations). ### Custom Types config for Laravel If you are using Laravel tag your type with: ```php app->tag([DateTimeType::class], TypeInterface::class); } } ``` Your custom type is now registered and is available in the `TypesContainer`. To use it please [modify the extracted types](#modify-the-extracted-types) or use it directly in [custom queries](#custom-queries) or [custom mutations](#custom-mutations). ## Modify the Extracted Types The GraphQL schema and its types are extracted from your resources. In some cases, you would want to modify the extracted types for instance to use your custom ones. To do so, you need to decorate the `api_platform.graphql.type_converter` service: ### Symfony TypeConverter Decoration ```yaml # api/config/services.yaml services: # ... 'App\Type\TypeConverter': decorates: api_platform.graphql.type_converter ``` ### Laravel TypeConverter Decoration ```php app->extend(TypeConverterInterface::class, function (TypeConverterInterface $typeConverter) { return new TypeConverter($typeConverter); }); } } ``` Then, your class needs to look like this: ```php defaultTypeConverter->convertType($type, $input, $rootOperation, $resourceClass, $rootResource, $property, $depth); } public function resolveType(string $type): ?GraphQLType { return $this->defaultTypeConverter->resolveType($type); } } ``` In this case, the `publicationDate` property of the `Book` class will have a custom `DateTime` type. You can even apply this logic for a kind of property. Replace the previous condition with something like this: ```php if (Type::BUILTIN_TYPE_OBJECT === $type->getBuiltinType() && is_a($type->getClassName(), \DateTimeInterface::class, true) ) { return 'DateTime'; } ``` All `DateTimeInterface` properties will have the `DateTime` type in this example. ## Changing the Serialization Context Dynamically [As REST](serialization.md#changing-the-serialization-context-dynamically), it's possible to add dynamically a (de)serialization group when resolving a query or a mutation. There are some differences though. The service is `api_platform.graphql.serializer.context_builder` and the method to override is `create`. The decorator could be like this: ### Symfony Serialization Context Decoration ```php decorated->create($resourceClass, $operationName, $resolverContext, $normalization); $resourceClass = $context['resource_class'] ?? null; if ($resourceClass === Book::class && isset($context['groups']) && $this->authorizationChecker->isGranted('ROLE_ADMIN') && false === $normalization) { $context['groups'][] = 'admin:input'; } return $context; } } ``` ### Laravel Serialization Context Decoration ```php decorated->create($resourceClass, $operationName, $resolverContext, $normalization); $resourceClass = $context['resource_class'] ?? null; if ($resourceClass === Book::class && isset($context['groups']) && $this->isAdmin() && !$normalization) { $context['groups'][] = 'admin:input'; } return $context; } private function isAdmin(): bool { $user = Auth::user(); return $user && $user->role === 'admin'; } } ``` ## Export the Schema in SDL > [!WARNING] This command is not yet available with Laravel, you're welcome to contribute > [on GitHub](https://github.com/api-platform/core) You may need to export your schema in SDL (Schema Definition Language) to import it in some tools. The `api:graphql:export` command is provided to do so: ```shell-session bin/console api:graphql:export -o path/to/your/volume/schema.graphql ``` Since the command prints the schema to the output if you don't use the `-o` option, you can also use this command: ```shell-session bin/console api:graphql:export > path/in/host/schema.graphql ``` ## Handling File Upload with Symfony If you use Symfony, please follow the [file upload documentation](../symfony/file-upload.md), only the differences will be documented here. The file upload with GraphQL follows the [GraphQL multipart request specification](https://github.com/jaydenseric/graphql-multipart-request-spec). You can also upload multiple files at the same time. ### Configuring the Entity Receiving the Uploaded File Configure the entity by adding a [custom mutation resolver](#custom-mutations): ```php ['media_object_read']], types: ['https://schema.org/MediaObject'], graphQlOperations: [ new Mutation( name: 'upload', resolver: CreateMediaObjectResolver::class, deserialize: false, args: [ 'file' => [ 'type' => 'Upload!', 'description' => 'The file to upload' ] ] ) ] )] class MediaObject { #[ORM\Id, ORM\Column, ORM\GeneratedValue] protected ?int $id = null; #[ApiProperty(types: ['https://schema.org/contentUrl'])] #[Groups(['media_object_read'])] public ?string $contentUrl = null; /** * @Vich\UploadableField(mapping="media_object", fileNameProperty="filePath") */ #[Assert\NotNull(groups: ['media_object_create'])] public ?File $file = null; #[ORM\Column(nullable: true)] public ?string $filePath = null; public function getId(): ?int { return $this->id; } } ``` As you can see, a dedicated type `Upload` is used in the argument of the `upload` mutation. If you need to upload multiple files, replace `'file' => ['type' => 'Upload!', 'description' => 'The file to upload']` with `'files' => ['type' => '[Upload!]!', 'description' => 'Files to upload']`. You don't need to create it, it's provided in API Platform. ### Resolving the File Upload The corresponding resolver you added in the resource configuration should be written like this: ```php file = $uploadedFile; return $mediaObject; } } ``` For handling the upload of multiple files, iterate over `$context['args']['input']['files']`. ### Normalization of MediaObjects In the constructor of the `MediaObjectNormalizer`, the injected Normalizer must be replaced with the one from the `api_platform.graphql.normalizer.item` from GraphQL: ````php [ // ... 'multipart' => ['multipart/form-data'] ], ]; ``` You can now upload files using the `createMediaObject` mutation, for details check [GraphQL multipart request specification](https://github.com/jaydenseric/graphql-multipart-request-spec) and for an example implementation for the Apollo client check out [Apollo Upload Client](https://github.com/jaydenseric/apollo-upload-client). ```graphql mutation CreateMediaObject($file: Upload!) { uploadMediaObject(input: { file: $file }) { mediaObject { id contentUrl } } } ``` ## Change Default Descriptions By default, API Platform generates descriptions for mutations and subscriptions. If you want to change them, or add some for queries, you can do it in the resource declaration, at the operation level, with the `description` attribute. For instance, if you want to change the description of the `create` mutation: ```php */ final class BlogPostProvider implements ProviderInterface { private array $data; public function __construct() { $this->data = [ 'ab' => new BlogPost('ab'), 'cd' => new BlogPost('cd'), ]; } public function provide(Operation $operation, array $uriVariables = [], array $context = []): BlogPost|null { return $this->data[$uriVariables['id']] ?? null; } } ``` For the example, we store the list of our blog posts in an associative array `$data`. As this operation expects a `BlogPost`, the `provide` methods return the instance of the `BlogPost` corresponding to the ID passed in the URL. If the ID doesn't exist in the associative array, `provide()` returns `null`. API Platform will automatically generate a 404 response if the provider returns `null`. The `$uriVariables` parameter contains an array with the values of the URI variables. To use this provider we need to configure the provider on the operation: ```php */ final class BlogPostProvider implements ProviderInterface { // … public function provide(Operation $operation, array $uriVariables = [], array $context = []): iterable|BlogPost|null { if ($operation instanceof CollectionOperationInterface) { return $this->data; } return $this->data[$uriVariables['id']] ?? null; } } ``` We then need to configure this same provider on the BlogPost `GetCollection` operation, or for every operation via the `ApiResource` attribute: ```php */ final class BlogPostProvider implements ProviderInterface { private array $data; public function __construct() { $this->data = [ 'ab' => new BlogPost('ab'), 'cd' => new BlogPost('cd'), ]; } public function provide(Operation $operation, array $uriVariables = [], array $context = []): BlogPost|null { return $this->data[$uriVariables['id']] ?? null; } } ``` For the example, we store the list of our blog posts in an associative array `$data`. As this operation expects a `BlogPost`, the `provide` methods return the instance of the `BlogPost` corresponding to the ID passed in the URL. If the ID doesn't exist in the associative array, `provide()` returns `null`. API Platform will automatically generate a 404 response if the provider returns `null`. The `$uriVariables` parameter contains an array with the values of the URI variables. To use this provider we need to configure the provider on the operation: ```php */ final class BlogPostProvider implements ProviderInterface { // … public function provide(Operation $operation, array $uriVariables = [], array $context = []): iterable|BlogPost|null { if ($operation instanceof CollectionOperationInterface) { return $this->data; } return $this->data[$uriVariables['id']] ?? null; } } ``` We then need to configure this same provider on the BlogPost `GetCollection` operation, or for every operation via the `ApiResource` attribute: ```php */ final class BookRepresentationProvider implements ProviderInterface { public function __construct( #[Autowire(service: 'api_platform.doctrine.orm.state.item_provider')] private ProviderInterface $itemProvider, ) { } public function provide(Operation $operation, array $uriVariables = [], array $context = []): AnotherRepresentation { $book = $this->itemProvider->provide($operation, $uriVariables, $context); return new AnotherRepresentation( // Add DTO constructor params here. // $book->getTitle(), ); } } ``` And configure that you want to use this provider on the Book resource: ```php */ final class BookRepresentationProvider implements ProviderInterface { public function __construct( private ProviderInterface $itemProvider, ) { } public function provide(Operation $operation, array $uriVariables = [], array $context = []): AnotherRepresentation { $book = $this->itemProvider->provide($operation, $uriVariables, $context); return new AnotherRepresentation( // Add DTO constructor params here. // $book->getTitle(), ); } } ``` And we bind the [ItemProvider](https://github.com/api-platform/core/blob/main/src/Laravel/Eloquent/State/ItemProvider.php) in our Service Provider ```php app->singleton(BookRepresentationProvider::class, function (Application $app) { return new BookRepresentationProvider( $app->make(ItemProvider::class), ); }); } //... } ``` Finally, configure that you want to use this provider on the Book resource: ```php */ final class BlogPostProcessor implements ProcessorInterface { /** * @return BlogPost|void */ public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed { // call your persistence layer to save $data return $data; } } ``` The `process()` method must return the created or modified object, or nothing (that's why `void` is allowed) for `DELETE` operations. The `process()` method can also take an object as input, in the `$data` parameter, that isn't of the same type that its output (the returned object). See [the DTO documentation entry](dto.md) for more details. We then configure our operation to use this processor: ```php */ final class BlogPostProcessor implements ProcessorInterface { /** * @return BlogPost|void */ public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed { // call your persistence layer to save $data return $data; } } ``` The `process()` method must return the created or modified object, or nothing (that's why `void` is allowed) for `DELETE` operations. The `process()` method can also take an object as input, in the `$data` parameter, that isn't of the same type that its output (the returned object). See [the DTO documentation entry](dto.md) for more details. We then configure our operation to use this processor: ```php ```php The corresponding processor receives the data returned by the provider and can transform or act on it: ```php */ final class CarProcessor implements ProcessorInterface { public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed { // $data is the collection returned by the provider. // Add side-effects or transformations here, then return the data. return $data; } } ``` ## Hooking into the Built-In State Processors ### Symfony State Processor mechanism If you want to execute custom business logic before or after persistence, this can be achieved by using [composition](https://en.wikipedia.org/wiki/Object_composition). Here is an implementation example which uses [Symfony Mailer](https://symfony.com/doc/current/mailer.html) to send new users a welcome email after a REST `POST` or GraphQL `create` operation, in a project using the native Doctrine ORM state processor: ```php */ final class UserProcessor implements ProcessorInterface { public function __construct( #[Autowire(service: 'api_platform.doctrine.orm.state.persist_processor')] private ProcessorInterface $persistProcessor, #[Autowire(service: 'api_platform.doctrine.orm.state.remove_processor')] private ProcessorInterface $removeProcessor, private MailerInterface $mailer, ) { } /** * @return User|void */ public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed { if ($operation instanceof DeleteOperationInterface) { return $this->removeProcessor->process($data, $operation, $uriVariables, $context); } $result = $this->persistProcessor->process($data, $operation, $uriVariables, $context); $this->sendWelcomeEmail($data); return $result; } private function sendWelcomeEmail(User $user): void { // Your welcome email logic... // $this->mailer->send(...); } } ``` The `Autowire` attribute is used to inject the built-in processor services registered by API Platform. If you're using Doctrine MongoDB ODM instead of Doctrine ORM, replace `orm` by `odm` in the name of the injected services. Finally, configure that you want to use this processor on the User resource: ```php */ final class UserProcessor implements ProcessorInterface { public function __construct( private ProcessorInterface $persistProcessor, private ProcessorInterface $removeProcessor, ) { } /** * @return User|void */ public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed { if ($operation instanceof DeleteOperationInterface) { return $this->removeProcessor->process($data, $operation, $uriVariables, $context); } $result = $this->persistProcessor->process($data, $operation, $uriVariables, $context); $this->sendWelcomeEmail($data); return $result; } private function sendWelcomeEmail(User $user): void { // Your welcome email logic... // Mail::to($user->getEmail())->send(new WelcomeMail($user)); } } ``` Next, we bind the [PersistProcessor](https://github.com/api-platform/core/blob/main/src/Laravel/Eloquent/State/PersistProcessor.php) and [RemoveProcessor](https://github.com/api-platform/core/blob/main/src/Laravel/Eloquent/State/RemoveProcessor.php) in our Service Provider: ```php app->tag(UserProcessor::class, ProcessorInterface::class); $this->app->singleton(UserProcessor::class, function (Application $app) { return new UserProcessor( $app->make(PersistProcessor::class), $app->make(RemoveProcessor::class), ); }); } // ... } ``` Finally, configure that you want to use this processor on the User resource: ```php Filtering and Searching screencast
Watch the Filtering & Searching screencast

> [!WARNING] For maximum flexibility and to ensure future compatibility, it is strongly recommended > to configure your filters via the parameters attribute using `QueryParameter`. The legacy method > using the `ApiFilter` attribute is not recommended. ## Declaring Parameters The recommended way to define parameters is by using Parameter attributes directly on a resource class or on an operation. API Platform provides two main types of Parameter attributes based on their location (matching the OpenAPI `in` configuration): - `ApiPlatform\Metadata\QueryParameter`: For URL query parameters (e.g., `?name=value`). - `ApiPlatform\Metadata\HeaderParameter`: For HTTP headers (e.g., `Custom-Header: value`). ### List of Available Filters When defining a `QueryParameter`, you must specify the filtering logic using the `filter` option. Here is a list of available filters you can use. You can pass the filter class name (recommended) or a new instance: - **`SortFilter`**: For sorting results by a single property. Designed exclusively for use with `QueryParameter`. Supports dot notation for nested/related properties and the `nullsComparison` option. Recommended over `OrderFilter` for new code. - Usage: `new QueryParameter(filter: new SortFilter(), property: 'name')` - **`DateFilter`**: For filtering by date intervals (e.g., `?createdAt[after]=...`). - Usage: `new QueryParameter(filter: DateFilter::class)` - **`ExactFilter`**: For exact value matching. Supports dot notation for nested properties. - Usage: `new QueryParameter(filter: ExactFilter::class)` - **`PartialSearchFilter`**: For partial string matching (SQL `LIKE %...%`). Supports dot notation for nested properties. - Usage: `new QueryParameter(filter: PartialSearchFilter::class)` - **`IriFilter`**: For filtering by IRIs (e.g., relations). Supports dot notation for nested associations. - Usage: `new QueryParameter(filter: IriFilter::class)` - **`ComparisonFilter`**: A decorator that wraps an equality filter (`ExactFilter`, `UuidFilter`) to add `gt`, `gte`, `lt`, `lte`, and `ne` operators. Replaces `DateFilter`, `NumericFilter`, and `RangeFilter` for comparison use cases. - Usage: `new QueryParameter(filter: new ComparisonFilter(new ExactFilter()), property: 'price')` - **`FreeTextQueryFilter`**: Applies a filter across multiple properties using a single parameter. - Usage: `new QueryParameter(filter: new FreeTextQueryFilter(new PartialSearchFilter()), properties: ['name', 'description'])` - **`OrFilter`**: A decorator that forces a filter to combine criteria with `OR` instead of `AND`. - Usage: `new QueryParameter(filter: new OrFilter(new ExactFilter()), properties: ['name', 'ean'])` - **`BooleanFilter`**: For boolean field filtering (legacy, `ExactFilter` is recommended instead). - Usage: `new QueryParameter(filter: BooleanFilter::class)` - **`NumericFilter`**: For numeric field filtering (legacy, `ExactFilter` or `ComparisonFilter` is recommended instead). - Usage: `new QueryParameter(filter: NumericFilter::class)` - **`RangeFilter`**: For range-based filtering (legacy, `ComparisonFilter` is recommended instead). - Usage: `new QueryParameter(filter: RangeFilter::class)` - **`ExistsFilter`**: For checking existence of nullable values. - Usage: `new QueryParameter(filter: ExistsFilter::class)` - **`OrderFilter`**: For sorting results (legacy, `SortFilter` is recommended instead). - Usage: `new QueryParameter(filter: OrderFilter::class)` > [!TIP] Always check the specific documentation for your persistence layer (Doctrine ORM, MongoDB > ODM, Laravel Eloquent) to see the exact namespace and available options for these filters. ### How Modern Filters Work Modern filters (those that do **not** extend `AbstractFilter`) are designed around a clear separation between **metadata time** and **runtime**. **At metadata time** (`ParameterResourceMetadataCollectionFactory`), when the application boots: - **`:property` placeholders are expanded**: A parameter key like `'search[:property]'` is expanded into one concrete parameter per property (e.g., `search[title]`, `search[author]`). Properties are auto-discovered from the entity or explicitly listed via the `properties` option. - **Nested property paths are resolved**: Dot-notation properties (e.g., `author.name`) are validated against entity metadata and association chains are stored so they don't need to be re-resolved on every request. - **OpenAPI and JSON Schema documentation is extracted**: Filters implementing `OpenApiParameterFilterInterface` or `JsonSchemaFilterInterface` provide their documentation once during metadata collection. **At runtime** (`ParameterExtension`), when a request comes in: - The extension simply reads the parameter value from the request, injects dependencies (`ManagerRegistry`, `Logger`) if needed, and calls `$filter->apply()`. All the metadata work has already been done. This design has two benefits for developers: 1. **Less boilerplate**: You declare `'price' => new QueryParameter(filter: new ComparisonFilter(new ExactFilter()))` and the framework handles property discovery, OpenAPI documentation, JSON Schema generation, and association traversal automatically. 2. **Better performance**: Expensive metadata operations (property resolution, association chain walking, schema generation) happen once at boot time and are cached, keeping the per-request hot path minimal. Legacy filters extending `AbstractFilter` predate this architecture. They mix metadata concerns with runtime logic and require service injection (`ManagerRegistry`, `NameConverter`) that can fail when instantiated with `new` inside an attribute. See the [migration guide](doctrine-filters.md#migrating-from-apifilter-to-queryparameter) for how to upgrade. ### Global Default Parameters Instead of repeating the same parameter configuration on every resource, you can define global default parameters that are automatically applied to all resources. This is done via the `defaults` key in your API Platform configuration. Add a `parameters` map under `defaults` in your API Platform configuration. Each entry maps a fully-qualified parameter class name to its options. ```yaml # Symfony: api/config/packages/api_platform.yaml api_platform: defaults: parameters: ApiPlatform\Metadata\HeaderParameter: key: "API-Token" required: true description: "API authentication token" ApiPlatform\Metadata\QueryParameter: key: "api_version" required: false description: "API version" ``` ```php [ 'parameters' => [ \ApiPlatform\Metadata\HeaderParameter::class => [ 'key' => 'API-Token', 'required' => true, 'description' => 'API authentication token', ], \ApiPlatform\Metadata\QueryParameter::class => [ 'key' => 'api_version', 'required' => false, 'description' => 'API version', ], ], ], ]; ``` Every resource will automatically expose these parameters on all their operations. You can declare a parameter on the resource class to make it available for all its operations: ```php new QueryParameter(description: 'Filter our friends by name'), 'Request-ID' => new HeaderParameter(description: 'A unique request identifier') // keys are case insensitive ] ) ] )] class Friend { // ... } ``` ### Using Filters with DateTime Properties When working with `DateTime` or `DateTimeImmutable` properties, the system might default to exact matching. To enable date comparison operators (`gt`, `gte`, `lt`, `lte`), use `ComparisonFilter` wrapping `ExactFilter`: ```php new QueryParameter( filter: new ComparisonFilter(new ExactFilter()), property: 'startDate', ), 'endDate' => new QueryParameter( filter: new ComparisonFilter(new ExactFilter()), property: 'endDate', ), ] ) ])] class Event { // ... } ``` This configuration allows clients to filter events by date ranges using queries like: - `/events?startDate[gte]=2023-01-01` — events starting on or after January 1st 2023 - `/events?endDate[lt]=2023-12-31` — events ending before December 31st 2023 - `/events?startDate[gte]=2023-01-01&endDate[lte]=2023-12-31` — events within a date range ### Filtering a Single Property Most of the time, a parameter maps directly to a property on your resource. For example, a `?name=Frodo` query parameter would filter for resources where the `name` property is "Frodo". This behavior is often handled by built-in or custom filters that you link to the parameter. For Hydra, you can map a query parameter to `hydra:freetextQuery` to indicate a general-purpose search query. ```php new QueryParameter(property: 'hydra:freetextQuery', required: true) ] ) ])] class Issue {} ``` This will generate the following Hydra `IriTemplateMapping`: ```json { "@context": "http://www.w3.org/ns/hydra/context.jsonld", "@type": "IriTemplate", "template": "http://api.example.com/issues{?q}", "variableRepresentation": "BasicRepresentation", "mapping": [ { "@type": "IriTemplateMapping", "variable": "q", "property": "hydra:freetextQuery", "required": true } ] } ``` ### Filtering Multiple Properties with `:property` Sometimes you need a generic filter that can operate on multiple properties. You can achieve this by using the `:property` placeholder in the parameter's `key`. ```php new QueryParameter( filter: 'api_platform.doctrine.orm.search_filter.instance' ) ] ) ])] class Book { // ... } ``` This configuration creates a dynamic parameter. API clients can now filter on any of the properties configured in the `SearchFilter` (in this case, `title` and `description`) by using a URL like `/books?search[title]=Ring` or `/books?search[description]=journey`. When using the `:property` placeholder, API Platform automatically creates as many parameters as there are properties. Each filter will be called by each detected parameter: ```php public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void { $parameter = $context['parameter'] ?? null; dump(key: $parameter->getKey(), value: $parameter->getValue()); // shows key: search[title], value: Ring } ``` > [!NOTE] We are using `api_platform.doctrine.orm.search_filter.instance` (exists also for ODM). > Indeed this is a special instance of the search filter where `properties` can be changed during > runtime. This is considered as "legacy filter" below, in API Platform 4.0 we'll recommend to > create a custom filter or to use the `PartialSearchFilter`. ### Restricting Properties with `:property` Placeholders Filters that work on a per-parameter basis can also use the `:property` placeholder and use the parameter's `properties` configuration: ```php new QueryParameter( properties: ['title', 'author'], // Only these properties get parameters created, defaults to all properties filter: new PartialSearchFilter() ) ] ) ])] class Book { // ... } ``` This will create 2 parameters: `search[title]` and `search[author]`, here is an example of the associated filter for Doctrine ORM: ```php getValue(); // Get the property for this specific parameter $property = $parameter->getProperty(); $alias = $queryBuilder->getRootAliases()[0]; $field = $alias.'.'.$property; $parameterName = $queryNameGenerator->generateParameterName($property); $queryBuilder ->andWhere($queryBuilder->expr()->like('LOWER('.$field.')', ':'.$parameterName)) ->setParameter($parameterName, '%'.strtolower($value).'%'); } } ``` **How it works:** 1. API Platform creates individual parameters: `search[title]` and `search[author]` only 2. URLs like `/books?search[description]=foo` are ignored (no parameter exists) 3. Each parameter calls the filter with its specific property via `$parameter->getProperty()` 4. The filter processes only that one property This approach is recommended for new filters as it's more flexible and allows true property restriction via the parameter configuration. > [!NOTE] Invalid values are usually ignored by our filters, use [validation](#parameter-validation) > to trigger errors for wrong parameter values. ## OpenAPI and JSON Schema You have full control over how your parameters are documented in OpenAPI. ### Customizing the OpenAPI Parameter You can pass a fully configured `ApiPlatform\OpenApi\Model\Parameter` object to the `openApi` property of your parameter attribute. This gives you total control over the generated documentation. ```php new QueryParameter( schema: ['enum' => ['a', 'b'], 'uniqueItems' => true], castToArray: true, openApi: new OpenApiParameter(name: 'enum', in: 'query', style: 'deepObject') ) ] ) ])] class User {} ``` ### Using JSON Schema and Type Casting The `schema` property allows you to define validation rules using JSON Schema keywords. This is useful for simple validation like ranges, patterns, or enumerations. When you define a `schema`, API Platform can often infer the native PHP type of the parameter. For instance, `['type' => 'boolean']` implies a boolean. If you want to ensure the incoming string value (e.g., "true", "0") is cast to its actual native type before validation and filtering, set `castToNativeType` to `true`. ```php new QueryParameter( schema: ['type' => 'boolean'], castToNativeType: true ) ] ) ])] class Setting {} ``` If you need a custom validation function use the `castFn` property of the `Parameter` class. ## Parameter Validation You can enforce validation rules on your parameters using the `required` property or by attaching Symfony Validator constraints. ```php new QueryParameter( description: 'Filter by country code.', constraints: [new Assert\Country()] ), 'X-Request-ID' => new HeaderParameter( description: 'A unique request identifier.', required: true, constraints: [new Assert\Uuid()] ) ] ) ])] class User {} ``` > [!NOTE] When `castToNativeType` is enabled, API Platform infers type validation from the JSON > Schema. The `ApiPlatform\Validator\Util\ParameterValidationConstraints` trait can be used to automatically infer validation constraints from the JSON Schema and OpenAPI definitions of a parameter. Here is the list of validation constraints that are automatically inferred from the JSON Schema and OpenAPI definitions of a parameter. ### From OpenAPI Definition - **`allowEmptyValue`**: If set to `false`, a `Symfony\Component\Validator\Constraints\NotBlank` constraint is added. ### From JSON Schema (`schema` property) - **`minimum`** / **`maximum`**: - If both are set, a `Symfony\Component\Validator\Constraints\Range` constraint is added. - If only `minimum` is set, a `Symfony\Component\Validator\Constraints\GreaterThanOrEqual` constraint is added. - If only `maximum` is set, a `Symfony\Component\Validator\Constraints\LessThanOrEqual` constraint is added. - **`exclusiveMinimum`** / **`exclusiveMaximum`**: - If `exclusiveMinimum` is used, it becomes a `Symfony\Component\Validator\Constraints\GreaterThan` constraint. - If `exclusiveMaximum` is used, it becomes a `Symfony\Component\Validator\Constraints\LessThan` constraint. - **`pattern`**: Becomes a `Symfony\Component\Validator\Constraints\Regex` constraint. - **`minLength`** / **`maxLength`**: Becomes a `Symfony\Component\Validator\Constraints\Length` constraint. - **`multipleOf`**: Becomes a `Symfony\Component\Validator\Constraints\DivisibleBy` constraint. - **`enum`**: Becomes a `Symfony\Component\Validator\Constraints\Choice` constraint with the specified values. - **`minItems`** / **`maxItems`**: Becomes a `Symfony\Component\Validator\Constraints\Count` constraint (for arrays). - **`uniqueItems`**: If `true`, becomes a `Symfony\Component\Validator\Constraints\Unique` constraint (for arrays). - **`type`**: - If set to `'array'`, a `Symfony\Component\Validator\Constraints\Type('array')` constraint is added. - If `castToNativeType` is also `true`, the schema `type` will add a `Symfony\Component\Validator\Constraints\Type` constraint for `'boolean'`, `'integer'`, and `'number'` (as `float`). ### From the Parameter's `required` Property - **`required`**: If set to `true`, a `Symfony\Component\Validator\Constraints\NotNull` constraint is added. ### Strict Parameter Validation By default, API Platform allows clients to send extra query parameters that are not defined in the operation's `parameters`. To enforce a stricter contract, you can set `strictQueryParameterValidation` to `true` on an operation. If an unsupported parameter is sent, API Platform will return a 400 Bad Request error. ```php new QueryParameter(), ] ) ])] class StrictParameters {} ``` With this configuration, a request to `/strict_query_parameters?bar=test` will fail with a 400 error because `bar` is not a supported parameter. ### Property filter > [!NOTE] We strongly recommend using [Vulcain](https://vulcain.rocks) instead of this filter. > Vulcain is faster, allows a better hit rate, and is supported out of the box in the API Platform > Symfony variant. [!NOTE] When unsing JSON:API check out the > [specific SparseFieldset and Sort filters](./content-negotiation/#jsonapi-sparse-fieldset-and-sort-parameters) The property filter adds the possibility to select the properties to serialize (sparse fieldsets). Syntax: `?properties[]=&properties[][]=` You can add as many properties as you need. Enable the filter: ```php new QueryParameter(filter: PropertyFilter::class)] )] class Book { // ... } ``` Three arguments are available to configure the filter: - `parameterName` is the query parameter name (default `properties`) - `overrideDefaultProperties` allows to override the default serialization properties (default `false`) - `whitelist` properties whitelist to avoid uncontrolled data exposure (default `null` to allow all properties) Given that the collection endpoint is `/books`, you can filter the serialization properties with the following query: `/books?properties[]=title&properties[]=author`. If you want to include some properties of the nested "author" document, use: `/books?properties[]=title&properties[author][]=name`. ## Parameter Providers Parameter Providers are powerful services that can inspect, transform, or provide values for parameters. They can even modify the current `Operation` metadata on the fly. A provider is a class that implements `ApiPlatform\State\ParameterProviderInterface`. ### `IriConverterParameterProvider` This built-in provider takes an IRI string (e.g., `/users/1`) and converts it into the corresponding Doctrine entity object. It supports both single IRIs and arrays of IRIs. ```php new QueryParameter(provider: IriConverterParameterProvider::class), 'related' => new QueryParameter( provider: IriConverterParameterProvider::class, extraProperties: ['fetch_data' => true] // Forces fetching the entity data ), ], provider: [self::class, 'provideDummyFromParameter'], ) ])] class WithParameter { public static function provideDummyFromParameter(Operation $operation, array $uriVariables = [], array $context = []): object|array { // The value has been transformed from an IRI to an entity by the provider. $dummy = $operation->getParameters()->get('dummy')->getValue(); // If multiple IRIs were provided as an array, this will be an array of entities $related = $operation->getParameters()->get('related')->getValue(); return $dummy; } } ``` #### Configuration Options The `IriConverterParameterProvider` supports the following options in `extraProperties`: - **`fetch_data`**: Boolean (default: `false`) - When `true`, forces the IRI converter to fetch the actual entity data instead of just creating a reference. ### `ReadLinkParameterProvider` This provider must be enabled before it can be used. ```yaml api_platform: enable_link_security: true ``` This provider fetches a linked resource from a given identifier. This is useful when you need to load a related entity to use later, for example in your own state provider. When you have an API resource with a custom `uriTemplate` that includes parameters, the `ReadLinkParameterProvider` can automatically resolve the linked resource using the operation's URI template. This is particularly useful for nested resources or when you need to load a parent resource based on URI variables. ```php new Link(schema: ['type' => 'string', 'format' => 'uuid'], property: 'id'), ], parameters: [ 'dummy' => new QueryParameter( provider: ReadLinkParameterProvider::class, extraProperties: [ 'resource_class' => Dummy::class, 'uri_template' => '/dummies/{id}' // Optional: specify the template for the linked resource ] ) ], provider: [self::class, 'provideDummyFromParameter'], )] class WithParameter { public static function provideDummyFromParameter(Operation $operation, array $uriVariables = [], array $context = []): object|array { // The dummy parameter has been resolved to the actual Dummy entity // based on the parameter value and the specified uri_template return $operation->getParameters()->get('dummy')->getValue(); } } ``` The provider will: - Take the parameter value (e.g., a UUID or identifier) - Use the `resource_class` to determine which resource to load - Optionally use the `uri_template` from `extraProperties` to construct the proper operation for loading the resource - Return the loaded entity, making it available in your state provider #### ReadLinkParameterProvider Configuration Options You can control the behavior of `ReadLinkParameterProvider` with these `extraProperties`: - **`resource_class`**: The class of the resource to load - **`uri_template`**: Optional URI template for the linked resource operation - **`uri_variable`**: Name of the URI variable to use when building URI variables array - **`throw_not_found`**: Boolean (default: `true`) - Whether to throw `NotFoundHttpException` when resource is not found ```php 'dummy' => new QueryParameter( provider: ReadLinkParameterProvider::class, extraProperties: [ 'resource_class' => Dummy::class, 'throw_not_found' => false, // Won't throw NotFoundHttpException if resource is missing 'uri_variable' => 'customId' // Use 'customId' as the URI variable name ] ) ``` ### Array Support Both `IriConverterParameterProvider` and `ReadLinkParameterProvider` support processing arrays of values. When you pass an array of identifiers or IRIs, they will return an array of resolved entities: ```php // For IRI converter: ?related[]=/dummies/1&related[]=/dummies/2 // For ReadLink provider: ?dummies[]=uuid1&dummies[]=uuid2 'items' => new QueryParameter( provider: ReadLinkParameterProvider::class, extraProperties: ['resource_class' => Dummy::class] ) ``` ### Creating a Custom Parameter Provider You can create your own providers to implement any custom logic. A provider must implement `ParameterProviderInterface`. The `provide` method can modify the parameter's value or even return a modified `Operation` to alter the request handling flow. For instance, a provider could add serialization groups to the normalization context based on a query parameter: ```php getValue(); if ('extended' === $value) { $context = $operation->getNormalizationContext(); $context[AbstractNormalizer::GROUPS][] = 'extended_read'; return $operation->withNormalizationContext($context); } return $operation; } } ``` ### Changing how to parse Query / Header Parameters We use our own algorithm to parse a request's query, if you want to do the parsing of `QUERY_STRING` yourself, set `_api_query_parameters` in the Request attributes (`$request->attributes->set('_api_query_parameters', [])`) yourself. By default we use Symfony's `$request->headers->all()`, you can also set `_api_header_parameters` if you want to parse them yourself. ## Creating Custom Filters For data-provider-specific filtering (e.g., Doctrine ORM), the recommended way to create a filter is to implement the corresponding `FilterInterface`. For Doctrine ORM, your filter should implement `ApiPlatform\Doctrine\Orm\Filter\FilterInterface`: ```php getValue(); // The parameter may not be present. // It's recommended to add validation (e.g., `required: true`) on the Parameter attribute // if the filter logic depends on the value. if ($value instanceof ParameterNotFound) { return; } $alias = $queryBuilder->getRootAliases()[0]; $parameterName = $queryNameGenerator->generateParameterName('regexp_name'); // Access the parameter's property or use the parameter key as fallback $property = $parameter->getProperty() ?? $parameter->getKey() ?? 'name'; // You can also access filter context if the parameter provides it $filterContext = $parameter->getFilterContext() ?? null; $queryBuilder ->andWhere(sprintf('REGEXP(%s.%s, :%s) = 1', $alias, $property, $parameterName)) ->setParameter($parameterName, $value); } // For BC, this function is not useful anymore when documentation occurs on the Parameter public function getDescription(): array { return []; } } ``` You can then instantiate this filter directly in your `QueryParameter`: ```php new QueryParameter(filter: new RegexpFilter()) ] ) ])] class User {} ``` > [!NOTE] A `filter` is either an instanceof `FilterInterface` or a string referencing a filter > service. ## Parameter Attribute Reference | Property | Description | | ------------------ | --------------------------------------------------------------------------------------------------------------------- | | `key` | The name of the parameter (e.g., `name`, `order`). | | `filter` | The filter service or instance that processes the parameter's value. | | `provider` | A service that transforms the parameter's value before it's used. | | `description` | A description for the API documentation. | | `property` | The resource property this parameter is mapped to. | | `required` | Whether the parameter is required. | | `constraints` | Symfony Validator constraints to apply to the value. | | `schema` | A JSON Schema for validation and documentation. | | `castToArray` | Casts the parameter value to an array. Useful for query parameters like `foo[]=1&foo[]=2`. Defaults to `true`. | | `castToNativeType` | Casts the parameter value to its native PHP type based on the `schema`. | | `openApi` | Customize OpenAPI documentation or hide the parameter (`false`). | | `hydra` | Hide the parameter from Hydra documentation (`false`). | | `security` | A [Symfony expression](https://symfony.com/doc/current/security/expressions.html) to control access to the parameter. | ## Parameter Security You can secure individual parameters using Symfony expression language. When a security expression evaluates to `false`, the parameter will be ignored and treated as if it wasn't provided. ```php new QueryParameter( security: 'is_granted("ROLE_ADMIN")' ), 'auth' => new HeaderParameter( security: '"secured" == auth', description: 'Only accessible when auth header equals "secured"' ), 'secret' => new QueryParameter( security: '"secured" == secret', description: 'Only accessible when secret parameter equals "secured"' ) ] ) ])] class SecureResource { // ... } ``` In the security expressions, you have access to: - Parameter values by their key name (e.g., `auth`, `secret`) - Standard security functions like `is_granted()` - The current user via `user` - Request object via `request` --- # Elasticsearch & OpenSearch Filters Source: https://api-platform.com/docs/core/elasticsearch-filters/ # Elasticsearch & OpenSearch Filters > [!NOTE] All filters documented on this page work with both Elasticsearch and OpenSearch. See the > [Elasticsearch & OpenSearch documentation](elasticsearch.md) for setup instructions. For further documentation on filters (including for Eloquent and Doctrine), please see the [Filters documentation](filters.md). > [!WARNING] For maximum flexibility and to ensure future compatibility, it is strongly recommended > to configure your filters via the parameters attribute using `QueryParameter`. The legacy method > using the `ApiFilter` attribute is **deprecated** and will be **removed** in version **5.0**. The modern way to declare filters is to associate them directly with an operation's parameters. This allows for more precise control over the exposed properties. Here is the recommended approach to apply a `MatchFilter` only to the title and author properties of a Book resource. ```php new QueryParameter( properties: ['title', 'author'], // Only these properties get parameters created filter: new MatchFilter() ) ] ) ])] class Book { // ... } ``` > [!TIP] This filter can be also defined directly on a specific operation like > `#[GetCollection(...)])` for finer control, like the following code: ```php new QueryParameter( properties: ['title', 'author'], // Only these properties get parameters created filter: new matchFilter() ) ] )] class Book { // ... } ``` ## Further Reading - Consult the documentation on [Per-Parameter Filters (Recommended Method)](../core/filters.md#2-per-parameter-filters-recommended). - If you are working with a legacy codebase, you can refer to the [documentation for the old syntax (deprecated)](../core/filters.md#1-legacy-filters-searchfilter-etc---not-recommended). ## Ordering Filter (Sorting) The order filter allows to [sort](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-sort.html) a collection against the given properties. Syntax: `?order[property]=` Enable the filter: ```php 'order'])] class Tweet { // ... } ``` ```yaml # config/services.yaml services: tweet.order_filter: parent: "api_platform.doctrine.orm.order_filter" arguments: $properties: { id: ~, date: ~ } $orderParameterName: "order" tags: ["api_platform.filter"] # The following are mandatory only if a _defaults section is defined with inverted values. # You may want to isolate filters in a dedicated file to avoid adding the following lines (by adding them in the defaults section) autowire: false autoconfigure: false public: false # config/api/Tweet.yaml App\Entity\Tweet: # ... filters: ["tweet.order_filter"] ``` Given that the collection endpoint is `/tweets`, you can filter tweets by ID and date in ascending or descending order: `/tweets?order[id]=asc&order[date]=desc`. By default, whenever the query does not specify the direction explicitly (e.g: `/tweets?order[id]&order[date]`), filters will not be applied unless you configure a default order direction to use: ```php 'asc', 'date' => 'desc'])] class Tweet { // ... } ``` ### Using a Custom Order Query Parameter Name A conflict will occur if `order` is also the name of a property with the term filter enabled. Luckily, the query parameter name to use is configurable: ```yaml # api/config/packages/api_platform.yaml api_platform: collection: order_parameter_name: "_order" # the URL query parameter to use is now "_order" ``` ## Match Filter The match filter allows us to find resources that [match](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query.html) the specified text on full-text fields. Syntax: `?property[]=value` Enable the filter: ```php $context['filters']['fullName'], 'operator' => 'and', ]; $requestBody['query']['constant_score']['filter']['bool']['must'][0]['match']['full_name'] = $andQuery; return $requestBody; } } ``` --- # Doctrine ORM and MongoDB ODM Filters Source: https://api-platform.com/docs/core/doctrine-filters/ # Doctrine ORM and MongoDB ODM Filters ## Introduction For further documentation on filters (including for Eloquent and Elasticsearch), please see the [Filters documentation](filters.md). > [!WARNING] For maximum flexibility and to ensure future compatibility, it is strongly recommended > to configure your filters via the parameters attribute using `QueryParameter`. The legacy method > using the `ApiFilter` attribute is not recommended. The modern way to declare filters is to associate them directly with an operation's parameters. This allows for more precise control over the exposed properties. Here is the recommended approach to apply a `PartialSearchFilter` only to the title and author properties of a Book resource. ```php new QueryParameter( properties: ['title', 'author'], // Only these properties get parameters created filter: new PartialSearchFilter() ) ] ) ])] class Book { // ... } ``` > [!TIP] This filter can be also defined directly on a specific operation like > `#[GetCollection(...)])` for finer control, like the following code: ```php new QueryParameter( properties: ['title', 'author'], // Only these properties get parameters created filter: new PartialSearchFilter() ) ] )] class Book { // ... } ``` ## Basic Knowledge Filters are services (see the section on [custom filters](../core/filters.md#creating-custom-filters)), the can be linked to an API Platform Operation through [parameters](./filters.md): For example, having a filter service declaration in `services.yaml`: ```yaml # api/config/filters.yaml services: offer.date_filter: parent: "api_platform.doctrine.orm.date_filter" arguments: [{ dateProperty: ~ }] tags: ["api_platform.filter"] ``` We're linking the filter `offer.date_filter` with the resource like this: ```php new QueryParameter(filter: 'offer.date_filter')])] class Offer { // ... } ``` > [!WARNING] Its discouraged to use a filter with properties in the dependency injection as it may > conflict with how `QueryParameter` works. We recommend to use a per-parameter filter or to use the > :property placeholder with a defined `filterContext` specifying your strategy for a given set of > parameters. Since API platform 4.2 we're allowing singleton objects, indeed a filter now acts on a single parameter associated with a single scalar value (or a list). You may use the [`:property` placeholder](./filters.md#filtering-multiple-properties-with-property)) ```php new QueryParameter( filter: new DateFilter(), ), ], )] class Offer { // ... } ``` For MongoDB ODM, all the filters are in the namespace `ApiPlatform\Doctrine\Odm\Filter`. The filter services all begin with `api_platform.doctrine_mongodb.odm`. ## Search Filter > [!WARNING] The SearchFilter is a multi-type filter that may have inconsistencies (e.g., you can > search a partial date with LIKE). We recommend using type-specific filters such as `ExactFilter`, > `PartialSearchFilter`, `ComparisonFilter`, or `IriFilter` instead. See the > [migration guide](#migrating-from-apifilter-to-queryparameter). ### Built-in Search Filters since API Platform >= 4.2 To add some search filters, choose over this new list: - [SortFilter](#sort-filter) (sort a collection by a property; supports nested properties via dot notation) - [IriFilter](#iri-filter) (filter on IRIs; supports nested associations via dot notation) - [ExactFilter](#exact-filter) (filter with exact value; supports nested properties via dot notation) - [PartialSearchFilter](#partial-search-filter) (filter using a `LIKE %value%`; supports nested properties via dot notation) - [ComparisonFilter](#comparison-filter) (filter with comparison operators `gt`, `gte`, `lt`, `lte`, `ne`; replaces `DateFilter`, `NumericFilter`, and `RangeFilter`) - [FreeTextQueryFilter](#free-text-query-filter) (allows you to apply multiple filters to multiple properties of a resource at the same time, using a single parameter in the URL) - [OrFilter](#or-filter) (apply a filter using `orWhere` instead of `andWhere`) ### SearchFilter If Doctrine ORM or MongoDB ODM support is enabled, using the search filter service requires you to registering a filter service in the `api/config/services.yaml` file and adding an attribute to your resource configuration: ```yaml app_search_filter_via_parameter: parent: "api_platform.doctrine.orm.search_filter" arguments: [{ "id": "exact", "price": "exact", "description": "partial" }] # Declare strategies for each property tags: [{ name: "api_platform.filter", id: "app_search_filter_via_parameter" }] ``` The search filter supports `exact`, `partial`, `start`, `end`, and `word_start` matching strategies: - `partial` strategy uses `LIKE %text%` to search for fields that contain `text`. - `start` strategy uses `LIKE text%` to search for fields that start with `text`. - `end` strategy uses `LIKE %text` to search for fields that end with `text`. - `word_start` strategy uses `LIKE text% OR LIKE % text%` to search for fields that contain words starting with `text`. Prepend the letter `i` to the filter if you want it to be case insensitive. For example `ipartial` or `iexact`. Note that this will use the `LOWER` function and will impact performance [as described in the performance documentation](./performance#search-filter). Case insensitivity may already be enforced at the database level depending on the [collation](https://en.wikipedia.org/wiki/Collation) used. If you are using MySQL, note that the commonly used `utf8_unicode_ci` collation (and its sibling `utf8mb4_unicode_ci`) are already case-insensitive, as indicated by the `_ci` part in their names. Note: Search filters with the `exact` strategy can have multiple values for the same property (in this case the condition will be similar to a SQL IN clause). Syntax: `?property[]=foo&property[]=bar` In the following example, we will see how to allow the filtering of e-commerce offers (a list): ```php new QueryParameter(filter: new IriFilter()), ], )] class Chicken { //... } ``` Given that the endpoint is `/chickens`, you can filter chickens by chicken coop with the following query: `/chickens?chickenCoop=/chickenCoop/1`. It will return all the chickens that live in chicken coop number 1. `IriFilter` supports filtering through nested associations using dot notation in the `property` argument. See [Filtering on Nested Properties](#filtering-on-nested-properties). ## Exact Filter The exact filter allows filtering a resource using exact values. Syntax: `?property=value` The value can take any scalar value or array of values. This filter can be used on the ApiResource attribute or in the operation attribute, for e.g., the `#GetCollection()` attribute: ```php // api/src/ApiResource/Chicken.php #[GetCollection( parameters: [ 'name' => new QueryParameter(filter: new ExactFilter()), ], )] class Chicken { //... } ``` Given that the endpoint is `/chickens`, you can filter chickens by name with the following query: `/chikens?name=Gertrude`. It will return all the chickens that are exactly named _Gertrude_. `ExactFilter` supports filtering on nested properties using dot notation in the `property` argument. See [Filtering on Nested Properties](#filtering-on-nested-properties). ## Partial Search Filter The partial search filter allows filtering a resource using partial values. Syntax: `?property=value` The value can take any scalar value or array of values. This filter can be used on the ApiResource attribute or in the operation attribute, for e.g., the `#GetCollection()` attribute: ```php // api/src/ApiResource/Chicken.php #[GetCollection( parameters: [ 'name' => new QueryParameter(filter: new PartialSearchFilter()), ], )] class Chicken { //... } ``` Given that the endpoint is `/chickens`, you can filter chickens by name with the following query: `/chikens?name=tom`. It will return all chickens where the name contains the substring _tom_. > [!NOTE] This filter performs a case-insensitive search. It automatically normalizes both the input > value and the stored data (for e.g., by converting them to lowercase) before making the > comparison. `PartialSearchFilter` supports searching on nested properties using dot notation in the `property` argument. See [Filtering on Nested Properties](#filtering-on-nested-properties). ## Free Text Query Filter The free text query filter allows filtering allows you to apply a single filter across a list of properties. Its primary role is to repeat a filter's logic for each specified field. Syntax: `?property=value` The value can take any scalar value or array of values. This filter can be used on the ApiResource attribute or in the operation attribute, for e.g., the `#GetCollection()` attribute: ```php // api/src/ApiResource/Chicken.php #[GetCollection( parameters: [ 'q' => new QueryParameter( filter: new FreeTextQueryFilter(new PartialSearchFilter()), properties: ['name', 'ean'] ), ], )] class Chicken { //... } ``` Given that the endpoint is `/chickens`, you can filter chickens by name with the following query: `/chikens?q=tom`. **Result**: This request will return all chickens where: - the `name` is exactly "FR123456" - **AND** - the `ean` is exactly "FR123456". For the `OR` option refer to the [OrFilter](#or-filter). ## Or Filter The or filter allows you to explicitly change the logical condition used by the filter it wraps. Its sole purpose is to force a filter to combine its criteria with OR instead of the default AND. It's the ideal tool for creating a search parameter that should find a match in any of the specified fields, but not necessarily all of them. Syntax: `?property=value` The value can take any scalar value or array of values. The `OrFilter` is a decorator: it is used by "wrapping" another, more specific filter (like for e.g. `PartialSearchFilter` or `ExactFilter`). The real power emerges when you combine these decorators. For instance, to create an "autocomplete" feature that finds exact matches in one of several fields. Example of usage: ```php // api/src/ApiResource/Chicken.php #[GetCollection( parameters: [ 'autocomplete' => new QueryParameter( filter: new FreeTextQueryFilter(new OrFilter(new ExactFilter())), properties: ['name', 'ean'] ), ], )] class Chicken { //... } ``` Given that the endpoint is `/chickens`, you can filter chickens by name with the following query: `/chikens?autocomplete=tom`. **Result**: This request will return all chickens where: - the `name` is exactly "FR123456" - OR - the `ean` is exactly "FR123456". ## Comparison Filter > [!NOTE] `ComparisonFilter` is experimental and its API may change before a stable release. The comparison filter is a decorator that wraps an equality filter (such as `ExactFilter`) and adds comparison operators to it. It lets clients filter a collection using greater-than, greater-than-or-equal, less-than, less-than-or-equal, and not-equal comparisons on any filterable property. Syntax: `?parameter[]=value` Available operators: | Operator | SQL equivalent | Description | | -------- | -------------- | ------------------------ | | `gt` | `>` | Strictly greater than | | `gte` | `>=` | Greater than or equal to | | `lt` | `<` | Strictly less than | | `lte` | `<=` | Less than or equal to | | `ne` | `!=` | Not equal to | `ComparisonFilter` is a decorator: it is applied by wrapping another filter. The canonical pairing is with `ExactFilter` for standard properties, or with `UuidFilter` for UUID columns. It works for Doctrine ORM (`ApiPlatform\Doctrine\Orm\Filter\ComparisonFilter`) and Doctrine MongoDB ODM (`ApiPlatform\Doctrine\Odm\Filter\ComparisonFilter`). ```php new QueryParameter( filter: new ComparisonFilter(new ExactFilter()), property: 'price', ), ], )] class Product { // ... } ``` Given that the collection endpoint is `/products`, you can filter products by price range with the following queries: - `/products?price[gt]=10` — products whose price is strictly greater than 10 - `/products?price[gte]=10` — products whose price is greater than or equal to 10 - `/products?price[lt]=100` — products whose price is strictly less than 100 - `/products?price[lte]=100` — products whose price is less than or equal to 100 - `/products?price[ne]=0` — products whose price is not equal to 0 ### Range Queries (Combining Operators) There is no dedicated `between` operator. To filter within a range, combine `gte` and `lte` (or `gt` and `lt`) in a single request: ```http GET /products?price[gte]=10&price[lte]=100 ``` This returns all products whose price is between 10 and 100 inclusive. ### DateTime Support `ComparisonFilter` accepts `DateTimeInterface` values. When the underlying property is typed as a `DateTime` or `DateTimeImmutable`, API Platform automatically casts the raw string from the query string into a `DateTimeImmutable` before passing it to the filter. Any format accepted by the PHP [`DateTimeImmutable` constructor](https://www.php.net/manual/en/datetime.construct.php) is valid. ```php new QueryParameter( filter: new ComparisonFilter(new ExactFilter()), property: 'startDate', ), ], )] class Event { // ... } ``` Example request to fetch events starting after a given date: ```http GET /events?startDate[gt]=2025-01-01T00:00:00Z ``` ### UUID Support `ComparisonFilter` can also wrap `UuidFilter` to enable comparison operators on UUID columns. This is especially useful for cursor-based pagination on time-ordered UUIDs (UUID v7), where the lexicographic order of UUIDs matches their chronological order. ```php new QueryParameter( filter: new ComparisonFilter(new UuidFilter()), property: 'id', ), ], )] class Device { // ... } ``` Example requests: - `/devices?id[gt]=0192d4e0-7b5a-7a3f-9e1c-4b8f2a1c3d5e` — devices created after the given UUID - `/devices?id[gte]=...&id[lte]=...` — devices within a UUID range - `/devices?id[ne]=...` — exclude a specific device `UuidFilter` handles the conversion of UUID strings to their database binary representation via Doctrine's type system, which is required for correct comparisons on binary UUID columns. ### OpenAPI Documentation `ComparisonFilter` automatically generates five OpenAPI query parameters for each configured parameter key, one per operator. For a parameter named `price`, the generated parameters are `price[gt]`, `price[gte]`, `price[lt]`, `price[lte]`, and `price[ne]`. ## Date Filter > [!TIP] Consider using [`ComparisonFilter`](#comparison-filter) wrapping `ExactFilter` as a modern > replacement. `ComparisonFilter` does not extend `AbstractFilter`, works natively with > `QueryParameter`, and supports the same date comparison use cases with `gt`, `gte`, `lt`, `lte` > operators. The date filter allows filtering a collection by date intervals. Syntax: `?property[]=value` The value can take any date format supported by the [`\DateTime` constructor](https://www.php.net/manual/en/datetime.construct.php). The `after` and `before` filters will filter including the value whereas `strictly_after` and `strictly_before` will filter excluding the value. Like other filters, the Date Filter must be explicitly enabled: ### Date Filter using the QueryParameter Syntax (recommended) ```php new QueryParameter( filter: new DateFilter(), property: 'createdAt' // Facultative if you use the exact property name for the parameter name (for e.g., if you use "createdAt" instead of "created", the property is auto-discovered) ), ], )] class Offer { // ... } ``` > [!TIP] For other syntaxes, for e.g., if you want to new syntax with the ApiResource attribute take > a look [in the Introduction section](#introduction). ### Date Filter using the ApiFilter Attribute Syntax (not recommended) Basically the ApiFilter declares the correct service under the hood. We recommend to use `QueryParameter` as they're more declarative and hide less complexity. ```php ### Result using the Date Filter Given that the collection endpoint is `/offers`, you can filter offers by date with the following query: `/offers?createdAt[after]=2018-03-19`. It will return all offers where `createdAt` is superior or equal to `2018-03-19`. ### Managing `null` Values The date filter is able to deal with date properties having `null` values. Four behaviors are available at the property level of the filter: | Description | Strategy to set | | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------- | | Use the default behavior of the DBMS | `null` | | Exclude items | `ApiPlatform\Doctrine\Common\Filter\DateFilterInterface::EXCLUDE_NULL` (`exclude_null`) | | Consider items as oldest | `ApiPlatform\Doctrine\Common\Filter\DateFilterInterface::INCLUDE_NULL_BEFORE` (`include_null_before`) | | Consider items as youngest | `ApiPlatform\Doctrine\Common\Filter\DateFilterInterface::INCLUDE_NULL_AFTER` (`include_null_after`) | | Always include items | `ApiPlatform\Doctrine\Common\Filter\DateFilterInterface::INCLUDE_NULL_BEFORE_AND_AFTER` (`include_null_before_and_after`) | For instance, exclude entries with a property value of `null` with the following service definition: #### Managing `null` Values with the Date Filter using the QueryParameter Syntax (recommended) ```php new QueryParameter( filter: new DateFilter(), filterContext: DateFilterInterface::EXCLUDE_NULL, ), ])] class Offer { // ... } ``` Or you can also use the `properties` attribute on the `DateFilter` to apply your [`null` strategy](#managing-null-values): ```php new QueryParameter( filter: new DateFilter(properties: ['dateProperty' => DateFilterInterface::EXCLUDE_NULL]), ), ])] class Offer { // ... } ``` > [!TIP] For other syntaxes, for e.g., if you want to new syntax with the ApiResource attribute take > a look [in the Introduction section](#introduction). ## Boolean Filter > [!TIP] Consider using [`ExactFilter`](#exact-filter) as a modern replacement. `ExactFilter` does > not extend `AbstractFilter` and works natively with `QueryParameter`. The boolean filter allows you to search on boolean fields and values. Syntax: `?property=` Enable the filter: ### Boolean Filter using the QueryParameter Syntax ```php new QueryParameter(filter: new BooleanFilter()), ] )] class Offer { // ... } ``` > [!TIP] For other syntaxes, for e.g., if you want to new syntax with the ApiResource attribute take > a look [in the Introduction section](#introduction). ### Result using the Boolean Filter Given that the collection endpoint is `/offers`, you can filter offers with the following query: `/offers?isAvailableGenericallyInMyCountry=true`. It will return all offers where `isAvailableGenericallyInMyCountry` equals `true`. ## Numeric Filter > [!TIP] For comparison operations on numeric fields, consider using > [`ComparisonFilter`](#comparison-filter) wrapping `ExactFilter`. `ComparisonFilter` does not > extend `AbstractFilter`, works natively with `QueryParameter`, and provides `gt`, `gte`, `lt`, > `lte`, and `ne` operators. For exact numeric matching, `ExactFilter` alone is sufficient. The numeric filter allows you to search on numeric fields and values. Syntax: `?property=` Enable the filter: ### Numeric Filter using the QueryParameter Syntax ```php new QueryParameter(filter: new NumericFilter()), ] )] class Offer { // ... } ``` > [!TIP] For other syntaxes, for e.g., if you want to new syntax with the ApiResource attribute take > a look [in the Introduction section](#introduction). ### Result using the Numeric Filter Given that the collection endpoint is `/offers`, you can filter offers with the following query: `/offers?sold=1`. It will return all offers with `sold` equals `1`. ## Range Filter > [!TIP] Consider using [`ComparisonFilter`](#comparison-filter) wrapping `ExactFilter` as a modern > replacement. `ComparisonFilter` does not extend `AbstractFilter`, works natively with > `QueryParameter`, and supports range queries by combining `gte` and `lte` operators (e.g., > `?price[gte]=10&price[lte]=100`). The range filter allows you to filter by a value lower than, greater than, lower than or equal, greater than or equal and between two values. Syntax: `?property[]=value` Enable the filter: ### Range Filter using the QueryParameter Syntax ```php new QueryParameter(filter: new RangeFilter()), ] )] class Offer { // ... } ``` > [!TIP] For other syntaxes, for e.g., if you want to new syntax with the ApiResource attribute take > a look [in the Introduction section](#introduction). ### Result using the Range Filter Given that the collection endpoint is `/offers`, you can filter the price with the following query: `/offers?price[between]=12.99..15.99`. It will return all offers with `price` between 12.99 and 15.99. You can filter offers by joining two values, for example: `/offers?price[gt]=12.99&price[lt]=19.99`. ## Exists Filter The "exists" filter allows you to select items based on a nullable field value. It will also check the emptiness of a collection association. Syntax: `?exists[property]=` Enable the filter: ### Exists Filter using the QueryParameter Syntax ```php new QueryParameter(filter: new ExistsFilter()), ] )] class Offer { // ... } ``` > [!TIP] For other syntaxes, for e.g., if you want to new syntax with the ApiResource attribute take > a look [in the Introduction section](#introduction). ### Result using the Exists Filter Given that the collection endpoint is `/offers`, you can filter offers on the nullable field with the following query: `/offers?exists[transportFees]=true`. It will return all offers where `transportFees` is not `null`. ### Using a Custom Exists Query Parameter Name (deprecated) > [!TIP] Since API Platform 4.2 defined the query parameter yourself and you don't need the above > configuration. A conflict will occur if `exists` is also the name of a property with the search filter enabled. Luckily, the query parameter name to use is configurable: ```yaml # api/config/packages/api_platform.yaml api_platform: collection: exists_parameter_name: "not_null" # the URL query parameter to use is now "not_null" ``` ## Order Filter (Sorting) > [!TIP] Consider using [`SortFilter`](#sort-filter) as a modern replacement. `SortFilter` does not > extend `AbstractFilter` and works natively with `QueryParameter`. The order filter allows sorting a collection against the given properties. Syntax: `?order[property]=` Enable the filter: ### Order Filter using the QueryParameter Syntax ```php new QueryParameter(filter: new OrderFilter()), ] )] class Offer { // ... } ``` Or you can define one Query Parameter `'order[:property]'`, which uses an Order Filter and allow you to sort on all available properties, thanks to this code: ```php new QueryParameter(filter: new OrderFilter()), ] )] class Offer { // ... } ``` After that, you can use it with the following query: `/offers?order[name]=desc&order[id]=asc`. > [!TIP] For other syntaxes, for e.g., if you want to new syntax with the ApiResource attribute take > a look [in the Introduction section](#introduction). ### Result using the Order Filter Given that the collection endpoint is `/offers`, you can filter offers by name in ascending order and then by ID in descending order with the following query: `/offers?order[name]=desc&order[id]=asc`. ### Basic Directions Strategies with the Order Filter By default, whenever the query does not specify the direction explicitly (e.g.: `/offers?order[name]&order[id]`), filters will not be applied unless you configure a default order direction to use: #### Basic Strategies | Description | Strategy to set | | ----------- | ---------------------------------------------------------------------------------- | | Ascending | `ApiPlatform\Doctrine\Common\Filter\OrderFilterInterface::DIRECTION_DESC` (`DESC`) | | Descending | `ApiPlatform\Doctrine\Common\Filter\OrderFilterInterface::DIRECTION_ASC` (`ASC`) | #### Other Strategies For other sort strategies (about `null` values), please refer to the [Handling Null Values with the Order Filter section](#comparing-with-null-values-using-order-filter). #### Order Filter Direction using the QueryParameter Syntax ```php new QueryParameter(filter: new OrderFilter(), filterContext: OrderFilterInterface::DIRECTION_ASC ), 'name' => new QueryParameter(filter: new OrderFilter(), filterContext: OrderFilterInterface::DIRECTION_DESC), ] )] class Offer { // ... } ``` Or you can also use the `properties` attribute on the `OrderFilter` to apply your [`direction` strategy](#basic-directions-strategies-with-the-order-filter): ```php new QueryParameter(filter: new OrderFilter(properties: ['id' => OrderFilterInterface::DIRECTION_ASC])), 'name' => new QueryParameter(filter: new OrderFilter(properties: ['name' => OrderFilterInterface::DIRECTION_DESC])), ] )] class Offer { // ... } ``` ### Comparing with Null Values using Order Filter When the property used for ordering can contain `null` values, you may want to specify how `null` values are treated in the comparison: | Description | Strategy to set | | ------------------------------------ | ---------------------------------------------------------------------------------------------------- | | Use the default behavior of the DBMS | `null` | | Consider items as smallest | `ApiPlatform\Doctrine\Common\Filter\OrderFilterInterface::NULLS_SMALLEST` (`nulls_smallest`) | | Consider items as largest | `ApiPlatform\Doctrine\Common\Filter\OrderFilterInterface::NULLS_LARGEST` (`nulls_largest`) | | Order items always first | `ApiPlatform\Doctrine\Common\Filter\OrderFilterInterface::NULLS_ALWAYS_FIRST` (`nulls_always_first`) | | Order items always last | `ApiPlatform\Doctrine\Common\Filter\OrderFilterInterface::NULLS_ALWAYS_LAST` (`nulls_always_last`) | > [!TIP] For other sort strategies (including `ASC` and `DESC`), please refer to the > [Handling Basic Directions with the Order Filter section](#basic-directions-strategies-with-the-order-filter). For instance, treat entries with a property value of `null` as the smallest, with the following service definition: ### Comparing with Null Values using Order Filter using the Query Parameter Syntax ```php new QueryParameter( filter: new OrderFilter(), property: 'validFrom', filterContext: OrderFilterInterface::NULLS_SMALLEST, ), 'default_direction' => new QueryParameter( filter: new OrderFilter(), property: 'validFrom', filterContext: OrderFilterInterface::DIRECTION_DESC, ), ] )] class Offer { // ... } ``` ## Sort Filter The `SortFilter` is a parameter-based filter designed exclusively for use with `QueryParameter`. Unlike the [`OrderFilter`](#order-filter-sorting), it does not extend `AbstractFilter` and works with a single parameter per sorted property. This makes it straightforward to declare sort parameters with full control over naming and behavior. **ORM**: `ApiPlatform\Doctrine\Orm\Filter\SortFilter` **ODM**: `ApiPlatform\Doctrine\Odm\Filter\SortFilter` ### Basic Usage Each `QueryParameter` using `SortFilter` controls sorting for one property. The filter accepts `asc`, `desc`, `ASC`, and `DESC` as values. Any other value causes a 422 validation error, because the filter publishes a JSON Schema `enum` constraint automatically. ```php new QueryParameter(filter: new SortFilter(), property: 'name'), 'orderDate' => new QueryParameter(filter: new SortFilter(), property: 'createdAt'), ] ), ] )] class Book { // ... } ``` Clients can then sort with: - `GET /books?order=asc` — sort by name ascending - `GET /books?orderDate=desc` — sort by creation date descending - `GET /books?order=asc&orderDate=desc` — combine both ### Handling Null Values When a sorted property can be `null`, use the `nullsComparison` constructor argument to specify how null values are ordered relative to non-null values: | Strategy | Constant | | ------------------------------- | ------------------------------------------ | | Use the default DBMS behavior | `null` (default) | | Null values always sort first | `OrderFilterInterface::NULLS_ALWAYS_FIRST` | | Null values always sort last | `OrderFilterInterface::NULLS_ALWAYS_LAST` | | Null values treated as smallest | `OrderFilterInterface::NULLS_SMALLEST` | | Null values treated as largest | `OrderFilterInterface::NULLS_LARGEST` | ```php new QueryParameter(filter: new SortFilter(), property: 'name'), 'orderDate' => new QueryParameter( filter: new SortFilter(nullsComparison: OrderFilterInterface::NULLS_ALWAYS_LAST), property: 'createdAt' ), ] ), ] )] class Book { // ... } ``` ### Sorting by Nested Properties The `SortFilter` supports dot notation to sort by properties of related entities (associations). API Platform resolves the necessary JOINs (ORM) or aggregation pipeline stages (ODM) at metadata time, so no runtime overhead is added for each request. ```php new QueryParameter( filter: new SortFilter(), property: 'department.name' ), // Sort by a property two hops away (employee → department → company) 'orderCompany' => new QueryParameter( filter: new SortFilter(nullsComparison: OrderFilterInterface::NULLS_ALWAYS_LAST), property: 'department.company.name' ), ] ), ] )] class Employee { #[ORM\ManyToOne(targetEntity: Department::class)] private Department $department; // ... } ``` Example queries: - `GET /employees?orderDept=asc` — sort by department name - `GET /employees?orderCompany=desc` — sort by company name through two associations ### MongoDB ODM Usage For MongoDB ODM, the `SortFilter` uses the aggregation pipeline. References between documents must use `storeAs: 'id'` (not DBRef) for the `$lookup` stage to work correctly. Embedded documents are accessed via dot notation without a `$lookup`. ```php new QueryParameter( filter: new SortFilter(), property: 'department.name' ), 'orderDate' => new QueryParameter( filter: new SortFilter(nullsComparison: OrderFilterInterface::NULLS_ALWAYS_LAST), property: 'createdAt' ), ] ), ] )] class Employee { // storeAs: 'id' is required for $lookup to work; DBRef is not supported #[ODM\ReferenceOne(targetDocument: Department::class, storeAs: 'id')] private Department $department; // ... } ``` ## Migrating from ApiFilter to QueryParameter API Platform 4.2+ introduces a new generation of filters designed to work natively with `QueryParameter`. These filters do not extend `AbstractFilter` and avoid the issues that arise when legacy filters are instantiated with `new` inside an attribute (missing `ManagerRegistry`, `NameConverter`, `Logger`). The following table shows how to replace each legacy filter. All modern replacements are available for both Doctrine ORM (`ApiPlatform\Doctrine\Orm\Filter\*`) and MongoDB ODM (`ApiPlatform\Doctrine\Odm\Filter\*`). | Legacy filter (`AbstractFilter`) | Modern replacement | | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | | `SearchFilter` (exact strategy) | [`ExactFilter`](#exact-filter) | | `SearchFilter` (partial, start, end, word_start strategies) | [`PartialSearchFilter`](#partial-search-filter) | | `SearchFilter` (relations / IRI matching) | [`IriFilter`](#iri-filter) | | `BooleanFilter` | [`ExactFilter`](#exact-filter) | | `DateFilter` | [`ComparisonFilter(new ExactFilter())`](#comparison-filter) | | `NumericFilter` | [`ExactFilter`](#exact-filter) (exact) or [`ComparisonFilter(new ExactFilter())`](#comparison-filter) (range) | | `RangeFilter` | [`ComparisonFilter(new ExactFilter())`](#comparison-filter) | | `OrderFilter` | [`SortFilter`](#sort-filter) | | `ExistsFilter` | No modern replacement yet — keep using `ExistsFilter` | ### Example: Migrating a DateFilter Before (legacy): ```php new QueryParameter( filter: new ComparisonFilter(new ExactFilter()), property: 'createdAt', ), ], )] class Offer { // ... } ``` The query syntax changes from `?createdAt[after]=2025-01-01` to `?createdAt[gte]=2025-01-01`. ### Example: Migrating a RangeFilter Before (legacy): ```php new QueryParameter( filter: new ComparisonFilter(new ExactFilter()), property: 'price', ), ], )] class Product { // ... } ``` The query syntax changes from `?price[between]=10..100` to `?price[gte]=10&price[lte]=100`. ### MongoDB ODM The migration works the same way for MongoDB ODM — just use the ODM namespace: ```php new QueryParameter( filter: new ComparisonFilter(new ExactFilter()), property: 'createdAt', ), ], )] class Event { // ... } ``` The same modern filters are available for both ORM and ODM: `ExactFilter`, `PartialSearchFilter`, `ComparisonFilter`, `SortFilter`, and `IriFilter`. > [!NOTE] Legacy filters extending `AbstractFilter` still work with `QueryParameter` but may have > issues with `nameConverter` when properties use camelCase names. If you encounter silent filter > failures with camelCase properties (e.g., `createdAt`, `firstName`), upgrading to the modern > filter equivalents listed above is the recommended solution. ## Filtering on Nested Properties Parameter-based filters (`QueryParameter`) support nested/related properties via dot notation. The following filters handle the necessary JOINs (ORM) or `$lookup`/`$unwind` pipeline stages (ODM) automatically: | Filter | ORM nested support | ODM nested support | | --------------------- | ------------------ | ------------------ | | `SortFilter` | Yes | Yes | | `IriFilter` | Yes | Yes | | `ExactFilter` | Yes | Yes | | `PartialSearchFilter` | Yes | Yes | | `FreeTextQueryFilter` | Yes (via delegate) | Yes (via delegate) | Use the `property` argument on `QueryParameter` with dot notation to target nested properties: ```php new QueryParameter(filter: new IriFilter(), property: 'department'), // Sort by a property of the related department (one hop) 'orderDept' => new QueryParameter(filter: new SortFilter(), property: 'department.name'), // Filter by company IRI through department (two hops) 'departmentCompany' => new QueryParameter( filter: new IriFilter(), property: 'department.company' ), // Sort by company name (two hops) 'orderCompany' => new QueryParameter(filter: new SortFilter(), property: 'department.company.name'), ] ), ] )] class Employee { #[ORM\ManyToOne(targetEntity: Department::class)] private Department $department; // ... } ``` Example queries: - `GET /employees?department=/api/departments/1` — filter by department IRI - `GET /employees?orderDept=asc` — sort by department name - `GET /employees?departmentCompany=/api/companies/1` — filter by company through department - `GET /employees?orderCompany=desc` — sort by company name Multiple parameters targeting the same relation path share the same JOIN (ORM) or `$lookup` stage (ODM), so there is no duplication in the generated query. ### Nested Properties with the Legacy ApiFilter Syntax (deprecated) > [!WARNING] The legacy method using the `ApiFilter` attribute is **deprecated** and scheduled for > **removal** in API Platform **5.0**. We strongly recommend migrating to the new `QueryParameter` > syntax described above. For legacy code, the built-in filters that extend `AbstractFilter` support nested properties using the dot (`.`) syntax, e.g.: ```php 'exact'])] class Offer { // ... } ``` ```yaml # config/services.yaml services: offer.order_filter: parent: "api_platform.doctrine.orm.order_filter" arguments: [{ product.releaseDate: ~ }] tags: ["api_platform.filter"] # The following are mandatory only if a _defaults section is defined with inverted values. # You may want to isolate filters in a dedicated file to avoid adding the following lines (by adding them in the "defaults" section) autowire: false autoconfigure: false public: false offer.search_filter: parent: "api_platform.doctrine.orm.search_filter" arguments: [{ product.color: "exact" }] tags: ["api_platform.filter"] # The following are mandatory only if a _defaults section is defined with inverted values. # You may want to isolate filters in a dedicated file to avoid adding the following lines (by adding them in the "defaults" section) autowire: false autoconfigure: false public: false # config/api/Offer.yaml App\Entity\Offer: # ... operations: ApiPlatform\Metadata\GetCollection: filters: ["offer.order_filter", "offer.search_filter"] ``` The above allows you to find offers by their respective product's color: `http://localhost:8000/api/offers?product.color=red`, or order offers by the product's release date: `http://localhost:8000/api/offers?order[product.releaseDate]=desc` ## Enabling a Filter for All Properties of a Resource > [!WARNING] The legacy method using the `ApiFilter` attribute is **deprecated** and scheduled for > **removal** in API Platform **5.0**. We strongly recommend migrating to the new `QueryParameter` > syntax, which is detailed in the [Introduction](#introduction). You can use the `:property` > placeholder instead and it is recommended to use a filter for each type of data you are filtering. As we have seen in previous examples, properties where filters can be applied must be explicitly declared. If you don't care about security and performance (for e.g., an API with restricted access), it is also possible to enable built-in filters for all properties: ```php **Note: Filters on nested properties must still be enabled explicitly to keep things sane.** Regardless of this option, filters can be applied on a property only if: - the property exists - the value is supported (ex: `asc` or `desc` for the order filters). It means that the filter will be **silently** ignored if the property: - it does not exist - it is not enabled - has an invalid value ## Decorate a Doctrine filter using Symfony A filter that implements the `ApiPlatform\Doctrine\Common\Filter\PropertyAwareFilterInterface` interface can be decorated: ```php namespace App\Doctrine\Filter; use ApiPlatform\Doctrine\Common\Filter\PropertyAwareFilterInterface; use ApiPlatform\Doctrine\Orm\Filter\FilterInterface; use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; use ApiPlatform\Metadata\Operation; use Doctrine\ORM\QueryBuilder; use Symfony\Component\DependencyInjection\Attribute\Autowire; final class SearchTextAndDateFilter implements FilterInterface { public function __construct(#[Autowire('@api_platform.doctrine.orm.search_filter.instance')] readonly FilterInterface $searchFilter, #[Autowire('@api_platform.doctrine.orm.date_filter.instance')] readonly FilterInterface $dateFilter, protected ?array $properties = null, private array $dateFilterProperties = [], private array $searchFilterProperties = []) { } // This function is only used to hook in documentation generators (supported by Swagger and Hydra) public function getDescription(string $resourceClass): array { if ($this->searchFilter instanceof PropertyAwareFilterInterface) { $this->searchFilter->setProperties($this->searchFilterProperties); } if ($this->dateFilter instanceof PropertyAwareFilterInterface) { $this->dateFilter->setProperties($this->dateFilterProperties); } return array_merge($this->searchFilter->getDescription($resourceClass), $this->dateFilter->getDescription($resourceClass)); } public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void { if ($this->searchFilter instanceof PropertyAwareFilterInterface) { $this->searchFilter->setProperties($this->searchFilterProperties); } if ($this->dateFilter instanceof PropertyAwareFilterInterface) { $this->dateFilter->setProperties($this->dateFilterProperties); } $this->searchFilter->apply($queryBuilder, $queryNameGenerator, $resourceClass, $operation, $context); $this->dateFilter->apply($queryBuilder, $queryNameGenerator, $resourceClass, $operation, $context); } } ``` This can be used with parameters using attributes: ```php namespace App\Entity; use ApiPlatform\Metadata\QueryParameter; #[GetCollection( uriTemplate: 'search_filter_parameter{._format}', parameters: [ 'searchOnTextAndDate[:property]' => new QueryParameter(filter: 'app_filter_date_and_search'), ] )] // Note that we link the parameter filter and this filter using the "alias" option: #[ApiFilter(SearchTextAndDateFilter::class, alias: 'app_filter_date_and_search', properties: ['foo', 'createdAt'], arguments: ['dateFilterProperties' => ['createdAt' => 'exclude_null'], 'searchFilterProperties' => ['foo' => 'exact']])] #[ORM\Entity] class SearchFilterParameter { /** * @var int The id */ #[ORM\Column(type: 'integer')] #[ORM\Id] #[ORM\GeneratedValue(strategy: 'AUTO')] private ?int $id = null; #[ORM\Column(type: 'string')] private string $foo = ''; #[ORM\Column(type: 'datetime_immutable', nullable: true)] private ?\DateTimeImmutable $createdAt = null; public function getId(): ?int { return $this->id; } public function getFoo(): string { return $this->foo; } public function setFoo(string $foo): void { $this->foo = $foo; } public function getCreatedAt(): ?\DateTimeImmutable { return $this->createdAt; } public function setCreatedAt(\DateTimeImmutable $createdAt): void { $this->createdAt = $createdAt; } } ``` ## Using Doctrine ORM Filters Doctrine ORM features [a filter system](https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/filters.html) that allows the developer to add SQL to the conditional clauses of queries, regardless of the place where the SQL is generated (for e.g., from a DQL query, or by loading associated entities). These are applied to collections and items and therefore are incredibly useful. The following information, specific to Doctrine filters in Symfony, is based upon [a great article posted on Michaël Perrin's blog](https://www.michaelperrin.fr/blog/2014/12/doctrine-filters). Suppose we have a `User` entity and an `Order` entity related to the `User` one. A user should only see his orders and no one else's. ```php getReflectionClass()->getAttributes(UserAware::class)[0] ?? null; $fieldName = $userAware?->getArguments()['userFieldName'] ?? null; if ($fieldName === '' || is_null($fieldName)) { return ''; } try { // Don't worry, getParameter automatically escapes parameters $userId = $this->getParameter('id'); } catch (\InvalidArgumentException $e) { // No user ID has been defined return ''; } if (empty($fieldName) || empty($userId)) { return ''; } return sprintf('%s.%s = %s', $targetTableAlias, $fieldName, $userId); } } ``` Now, we must configure the Doctrine filter. ```yaml # api/config/packages/api_platform.yaml doctrine: orm: filters: user_filter: class: App\Filter\UserFilter enabled: true ``` Done: Doctrine will automatically filter all `UserAware`entities! ## Creating Custom Doctrine ORM Filters Doctrine ORM filters have access to the context created from the HTTP request and to the `QueryBuilder` instance used to retrieve data from the database. They are only applied to collections. If you want to deal with the DQL query generated to retrieve items, [extensions](extensions.md) are the way to go. A Doctrine ORM filter is basically a class implementing the `ApiPlatform\Doctrine\Orm\Filter\FilterInterface`. For `MongoDB (ODM)` filters, please refer to [Creating Custom Doctrine ODM Filters documentation](#creating-custom-doctrine-mongodb-odm-filters). ### Creating Custom Doctrine ORM Filters With The New Syntax (API Platform >= 4.2) Advantages of the new approach: - Simplicity: No more need to extend `AbstractFilter`. A simple implementation of `FilterInterface` is all it takes. - Clarity and Code Quality: The logic is more direct and decoupled. - Tooling: A make command is available to generate all the boilerplate code. #### Generating the Filter ORM Skeleton To get started, API Platform includes a very handy make command to generate the basic structure of an ORM filter: ```console bin/console make:filter orm ``` Then, provide the name of your filter, for example `MonthFilter`, or pass it directly as an argument: ```console make:filter orm MyCustomFilter ``` You will get a file at `api/src/Filter/MonthFilter.php` with the following content: ```php getValue(); // Retrieve the property // $property = $parameter->getProperty(); // Retrieve alias and parameter name // $alias = $queryBuilder->getRootAliases()[0]; // $parameterName = $queryNameGenerator->generateParameterName($property); // TODO: make your awesome query using the $queryBuilder // $queryBuilder-> } } ``` #### Implementing a Custom ORM Filter Let's create a concrete filter that allows fetching entities based on the month of a date field (for e.g., `createdAt`). The goal is to be able to call a URL like `GET /invoices?createdAtMonth=7` to get all invoices created in July. Here is the complete and corrected code for the filter: ```php getValue(); $parameterName = $queryNameGenerator->generateParameterName($property); $alias = $queryBuilder->getRootAliases()[0]; $queryBuilder ->andWhere(sprintf('MONTH(%s.%s) = :%s', $alias, $property, $parameterName)) ->setParameter($parameterName, $monthValue); } } ``` Now that the filter is created, it must be associated with an API resource. We use the `QueryParameter` object on a `#[GetCollection]` operation attribute for this. For other syntax please refer to [this documentation](#introduction). ```php new QueryParameter( filter: new MonthFilter(), property: 'createdAt' ), ] )] class Invoice { // ... } ``` And that's it! ✅ Your filter is operational. A request like `GET /invoices?createdAtMonth=7` will now correctly return the invoices from July! #### Adding Custom Filter ORM Validation And A Better Typing Currently, our filter accepts any value, like `createdAtMonth=99` or `createdAtMonth=foo`, which could cause errors. To validate inputs and ensure the correct type, we can implement the `JsonSchemaFilterInterface`. This allows delegating validation to API Platform, respecting the [SOLID Principles](https://en.wikipedia.org/wiki/SOLID). > [!NOTE] Even with our internal systems, some additional **manual validation** is needed to ensure > greater accuracy. However, we already take care of a lot of these validations for you. > > You can see how this works directly in our code components: > > - The `ParameterValidatorProvider` for **Symfony** can be found > [in the Symfony ParameterValidatorProvider.php file](https://github.com/api-platform/core/blob/c9692b509d5b641104addbadb349b9bcab83e251/src/Symfony/Validator/State/ParameterValidatorProvider.php). > - The `ParameterValidatorProvider` for **Laravel** is located > [in the Laravel ParameterValidatorProvider.php file](https://github.com/api-platform/core/blob/c9692b509d5b641104addbadb349b9bcab83e251/src/Laravel/State/ParameterValidatorProvider.php). > > Additionally, we filter out empty values within our `ParameterExtension` classes. For instance, > the **Doctrine ORM** `ParameterExtension` > [handles this filtering here](https://github.com/api-platform/core/blob/c9692b509d5b641104addbadb349b9bcab83e251/src/Doctrine/Orm/Extension/ParameterExtension.php#L51C13-L53C14). ```php 'integer', // <=> Symfony\Component\Validator\Constraints\Range 'minimum' => 1, 'maximum' => 12, ]; } } ``` With this code, under the hood, API Platform automatically adds a [Symfony Range constraint](https://symfony.com/doc/current/reference/constraints/Range.html). This ensures the parameter only accepts values between `1` and `12` (inclusive), which is exactly what we need. This approach offers two key benefits: - Automatic Validation: It rejects other data types and invalid values, so you get an integer directly. - Simplified Logic: You can retrieve the value with `$monthValue = $parameter->getValue();` knowing it's already a - validated integer. This means you **don't have to add custom validation to your filter class, entity, or model**. The validation is handled for you, making your code cleaner and more efficient. > [!TIP] For a complete list of constraints, see the > [complete OpenApi format in the documentation](../core/filters.md#from-openapi-definition). ### Documenting the ORM Filter (OpenAPI) #### The Simple Method (for scalar types) On A Custom ORM Filter If your filter expects a simple type (`int`, `string`, `bool`, or arrays of these types), the quickest way is to use the `OpenApiFilterTrait`. ```php */ public function getOpenApiParameters(Parameter $parameter): array { // Example for a filter that expects an array of values // like ?myParam[key1]=value1&myParam[key2]=value2 return [ new OpenApiParameter( name: $parameter->getKey(), in: 'query', description: 'A custom filter for complex objects.', style: 'deepObject', explode: true ) ]; } } ``` ## Creating Custom Doctrine MongoDB ODM Filters For `Doctrine ORM` filters, please refer to [Creating Custom Doctrine ORM Filters documentation](#creating-custom-doctrine-orm-filters). Doctrine MongoDB ODM filters have access to the context created from the HTTP request and to the [aggregation builder](https://www.doctrine-project.org/projects/doctrine-mongodb-odm/en/current/reference/aggregation-builder.html) instance used to retrieve data from the database and to execute [complex operations on data](https://docs.mongodb.com/manual/aggregation/). They are only applied to collections. If you want to deal with the aggregation pipeline generated to retrieve items, [extensions](extensions.md) are the way to go. A Doctrine MongoDB ODM filter is basically a class implementing the `ApiPlatform\Doctrine\Odm\Filter\FilterInterface`. ### Creating Custom Doctrine ODM Filters With The New Syntax (API Platform >= 4.2) Advantages of the new approach: - Simplicity: No more need to extend `AbstractFilter`. A simple implementation of `FilterInterface` is all it takes. - Clarity and Code Quality: The logic is more direct and decoupled. - Tooling: A make command is available to generate all the boilerplate code. #### Generating the Filter ODM Skeleton To get started, API Platform includes a very handy make command to generate the basic structure of an ODM filter: ```console bin/console make:filter odm ``` Then, provide the name of your filter, for example `MonthFilter`, or pass it directly as an argument: ```console make:filter orm MyCustomFilter ``` You will get a file at `api/src/Filter/MonthFilter.php` with the following content: ```php getValue(); // Retrieve the property // $property = $parameter->getProperty(); // TODO: make your awesome query using the $aggregationBuilder // $aggregationBuilder-> } } ``` #### Implementing a Custom ODM Filter Let's create a concrete filter that allows fetching entities based on the month of a date field (for e.g., `createdAt`). The goal is to be able to call a URL like `GET /invoices?createdAtMonth=7` to get all invoices created in July. Here is the complete and corrected code for the filter: ```php getValue(); $property = $parameter->getProperty(); $aggregationBuilder->match( $aggregationBuilder->expr()->operator('$expr', [ '$eq' => [ ['$month' => '$' . $property], $monthValue ] ]) ); } } ``` Now that the filter is created, it must be associated with an API resource. We use the `QueryParameter` object on a `#[GetCollection]` operation attribute for this. For other syntax please refer to [this documentation](#introduction). ```php new QueryParameter( filter: new MonthFilter(), property: 'createdAt' ), ] )] class Invoice { // ... } ``` And that's it! ✅ Your filter is operational. A request like `GET /invoices?createdAtMonth=7` will now correctly return the invoices from July! #### Adding Custom Filter ODM Validation And A Better Typing Currently, our filter accepts any value, like `createdAtMonth=99` or `createdAtMonth=foo`, which could cause errors. To validate inputs and ensure the correct type, we can implement the `JsonSchemaFilterInterface`. This allows delegating validation to API Platform, respecting the [SOLID Principles](https://en.wikipedia.org/wiki/SOLID). > [!NOTE] Even with our internal systems, some additional **manual validation** is needed to ensure > greater accuracy. However, we already take care of a lot of these validations for you. > > You can see how this works directly in our code components: > > - The `ParameterValidatorProvider` for **Symfony** can be found > [in the Symfony ParameterValidatorProvider.php file](https://github.com/api-platform/core/blob/c9692b509d5b641104addbadb349b9bcab83e251/src/Symfony/Validator/State/ParameterValidatorProvider.php). > - The `ParameterValidatorProvider` for **Laravel** is located > [in the Laravel ParameterValidatorProvider.php file](https://github.com/api-platform/core/blob/c9692b509d5b641104addbadb349b9bcab83e251/src/Laravel/State/ParameterValidatorProvider.php). > > Additionally, we filter out empty values within our `ParameterExtension` classes. For instance, > the **Doctrine ODM** `ParameterExtension` > [handles this filtering here](https://github.com/api-platform/core/blob/c9692b509d5b641104addbadb349b9bcab83e251/src/Doctrine/Odm/Extension/ParameterExtension.php#L50-L52). ```php 'integer', // <=> Symfony\Component\Validator\Constraints\Range 'minimum' => 1, 'maximum' => 12, ]; } } ``` With this code, under the hood, API Platform automatically adds a [Symfony Range constraint](https://symfony.com/doc/current/reference/constraints/Range.html). This ensures the parameter only accepts values between `1` and `12` (inclusive), which is exactly what we need. This approach offers two key benefits: - Automatic Validation: It rejects other data types and invalid values, so you get an integer directly. - Simplified Logic: You can retrieve the value with `$monthValue = $parameter->getValue();` knowing it's already a - validated integer. This means you **don't have to add custom validation to your filter class, entity, or model**. The validation is handled for you, making your code cleaner and more efficient. > [!TIP] For a complete list of constraints, see the > [full OpenApi format in the documentation](../core/filters.md#from-openapi-definition). ### Documenting the ODM Filter (OpenAPI) #### The Simple Method (for scalar types) On A Custom ODM Filter If your filter expects a simple type (`int`, `string`, `bool`, or arrays of these types), the quickest way is to use the `OpenApiFilterTrait`. ```php */ public function getOpenApiParameters(Parameter $parameter): array { // Example for a filter that expects an array of values // like ?myParam[key1]=value1&myParam[key2]=value2 return [ new OpenApiParameter( name: $parameter->getKey(), in: 'query', description: 'A custom filter for complex objects.', style: 'deepObject', explode: true ) ]; } } ``` --- # Subresources Source: https://api-platform.com/docs/core/subresources/ # Subresources A Subresource is another way of declaring a resource that usually involves a more complex URI. In API Platform you can declare as many `ApiResource` as you want on a PHP class creating Subresources. Subresources work well by implementing your own state [providers](./state-providers.md) or [processors](./state-processors.md). In API Platform, we provide functional Doctrine and Eloquent layers for subresources, as long as the correct configuration for URI variables is added. ## URI Variables Configuration URI Variables are configured via the `uriVariables` node on an `ApiResource`. It's an array indexed by the variables present in your URI, `/companies/{companyId}/employees/{id}` has two URI variables `companyId` and `id`. For each of these, we need to create a `Link` between the previous and the next node, in this example the link between a Company and an Employee. If you're using the Doctrine or the Eloquent implementation, queries are automatically built using the provided links. ### Answer to a Question > [!NOTE] In Symfony we use the term “entities”, while the following documentation is mostly for > Laravel “models”. For this example we have two classes, a Question and an Answer. We want to find the Answer to the Question about the Universe using the following URI: `/question/42/answer`. Let's start by defining the resources: ```php id; } // ... } // api/src/Entity/Question.php namespace App\Entity; use ApiPlatform\Metadata\ApiResource; use Doctrine\ORM\Mapping as ORM; #[ORM\Entity] #[ApiResource] class Question { #[ORM\Id, ORM\Column, ORM\GeneratedValue] private ?int $id = null; #[ORM\Column(type: 'text')] public string $content; #[ORM\OneToOne] #[ORM\JoinColumn(referencedColumnName: 'id', unique: true)] public Answer $answer; public function getId(): ?int { return $this->id; } // ... } ``` ```yaml # The YAML syntax is only supported for Symfony # api/config/api_platform/resources.yaml resources: App\Entity\Answer: ~ App\Entity\Question: ~ ``` ```xml ``` Now to create a new way of retrieving an Answer we will declare another resource on the `Answer` class. To make things work, API Platform needs information about how to retrieve the `Answer` belonging to the `Question`, this is done by configuring the `uriVariables`: ```php new Link( fromClass: Question::class, fromProperty: 'answer' ) ], operations: [new Get()] )] class Answer { // ... } ``` ```yaml # The YAML syntax is only supported for Symfony # api/config/api_platform/resources.yaml resources: App\Entity\Answer: uriTemplate: /questions/{id}/answer uriVariables: id: fromClass: App\Entity\Question fromProperty: answer operations: ApiPlatform\Metadata\Get: ~ App\Entity\Question: ~ ``` ```xml ``` In this example, we instructed API Platform that the `Answer` we retrieve comes **from** the **class** `Question` **from** the **property** `answer` of that class. URI Variables are defined using Links (`ApiPlatform\Metadata\Link`). A `Link` can be binded either from or to a class and a property. If we had a `relatedQuestions` property on the `Answer` we could retrieve the collection of related questions via the following definition: ```php #[ApiResource( uriTemplate: '/answers/{id}/related_questions.{_format}', uriVariables: [ 'id' => new Link(fromClass: Answer::class, fromProperty: 'relatedQuestions') ], operations: [new GetCollection()] )] ``` ```yaml #The YAML syntax is only supported for Symfony # api/config/api_platform/resources.yaml resources: App\Entity\Question: uriTemplate: /answers/{id}/related_questions.{_format} uriVariables: id: fromClass: App\Entity\Answer fromProperty: relatedQuestions operations: ApiPlatform\Metadata\GetCollection: ~ ``` ```xml ``` ### Company Employee's > [!NOTE] In Symfony we use the term “entities”, while the following documentation is mostly for > Laravel “models”. Note that in this example, we declared an association using Doctrine only between Employee and Company using a ManyToOne. There is no inverse association hence the use of `toProperty` in the URI Variables definition. The following declares a few subresources: - `/companies/{companyId}/employees/{id}` - get an employee belonging to a company - `/companies/{companyId}/employees` - get the company employee's ```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() ] )] 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; } } ``` Now let's add the Company class: ```php new Link(fromClass: Employee::class, fromProperty: 'company'), ], operations: [ new Get() ] )] class Company { // ... } ``` ## Security > [!WARNING] This is not yet available with Laravel, you're welcome to contribute > [on GitHub](https://github.com/api-platform/core) In order to use Symfony's built-in security system on subresources the security option of the `Link` attribute can be used. To restrict the access to a subresource based on the parent object simply use the Symfony expression language as you would do normally, with the exception that the name defined in `toProperty` or `fromProperty` is used to access the object. Alternatively you can also use the `securityObjectName` to set a custom name. ```php new Link(fromClass: Employee::class, toProperty: 'company', security: "is_granted(some_voter, company)"), ], operations: [ new Get() ] )] class Company { // ... } ``` This is currently an experimental feature disabled by default. To enable it please set `enable_link_security` to true: ```yaml # api/config/packages/api_platform.yaml api_platform: enable_link_security: true ``` --- # The Serialization Process Source: https://api-platform.com/docs/core/serialization/ # The Serialization Process For documentation on how to expose PHP 8.1+ Enums as API resources, refer to the [Enums documentation](enums.md). ## Overall Process API Platform embraces and extends the Symfony Serializer Component to transform PHP entities in (hypermedia) API responses.

Serializer screencast
Watch the Serializer screencast

The main serialization process has two stages: ![Serializer workflow](images/SerializerWorkflow.png) > As you can see in the picture above, an array is used as a man-in-the-middle. This way, Encoders > will only deal with turning specific formats into arrays and vice versa. The same way, Normalizers > will deal with turning specific objects into arrays and vice versa. -- > [The Symfony documentation](https://symfony.com/doc/current/components/serializer.html) Unlike Symfony or Laravel themselves, API Platform leverages custom normalizers, its router and the [state provider](state-providers.md) system to perform an advanced transformation. Metadata are added to the generated document including links, type information, pagination data or available filters. The API Platform Serializer is extendable. You can register custom normalizers and encoders in order to support other formats. You can also decorate existing normalizers to customize their behaviors. ## Available Serializers - [JSON-LD](https://json-ld.org) serializer `api_platform.jsonld.normalizer.item` JSON-LD, or JavaScript Object Notation for Linked Data, is a method of encoding Linked Data using JSON. It is a World Wide Web Consortium Recommendation. - [HAL](https://en.wikipedia.org/wiki/Hypertext_Application_Language) serializer `api_platform.hal.normalizer.item` - JSON, XML, CSV, YAML serializer (using the Symfony serializer) `api_platform.serializer.normalizer.item` ## The Serialization Context, Groups and Relations

Serialization Groups screencast
Watch the Serialization Groups screencast

API Platform allows you to specify the `$context` variable used by the Symfony Serializer. This variable is an associative array that has a handy `groups` key allowing you to choose which properties of the resource are exposed during the normalization (read) and denormalization (write) processes. It relies on the [serialization (and deserialization) groups](https://symfony.com/doc/current/components/serializer.html#attributes-groups) feature of the Symfony Serializer component. In addition to groups, you can use any option supported by the Symfony Serializer. For example, you can use [`enable_max_depth`](https://symfony.com/doc/current/components/serializer.html#handling-serialization-depth) to limit the serialization depth. ### Configuration for Symfony Just like other Symfony and API Platform components, the Serializer component can be configured using attributes, XML or YAML. Since attributes are easy to understand, we will use them in the following examples. > [!NOTE] If you are not using the API Platform Symfony variant, you need to enable annotation > support in the serializer configuration as outlined below, depending on your Symfony version. #### Configuration for Symfony `<= 6.4` ##### General Case Add the following configuration to your `framework.yaml` file: ```yaml # api/config/packages/framework.yaml framework: serializer: { enable_annotations: true } ``` ##### Using Symfony Flex If you use [Symfony Flex](https://github.com/symfony/flex) and Symfony `<= 6.4`, simply run the following command: ```console composer req doctrine/annotations ``` You're all set! #### Configuration for Symfony `>= 7.0` If you are using Symfony >= 7.0, [annotations have been replaced by attributes](https://www.doctrine-project.org/2022/11/04/annotations-to-attributes.html). Update your configuration as follows: ```diff # api/config/packages/framework.yaml framework: - serializer: { enable_annotations: true } + serializer: { enable_attributes: true } ``` #### Additional Syntax Configuration for All Versions If you want to use YAML or XML for serialization, add the mapping path to the serializer configuration: ```yaml # api/config/packages/framework.yaml framework: serializer: mapping: paths: ["%kernel.project_dir%/config/serialization"] ``` ```xml %kernel.project_dir%/config/serialization ``` ## Using Serialization Groups It is simple to specify what groups to use in the API system: 1. Add the normalization context and denormalization context attributes to the resource, and specify which groups to use. Here you see that we add `read` and `write`, respectively. You can use any group names you wish. 2. Apply the groups to properties in the object. ```php ['read']], denormalizationContext: ['groups' => ['write']], )] class Book { #[Groups(['read', 'write'])] public $name; #[Groups('write')] public $author; // ... } ``` ```yaml # The YAML syntax is only supported for Symfony # api/config/api_platform/resources.yaml resources: App\ApiResource\Book: normalizationContext: groups: ["read"] denormalizationContext: groups: ["write"] # The YAML syntax is only supported for Symfony # api/config/serialization/Book.yaml App\ApiResource\Book: attributes: name: groups: ["read", "write"] author: groups: ["write"] ``` ```xml read write read write write ``` In the previous example, the `name` property will be visible when reading (`GET`) the object, and it will also be available to write (`PUT` / `PATCH` / `POST`). The `author` property will be write-only; it will not be visible when serialized responses are returned by the API. Internally, API Platform passes the value of the `normalizationContext` as the 3rd argument of [the `Serializer::serialize()` method](https://api.symfony.com/master/Symfony/Component/Serializer/SerializerInterface.html#method_serialize) during the normalization process. `denormalizationContext` is passed as the 4th argument of [the `Serializer::deserialize()` method](https://api.symfony.com/master/Symfony/Component/Serializer/SerializerInterface.html#method_deserialize) during denormalization (writing). To configure the serialization groups of classes's properties, you must use directly [the Symfony Serializer's configuration files or attributes](https://symfony.com/doc/current/components/serializer.html#attributes-groups). In addition to the `groups` key, you can configure any Symfony Serializer option through the `$context` parameter (e.g. the `enable_max_depth`key when using [the `@MaxDepth` annotation](https://symfony.com/doc/current/components/serializer.html#handling-serialization-depth)). Any serialization and deserialization group that you specify will also be leveraged by the built-in actions and the Hydra documentation generator. ## Using Serialization Groups per Operation

Relations screencast
Watch the Relations screencast

By default, the serializer provided with API Platform represents relations between objects using [dereferenceable IRIs](https://en.wikipedia.org/wiki/Internationalized_Resource_Identifier). They allow you to retrieve details for related objects by issuing extra HTTP requests. However, for performance reasons, it is sometimes preferable to avoid forcing the client to issue extra HTTP requests. It is possible to specify normalization and denormalization contexts (as well as any other attribute) on a per-operation basis. API Platform will always use the most specific definition. For instance, if normalization groups are set both at the resource level and at the operation level, the configuration set at the operation level will be used and the resource level ignored. In the following example we use different serialization groups for the `GET` and `PATCH` operations: ```php ['get']])] #[Get] #[Patch(normalizationContext: ['groups' => ['patch']])] class Book { #[Groups(['get', 'patch'])] public $name; #[Groups('get')] public $author; // ... } ``` ```yaml # The YAML syntax is only supported for Symfony # api/config/api_platform/resources/Book.yaml App\ApiResource\Book: normalizationContext: groups: ['get'] operations: ApiPlatform\Metadata\Get: ~ ApiPlatform\Metadata\Get: ~ ApiPlatform\Metadata\Patch: normalizationContext: groups: ['patch'] # The YAML syntax is only supported for Symfony # api/config/serializer/Book.yaml App\ApiResource\Book: attributes: name: groups: ['get', 'patch'] author: groups: ['get'] ``` ```xml get patch get patch get ``` The `name` and `author` properties will be included in the document generated during a `GET` operation because the configuration defined at the resource level is inherited. However the document generated when a `PATCH` request will be received will only include the `name` property because of the specific configuration for this operation. Refer to the [operations](operations.md) documentation to learn more. ## Embedding Relations

Relations screencast
Watch the Relations screencast

By default, the serializer provided with API Platform represents relations between objects using [dereferenceable IRIs](https://en.wikipedia.org/wiki/Internationalized_Resource_Identifier). They allow you to retrieve details for related objects by issuing extra HTTP requests. However, for performance reasons, it is sometimes preferable to avoid forcing the client to issue extra HTTP requests. **Note:** We strongly recommend using [Vulcain](https://vulcain.rocks) instead of this feature. Vulcain allows creating faster (better hit rate) and better designed APIs than relying on compound documents, and is supported out of the box in the API Platform Symfony variant. ### Normalization In the following JSON document, the relation from a book to an author is by default represented by an URI: ```json { "@context": "/contexts/Book", "@id": "/books/62", "@type": "Book", "name": "My awesome book", "author": "/people/59" } ``` It is possible to embed related objects (in their entirety, or only some of their properties) directly in the parent response through the use of serialization groups. By using the following serialization groups attributes (`#[Groups]`), a JSON representation of the author is embedded in the book response. As soon as any of the author's attributes is in the `book` group, the author will be embedded. ```php ['book']])] class Book { #[Groups('book')] public $name; #[Groups('book')] public $author; // ... } ``` ```yaml # The YAML syntax is only supported for Symfony # api/config/api_platform/resources/Book.yaml App\ApiResource\Book: normalizationContext: groups: ['book'] # The YAML syntax is only supported for Symfony # api/config/serializer/Book.yaml App\ApiResource\Book: attributes: name: groups: ['book'] author: groups: ['book'] ``` ```php The generated JSON using previous settings is below: ```json { "@context": "/contexts/Book", "@id": "/books/62", "@type": "Book", "name": "My awesome book", "author": { "@id": "/people/59", "@type": "Person", "name": "Kévin Dunglas" } } ``` In order to optimize such embedded relations, the default Doctrine state provider will automatically join entities on relations marked as [`EAGER`](https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/attributes-reference.html#manytoone). This avoids the need for extra queries to be executed when serializing the related objects. Instead of embedding relations in the main HTTP response, you may want [to "push" them to the client using HTTP/2 server push](push-relations.md). ### Denormalization It is also possible to embed a relation in `PUT`, `PATCH` and `POST` requests. To enable that feature, set the serialization groups the same way as normalization. For example: ```php ['book']])] class Book { // ... } ``` ```yaml # The YAML syntax is only supported for Symfony # api/config/api_platform/resources/Book.yaml App\ApiResource\Book: denormalizationContext: groups: ["book"] ``` The following rules apply when denormalizing embedded relations: - If an `@id` key is present in the embedded resource, then the object corresponding to the given URI will be retrieved through the state provider. Any changes in the embedded relation will also be applied to that object. - If no `@id` key exists, a new object will be created containing state provided in the embedded JSON document. You can specify as many embedded relation levels as you want. ### Force IRI with relations of the same type (parent/childs relations) It is a common problem to have entities that reference other entities of the same type: ```php ['person']], denormalizationContext: ['groups' => ['person']] )] class Person { #[Groups('person')] public string $name; #[Groups('person')] public ?Person $parent; // Note that a Person instance has a relation with another Person. // ... } ``` ```yaml # The YAML syntax is only supported for Symfony # api/config/api_platform/resources/Person.yaml App\ApiResource\Person: normalizationContext: groups: ['person'] denormalizationContext: groups: ['person'] # The YAML syntax is only supported for Symfony # api/config/serializer/Person.yaml App\ApiResource\Person: attributes: name: groups: ['person'] parent: groups: ['person'] ``` The problem here is that the **$parent** property become automatically an embedded object. Besides, the property won't be shown on the OpenAPI view. To force the **$parent** property to be used as an IRI, add an `#[ApiProperty(readableLink: false, writableLink: false)]` annotation: ```php ['person']], denormalizationContext: ['groups' => ['person']] )] class Person { #[Groups('person')] public string $name; #[Groups('person')] #[ApiProperty(readableLink: false, writableLink: false)] public ?Person $parent; // This property is now serialized/deserialized as an IRI. // ... } ``` ```yaml # The YAML syntax is only supported for Symfony # api/config/api_platform/resources/Person.yaml resources: App\ApiResource\Person: normalizationContext: groups: ["person"] denormalizationContext: groups: ["person"] properties: App\ApiResource\Person: parent: readableLink: false writableLink: false # The YAML syntax is only supported for Symfony # api/config/serializer/Person.yaml App\ApiResource\Person: attributes: name: groups: ["person"] parent: groups: ["person"] ``` ### Plain Identifiers for Symfony Instead of sending an IRI to set a relation, you may want to send a plain identifier. To do so, you must create your own denormalizer: ```php iriConverter = $iriConverter; } public function denormalize($data, $class, $format = null, array $context = []) { $data['relatedDummy'] = $this->iriConverter->getIriFromResource(resource: RelatedDummy::class, context: ['uri_variables' => ['id' => $data['relatedDummy']]]); return $this->denormalizer->denormalize($data, $class, $format, $context + [__CLASS__ => true]); } public function supportsDenormalization($data, $type, $format = null, array $context = []): bool { return \in_array($format, ['json', 'jsonld'], true) && is_a($type, Dummy::class, true) && !empty($data['relatedDummy']) && !isset($context[__CLASS__]); } public function getSupportedTypes(?string $format): array { return [ Dummy::class => false, ]; } } ``` ## Property Normalization Context for Symfony If you want to change the (de)normalization context of a property, for instance if you want to change the format of the date time, you can do so by using the `#[Context]` attribute from the Symfony Serializer component. For instance: ```php 'Y-m-d'])] public ?\DateTimeInterface $publicationDate = null; } ``` In the above example, you will receive the book's data like this: ```json { "@context": "/contexts/Book", "@id": "/books/3", "@type": "https://schema.org/Book", "publicationDate": "1989-06-16" } ``` It's also possible to only change the denormalization or normalization context: ```php 'Y-m-d'])] public ?\DateTimeInterface $publicationDate = null; } ``` Groups are also supported: ```php \DateTime::RFC3339])] #[Context( context: [DateTimeNormalizer::FORMAT_KEY => \DateTime::RFC3339_EXTENDED], groups: ['extended'], )] public ?\DateTimeInterface $publicationDate = null; } ``` ## Calculated Field using Doctrine Sometimes you need to expose calculated fields. This can be done by leveraging the groups. This time not on a property, but on a method. ```php 'greeting:collection:get'])] class Greeting { #[ORM\Id, ORM\Column, ORM\GeneratedValue] #[Groups("greeting:collection:get")] private ?int $id = null; private int $a = 1; private int $b = 2; #[ORM\Column] #[Groups("greeting:collection:get")] public string $name = ''; public function getId(): int { return $this->id; } #[Groups('greeting:collection:get')] // <- MAGIC IS HERE, you can set a group on a method. public function getSum(): int { return $this->a + $this->b; } } ``` ```yaml # The YAML syntax is only supported for Symfony # api/config/api_platform/resources/Greeting.yaml App\Entity\Greeting: operations: ApiPlatform\Metadata\GetCollection: normalizationContext: groups: 'greeting:collection:get' # The YAML syntax is only supported for Symfony # api/config/serializer/Greeting.yaml App\Entity\Greeting: attributes: id: groups: 'greeting:collection:get' name: groups: 'greeting:collection:get' sum: groups: 'greeting:collection:get' ``` ## Changing the Serialization Context Dynamically

Context Builder & Service Decoration screencast
Watch the Context Builder & Service Decoration screencast

Let's imagine a resource where most fields can be managed by any user, but some can be managed only by admin users: ```php ['book:output']], denormalizationContext: ['groups' => ['book:input']], )] class Book { // ... /** * This field can be managed only by an admin */ #[Groups(['book:output', 'admin:input'])] public bool $active = false; /** * This field can be managed by any user */ #[Groups(['book:output', 'book:input'])] public string $name; // ... } ``` ```yaml # The YAML syntax is only supported for Symfony # api/config/api_platform/resources/Book.yaml App\ApiResource\Book: normalizationContext: groups: ['book:output'] denormalizationContext: groups: ['book:input'] # The YAML syntax is only supported for Symfony # api/config/serializer/Book.yaml App\ApiResource\Book: attributes: active: groups: ['book:output', 'admin:input'] name: groups: ['book:output', 'book:input'] ``` All entry points are the same for all users, so we should find a way to detect if the authenticated user is an admin, and if so dynamically add the `admin:input` value to deserialization groups in the `$context` array. API Platform implements a `ContextBuilder`, which prepares the context for serialization & deserialization. Let's [decorate this service](https://symfony.com/doc/current/service_container/service_decoration.html) to override the `createFromRequest` method: ```yaml # The YAML syntax is only supported for Symfony # api/config/services.yaml services: # ... 'App\Serializer\BookContextBuilder': decorates: "api_platform.serializer.context_builder" arguments: ['@App\Serializer\BookContextBuilder.inner'] autoconfigure: false ``` ```php decorated = $decorated; $this->authorizationChecker = $authorizationChecker; } public function createFromRequest(Request $request, bool $normalization, ?array $extractedAttributes = null): array { $context = $this->decorated->createFromRequest($request, $normalization, $extractedAttributes); $resourceClass = $context['resource_class'] ?? null; if ($resourceClass === Book::class && isset($context['groups']) && $this->authorizationChecker->isGranted('ROLE_ADMIN') && false === $normalization) { $context['groups'][] = 'admin:input'; } return $context; } } ``` If the user has the `ROLE_ADMIN` permission and the subject is an instance of Book, `admin:input` group will be dynamically added to the denormalization context. The `$normalization` variable lets you check whether the context is for normalization (if `TRUE`) or denormalization (`FALSE`). ## Changing the Serialization Context on a Per-item Basis for Symfony The example above demonstrates how you can modify the normalization/denormalization context based on the current user permissions for all books. Sometimes, however, the permissions vary depending on what book is being processed. Think of ACL's: User "A" may retrieve Book "A" but not Book "B". In this case, we need to leverage the power of the Symfony Serializer and register our own normalizer that adds the group on every single item (note: priority `64` is an example; it is always important to make sure your normalizer gets loaded first, so set the priority to whatever value is appropriate for your application; higher values are loaded earlier): ```yaml # api/config/services.yaml services: 'App\Serializer\BookAttributeNormalizer': arguments: ["@security.token_storage"] tags: - { name: "serializer.normalizer", priority: 64 } ``` The Normalizer class is a bit harder to understand, because it must ensure that it is only called once and that there is no recursion. To accomplish this, it needs to be aware of the parent Normalizer instance itself. Here is an example: ```php tokenStorage = $tokenStorage; } public function normalize($object, $format = null, array $context = []) { if ($this->userHasPermissionsForBook($object)) { $context['groups'][] = 'can_retrieve_book'; } $context[self::ALREADY_CALLED] = true; return $this->normalizer->normalize($object, $format, $context); } public function supportsNormalization($data, $format = null, array $context = []) { // Make sure we're not called twice if (isset($context[self::ALREADY_CALLED])) { return false; } return $data instanceof Book; } private function userHasPermissionsForBook($object): bool { // Get permissions from user in $this->tokenStorage // for the current $object (book) and // return true or false } public function getSupportedTypes(?string $format): array { return [ Book::class => true ]; } } ``` This will add the serialization group `can_retrieve_book` only if the currently logged-in user has access to the given book instance. Note: In this example, we use the `TokenStorageInterface` to verify access to the book instance. However, Symfony provides many useful other services that might be better suited to your use case. For example, the [`AuthorizationChecker`](https://symfony.com/doc/current/components/security/authorization.html#authorization-checker). ## Name Conversion for Symfony The Serializer Component provides a handy way to map PHP field names to serialized names. See the related [Symfony documentation](https://symfony.com/doc/current/components/serializer.html#converting-property-names-when-serializing-and-deserializing). To use this feature, declare a new name converter service. For example, you can convert `CamelCase` to `snake_case` with the following configuration: ```yaml # api/config/services.yaml services: 'Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter': ~ ``` ```yaml # api/config/packages/api_platform.yaml api_platform: name_converter: 'Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter' ``` If symfony's `MetadataAwareNameConverter` is available it'll be used by default. If you specify one in ApiPlatform configuration, it'll be used. Note that you can use decoration to benefit from this name converter in your own implementation. ## Decorating a Serializer and Adding Extra Data for Symfony In the following example, we will see how we add extra information to the serialized output. Here is how we add the date on each request in `GET`: ```yaml # api/config/services.yaml services: 'App\Serializer\ApiNormalizer': # By default .inner is passed as argument decorates: "api_platform.jsonld.normalizer.item" ``` Note: this normalizer will work only for JSON-LD format, if you want to process JSON data too, you have to decorate another service: ```yaml # Need a different name to avoid duplicate YAML key "app.serializer.normalizer.item.json": class: 'App\Serializer\ApiNormalizer' decorates: "api_platform.serializer.normalizer.item" ``` ```php decorated = $decorated; } public function supportsNormalization($data, $format = null) { return $this->decorated->supportsNormalization($data, $format); } public function normalize($object, $format = null, array $context = []) { $data = $this->decorated->normalize($object, $format, $context); if (is_array($data)) { $data['date'] = date(\DateTime::RFC3339); } return $data; } public function supportsDenormalization($data, $type, $format = null) { return $this->decorated->supportsDenormalization($data, $type, $format); } public function denormalize($data, string $type, string $format = null, array $context = []) { return $this->decorated->denormalize($data, $type, $format, $context); } public function setSerializer(SerializerInterface $serializer) { if($this->decorated instanceof SerializerAwareInterface) { $this->decorated->setSerializer($serializer); } } public function getSupportedTypes(?string $format): array { return $this->decorated->getSupportedTypes($format); } } ``` ## Entity/Model Identifier Case API Platform is able to guess the entity/model identifier using Doctrine metadata ([ORM](https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/basic-mapping.html#identifiers-primary-keys), [MongoDB ODM](https://www.doctrine-project.org/projects/doctrine-mongodb-odm/en/current/reference/basic-mapping.html#identifiers)) or Laravel Eloquent metadata ([ORM](https://laravel.com/docs/eloquent#primary-keys)). For ORM, it also supports [Doctrine composite identifiers](https://www.doctrine-project.org/projects/doctrine-orm/en/current/tutorials/composite-primary-keys.html) and [Eloquent composite identifiers](https://laravel.com/docs/eloquent#composite-primary-keys). If you are not using the Doctrine ORM or MongoDB ODM Provider, you must explicitly mark the identifier using the `identifier` attribute of the `ApiPlatform\Metadata\ApiProperty` annotation. For example: ```php ``` In some cases, you will want to set the identifier of a resource from the client (e.g. a client-side generated UUID, or a slug). In such cases, you must make the identifier property a writable class property. Specifically, to use client-generated IDs, you must do the following: 1. create a setter for the identifier of the entity/model (e.g. `public function setId(string $id)`) or make it a `public` property , 2. add the denormalization group to the property (only if you use a specific denormalization group), and, 3. if you use Doctrine ORM, be sure to **not** mark this property with [the `@GeneratedValue` annotation](http://docs.doctrine-project.org/projects/doctrine-orm/en/current/reference/basic-mapping.html#identifier-generation-strategies) or use the `NONE` value ## Embedding the JSON-LD Context By default, the generated [JSON-LD context](https://www.w3.org/TR/json-ld/#the-context) (`@context`) is only referenced by an IRI. A client that uses JSON-LD must send a second HTTP request to retrieve it: ```json { "@context": "/contexts/Book", "@id": "/books/62", "@type": "Book", "name": "My awesome book", "author": "/people/59" } ``` You can configure API Platform to embed the JSON-LD context in the root document by adding the `jsonld_embed_context` attribute to the `#[ApiResource]` annotation: ```php true])] class Book { // ... } ``` ```yaml # The YAML syntax is only supported for Symfony # api/config/api_platform/resources/Book.yaml App\ApiResource\Book: normalizationContext: jsonldEmbedContext: true ``` The JSON output will now include the embedded context: ```json { "@context": { "@vocab": "http://localhost:8000/apidoc#", "hydra": "http://www.w3.org/ns/hydra/core#", "name": "https://schema.org/name", "author": "https://schema.org/author" }, "@id": "/books/62", "@type": "Book", "name": "My awesome book", "author": "/people/59" } ``` ## Collection Relation using Doctrine This is a special case where, in an entity, you have a `toMany` relation. By default, Doctrine will use an `ArrayCollection` to store your values. This is fine when you have a _read_ operation, but when you try to _write_ you can observe an issue where the response is not reflecting the changes correctly. It can lead to client errors even though the update was correct. Indeed, after an update on this relation, the collection looks wrong because `ArrayCollection`'s indices are not sequential. To change this, we recommend to use a getter that returns `$collectionRelation->getValues()`. Thanks to this, the relation is now a real array which is sequentially indexed. ```php cars = new ArrayCollection(); } public function addCar(DummyCar $car) { $this->cars[] = $car; } public function removeCar(DummyCar $car) { $this->cars->removeElement($car); } public function getCars() { return $this->cars->getValues(); } public function getId() { return $this->id; } } ``` For reference please check [#1534](https://github.com/api-platform/core/pull/1534). --- # Validation Source: https://api-platform.com/docs/core/validation/ # Validation API Platform takes care of validating the data sent to the API by the client (usually user data entered through forms). - For Symfony users, refer to the [Validation with Symfony documentation](/symfony/validation.md). - For Laravel users, refer to the [Validation with Laravel documentation](/laravel/validation.md). --- # Security Source: https://api-platform.com/docs/core/security/ # Security API Platform provides advanced authentication and authorization features to secure your API. When using API Platform for Symfony, API Platform leverages the [Symfony Security component](https://symfony.com/doc/current/security.html) to help you secure your API. When using API Platform for Laravel, it provides an integration with popular authentication packages for Laravel, and with the built-in authorization features of the framework. - For Symfony users, refer to the [Security with Symfony documentation](/symfony/security.md). - For Laravel users, refer to the [Security with Laravel documentation](/laravel/security.md). --- # Content Negotiation Source: https://api-platform.com/docs/core/content-negotiation/ # Content Negotiation The API system has built-in [content negotiation](https://en.wikipedia.org/wiki/Content_negotiation) capabilities. By default, only the [JSON-LD](https://json-ld.org) format is enabled. However API Platform supports many more formats and can be extended. The framework natively supports JSON-LD (and Hydra), GraphQL, JSON:API, HAL, YAML, CSV, HTML (API docs), raw JSON and raw XML. Using the raw JSON or raw XML formats is discouraged, prefer using JSON-LD instead, which provides more feature and is as easy to use. API Platform also supports [JSON Merge Patch (RFC 7396)](https://tools.ietf.org/html/rfc7396) the JSON:API [`PATCH`](https://jsonapi.org/format/#crud-updating) formats, as well as [Problem Details (RFC 7807)](https://tools.ietf.org/html/rfc7807), [Hydra](https://www.hydra-cg.com/spec/latest/core/#description-of-http-status-codes-and-errors) and [JSON:API](https://jsonapi.org/format/#errors) error formats.

Formats screencast
Watch the Formats screencast

API Platform will automatically detect the best resolving format depending on: - enabled formats (see below) - the requested format, specified in either [the `Accept` HTTP header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept) or as an extension appended to the URL Available formats are: | Format | Format name | MIME types | Backward Compatibility guaranteed | | ----------------------------------------------------- | ----------- | ----------------------------- | --------------------------------- | | [JSON-LD](https://json-ld.org) | `jsonld` | `application/ld+json` | yes | | [GraphQL](graphql.md) | n/a | n/a | yes | | [JSON:API](https://jsonapi.org/) | `jsonapi` | `application/vnd.api+json` | yes | | [HAL](https://stateless.group/hal_specification.html) | `jsonhal` | `application/hal+json` | yes | | [YAML](https://yaml.org/) | `yaml` | `application/yaml` | no | | [CSV](https://tools.ietf.org/html/rfc4180) | `csv` | `text/csv` | no | | [HTML](https://whatwg.org/) (API docs) | `html` | `text/html` | no | | [XML](https://www.w3.org/XML/) | `xml` | `application/xml`, `text/xml` | no | | [JSON](https://www.json.org/) | `json` | `application/json` | no | If the client's requested format is not specified, the response format will be the first format defined in the `formats` configuration key (see below). If the request format is not supported, an [Unsupported Media Type](https://developer.mozilla.org/fr/docs/Web/HTTP/Status/415) error will be returned. Examples showcasing how to use the different mechanisms are available [in the API Platform test suite](https://github.com/api-platform/core/blob/main/features/main/content_negotiation.feature). ## Configuring Formats Globally The first required step is to configure allowed formats. The following configuration will enable the support of XML (built-in) and of a custom format called `myformat` and having `application/vnd.myformat` as [MIME type](https://en.wikipedia.org/wiki/Media_type). ```yaml # api/config/packages/api_platform.yaml api_platform: formats: jsonld: ["application/ld+json"] jsonhal: ["application/hal+json"] jsonapi: ["application/vnd.api+json"] json: ["application/json"] xml: ["application/xml", "text/xml"] yaml: ["application/x-yaml"] csv: ["text/csv"] html: ["text/html"] myformat: ["application/vnd.myformat"] ``` To enable GraphQL support, [read the dedicated chapter](graphql.md). Because the Symfony Serializer component is able to serialize objects in XML, sending an `Accept` HTTP header with the `text/xml` string as value is enough to retrieve XML documents from our API. However API Platform knows nothing about the `myformat` format. We need to register an encoder and optionally a normalizer for this format. ## Configuring PATCH Formats By default, API Platform supports JSON Merge Patch and JSON:API PATCH formats. Support for the JSON:API PATCH format is automatically enabled if JSON:API support is enabled. JSON Merge Patch support must be enabled explicitly: ```yaml # api/config/packages/api_platform.yaml api_platform: patch_formats: json: ["application/merge-patch+json"] jsonapi: ["application/vnd.api+json"] ``` When support for at least one PATCH format is enabled, [an `Accept-Patch` HTTP header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Patch) containing the list of supported patch formats is automatically added to all HTTP responses for items. ## Configuring Error Formats API Platform will try to send to the client the error format matching with the format request with the `Accept` HTTP headers (or the URL extension). For instance, if a client request a JSON-LD representation of a resource, and an error occurs, then API Platform will serialize this error using the Hydra format (Hydra is a vocabulary for JSON-LD containing a standard representation of API errors). Available formats can also be configured: ```yaml # api/config/packages/api_platform.yaml api_platform: error_formats: jsonproblem: ["application/problem+json"] jsonld: ["application/ld+json"] # Hydra error formats jsonapi: ["application/vnd.api+json"] ``` ## Configuring Formats For a Specific Resource or Operation Support for specific formats can also be configured at resource and operation level using the `inputFormats` and `outputFormats` attributes. `inputFormats` controls the formats accepted in request bodies while `outputFormats` controls formats available for responses. The `format` attribute can be used as a shortcut, it sets both the `inputFormats` and `outputFormats` in one time. ```php ['text/csv']])] class Book { // ... } ``` In the example above, `xml` or `jsonld` will be allowed and there is no need to specify the MIME types as they are already defined in the configuration. Additionally the `csv` format is added with the MIME type `text/csv`. It is also important to notice that the usage of this attribute will override the formats defined in the configuration, therefore this configuration might disable the `json` or the `html` on this resource for example. You can specify different accepted formats at operation level too, it's especially convenient to configure formats available for the `PATCH` method: ```php ['text/csv']], operations: [ new Patch(inputFormats: ['json' => ['application/merge-patch+json']]), new GetCollection(), new Post(), ])] class Book { // ... } ``` ```yaml resources: App\Entity\Book: formats: 0: "jsonld" # format already defined in the config csv: "text/csv" operations: ApiPlatform\Metadata\Get: formats: json: ["application/merge-patch+json"] # works also with "application/merge-patch+json" ``` ```xml jsonld text/csv application/merge-patch+json ``` ## Supporting Custom Formats The API Platform content negotiation system is extendable. You can add support for formats not available by default by creating custom normalizers and encoders. Refer to the Symfony documentation to learn [how to create and register such classes](https://symfony.com/doc/current/serializer.html#adding-normalizers-and-encoders). Then, register the new format in the configuration: ```yaml # api/config/packages/api_platform.yaml api_platform: formats: # ... myformat: ["application/vnd.myformat"] ``` You will also need to declare an encoder which supports the new format: ```yaml services: app.api-platform.myformat.encoder: class: ApiPlatform\Serializer\JsonEncoder arguments: $format: "myformat" # The following lines are only needed if autoconfigure is disabled # tags: # - { name: 'serializer.encoder' } ``` API Platform will automatically call the serializer with your defined format name as `format` parameter during the deserialization process (`myformat` in the example). It will then return the result to the client with the requested MIME type using its built-in responder. For non-standard formats, [a vendor, vanity or unregistered MIME type should be used](https://en.wikipedia.org/wiki/Media_type#Vendor_tree). ### Reusing the API Platform Infrastructure Using composition is the recommended way to implement a custom normalizer. You can use the following template to start your own implementation of `CustomItemNormalizer`: ```yaml # api/config/services.yaml services: 'App\Serializer\CustomItemNormalizer': arguments: ["@api_platform.serializer.normalizer.item"] # Uncomment if you don't use the autoconfigure feature #tags: [ 'serializer.normalizer' ] # ... ``` ```php normalizer = $normalizer; } public function denormalize($data, $type, $format = null, array $context = []) { return $this->normalizer->denormalize($data, $type, $format, $context); } public function supportsDenormalization($data, $type, $format = null) { return $this->normalizer->supportsDenormalization($data, $type, $format); } public function normalize($object, $format = null, array $context = []) { return $this->normalizer->normalize($object, $format, $context); } public function supportsNormalization($data, $format = null) { return $this->normalizer->supportsNormalization($data, $format); } public function getSupportedTypes(?string $format): array { return [ 'object' => null, '*' => false, ]; } } ``` For example if you want to make the `csv` format work for even complex entities with a lot of hierarchy, you have to flatten or remove overly complex relations: ```php normalizer->normalize($object, $format, $context); if ('csv' !== $format || !is_array($result)) { return $result; } foreach ($result as $key => $value) { if (is_array($value) && array_keys(array_keys($value)) === array_keys($value)) { unset($result[$key]); } } return $result; } // ... } ``` Read more about the [serialization here](./serialization) or check out how to decode [Form Data](./form-data) ### Contributing Support for New Formats Adding support for **standard** formats upstream is welcome! We'll be glad to merge new encoders and normalizers in API Platform. ## JSON:API sparse fieldset and sort parameters > [!WARNING] > The SortFilter is for Eloquent, the Doctrine equivalent is the OrderFilter. The > config/api-platform.php is Laravel specific. When working with JSON:API you may want to declare the `SparseFieldset` and the `SortFilter` globally: ```php [ new QueryParameter(key: 'fields', filter: SparseFieldset::class), new QueryParameter(key: 'sort', filter: SortFilter::class), ], ]; ``` It's also possible to declare this per-resource such as: ```php use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\QueryParameter; use ApiPlatform\JsonApi\Filter\SparseFieldset; use ApiPlatform\Laravel\Eloquent\Filter\JsonApi\SortFilter; #[ApiResource] #[QueryParameter(key: 'fields', filter: SparseFieldset::class)] #[QueryParameter(key: 'sort', filter: SortFilter::class)] class Book extends Model {} ``` or per-operation: ```php use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\QueryParameter; use ApiPlatform\JsonApi\Filter\SparseFieldset; use ApiPlatform\Laravel\Eloquent\Filter\JsonApi\SortFilter; #[GetCollection( parameters: [ new QueryParameter(key: 'fields', filter: SparseFieldset::class), new QueryParameter(key: 'sort', filter: SortFilter::class) ] )] class Book extends Model {} ``` ### Advertising Linked Data Platform HTTP Headers (Allow & Accept-Post) API Platform automatically adds two HTTP headers to responses for resources: - **Allow** advertises enabled HTTP methods on the _current URI template_. - **Accept-Post** advertises POST-able media types (from operation input formats) and is only present when a POST operation exists for the template. > See [LDP §4.2 / Primer notes on Accept-Post](https://www.w3.org/TR/ldp/#Accept-Post) and typical > exposure via OPTIONS. --- # JSON:API Source: https://api-platform.com/docs/core/jsonapi/ # JSON:API API Platform supports the [JSON:API](https://jsonapi.org) format. When a client sends a request with an `Accept: application/vnd.api+json` header, API Platform serializes responses following the JSON:API specification. For details on enabling formats, see [content negotiation](content-negotiation.md). ## Entity Identifiers as Resource IDs We recommend configuring API Platform to use entity identifiers as the `id` field of JSON:API resource objects. This will become the default in 5.x: ```yaml # config/packages/api_platform.yaml api_platform: jsonapi: use_iri_as_id: false ``` With this configuration, the JSON:API `id` field contains the entity identifier (e.g., `"10"`) instead of the full IRI (e.g., `"/dummies/10"`). A `links.self` field is added to each resource object for navigation: ```json { "data": { "id": "10", "type": "Dummy", "links": { "self": "/dummies/10" }, "attributes": { "name": "Dummy #10" }, "relationships": { "relatedDummy": { "data": { "id": "1", "type": "RelatedDummy" } } } } } ``` Relationships reference related resources by entity identifier and `type`. ### Composite Identifiers Resources with composite identifiers use a semicolon-separated string as the `id` value (e.g., `"field1=val1;field2=val2"`). ### Resources Without a Standalone Item Endpoint API Platform must resolve the IRI for any resource that appears in a relationship. If a resource has no standalone `GET` item endpoint (for example, it is only exposed as a subresource), IRI resolution fails. Use the `NotExposed` operation to register a URI template for internal IRI resolution without exposing a public endpoint. A `NotExposed` operation registers the route internally but returns a `404` response when accessed directly: ```php [ 'allow_client_generated_id' => true, ], ``` ### Enabling Per Operation Use the `denormalizationContext` on the `#[Post]` operation to enable the feature for a single endpoint without affecting the rest of the API: ```php ['application/vnd.api+json']], operations: [ new Get(uriTemplate: '/books/{id}'), new Post( uriTemplate: '/books', denormalizationContext: [ItemNormalizer::ALLOW_CLIENT_GENERATED_ID => true], ), ], )] class Book { public ?string $id = null; public string $title = ''; } ``` A request that supplies `data.id` is then accepted: ```http POST /api/books HTTP/1.1 Accept: application/vnd.api+json Content-Type: application/vnd.api+json { "data": { "type": "Book", "id": "01932b4c-a3f1-7b7e-9e5b-3d8f1c2e4a6d", "attributes": { "title": "Hyperion" } } } ``` The supplied `id` is passed to the entity's `id` setter. The processor is responsible for persisting it. The response output schema still requires `id`; only the `POST` input schema marks it as optional. --- # Pagination Source: https://api-platform.com/docs/core/pagination/ # Pagination

Pagination screencast
Watch the Pagination screencast

API Platform has native support for paged collections. Pagination is enabled by default for all collections. Each collection contains 30 items per page. The activation of the pagination and the number of elements per page can be configured from: - the server-side (globally or per resource) - the client-side, via a custom GET parameter (disabled by default) When issuing a `GET` request on a collection containing more than 1 page (here `/books`), a [Hydra collection](https://www.hydra-cg.com/spec/latest/core/#collections) is returned. It's a valid JSON(-LD) document containing items of the requested page and metadata. ```json { "@context": "/contexts/Book", "@id": "/books", "@type": "Collection", "member": [ { "@id": "/books/1", "@type": "https://schema.org/Book", "name": "My awesome book" }, { "_": "Other items in the collection..." } ], "totalItems": 50, "view": { "@id": "/books?page=1", "@type": "PartialCollectionView", "first": "/books?page=1", "last": "/books?page=2", "next": "/books?page=2" } } ``` Hypermedia links to the first, the last, previous and the next page in the collection are displayed as well as the number of total items in the collection. The name of the page parameter can be changed with the following configuration: ## Changing page parameter name with Symfony ```yaml # api/config/packages/api_platform.yaml api_platform: collection: pagination: page_parameter_name: _page ``` ## Changing page parameter name with Laravel ```php [ 'pagination' => [ 'page_parameter_name' => '_page', ] ], ]; ``` ## Disabling the Pagination Paginating collections is generally accepted as a good practice. It allows browsing large collections without too much overhead as well as preventing [DOS attacks](https://en.wikipedia.org/wiki/Denial-of-service_attack). However, for small collections, it can be convenient to fully disable the pagination. ### Disabling the Pagination Globally #### Disabling the Pagination Globally with Symfony The pagination can be disabled for all resources using this configuration: ```yaml # api/config/packages/api_platform.yaml api_platform: defaults: pagination_enabled: false ``` #### Disabling the Pagination Globally with Laravel The pagination can be disabled for all resources using this configuration: ```php [ 'pagination_enabled' => false, ], ]; ``` ### Disabling the Pagination For a Specific Resource It can also be disabled for a specific resource: ```php ### Disabling the Pagination For a Specific Operation You can also disable an operation for a specific operation: ```php ``` ### Allowing the Client to Control Pagination By default, clients cannot enable or disable pagination via query parameters (`pagination_client_enabled` defaults to `false`). You can allow clients to control pagination by setting this option to `true`. #### Allowing the Client to Control Pagination Globally ##### Allowing the Client to Control Pagination Globally with Symfony To enable this feature globally, use the following configuration: ```yaml # api/config/packages/api_platform.yaml api_platform: defaults: pagination_client_enabled: true collection: pagination: enabled_parameter_name: pagination # optional ``` The pagination can now be enabled or disabled by adding a query parameter named `pagination`: - `GET /books?pagination=false`: disabled - `GET /books?pagination=true`: enabled Any value accepted by the [`FILTER_VALIDATE_BOOLEAN`](https://www.php.net/manual/en/filter.filters.validate.php) filter can be used as the value. ##### Allowing the Client to Control Pagination Globally with Laravel To enable this feature globally, use the following configuration: ```php [ 'pagination_client_enabled' => true, ], 'collection' => [ 'pagination' => [ 'enabled_parameter_name' => 'pagination', // optional ], ], ]; ``` The pagination can now be enabled or disabled by adding a query parameter named `pagination`: - `GET /books?pagination=false`: disabled - `GET /books?pagination=true`: enabled Any value accepted by the [`FILTER_VALIDATE_BOOLEAN`](https://www.php.net/manual/en/filter.filters.validate.php) filter can be used as the value. #### Allowing the Client to Control Pagination For a Specific Resource The client ability to control pagination can also be enabled for a specific resource: ```php [ 'pagination_items_per_page' => 30, ], ]; ``` ### Changing the Number of Items per Page For a Specific Resource ```php [ 'pagination_client_items_per_page' => true, ], 'collection' => [ 'pagination' => [ 'items_per_page_parameter_name' => 'itemsPerPage', ], ], ]; ``` The number of items per page can now be changed adding a query parameter named `itemsPerPage`: `GET /books?itemsPerPage=20`. #### Changing the Number of Items per Page Client-side For a Specific Resource Changing the number of items per page can be enabled (or disabled) for a specific resource: ```php [ 'pagination_maximum_items_per_page' => 50, ], ]; ``` ### Changing Maximum Items Per Page For a Specific Resource ```php [ 'pagination_partial' => true, // Disabled by default ], ]; ``` ### Partial Pagination For a Specific Resource ```php [ 'pagination_client_partial' => true, // Disabled by default ], 'collection' => [ 'pagination' => [ 'partial_parameter_name' => 'partial' // Default value ], ], ]; ``` The partial pagination retrieval can now be changed by toggling a query parameter named `partial`: `GET /books?partial=true`. #### Partial Pagination Client-side For a Specific Resource ```php 'id', 'direction' => 'DESC'] ] )] #[ApiFilter(RangeFilter::class, properties: ["id"])] #[ApiFilter(OrderFilter::class, properties: ["id" => "DESC"])] class Book { // ... } ``` To know more about cursor-based pagination take a look at [this blog post on medium (draft)](https://medium.com/@sroze/74fd1d324723). ## Pagination for Custom State Providers If you are using custom state providers (not the provided Doctrine ORM, Doctrine ODM, ElasticSearch or Laravel Eloquent ones) and if you want your results to be paginated, you will need to return an instance of a `ApiPlatform\State\Pagination\PartialPaginatorInterface` or `ApiPlatform\State\Pagination\PaginatorInterface`. A few existing classes are provided to make it easier to paginate the results: - `ApiPlatform\State\Pagination\ArrayPaginator` - `ApiPlatform\State\Pagination\TraversablePaginator` ## Controlling The Behavior of The Doctrine ORM Paginator The [PaginationExtension](https://github.com/api-platform/core/blob/main/src/Doctrine/Orm/Extension/PaginationExtension.php) of API Platform performs some checks on the `QueryBuilder` to guess, in most common cases, the correct values to use when configuring the Doctrine ORM Paginator: - `$fetchJoinCollection` argument: Whether there is a join to a collection-valued association. When set to `true`, the Doctrine ORM Paginator will perform an additional query, in order to get the correct number of results. You can configure this using the `paginationFetchJoinCollection` attribute on a resource or on a per-operation basis: ```php tokenStorage = $tokenStorage; } public function getBooksByFavoriteAuthor(int $page = 1): Paginator { $firstResult = ($page -1) * self::ITEMS_PER_PAGE; $user = $this->tokenStorage->getToken()->getUser(); $queryBuilder = $this->createQueryBuilder(); $queryBuilder->select('b') ->from(Book::class, 'b') ->where('b.author = :author') ->setParameter('author', $user->getFavoriteAuthor()->getId()) ->andWhere('b.publicatedOn IS NOT NULL'); $criteria = Criteria::create() ->setFirstResult($firstResult) ->setMaxResults(self::ITEMS_PER_PAGE); $queryBuilder->addCriteria($criteria); $doctrinePaginator = new DoctrinePaginator($queryBuilder); $paginator = new Paginator($doctrinePaginator); return $paginator; } } ``` The Controller would look like this: ```php query->get('page', 1); return $bookRepository->getBooksByFavoriteAuthor($page); } } ``` The service needs to use the proper repository method. You can also use the Query object inside the repository method and pass it to the Paginator instead of passing the QueryBuilder and using Criteria. Second Example: ```php tokenStorage->getToken()->getUser(); $queryBuilder = $this->createQueryBuilder(); $queryBuilder->select('b') ->from(Book::class, 'b') ->where('b.author = :author') ->setParameter('author', $user->getFavoriteAuthor()->getId()) ->andWhere('b.publicatedOn IS NOT NULL'); $query = $queryBuilder->getQuery() ->setFirstResult($firstResult) ->setMaxResults(self::ITEMS_PER_PAGE); $doctrinePaginator = new DoctrinePaginator($query); $paginator = new Paginator($doctrinePaginator); return $paginator; } } ``` --- # Deprecating Resources and Properties (Alternative to Versioning) Source: https://api-platform.com/docs/core/deprecations/ # Deprecating Resources and Properties (Alternative to Versioning) A best practice regarding web API development is to apply [the evolution strategy](https://phil.tech/api/2018/05/02/api-evolution-for-rest-http-apis/) to indicate to client applications which resource types, operations and fields are deprecated and shouldn't be used anymore. While versioning an API requires modifying all clients to upgrade, even the ones not impacted by the changes. It's a tedious task that should be avoided as much as possible. On the other hand, the evolution strategy (also known as versionless APIs) consists of deprecating the fields, resources types or operations that will be removed at some point. Most modern API formats including [JSON-LD / Hydra](content-negotiation.md), [GraphQL](graphql.md) and [OpenAPI](openapi.md) allow you to mark resources types, operations or fields as deprecated. ## Deprecating Resource Classes, Operations and Properties When using API Platform, it's easy to mark a whole resource, a specific operation or a specific property as deprecated. All documentation formats mentioned in the introduction will then automatically take the deprecation into account. To deprecate a resource class, use the `deprecationReason` attribute: ```php ```php - With JSON-lD / Hydra, [an `owl:deprecated` annotation property](https://www.w3.org/TR/owl2-syntax/#Annotation_Properties) will be added to the appropriate data structure - With Swagger / OpenAPI, [a `deprecated` property](https://swagger.io/docs/specification/2-0/paths-and-operations/) will be added - With GraphQL, the [`isDeprecated` and `deprecationReason` properties](https://facebook.github.io/graphql/June2018/#sec-Deprecation) will be added to the schema ## Setting the `Sunset` HTTP Header to Indicate When a Resource or an Operation Will Be Removed [The `Sunset` HTTP response header (RFC 8594)](https://www.rfc-editor.org/rfc/rfc8594) indicates that a URI is likely to become unresponsive at a specified point in the future. It is especially useful to indicate when a deprecated URL will not be available anymore. Thanks to the `sunset` attribute, API Platform makes it easy to set this header for all URLs related to a resource class: ```php '1688169599', // Unix timestamp ], links: [ new Link('deprecation', 'https://developer.example.com/deprecation'), ], ), ], )] class DeprecationHeader { // ... } ``` ## Path versioning > [!NOTE] REST and GraphQL architectures recommend to use deprecations instead of path versioning. You can prefix your URI Templates and change the representation using serialization groups: ```php ['v1']])] #[Get(uriTemplate: '/v2/books/{id}', normalizationContext: ['groups' => ['v2']])] class Parchment { #[Groups(['v1'])] public $name; #[Groups(['v2'])] public $title; } ``` > [!NOTE] It's also possible to use the configuration `route_prefix` to prefix all your operations. --- # Overriding Default Order Source: https://api-platform.com/docs/core/default-order/ # Overriding Default Order API Platform provides an easy way to override the default order of items in your collection. By default, items in the collection are ordered in ascending (ASC) order by their resource identifier(s). If you want to customize this order, you must add an `order` attribute on your ApiResource annotation: ```php 'ASC'])] class Book { // ... /** * ... */ public $foo; // ... } ``` ```yaml # api/config/api_platform/resources/Book.yaml # The YAML syntax is only supported for Symfony App\ApiResource\Book: order: foo: ASC ``` This `order` attribute is used as an array: the key defines the order field, the values defines the direction. If you only specify the key, `ASC` direction will be used as default. For example, to order by `foo` & `bar`: ```php It's also possible to configure the default order on an association property: ```php Another possibility is to apply the default order for a specific collection operation. ```php 'DESC'])], new GetCollection(name: 'get_asc_custom', uriTemplate: 'custom_collection_asc_foos', order: ['name' => 'ASC'])] ])] class Book { // ... /** * @var string */ public $name; // ... } ``` ```yaml # api/config/api_platform/resources/Book.yaml # The YAML syntax is only supported for Symfony App\ApiResource\Book: ApiPlatform\Metadata\GetCollection: ~ get_desc_custom: class: ApiPlatform\Metadata\GetCollection uriTemplate: custom_collection_desc_foos order: name: DESC get_asc_custom: class: ApiPlatform\Metadata\GetCollection uriTemplate: custom_collection_asc_foos order: name: ASC ``` ## Global Default Order and Operation Order Precedence `api_platform.defaults.order` (set under the `defaults:` key in `api_platform.yaml`) applies an order to every operation. It is not a fallback that yields to operation-level configuration — it is a cross-cutting invariant that is always applied. When an operation also defines its own `order`, the two arrays are **merged**: the global default keys come first, followed by the operation-level keys. This means the global default takes priority in the SQL `ORDER BY` clause, and the operation-level keys act as tie-breakers. For example, given this configuration: ```yaml # config/packages/api_platform.yaml api_platform: defaults: order: createdAt: DESC ``` And this resource: ```php 'ASC']), ])] class Book { public \DateTimeImmutable $createdAt; public string $title; } ``` The effective order for the `GetCollection` operation is `['createdAt' => 'DESC', 'title' => 'ASC']`, producing `ORDER BY createdAt DESC, title ASC`. The global default is prepended to the operation-level order, not replaced by it. This behavior is intentional. `api_platform.defaults.order` is designed for invariants such as "always order by `createdAt DESC`" that must hold across all collections regardless of which operation is called. **If you want an operation to use a specific order with no global keys prepended**, do not set `api_platform.defaults.order`. Instead, set the `order` explicitly on each operation or resource where you need it. For more control over ordering logic, implement a custom [Doctrine ORM extension](extensions.md) that replaces the built-in `OrderExtension`. --- # Performance and Cache Source: https://api-platform.com/docs/core/performance/ # Performance and Cache ## Enabling the Built-in HTTP Cache Invalidation System Exposing a hypermedia API has [many advantages](http://blog.theamazingrando.com/in-band-vs-out-of-band.html). One is the ability to know exactly which resources are included in HTTP responses created by the API. We used this specificity to make API Platform apps blazing fast. When the cache mechanism [is enabled](configuration.md), API Platform collects identifiers of every resource included in a given HTTP response (including lists, embedded documents, and subresources) and returns them in a special HTTP header called [Cache-Tags](https://support.cloudflare.com/hc/en-us/articles/206596608-How-to-Purge-Cache-Using-Cache-Tags-Enterprise-only-). A caching [reverse proxy](https://en.wikipedia.org/wiki/Reverse_proxy) supporting cache tags (e.g. Varnish, Cloudflare, Fastly) must be put in front of the web server and store all responses returned by the API with a high [TTL](https://en.wikipedia.org/wiki/Time_to_live). This means that after the first request, all subsequent requests will not hit the web server, and will be served instantly from the cache. When a resource is modified, API Platform takes care of purging all responses containing it in the proxy’s cache. This ensures that the content served will always be fresh because the cache is purged in real time. Support for most specific cases such as the invalidation of collections when a document is added or removed or for relationships and inverse relations is built-in. ### Integrations #### Built-in Caddy HTTP Cache The Docker setup of the API Platform Symfony variant relies on the [Caddy web server](https://caddyserver.com) which provides an official HTTP cache module called [cache-handler](https://github.com/caddyserver/cache-handler), that is based on [Souin](https://github.com/darkweak/souin). The integration using the cache handler is quite simple. You just have to update the `api/Dockerfile` to build your caddy instance with the HTTP cache ```diff # Versions -FROM dunglas/frankenphp:1-php8.3 AS frankenphp_upstream +FROM dunglas/frankenphp:1-builder-php8.3 AS builder +COPY --from=caddy:builder /usr/bin/xcaddy /usr/bin/xcaddy + +RUN apt-get update && apt-get install --no-install-recommends -y \ + git + +ENV CGO_ENABLED=1 XCADDY_SETCAP=1 XCADDY_GO_BUILD_FLAGS="-ldflags \"-w -s -extldflags '-Wl,-z,stack-size=0x80000'\"" +RUN xcaddy build \ + --output /usr/local/bin/frankenphp \ + --with github.com/dunglas/frankenphp/caddy \ + --with github.com/dunglas/mercure/caddy \ + --with github.com/dunglas/vulcain/caddy \ + --with github.com/dunglas/caddy-cbrotli \ + # You should use another storage than the default one (e.g. otter). + # The list of the available storages can be find either on the documentation website (https://docs.souin.io/docs/storages/) or on the storages repository https://github.com/darkweak/storages + --with github.com/caddyserver/cache-handler + # Or use the following lines instead of the cache-handler one for the latest improvements + #--with github.com/darkweak/souin/plugins/caddy \ + #--with github.com/darkweak/storages/otter/caddy + +FROM dunglas/frankenphp:latest AS frankenphp_upstream +COPY --from=builder --link /usr/local/bin/frankenphp /usr/local/bin/frankenphp ``` Update your Caddyfile with the following configuration: ```caddyfile { cache # ... } # ... ``` This will tell to caddy to use the HTTP cache and activate the tag-based invalidation API. You can refer to the [cache-handler documentation](https://github.com/caddyserver/cache-handler) or the [souin website documentation](https://docs.souin.io) to learn how to configure the HTTP cache server. Set up HTTP cache invalidation in your API Platform project using the Symfony or Laravel configuration below: ##### Cache Invalidation Configuration using Symfony ```yaml api_platform: http_cache: invalidation: # We assume that your API can reach your caddy/frankenphp instance by the hostname http://php. # The endpoint /souin-api/souin is the default path to the invalidation API. urls: ["http://php:2019/souin-api/souin"] purger: api_platform.http_cache.purger.souin ``` ##### Cache Invalidation Configuration using Laravel ```php [ 'invalidation' => [ // We assume that your API can reach your caddy instance by the hostname http://caddy. // The endpoint /souin-api/souin is the default path to the invalidation API. 'urls' => ['http://caddy/souin-api/souin'], 'purger' => 'api_platform.http_cache.purger.souin', ] ], ]; ``` Don't forget to set your `Cache-Control` directive to enable caching on your API resource class. This can be achieved using the `cacheHeaders` property: ```php use ApiPlatform\Metadata\ApiResource; #[ApiResource( cacheHeaders: [ 'public' => true, 'max_age' => 60, ] )] class Book { // ... } ``` And voilà, you have a fully working HTTP cache with an invalidation API. #### Varnish Integration with Varnish and Doctrine ORM is shipped with the core library. ##### Varnish cache invalidation system using Symfony Add the following configuration to enable the cache invalidation system: ```yaml api_platform: http_cache: invalidation: enabled: true varnish_urls: ["%env(VARNISH_URL)%"] defaults: cache_headers: max_age: 0 shared_max_age: 3600 vary: ["Content-Type", "Authorization", "Origin"] ``` ##### Varnish cache invalidation system using Laravel Add the following configuration to enable the cache invalidation system: ```php [ 'invalidation' => [ 'enabled' => true, 'varnish_urls' => ['%env(VARNISH_URL)%'], ] ], 'defaults' => [ 'cache_headers' => [ 'max_age' => 0, 'shared_max_age' => 3600, 'vary' => ['Content-Type', 'Authorization', 'Origin'], ] ], ]; ``` ## Configuration Support for reverse proxies other than Varnish or Caddy with the HTTP cache module can be added by implementing the `ApiPlatform\HttpCache\PurgerInterface`. Three purgers are available, the built-in caddy HTTP cache purger (`api_platform.http_cache.purger.souin`), the HTTP tags (`api_platform.http_cache.purger.varnish.ban`), the surrogate key implementation (`api_platform.http_cache.purger.varnish.xkey`). You can specify the implementation using the `purger` configuration node, for example, to use the `Xkey` implementation see the Symfony or Laravel configuration below: ### Exemple of Varnish Xkey implementation using Symfony ```yaml api_platform: http_cache: invalidation: enabled: true varnish_urls: ["%env(VARNISH_URL)%"] purger: "api_platform.http_cache.purger.varnish.xkey" public: true defaults: cache_headers: max_age: 0 shared_max_age: 3600 vary: ["Content-Type", "Authorization", "Origin"] invalidation: xkey: glue: ", " ``` ### Exemple of Varnish Xkey implementation using Laravel ```php [ 'invalidation' => [ 'enabled' => true, 'varnish_urls' => ['%env(VARNISH_URL)%'], 'purger' => 'api_platform.http_cache.purger.varnish.xkey', ], 'public' => true, ], 'defaults' => [ 'cache_headers' => [ 'max_age' => 0, 'shared_max_age' => 3600, 'vary' => ['Content-Type', 'Authorization', 'Origin'], 'invalidation' => [ 'xkey' => [ 'glue' => ', ', ] ], ] ], ]; ``` In addition to the cache invalidation mechanism, you may want to [use HTTP/2 Server Push to pre-emptively send relations to the client](push-relations.md). ### Extending Cache-Tags for Invalidation Sometimes you need individual resources like `/me`. To work properly, the `Cache-Tags` header needs to be augmented with these resources. Here is an example of how this can be done: ```php ['extendResources', EventPriorities::POST_READ] ]; } public function extendResources(RequestEvent $event): void { $request = $event->getRequest(); $class = $request->attributes->get('_api_resource_class'); if ($class === User::class) { $resources = [ '/me' ]; $request->attributes->set('_resources', $request->attributes->get('_resources', []) + (array)$resources); } } } ``` ## Setting Custom HTTP Cache Headers The `cacheHeaders` attribute can be used to set custom HTTP cache headers: ```php use ApiPlatform\Metadata\ApiResource; #[ApiResource( cacheHeaders: [ 'max_age' => 60, 'shared_max_age' => 120, 'vary' => ['Authorization', 'Accept-Language'], ] )] class Book { // ... } ``` For all endpoints related to this resource class, the following HTTP headers will be set: ```http Cache-Control: max-age=60, public, s-maxage=120 Vary: Authorization, Accept-Language ``` It's also possible to set different cache headers per operation: ```php use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Get; #[ApiResource] #[Get( cacheHeaders: [ 'max_age' => 60, 'shared_max_age' => 120, ] )] class Book { // ... } ``` ## Enabling the Metadata Cache Computing metadata used by the bundle is a costly operation. Fortunately, metadata can be computed once and then cached. API Platform internally uses a [PSR-6](https://www.php-fig.org/psr/psr-6/) cache. If the Symfony Cache component is available (the default in the API Platform Symfony variant), it automatically enables support for the best cache adapter available. Best performance is achieved using [APCu](https://github.com/krakjoe/apcu). Be sure to have the APCu extension installed on your production server (this is the case by default in the Docker image provided by the API Platform Symfony variant). API Platform will automatically use it. ## Using FrankenPHP's Worker Mode API response times can be significantly improved by enabling [FrankenPHP's worker mode](https://frankenphp.dev/docs/worker/). This feature is enabled by default in the production environment of the API Platform Symfony variant. ## Doctrine Queries and Index ### Search Filter When using the `SearchFilter` and case insensitivity, Doctrine will use the `LOWER` SQL function. Depending on your driver, you may want to carefully index it by using a [function-based index,](https://use-the-index-luke.com/sql/where-clause/functions/case-insensitive-search) or it will impact performance with a huge collection. [Here are some examples to index LIKE filters](https://use-the-index-luke.com/sql/where-clause/searching-for-ranges/like-performance-tuning) depending on your database driver. ### Eager Loading By default, Doctrine comes with [lazy loading](https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/working-with-objects.html#by-lazy-loading) - usually a killer time-saving feature but also a performance killer with large applications. Fortunately, Doctrine offers another approach to solve this problem: [eager loading](https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/working-with-objects.html#by-eager-loading). This can easily be enabled for a relation: `#[ORM\ManyToOne(fetch: "EAGER")]`. By default, in API Platform, we chose to force eager loading for all relations, with or without the Doctrine `fetch` attribute. Thanks to the eager loading [extension](extensions.md). The `EagerLoadingExtension` will join every readable association according to the serialization context. If you want to fetch an association that is not serializable, you have to bypass `readable` and `readableLink` by using the `fetchEager` attribute on the property declaration, for example: ```php ... #[ApiProperty(fetchEager: true)] public $foo; ... ``` > **Warning**: to trigger the `EagerLoadingExtension` you must use > [Serializer groups](serialization.md) on relations properties. #### Max Joins There is a default restriction with this feature. We allow up to 30 joins per query. Beyond that, an `ApiPlatform\Exception\RuntimeException` exception will be thrown but this value can easily be increased with a bit of configuration: ```yaml # api/config/packages/api_platform.yaml api_platform: eager_loading: max_joins: 100 ``` Be careful when you exceed this limit, it's often caused by the result of a circular reference. [Serializer groups](serialization.md) can be a good solution to fix this issue. #### Fetch Partial If you want to fetch only partial data according to serialization groups, you can enable the `fetch_partial` parameter: ```yaml # api/config/packages/api_platform.yaml api_platform: eager_loading: fetch_partial: true ``` It is disabled by default. If enabled, Doctrine ORM entities will not work as expected if any of the other fields are used. #### Force Eager As mentioned above, by default we force eager loading for all relations. This behavior can be modified in the configuration to apply it only on join relations having the `EAGER` fetch mode: ```yaml # api/config/packages/api_platform.yaml api_platform: eager_loading: force_eager: false ``` #### Override at Resource and Operation Level When eager loading is enabled, whatever the status of the `force_eager` parameter, you can easily override it directly from the configuration of each resource. You can do this at the resource level, at the operation level, or both: ```php **Note:** JsonStreamer only serializes **public properties**, and it relies on their type > declarations to build its encoders. Make sure every serialized property is public and typed; > values exposed only through getters or non-public properties will not be streamed. ## Profiling with Blackfire.io Blackfire.io allows you to monitor the performance of your applications. For more information, visit the [Blackfire.io website](https://blackfire.io/). To configure Blackfire.io follow these steps: 1. Add the following to your `compose.override.yaml` file: ```yaml services: # ... blackfire: image: blackfire/blackfire:2 environment: # Exposes the host BLACKFIRE_SERVER_ID and TOKEN environment variables. - BLACKFIRE_SERVER_ID - BLACKFIRE_SERVER_TOKEN - BLACKFIRE_DISABLE_LEGACY_PORT=1 ``` 1. Add your Blackfire.io ID and server token to your `.env` file at the root of your project (be sure not to commit this to a public repository): ```console BLACKFIRE_SERVER_ID=xxxxxxxxxx BLACKFIRE_SERVER_TOKEN=xxxxxxxxxx ``` Or set it in the console before running Docker commands: ```console export BLACKFIRE_SERVER_ID=xxxxxxxxxx export BLACKFIRE_SERVER_TOKEN=xxxxxxxxxx ``` 1. Install and configure the Blackfire probe in the app container, by adding the following to your `./Dockerfile`: ```dockerfile RUN version=$(php -r "echo PHP_MAJOR_VERSION.PHP_MINOR_VERSION.(PHP_ZTS ? '-zts' : '');") \ && architecture=$(uname -m) \ && curl -A "Docker" -o /tmp/blackfire-probe.tar.gz -D - -L -s https://blackfire.io/api/v1/releases/probe/php/linux/$architecture/$version \ && mkdir -p /tmp/blackfire \ && tar zxpf /tmp/blackfire-probe.tar.gz -C /tmp/blackfire \ && mv /tmp/blackfire/blackfire-*.so $(php -r "echo ini_get ('extension_dir');")/blackfire.so \ && printf "extension=blackfire.so\nblackfire.agent_socket=tcp://blackfire:8307\n" > $PHP_INI_DIR/conf.d/blackfire.ini \ && rm -rf /tmp/blackfire /tmp/blackfire-probe.tar.gz ``` 1. Rebuild and restart all your containers ```console docker compose build docker compose up --wait ``` For details on how to perform profiling, see [the Blackfire.io documentation](https://blackfire.io/docs/integrations/docker#using-the-client-for-http-profiling). --- # Extensions for Doctrine, Eloquent and Elasticsearch Source: https://api-platform.com/docs/core/extensions/ # Extensions for Doctrine, Eloquent and Elasticsearch API Platform provides a system to extend queries on items and collections. Extensions are specific to Doctrine, Eloquent and Elasticsearch-PHP, and therefore, the Doctrine ORM / MongoDB ODM support, Eloquent support or the Elasticsearch reading support must be enabled to use this feature. If you use custom providers it's up to you to implement your own extension system or not. You can find a working example of a custom extension in the [API Platform's demo application](https://github.com/api-platform/demo/blob/4.0/api/src/Doctrine/Orm/Extension/BookmarkQueryCollectionExtension.php). ## Custom Doctrine ORM Extension Custom extensions must implement the `ApiPlatform\Doctrine\Orm\Extension\QueryCollectionExtensionInterface` and / or the `ApiPlatform\Doctrine\Orm\Extension\QueryItemExtensionInterface` interfaces, to be run when querying for a collection of items and when querying for an item respectively. If you use [custom state providers](state-providers.md), they must support extensions and be aware of active extensions to work properly. ### Example In the following example, we will see how to always get the offers owned by the current user. We will set up an exception, whenever the user has the `ROLE_ADMIN`. Given these two entities: ```php addWhere($queryBuilder, $resourceClass); } public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, ?Operation $operation = null, array $context = []): void { $this->addWhere($queryBuilder, $resourceClass); } private function addWhere(QueryBuilder $queryBuilder, string $resourceClass): void { if (Offer::class !== $resourceClass || $this->security->isGranted('ROLE_ADMIN') || null === $user = $this->security->getUser()) { return; } $rootAlias = $queryBuilder->getRootAliases()[0]; $queryBuilder->andWhere(sprintf('%s.user = :current_user', $rootAlias)); $queryBuilder->setParameter('current_user', $user->getId()); } } ``` Note that the default `rootAlias` is "o" in the bundled `ItemProvider` and `CollectionProvider`, so you should use different aliases for your custom joins in your extension. Finally, if you're not using the autoconfiguration, you have to register the custom extension with either of those tags: ```yaml # api/config/services.yaml services: # ... 'App\Doctrine\CurrentUserExtension': tags: - { name: api_platform.doctrine.orm.query_extension.collection } - { name: api_platform.doctrine.orm.query_extension.item } ``` The `api_platform.doctrine.orm.query_extension.collection` tag will register this service as a collection extension. The `api_platform.doctrine.orm.query_extension.item` does the same thing for items. Note that your extensions should have a positive priority if defined. Internal extensions have negative priorities, for reference: | Service name | Priority | Class | | ---------------------------------------------------------------------- | -------- | -------------------------------------------------------------- | | `api_platform.doctrine.orm.query_extension.eager_loading` (item) | -8 | ApiPlatform\Doctrine\Orm\Extension\EagerLoadingExtension | | `api_platform.doctrine.orm.query_extension.filter` | -16 | ApiPlatform\Doctrine\Orm\Extension\FilterExtension | | `api_platform.doctrine.orm.query_extension.filter_eager_loading` | -17 | ApiPlatform\Doctrine\Orm\Extension\FilterEagerLoadingExtension | | `api_platform.doctrine.orm.query_extension.eager_loading` (collection) | -18 | ApiPlatform\Doctrine\Orm\Extension\EagerLoadingExtension | | `api_platform.doctrine.orm.query_extension.order` | -32 | ApiPlatform\Doctrine\Orm\Extension\OrderExtension | | `api_platform.doctrine.orm.query_extension.pagination` | -64 | ApiPlatform\Doctrine\Orm\Extension\PaginationExtension | #### Blocking Anonymous Users using Symfony This example adds a `WHERE` clause condition only when a fully authenticated user without `ROLE_ADMIN` tries to access a resource. It means that anonymous users will be able to access all data. To prevent this potential security issue, the API must ensure that the current user is authenticated. To secure the access to endpoints, use the following access control rule: ```yaml # app/config/package/security.yaml security: # ... access_control: # ... - { path: ^/offers, roles: IS_AUTHENTICATED_FULLY } - { path: ^/users, roles: IS_AUTHENTICATED_FULLY } ``` ## Custom Doctrine MongoDB ODM Extension Creating custom extensions is the same as with Doctrine ORM. The interfaces are: - `ApiPlatform\Doctrine\Odm\Extension\AggregationItemExtensionInterface` and `ApiPlatform\Doctrine\Odm\Extension\AggregationCollectionExtensionInterface` to add stages to the [aggregation builder](https://www.doctrine-project.org/projects/doctrine-mongodb-odm/en/current/reference/aggregation-builder.html). - `ApiPlatform\Doctrine\Odm\Extension\AggregationResultItemExtensionInterface` and `ApiPlatform\Doctrine\Odm\Extension\AggregationResultCollectionExtensionInterface` to return a result. The tags are `api_platform.doctrine_mongodb.odm.aggregation_extension.item` and `api_platform.doctrine_mongodb.odm.aggregation_extension.collection`. The custom extensions receive the [aggregation builder](https://www.doctrine-project.org/projects/doctrine-mongodb-odm/en/current/reference/aggregation-builder.html), used to execute [complex operations on data](https://docs.mongodb.com/manual/aggregation/). ## Custom Eloquent Extension Custom extensions must implement `ApiPlatform\Laravel\Eloquent\Extension\QueryExtensionInterface` and be tagged with the interface name, so they will be executed both when querying for a collection of items and when querying for an item. ```php getModel() instanceof Offer) { return $builder; } if (!$builder->getModel() instanceof Offer || !($user = Auth::user()) instanceof User || $user->is_admin) { return $builder; } return $builder->where('user_id', $user->id); } } ``` ```php app->tag([OfferExtension::class], QueryExtensionInterface::class); } } ``` ## Custom Elasticsearch Extension Currently only extensions querying for a collection of items through a [search request](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-body.html) are supported. So your custom extensions must implement the `RequestBodySearchCollectionExtensionInterface`. Register your custom extensions as services and tag them with the `api_platform.elasticsearch.request_body_search_extension.collection` tag. --- # Using Data Transfer Objects (DTOs) Source: https://api-platform.com/docs/core/dto/ # Using Data Transfer Objects (DTOs)

Custom Resources screencast Watch the Custom Resources screencast

The DTO pattern isolates your public API contract from your internal data model (Entities). This decoupling allows you to evolve your data structure without breaking the API and provides finer control over validation and serialization. In API Platform, [the general design considerations](design.md) recommended pattern is [DTO](https://en.wikipedia.org/wiki/Data_transfer_object) as a Resource: the class marked with `#[ApiResource]` is the DTO, effectively becoming the "contract" of your API. This reference covers three implementation strategies: - For automated CRUD operations, link a DTO Resource to an Entity: [State Options](#1-the-dto-resource-state-options) - For automated Write operation, use input DTOs with stateOptions: [Automated Mapped Inputs](#2-automated-mapped-inputs-and-outputs) - For specific business actions, use input DTOs with custom State Processors : [Custom Business Logic](#3-custom-business-logic-custom-processor) ## 1. The DTO Resource (State Options) > [!WARNING] This is a Symfony only feature in 4.2 and is not working properly without > symfony/object-mapper:^7.4 or symfony/object-mapper:^8.0 You can map a DTO Resource directly to a Doctrine Entity using stateOptions. This automatically configures the built-in State Providers and Processors to fetch/persist data using the Entity and map it to your Resource (DTO) using the Symfony Object Mapper. > [!WARNING] You must apply the #[Map] attribute to your DTO class. This signals API Platform to use > the Object Mapper for transforming data between the Entity and the DTO. ### The Entity First, ensure your entity is a standard Doctrine entity. ```php // src/Entity/Book.php namespace App\Entity; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Validator\Constraints as Assert; #[ORM\Entity] class Book { #[ORM\Id, ORM\GeneratedValue, ORM\Column] public private(set) int $id; #[ORM\Column(length: 13)] #[Assert\NotBlank, Assert\Isbn] public string $isbn; #[ORM\Column(length: 255)] #[Assert\NotBlank, Assert\Length(max: 255)] public string $title; #[ORM\Column(length: 255)] #[Assert\NotBlank, Assert\Length(max: 255)] public string $description; #[ORM\Column] #[Assert\PositiveOrZero] public int $price; } ``` ### The API Resource (Main DTO) The Resource DTO handles the public representation. We use `#[Map]` to handle differences between the internal model (title) and the public API (name), as well as value transformations (`formatPrice`). ```php // src/Api/Resource/Book.php namespace App\Api\Resource; use ApiPlatform\Doctrine\Orm\State\Options; use ApiPlatform\Metadata\ApiResource; use App\Entity\Book as BookEntity; use Symfony\Component\ObjectMapper\Attribute\Map; #[ApiResource( shortName: 'Book', // 1. Link this DTO to the Doctrine Entity stateOptions: new Options(entityClass: BookEntity::class), operations: [ /* ... defined in next sections ... */ ] )] #[Map(source: BookEntity::class)] final class Book { public int $id; // 2. Map the Entity 'title' property to the DTO 'name' property #[Map(source: 'title')] public string $name; public string $description; public string $isbn; // 3. Use a custom static method to transform the price #[Map(transform: [self::class, 'formatPrice'])] public string $price; public static function formatPrice(mixed $price, object $source): int|string { // Entity (int) -> DTO (string) if ($source instanceof BookEntity) { return number_format($price / 100, 2).'$'; } // DTO (string) -> Entity (int) if ($source instanceof self) { return 100 * (int) str_replace('$', '', $price); } throw new \LogicException(\sprintf('Unexpected "%s" source.', $source::class)); } } ``` ### Implementation Details: The Object Mapper Magic Automated mapping relies on two internal classes: `ApiPlatform\State\Provider\ObjectMapperProvider` and `ApiPlatform\State\Processor\ObjectMapperProcessor`. These classes act as decorators around the standard Provider/Processor chain. They are activated when: - The Object Mapper component is available. - `stateOptions` are configured with an `entityClass` (or `documentClass` for ODM). - The Resource (and Entity for writes) classes have the `#[Map]` attribute. #### How it works internally **Read (GET):** The `ObjectMapperProvider` delegates fetching the data to the underlying Doctrine provider (which returns an Entity). It then uses `$objectMapper->map($entity, $resourceClass)` to transform the Entity into your DTO Resource. **Write (POST/PUT/PATCH):** The `ObjectMapperProcessor` receives the deserialized Input DTO. It uses `$objectMapper->map($inputDto, $entityClass)` to transform the input into an Entity instance. It then delegates to the underlying Doctrine processor (to persist the Entity). Finally, it maps the persisted Entity back to the Output DTO Resource. ## 2. Automated Mapped Inputs and Outputs Ideally, your read and write models should differ. You might want to expose less data in a collection view (Output DTO) or enforce strict validation during creation/updates (Input DTOs). ### Input DTOs (Write Operations) For POST and PATCH, we define specific DTOs. The `#[Map(target: BookEntity::class)]` attribute tells the system to map this DTO onto the Entity class before persistence. #### CreateBook DTO ```php // src/Api/Dto/CreateBook.php namespace App\Api\Dto; use App\Entity\Book as BookEntity; use Symfony\Component\ObjectMapper\Attribute\Map; use Symfony\Component\Validator\Constraints as Assert; #[Map(target: BookEntity::class)] final class CreateBook { #[Assert\NotBlank, Assert\Length(max: 255)] #[Map(target: 'title')] // Maps 'name' input to 'title' entity field public string $name; #[Assert\NotBlank, Assert\Length(max: 255)] public string $description; #[Assert\NotBlank, Assert\Isbn] public string $isbn; #[Assert\PositiveOrZero] public int $price; } ``` ### UpdateBook DTO ```php attributes->get('read_data')) { throw new NotFoundHttpException('Not Found'); } // 2. Apply Business Logic // $data is the validated DiscountBook DTO $entity->price = (int) ($entity->price * (1 - $data->percentage / 100)); // 3. Persist using the inner processor $entity = $this->persistProcessor->process($entity, $operation, $uriVariables, $context); // 4. Map the updated Entity back to the main Book Resource return $this->objectMapper->map($entity, $operation->getClass()); } } ``` ### Registering the Custom Operation Finally, register the custom operation in your Book resource. ```php // src/Api/Resource/Book.php #[ApiResource( operations: [ // ... standard operations ... new Post( uriTemplate: '/books/{id}/discount', uriVariables: ['id'], input: DiscountBook::class, processor: DiscountBookProcessor::class, status: 200, ), ] )] final class Book { /* ... */ } ``` --- # OpenAPI Specification Support (formerly Swagger) Source: https://api-platform.com/docs/core/openapi/ # OpenAPI Specification Support (formerly Swagger) API Platform natively supports the [OpenAPI](https://www.openapis.org/) API specification format. ![Screenshot](../symfony/images/swagger-ui-1.png)

OpenAPI screencast
Watch the OpenAPI screencast

The specification of the API is available at the `/docs.jsonopenapi` path. By default, OpenAPI v3 is used. You can also get an OpenAPI v3-compliant version thanks to the `spec_version` query parameter: `/docs.jsonopenapi?spec_version=3` It also integrates a customized version of [Swagger UI](https://swagger.io/swagger-ui/) and [ReDoc](https://rebilly.github.io/ReDoc/), some nice tools to display the API documentation in a user friendly way. ## Using the OpenAPI Command You can also dump an OpenAPI specification for your API. OpenAPI, JSON format: ```console bin/console api:openapi:export ``` OpenAPI, YAML format (you need to install `symfony/yaml` for this to work): ```console bin/console api:openapi:export --yaml ``` Create a file containing the specification: ```console bin/console api:openapi:export --output=swagger_docs.json ``` If you want to use the old OpenAPI v2 (Swagger) JSON format, use: ```console bin/console api:swagger:export ``` It is also possible to use OpenAPI v3.0.0 format: ```console bin/console api:openapi:export --spec-version=3.0.0 ``` ## Create several versions of a specification You can now decline a same OpenAPI specification in multiple versions using the `x-apiplatform-tags` tag: ```php use ApiPlatform\OpenApi\Factory\OpenApiFactory; #[GetCollection(openapi: new Operation(extensionProperties: [OpenApiFactory::API_PLATFORM_TAG => ['customer', 'developer']]))] #[Post(openapi: new Operation(extensionProperties: [OpenApiFactory::API_PLATFORM_TAG => 'developer']))] class Book {} ``` Then, either use the query parameter for the web version such as `/docs?filter_tags[]=customer` or through the command line: ```console bin/console api:openapi:export --filter-tags=customer ``` To produce a specification including only the operation matching your tag. ## Overriding the OpenAPI Specification API Platform generates the OpenAPI specification through the `ApiPlatform\OpenApi\Factory\OpenApiFactoryInterface` service. To customize it, you need to create your own factory service that **decorates** (wraps) the original one. In the following example, we will see how to override the title and the base path URL of the Swagger documentation and add a custom filter for the `GET` operation of `/api/grumpy_pizzas/{id}` path. First, create a custom OpenAPI factory that decorates the original service: ```php decorated->__invoke($context); $pathItem = $openApi->getPaths()->getPath('/api/grumpy_pizzas/{id}'); $operation = $pathItem->getGet(); $openApi->getPaths()->addPath('/api/grumpy_pizzas/{id}', $pathItem->withGet( $operation->withParameters(array_merge( $operation->getParameters(), [new Model\Parameter('fields', 'query', 'Fields to remove of the output')] )) )); $openApi = $openApi->withInfo((new Model\Info('New Title', 'v2', 'Description of my custom API'))->withExtensionProperty('info-key', 'Info value')); $openApi = $openApi->withExtensionProperty('key', 'Custom x-key value'); $openApi = $openApi->withExtensionProperty('x-value', 'Custom x-value value'); // to define base path URL $openApi = $openApi->withServers([new Model\Server('https://foo.bar')]); return $openApi; } } ``` Then configure it as a decorator in your service container: ### Decorate with Symfony Symfony allows to [decorate services](https://symfony.com/doc/current/service_container/service_decoration.html) as following: ```php ### Decorate with Laravel Laravel allows to [decorate services](https://laravel.com/docs/container#extending-bindings), as following: ```php app->extend(OpenApiFactoryInterface::class, function (OpenApiFactoryInterface $factory) { return new OpenApiFactory($factory); }); } } ``` The impact on the swagger-ui is the following: ![Swagger UI](images/swagger-ui-modified.png) ## Using the OpenAPI and Swagger Contexts Sometimes you may want to change the information included in your OpenAPI documentation. For the full list of available configurations, please refer to the [OpenAPI Specifications](https://spec.openapis.org/oas/latest.html). The current doc page only gives some examples but focuses mostly on the OpenAPI integration inside API Platform without telling you all you can pass into the attributes. The following configuration will give you total control over your OpenAPI definitions: ```php 'integer', 'example' => 1 ] )] #[Assert\NotBlank] private ?int $id = null; /** * @param string $name A name property - this description will be available in the API documentation too. * */ #[Assert\NotBlank] #[ApiProperty( description: 'A name property - this description will be available in the API documentation too.', openapiContext: [ 'type' => 'string', 'enum' => ['one', 'two'], 'example' => 'one' ] )] #[Assert\NotBlank] public string $name; #[ApiProperty( description: 'A timestamp property.', openapiContext: [ 'type' => 'string', 'format' => 'date-time', ] )] #[Assert\DateTime] public string $timestamp; // Optionnel : Ajout d'un constructeur pour faciliter l'initialisation public function __construct(string $name, string $timestamp) { $this->name = $name; $this->timestamp = $timestamp; } } ``` ```yaml # api/config/api_platform/properties.yaml # The YAML syntax is only supported for Symfony properties: App\ApiResource\Product: name: openapiContext: type: string enum: ["one", "two"] example: one timestamp: openapiContext: type: string format: date-time ``` ```xml type one two one string date-time ``` This will produce the following Swagger documentation: ```json "components": { "schemas": { "GrumpyPizza:jsonld": { "type": "object", "description": "", "properties": { "@context": { "readOnly": true, "type": "string" }, "@id": { "readOnly": true, "type": "string" }, "@type": { "readOnly": true, "type": "string" }, "timestamp": { "type": "string", "format": "date-time" }, "id": { "readOnly": true, "type": "integer" }, "name": { "type": "string", "enum": [ "one", "two" ], "example": "one" } } } } } ``` To pass a context to the OpenAPI **v2** generator, use the `swaggerContext` attribute (notice the prefix: `swagger` instead of `openapi`). For documentation on how to expose PHP 8.1+ Enums as API resources, refer to the [Enums documentation](enums.md). ## Disabling an Operation From OpenAPI Documentation Sometimes you may want to disable an operation from the OpenAPI documentation, for example to not exposing it. Using the `openapi` boolean option disables this operation from the OpenAPI documentation: ```php ``` Note: as your route is not exposed, you may want to return a HTTP 404 if it's called. Prefer using the `NotExposedAction` controller instead. ## Changing the Name of a Definition API Platform generates a definition name based on the serializer `groups` defined in the (`de`)`normalizationContext`. It's possible to override the name thanks to the `openapi_definition_name` option: ```php use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Post; #[ApiResource] #[Post(denormalizationContext: ['groups' => ['user:read'], 'openapi_definition_name' => 'Read'])] class User { // ... } ``` It's also possible to re-use the (`de`)`normalizationContext`: ```php use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Post; #[ApiResource] #[Post(denormalizationContext: [User::API_WRITE])] class User { const API_WRITE = [ 'groups' => ['user:read'], 'openapi_definition_name' => 'Read', ]; } ``` ## Changing Operations in the OpenAPI Documentation You also have full control over both built-in and custom operations documentation. ```php [ 'schema' => [ 'type' => 'object', 'properties' => [ 'name' => ['type' => 'string'], 'description' => ['type' => 'string'] ] ], 'example' => [ 'name' => 'Mr. Rabbit', 'description' => 'Pink Rabbit' ] ] ]) ), responses: [ 201 => new Model\Response( content: new \ArrayObject([ 'application/json' => [ 'schema' => [ 'type' => 'object', 'properties' => [ 'status' => ['type' => 'string'], 'description' => ['type' => 'string'] ] ], 'example' => [ 'status' => 'success', 'description' => 'Rabbit picture created.', ] ] ] ) ) ], ) )] class Rabbit { // ... } ``` ```yaml # The YAML syntax is only supported for Symfony resources: App\ApiResource\Rabbit: operations: create_rabbit: class: ApiPlatform\Metadata\Post path: "/rabbit/create" controller: App\Controller\RandomRabbit openapi: summary: Random rabbit picture description: > # Pop a great rabbit picture by color! ![A great rabbit](https://rabbit.org/graphics/fun/netbunnies/jellybean1-brennan1.jpg) requestBody: content: application/json: schema: type: object properties: name: { type: string } description: { type: string } example: name: Mr. Rabbit description: Pink rabbit ``` ```xml object string string ``` ![Impact on Swagger UI](../symfony/images/swagger-ui-2.png) ## Disabling Swagger UI or ReDoc ### Disabling Swagger UI or ReDoc with Symfony To disable Swagger UI (ReDoc will be shown by default): ```yaml # api/config/packages/api_platform.yaml api_platform: # ... enable_swagger_ui: false ``` To disable ReDoc: ```yaml # api/config/packages/api_platform.yaml api_platform: # ... enable_re_doc: false ``` ### Disabling Swagger UI or ReDoc with Laravel To disable Swagger UI (ReDoc will be shown by default): ```php false, ]; ``` To disable ReDoc: ```php false, ]; ``` ## Changing the Location of Swagger UI By default, the Swagger UI is available at the API location (when the HTML format is asked) and at the route `/docs`. You may want to change its route and/or disable it at the API location. ### Changing the Route #### Changing the Route with Symfony Manually register the Swagger UI controller: ```yaml # app/config/routes.yaml api_doc: path: /api_documentation controller: api_platform.action.documentation ``` Change `/api_documentation` to the URI you wish Swagger UI to be accessible on. #### Changing the Route with Laravel Manually register the Swagger UI controller: ```php // routes/web.php use Illuminate\Support\Facades\Route; use ApiPlatform\Laravel\Controller\DocumentationController; Route::post('/api_documentation', DocumentationController::class) ->name('api_doc'); ``` Change `/api_documentation` to the URI you wish Swagger UI to be accessible on. ### Disabling Swagger UI at the API Location To disable the Swagger UI at the API location, disable both Swagger UI and ReDoc. With Symfony use: ```yaml # api/config/packages/api_platform.yaml api_platform: # ... enable_swagger_ui: false enable_re_doc: false ``` Or with Laravel use: ```php false, 'enable_re_doc' => false, ]; ``` If you have manually registered the Swagger UI controller, the Swagger UI will still be accessible at the route you have chosen. ## Using a custom Asset Package in Swagger UI > [!WARNING] This feature is not yet available with Laravel, you're welcome to contribute > [on GitHub](https://github.com/api-platform/core) Sometimes you may want to use a different [Asset Package](https://symfony.com/doc/current/reference/configuration/framework.html#packages) for the Swagger UI. In this way you'll have more fine-grained control over the asset URL generations. This is useful i.e. if you want to use different base path, base URL or asset versioning strategy. Specify a custom asset package name: ```yaml # config/packages/api_platform.yaml api_platform: asset_package: "api_platform" ``` Set or override asset properties per package: ```yaml # config/packages/framework.yaml framework: # ... assets: base_path: "/custom_base_path" # the default packages: api_platform: base_path: "/" ``` ## Overriding the UI Template You can extend the default UI Template using the Symfony and Laravel instructions below: ### Overriding the UI Template using Symfony As described [in the Symfony documentation](https://symfony.com/doc/current/templating/overriding.html), it's possible to override the Twig template that loads Swagger UI and renders the documentation: ```twig {# templates/bundles/ApiPlatformBundle/SwaggerUi/index.html.twig #} {% if title %}{{ title }} {% endif %}My custom template {# ... #} ``` You may want to copy the [one shipped with API Platform](https://github.com/api-platform/core/blob/main/src/Symfony/Bundle/Resources/views/SwaggerUi/index.html.twig) and customize it. ### Overriding the UI Template using Laravel As described [in the Laravel documentation](https://laravel.com/docs/packages#overriding-package-views), it's possible to override the Blade template that loads Swagger UI and renders the documentation: ```html {{-- resources/views/vendor/api-platform/swagger-ui.blade.php --}} @if(isset($title)) {{ $title }} @endif My custom template {{-- ... --}} ``` You may want to copy the [one shipped with API Platform](https://github.com/api-platform/core/blob/main/src/Laravel/resources/views/swagger-ui.blade.php) and customize it. ## Compatibility Layer with Amazon API Gateway [AWS API Gateway](https://aws.amazon.com/api-gateway/) supports OpenAPI partially, but it [requires some changes](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-known-issues.html). API Platform provides a way to be compatible with Amazon API Gateway. To enable API Gateway compatibility on your OpenAPI docs, add `api_gateway=true` as query parameter: `http://www.example.com/docs.jsonopenapi?api_gateway=true`. The flag `--api-gateway` is also available through the command-line. ## OAuth ### OAuth using Symfony If you implemented OAuth on your API, you should configure OpenApi's authorization using API Platform's configuration: ```yaml # config/packages/api_platform.yaml api_platform: oauth: # To enable or disable OAuth. enabled: false # The OAuth client ID. clientId: "" # The OAuth client secret. clientSecret: "" # The OAuth type. type: "oauth2" # The OAuth flow grant type. flow: "application" # The OAuth token url. tokenUrl: "/oauth/v2/token" # The OAuth authentication url. authorizationUrl: "/oauth/v2/auth" # The OAuth scopes. scopes: [] ``` Note that `clientId` and `clientSecret` are being used by the SwaggerUI if enabled. ### OAuth using Laravel If you implemented OAuth on your API, you should configure OpenApi's authorization using API Platform's configuration: ```php [ 'enabled' => false, // To enable or disable OAuth. 'clientId' => '', // The OAuth client ID. 'clientSecret' => '', // The OAuth client secret. 'type' => 'oauth2', // The OAuth flow grant type. 'authorizationUrl' => '/oauth/v2/auth' // The OAuth authentication url. 'scopes' => [], // The OAuth scopes. ], ]; ``` Note that `clientId` and `clientSecret` are being used by the SwaggerUI if enabled. ### Configure the OAuth Scopes Option #### Configure the OAuth Scopes Option using Symfony The `api_platform.oauth.scopes` option requires an array value with the scopes name and description. For example: ```yaml api_platform: oauth: scopes: profile: "This scope value requests access to the End-User's default profile Claims, which are: name, family_name, given_name, middle_name, nickname, preferred_username, profile, picture, website, gender, birthdate, zoneinfo, locale, and updated_at." email: "This scope value requests access to the email and email_verified Claims." address: "This scope value requests access to the address Claim." phone: "This scope value requests access to the phone_number and phone_number_verified Claims." ``` > [!NOTE] If you're using an OpenID Connect server (such as Keycloak or Auth0), the `openid` scope > **must** be set according to the > [OpenID Connect specification](https://openid.net/specs/openid-connect-core-1_0.html). #### Configure the OAuth Scopes Option using Laravel The `api_platform.oauth.scopes` option requires an array value with the scopes name and description. For example: ```php [ 'scopes' => [ 'profile' => "This scope value requests access to the End-User's default profile Claims, which are: name, family_name, given_name, middle_name, nickname, preferred_username, profile, picture, website, gender, birthdate, zoneinfo, locale, and updated_at.", 'email' => 'This scope value requests access to the email and email_verified Claims.', 'address' => 'This scope value requests access to the address Claim.', 'phone' => 'This scope value requests access to the phone_number and phone_number_verified Claims.', ] ], ]; ``` > [!NOTE] If you're using an OpenID Connect server (such as Keycloak or Auth0), the `openid` scope > **must** be set according to the > [OpenID Connect specification](https://openid.net/specs/openid-connect-core-1_0.html). ## Info Object The [info object](https://swagger.io/specification/#info-object) provides metadata about the API like licensing information or a contact. You can specify this information using API Platform's configuration below: ### Info Object Configuration using Symfony ```yaml api_platform: # The title of the API. title: "API title" # The description of the API. description: "API description" # The version of the API. version: "0.0.0" openapi: # The contact information for the exposed API. contact: # The identifying name of the contact person/organization. name: # The URL pointing to the contact information. MUST be in the format of a URL. url: # The email address of the contact person/organization. MUST be in the format of an email address. email: # A URL to the Terms of Service for the API. MUST be in the format of a URL. termsOfService: # The license information for the exposed API. license: # The license name used for the API. name: # URL to the license used for the API. MUST be in the format of a URL. url: ``` ### Info Object Configuration using Laravel ```php 'API title', // The title of the API. 'description' => 'API description', // The description of the API. 'version' => '0.0.0', // The version of the API. //... 'openapi' => [ 'contact' => [ // The contact information for the exposed API. 'name' => '', // The identifying name of the contact person/organization. 'url' => '', // The URL pointing to the contact information. MUST be in the format of a URL. 'email' => '', // The email address of the contact person/organization. MUST be in the format of an email address. ], 'termsOfService' => '', // A URL to the Terms of Service for the API. MUST be in the format of a URL. 'license' => [ // The license information for the exposed API. 'name' => '', // The license name used for the API. 'url' => '', // URL to the license used for the API. MUST be in the format of a URL. ] ], ]; ``` --- # JSON Schema Support Source: https://api-platform.com/docs/core/json-schema/ # JSON Schema Support [JSON Schema](https://json-schema.org/) is a popular vocabulary to describe the shape of JSON documents. A variant of JSON Schema is also used [in OpenAPI specifications](openapi.md). API Platform provides an infrastructure to generate JSON Schemas for any resource, represented in any format (including JSON-LD). The generated schema can be used with libraries such as [react-json-schema-form](https://github.com/rjsf-team/react-jsonschema-form) to build forms for the documented resources, or to [be used for validation](https://json-schema.org/implementations.html#validators). ## Generating a JSON Schema > [!WARNING] These commands are not yet available with Laravel, you're welcome to contribute > [on GitHub](https://github.com/api-platform/core) To export the schema corresponding to an API Resource, run the following command: ```console bin/console api:json-schema:generate 'App\ApiResource\Book' ``` To see all options available, try: ```console bin/console help api:json-schema:generate ``` ## Overriding the JSON Schema Specification In a unit testing context, API Platform does not use the same schema version as the schema used when generating the API documentation. The version used by the documentation is the OpenAPI Schema version and the version used by unit testing is the JSON Schema version. > [!NOTE] For assertions about JSON schemas in Laravel, refer to the > [API Test Assertions in Laravel documentation](../laravel/testing.md#api-test-assertions-with-laravel). When [Testing the API](../core/testing.md), JSON Schemas are useful to generate and automate unit testing. API Platform provides specific unit testing functionalities like [`assertMatchesResourceCollectionJsonSchema()`](../symfony/testing.md#writing-functional-tests) or [`assertMatchesResourceItemJsonSchema()`](../symfony/testing.md#writing-functional-tests) methods. These methods generate a JSON Schema then do unit testing based on the generated schema automatically. Usually, the fact that API Platform uses a different schema version for unit testing is not a problem, but sometimes you may need to use the [`ApiProperty`](openapi.md#using-the-openapi-and-swagger-contexts) attribute to specify a [calculated field](serialization.md#calculated-field) type by overriding the OpenAPI Schema for the calculated field to be correctly documented. When you will use [`assertMatchesResourceCollectionJsonSchema()`](../symfony/testing.md#writing-functional-tests) or [`assertMatchesResourceItemJsonSchema()`](../symfony/testing.md#writing-functional-tests) functions the unit test will fail on this [calculated field](serialization.md#calculated-field) as the unit testing process doesn't use the `openapi_context`you specified because API Platform is using the JSON Schema version instead at this moment. So there is a way to override JSON Schema specification for a specific property in the JSON Schema used by the unit testing process. You will need to add the `json_schema_context` property in the [`ApiProperty`](openapi.md#using-the-openapi-and-swagger-contexts) attribute to do this, example: ```php 'integer', 'example' => 1 ] )] private ?int $id = null; // [...] public function getId(): ?int { return $this->id; } #[ApiProperty( openapiContext: [ 'type' => 'array', 'items' => ['type' => 'integer'] ], jsonSchemaContext: [ 'type' => 'array', 'items' => ['type' => 'integer'] ] )] public function getSomeNumbers(): array { return [1, 2, 3, 4]; } } ``` You can obtain more information about the available [JSON Schema Types and format here](https://json-schema.org/understanding-json-schema/reference/type.html). ## Generating a JSON Schema Programmatically To generate JSON Schemas programmatically, use the `api_platform.json_schema.schema_factory`. For further information, please consult the following documentations: - [Symfony: Fetching and Using Services](https://symfony.com/doc/current/service_container.html#fetching-and-using-services) - [Laravel: Resolving Services](https://laravel.com/docs/container#resolving) ## Testing API Platform provides a PHPUnit assertion to test if a response is valid according to a given Schema: `assertMatchesJsonSchema()`. Refer to [the testing documentation](../core/testing.md) for more details. --- # MCP: Exposing Your API to AI Agents Source: https://api-platform.com/docs/core/mcp/ # MCP: Exposing Your API to AI Agents API Platform integrates with the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) to expose your API as tools and resources that AI agents (LLMs) can discover and interact with. MCP defines a standard way for AI models to discover available tools, understand their input schemas, and invoke them. API Platform leverages its existing metadata system — state processors, validation, serialization — to turn your PHP classes into MCP-compliant tool definitions. > **Note:** The MCP integration is marked `@experimental`. The API may change between minor > releases. ## Installation ### Installing on Symfony Install `api-platform/mcp` and the [MCP Bundle](https://github.com/symfony/ai): ```console composer require api-platform/mcp symfony/mcp-bundle ``` ### Installing on Laravel Install `api-platform/mcp` and the [MCP Bundle](https://github.com/symfony/ai): ```console composer require api-platform/mcp symfony/mcp-bundle ``` ## Configuring the MCP Server ### Configuring Symfony Enable the MCP server and configure the transport in your Symfony configuration: ```yaml # config/packages/mcp.yaml mcp: client_transports: http: true stdio: false http: path: "/mcp" session: store: "file" directory: "%kernel.cache_dir%/mcp" ttl: 3600 ``` You can also configure API Platform's MCP integration in `api_platform.yaml`: ```yaml # config/packages/api_platform.yaml api_platform: mcp: enabled: true # default: true format: jsonld # default: 'jsonld' ``` The `format` option sets the serialization format used for MCP tool structured content. It must be a format registered in `api_platform.formats` (e.g. `jsonld`, `json`, `jsonapi`). The default `jsonld` produces rich semantic output with `@context`, `@id`, and `@type` fields. ### Configuring Laravel MCP is enabled by default in the Laravel configuration: ```php // config/api-platform.php return [ // ... 'mcp' => [ 'enabled' => true, ], ]; ``` The MCP endpoint is automatically registered at `/mcp`. ## Architecture API Platform registers its own `Handler` with the MCP SDK. The handler's `supports()` method returns `true` only for tools and resources that are registered through API Platform metadata. If a requested tool or resource is not found in the API Platform registry, the handler returns `false` and the MCP SDK proceeds through its own handler chain. This means you can register both API Platform MCP tools and native `mcp/sdk` tools on the same server — they coexist without conflict. ## Declaring MCP Tools MCP tools let AI agents invoke operations on your API. The primary pattern uses `#[McpTool]` as a class attribute: the class properties define the tool's input schema, and a [state processor](state-processors.md) handles the command. This follows a CQRS-style approach: tools receive input from AI agents and process it through your application logic. ### Simple Tool ```php message; } public function setMessage(string $message): void { $this->message = $message; } public function getPriority(): int { return $this->priority; } public function setPriority(int $priority): void { $this->priority = $priority; } public static function process($data): mixed { $data->setMessage('Processed: ' . $data->getMessage()); $data->setPriority($data->getPriority() + 10); return $data; } } ``` The class properties (`$message`, `$priority`) become the tool's `inputSchema`. When an AI agent calls this tool, API Platform deserializes the input into a `ProcessMessage` instance and passes it to the processor. The returned object is serialized back as structured content. You can also use a [dedicated state processor service](state-processors.md) instead of a static method — any callable or service class implementing `ProcessorInterface` works. ### Using a Separate Input DTO When the tool's input schema should differ from the class itself, use the `input` option to specify a separate DTO: ```php managerRegistry->getRepository(Book::class)->findAll(); } } ``` ### Returning a Collection with McpToolCollection Use `McpToolCollection` instead of `McpTool` when a tool returns a collection of items. It extends `McpTool` and implements `CollectionOperationInterface`, which signals to the serializer and schema factory that the output is a list. ```php new McpToolCollection( description: 'List Books', input: SearchQuery::class, processor: SearchBooksProcessor::class, structuredContent: true, ), ] )] class Book { public ?int $id = null; public ?string $title = null; public ?string $isbn = null; } ``` When `structuredContent: true`, the structured content response includes `@context`, `hydra:totalItems`, and a `hydra:member` array containing the serialized items. The output schema published to MCP clients reflects this collection structure. ### Returning Custom Results By default, tool results are serialized using API Platform's [serialization](serialization.md) system with structured content (JSON). If you need full control over the response, return a `CallToolResult` directly from your processor and set `structuredContent: false`: ```php getTitle()}\n\n{$data->getContent()}"; return new CallToolResult( [new TextContent($markdown)], false ); } } ``` Setting `structuredContent: false` disables the automatic JSON serialization. When returning a `CallToolResult`, the response is sent as-is to the AI agent. ## Using McpTool with ApiResource The standalone `#[McpTool]` class attribute is convenient for simple tools, but you can also declare MCP tools inside an `#[ApiResource]` attribute using the `mcp` parameter. This is the appropriate pattern when: - The class should not expose any HTTP endpoints (`operations: []`) - You need to combine multiple MCP operations on a single class - You need fine-grained control that is cleaner to express at the resource level The `mcp` parameter accepts an associative array of `McpTool` or `McpResource` instances, keyed by the operation name. The array key is the tool name — the `name` parameter inside `new McpTool(...)` is redundant when using this pattern and should be omitted. Setting `operations: []` means no HTTP routes are registered for the class — it exists solely as an MCP tool definition. ### Simple Tool with a Dedicated Processor ```php new McpTool( description: 'Navigate to a Hydra API resource by URI.', processor: ReadHydraResourceProcessor::class, structuredContent: false, ), ], )] class ReadHydraResource { public string $uri; } ``` The class properties define the tool's `inputSchema`. The processor receives a `ReadHydraResource` instance and returns the result. Because `structuredContent: false` is set, the processor can return a `CallToolResult` directly, bypassing automatic JSON serialization. ### Customizing Property Schemas with ApiProperty Some LLM providers reject JSON Schema union types such as `["array", "null"]` that PHP nullable types produce by default. Use `#[ApiProperty(schema: [...])]` to override the generated schema for a specific property: ```php new McpTool( description: 'Execute a state-changing operation on a Hydra API resource.', processor: InvokeHydraOperationProcessor::class, structuredContent: false, ), ], )] class InvokeHydraOperation { public string $uri; public string $method; #[ApiProperty(schema: ['type' => 'object', 'description' => 'JSON payload for the request'])] public ?array $payload = null; } ``` Without the `#[ApiProperty]` override, `?array $payload` would generate `{"type": ["array", "null"]}`. The explicit schema replaces it with `{"type": "object", ...}`, which all major LLM providers accept. ## Validation The MCP SDK already validates tool inputs against the JSON Schema at the transport level (types, required fields, etc.). API Platform's own validation pipeline is disabled by default for MCP tools. Set `validate: true` to enable business-level validation — constraints like email format, string length, or custom rules that go beyond structural schema checks. On Symfony, use [Symfony Validator constraints](../symfony/validation.md): ```php 'required|min:3|max:50', 'email' => 'required|email', ] )] ``` ## Declaring MCP Resources MCP resources expose read-only content that AI agents can retrieve — documentation, configuration, reference data, etc. Use the `McpResource` attribute with a [state provider](state-providers.md): ```php new McpResource( uri: 'resource://my-app/documentation', name: 'App-Documentation', description: 'Application API documentation', mimeType: 'text/markdown', provider: [self::class, 'provide'] ), ] )] class Documentation { public function __construct( private string $content, private string $uri, ) {} // getters and setters... public static function provide(): self { return new self( content: '# My API Documentation\n\nWelcome to the API.', uri: 'resource://my-app/documentation' ); } } ``` The `uri` must be unique across the MCP server and follows the `resource://` URI scheme. ## McpTool Options The `McpTool` attribute accepts all standard [operation options](operations.md) plus: | Option | Description | | -------------------- | ------------------------------------------------------------------------- | | `name` | Tool name exposed to AI agents (defaults to the class short name) | | `description` | Human-readable description of the tool (defaults to class DocBlock) | | `structuredContent` | Whether to include JSON structured content in responses (default: `true`) | | `input` | A separate DTO class to use as the tool's input schema | | `output` | A separate DTO class to use as the tool's output representation | | `inputFormats` | Serialization formats for deserializing the tool input (e.g. `['json']`) | | `outputFormats` | Serialization formats for structured content (e.g. `['jsonld']`) | | `contentNegotiation` | Whether to enable HTTP content negotiation (default: `false` for MCP) | | `validate` | Whether to run the validation pipeline (default: `false` for MCP) | | `annotations` | MCP tool annotations describing behavior hints | | `icons` | List of icon URLs representing the tool | | `meta` | Arbitrary metadata | | `rules` | Laravel validation rules (Laravel only) | ## McpResource Options The `McpResource` attribute accepts all standard [operation options](operations.md) plus: | Option | Description | | -------------------- | -------------------------------------------------------------------------- | | `uri` | Unique URI identifying this resource (required, uses `resource://` scheme) | | `name` | Human-readable name for the resource | | `description` | Description of the resource (defaults to class DocBlock) | | `structuredContent` | Whether to include JSON structured content (default: `true`) | | `contentNegotiation` | Whether to enable HTTP content negotiation (default: `false` for MCP) | | `mimeType` | MIME type of the resource content | | `size` | Size in bytes, if known | | `annotations` | MCP resource annotations | | `icons` | List of icon URLs | | `meta` | Arbitrary metadata | --- # Using AI Coding Agents with API Platform Source: https://api-platform.com/docs/core/ai-agents/ # Using AI Coding Agents with API Platform AI coding agents — Claude Code, Cursor, GitHub Copilot, OpenAI Codex, Gemini, and others — are trained on snapshots of public code. Those snapshots lag behind the framework: an agent may suggest the `#[ApiFilter]` attribute, legacy controllers, or a serialization pattern that API Platform 4.x has since replaced. The result is plausible-looking code that does not match the current canonical way of doing things. This guide describes two complementary ways to keep an agent on current API Platform 4.x APIs while it works in your project: - The [API Platform skillset](#claude-code-the-api-platform-skillset), a Claude Code plugin that teaches Claude the canonical patterns, verified against `api-platform/core`. - An agent-agnostic [`AGENTS.md`](#other-agents-cursor-github-copilot-openai-codex-gemini) file that points any agent at the official documentation as the source of truth. > **Note:** This guide covers _dev-time_ guidance — keeping the agent that writes your code on > current APIs. Exposing your running API _to_ AI agents as callable tools is a different concern, > covered by the [MCP integration](mcp.md). ## Claude Code: the API Platform skillset The [API Platform skillset](https://github.com/api-platform/skillset) is a [Claude Code](https://docs.claude.com/en/docs/claude-code/overview) plugin that ships 15 skills for API Platform 4.x development. Each skill teaches the current canonical way to do one thing, verified against `api-platform/core`, and covers both the Symfony and Laravel integrations. A skill is more than a documentation link: it is a focused set of instructions, code patterns, and constraints that Claude loads on demand. When you ask Claude to add a filter, the `api-filter` skill loads and steers it toward `QueryParameter` rather than the legacy `#[ApiFilter]` attribute. ### Installing the skillset The plugin is distributed through a Claude Code marketplace. Add the marketplace, then install the plugin: ```console /plugin marketplace add api-platform/skillset /plugin install api-platform@api-platform-skillset ``` These are [Claude Code slash commands](https://docs.claude.com/en/docs/claude-code/slash-commands) — run them from inside a Claude Code session, not from your shell. ### How skills load Skills are namespaced as `api-platform:` (for example `api-platform:api-filter`). You do not invoke them manually: Claude loads a skill automatically when the task at hand is relevant to it. No configuration is required once the plugin is installed. ### Available skills | Skill | Covers | | ---------------------- | -------------------------------------------------------------------------------------------- | | `api-resource` | Resources, DTOs, Object Mapper, nested sub-resources, custom operations | | `api-filter` | Collection filters with `QueryParameter`, legacy `#[ApiFilter]` migration | | `state-provider` | Custom read logic, decorating Doctrine providers, computed fields | | `state-processor` | Custom write logic, soft-delete, file downloads, side effects | | `operations` | Operation security expressions, validation groups, parameter validation, deprecation | | `securing-collections` | Multi-tenant isolation with Doctrine extensions and link handlers | | `custom-validator` | Custom validation constraints for business rules | | `serialization-groups` | Serialization contexts and `#[Groups]`, with guidance on when DTOs are better | | `pagination` | Page-based, partial, and cursor pagination | | `errors` | RFC 7807 Problem Details, `#[ErrorResource]`, exception-to-status mapping | | `graphql` | GraphQL operations, resolvers, Relay pagination | | `mercure` | Real-time updates over Mercure (Symfony only) | | `api-platform-mcp` | Exposing resources to AI agents via MCP: `#[McpTool]`, `McpToolCollection`, `#[McpResource]` | | `api-docs` | OpenAPI customization, hiding operations, factory decoration | | `api-test` | Functional tests with `ApiTestCase` (Symfony) and HTTP tests (Laravel) | ### Keeping the skillset up to date The canonical patterns evolve with each release. Update the marketplace to pull the latest skills: ```console /plugin marketplace update api-platform-skillset ``` ## Other agents: Cursor, GitHub Copilot, OpenAI Codex, Gemini The skillset is a Claude Code plugin: its `SKILL.md` plus `.claude-plugin/` marketplace format is specific to Claude Code, so it does not load into other agents. For Cursor, GitHub Copilot, OpenAI Codex, Gemini, and any other agent, the portable option is an `AGENTS.md` file at the root of your project. [`AGENTS.md`](https://agents.md/) is a convention — a plain Markdown file that most coding agents read for project-specific instructions. It is lighter-weight than the Claude skillset: it does not ship verified code patterns, it points the agent at where the current truth lives. The single most useful instruction is to treat the official documentation as authoritative over training data: ```markdown # AGENTS.md This project uses [API Platform](https://api-platform.com) 4.x. The official documentation at is the source of truth. Prefer it over patterns from your training data, which may describe older versions of the framework. In particular: - Declare collection filters with `QueryParameter`, not the legacy `#[ApiFilter]` attribute. - Read and write logic belongs in state providers and state processors, not in custom controllers. - Check the documentation for the current canonical pattern before generating API Platform code. ``` Tailor the bullet list to the conventions your project cares about. The point is to redirect the agent to current documentation rather than letting it rely on stale training data. Claude Code also reads a `CLAUDE.md` file. If you maintain both an `AGENTS.md` and use Claude Code, keep a thin `CLAUDE.md` that imports the shared file rather than duplicating its content: ```markdown # CLAUDE.md @AGENTS.md ``` This keeps a single source of project guidance for every agent. For Claude Code specifically, the [skillset](#claude-code-the-api-platform-skillset) remains the richer option and complements `AGENTS.md`. ## Documentation index for LLMs The documentation is also published in the [llms.txt](https://llmstxt.org) format, an agent-agnostic index any LLM can fetch to ground its answers in the current documentation: - — a curated index of the documentation, one link per page, grouped by chapter. - — the full text of every current-version page, concatenated. Where `AGENTS.md` tells an agent _how_ to behave, `llms.txt` tells it _where_ the current reference lives. The two are complementary: point your `AGENTS.md` at these URLs so the agent reads the documentation instead of relying on stale training data. ## Related: exposing your API to agents with MCP This guide is about the agent that _writes_ your code. A separate concern is letting an AI agent _call_ your running API at runtime — searching your books, creating an order, reading a resource. That is what the [Model Context Protocol (MCP) integration](mcp.md) provides: it turns your API Platform resources into MCP tools and resources an agent can discover and invoke. The two are independent and can be used together: - The skillset and `AGENTS.md` shape the code an agent produces during development. - MCP exposes your deployed API as callable tools at runtime. The `api-platform-mcp` skill in the skillset covers writing those MCP tool definitions; see [Exposing Your API to AI Agents](mcp.md) for the full MCP reference. --- # Creating Async APIs using the Mercure Protocol Source: https://api-platform.com/docs/core/mercure/ # Creating Async APIs using the Mercure Protocol API Platform can automatically push the modified version of the resources exposed by the API to the currently connected clients (webapps, mobile apps...) using [the Mercure protocol](https://mercure.rocks). > _Mercure_ is a protocol allowing to push data updates to web browsers and other HTTP clients in a > convenient, fast, reliable and battery-efficient way. It is especially useful to publish real-time > updates of resources served through web APIs, to reactive web and mobile apps. > > —[https://mercure.rocks](https://mercure.rocks) API Platform detects changes made to your Doctrine entities, and sends the updated resources to the Mercure hub. Then, the Mercure hub dispatches the updates to all connected clients using [Server-sent Events (SSE)](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events). ![Mercure subscriptions](images/mercure-subscriptions.png) ## Installing Mercure Support Mercure support is already installed, configured and enabled in [the API Platform Symfony variant](../symfony/index.md). If you use it, you have nothing more to do, and you can skip to the next section. If you installed API Platform using another method (e.g., `composer require api`), you will need to set up the following: 1. A [Mercure hub](https://mercure.rocks/docs/getting-started). 2. One of the following, depending on your framework: - For Symfony users: the [MercureBundle](https://symfony.com/doc/current/mercure.html). - For Laravel users: the [Laravel Mercure Broadcaster](https://github.com/mvanduijker/laravel-mercure-broadcaster). ## Pushing the API Updates Use the `mercure` attribute to hint API Platform that it must dispatch the updates regarding the given resources to the Mercure hub: ```php true])] class Book { // ... } ``` It's also possible to execute an _expression_ (using the [Symfony Expression Language component](https://symfony.com/doc/current/components/expression_language.html)), to generate the options dynamically: ```php true]; // ... } ``` ## Available Options In addition to `private`, the following options are available: - `topics`: the list of topics of this update, if not the resource IRI is used - `data`: the content of this update, if not set the content will be the serialization of the resource using the default format - `id`: the SSE ID of this event, if not set the ID will be generated by the Mercure Hub - `type`: the SSE type of this event, if not set this field is omitted - `retry`: the `retry` field of the SSE, if not set this field is omitted - `normalization_context`: the specific normalization context to use for the update. ## Dispatching Restrictive Updates (Security Mode) Use `iri` (iriConverter) and `escape` (rawurlencode) functions to add an alternative topic, in order to restrict a subscriber with `topic_selector` to receive only publications that are authorized (partner match). > Let's say that a subscriber wants to receive updates concerning all book resources it has access > to. The subscriber can use the topic selector `https://example.com/books/{id}` as value of the > topic query parameter. Adding this same URI template to the mercure.subscribe claim of the JWS > presented by the subscriber to the hub would allow this subscriber to receive all updates for all > book resources. It is not what we want here: this subscriber is only authorized to access some of > these resources. > > To solve this problem, the mercure.subscribe claim could contain a topic selector such as: > `https://example.com/users/foo/{?topic}`. > > The publisher could then take advantage of the previously described behavior by publishing a > private update having `https://example.com/books/1` as canonical topic and > `https://example.com/users/foo/?topic=https%3A%2F%2Fexample.com%2Fbooks%2F1` as alternate topic. > > —[https://mercure.rocks/spec#subscribers](https://mercure.rocks/spec#subscribers) Below is an example using the `topics` option: ```php true, // the '@=' prefix is required when using expressions for arguments in topics 'topics' => [ '@=iri(object)', '@=iri(object.getOwner()) ~ "/?topic=" ~ escape(iri(object))', '@=iri(object, '.UrlGeneratorInterface::ABS_PATH.')', // you can also change the reference type 'https://example.com/books/1', ], ], )] class Book { private ?User $owner; public function getOwner(): ?User { return $this->owner; } } ``` Using an _expression_ function: ```php true, 'topics' => [$topic1, $topic2, $topic3, $topic4], ]; } public function getOwner(): ?User { return $this->owner; } } ``` In this case, the JWT Token for the subscriber should contain: ```json { "mercure": { "subscribe": ["https://example.com/users/foo/{?topic}"] } } ``` The subscribe topic should be: `https://example.com/books/{id}` --- # Pushing Related Resources Using HTTP/2 Source: https://api-platform.com/docs/core/push-relations/ # Pushing Related Resources Using HTTP/2 > HTTP/2 allows a server to pre-emptively send (or "push") responses (along with corresponding > "promised" requests) to a client in association with a previous client-initiated request. This can > be useful when the server knows the client will need to have those responses available in order to > fully process the response to the original request. > > —[RFC 7540](https://tools.ietf.org/html/rfc7540#section-8.2) API Platform leverages this capability by pushing relations of a resource to clients. > [!NOTE] We strongly recommend using [Vulcain](https://vulcain.rocks) instead of this feature. > Vulcain is faster, cleaner, more flexible, and is supported out of the box in > [the API Platform Symfony variant](../symfony/index.md). ```php false]) ``` ## Exception status code decision There are many ways of configuring the exception status code we recommend reading the guides on how to use an [Error Provider](https://api-platform.com/docs/guides/error-provider/) or create an [Error Resource](https://api-platform.com/docs/guides/error-resource/). The decision works like this, if you are using API Platform with Symfony: 1. We look at `exception_to_status` and take one if there's a match 2. If your exception is a `Symfony\Component\HttpKernel\Exception\HttpExceptionInterface` we get its status. 3. If the exception is a `ApiPlatform\Metadata\Exception\ProblemExceptionInterface` and there is a status we use it 4. Same for `ApiPlatform\Metadata\Exception\HttpExceptionInterface` 5. Use defaults for the following exceptions: - `Symfony\Component\HttpFoundation\Exception\RequestExceptionInterface` => 400 - `ApiPlatform\Symfony\Validator\Exception\ValidationException` => 422 6. The status defined on an `ErrorResource` 7. 500 is the fallback And like this, if you are using API Platform with Laravel: 1. Check an `exception_to_status` array and use its value if a match is found. 2. If the exception implements `Illuminate\Contracts\Http\Exception\HttpResponseException`, retrieve its HTTP status. 3. If the exception implements `App\Contracts\Exceptions\ProblemExceptionInterface` and a status is defined, use it. 4. Similarly, check for `App\Contracts\Exceptions\HttpExceptionInterface`. 5. Use defaults for the following exceptions: - `Illuminate\Http\Exceptions\HttpResponseException` => 400 - `ApiPlatform\Symfony\Validator\Exception\ValidationException` => 422 6. The status defined on an `ErrorResource` 7. Fallback to 500. ## Exception to status The framework also allows you to configure the HTTP status code sent to the clients when custom exceptions are thrown on an API Platform resource operation. In the following example, we throw a domain exception from the business layer of the application and configure API Platform to convert it to a `404 Not Found` error: ```php ['checkProductAvailability', EventPriorities::PRE_VALIDATE], ]; } public function checkProductAvailability(ViewEvent $event): void { $product = $event->getControllerResult(); if (!$product instanceof Product || !$event->getRequest()->isMethodSafe(false)) { return; } if (!$product->isPubliclyAvailable()) { // Using internal codes for a better understanding of what's going on throw new ProductNotFoundException(sprintf('The product "%s" does not exist.', $product->getId())); } } } ``` If you use the API Platform Symfony variant, this event listener will be automatically registered. If you use a custom installation, [learn how to extend API Platform](extending.md). Then, configure the framework to catch `App\Exception\ProductNotFoundException` exceptions and convert them into `404` errors: ### Exception to status Configuration using Symfony ```yaml # config/packages/api_platform.yaml api_platform: # ... exception_to_status: # The 4 following handlers are registered by default, keep those lines to prevent unexpected side effects Symfony\Component\Serializer\Exception\ExceptionInterface: 400 # Use a raw status code (recommended) ApiPlatform\Exception\InvalidArgumentException: !php/const Symfony\Component\HttpFoundation\Response::HTTP_BAD_REQUEST ApiPlatform\ParameterValidator\Exception\ValidationExceptionInterface: 400 Doctrine\ORM\OptimisticLockException: 409 # Validation exception ApiPlatform\Validator\Exception\ValidationException: !php/const Symfony\Component\HttpFoundation\Response::HTTP_UNPROCESSABLE_ENTITY # Custom mapping App\Exception\ProductNotFoundException: 404 # Here is the handler for our custom exception ``` ### Exception to status Configuration using Laravel ```php [ // The 3 following handlers are registered by default, keep those lines to prevent unexpected side effects Symfony\Component\Serializer\Exception\ExceptionInterface::class => 400, ApiPlatform\Exception\InvalidArgumentException::class => Illuminate\Http\Response::HTTP_BAD_REQUEST, ApiPlatform\ParameterValidator\Exception\ValidationExceptionInterface => 400, //Validation exception ApiPlatform\Validator\Exception\ValidationException::class => Illuminate\Http\Response::HTTP_UNPROCESSABLE_ENTITY, //Custom mapping App\Exception\ProductNotFoundException::class => 404 // Here is the handler for our custom exception ], ]; ``` > [!WARNING] The `exception_to_status` configuration is **not merged** with the defaults: as soon as > you declare your own mapping, it replaces the built-in one entirely. If you omit the default > handlers shown above, exceptions that were previously mapped to `400` (such as the > `Symfony\Component\Serializer\Exception\ExceptionInterface` thrown when denormalizing a relation > to a non-existing item, or an invalid backed enum value) will fall through to the `500` fallback. > Always keep the default lines and append your custom mappings to them. Any type of `Exception` can be thrown, API Platform will convert it to a Symfony's `HttpException` (note that it means the exception will be flattened and lose all of its custom properties). The framework also takes care of serializing the error description according to the request format. For instance, if the API should respond in JSON-LD, the error will be returned in this format as well: `GET /products/1234` ```json { "@context": "/contexts/Error", "@type": "Error", "title": "An error occurred", "description": "The product \"1234\" does not exist." } ``` ### Message Scope Depending on the status code you use, the message may be replaced with a generic one in production to avoid leaking unwanted information. If your status code is >= 500 and < 600, the exception message will only be displayed in debug mode (dev and test). In production, a generic message matching the status code provided will be shown instead. If you are using an unofficial HTTP code, a general message will be displayed. In any other cases, your exception message will be sent to end users. ### Fine-grained Configuration The `exceptionToStatus` configuration can be set on resources and operations: ```php 404] operations: [ new Get(exceptionToStatus: [ProductWasRemovedException::class => 410]), new GetCollection(), new Post() ] )] class Book { // ... } ``` Exceptions mappings defined on operations take precedence over mappings defined on resources, which take precedence over the global config. ## Control your exceptions With `rfc_7807_compliant_errors` a few things happen. First Hydra exception are compatible with the JSON Problem specification. Default exception that are handled by API Platform in JSON will be returned as `application/problem+json`. To customize the API Platform response, replace the `api_platform.state.error_provider` with your own provider: ```php getRequestFormat(); $exception = $request->attributes->get('exception'); /** @var \ApiPlatform\Metadata\HttpOperation $operation */ $status = $operation->getStatus() ?? 500; // You don't have to use this, you can use a Response, an array or any object (preferably a resource that API Platform can handle). $error = Error::createFromException($exception, $status); // care about hiding information as this can be a security leak if ($status >= 500) { $error->setDetail('Something went wrong'); } return $error; } } ``` ```yaml # The YAML syntax is only supported for Symfony api_platform.state.error_provider: class: 'App\State\ErrorProvider' tags: - key: "api_platform.state.error_provider" name: "api_platform.state_provider" ``` Note that our validation exception have their own error provider at: ```yaml # The YAML syntax is only supported for Symfony api_platform.validator.state.error_provider: tags: - key: "api_platform.validator.state.error_provider" name: "api_platform.state_provider" ``` ## Domain exceptions Another way of having full control over domain exceptions is to create your own Error resource: ```php ['trace', 'file', 'line', 'code', 'message', 'traceAsString']]` because you usually don't want these. You can override this context value if you want. ## Document your exceptions Since 3.4, you also have the possibility to link your specific domain exceptions to your ApiResources so that they appear directly in your OpenAPI definition ! Let's say that you have a `Greetings` resource, and that one of its providers can throw the following exception for the `ApiPlatform\Metadata\GetCollection` Operation: ```php use ApiPlatform\Metadata\ErrorResource; use ApiPlatform\Metadata\Exception\ProblemExceptionInterface; #[ErrorResource] class MyDomainException extends \Exception implements ProblemExceptionInterface { public function getType(): string { return '/errors/418'; } public function getTitle(): ?string { return 'Teapot error'; } public function getStatus(): ?int { return 418; } public function getDetail(): ?string { return $this->getMessage(); } public function getInstance(): ?string { return null; } public string $myCustomField = 'I usually prefer coffee.'; } ``` As long as your Exception implements `ApiPlatform\Metadata\Exception\ProblemExceptionInterface` and has the `ErrorResource` attribute, you can then map it to your Operation this way: ```php use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\GetCollection; use App\Exception\MyDomainException; #[ApiResource(operations: [ new GetCollection(errors: [MyDomainException::class]) ], )] class Greeting { } ``` This will automatically document your potential domain exception as a Response in the OpenAPI definition, and show it in the UI : ![Swagger UI](images/open-api-documented-error.png) --- # Using External Vocabularies Source: https://api-platform.com/docs/core/external-vocabularies/ # Using External Vocabularies JSON-LD allows to define classes and properties of your API with open vocabularies such as [Schema.org](https://schema.org) and [Good Relations](https://www.heppnetz.de/projects/goodrelations/). API Platform provides attributes usable on PHP classes and properties for specifying a related external [IRI](https://en.wikipedia.org/wiki/Internationalized_resource_identifier). ```php 'api_platform.metadata.path_segment_name_generator.dash' ]; ``` ## Create a Custom Operation Path Resolver Let's assume we need URLs without separators (e.g. `api.tld/myresources`) ### Defining the Operation Segment Name Generator Make sure the custom segment generator implements [`ApiPlatform\Metadata\Operation\PathSegmentNameGeneratorInterface`](https://github.com/api-platform/core/blob/main/src/Metadata/Operation/PathSegmentNameGeneratorInterface.php): ```php dashize($name); return $name; } private function dashize(string $string): string { return strtolower(preg_replace('~(?<=\\w)([A-Z])~', '-$1', $string)); } } ``` Note that `$name` contains a camelCase string, by default the resource class name (e.g. `MyResource`). ### Registering the Service (for Symfony only) If you haven't disabled the autowiring option, the service will be registered automatically and you have nothing more to do. Otherwise, you must register this class as a service like in the following example: ```yaml # api/config/services.yaml services: # ... 'App\Operation\SingularPathSegmentNameGenerator': ~ ``` ### Configuring the Service #### Configuring It using Symfony ```yaml # api/config/packages/api_platform.yaml api_platform: path_segment_name_generator: 'App\Operation\SingularPathSegmentNameGenerator' ``` #### Configuring It using Laravel ```php App\Operation\SingularPathSegmentNameGenerator::class ]; ``` --- # URL Generation Strategy Source: https://api-platform.com/docs/core/url-generation-strategy/ # URL Generation Strategy By default, API Platform generates all URLs as absolute paths to the base URL. For instance, in JSON-LD, you will get a collection like this: ```json { "@context": "/contexts/Book", "@id": "/books", "@type": "Collection", "member": [ { "@id": "/books/1", "@type": "https://schema.org/Book", "name": "My awesome book" } ], "totalItems": 1 } ``` You may want to use absolute URLs (for instance if resources are used in another API) or network paths instead. ## Configure URL Generation Globally It can be configured globally using one of the configurations below: ### Configure URL Generation Globally using Symfony ```yaml # api/config/packages/api_platform.yaml api_platform: defaults: url_generation_strategy: !php/const ApiPlatform\Metadata\UrlGeneratorInterface::ABS_URL ``` ### Configure URL Generation Globally using Laravel ```php [ 'url_generation_strategy' => ApiPlatform\Metadata\UrlGeneratorInterface::ABS_URL ], ]; ``` ## Configure URL Generation for a Specific Resource It can also be configured only for a specific resource: ```php ``` For the above configuration, the collection will be like this: ```json { "@context": "http://example.com/contexts/Book", "@id": "http://example.com/books", "@type": "Collection", "member": [ { "@id": "http://example.com/books/1", "@type": "https://schema.org/Book", "name": "My awesome book" } ], "totalItems": 1 } ``` --- # Extending JSON-LD AND Hydra Contexts Source: https://api-platform.com/docs/core/extending-jsonld-context/ # Extending JSON-LD AND Hydra Contexts ## JSON-LD

JSON-LD screencast
Watch the JSON-LD screencast

API Platform provides the possibility to extend the JSON-LD context of properties. This allows you to describe JSON-LD-typed values, inverse properties using the `@reverse` keyword, and you can even overwrite the `@id` property this way. Everything you define within the following annotation will be passed to the context. This provides a generic way to extend the context. ```php 'http://yourcustomid.com', '@type' => 'http://www.w3.org/2001/XMLSchema#string', 'someProperty' => [ 'a' => 'textA', 'b' => 'textB' ] ] )] public $name; // ... } ``` The generated context will now have your custom attributes set: `GET /contexts/Book` ```json { "@context": { "@vocab": "http://example.com/apidoc#", "hydra": "http://www.w3.org/ns/hydra/core#", "name": { "@id": "http://yourcustomid.com", "@type": "http://www.w3.org/2001/XMLSchema#string", "someProperty": { "a": "textA", "b": "textB" } } } } ``` Note that you do not have to provide the `@id` attribute. If you do not provide an `@id` attribute, the value from `iri` will be used. ## Hydra

Hydra screencast
Watch the Hydra screencast

It's also possible to replace the Hydra context used by the documentation generator: ```php 'bar']) ])] class Book { //... } ``` ```yaml # api/config/api_platform/resources.yaml # The YAML syntax is only supported for Symfony resources: App\ApiResource\Book: operations: ApiPlatform\Metadata\Get: hydraContext: { foo: "bar" } ``` ```xml bar ``` --- # Identifiers Source: https://api-platform.com/docs/core/identifiers/ # Identifiers Every item operation has an identifier in its URL. Although this identifier is usually a number, it can also be an `UUID`, a date, or the type of your choice. To help with your development experience, we introduced an identifier normalization process. ## Custom Identifier Normalizer > [!WARNING] This feature is not yet available with Laravel, if you need it please open a Feature > Request issue! In the following chapter, we're assuming that `App\Uuid` is a project-owned class > that manages a time-based UUID. Let's say you have the following class, which is identified by a `UUID` type. In this example, `UUID` is not a simple string but an object with many attributes. ```php ``` Once registered as an `ApiResource`, having an existing person, it will be accessible through the following URL: `/people/110e8400-e29b-11d4-a716-446655440000`. Note that the property identifying our resource is named `code`. Let's create a `Provider` for the `Person` resource: ```php */ final class PersonProvider implements ProviderInterface { public function provide(Operation $operation, array $uriVariables = [], array $context = []): Person { // Our identifier is: // $uriVariables['code'] // although it's a string, it's not an instance of Uuid and we wanted to retrieve the timestamp of our time-based uuid: // $uriVariable['code']->getTimestamp() } } ``` To cover this use case, we need to `transform` the identifier to an instance of our `App\Uuid` class. This case is covered by an URI variable transformer: ```php getMessage()); } } /** * Checks whether the given uri variable is supported for transformation by this transformer. * * @param mixed $value The uri variable value to transform * @param array $types The types to which the data should be transformed * @param array $context Options available to the transformer */ public function supportsTransformation($value, array $types, array $context = []): bool { foreach ($types as $type) { if (is_a($type, Uuid::class, true)) { return true; } } return false; } } ``` Tag this service as an `api_platform.uri_variables.transformer` using one of the configurations below: ### Tag the Service using Symfony ```yaml # api/config/services.yaml # The YAML syntax is only supported for Symfony services: App\Identifier\UuidUriVariableTransformer: tags: - { name: api_platform.uri_variables.transformer } ``` ```xml ``` Your `PersonProvider` will now work as expected! ## Decorating the IdentifiersExtractor The `IdentifiersExtractor` is responsible for extracting identifiers from a resource. By default, when `\DateTime` objects are used as identifiers, their serialization to `IRI` format is handled by the `DateTimeUriVariableTransformer`, which internally uses Symfony's `DateTimeNormalizer`. If you need a custom format, you can decorate the `IdentifiersExtractor`. Let's say you want to format all `\DateTime` identifiers to `Y-m-d`. First, create a custom `IdentifiersExtractor` that decorates the original: ```php decorated->getIdentifiersFromItem($item, $options); foreach ($identifiers as $key => $value) { if ($value instanceof DateTimeInterface) { $identifiers[$key] = $value->format('Y-m-d'); } } return $identifiers; } } ``` Then, configure the service decoration in your `services.yaml`: ```yaml # api/config/services.yaml services: App\Identifier\DateTimeIdentifiersExtractor: decorates: "api_platform.identifiers.identifiers_extractor" arguments: ["@.inner"] public: false ``` ```xml ``` Now, all `\DateTime` identifiers will be formatted as `Y-m-d` in your resource IRIs. ## Changing Identifier in a Doctrine Entity If your resource is also a Doctrine entity and you want to use another identifier other than the Doctrine one, you have to unmark it: ```php offers = new ArrayCollection(); } public function getId(): ?int { return $this->id; } public function addOffer(Offer $offer): void { $offer->product = $this; $this->offers->add($offer); } public function removeOffer(Offer $offer): void { $offer->product = null; $this->offers->removeElement($offer); } // ... } ``` ```php id; } } ``` When defining references, always use the ID for storing them instead of the native [DBRef](https://docs.mongodb.com/manual/reference/database-references/#dbrefs). It allows API Platform to manage [filtering on nested properties](filters.md#apifilter-annotation) by using [lookups](https://docs.mongodb.com/manual/reference/operator/aggregation/lookup/). ## Filtering Doctrine MongoDB ODM filters are practically the same as Doctrine ORM filters. See how to use them and how to create custom ones in the [filters documentation](filters.md). ## Creating Custom Extensions See how to create Doctrine MongoDB ODM custom extensions in the [extensions documentation](extensions.md). ## Adding Execute Options If you want to add some command options when executing an aggregate query (see the [related documentation in MongoDB manual](https://docs.mongodb.com/manual/reference/command/aggregate/#command-fields)), you can do it in your resource configuration, at the operation or the resource level. For instance at the operation level: ```php ['execute_options' => ['allowDiskUse' => true]]])] class Offer { // ... } ``` Or at the resource level: ```php ['execute_options' => ['allowDiskUse' => true]]])] class Offer { // ... } ``` --- # Elasticsearch & OpenSearch Support Source: https://api-platform.com/docs/core/elasticsearch/ # Elasticsearch & OpenSearch Support ## Overview [Elasticsearch](https://www.elastic.co/elasticsearch/) and [OpenSearch](https://opensearch.org/) are distributed RESTful search and analytics engines capable of solving a growing number of use cases: application search, security analytics, metrics, logging, etc. OpenSearch is an open-source fork of Elasticsearch. API Platform comes natively with **reading** support for both Elasticsearch and OpenSearch. It uses internally the official PHP clients: [Elasticsearch-PHP](https://www.elastic.co/guide/en/elasticsearch/client/php-api/current/index.html) or [OpenSearch-PHP](https://opensearch.org/docs/latest/clients/php/). API Platform supports Elasticsearch >= 7.11.0 < 8.0, Elasticsearch >= 8.4 < 9.0, and OpenSearch >= 2.x. Support for Elasticsearch 8 was introduced in API Platform 3.2, and OpenSearch support was introduced in API Platform 4.3. ## Enabling Reading Support To enable the reading support, require the appropriate PHP client using Composer. For Elasticsearch 8: ```console composer require elasticsearch/elasticsearch:^8.4 ``` For Elasticsearch 7: ```console composer require elasticsearch/elasticsearch:^7.11 ``` For OpenSearch: ```console composer require opensearch-project/opensearch-php:^2.5 ``` Then, enable it inside the API Platform configuration, using one of the configurations below: ### Enabling Reading Support using Symfony For Elasticsearch: ```yaml # api/config/packages/api_platform.yaml parameters: # ... env(ELASTICSEARCH_HOST): "http://localhost:9200" api_platform: # ... mapping: paths: ["%kernel.project_dir%/src/Model"] elasticsearch: hosts: ["%env(ELASTICSEARCH_HOST)%"] #... ``` For OpenSearch, set the `client` option to `opensearch`: ```yaml # api/config/packages/api_platform.yaml parameters: # ... env(OPENSEARCH_HOST): "http://localhost:9200" api_platform: # ... mapping: paths: ["%kernel.project_dir%/src/Model"] elasticsearch: client: opensearch hosts: ["%env(OPENSEARCH_HOST)%"] #... ``` #### SSL Configuration When connecting over HTTPS with self-signed certificates or custom Certificate Authorities, you can configure SSL verification. This works for both Elasticsearch and OpenSearch. **With a custom CA bundle:** ```yaml # config/packages/api_platform.yaml api_platform: elasticsearch: hosts: ["%env(ELASTICSEARCH_HOST)%"] ssl_ca_bundle: "/path/to/ca-bundle.crt" ``` **Disable SSL verification (dev/test only):** ```yaml # config/packages/api_platform.yaml api_platform: elasticsearch: hosts: ["%env(ELASTICSEARCH_HOST)%"] ssl_verification: false # Never use in production ``` > [!NOTE] You cannot use both options together. ### Enabling Reading Support using Laravel For Elasticsearch: ```php [ 'paths' => [ base_path('app/Models'), ], ], 'elasticsearch' => [ 'hosts' => [ env('ELASTICSEARCH_HOST', 'http://localhost:9200'), ], ], ]; ``` For OpenSearch: ```php [ 'paths' => [ base_path('app/Models'), ], ], 'elasticsearch' => [ 'client' => 'opensearch', 'hosts' => [ env('OPENSEARCH_HOST', 'http://localhost:9200'), ], ], ]; ``` ## Creating Models API Platform follows the best practices of Elasticsearch: - a single index per resource should be used because Elasticsearch is going to [drop support for index types and will allow only a single type per index](https://www.elastic.co/guide/en/elasticsearch/reference/current/removal-of-types.html); - index name should be the short resource name in lower snake_case; - the default `_doc` type should be used; - all fields should be lower case and should use camelCase for combining words. This involves having mappings and models which absolutely match each other. Here is an example of mappings for 2 resources, `User` and `Tweet`, and their models: `PUT user` ```json { "mappings": { "_doc": { "properties": { "id": { "type": "keyword" }, "gender": { "type": "keyword" }, "age": { "type": "integer" }, "first_name": { "type": "text" }, "last_name": { "type": "text" }, "tweets": { "type": "nested", "properties": { "id": { "type": "keyword" }, "date": { "type": "date", "format": "yyyy-MM-dd HH:mm:ss" }, "message": { "type": "text" } }, "dynamic": "strict" } }, "dynamic": "strict" } } } ``` `PUT tweet` ```json { "mappings": { "_doc": { "properties": { "id": { "type": "keyword" }, "author": { "properties": { "id": { "type": "keyword" }, "gender": { "type": "keyword" }, "age": { "type": "integer" }, "first_name": { "type": "text" }, "last_name": { "type": "text" } }, "dynamic": "strict" }, "date": { "type": "date", "format": "yyyy-MM-dd HH:mm:ss" }, "message": { "type": "text" } }, "dynamic": "strict" } } } ``` ```php [!WARNING] In API Platform 4.0 with Symfony, you need `use_symfony_listeners: true` to activate > event listeners. --- > [!NOTE] Using Kernel event with API Platform should be mostly limited to tweaking the generated > HTTP response. Also, GraphQL is **not supported**. We recommend to use > [System providers and processors](extending.md#system-providers-and-processors) to extend API > Platform internals. API Platform Core implements the [Action-Domain-Responder](https://github.com/pmjones/adr) pattern. This implementation is covered in depth in the [Creating custom operations and controllers](operations.md#creating-custom-operations-and-controllers) chapter. Basically, API Platform Core executes an action class that will return an entity or a collection of entities. Then a series of event listeners are executed which validate the data, persist it in database, serialize it (typically in a JSON-LD document) and create an HTTP response that will be sent to the client. To do so, API Platform Core leverages [events triggered by the Symfony HTTP Kernel](https://symfony.com/doc/current/reference/events.html#kernel-events). You can also hook your own code to those events. There are handy and powerful extension points available at all points of the request lifecycle. If you are using Doctrine, lifecycle events ([ORM](https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/events.html#lifecycle-events), [MongoDB ODM](https://www.doctrine-project.org/projects/doctrine-mongodb-odm/en/current/reference/events.html#lifecycle-events)) are also available if you want to hook into the persistence layer's object lifecycle. ## Built-in Event Listeners These built-in event listeners are registered for routes managed by API Platform: | Name | Event | [Pre & Post hooks](#custom-event-listeners) | Priority | Description | | -------------------------------- | ------------------ | ------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------- | | `AddFormatListener` | `kernel.request` | None | 28 | Guesses the best response format ([content negotiation](content-negotiation.md)) | | `ReadListener` | `kernel.request` | `PRE_READ`, `POST_READ` | 4 | Retrieves data from the persistence system using the [state providers](state-providers.md) (`GET`, `PUT`, `PATCH`, `DELETE`) | | `QueryParameterValidateListener` | `kernel.request` | None | 2 | Validates query parameters | | `DeserializeListener` | `kernel.request` | `PRE_DESERIALIZE`, `POST_DESERIALIZE` | 2 | Deserializes data into a PHP entity (`POST`); updates the entity retrieved using the state provider (`PUT`, `PATCH`) | | `DenyAccessListener` | `kernel.request` | None | 1 | Enforces [access control](security.md) using Security expressions | | `ValidateListener` | `kernel.view` | `PRE_VALIDATE`, `POST_VALIDATE` | 64 | [Validates data](validation.md) (`POST`, `PUT`, `PATCH`) | | `WriteListener` | `kernel.view` | `PRE_WRITE`, `POST_WRITE` | 32 | Persists changes in the persistence system using the [state processors](state-processors.md) (`POST`, `PUT`, `PATCH`, `DELETE`) | | `SerializeListener` | `kernel.view` | `PRE_SERIALIZE`, `POST_SERIALIZE` | 16 | Serializes the PHP entity in string [according to the request format](content-negotiation.md) | | `RespondListener` | `kernel.view` | `PRE_RESPOND`, `POST_RESPOND` | 8 | Transforms serialized to a `Symfony\Component\HttpFoundation\Response` instance | | `AddLinkHeaderListener` | `kernel.response` | None | 0 | Adds a `Link` HTTP header pointing to the Hydra documentation | | `ValidationExceptionListener` | `kernel.exception` | None | 0 | Serializes validation exceptions in the Hydra format | | `ExceptionListener` | `kernel.exception` | None | -96 | Serializes PHP exceptions in the Hydra format (including the stack trace in debug mode) | Some of these built-in listeners can be enabled/disabled by setting operation attributes: | Attribute | Type | Default | Description | | -------------------------- | ------ | ------- | ---------------------------------------------------- | | `query_parameter_validate` | `bool` | `true` | Enables or disables `QueryParameterValidateListener` | | `read` | `bool` | `true` | Enables or disables `ReadListener` | | `deserialize` | `bool` | `true` | Enables or disables `DeserializeListener` | | `validate` | `bool` | `true` | Enables or disables `ValidateListener` | | `write` | `bool` | `true` | Enables or disables `WriteListener` | | `serialize` | `bool` | `true` | Enables or disables `SerializeListener` | Some of these built-in listeners can be enabled/disabled by setting request attributes (for instance in the [`defaults` attribute of an operation](operations.md#recommended-method)): | Attribute | Type | Default | Description | | -------------- | ------ | ------- | ----------------------------------------------------------------------------- | | `_api_receive` | `bool` | `true` | Enables or disables `ReadListener`, `DeserializeListener`, `ValidateListener` | | `_api_respond` | `bool` | `true` | Enables or disables `SerializeListener`, `RespondListener` | | `_api_persist` | `bool` | `true` | Enables or disables `WriteListener` | ## Custom Event Listeners Registering your own event listeners to add extra logic is convenient. The [`ApiPlatform\Symfony\EventListener\EventPriorities`](https://github.com/api-platform/core/blob/main/src/Symfony/EventListener/EventPriorities.php) class comes with a convenient set of class constants corresponding to commonly used priorities: | Constant | Event | Priority | | ------------------ | ----------------- | -------- | | `PRE_READ` | `kernel.request` | 5 | | `POST_READ` | `kernel.request` | 3 | | `PRE_DESERIALIZE` | `kernel.request` | 3 | | `POST_DESERIALIZE` | `kernel.request` | 1 | | `PRE_VALIDATE` | `kernel.view` | 65 | | `POST_VALIDATE` | `kernel.view` | 63 | | `PRE_WRITE` | `kernel.view` | 33 | | `POST_WRITE` | `kernel.view` | 31 | | `PRE_SERIALIZE` | `kernel.view` | 17 | | `POST_SERIALIZE` | `kernel.view` | 15 | | `PRE_RESPOND` | `kernel.view` | 9 | | `POST_RESPOND` | `kernel.response` | 0 | In the following example, we will send a mail each time a new book is created using the API: ```php ['sendMail', EventPriorities::POST_WRITE], ]; } public function sendMail(ViewEvent $event): void { $book = $event->getControllerResult(); $method = $event->getRequest()->getMethod(); if (!$book instanceof Book || Request::METHOD_POST !== $method) { return; } $message = (new Email()) ->from('system@example.com') ->to('contact@les-tilleuls.coop') ->subject('A new book has been added') ->text(sprintf('The book #%d has been added.', $book->getId())); $this->mailer->send($message); } } ``` If you use the API Platform Symfony variant, creating the previous class is enough. The Symfony DependencyInjection component will automatically register this subscriber as a service and will inject its dependencies thanks to the [autowiring feature](https://symfony.com/doc/current/service_container/autowiring.html). Alternatively, [the subscriber must be registered manually](https://symfony.com/doc/current/components/event_dispatcher.html#connecting-listeners). --- # JWT Authentication Source: https://api-platform.com/docs/core/jwt/ # JWT Authentication > [JSON Web Token (JWT)](https://jwt.io/) is a JSON-based open standard > ([RFC 7519](https://tools.ietf.org/html/rfc7519)) for creating access tokens that assert some > number of claims. For example, a server could generate a token that has the claim "logged in as > admin" and provide that to a client. The client could then use that token to prove that he/she is > logged in as admin. The tokens are signed by the server's key, so the server is able to verify > that the token is legitimate. The tokens are designed to be compact, URL-safe and usable > especially in web browser single sign-on (SSO) context. > > ―[Wikipedia](https://en.wikipedia.org/wiki/JSON_Web_Token) - For Symfony users, check out the [JWT Authentication with Symfony documentation](/symfony/jwt.md). - For Laravel users, explore the [JWT Authentication with Laravel documentation](/laravel/jwt.md). --- # Accept application/x-www-form-urlencoded Form Data Source: https://api-platform.com/docs/core/form-data/ # Accept `application/x-www-form-urlencoded` Form Data API Platform only supports raw documents as request input (encoded in JSON, XML, YAML...). This has many advantages including support of types and the ability to send back to the API documents originally retrieved through a `GET` request. However, sometimes - for instance, to support legacy clients - it is necessary to accept inputs encoded in the traditional [`application/x-www-form-urlencoded`](https://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.1) format (HTML form content type). This can easily be done using the powerful [System providers and processors](extending.md#system-providers-and-processors) of the framework. > [!WARNING] > Adding support for `application/x-www-form-urlencoded` makes your API vulnerable to > [CSRF (Cross-Site Request Forgery)]() > attacks. > It's crucial to implement proper countermeasures to protect your application. > > If you're using Symfony, make sure you enable > [Stateless CSRF protection](https://symfony.com/blog/new-in-symfony-7-2-stateless-csrf). > > If you're working with Laravel, refer to the > [Laravel CSRF documentation](https://laravel.com/docs/csrf) to ensure adequate protection against > such attacks. ## Configuration First, you must register the form format and map it to the `application/x-www-form-urlencoded` MIME type in your API Platform configuration: ```yaml # api\_platform.yaml api_platform: formats: jsonld: ["application/ld+json"] form: ["application/x-www-form-urlencoded"] ``` ## Creating a Decoder The Symfony Serializer (used by API Platform) does not decode `application/x-www-form-urlencoded` by default. You need to create a custom decoder that implements DecoderInterface to handle this format. ```php ['application/x-www-form-urlencoded']], processor: [self::class, 'process'] )] class FormData { public string $name; public static function process(mixed $data, Operation $operation, array $uriVariables, array $context) { return $data; } } ``` --- # Bootstrapping the Core Library Source: https://api-platform.com/docs/core/bootstrap/ # Bootstrapping the Core Library You may want to run a minimal version of API Platform. This one file runs API Platform (without GraphQL, Eloquent, Doctrine MongoDB...). It requires the following Composer packages: > [!NOTE] > > This documentation is a work in progress we're working on improving it, in the mean time we > declare most of the services manually in the > [ApiPlatformProvider](https://github.com/api-platform/core/blob/64768a6a5b480e1b8e33c639fb28b27883c69b79/src/Laravel/ApiPlatformProvider.php) > it can be source of inspiration. ## Components API Platform is installable as a set of components, for example: ```console composer require \ api-platform/serializer \ api-platform/metadata \ api-platform/state \ api-platform/jsonld \ phpdocumentor/reflection-docblock \ symfony/property-info \ symfony/routing \ symfony/validator ``` --- # Configuration Reference Source: https://api-platform.com/docs/core/configuration/ # Configuration Reference ## Symfony Configuration Here's the complete configuration of [API Platform for Symfony](../symfony/index.md) including default values: ```yaml # api/config/packages/api_platform.yaml api_platform: # The title of the API. title: 'API title' # The description of the API. description: 'API description' # The version of the API. version: '0.0.0' # Set this to false if you want Webby to disappear. show_webby: true # Specify a name converter to use. name_converter: ~ # Specify an asset package name to use. asset_package: null # Specify a path name generator to use. path_segment_name_generator: 'api_platform.path_segment_name_generator.underscore' validator: # Enable the serialization of payload fields when a validation error is thrown. # If you want to serialize only some payload fields, define them like this: [ severity, anotherPayloadField ] serialize_payload_fields: [] # To enable or disable query parameters validation on collection GET requests query_parameter_validation: true eager_loading: # To enable or disable eager loading. enabled: true # Fetch only partial data according to serialization groups. # If enabled, Doctrine ORM entities will not work as expected if any of the other fields are used. fetch_partial: false # Max number of joined relations before EagerLoading throws a RuntimeException. max_joins: 30 # Force join on every relation. # If disabled, it will only join relations having the EAGER fetch mode. force_eager: true # Enable the Swagger documentation and export. enable_swagger: true # Enable Swagger UI. enable_swagger_ui: true # Enable ReDoc. enable_re_doc: true # Enable the entrypoint. enable_entrypoint: true # Enable the docs. enable_docs: true # Enable the data collector and the WebProfilerBundle integration. enable_profiler: true collection: # The name of the query parameter to filter nullable results (with the ExistsFilter). exists_parameter_name: 'exists' # The default order of results. order: 'ASC' # The name of the query parameter to order results (with the OrderFilter). order_parameter_name: 'order' pagination: # The default name of the parameter handling the page number. page_parameter_name: 'page' # The name of the query parameter to enable or disable pagination. enabled_parameter_name: 'pagination' # The name of the query parameter to set the number of items per page. items_per_page_parameter_name: 'itemsPerPage' # The name of the query parameter to enable or disable the partial pagination. partial_parameter_name: 'partial' mapping: # The list of paths with files or directories where the bundle will look for additional resource files. paths: [] # The list of your resources class directories. Defaults to the directories of the mapping paths but might differ. resource_class_directories: - '%kernel.project_dir%/src/Entity' doctrine: # To enable or disable Doctrine ORM support. enabled: true doctrine_mongodb_odm: # To enable or disable Doctrine MongoDB ODM support. enabled: false oauth: # To enable or disable OAuth. enabled: false # The OAuth client ID. clientId: '' # The OAuth client secret. clientSecret: '' # The OAuth type. type: 'oauth2' # The OAuth flow grant type. flow: 'application' # The OAuth token URL. Make sure to check the specification tokenUrl is not needed for an implicit flow. tokenUrl: '' # The OAuth authentication URL. authorizationUrl: '' # The OAuth scopes. scopes: [] graphql: # Enabled by default with installed api-platform/graphql. enabled: false # The default IDE (graphiql or graphql-playground) used when going to the GraphQL endpoint. False to disable. default_ide: 'graphiql' graphiql: # Enabled by default with installed api-platform/graphql and Twig. enabled: false graphql_playground: # Enabled by default with installed api-platform/graphql and Twig. enabled: false introspection: # Enabled by default with installed api-platform/graphql. enabled: true # The nesting separator used in the filter names. nesting_separator: _ collection: pagination: enabled: true swagger: # The active versions of OpenAPI to be exported or used in the swagger_ui. The first value is the default. versions: [2, 3] # The swagger API keys. api_keys: [] # The name of the header or query parameter containing the API key. # name: '' # Whether the API key should be a query parameter or a header. # type: 'query' or 'header' swagger_ui_extra_configuration: # Controls the default expansion setting for the operations and tags. It can be 'list' (expands only the tags), 'full' (expands the tags and operations) or 'none' (expands nothing). docExpansion: list # If set, enables filtering. The top bar will show an edit box that you can use to filter the tagged operations that are shown. filter: false # You can use any other configuration parameters too. openapi: # The contact information for the exposed API. contact: # The identifying name of the contact person/organization. name: # The URL pointing to the contact information. MUST be in the format of a URL. url: # The email address of the contact person/organization. MUST be in the format of an email address. email: # A URL to the Terms of Service for the API. MUST be in the format of a URL. termsOfService: # The license information for the exposed API. license: # The license name used for the API. name: # URL to the license used for the API. MUST be in the format of a URL. url: http_cache: # To make all responses public by default. public: ~ invalidation: # To enable the tags-based cache invalidation system. enabled: false # URLs of the Varnish servers to purge using cache tags when a resource is updated. varnish_urls: [] # To pass options to the client charged with the request. request_options: [] # Use another service as the purger for example "api_platform.http_cache.purger.varnish.xkey" purger: 'api_platform.http_cache.purger.varnish.ban' mercure: # Enabled by default with installed symfony/mercure-bundle. enabled: false # The URL sent in the Link HTTP header. If not set, will default to MercureBundle's default hub URL. hub_url: null messenger: # Enabled by default with installed symfony/messenger and not installed symfony/symfony. enabled: false elasticsearch: # To enable or disable Elasticsearch support. enabled: false # The hosts to the Elasticsearch nodes. hosts: [] # The mapping between resource classes and indexes. mapping: [] # The list of exceptions mapped to their HTTP status code. exception_to_status: # With a status code. Symfony\Component\Serializer\Exception\ExceptionInterface: 400 # Or with a constant defined in the 'Symfony\Component\HttpFoundation\Response' class. ApiPlatform\Exception\InvalidArgumentException: !php/const Symfony\Component\HttpFoundation\Response::HTTP_BAD_REQUEST ApiPlatform\Exception\FilterValidationException: 400 Doctrine\ORM\OptimisticLockException: 409 # ... # The list of enabled formats. The first one will be the default. formats: jsonld: mime_types: ['application/ld+json'] json: mime_types: ['application/json'] html: mime_types: ['text/html'] # ... # The list of enabled patch formats. The first one will be the default. patch_formats: [] # The list of enabled docs formats. The first one will be the default. docs_formats: jsonld: ['application/ld+json'] #jsonapi: ['application/vnd.api+json'] jsonopenapi: ['application/vnd.openapi+json'] html: ['text/html'] # The list of enabled error formats. The first one will be the default. error_formats: jsonproblem: mime_types: ['application/problem+json'] jsonld: mime_types: ['application/ld+json'] # ... # Global resources defaults, see in the next section. defaults: # ... ``` ### Global Resources Defaults for Symfony If you need to globally configure all the resources instead of adding configuration in each one, it's possible to do so with the `defaults` key: ```yaml # api/config/packages/api_platform.yaml api_platform: defaults: description: ~ iri: ~ short_name: ~ operations: ~ graphql: ~ elasticsearch: ~ security: ~ security_message: ~ security_post_denormalize: ~ security_post_denormalize_message: ~ cache_headers: # Automatically generate etags for API responses. etag: true # Default value for the response max age. max_age: 3600 # Default value for the response shared (proxy) max age. shared_max_age: 3600 # Default values of the "Vary" HTTP header. vary: ["Accept"] invalidation: xkey: glue: " " normalization_context: # Default value to omit null values in conformance with the JSON Merge Patch RFC. skip_null_values: true denormalization_context: ~ swagger_context: ~ openapi_context: ~ deprecation_reason: ~ fetch_partial: ~ force_eager: ~ formats: ~ filters: ~ hydra_context: ~ mercure: ~ messenger: ~ order: ~ # To enable or disable pagination for all resource collections. pagination_enabled: true # To allow the client to enable or disable the pagination. pagination_client_enabled: false # To allow the client to set the number of items per page. pagination_client_items_per_page: false # To allow the client to enable or disable the partial pagination. pagination_client_partial: false # The default number of items per page. pagination_items_per_page: 30 # The maximum number of items per page. pagination_maximum_items_per_page: ~ # To allow partial pagination for all resource collections. # This improves performances by skipping the `COUNT` query. pagination_partial: false # To use cursor-based pagination. pagination_via_cursor: ~ pagination_fetch_join_collection: ~ route_prefix: ~ validation_groups: ~ sunset: ~ input: ~ output: ~ stateless: ~ schemes: ~ options: ~ host: ~ parameters: ~ # The URL generation strategy to use for IRIs url_generation_strategy: !php/const ApiPlatform\Metadata\UrlGeneratorInterface::ABS_PATH # To enable collecting denormalization errors collectDenormalizationErrors: false # ... ``` ## Laravel Configuration Here's the complete configuration of [API Platform for Laravel](../laravel/index.md) including default values: ```php 'API title', // The description of the API. 'description' => 'API description', // The version of the API. 'version' => '0.0.0', // Set this to false if you want Webby to disappear. 'show_webby' => true, // Specify a name converter to use. 'name_converter' => null, // Specify an asset package name to use. 'asset_package' => null, // Specify a path name generator to use. 'path_segment_name_generator' => 'api_platform.path_segment_name_generator.underscore', 'validator' => [ // Enable the serialization of payload fields when a validation error is thrown. // If you want to serialize only some payload fields, define them like this: [ severity, anotherPayloadField ] 'serialize_payload_fields' => [], // To enable or disable query parameters validation on collection GET requests 'query_parameter_validation' => true, ], 'eager_loading' => [ // To enable or disable eager loading. 'enabled' => true, // Fetch only partial data according to serialization groups. // If enabled, Doctrine ORM entities will not work as expected if any of the other fields are used. 'fetch_partial' => false, // Max number of joined relations before EagerLoading throws a RuntimeException. 'max_joins' => 30, // Force join on every relation. // If disabled, it will only join relations having the EAGER fetch mode. 'force_eager' => true, ], // Enable the Swagger documentation and export. 'enable_swagger' => true, // Enable Swagger UI. 'enable_swagger_ui' => true, // Enable ReDoc. 'enable_re_doc' => true, // Enable the entrypoint. 'enable_entrypoint' => true, // Enable the docs. 'enable_docs' => true, // Enable the data collector and the WebProfilerBundle integration. 'enable_profiler' => true, 'collection' => [ // The name of the query parameter to filter nullable results (with the ExistsFilter). 'exists_parameter_name' => 'exists', // The default order of results. 'order' => 'ASC', // The name of the query parameter to order results (with the OrderFilter). 'order_parameter_name' => 'order', 'pagination' => [ // The default name of the parameter handling the page number. 'page_parameter_name' => 'page', // The name of the query parameter to enable or disable pagination. 'enabled_parameter_name' => 'pagination', // The name of the query parameter to set the number of items per page. 'items_per_page_parameter_name' => 'itemsPerPage', // The name of the query parameter to enable or disable the partial pagination. 'partial_parameter_name' => 'partial', ], ], 'mapping' => [ // The list of paths with files or directories where the bundle will look for additional resource files. 'paths' => [], ], // The list of your resources class directories. Defaults to the directories of the mapping paths but might differ. 'resource_class_directories' => [ '%kernel.project_dir%/src/Entity', ], 'doctrine' => [ // To enable or disable Doctrine ORM support. 'enabled' => true, ], 'doctrine_mongodb_odm' => [ // To enable or disable Doctrine MongoDB ODM support. 'enabled' => false, ], 'oauth' => [ // To enable or disable OAuth. 'enabled' => false, // The OAuth client ID. 'clientId' => '', // The OAuth client secret. 'clientSecret' => '', // The OAuth type. 'type' => 'oauth2', // The OAuth flow grant type. 'flow' => 'application', // The OAuth token URL. Make sure to check the specification tokenUrl is not needed for an implicit flow. 'tokenUrl' => '', // The OAuth authentication URL. 'authorizationUrl' => '', // The OAuth scopes. 'scopes' => [], ], 'graphql' => [ // Enabled by default with installed api-platform/graphql. 'enabled' => false, // The default IDE (graphiql or graphql-playground) used when going to the GraphQL endpoint. False to disable. 'default_ide' => 'graphiql', 'graphiql' => [ // Enabled by default with installed api-platform/graphql. 'enabled' => false, ], 'introspection' => [ // Enabled by default with installed api-platform/graphql. 'enabled' => true, ], // The nesting separator used in the filter names. 'nesting_separator' => '_', 'collection' => [ 'pagination' => [ 'enabled' => true, ], ], ], 'swagger' => [ // The active versions of OpenAPI to be exported or used in the swagger_ui. The first value is the default. 'versions' => [2, 3], // The swagger API keys. 'api_keys' => [], 'swagger_ui_extra_configuration' => [ // Controls the default expansion setting for the operations and tags. It can be 'list' (expands only the tags), 'full' (expands the tags and operations) or 'none' (expands nothing). 'docExpansion' => 'list', // If set, enables filtering. The top bar will show an edit box that you can use to filter the tagged operations that are shown. 'filter' => false, // If set to true, it persists authorization data and it would not be lost on browser close/refresh 'persistAuthorization' => true, // You can use any other configuration parameters too. ], ], 'openapi' => [ // The contact information for the exposed API. 'contact' => [ // The identifying name of the contact person/organization. 'name' => null, // The URL pointing to the contact information. MUST be in the format of a URL. 'url' => null, // The email address of the contact person/organization. MUST be in the format of an email address. 'email' => null, ], // A URL to the Terms of Service for the API. MUST be in the format of a URL. 'termsOfService' => null, // The license information for the exposed API. 'license' => [ // The license name used for the API. 'name' => null, // URL to the license used for the API. MUST be in the format of a URL. 'url' => null, ], ], 'http_cache' => [ // To make all responses public by default. 'public' => null, 'invalidation' => [ // To enable the tags-based cache invalidation system. 'enabled' => false, // URLs of the Varnish servers to purge using cache tags when a resource is updated. 'varnish_urls' => [], // To pass options to the client charged with the request. 'request_options' => [], // Use another service as the purger for example "api_platform.http_cache.purger.varnish.xkey" 'purger' => 'api_platform.http_cache.purger.varnish.ban', ], ], 'mercure' => [ // Enabled by default with installed symfony/mercure-bundle. 'enabled' => false, // The URL sent in the Link HTTP header. If not set, will default to MercureBundle's default hub URL. 'hub_url' => null, ], 'messenger' => [ // Enabled by default with installed symfony/messenger and not installed symfony/symfony. 'enabled' => false, ], 'elasticsearch' => [ // To enable or disable Elasticsearch support. 'enabled' => false, // The hosts to the Elasticsearch nodes. 'hosts' => [], // The mapping between resource classes and indexes. 'mapping' => [], ], // The list of exceptions mapped to their HTTP status code. 'exception_to_status' => [ AuthenticationException::class => 401, AuthorizationException::class => 403 ], // The list of routes. 'routes' => [ // Global middleware applied to every API Platform routes // 'middleware' => [] ], // The list of resources. 'resources' => [ app_path('Models'), ], // The list of enabled patch formats. The first one will be the default. 'formats' => [ 'jsonld' => ['mime_types' => ['application/ld+json']], 'json' => ['mime_types' => ['application/json']], 'html' => ['mime_types' => ['text/html']], ], // The list of enabled patch formats. The first one will be the default. 'patch_formats' => [ 'json' => ['application/merge-patch+json'], ], // The list of enabled docs formats. The first one will be the default. 'docs_formats' => [ 'jsonld' => ['application/ld+json'], //'jsonapi' => ['application/vnd.api+json'], 'jsonopenapi' => ['application/vnd.openapi+json'], 'html' => ['text/html'], ], // The list of enabled error formats. The first one will be the default. 'error_formats' => [ 'jsonproblem' => ['mime_types' => ['application/problem+json']], ], // Global resources defaults, see in the next section. 'defaults' => [ 'pagination_enabled' => true, 'pagination_partial' => false, 'pagination_client_enabled' => false, 'pagination_client_items_per_page' => false, 'pagination_client_partial' => false, 'pagination_items_per_page' => 30, 'pagination_maximum_items_per_page' => 30, 'route_prefix' => '/api', 'middleware' => [], ], ]; ``` ### Global Resources Defaults for Laravel If you need to globally configure all the resources instead of adding configuration in each one, it's possible to do so with the `defaults` key: ```php [ 'description' => null, 'iri' => null, 'short_name' => null, 'operations' => null, 'graphql' => null, 'elasticsearch' => null, 'security' => null, 'security_message' => null, 'security_post_denormalize' => null, 'security_post_denormalize_message' => null, 'cache_headers' => [ // Automatically generate etags for API responses. 'etag' => true, // Default value for the response max age. 'max_age' => 3600, // Default value for the response shared (proxy) max age. 'shared_max_age' => 3600, // Default values of the "Vary" HTTP header. 'vary' => ['Accept'], 'invalidation' => [ 'xkey' => [ 'glue' => ' ', ], ], ], 'normalization_context' => [ // Default value to omit null values in conformance with the JSON Merge Patch RFC. 'skip_null_values' => true, ], 'denormalization_context' => null, 'swagger_context' => null, 'openapi_context' => null, 'deprecation_reason' => null, 'fetch_partial' => null, 'force_eager' => null, 'formats' => null, 'filters' => null, 'hydra_context' => null, 'mercure' => null, 'messenger' => null, 'order' => null, // To enable or disable pagination for all resource collections. 'pagination_enabled' => true, // To allow the client to enable or disable the pagination. 'pagination_client_enabled' => false, // To allow the client to set the number of items per page. 'pagination_client_items_per_page' => false, // To allow the client to enable or disable the partial pagination. 'pagination_client_partial' => false, // The default number of items per page. 'pagination_items_per_page' => 30, // The maximum number of items per page. 'pagination_maximum_items_per_page' => null, // To allow partial pagination for all resource collections. // This improves performances by skipping the `COUNT` query. 'pagination_partial' => false, // To use cursor-based pagination. 'pagination_via_cursor' => null, 'pagination_fetch_join_collection' => null, 'route_prefix' => null, 'validation_groups' => null, 'sunset' => null, 'input' => null, 'output' => null, 'stateless' => null, 'parameters' => null, // The URL generation strategy to use for IRIs 'url_generation_strategy' => UrlGeneratorInterface::ABS_PATH, // To enable collecting denormalization errors 'collectDenormalizationErrors' => false, ], ]; ``` --- # Client Integrations Source: https://api-platform.com/docs/core/client-integration/ # Client Integrations ## Edge Side API (ESA) > [Edge Side APIs (ESA)](https://edge-side-api.rocks/) is an architectural pattern that allows the > creation of more reliable, efficient, and less resource-intensive APIs. It revives the core > REST/HATEOAS principles while taking full advantage of the new capabilities provided by the web > platform. > > ESA promotes a mixed approach (synchronous and asynchronous), offering simplicity in development > and use, exceptional performance, and the ability for clients to receive real-time updates of the > resources they fetched. ESA also leverages existing standards to expose API documentation, > enabling the creation of generic clients capable of discovering the API’s capabilities at runtime. > > — _From [ESA White Paper](https://edge-side-api.rocks/white-paper)_ ## JavaScript Client Integrations API Platform offers a suite of tools and libraries that streamline the integration of JavaScript clients with APIs. These tools simplify development by automating tasks such as data fetching, administration panel creation, and real-time updates. Below is a detailed overview of the available clients, libraries, and their usage. ### Clients and Tools Overview #### Admin API Platform Admin is a dynamic administration panel generator built with [React-Admin](https://marmelab.com/react-admin/). It automatically adapts to your API schema and provides extensive customization options. It can read an [OpenAPI](https://www.openapis.org/) specification or a [Hydra](https://www.hydra-cg.com/) specification. API Platform supports both [OpenAPI](openapi.md) and [Hydra](extending-jsonld-context.md#hydra) from scratch! [Learn more about API Platform Admin](../admin/index.md). #### Create Client The Client Generator creates JavaScript/TypeScript clients based on your API documentation. It generates code that integrates seamlessly with your API endpoints, reducing development time and errors. [Learn more about the Create Client](../create-client/index.md) ### JavaScript Libraries #### api-platform/ld The [api-platform/ld](https://edge-side-api.rocks/linked-data) JavaScript library simplifies working with Linked Data. It helps parse and serialize data in formats such as [JSON-LD](extending-jsonld-context.md#json-ld), making it easier to handle complex relationships in your applications. For example, let's load authors when required with a Linked Data approach. Given an API referencing books and their authors, where `GET /books/1` returns: ```json { "@id": "/books/1", "@type": ["https://schema.org/Book"], "title": "Hyperion", "author": "https://localhost/authors/1" } ``` Use an [URLPattern](https://urlpattern.spec.whatwg.org/) to load authors automatically when fetching an author property such as `books.author?.name`: ```javascript import ld from "@api-platform/ld"; const pattern = new URLPattern("/authors/:id", "https://localhost"); const books = await ld("/books", { urlPattern: pattern, onUpdate: (newBooks) => { log(); }, }); function log() { console.log(books.author?.name); } log(); ``` With [api-platform/ld](https://edge-side-api.rocks/linked-data), authors are automatically loaded when needed. [Read the full documentation](https://edge-side-api.rocks/linked-data). #### api-platform/mercure [Mercure](https://mercure.rocks/spec) is a real-time communication protocol. The [api-platform/mercure](https://edge-side-api.rocks/mercure) library enables you to subscribe to updates and deliver real-time data seamlessly. Our frontend library allows you to subscribe to updates with efficiency, re-using the hub connection and adding topics automatically as they get requested. API Platform [supports Mercure](mercure.md) and automatically sets the [Link header](https://mercure.rocks/spec#content-negotiation) making auto-discovery a breeze. For example: ```javascript import mercure, { close } from "@api-platform/mercure"; const res = await mercure("https://localhost/authors/1", { onUpdate: (author) => console.log(author), }); const author = res.then((res) => res.json()); // Close if you need to history.onpushstate = function (e) { close("https://localhost/authors/1"); }; ``` Assuming `/authors/1` returned the following: ```http Link: ; rel="self" Link: ; rel="mercure" ``` An `EventSource` subscribes to the topic `https://localhost/authors/1` on the hub `https://localhost/.well-known/mercure`. [Read the full documentation](https://edge-side-api.rocks/mercure). #### api-platform/api-doc-parser The [api-platform/api-doc-parser](https://github.com/api-platform/api-doc-parser) that parses Hydra, Swagger, OpenAPI, and GraphQL documentation into an intermediate format for generating API clients and scaffolding code. It integrates well with API Platform and supports auto-detecting resource relationships. Key Features: - Multi-format support: Parses Hydra, Swagger (OpenAPI v2), OpenAPI v3, and GraphQL. - Intermediate representation: Converts API docs into a usable format for generating clients, scaffolding code, or building admin interfaces. - API Platform integration: Works seamlessly with API Platform. - Auto-detection of resource relationships: Automatically detects relationships between resources based on documentation. Example: Parsing [Hydra](http://hydra-cg.com/) API Documentation: ```javascript import { parseHydraDocumentation } from "@api-platform/api-doc-parser"; parseHydraDocumentation("https://demo.api-platform.com").then(({ api }) => console.log(api)); ``` This example fetches Hydra documentation from `https://demo.api-platform.com`, parses it, and logs the resulting API structure. The `parseHydraDocumentation` method is particularly useful for building metadata-driven clients or handling advanced API interactions. [Read the full documentation](https://github.com/api-platform/api-doc-parser). --- # The Schema Generator Source: https://api-platform.com/docs/schema-generator/ # The Schema Generator `schema` is a command-line tool part of [the API Platform framework](https://api-platform.com) that instantly generates a set of PHP classes from [RDF](https://en.wikipedia.org/wiki/Resource_Description_Framework) vocabularies such as (but not limited to) [Schema.org](https://schema.org) or [ActivityStreams](https://www.w3.org/TR/activitystreams-core/). Alternatively, it can generate PHP classes from an [OpenAPI](https://www.openapis.org/) documentation. [Find and browse](https://lov.linkeddata.es/) (or create) a vocabulary that fits your needs, choose the types and properties you need, run our code generator and you're done! Alternatively, design your API with tools like [Stoplight](https://stoplight.io/), export the OpenAPI documentation, run our code generator and your API is ready! ![Stoplight](images/stoplight.png) You get a fully featured PHP data model including: - A set of PHP entities with properties, constants (enum values), getters, setters, adders and removers. The class hierarchy provided by the vocabulary will be translated to a PHP class hierarchy with parents as `abstract` classes. The generated code complies with [PSR](https://www.php-fig.org/) coding standards; - Full, high-quality PHPDoc and type declarations for classes, properties, constants and methods extracted from the vocabulary; - Doctrine ORM or MongoDB ODM attributes mapping including database columns / fields with type guessing, relations with cardinality guessing, smart class inheritance (through the `#[MappedSuperclass]` or `#[InheritanceType]` attributes depending on if the resource is used in a relation); - Data validation through [Symfony Validator](https://symfony.com/doc/current/book/validation.html) attributes including enum support (choices) and check for required properties; - API Platform attributes; - Interfaces and [Doctrine `ResolveTargetEntityListener`](https://www.doctrine-project.org/projects/doctrine-orm/en/current/cookbook/resolve-target-entity-listener.html) support; - Custom PHP namespace support; - List of values provided the vocabulary with [PHP Enum](https://github.com/myclabs/php-enum) classes. Bonus: - The code generator is fully configurable and extendable. All features can be deactivated (e.g., the Doctrine mapping generator) and a custom generator can be added; - The code generator can load previously generated files and add new changes while keeping the user-added ones; - The generated code can be used as is in a [Symfony](https://symfony.com) app (but it will work too in a raw PHP project or any other framework including [Laravel](https://laravel.com) and [Zend Framework](https://framework.zend.com/)). ## What Is Schema.org? Schema.org is a vocabulary representing common data structures and their relations. Schema.org can be exposed as [JSON-LD](https://en.wikipedia.org/wiki/JSON-LD), [microdata]() and [RDF](https://en.wikipedia.org/wiki/Resource_Description_Framework). Extracting semantical data exposed in the Schema.org vocabulary is supported by a growing number of companies including Google (Search, Gmail), Yahoo!, Bing and Yandex. ## Why Use Schema.org Data to Generate a PHP Model? ### Don't Reinvent the Wheel Data models provided by Schema.org are popular and were proven efficient. They cover a broad spectrum of topics including creative works, e-commerce, events, medicine, social networking, people, postal addresses, organization data, places or reviews. Schema.org has its root in a ton of preexisting well-designed vocabularies and is successfully used by more and more websites and applications. Pick schemas applicable to your application, generate your PHP model, then customize and specialize it to fit your needs. ### Improve SEO and User Experience Adding Schema.org markup to websites and apps increases their ranking in search engines results and enables awesome features such as [Google Rich Snippets](https://support.google.com/webmasters/answer/99170?hl=en) and [Gmail markup](https://developers.google.com/gmail/markup/overview). Mapping your app data model to Schema.org structures can be tedious. When using the generator, your data model will be derived from Schema.org. Adding microdata markup to your templates or serializing your data as JSON-LD will not require specific mapping nor adaptation. It's a matter of minutes. ### Be Ready for The Future Schema.org improves the interoperability of your applications. Used with hypermedia technologies such as [Hydra](https://www.hydra-cg.com/) it's a big step towards the semantic and machine-readable web. It opens the way to generic web API clients able to extract and process data from any website or app using such technologies. ## Documentation - [Getting Started](getting-started.md) - [Configuration](configuration.md) --- # Getting Started Source: https://api-platform.com/docs/schema-generator/getting-started/ # Getting Started ## Installation If you use [the API Platform Symfony variant](../symfony/index.md), the Schema Generator is already installed as a development dependency of your project and can be invoked with: ```console vendor/bin/schema ``` The Schema Generator can also [be downloaded independently as a PHAR](https://github.com/api-platform/schema-generator/releases) or installed in an existing project using [Composer](https://getcomposer.org): ```console composer require --dev api-platform/schema-generator ``` ## Configuration The Schema Generator can either be used with Schema.org types (see [model scaffolding](#model-scaffolding)) or with an OpenAPI documentation (see [OpenAPI generation](#openapi-generation)). Choose your preferred way of designing your API, and [run the generator](#usage)! ### Model Scaffolding Start by browsing [Schema.org](https://schema.org) (or any other RDF vocabulary) and pick types applicable to your application. Schema.org provides tons of schemas including (but not limited to) representations of people, organizations, events, postal addresses, creative work and e-commerce structures. Many other open vocabularies can be found on [the LOV website](https://lov.linkeddata.es/). Then, write a simple YAML config file similar to the following. Here we will generate a data model for an address book with the following data: - a [`Person`](https://schema.org/Person) which inherits from [`Thing`](https://schema.org/Thing); - a [`PostalAddress`](https://schema.org/PostalAddress) (without its class hierarchy). ```yaml # api/config/schema.yaml # The list of types and properties we want to use types: # Parent class of Person Thing: properties: name: ~ Person: # Enable the generation of the class hierarchy (not enabled by default) parent: ~ properties: familyName: ~ givenName: ~ additionalName: ~ address: ~ PostalAddress: properties: # Force the type of the addressCountry property to text addressCountry: { range: "Text" } addressLocality: ~ addressRegion: ~ postOfficeBoxNumber: ~ postalCode: ~ streetAddress: ~ ``` **Note:** If no properties are specified for a given type, all its properties will be generated. The generator also supports enumeration generation. For subclasses of [`Enumeration`](https://schema.org/Enumeration), the generator will automatically create a class extending the Enum type provided by [myclabs/php-enum](https://github.com/myclabs/php-enum). Don't forget to install this library in your project. Refer you to PHP Enum documentation to see how to use it. The Symfony validation annotation generator automatically takes care of enumerations to validate choices values. A config file generating an enum class: ```yaml types: OfferItemCondition: # The generator will automatically guess that OfferItemCondition is subclass of Enum properties: {} # Remove all properties of the parent class ``` ### OpenAPI Generation Design your API with tools like [Stoplight](https://stoplight.io/). Export your OpenAPI documentation to a JSON or to a YAML file and place it somewhere in your project (for instance in `api/openapi.yaml`). Write the following config file: ```yaml # api/config/schema.yaml openApi: file: "../openapi.yaml" ``` ## Usage Run the generator with the config file as parameter: ```console vendor/bin/schema generate api/src/ api/config/schema.yaml -vv ``` Using [the API Platform Symfony variant](../symfony/index.md): ```console vendor/bin/schema generate src/ config/schema.yaml -vv ``` The corresponding PHP classes will be automatically generated in the `src/` directory! Note that the generator takes care of creating directories corresponding to the namespace structure. Without configuration file, the tool will build the entire Schema.org vocabulary. ## Load Previously Generated Files If you launch the schema generator again, the previously generated files will be loaded. It will try to keep as much user-added changes as possible while adding the new changes from the configuration file. You can also choose to overwrite the file instead. ## Going Further Browse [the configuration documentation](configuration.md). ### Cardinality Extraction The Cardinality Extractor is a standalone tool (also used internally by the generator) extracting a property's cardinality. It extracts cardinality described with the [Web Ontology Language (OWL)](https://en.wikipedia.org/wiki/Web_Ontology_Language) vocabulary or in [GoodRelations](https://www.heppnetz.de/projects/goodrelations/). When cardinality cannot be automatically extracted, its value is set to `unknown`. Usage: ```console vendor/bin/schema extract-cardinalities ``` --- # Configuration Source: https://api-platform.com/docs/schema-generator/configuration/ # Configuration The following options can be used in the configuration file. ## Customizing PHP Namespaces Namespaces of generated PHP classes can be set globally, respectively for entities, enumerations and interfaces (if used with [Doctrine Resolve Target Entity Listener option](#interfaces-and-doctrine-resolve-target-entity-listener)). Example: ```yaml namespaces: entity: "App\ECommerce\Entity" enum: "App\ECommerce\Enum" interface: "App\ECommerce\Model" ``` Namespaces can also be specified for a specific type. It will take precedence over any globally configured namespace. Example: ```yaml types: Thing: namespaces: class: "App\Common\Entity" # Namespace for the Thing entity (works for enumerations too) interface: "App\Schema\Model" # Namespace of the related interface ``` ## Forcing a Field Type (Range) RDF allows a property to have several types (ranges). However, the generator allows only one type per property. If not configured, it will use the first defined type. The `range` option is useful to set the type of a given property. It can also be used to force a type (even if not in the RDF vocabulary definition). Example: ```yaml types: Brand: properties: logo: { range: "ImageObject" } # Force the range of the logo property to ImageObject (can also be a URL according to Schema.org) PostalAddress: properties: addressCountry: { range: "Text" } # Force the type to Text instead of Country. It will be converted to the PHP string type. ``` ## Forcing a Field Cardinality The cardinality of a property is automatically guessed. The `cardinality` option allows to override the guessed value. Supported cardinalities are: - `(0..1)`: scalar, not required - `(0..*)`: array, not required - `(1..1)`: scalar, required - `(1..*)`: array, required - `(*..0)` - `(*..1)` - `(*..*)` Cardinalities are enforced by the class generator, the Doctrine ORM generator and the Symfony validation generator. Example: ```yaml types: Product: properties: sku: cardinality: "(0..1)" ``` ## Changing the Default Cardinality When a cardinality has not been guessed, a default cardinality will be used instead. By default, the cardinality `(1..1)` is used, but you can change it like this: ```yaml relations: defaultCardinality: "(1..*)" ``` ## Adding a Custom Attribute or Modifying a Generated Attribute You can add any custom attribute you want, or you can modify the arguments of any generated attribute, for a property, a class or even a whole vocabulary! For instance, if you want to change the join table name and add security for a specific relation: ```yaml types: Organization: properties: contactPoint: attributes: ORM\JoinTable: { name: organization_contactPoint } # Instead of organization_contact_point by default ApiProperty: { security: "is_granted('ROLE_ADMIN')" } ``` To add a custom attribute, you also need to add it in the `uses` option: ```yaml uses: App\Attributes\MyAttribute: ~ types: Book: attributes: - ApiResource: { routePrefix: "/library" } # Add a route prefix for this resource - MyAttribute: ~ # Note the optional usage of a hyphen list: it allows to preserve the order of attributes ``` ## Forcing (or Enabling) a Class Parent Override the guessed class hierarchy of a given type with this option. Example: ```yaml types: ImageObject: parent: Thing # Force the parent to be Thing instead of CreativeWork > MediaObject properties: ~ Drug: parent: ~ # Enable the class hierarchy for this type ``` ## Forcing a Class to be Abstract Force a class to be (or to not be) `abstract`. By default, it will be guessed, depending on the class hierarchy and if the class is used in a relation. Example: ```yaml types: Person: abstract: true ``` ## Define API Platform Operations API Platform operations can be added this way: ```yaml types: Person: operations: Get: ~ GetCollection: routeName: get_person_collection ``` ## Forcing a Nullable Property Force a property to be (or to not be) `nullable`. By default, this option is `null`: the cardinality will be used to determine the nullability. If no cardinality is found, it will be `true`. Example: ```yaml Person: properties: name: { nullable: false } ``` The `#[Assert\NotNull]` constraint is automatically added. ```php name = $name; return $this; } ``` Use the following config: ```yaml fluentMutatorMethods: true ``` ## Disabling the `id` Generator By default, the generator adds a property called `id` not provided by Schema.org. This is useful when generating an entity for use with an ORM or an ODM but not when generating DTOs. This behavior can be disabled with the following setting: ```yaml id: generate: false ``` ## Generating UUIDs It's also possible to let the DBMS generate [UUIDs](https://en.wikipedia.org/wiki/Universally_unique_identifier) instead of auto-incremented integers: ```yaml id: generationStrategy: uuid ``` ## User-submitted UUIDs To manually set a UUID instead of letting the DBMS generate it, use the following config: ```yaml id: generationStrategy: uuid writable: true ``` ## Generating Custom IDs With this configuration option, an `$id` property of type `string` and the corresponding getters and setters will be generated, but the DBMS will not generate anything. The ID must be set manually. ```yaml id: generationStrategy: none ``` ## Disabling Usage of Doctrine Collections By default, the generator uses classes provided by the [Doctrine Collections](https://github.com/doctrine/collections) library to store collections of entities. This is useful (and required) when using Doctrine ORM or Doctrine MongoDB ODM. This behavior can be disabled (to fall back to standard arrays) with the following setting: ```yaml doctrine: useCollection: false ``` ## Changing the Field Visibility Generated fields have a `private` visibility and are exposed through getters and setters. The default visibility can be changed with the `fieldVisibility` option. Example: ```yaml fieldVisibility: "protected" ``` ## Generating `Assert\Type` Attributes It's possible to automatically generate Symfony validator's `#[Assert\Type]` attributes using the following config: ```yaml validator: assertType: true ``` ## Forcing Doctrine Inheritance Mapping Attribute The generator is able to handle inheritance in a smart way: - If a class has children and is referenced by a relation, it will generate an inheritance mapping strategy with `#[InheritanceType]` (configurable, see below), `#[DiscriminatorColumn]` (`#[DiscriminatorField]` for ODM) and `#[DiscriminatorMap]`. The discriminator map will be filled with all possible values. - If a class has children but is not referenced by a relation, it will generate a mapped superclass (`#[MappedSuperclass]`). If this mapped superclass defines relations and is used by multiple children, the generator will add `#[AssociationOverride]` attributes to them (see the [related Doctrine documentation](https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/inheritance-mapping.html#association-override)), thanks to the special `DoctrineOrmAssociationOverrideAttributeGenerator`. - If a class has no child, an `#[Entity]` (or `#[Document]` for ODM) attribute is used. If this behaviour does not suit you, the inheritance attribute can be forced in the following way: ```yaml doctrine: inheritanceType: SINGLE_TABLE # Default: JOINED inheritanceAttributes: CustomInheritanceAttribute: [] ``` ## Interfaces and Doctrine Resolve Target Entity Listener [`ResolveTargetEntityListener`](https://www.doctrine-project.org/projects/doctrine-orm/en/current/cookbook/resolve-target-entity-listener.html) is a feature of Doctrine to keep modules independent. It allows to specify interfaces and `abstract` classes in relation mappings. If you set the option `useInterface` to true, the generator will generate an interface corresponding to each generated entity and will use them in relation mappings. To let the schema generator generate the mapping file usable with Symfony, add the following to your config file: ```yaml doctrine: resolveTargetEntityConfigPath: path/to/doctrine.xml ``` The default mapping file format is XML, but you can change it to YAML with the following option: ```yaml doctrine: resolveTargetEntityConfigPath: path/to/doctrine.yaml resolveTargetEntityConfigType: YAML # Supports XML & YAML ``` ### Doctrine Resolve Target Entity Config Type By default, the mapping file is in XML. If you want to have a YAML file, add the following: ```yaml doctrine: resolveTargetEntityConfigPath: path/to/doctrine.yaml resolveTargetEntityConfigType: yaml ``` ## Custom Schemas The generator can use your own schema definitions. They must be written in RDF/XML and follow the format of the [Schema.org's definition](https://schema.org/version/latest/schemaorg-current-https.rdf). This is useful to document your [Schema.org extensions](https://schema.org/docs/extension.html) and use them to generate the PHP data model of your application. Example: ```yaml vocabularies: - https://github.com/schemaorg/schemaorg/raw/main/data/releases/13.0/schemaorg-current-https.rdf - http://example.com/data/myschema.rdf # Additional types ``` You can also use any other vocabulary. Check the [Linked Open Vocabularies](https://lov.linkeddata.es/dataset/lov/) to find one fitting your needs. For instance, to generate a data model from the [Video Game Ontology](http://purl.org/net/VideoGameOntology), use the following config file: ```yaml vocabularies: - http://vocab.linkeddata.es/vgo/GameOntologyv3.owl # The URL of the vocabulary definition types: Session: vocabularyNamespace: http://purl.org/net/VideoGameOntology# # ... ``` ## All Types, Resolve Types and Exclude If you use multiple vocabularies, and you need to generate all types for some ones, only generate types when they are used for some others and exclude some types, you can do so with this kind of configuration: ```yaml vocabularies: # Schema.org classes will only be generated when one of its type is used in the other vocabularies. - { uri: "https://schema.org/version/latest/schemaorg-current-https.rdf", format: null, allTypes: false, } - http://vocab.linkeddata.es/vgo/GameOntologyv3.owl allTypes: true # Generate all types by default for vocabularies resolveTypes: true # Resolve types in other vocabularies types: GameEvent: exclude: true # Exclude the GameEvent type ``` ## Checking GoodRelation Compatibility If the `checkIsGoodRelations` option is set to `true`, the generator will emit a warning if an encountered property is not par of the [GoodRelations](https://www.heppnetz.de/projects/goodrelations/) schema. This is useful when generating e-commerce data models. ## Author PHPDoc Add a `@author` PHPDoc annotation to class DocBlock. Example: ```yaml author: "Kévin Dunglas " ``` ## PHP File Header Prepend all generated PHP files with a custom comment. Example: ```yaml header: | /* * This file is part of the Ecommerce package. * * (c) Kévin Dunglas * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ ``` ## Disabling Generators and Creating Custom Ones By default, all generators except `DoctrineMongoDBAttributeGenerator` are enabled. You can specify the list of generators to use with the `annotationGenerators` and `attributeGenerators` option. Example (enabling only the PHPDoc generator): ```yaml annotationGenerators: - ApiPlatform\SchemaGenerator\AnnotationGenerator\PhpDocAnnotationGenerator attributeGenerators: [] ``` You can write your own generators by implementing the `AnnotationGeneratorInterface` or `AttributeGeneratorInterface`. The `AbstractAnnotationGenerator` or `AbstractAttributeGenerator` provides helper methods useful when creating your own generators. Enabling a custom attribute generator and the PHPDoc generator: ```yaml annotationGenerators: - ApiPlatform\SchemaGenerator\AnnotationGenerator\PhpDocAnnotationGenerator attributeGenerators - Acme\Generators\MyGenerator ``` ## Full Configuration Reference ```yaml openApi: file: null # RDF vocabularies vocabularies: # Prototype uri: # RDF vocabulary to use uri: ~ # Example: 'https://schema.org/version/latest/schemaorg-current-https.rdf' # RDF vocabulary format format: null # Example: rdfxml # Generate all types for this vocabulary, even if an explicit configuration exists. If allTypes is enabled globally, it can be disabled for this particular vocabulary allTypes: null # Attributes (merged with generated attributes) attributes: [] # Namespace of the vocabulary to import vocabularyNamespace: "https://schema.org/" # Example: 'http://www.w3.org/ns/activitystreams#' # Relations configuration relations: # OWL relation URIs containing cardinality information in the GoodRelations format uris: # Example: 'https://archive.org/services/purl/goodrelations/v1.owl' # Default: - https://archive.org/services/purl/goodrelations/v1.owl # The default cardinality to use when it cannot be extracted defaultCardinality: (1..1) # One of "(0..1)"; "(0..*)"; "(1..1)"; "(1..*)"; "(*..0)"; "(*..1)"; "(*..*)" # Debug mode debug: false # Use old API Platform attributes (API Platform < 2.7) apiPlatformOldAttributes: false # IDs configuration id: # Automatically add an ID field to entities generate: true # The ID generation strategy to use ("none" to not let the database generate IDs). generationStrategy: auto # One of "auto"; "none"; "uuid"; "mongoid" # Is the ID writable? Only applicable if "generationStrategy" is "uuid". writable: false # Generate interfaces and use Doctrine's Resolve Target Entity feature useInterface: false # Emit a warning if a property is not derived from GoodRelations checkIsGoodRelations: false # A license or any text to use as header of generated files header: null # Example: '// (c) Kévin Dunglas ' # PHP namespaces namespaces: # The global namespace's prefix prefix: null # Example: App\ # The namespace of the generated entities entity: App\Entity # Example: App\Entity # The namespace of the generated enumerations enum: App\Enum # Example: App\Enum # The namespace of the generated interfaces interface: App\Model # Example: App\Model # Custom uses (for instance if you use a custom attribute) uses: # Prototype name: # Name of this use name: ~ # Example: App\Attributes\MyAttribute # The alias to use for this use alias: null # Doctrine doctrine: # Use Doctrine's ArrayCollection instead of standard arrays useCollection: true # The Resolve Target Entity Listener config file path resolveTargetEntityConfigPath: null # The Resolve Target Entity Listener config file type resolveTargetEntityConfigType: XML # One of "XML"; "yaml" # Doctrine inheritance attributes (if set, no other attributes are generated) inheritanceAttributes: [] # The inheritance type to use when an entity is referenced by another and has child inheritanceType: JOINED # One of "JOINED"; "SINGLE_TABLE"; "SINGLE_COLLECTION"; "TABLE_PER_CLASS"; "COLLECTION_PER_CLASS"; "NONE" # Maximum length of any given database identifier, like tables or column names maxIdentifierLength: 63 # Symfony Validator Component validator: # Generate @Assert\Type annotation assertType: false # The value of the phpDoc's @author annotation author: false # Example: 'Kévin Dunglas ' # Visibility of entities fields fieldVisibility: private # One of "private"; "protected"; "public" # Set this flag to false to not generate getter, setter, adder and remover methods accessorMethods: true # Set this flag to true to generate fluent setter, adder and remover methods fluentMutatorMethods: false rangeMapping: # Prototype name: ~ # Generate all types, even if an explicit configuration exists allTypes: false # If a type is present in a vocabulary but not explicitly imported (types) or if the vocabulary is not totally imported (allTypes), it will be generated resolveTypes: false # Types to import from the vocabulary types: # Prototype id: # Exclude this type, even if "allTypes" is set to true" exclude: false # Namespace of the vocabulary of this type (defaults to the global "vocabularyNamespace" entry) vocabularyNamespace: null # Example: 'http://www.w3.org/ns/activitystreams#' # Is the class abstract? (null to guess) abstract: null # Is the class embeddable? embeddable: false # Type namespaces namespaces: # The namespace for the generated class (override any other defined namespace) class: null # The namespace for the generated interface (override any other defined namespace) interface: null # Attributes (merged with generated attributes) attributes: [] # The parent class, set to false for a top level class parent: false # If declaring a custom class, this will be the class from which properties type will be guessed guessFrom: Thing # Operations for the class operations: [] # Import all existing properties allProperties: false # Properties of this type to use properties: # Prototype id: # Exclude this property, even if "allProperties" is set to true" exclude: false # The property range range: null # Example: Offer cardinality: unknown # One of "(0..1)"; "(0..*)"; "(1..1)"; "(1..*)"; "(*..0)"; "(*..1)"; "(*..*)"; "unknown" # Symfony Serialization Groups groups: [] # The doctrine mapped by attribute mappedBy: null # Example: partOfSeason # The doctrine inversed by attribute inversedBy: null # Example: episodes # Is the property readable? readable: true # Is the property writable? writable: true # Is the property nullable? (if null, cardinality will be used: will be true if no cardinality found) nullable: null # Is the property required? required: true # The property unique unique: false # Is the property embedded? embedded: false # Attributes (merged with generated attributes) attributes: [] # Annotation generators to use annotationGenerators: # Default: - ApiPlatform\SchemaGenerator\AnnotationGenerator\PhpDocAnnotationGenerator # Attribute generators to use attributeGenerators: # Defaults: - ApiPlatform\SchemaGenerator\AttributeGenerator\DoctrineOrmAttributeGenerator - ApiPlatform\SchemaGenerator\AttributeGenerator\DoctrineOrmAssociationOverrideAttributeGenerator - ApiPlatform\SchemaGenerator\AttributeGenerator\ApiPlatformCoreAttributeGenerator - ApiPlatform\SchemaGenerator\AttributeGenerator\ConstraintAttributeGenerator - ApiPlatform\SchemaGenerator\AttributeGenerator\ConfigurationAttributeGenerator # Directories for custom generator twig templates generatorTemplates: [] ``` --- # The API Platform Admin Source: https://api-platform.com/docs/admin/ # The API Platform Admin API Platform **Admin** is a tool to automatically create a beautiful (Material Design) and fully-featured administration interface for any API implementing specification formats supported by [`@api-platform/api-doc-parser`](https://github.com/api-platform/api-doc-parser). In particular, that includes: - APIs using [the Hydra Core Vocabulary](https://www.hydra-cg.com/) - APIs exposing an [OpenAPI documentation](https://www.openapis.org/) Of course, API Platform Admin is the perfect companion of APIs created using [the API Platform framework](https://api-platform.com). But it also supports APIs written with any other programming language or framework as long as they expose a standard Hydra or OpenAPI documentation. ## Based On React Admin API Platform Admin is a Single Page Application (SPA), based on [React Admin](https://marmelab.com/react-admin/), a powerful frontend framework for building B2B applications on top of REST/GraphQL APIs, written in TypeScript and React. Thanks to its built-in **guessers**, API Platform Admin parses the API documentation then uses React Admin to expose a nice, responsive management interface (Create-Retrieve-Update-Delete, i.e. CRUD) for all documented resource types. Afterwards, you can **customize everything** by using the numerous components provided by [React Admin](https://marmelab.com/react-admin/documentation.html) and [MUI](https://mui.com/), or even writing your own [React](https://reactjs.org/) components.

React Admin Screencast
Watch the React Admin screencast

## Features Simply by reading your API documentation, API Platform Admin provides the following features: - Generate 'list', 'create', 'show', and 'edit' views for all resources - Automatically detect the type for inputs and fields - Client-side [validation](./validation.md) on required inputs - Pagination - Filtering and ordering - Easily view and edit [related records](./handling-relations.md) - Display the related resource’s name instead of its IRI ([using the Schema.org vocabulary](./schema.md#displaying-related-resources-name-instead-of-its-iri)) - Nicely displays server-side errors (e.g. advanced validation) - Real-time updates with [Mercure](https://mercure.rocks) By [leveraging React Admin components](./advanced-customization.md), you can further customize the generated interface and get access to many more features: - Powerful Datagrid components - Search and filtering - Advanced form validation - Undoable mutations - Authentication - Access Control - Internationalization - [And many more](https://marmelab.com/react-admin/Features.html) ## Next Step Get your Admin up and running by following the [Getting Started guide](./getting-started.md). --- # Getting Started Source: https://api-platform.com/docs/admin/getting-started/ # Getting Started ## API Platform Symfony variant If you use the [API Platform Symfony variant](../symfony/), good news, API Platform Admin is already installed! 🎉 You can access it by visiting `/admin` on your API Platform application. When running locally, you can also click on the "Admin" button of the welcome page at [https://localhost](https://localhost). ![API Platform welcome page](./images/api-platform-welcome-page.png) Here is what it looks like with a simple API exposing a `Greetings` resource: ![Basic admin with the Greetings resource](./images/basic-admin-greetings.png) ## Manual Installation If you did not use the Symfony variant of API Platform and need to install API Platform Admin manually, follow this guide. First, let's scaffold a React Admin Application by using the [Create React Admin](https://marmelab.com/react-admin/CreateReactAdmin.html) tool: ```bash npx create-react-admin@latest my-admin cd my-admin ``` Then, install the `@api-platform/admin` library: ```bash npm install @api-platform/admin ``` Now you can use either: - [``](./getting-started.md#using-hydraadmin) to connect your app to an API exposing a Hydra documentation - [``](./getting-started.md#using-openapiadmin) to connect your app to an API exposing an OpenAPI documentation ## Using `HydraAdmin` You can use the [``](./components.md#hydraadmin) component exported by `@api-platform/admin` to connect your app to an API exposing a Hydra documentation. If you used Create React Admin, you can replace the content of `src/App.tsx` by: ```tsx import { HydraAdmin } from "@api-platform/admin"; // Replace with your own API entrypoint // For instance if https://example.com/api/books is the path to the collection of book resources, then the entrypoint is https://example.com/api export const App = () => ; ``` **Tip:** if you don't want to hardcode the API URL, you can [use an environment variable](https://vite.dev/guide/env-and-mode). Your new administration interface is ready! `HydraAdmin` will automatically fetch the Hydra documentation of your API and generate CRUD pages for all the resources it exposes. Type `npm run dev` to try it! ![Basic admin with the Greetings resource](./images/basic-admin-greetings.png) **Tip:** There are more props you can pass to the `HydraAdmin` component to customize the dataProvider or the connection to Mercure. Check the [API documentation](./components.md#hydraadmin) for more information. **Tip:** You may also need to configure your API to set the correct CORS headers. Refer to the [Configuring CORS](./getting-started.md#configuring-cors) section below to learn more. ## Using `OpenApiAdmin` You can use the [``](./components.md#openapiadmin) component exported by `@api-platform/admin` to connect your app to an API exposing an OpenAPI documentation. If you used Create React Admin, you can replace the content of `src/App.tsx` by: ```tsx import { OpenApiAdmin } from "@api-platform/admin"; // Replace with your own API entrypoint export const App = () => ( ); ``` **Tip:** If you don't want to hardcode the API URL, you can use an environment variable (see [Vite.js](https://vite.dev/guide/env-and-mode) or [Next.js](https://nextjs.org/docs/pages/building-your-application/configuring/environment-variables) docs). Your new administration interface is ready! `OpenApiAdmin` will automatically fetch the Hydra documentation of your API and generate CRUD pages for all the resources it exposes. Type `npm run dev` to try it! ![Basic admin with the Greetings resource](./images/basic-admin-greetings.png) **Tip:** There are more props you can pass to the `OpenApiAdmin` component to customize the dataProvider or the connection to Mercure. Check the [API documentation](./components.md#openapiadmin) for more information. **Tip:** You may also need to configure your API to set the correct CORS headers. Refer to the [Configuring CORS](./getting-started.md#configuring-cors) section below to learn more. ## Configuring CORS Be sure to make your API send proper [CORS HTTP headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) to allow the admin's domain to access it. To do so, if you use the API Platform Symfony variant, update the value of the `CORS_ALLOW_ORIGIN` parameter in `api/.env` (it will be set to `^https?://localhost:?[0-9]*$` by default). If you use a custom installation of Symfony and [API Platform Core](../core/), you will need to adjust the [NelmioCorsBundle configuration](https://github.com/nelmio/NelmioCorsBundle#configuration) to expose the `Link` HTTP header and to send proper CORS headers on the route under which the API will be served (`/api` by default). Here is a sample configuration: ```yaml # config/packages/nelmio_cors.yaml nelmio_cors: paths: "^/api/": origin_regex: true allow_origin: ["^http://localhost:[0-9]+"] # You probably want to change this regex to match your real domain allow_methods: ["GET", "OPTIONS", "POST", "PUT", "PATCH", "DELETE"] allow_headers: ["Content-Type", "Authorization"] expose_headers: ["Link"] max_age: 3600 ``` Clear the cache to apply this change: ```console bin/console cache:clear --env=prod ``` ## Next Step Learn how to add more features to your generated Admin by [Customizing the Schema](./schema.md). --- # Customizing the Schema Source: https://api-platform.com/docs/admin/schema/ # Customizing the Schema Both [`HydraAdmin`](./components.md#hydraadmin) and [`OpenApiAdmin`](./components.md#openapiadmin) leverage introspection of the API schema to discover its capabilities, like **filtering** and **sorting**. They also detect wether the API has real-time capabilities using [Mercure](./real-time-mercure.md), and automatically enable it if it does. Lastly, API Platform Admin has native support for the popular [Schema.org](./schema.md#about-schemaorg) vocabulary, which enables it to automatically use the field type matching your data, or display a related resource's name instead of its IRI. ## Adding Filtering Capabilities You can add the [`ApiFilter` attribute](../core/filters.md#apifilter-attribute) to an API Platform resource to configure a filter on a property. For instance, here is how configure filtering on the `id`, `title` and `author` properties of a `Book` resource: ```php 'exact', 'title' => 'ipartial', 'author' => 'ipartial' ])] class Book { // ... } ``` If you are using the guessers, the Admin will automatically update the Book list view to include a filter on the selected properties. ![Filtering on the title property](./images/admin-filter.png) **Tip:** Learn more about the [`ApiFilter` attribute](../core/filters.md#apifilter-attribute) in the core documentation. ## Adding Sorting Capabilities You can also use the [`ApiFilter` attribute](../core/filters.md#apifilter-attribute) on an API Plaform resource to configure sorting. For instance, here is how to configure sorting on the `id`, `isbn`, `title`, `author` and `publicationDate` properties of a `Book` resource: ```php 'ASC', 'isbn' => 'ASC', 'title' => 'ASC', 'author' => 'ASC', 'publicationDate' => 'DESC' ])] class Book { // ... } ``` If you are using the guessers, the Admin will automatically update the Book list view to make the selected columns sortable. ![Sorting by the title property](./images/admin-sort.png) **Tip:** Learn more about the [`ApiFilter` attribute](../core/filters.md#apifilter-attribute) in the core documentation. ## Enabling Real-Time Updates You can use the `mercure` attribute to hint API Platform that it must dispatch the updates regarding the given resources to the Mercure hub: ```php Schema.org is a collaborative, community activity with a mission to create, maintain, and promote > schemas for structured data on the Internet, on web pages, in email messages, and beyond. To leverage this capability, your API must use the JSON-LD format and the appropriate Schema.org types. The following examples will use [API Platform Core](../core/) to create such API, but keep in mind that this feature will work with any JSON-LD API using the Schema.org vocabulary, regardless of the used web framework or programming language. ## Displaying Related Resource's Name Instead of its IRI By default, IRIs of related objects are displayed in lists and forms. However, it is often more user-friendly to display a string representation of the resource (such as its name) instead of its ID. To configure which property should be shown to represent your entity, map the property containing the name of the object with the `https://schema.org/name` type: ```php // api/src/Entity/Person.php ... #[ApiProperty(iris: ["https://schema.org/name"])] private $name; ... ``` | With IRI | With Resource Name | | ---------------------------------------------------------------- | ---------------------------------------------------------------------------- | | ![Related Record With IRI](./images/related-record-with-iri.png) | ![Related Record With Resource Name](./images/related-record-with-name.png) | ## Emails, URLs and Identifiers Besides, it is also possible to use the documentation to customize some fields automatically while configuring the semantics of your data. The following Schema.org types are currently supported by API Platform Admin: - `https://schema.org/email`: the field will be rendered using the [``](https://marmelab.com/react-admin/EmailField.html) React Admin component - `https://schema.org/url`: the field will be rendered using the [``](https://marmelab.com/react-admin/UrlField.html) React Admin component - `https://schema.org/identifier`: the field will be formatted properly in inputs Note: if you already use validation on your properties, the semantics are already configured correctly (see [the correspondence table](../core/validation.md#open-vocabulary-generated-from-validation-metadata))! ## Next Step Learn how to tweak the generated Admin by [Customizing the Guessers](./customizing.md). --- # Customizing the Guessers Source: https://api-platform.com/docs/admin/customizing/ # Customizing the Guessers Using `` or `` directly is a great way to quickly get started with API Platform Admin. They will introspect your API schema (using `@api-platform/api-doc-parser`) and automatically generate CRUD pages for all the resources it exposes. They will even [configure filtering, sorting, and real-time updates with Mercure](./schema.md) if your API supports it. For some this may be enough, but you will often find yourself wanting to customize the generated pages further. For instance, you may want to: - Hide or reorder resources in the menu - Hide or reorder columns in the list view - Hide or reorder fields in the show, create and edit views - Customize the generated list, e.g. add a default sort order - Customize the generated create and edit views, e.g. to add a warning when there are unsaved changes - Customize the generated inputs, e.g. set a custom label or make a text input multiline Such changes can't be achieved by modifying the Schema, they require customizing the React components generated by API Platform Admin. Fortunately, API Platform Admin has you covered! ## From `` To `` If you are using `` or `` directly, there is a simple way to start customizing the generated pages. Simply open your browser's developer tools and look at the console. You will see messages like this: ```txt If you want to override at least one resource, paste this content in the component of your app: ``` This message tells you which resources are exposed by your API and how to customize the generated pages for each of them. Let's say we'd like to hide the `greetings` resource from the menu. We can do this by replacing the `` component (`` in our case) children with a list of ``: ```diff -import { HydraAdmin } from "@api-platform/admin"; +import { HydraAdmin, ResourceGuesser } from "@api-platform/admin"; const App = () => ( - + + + + ); ``` Now the `greetings` resource will no longer be displayed in the menu. ![Customized Admin menu](./images/admin-menu.png) `` also accepts all props react-admin's [``](https://marmelab.com/react-admin/Resource.html) component accepts. This means that, for instance, you can use the `list` prop to use your own list component, but keep using the create, edit and show components introspected by ``: ```diff import { HydraAdmin, ResourceGuesser } from "@api-platform/admin"; +import { BookList } from "./BookList"; const App = () => ( - + ); ``` Likewise, you can use the `icon` prop to customize the icon displayed in the menu: ```diff +import AutoStoriesIcon from '@mui/icons-material/AutoStories'; +import ReviewsIcon from '@mui/icons-material/Reviews'; import { HydraAdmin, ResourceGuesser } from "@api-platform/admin"; const App = () => ( - + - + ); ``` Here is the result: ![Admin menu with custom icons](./images/admin-menu-icons.png) ## Customizing the `` By default, `` will render a `` component as the list view for a resource. This component will automatically introspect the API schema and generate a list view with all the fields of the resource. ![Admin default generated list view](./images/admin-default-list.png) This is already usable, but may not provide the best user experience yet. To start customizing the list view, you can look at the DevTools console. You will see messages like this: ```txt If you want to override at least one field, create a BookList component with this content: import { ListGuesser, FieldGuesser } from "@api-platform/admin"; export const BookList = () => ( ); Then, update your main admin component: import { HydraAdmin, ResourceGuesser } from "@api-platform/admin"; import { BookList } from './BookList'; const App = () => ( {/* ... */} ); ``` If you follow these instructions, you will end up with the same view as before, but now you can start customizing it. For instance, we'll hide the 'Description' column as it takes too much space (we'll reserve that to the show view). And we will also add a default sort order to show the most recent books first. Here's how to achieve this: ```diff export const BookList = () => ( - + - ); ``` And here is the result: ![Admin with customized list guesser](./images/admin-custom-list-guesser.png) That's already better isn't it? 🙂 ## Customizing the `` Removing or reordering `` components is not the only thing we can do. We can also customize them. Indeed, `` will forward additional props to the underlying React Admin [Field component](https://marmelab.com/react-admin/Fields.html). This means we can use any [common field prop](https://marmelab.com/react-admin/Fields.html#common-field-props) on them. For instance, let's add a `label` prop to customize the label of the ISBN column to be all uppercase: ```diff export const BookList = () => ( - + ); ``` And here is the result: ![Admin with customized list guesser and field guesser](./images/admin-custom-list-field-guesser.png) ## Customizing the `` Following the same principles as the `` (including looking at the DevTools console) we can customize the show view. In the following example, the show view for the `books` resource was customized to make the label of the `isbn` field uppercase: ```tsx import { HydraAdmin, ResourceGuesser, ShowGuesser, FieldGuesser } from "@api-platform/admin"; const BookShow = () => ( ); export default () => ( ); ``` Here is the result: ![Admin with customized show guesser](./images/admin-custom-show-guesser.png) ## From `` To React Admin Fields As mentioned in the [Customizing the ``](./customizing.md#customizing-the-fieldguesser) section, we can use any [common field prop](https://marmelab.com/react-admin/Fields.html#common-field-props) from React Admin to customize the `` elements. However in some cases you may want to go further and use a React Admin [field components](https://marmelab.com/react-admin/Fields.html), such as [``](https://marmelab.com/react-admin/TextField.html), [``](https://marmelab.com/react-admin/DateField.html) or [``](https://marmelab.com/react-admin/ReferenceField.html) directly, to access more advanced features. For instance, you can replace a `` with a [``](https://marmelab.com/react-admin/DateField.html) to control more precisely how the publication date is displayed, leveraging the [`showTime`](https://marmelab.com/react-admin/DateField.html#showtime) prop: ```diff import { ShowGuesser, FieldGuesser } from '@api-platform/admin'; +import { DateField } from 'react-admin'; const ReviewShow = () => ( - + ); ``` ## Customizing the `` and `` Customizing the `` and `` is very similar to customizing the ``. We can start by looking at the DevTools console to get the initial code of the components. ```txt If you want to override at least one input, create a ReviewEdit component with this content: import { EditGuesser, InputGuesser } from "@api-platform/admin"; export const ReviewEdit = () => ( ); Then, update your main admin component: import { HydraAdmin, ResourceGuesser } from "@api-platform/admin"; import { ReviewEdit } from './ReviewEdit'; const App = () => ( {/* ... */} ); ``` Let's customize this `ReviewEdit` component to: - reorder the inputs - make the `body` input multiline - mark the `publicationDate` input as read-only ```diff export const ReviewEdit = () => ( - - - - - + + + + + ); ``` Here is the result: ![Admin with customized edit guesser](./images/admin-custom-edit-guesser.png) **Tip:** Here, we leveraged the `multiline` and `readOnly` props of the `` component. But you can use any [common input prop](https://marmelab.com/react-admin/Inputs.html#common-input-props) supported by React Admin [Inputs](https://marmelab.com/react-admin/Inputs.html) on them. ## From `` To React Admin Inputs As mentioned in the previous section, we can use any [common input prop](https://marmelab.com/react-admin/Inputs.html#common-input-props) from React Admin to customize the `` elements. However in some cases you may want to go further and use a React Admin [input components](https://marmelab.com/react-admin/Inputs.html), such as [``](https://marmelab.com/react-admin/TextInput.html), [``](https://marmelab.com/react-admin/DateInput.html) or [``](https://marmelab.com/react-admin/ReferenceInput.html) directly, to access more advanced features. A good example is to use an [Autocomplete Input to edit a relation](./handling-relations.md#using-an-autocomplete-input-for-relations). This leverages both [``](https://marmelab.com/react-admin/ReferenceInput.html) and [``](https://marmelab.com/react-admin/AutocompleteInput.html) to offer a better user experience when editing the relation: ```diff import { EditGuesser, InputGuesser } from '@api-platform/admin'; +import { ReferenceInput, AutocompleteInput } from 'react-admin'; const ReviewsEdit = () => ( - + + ({ title: searchText })} + optionText="title" + /> + ); ``` ![Admin With AutocompleteInput](./images/AutocompleteInput.png) > [!WARNING] When replacing `` with a React Admin input component, the validation > rules are not automatically applied. You will need to manually add them back. Fortunately, this is > very easy to do. Read the > [Validation With React Admin Inputs](./validation.md#validation-with-react-admin-inputs) section > to learn more. ## Next Step The above examples are limited to customizing the various API Platform Admin Guessers, but this is just the tip of the iceberg. By leveraging React Admin components and props, you can go much further in customizing the generated pages. Head to the next section, [Customizing the Admin](./advanced-customization.md), for step-by-step examples. --- # Customizing the Admin Source: https://api-platform.com/docs/admin/advanced-customization/ # Customizing the Admin In the previous sections, we have seen how to customize the generated Admin by [updating the schema](./schema.md), and by [customizing the guesser components](./customizing.md). But we can go much further in customizing the generated pages by leveraging React Admin components and props. In the following sections, we will for instance learn how to: - Change the default theme and layout - Display the number of related records instead of listing them - Navigate from the list using simple row click - Make mutations undoable - Improve the layout of a form - Switch to a tabbed layout - Create a custom field component - Add icons to the menu Let's dive in! ## Changing the Default Theme and Layout API Platform comes with its own [layout](https://marmelab.com/react-admin/Admin.html#layout) and [themes](https://marmelab.com/react-admin/Admin.html#theme) by default. ![Admin with default API Platform theme and layout](./images/api-platform-admin-theme.png) However you may not find them to your liking, or you may want to remove the API Platform logo from the top bar. To change the top bar logo, you will need to opt out of API Platform's default Layout component, and provide your own. You can for instance use the default [Layout](https://marmelab.com/react-admin/Layout.html) provided by `react-admin`. ```diff import { HydraAdmin, ResourceGuesser } from "@api-platform/admin"; +import { Layout } from 'react-admin'; export const App = () => ( - + ); ``` To customize the light and dark themes, you will need to use the [`theme`](https://marmelab.com/react-admin/Admin.html#theme) and [`darkTheme`](https://marmelab.com/react-admin/Admin.html#darktheme) props of the `` component. Here too, we can use the default themes provided by `react-admin`. ```diff import { HydraAdmin, ResourceGuesser } from "@api-platform/admin"; -import { Layout } from 'react-admin'; +import { Layout, defaultDarkTheme, defaultLightTheme } from 'react-admin'; export const App = () => ( - + ); ``` Here is the result: ![Admin with default react-admin theme and layout](./images/react-admin-theme.png) ## Displaying the Number of Related Records When dealing with related records, the default behavior of the guessers is to display the list of related records. However if there are many related records, it can be more suitable to display the number of related records instead. Reusing our example with `books` and `reviews`, here is how you can display the number of reviews for each book in the book list: ```diff import { ListGuesser, FieldGuesser } from '@api-platform/admin'; import { NumberField } from 'react-admin'; const BookList = () => ( - + ); ``` ![Admin with number of related records](./images/admin-reference-record-count.png) **Tip:** It is recommended to also set a custom `label` to the column, as the label is otherwise humanized from the `source` prop, which is no longer suitable with a source like `reviews.length`. ## Hiding the Show And Edit Buttons in the List View By default, the list guesser displays a `Show` and `Edit` button for each row. However the UX can often be improved by setting a default action when clicking on a row, and removing the `Show` and `Edit` buttons. To hide these buttons, we will need to replace the `` by a [``](https://marmelab.com/react-admin/List.html) component, provided by `react-admin`. Then, to get the same layout as before, we will choose to render the list items using a [``](https://marmelab.com/react-admin/Datagrid.html) component. `` will automatically set the row click action to redirect to the show view if there is one, or to the edit view otherwise. ```diff -import { ListGuesser, FieldGuesser } from '@api-platform/admin'; +import { FieldGuesser } from '@api-platform/admin'; -import { NumberField } from 'react-admin'; +import { List, Datagrid, NumberField } from 'react-admin'; const BookList = () => ( - + + + + - ); ``` The UI is now more concise: ![Admin with hidden show and edit buttons](./images/admin-datagrid.png) If you want, you can use the [`rowClick`](https://marmelab.com/react-admin/Datagrid.html#rowclick) prop to customize the row click action, for instance to redirect to the book edit view instead: ```diff const BookList = () => ( - + ); ``` **Tip:** Check out the [`` documentation](https://marmelab.com/react-admin/Datagrid.html) for more customization options. ## Enabling Undoable Mutations React Admin offers the possibility to make mutations (e.g. updating or deleting a record) undoable. When this feature is enabled, a notification will be displayed at the bottom of the page, allowing the user to undo the mutation for a certain amount of time. If the user clicks on the UNDO button, the record will be restored to its previous state. Otherwise, the change is persisted to the API. Let's, for instance, add the possibility to undo an update to a book. To do that, we will leverage the [`mutationMode`](https://marmelab.com/react-admin/Edit.html#mutationmode) prop provided by React Admin, and set its value to `"undoable"`. This is possible because the `` component is a wrapper around the [``](https://marmelab.com/react-admin/Edit.html) component provided by React Admin, and it will forward the `mutationMode` prop to it. ```diff import { EditGuesser, InputGuesser } from "@api-platform/admin"; export const BookEdit = () => ( - + ); ``` That's enough to display an undoable notification when updating a book: ![Admin with undoable mutations](./images/admin-undoable-mutation.png) **Tip:** The default `mutationMode` set by `` is `"pessimistic"`, however the default `mutationMode` set by React Admin's `` component is `"undoable"`. ## Warning the User When There Are Unsaved Changes Another feature offered by React Admin is the possibility to warn the user when there are unsaved changes in a form. When the user tries to navigate away from a form with unsaved changes, a confirmation dialog will be displayed, asking the user if they want to leave the page. This prevents the risk of losing unsaved data. To enable this feature, all we need to do is to leverage the [`warnWhenUnsavedChanges`](https://marmelab.com/react-admin/SimpleForm.html#warnwhenunsavedchanges) prop provided by React Admin. This is possible because the `` component is also a wrapper around the [``](https://marmelab.com/react-admin/SimpleForm.html) component provided by React Admin, and it will forward the `warnWhenUnsavedChanges` prop to it. ```diff import { EditGuesser, InputGuesser } from "@api-platform/admin"; export const BookEdit = () => ( - + ); ``` Now, if the user tries to navigate away from the form with unsaved changes, they will be warned: ![Admin with unsaved changes warning](./images/admin-warnWhenUnsavedChanges.png) ## Customizing the Form Layout As we saw earlier, `` actually renders two (nested) React Admin components: [``](https://marmelab.com/react-admin/Edit.html) and [``](https://marmelab.com/react-admin/SimpleForm.html). You can pass additional props to `` which will be forwarded to `` or `` accordingly. However there are cases where this won't be enough. For instance, if we want to customize the form layout, we will need to specifically target the form component to pass styling props (such as `sx`), or to replace the component altogether (e.g. to use a [``](https://marmelab.com/react-admin/TabbedForm.html) instead). So, for our example, let's first replace the `` by an `` and a ``. ```diff -import { EditGuesser, InputGuesser } from "@api-platform/admin"; +import { InputGuesser } from "@api-platform/admin"; +import { Edit, SimpleForm } from "react-admin"; export const BookEdit = () => ( - + + + + - ); ``` **Tip:** This will also enable [undoable mutation mode](./advanced-customization.md#enabling-undoable-mutations). Indeed, the default `mutationMode` set by `` is `"pessimistic"`, however the default `mutationMode` set by React Admin's `` component is `"undoable"`. You can set the `mutationMode` prop back to `"pessimistic"` if you want to keep the same behavior as before. By default, `` organizes the inputs in a very simple layout, simply stacking them vertically. Under the hood, it uses Material UI's [``](https://mui.com/material-ui/react-stack/) component. This means we can use with `` any prop that `` accepts, and customize the style of the component using [the `sx` prop](https://marmelab.com/react-admin/SX.html). For instance, let's limit the width of the inputs to 500px: ```diff export const BookEdit = () => ( - + ); ``` We can also use `` directly in the `` to customize the layout further: ```tsx import { InputGuesser } from "@api-platform/admin"; import { Edit, SimpleForm } from "react-admin"; import { Stack } from "@mui/material"; export const BookEdit = () => ( ); ``` With these simple changes we already get a more appealing form layout: ![Admin with customized form layout](./images/admin-form-layout.png) **Tip:** Feel free to look at the [``](https://marmelab.com/react-admin/Edit.html) and [``](https://marmelab.com/react-admin/SimpleForm.html) documentation pages to learn more about the customization options they offer. **Tip:** `` is not the only form layout provided by React Admin. You can also use another layout such as [``](https://marmelab.com/react-admin/TabbedForm.html), [``](https://marmelab.com/react-admin/LongForm.html), [``](https://marmelab.com/react-admin/AccordionForm.html), [``](https://marmelab.com/react-admin/WizardForm.html) or even [create your own](https://marmelab.com/react-admin/Form.html). ## Rendering Related Records in a Dedicated Tab Speaking of tabbed layout, a common pattern is to display related records in a dedicated tab of the show view of the main record. For instance, let's leverage the [``](https://marmelab.com/react-admin/TabbedShowLayout.html) component provided by React Admin to display the reviews of a book in a dedicated tab. We will also leverage `` to fetch the related reviews of a book, and `` to display them in a list. ```tsx import { Show, TabbedShowLayout, TextField, DateField, ReferenceArrayField, SimpleList, } from "react-admin"; const BookShow = () => ( review.author .split(" ") .map((name: string) => name[0]) .join("") } /> ); ``` Here is the result: ![Admin with tabbed show layout](./images/admin-tabbed-show-layout.png) **Tip:** Feel free to look at the [``](https://marmelab.com/react-admin/TabbedShowLayout.html), [``](https://marmelab.com/react-admin/ReferenceArrayField.html) and [``](https://marmelab.com/react-admin/SimpleList.html) documentation pages to learn more about the customization options they offer. ## Creating A Custom Field Component React Admin already provides numerous off-the-shelf [field](https://marmelab.com/react-admin/Fields.html) and [input](https://marmelab.com/react-admin/Inputs.html) components. However, you may still need to create your own custom field component to display a specific type of data, or to add a specific behavior. Fortunately, React Admin makes it easy to create custom [field](https://marmelab.com/react-admin/Fields.html#writing-your-own-field-component) or [input](https://marmelab.com/react-admin/Inputs.html#writing-your-own-input-component) components, thanks to the many building blocks it provides. Let's take a look at a concrete example. Let's say we want to create a custom field component to display a rating as a series of stars. We will leverage Material UI's [``](https://mui.com/material-ui/react-rating/) component for the rendering. Since the component is fairly simple, we won't create a dedicated React component, but will instead leverage [``](https://marmelab.com/react-admin/WithRecord.html), a React Admin component allowing to build a custom field on-the-fly. ```tsx import { ShowGuesser } from "@api-platform/admin"; import { FieldGuesser, WithRecord, Labeled } from "react-admin"; import { Rating } from "@mui/material"; const ReviewShow = () => ( } /> ); ``` Here is the result: ![Admin with custom field component](./images/admin-custom-field.png) **Tip:** For a more complex field component, the preferred approach would probably be to create a dedicated React component. You can then leverage the [`useRecordContext`](https://marmelab.com/react-admin/useRecordContext.html) hook to achieve the same result. **Tip:** Check out the [Writing Your Own Field Component](https://marmelab.com/react-admin/Fields.html#writing-your-own-field-component) documentation to learn more about creating custom field components. Now let's create a custom input component, allowing not only to display a rating as a series of stars, but also to edit it. Again, we will leverage Material UI's [``](https://mui.com/material-ui/react-rating/) component for the rendering. But this time, we will leverage the [`useInput`](https://marmelab.com/react-admin/useInput.html) hook provided by React Admin, which allows to easily create a custom input component. ```tsx import { useInput } from "react-admin"; import { Rating } from "@mui/material"; const RatingInput = (props: InputProps) => { const { field } = useInput(props); return ( { field.onChange(value); }} /> ); }; ``` As you see, the `RatingInput` component is really short. It simply needs to call `field.onChange` whenever the rating changes. Now let's use this custom input component in the `ReviewEdit` component: ```tsx import { Edit, SimpleForm, InputGuesser, Labeled } from "react-admin"; import { RatingInput } from "./RatingInput"; const ReviewEdit = () => ( ); ``` Here is the result: ![Admin with custom input component](./images/admin-custom-input.png) ## React Admin Components As you saw from the previous sections and examples, while API Platform Admin aims at providing a complete and ready-to-use admin interface with as little code as possible, it always provides the flexibility to fully customize every aspect of the generated admin, while keeping a pleasant developer experience. This is made possible thanks to the numerous **React Admin components**. They are battle-tested, backend agnostic, fully customizable solutions to common Admin requirements. Here are some examples, from the simplest to the most complete solutions: - [PrevNextButton](https://marmelab.com/react-admin/PrevNextButton.html) - [MenuLive](https://marmelab.com/react-admin/MenuLive.html) - [FilterList](https://marmelab.com/react-admin/FilterList.html) - [RevisionsButton](https://marmelab.com/react-admin/RevisionsButton.html) - [WizardForm](https://marmelab.com/react-admin/WizardForm.html) - [Calendar](https://marmelab.com/react-admin/Calendar.html) - [SmartRichTextInput](https://marmelab.com/react-admin/SmartRichTextInput.html) - [SolarLayout](https://marmelab.com/react-admin/SolarLayout.html) - And many more... React Admin already includes 230+ hooks and components. And it always allows you to make your own, thanks to the building blocks it provides. Feel free to read through its [All Features](https://marmelab.com/react-admin/Features.html) documentation page to discover them all. --- # Handling Relations Source: https://api-platform.com/docs/admin/handling-relations/ # Handling Relations API Platform Admin handles `one-to-one`, `many-to-one` and `one-to-many` relations automatically. However, in some cases, dependeing on whether or not you chose to embed the relation in the serialized data, you may need to customize the way the relation is displayed and/or can be edited. ## Working With Embedded Relations You can configure your API to [embed the related data](../core/serialization.md#embedding-relations) in the serialized response. ```js // Without Embedded Book Data { "@id": "/reviews/15", id: 15, rating: 5, body: "A must-read for any software developer. Martin's insights are invaluable.", author: "Alice Smith", book: "/books/7" } // With Embedded Book Data { "@id": "/reviews/15", id: 15, rating: 5, body: "A must-read for any software developer. Martin's insights are invaluable.", author: "Alice Smith", book: { "@id": "/books/7", id: 7, title: "Clean Code", author: "Robert C. Martin", } } ``` If you do so, by default the admin will render the full object as text field and text input, which is not very user-friendly. ![Embedded Relation With Full Object](images/embedded-relation-full-object.png) There are two ways you can handle this situation: 1. Change the Field and Input components to [display the fields you want](./handling-relations.md#displaying-a-field-of-an-embedded-relation) 2. Ask the admin to [return the embedded resources' IRI instead of the full record](./handling-relations.md#return-the-embedded-resources-iri-instead-of-the-full-record), by leveraging the `useEmbedded` parameter ### Displaying a Field of an Embedded Relation React Admin fields allow to use the dot notation (e.g. `book.title`) to target a field from an embedded relation. ```jsx import { HydraAdmin, FieldGuesser, ListGuesser, ResourceGuesser, } from '@api-platform/admin'; import { TextField } from 'react-admin'; const ReviewList = () => ( {/* Use react-admin components directly when you want complex fields. */} ); export const App = () => ( ); ``` ![Embedded Relation With Dot Notation](images/embedded-relation-dot-notation.png) Allowing to edit the relation, on the other hand, is a little trickier, as it requires transforming the record to replace the nested object by its IRI. Fortunately, this can be done by leveraging the `transform` prop of the `` component. We can edit the relation by leveraging either [``](https://marmelab.com/react-admin/ReferenceInput.html) for a `to-one` relation or [``](https://marmelab.com/react-admin/ReferenceArrayInput.html) for a `to-many` relation. ```jsx import { HydraAdmin, InputGuesser, EditGuesser, ResourceGuesser, } from '@api-platform/admin'; import { ReferenceInput, AutocompleteInput } from 'react-admin'; const reviewEditTransform = (values) => ({ ...values, book: values.book['@id'], }); const ReviewEdit = () => ( ({ title: searchText })} /> ); export const App = () => ( ); ``` This offers a nice and convenient way to edit the relation. ![Embedded Relation With ReferenceInput](images/embedded-relation-ReferenceInput.png) **Tip:** We also had to customize ``'s child [``](https://marmelab.com/react-admin/AutocompleteInput.html) component to override its `label` and `filterToQuery` props. You can learn more about why that's necessary in the [Using an AutoComplete Input for Relations](./handling-relations.md#using-an-autocomplete-input-for-relations) section. ### Return the Embedded Resources' IRI Instead of the Full Record You can also ask the admin to return the embedded resources' IRI instead of the full record, by setting the `useEmbedded` parameter of the Hydra data provider to `false`. ```jsx // admin/src/App.jsx import { HydraAdmin, dataProvider } from "@api-platform/admin"; const entrypoint = process.env.ENTRYPOINT; export const App = () => ( ); ``` This tells the dataProvider to return only the IRI in the record, discarding the embedded data. ```js // With useEmbedded=true (default) const record = { "@id": "/reviews/15", id: 15, rating: 5, body: "A must-read for any software developer. Martin's insights are invaluable.", author: "Alice Smith", book: { "@id": "/books/7", id: 7, title: "Clean Code", author: "Robert C. Martin", }, }; // With useEmbedded=false const record = { "@id": "/reviews/15", id: 15, rating: 5, body: "A must-read for any software developer. Martin's insights are invaluable.", author: "Alice Smith", book: "/books/7", }; ``` This way, the related record's IRI is returned and can be displayed. ![Embedded Relation With useEmbedded To False](images/embedded-relation-useEmbedded-false.png) We can improve the UI further by leveraging React Admin's [``](https://marmelab.com/react-admin/ReferenceField.html) component: ```jsx import { HydraAdmin, FieldGuesser, ListGuesser, ResourceGuesser, } from '@api-platform/admin'; import { ReferenceField, TextField } from 'react-admin'; const ReviewList = () => ( ); export const App = () => ( ); ``` This allows to display the title of the related book instead of its IRI. ![Embedded Relation With ReferenceField](images/embedded-relation-ReferenceField.png) Lastly, this also allows to easily edit the relation by leveraging either [``](https://marmelab.com/react-admin/ReferenceInput.html) for a `to-one` relation or [``](https://marmelab.com/react-admin/ReferenceArrayInput.html) for a `to-many` relation. ```jsx import { HydraAdmin, InputGuesser, EditGuesser, ResourceGuesser, } from '@api-platform/admin'; import { ReferenceInput, AutocompleteInput } from 'react-admin'; const ReviewEdit = () => ( ({ title: searchText })} /> ); export const App = () => ( ); ``` This offers a nice and convenient way to edit the relation. ![Embedded Relation With ReferenceInput](images/embedded-relation-ReferenceInput.png) **Tip:** We also had to customize ``'s child [``](https://marmelab.com/react-admin/AutocompleteInput.html) component to override its `filterToQuery` props. You can learn more about why that's necessary in the [Using an AutoComplete Input for Relations](./handling-relations.md#using-an-autocomplete-input-for-relations) section. ## Using an Autocomplete Input for Relations By default, `` will render a [``](https://marmelab.com/react-admin/SelectInput.html) when it detects a relation. We can improve the UX further by rendering an [``](https://marmelab.com/react-admin/AutocompleteInput.html) instead. `` allows to search for a related record by typing its name in an input field. This is much more convenient when there are many records to choose from. Let's consider an API exposing `Review` and `Book` resources linked by a `many-to-one` relation (through the `book` property). This API uses the following PHP code: ```php reviews = new ArrayCollection(); } } ``` Notice the "partial search" [filter](../core/filters.md) on the `title` property of the `Book` resource class. Now, let's configure API Platform Admin to enable autocompletion for the book selector. We will leverage the [``](https://marmelab.com/react-admin/ReferenceInput.html) and [``](https://marmelab.com/react-admin/AutocompleteInput.html) components from React Admin: ```jsx import { HydraAdmin, ResourceGuesser, CreateGuesser, EditGuesser, InputGuesser, } from '@api-platform/admin'; import { ReferenceInput, AutocompleteInput } from 'react-admin'; const ReviewsEdit = () => ( ({ title: searchText })} optionText="title" /> ); export const App = () => ( ); ``` The important things to note are: - the `filterToQuery` prop, which allows to search for books by title (leveraging the "partial search" filter mentioned above) - the `optionText` prop, which tells the `` component to render books using their `title` property You can now search for books by title in the book selector of the review form. ![Admin With AutocompleteInput](./images/AutocompleteInput.png) ## Displaying Related Record Name Instead of Their IRI Thanks to the [Schema.org](./schema.md) support, you can easily display the name of a related resource instead of its IRI. Follow the [Displaying Related Resource's Name Instead of its IRI](./schema.md#displaying-related-resources-name-instead-of-its-iri) section of the Schema.org documentation to implement this feature. ## Going Further React Admin can handle many types of relations, even `many-to-many`. You can learn more about them in the [Fields For Relationships](https://marmelab.com/react-admin/FieldsForRelationships.html) documentation. You can also read the [Handling Relationships in React Admin](https://marmelab.com/blog/2025/02/06/handling-relationships-in-react-admin.html) post from the React Admin blog for concrete examples and source code. --- # Validation Source: https://api-platform.com/docs/admin/validation/ # Validation API Platform Admin manages automatically two types of validation: client-side validation and server-side (or submission) validation. ## Client-side Validation If the API documentation indicates that a field is mandatory, API Platform Admin will automatically add a [required client-side validation](https://marmelab.com/react-admin/Validation.html#per-input-validation-built-in-field-validators). For instance, with API Platform as backend, if you write the following: ```php ` with a React Admin [input component](https://marmelab.com/react-admin/Inputs.html), such as [``](https://marmelab.com/react-admin/TextInput.html), [``](https://marmelab.com/react-admin/DateInput.html) or [``](https://marmelab.com/react-admin/ReferenceInput.html), you will need to **manually add the validation rules back**. Fortunately, this is very easy to do, thanks to the [`validate`](https://marmelab.com/react-admin/Inputs.html#validate) prop of the input components. For instance, here is how to replace the input for the required `title` field: ```diff import { EditGuesser, InputGuesser } from '@api-platform/admin'; +import { TextInput, required } from 'react-admin'; export const BookEdit = () => ( - + ); ``` React Admin already comes with several [built-in validators](https://marmelab.com/react-admin/Validation.html#per-input-validation-built-in-field-validators), such as: - `required(message)` if the field is mandatory, - `minValue(min, message)` to specify a minimum value for integers, - `maxValue(max, message)` to specify a maximum value for integers, - `minLength(min, message)` to specify a minimum length for strings, - `maxLength(max, message)` to specify a maximum length for strings, - `number(message)` to check that the input is a valid number, - `email(message)` to check that the input is a valid email address, - `regex(pattern, message)` to validate that the input matches a regular expression, - `choices(list, message)` to validate that the input is within a given list React Admin also supports [Global Validation](https://marmelab.com/react-admin/Validation.html#global-validation) (at the form level). Check out the [Form Validation](https://marmelab.com/react-admin/Validation.html) documentation to learn more. --- # Real-time Updates With Mercure Source: https://api-platform.com/docs/admin/real-time-mercure/ # Real-time Updates With Mercure API Platform Admin support real-time updates by using the [Mercure protocol](https://mercure.rocks). Updates are received by using the `useMercureSubscription` hook in the `ListGuesser`, `ShowGuesser` and `EditGuesser` components. To enable Mercure server-side, see the [related documentation](../core/mercure.md). Once enabled, API Platform Admin for Hydra will automatically detect that Mercure is enabled and will discover the Mercure hub URL by itself. ## Advanced Configuration If you want to customize the default Mercure configuration, you can either do it with a prop in the `` or `` component: ```javascript import { OpenApiAdmin } from "@api-platform/admin"; export default () => ( ); ``` Or in the data provider factory: ```javascript import { hydraDataProvider, fetchHydra } from "@api-platform/admin"; import { parseHydraDocumentation } from "@api-platform/api-doc-parser"; const dataProvider = baseHydraDataProvider({ entrypoint, httpClient: fetchHydra, apiDocumentationParser: parseHydraDocumentation, mercure: { hub: "https://mercure.rocks/hub" }, }); ``` The `mercure` object can take the following properties: - `hub`: the URL to your Mercure hub (default value: null ; when null it will be discovered by using API responses) - `jwt`: a subscriber JWT to access your Mercure hub (default value: null) - `topicUrl`: the topic URL of your resources (default value: entrypoint) --- # Authentication Support Source: https://api-platform.com/docs/admin/authentication-support/ # Authentication Support API Platform Admin delegates the authentication support to React Admin. Refer to the [Auth Provider Setup](https://marmelab.com/react-admin/Authentication.html) documentation for more information. **Tip:** Once you have set up the authentication, you can also configure React Admin to perform client-side Authorization checks. Refer to the [Authorization](https://marmelab.com/react-admin/Permissions.html) documentation for more information. ## HydraAdmin Enabling authentication support for [`` component](./components.md#hydra) consists of a few parts, which need to be integrated together. In the following steps, we will see how to: - Make authenticated requests to the API (i.e. include the `Authorization` header) - Redirect users to the login page if they are not authenticated - Clear expired tokens when encountering unauthorized `401` response ### Make Authenticated Requests First, we need to implement a `getHeaders` function, that will add the Bearer token from `localStorage` (if there is one) to the `Authorization` header. ```typescript const getHeaders = () => localStorage.getItem("token") ? { Authorization: `Bearer ${localStorage.getItem("token")}` } : {}; ``` Then, extend the Hydra `fetch` function to use the `getHeaders` function to add the `Authorization` header to the requests. ```typescript import { fetchHydra as baseFetchHydra } from "@api-platform/admin"; const fetchHydra = (url, options = {}) => baseFetchHydra(url, { ...options, headers: getHeaders, }); ``` ### Redirect To Login Page Then, we'll create a `` component, that will redirect users to the `/login` route if no token is available in the `localStorage`, and call the dataProvider's `introspect` function otherwise. ```tsx import { Navigate } from "react-router-dom"; import { useIntrospection } from "@api-platform/admin"; const RedirectToLogin = () => { const introspect = useIntrospection(); if (localStorage.getItem("token")) { introspect(); return <>; } return ; }; ``` ### Clear Expired Tokens Now, we will extend the `parseHydraDocumentaion` function (imported from the [@api-platform/api-doc-parser](https://github.com/api-platform/api-doc-parser) library). We will customize it to clear expired tokens when encountering unauthorized `401` response. ```typescript import { parseHydraDocumentation } from "@api-platform/api-doc-parser"; import { ENTRYPOINT } from "config/entrypoint"; const apiDocumentationParser = (setRedirectToLogin) => async () => { try { setRedirectToLogin(false); return await parseHydraDocumentation(ENTRYPOINT, { headers: getHeaders }); } catch (result) { const { api, response, status } = result; if (status !== 401 || !response) { throw result; } localStorage.removeItem("token"); setRedirectToLogin(true); return { api, response, status }; } }; ``` ### Extend The Data Provider Now, we can initialize the Hydra data provider with the custom `fetchHydra` (with custom headers) and `apiDocumentationParser` functions created earlier. ```typescript import { hydraDataProvider as baseHydraDataProvider } from "@api-platform/admin"; import { ENTRYPOINT } from "config/entrypoint"; const dataProvider = (setRedirectToLogin) => baseHydraDataProvider({ entrypoint: ENTRYPOINT, httpClient: fetchHydra, apiDocumentationParser: apiDocumentationParser(setRedirectToLogin), }); ``` ### Update The Admin Component Lastly, we can stitch everything together in the `Admin` component. ```tsx // src/Admin.tsx import Head from "next/head"; import { useState } from "react"; import { Navigate, Route } from "react-router-dom"; import { CustomRoutes } from "react-admin"; import { fetchHydra as baseFetchHydra, HydraAdmin, hydraDataProvider as baseHydraDataProvider, useIntrospection, } from "@api-platform/admin"; import { parseHydraDocumentation } from "@api-platform/api-doc-parser"; import authProvider from "utils/authProvider"; import { ENTRYPOINT } from "config/entrypoint"; // Functions and components created in the previous steps: const getHeaders = () => {...}; const fetchHydra = (url, options = {}) => {...}; const RedirectToLogin = () => {...}; const apiDocumentationParser = (setRedirectToLogin) => async () => {...}; const dataProvider = (setRedirectToLogin) => {...}; export const Admin = () => { const [redirectToLogin, setRedirectToLogin] = useState(false); return ( <> API Platform Admin {redirectToLogin ? ( } /> } /> ) : ( <> )} ); }; ``` ### Example Implementation For the implementation of the admin component, you can find a working example in the [API Platform's demo application](https://github.com/api-platform/demo/blob/4.0/pwa/components/admin/Admin.tsx). ## OpenApiAdmin This section explains how to set up and customize the [`` component](./components.md/#openapi) to enable authentication. In the following steps, we will see how to: - Make authenticated requests to the API (i.e. include the `Authorization` header) - Implement an authProvider to redirect users to the login page if they are not authenticated, and clear expired tokens when encountering unauthorized `401` response ### Making Authenticated Requests First, we need to create a custom `httpClient` to add authentication tokens (via the the `Authorization` HTTP header) to requests. We will then configure `openApiDataProvider` to use [`ra-data-simple-rest`](https://github.com/marmelab/react-admin/blob/master/packages/ra-data-simple-rest/README.md), a simple REST dataProvider for React Admin, and make it use the `httpClient` we created earlier. ```typescript // src/dataProvider.ts const getAccessToken = () => localStorage.getItem("token"); const httpClient = async (url: string, options: fetchUtils.Options = {}) => { options.headers = new Headers({ ...options.headers, Accept: "application/json", }) as Headers; const token = getAccessToken(); options.user = { token: `Bearer ${token}`, authenticated: !!token }; return await fetchUtils.fetchJson(url, options); }; const dataProvider = openApiDataProvider({ dataProvider: simpleRestProvider(API_ENTRYPOINT_PATH, httpClient), entrypoint: API_ENTRYPOINT_PATH, docEntrypoint: API_DOCS_PATH, }); ``` **Note:** The `simpleRestProvider` provider expect the API to include a `Content-Range` header in the response. You can find more about the header syntax in the [Mozilla’s MDN documentation: Content-Range](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Range). **Note:** The `getAccessToken` function retrieves the JWT token stored in the browser's localStorage. Replace it with your own logic in case you don't store the token that way. ### Creating The AuthProvider Now let's create and export an `authProvider` object that handles authentication and authorization logic. ```typescript // src/authProvider.ts interface JwtPayload { sub: string; username: string; } const getAccessToken = () => localStorage.getItem("token"); const authProvider = { login: async ({ username, password }: { username: string; password: string }) => { const request = new Request(API_AUTH_PATH, { method: "POST", body: JSON.stringify({ email: username, password }), headers: new Headers({ "Content-Type": "application/json" }), }); const response = await fetch(request); if (response.status < 200 || response.status >= 300) { throw new Error(response.statusText); } const auth = await response.json(); localStorage.setItem("token", auth.token); }, logout: () => { localStorage.removeItem("token"); return Promise.resolve(); }, checkAuth: () => (getAccessToken() ? Promise.resolve() : Promise.reject()), checkError: (error: { status: number }) => { const status = error.status; if (status === 401 || status === 403) { localStorage.removeItem("token"); return Promise.reject(); } return Promise.resolve(); }, getIdentity: () => { const token = getAccessToken(); if (!token) return Promise.reject(); const decoded = jwtDecode(token); return Promise.resolve({ id: decoded.sub, fullName: decoded.username, avatar: "", }); }, getPermissions: () => Promise.resolve(""), }; export default authProvider; ``` ### Updating The Admin Component Finally, we can update the `Admin` component to use the `authProvider` and `dataProvider` we created earlier. ```tsx // src/Admin.tsx import { OpenApiAdmin } from "@api-platform/admin"; import authProvider from "./authProvider"; import dataProvider from "./dataProvider"; import { API_DOCS_PATH, API_ENTRYPOINT_PATH } from "./config/api"; export default () => ( ); ``` --- # Handling File Upload Source: https://api-platform.com/docs/admin/file-upload/ # Handling File Upload If you need to handle the file upload in the server part, please follow [the related documentation](../symfony/file-upload.md). This documentation assumes you have a `/media_objects` endpoint accepting `multipart/form-data`-encoded data. To manage the upload in the admin part, you need to [customize the guessed create or edit form](./customizing.md#from-inputguesser-to-react-admin-inputs). Add a [``](https://marmelab.com/react-admin/FileInput.html) as a child of the guesser. For example, for the create form: ```js import { HydraAdmin, ResourceGuesser, CreateGuesser } from "@api-platform/admin"; import { FileField, FileInput } from "react-admin"; const MediaObjectsCreate = () => ( ); export const App = () => ( {/* ... */} ); ``` And that's it! The guessers are able to detect that you have used a `FileInput` and are passing this information to the data provider, through a `hasFileField` field in the `extraInformation` object, itself in the data. If you are using the Hydra data provider, it uses a `multipart/form-data` request instead of a JSON-LD one. **Note:** In the case of the `EditGuesser`, the HTTP method used becomes a `POST` instead of a `PUT`, to prevent a [PHP bug](https://bugs.php.net/bug.php?id=55815). --- # Performance Tips Source: https://api-platform.com/docs/admin/performance/ # Performance Tips To make the admin faster and greener, you can make some changes to your API. ## Retrieve All Relations in One Request By default, if your relations are not embedded and if you decide to display some fields belonging to relations in your resource list, the admin will fetch the relations one by one. In this case, it can be improved by doing only one request for all the related resources instead. To do so, you need to make sure the [search filter](../core/doctrine-filters.md#search-filter) is enabled for the identifier of the related resource. For instance, if you have a `book` resource having a relation to `author` resources and you display the author names on your book list, you can make sure the authors are retrieved in one go by writing: ```php `](./components.md#adminguesser), but configured specially for [Hydra](https://www.hydra-cg.com/). **Tip:** For OpenAPI documented APIs, use the [`` component](./components.md#openapiadmin) instead. **Tip:** If you want to use other formats (see supported formats: `@api-platform/api-doc-parser`) use [``](./components.md#adminguesser) instead. ```tsx // App.tsx import { HydraAdmin, ResourceGuesser } from "@api-platform/admin"; const App = () => ( {/* ... */} ); export default App; ``` ### HydraAdmin Props | Name | Type | Value | required | Description | | ------------ | ------------------- | ------------ | -------- | ---------------------------- | | entrypoint | string | - | yes | entrypoint of the API | | mercure | object|boolean | \* | no | configuration to use Mercure | | dataProvider | object | dataProvider | no | hydra data provider to use | \* `false` to explicitly disable, `true` to enable with default parameters or an object with the following properties: - `hub`: the URL to your Mercure hub - `jwt`: a subscriber JWT to access your Mercure hub - `topicUrl`: the topic URL of your resources ### Hydra Data Provider An implementation for the React Admin [dataProvider methods](https://marmelab.com/react-admin/DataProviderWriting.html): `create`, `delete`, `getList`, `getManyReference`, `getOne` and `update`. The `dataProvider` is used by API Platform Admin to communicate with the API. In addition, the specific `introspect` method parses your API documentation. Note that the `dataProvider` can be overridden to fit your API needs. ### Hydra Schema Analyzer Analyses your resources and retrieves their types according to the [Schema.org](https://schema.org) vocabulary. ## OpenApiAdmin Creates a complete Admin, as [``](./components.md#adminguesser), but configured specially for [OpenAPI](https://www.openapis.org/). **Tip:** If you want to use other formats (see supported formats: `@api-platform/api-doc-parser`) use [``](./components.md#adminguesser) instead. ```tsx // App.tsx import { OpenApiAdmin, ResourceGuesser } from "@api-platform/admin"; const App = () => ( {/* ... */} ); export default App; ``` ### OpenApiAdmin Props | Name | Type | Value | required | Description | | ------------- | ------------------- | ----- | -------- | ---------------------------- | | docEntrypoint | string | - | yes | doc entrypoint of the API | | entrypoint | string | - | yes | entrypoint of the API | | dataProvider | dataProvider | - | no | data provider to use | | mercure | object|boolean | \* | no | configuration to use Mercure | \* `false` to explicitly disable, `true` to enable with default parameters or an object with the following properties: - `hub`: the URL to your Mercure hub - `jwt`: a subscriber JWT to access your Mercure hub - `topicUrl`: the topic URL of your resources ### Open API Data Provider An implementation for the React Admin [dataProvider methods](https://marmelab.com/react-admin/DataProviderWriting.html): `create`, `delete`, `getList`, `getManyReference`, `getOne` and `update`. The `dataProvider` is used by API Platform Admin to communicate with the API. In addition, the specific `introspect` method parses your API documentation. Note that the `dataProvider` can be overridden to fit your API needs. ### Open API Schema Analyzer Analyses your resources and retrieves their types according to the [Schema.org](https://schema.org) vocabulary. ## AdminGuesser `` automatically renders an [`` component](https://marmelab.com/react-admin/Admin.html) for resources exposed by a web API documented with any format supported by `@api-platform/api-doc-parser`. ```tsx // App.tsx import { AdminGuesser } from "@api-platform/admin"; import dataProvider from "./dataProvider"; import schemaAnalyzer from "./schemaAnalyzer"; const App = () => ; export default App; ``` Use it if your API is neither documented with Hydra nor OpenAPI, but in a format supported by `@api-platform/api-doc-parser`. **Tip:** For Hydra documented APIs, use the [`` component](./components.md#hydraadmin) instead. **Tip:** For OpenAPI documented APIs, use the [`` component](./components.md#openapiadmin) instead. `` renders all exposed resources by default, but you can choose what resource you want to render by passing [`` components](./components.md#resourceguesser) as children. ```tsx // App.tsx import { AdminGuesser, ResourceGuesser } from "@api-platform/admin"; import dataProvider from "./dataProvider"; import schemaAnalyzer from "./schemaAnalyzer"; const App = () => ( ); export default App; ``` **Tip:** Deprecated resources are hidden by default, but you can add them back using an explicit `` component. ### AdminGuesser Props | Name | Type | Value | required | Description | | ----------------- | --------------- | -------------- | -------- | -------------------------------------------------------------------------------- | | dataProvider | object | dataProvider | yes | the dataProvider to use to communicate with your API | | schemaAnalyzer | object | schemaAnalyzer | yes | retrieves resource type according to [Schema.org](https://schema.org) vocabulary | | authProvider | object | authProvider | no | the authProvider to use to manage authentication | | admin | React component | - | no | React component to use to render the Admin | | includeDeprecated | boolean | true or false | no | displays or not deprecated resources | `` also accepts all props accepted by React Admin's [`` component](https://marmelab.com/react-admin/Admin.html), such as [`theme`](https://marmelab.com/react-admin/Admin.html#theme), [`darkTheme`](https://marmelab.com/react-admin/Admin.html#darktheme), [`layout`](https://marmelab.com/react-admin/Admin.html#layout) and many others. ## ResourceGuesser Based on React Admin [`` component](https://marmelab.com/react-admin/Resource.html), `` provides the default component to render for each view: [``](./components.md#createguesser), [``](./components.md#listguesser), [``](./components.md#editguesser) and [``](./components.md#showguesser). You can also pass your own component to use for any view, using the `create`, `list`, `edit` or `show` props. ```tsx // App.tsx import { AdminGuesser, ResourceGuesser } from "@api-platform/admin"; const App = () => ( {/* Uses the default guesser components for each CRUD view */} {/* Overrides only the list view */} ); export default App; ``` ### ResourceGuesser Props | Name | Type | Value | required | Description | | ------ | ------------------- | ----- | -------- | ------------------------------------------- | | name | string | - | yes | endpoint of the resource | | list | React ComponentType | - | no | the component to render for the list view | | create | React ComponentType | - | no | the component to render for the create view | | edit | React ComponentType | - | no | the component to render for the edit view | | show | React ComponentType | - | no | the component to render for the show view | `` also accepts all props accepted by React Admin's [`` component](https://marmelab.com/react-admin/Resource.html), such as [`recordRepresentation`](https://marmelab.com/react-admin/Resource.html#recordrepresentation), [`icon`](https://marmelab.com/react-admin/Resource.html#icon) or [`options`](https://marmelab.com/react-admin/Resource.html#options). ## ListGuesser Based on React Admin [``](https://marmelab.com/react-admin/List.html), `` displays a list of records in a [``](https://marmelab.com/react-admin/Datagrid.html). If no children are passed, it will display fields guessed from the schema. ```tsx // BooksList.tsx import { ListGuesser } from "@api-platform/admin"; export const BooksList = () => ( /* Will display fields guessed from the schema */ ); ``` It also accepts a list of fields as children. They can be either [``](./components.md#fieldguesser) elements, or any [field component](https://marmelab.com/react-admin/Fields.html) available in React Admin, such as [``](https://marmelab.com/react-admin/TextField.html), [``](https://marmelab.com/react-admin/DateField.html) or [``](https://marmelab.com/react-admin/ReferenceField.html) for instance. ```tsx // BooksList.tsx import { FieldGuesser, ListGuesser } from "@api-platform/admin"; import { DateField, NumberField } from "react-admin"; export const BooksList = () => ( {/* FieldGuesser comes from API Platform Admin */} {/* DateField and NumberField come from React Admin */} ); ``` ### ListGuesser Props `` accepts all props accepted by both React Admin [`` component](https://marmelab.com/react-admin/List.html) and [`` component](https://marmelab.com/react-admin/Datagrid.html). For instance you can pass props such as [`filters`](https://marmelab.com/react-admin/List.html#filters-filter-inputs), [`sort`](https://marmelab.com/react-admin/List.html#sort) or [`pagination`](https://marmelab.com/react-admin/List.html#pagination). ## CreateGuesser Displays a creation page for a single item. Uses React Admin [``](https://marmelab.com/react-admin/Create.html) and [``](https://marmelab.com/react-admin/SimpleForm.html) components. If no children are passed, it will display inputs guessed from the schema. ```tsx // BooksCreate.tsx import { CreateGuesser } from "@api-platform/admin"; export const BooksCreate = () => ( /* Will display inputs guessed from the schema */ ); ``` It also accepts a list of inputs as children, which can be either [``](./components.md#inputguesser) elements, or any [input component](https://marmelab.com/react-admin/Inputs.html) available in React Admin, such as [``](https://marmelab.com/react-admin/TextInput.html), [``](https://marmelab.com/react-admin/DateInput.html) or [``](https://marmelab.com/react-admin/ReferenceInput.html) for instance. ```tsx // BooksCreate.tsx import { CreateGuesser, InputGuesser } from "@api-platform/admin"; import { DateInput, TextInput, required } from "react-admin"; export const BooksCreate = () => ( {/* InputGuesser comes from API Platform Admin */} {/* DateInput and TextInput come from React Admin */} ); ``` ### CreateGuesser Props `` accepts all props accepted by both React Admin [`` component](https://marmelab.com/react-admin/Create.html) and [`` component](https://marmelab.com/react-admin/SimpleForm.html). For instance you can pass props such as [`redirect`](https://marmelab.com/react-admin/Create.html#redirect), [`defaultValues`](https://marmelab.com/react-admin/SimpleForm.html#defaultvalues) or [`warnWhenUnsavedChanges`](https://marmelab.com/react-admin/SimpleForm.html#warnwhenunsavedchanges). ## EditGuesser Displays an edition page for a single item. Uses React Admin [``](https://marmelab.com/react-admin/Edit.html) and [``](https://marmelab.com/react-admin/SimpleForm.html) components. If no children are passed, it will display inputs guessed from the schema. ```tsx // BooksEdit.tsx import { EditGuesser } from "@api-platform/admin"; export const BooksEdit = () => ( /* Will display inputs guessed from the schema */ ); ``` It also accepts a list of inputs as children, which can be either [``](./components.md#inputguesser) elements, or any [input component](https://marmelab.com/react-admin/Inputs.html) available in React Admin, such as [``](https://marmelab.com/react-admin/TextInput.html), [``](https://marmelab.com/react-admin/DateInput.html) or [``](https://marmelab.com/react-admin/ReferenceInput.html) for instance. ```tsx // BooksEdit.tsx import { EditGuesser, InputGuesser } from "@api-platform/admin"; import { DateInput, TextInput, required } from "react-admin"; export const BooksEdit = () => ( {/* InputGuesser comes from API Platform Admin */} {/* DateInput and TextInput come from React Admin */} ); ``` ### EditGuesser Props `` accepts all props accepted by both React Admin [`` component](https://marmelab.com/react-admin/Edit.html) and [`` component](https://marmelab.com/react-admin/SimpleForm.html). For instance you can pass props such as [`redirect`](https://marmelab.com/react-admin/Edit.html#redirect), [`mutationMode`](https://marmelab.com/react-admin/Edit.html#mutationmode), [`defaultValues`](https://marmelab.com/react-admin/SimpleForm.html#defaultvalues) or [`warnWhenUnsavedChanges`](https://marmelab.com/react-admin/SimpleForm.html#warnwhenunsavedchanges). ## ShowGuesser Displays a detailed page for one item. Based on React Admin [``](https://marmelab.com/react-admin/Show.html) ans [``](https://marmelab.com/react-admin/SimpleShowLayout.html) components. If you pass no children, it will display fields guessed from the schema. ```tsx // BooksShow.tsx import { ShowGuesser } from "@api-platform/admin"; export const BooksShow = () => ( /* Will display fields guessed from the schema */ ); ``` It also accepts a list of fields as children, which can be either [``](./components.md#fieldguesser) elements, or any [field component](https://marmelab.com/react-admin/Fields.html) available in React Admin, such as [``](https://marmelab.com/react-admin/TextField.html), [``](https://marmelab.com/react-admin/DateField.html) or [``](https://marmelab.com/react-admin/ReferenceField.html) for instance. ```tsx // BooksShow.tsx import { FieldGuesser, ShowGuesser } from "@api-platform/admin"; import { DateField, NumberField } from "react-admin"; export const BooksShow = () => ( {/* FieldGuesser comes from API Platform Admin */} {/* DateField and NumberField come from React Admin */} ); ``` ### ShowGuesser Props `` accepts all props accepted by both React Admin [`` component](https://marmelab.com/react-admin/Show.html) and [`` component](https://marmelab.com/react-admin/SimpleShowLayout.html). ## FieldGuesser Renders a field according to its type, using the [schema analyzer](./components.md#hydra-schema-analyzer). Based on React Admin [field components](https://marmelab.com/react-admin/Fields.html), such as [``](https://marmelab.com/react-admin/TextField.html), [``](https://marmelab.com/react-admin/DateField.html) or [``](https://marmelab.com/react-admin/ReferenceField.html). ```tsx // BooksShow.tsx import { FieldGuesser, ShowGuesser } from "@api-platform/admin"; export const BooksShow = () => ( {/* Renders a TextField */} {/* Renders a NumberField */} {/* Renders a DateField */} ); ``` ### FieldGuesser Props | Name | Type | Value | required | Description | | ------ | ------ | ----- | -------- | ------------------------------------ | | source | string | - | yes | name of the property of the resource | `` also accepts any [common field prop](https://marmelab.com/react-admin/Fields.html#common-field-props) supported by React Admin, such as [`label`](https://marmelab.com/react-admin/Fields.html#label) for instance. ## InputGuesser Renders an input according to its type, using the [schema analyzer](./components.md#hydra-schema-analyzer). Uses React Admin [input components](https://marmelab.com/react-admin/Inputs.html), such as [``](https://marmelab.com/react-admin/TextInput.html), [``](https://marmelab.com/react-admin/DateInput.html) or [``](https://marmelab.com/react-admin/ReferenceInput.html). ```tsx // BooksCreate.tsx import { CreateGuesser, InputGuesser } from "@api-platform/admin"; export const BooksCreate = () => ( {/* Renders a TextInput */} {/* Renders a NumberInput */} {/* Renders a DateInput */} ); ``` ### InputGuesser Props | Name | Type | Value | required | Description | | ------ | ------ | ----- | -------- | ------------------------------------ | | source | string | - | yes | name of the property of the resource | `` also accepts any [common input prop](https://marmelab.com/react-admin/Inputs.html#common-input-props) supported by React Admin, such as [`defaultValue`](https://marmelab.com/react-admin/Inputs.html#defaultvalue), [`readOnly`](https://marmelab.com/react-admin/Inputs.html#readonly), [`helperText`](https://marmelab.com/react-admin/Inputs.html#helpertext) or [`label`](https://marmelab.com/react-admin/Inputs.html#label). You can also pass props that are specific to a certain input component. For example, if you know an `` will render a `` and you would like that input to be multiline, you can set the [`multiline`](https://marmelab.com/react-admin/TextInput.html#multiline) prop. ```tsx ``` --- # API Platform Create Client Source: https://api-platform.com/docs/create-client/ # API Platform Create Client Create Client is the fastest way to scaffold fully featured webapps and native mobile apps from APIs supporting the [Hydra](https://www.hydra-cg.com/) or [OpenAPI](https://www.openapis.org/) format. ![Screencast](images/create-client-demo.gif) ## Generated React and React Native Apps, Updated in Real Time It is able to generate apps using the following frontend stacks: - [Next.js](nextjs.md) - [Nuxt](nuxt.md) - [Quasar](quasar.md) - [Vuetify](vuetify.md) - [React](react.md) - [React Native](react-native.md) - [Vue.js](vuejs.md) - [Or bring your custom generator](custom.md) Create Client works especially well with APIs built with the [API Platform](https://api-platform.com) framework. ## Features - Generates high-quality TypeScript: - list view (with pagination) - detail view - creation form - update form - delete button - Supports to-one and to-many relations - Uses the appropriate input type (`number`, `date`...) - Client-side validation - Subscribes to data updates pushed by servers supporting [the Mercure protocol](https://mercure.rocks) - Displays server-side validation errors under the related input (if using API Platform Core) - Integration with [Tailwind CSS](https://tailwindcss.com) (Next.js) or [Bootstrap](https://getbootstrap.com/) and [Font Awesome](https://fontawesome.com/) (other generators) - Integration with [React Native Elements](https://react-native-training.github.io/react-native-elements/) - Accessible to people with disabilities ([ARIA](https://www.w3.org/WAI/intro/aria) support in webapps) --- # Next.js Generator Source: https://api-platform.com/docs/create-client/nextjs/ # Next.js Generator ![List screenshot](images/nextjs/create-client-nextjs-list.png) The Next.js generator scaffolds components for server-side rendered (SSR) applications using [Next.js](https://nextjs.org/). ## Install The easiest way to get started is to install [the API Platform Symfony variant](../symfony/index.md). It contains a Next.js skeleton generated with Create Next App, a development Docker container to serve the webapp, and all the API Platform components you may need, including an API server supporting Hydra and OpenAPI. If you use API Platform, jump to the next section! Alternatively, create a Next.js application by executing: - Pnpm (recommended) ```console pnpm create next-app --typescript ``` - Npm ```console npm init next-app --typescript ``` - Yarn ```console yarn reate next-app --typescript ``` Install the required dependencies: - Pnpm (recommended) ```console pnpm install isomorphic-unfetch formik react-query ``` - Npm ```console npm install isomorphic-unfetch formik react-query ``` - Yarn ```console yarn add isomorphic-unfetch formik react-query ``` The generated HTML will contain [Tailwind CSS](https://tailwindcss.com) classes. Optionally, [follow the Tailwind installation guide for Next.js projects](https://tailwindcss.com/docs/guides/nextjs) (Tailwind is preinstalled in [the API Platform Symfony variant](../symfony/index.md)) ## Generating Routes If you are using the [API Platform Symfony variant](../symfony/index.md) generating all the code you need for a given resource is as simple as running the following command: ```console docker compose exec pwa \ pnpm create @api-platform/client --resource book -g next ``` Omit the resource flag to generate files for all resource types exposed by the API. Or if you don't use the standalone installation, run the following command instead: - Pnpm (recommended) ```console pnpm create @api-platform/client https://demo.api-platform.com . --generator next --resource book ``` - Npm ```console npm init @api-platform/client https://demo.api-platform.com . -- --generator next --resource book ``` - Yarn ```console yarn create @api-platform/client https://demo.api-platform.com . --generator next --resource book ``` Replace the URL by the entrypoint of your Hydra-enabled API. You can also use an OpenAPI documentation with `-f openapi3`. The code has been generated, and is ready to be executed! Add the layout to the app: ```typescript import type { AppProps } from "next/app"; import type { DehydratedState } from "react-query"; import Layout from "../components/common/Layout"; const App = ({ Component, pageProps }: AppProps<{dehydratedState: DehydratedState}>) => ( ); export default App; ``` ## Starting the Project You can launch the server with: - Pnpm (recommended) ```console pnpm dev ``` - Npm ```console npm run dev ``` - Yarn ```console yarn dev ``` Go to `http://localhost:3000/books/` to start using your app. ## Generating a production build locally with docker compose If you want to generate a production build locally with docker compose, follow [these instructions](../deployment/docker-compose.md). ## Screenshots ![List](images/nextjs/create-client-nextjs-list.png) ![Show](images/nextjs/create-client-nextjs-show.png) --- # Nuxt Generator Source: https://api-platform.com/docs/create-client/nuxt/ # Nuxt Generator Bootstrap a [Nuxt 3](https://nuxt.com/) application: ```console npx nuxi init my-app cd my-app ``` Install the required dependencies: ```console yarn add dayjs @pinia/nuxt qs @types/qs ``` To generate the code you need for a given resource, run the following command: ```console yarn create @api-platform/client https://demo.api-platform.com . --generator nuxt --resource foo ``` Replace the URL with the entrypoint of your Hydra-enabled API. You can also use an OpenAPI documentation with `https://demo.api-platform.com/docs.jsonopenapi` and `-f openapi3`. Omit the resource flag to generate files for all resource types exposed by the API. Add Pinia module in `nuxt.config.ts`: ```typescript // https://nuxt.com/docs/api/configuration/nuxt-config export default defineNuxtConfig({ // ... modules: ["@pinia/nuxt"], // ... }); ``` Delete `app.vue` as it will prevent Nuxt router to work correctly. Optionally, install Tailwind to get an app that looks good: ```console yarn add -D tailwindcss postcss autoprefixer yarn tailwindcss init -p ``` Add this code in `nuxt.config.ts`: ```typescript // https://nuxt.com/docs/api/configuration/nuxt-config export default defineNuxtConfig({ // ... css: ["~/assets/css/main.css"], postcss: { plugins: { tailwindcss: {}, autoprefixer: {}, }, }, // ... }); ``` And this code in `tailwind.config.js`: ```javascript /** @type {import('tailwindcss').Config} */ module.exports = { content: [ "./components/**/*.{js,vue,ts}", "./layouts/**/*.vue", "./pages/**/*.vue", "./plugins/**/*.{js,ts}", "./nuxt.config.{js,ts}", "./app.vue", ], theme: { extend: {}, }, plugins: [], }; ``` Create the file `assets/css/main.css` and add this code in it: ```css @tailwind base; @tailwind components; @tailwind utilities; ``` You can launch the server with: ```console yarn dev -o ``` Go to `https://localhost:3000/books/` to start using your app. ## Screenshots ![List](images/nuxt/create-client-nuxt-list.png) ![Edit](images/nuxt/create-client-nuxt-edit.png) --- # Vuetify Generator Source: https://api-platform.com/docs/create-client/vuetify/ # Vuetify Generator Bootstrap a Vuetify 3 application using `create-vuetify`: ```console npm init vuetify -- --typescript --preset essentials cd my-app ``` Install the required dependencies: ```console npm install dayjs qs @types/qs vue-i18n ``` To generate all the code you need for a given resource run the following command: ```console npm init @api-platform/client https://demo.api-platform.com src/ -- --generator vuetify --resource book ``` Replace the URL with the entrypoint of your Hydra-enabled API. You can also use an OpenAPI documentation with `https://demo.api-platform.com/docs.jsonopenapi` and `-f openapi3`. Omit the resource flag to generate files for all resource types exposed by the API. **Note:** Make sure to follow the result indications of the command to register the routes and the translations. Then add this import in `src/plugins/vuetify.ts`: ```typescript // src/plugins/vuetify.ts import { VDataTableServer } from "vuetify/labs/VDataTable"; ``` In the same file replace the export with: ```typescript // src/plugins/vuetify.ts export default createVuetify({ components: { VDataTableServer, }, }); ``` In `src/plugins/index.ts` add this import: ```typescript // src/plugins/index.ts import i18n from "@/plugins/i18n"; ``` In the same file add `.use(i18n)` chained with the other `use()` functions. You can launch the server with: ```console npm run dev ``` Go to `http://localhost:3000/books/` to start using your app. **Note:** In order to Mercure to work with the demo, you have to use the port 3000. --- # Quasar Framework Generator Source: https://api-platform.com/docs/create-client/quasar/ # Quasar Framework Generator Create a Quasar Framework application using [Quasar CLI](https://quasar.dev/start/quasar-cli): ```console npm i -g @quasar/cli npm init quasar cd my-app ``` It will ask you some questions, you can use these answers: ```console What would you like to build ? App with Quasar CLI, let's go! Project folder: my-app Pick Quasar version: Quasar v2 (Vue 3 | latest and greatest) Pick script types: Typescript Pick Quasar App CLI variant: Quasar App CLI with Vite Package name: my-app Pick a Vue component style: Composition API with ``` Optionally, install Tailwind to get an app that looks good: ```console npm install -D tailwindcss postcss autoprefixer npx tailwindcss init -p ``` Replace the content of `tailwind.config.js` by: ```js // tailwind.config.js /** @type {import('tailwindcss').Config} */ module.exports = { content: ["./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}"], theme: { extend: {}, }, plugins: [], }; ``` Replace the content of `src/assets/main.css` by: ```css @tailwind base; @tailwind components; @tailwind utilities; ``` You can launch the server with: ```console npm run dev ``` Go to `http://localhost:5173/books/` to start using your app. **Note:** In order to Mercure to work with the demo, you have to use the port 3000. --- # TypeScript Interfaces Source: https://api-platform.com/docs/create-client/typescript/ # TypeScript Interfaces The TypeScript Generator allows you to create [TypeScript interfaces](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#interfaces) that you can embed in any TypeScript-enabled project (React, Vue.js, Angular..). To do so, run the generator: ```console npm init @api-platform/client https://demo.api-platform.com src/ -- --generator typescript --resource foo # Replace the URL with the entrypoint of your Hydra-enabled API. ``` `src/` is where the interfaces will be generated. Omit the resource flag to generate files for all resource types exposed by the API. You can also use an OpenAPI documentation with `-f openapi3`. This command parses the Hydra documentation and creates one `.ts` file for each API Resource you have defined in your application, in the `interfaces` subfolder. **Note:** If you are not sure what the entrypoint is, see [Troubleshooting](troubleshooting.md). ## Example Assuming you have 2 resources in your application, `Foo` and `Bar`, when you run: ```console npm init @api-platform/client https://demo.api-platform.com src/ -- --generator typescript ``` You will obtain 2 `.ts` files arranged as following: - src/ - interfaces/ - foo.ts - bar.ts --- # Custom Generator Source: https://api-platform.com/docs/create-client/custom/ # Custom Generator Create Client provides support for many of the popular JS frameworks, but you may be using another framework or language and may need a solution adapted to your specific needs. For this scenario, you can write your own generator and pass it to the CLI using a path as the `-g` argument. You will probably want to extend or, at least, take a look at [BaseGenerator.js](https://github.com/api-platform/create-client/blob/main/src/generators/BaseGenerator.js), since the library expects some methods to be available, as well as one of the [included generators](https://github.com/api-platform/create-client/blob/main/src/generators/BaseGenerator.js) to make your own. ## Usage ```shell npm init @api-platform/client -- --generator "$(pwd)/path/to/custom/generator.js" -t "$(pwd)/path/to/templates" ``` The `-g` argument can point to any resolvable node module which means it can be a package dependency of the current project as well as any js file. ## Example Create Client makes use of the [Handlebars](https://handlebarsjs.com/) template engine. You can use any programming language or file type. Your generator can also pass data to your templates in any shape you want. In this example, we'll create a simple [Rust](https://www.rust-lang.org) file defining a new `struct` and creating some instances of this `struct`. ### Generator ```js // ./Generator.js import BaseGenerator from "@api-platform/create-client/lib/generators/BaseGenerator"; export default class extends BaseGenerator { constructor(params) { super(params); this.registerTemplates("", ["main.rs"]); } help() {} generate(api, resource, dir) { const context = { type: "Tilia", structure: [ { name: "name", type: "String" }, { name: "min_size", type: "u8" }, { name: "max_size", type: "u8" }, ], list: [ { name: "Tilia cordata", minSize: 50, maxSize: 80, }, { name: "Tilia platyphyllos", minSize: 50, maxSize: 70, }, { name: "Tilia tomentosa", minSize: 50, maxSize: 70, }, { name: "Tilia intermedia", minSize: 50, maxSize: 165, }, ], }; this.createDir(dir); this.createFile("main.rs", `${dir}/main.rs`, context, false); } } ``` ### Template ```rs // template/main.rs struct {{{type}}} { {{#each structure}} {{{name}}}: {{{type}}} {{/each}} } fn main() { let tilias = [ {{#each list}} Tilia { name: "{{{name}}}", min_size: {{{minSize}}}, max_size: {{{maxSize}}}, }, {{/each}} ]; } ``` Then we can use our generator: ```shell npm init @api-platform/client https://demo.api-platform.com out/ -g "$(pwd)/Generator.js" -t "$(pwd)/template" ``` which will produces: ```ts struct Tilia { name: String min_size: u8 max_size: u8 } fn main() { let tilias = [ Tilia { name: "Tilia cordata", min_size: 50, max_size: 80, }, Tilia { name: "Tilia platyphyllos", min_size: 50, max_size: 70, }, Tilia { name: "Tilia tomentosa", min_size: 50, max_size: 70, }, Tilia { name: "Tilia intermedia", min_size: 50, max_size: 165, }, ]; } ``` --- # Troubleshooting Source: https://api-platform.com/docs/create-client/troubleshooting/ # Troubleshooting ## Self-Signed TLS Certificate If you are running API Platform on development machine which does not have valid TLS certificate, add `NODE_TLS_REJECT_UNAUTHORIZED=0` before running create-client: ```console NODE_TLS_REJECT_UNAUTHORIZED=0 npm init @api-platform/client --generator typescript https://127.0.0.1:8000/api src/ ``` ## Authenticated API The generator does not perform any authentication, so you must ensure that all referenced Hydra paths for your API are accessible anonymously. If you are using API Platform this will at least include: ```console api_entrypoint ANY ANY ANY /{index}.{_format} api_doc ANY ANY ANY /docs.{_format} api_jsonld_context ANY ANY ANY /contexts/{shortName}.{_format} ``` ## ApiDocumentation doesn't exist If you receive `Error: The class http://www.w3.org/ns/hydra/core#ApiDocumentation doesn't exist.` you may have specified the documentation URL instead of the entrypoint. For example if you are using API Platform and your documentation URL is at [https://demo.api-platform.com/docs](https://demo.api-platform.com/docs) the entry point is likely at [https://demo.api-platform.com](https://demo.api-platform.com). You can see an example of the expected response from an entrypoint in your browser by visiting [https://demo.api-platform.com/index.jsonld](https://demo.api-platform.com/index.jsonld). ## Cannot read property '@type' If you receive `TypeError: Cannot read property '@type' of undefined` or `TypeError: Cannot read property '0' of undefined` check that the URL you specified is accessible and returns jsonld. You can check from the command line you are using by running something like `curl https://demo.api-platform.com/`. ## Dereferencing a URL did not result in a JSON object If you receive a message like this: ```console { Error at done (/usr/local/share/.config/yarn/global/node_modules/jsonld/js/jsonld.js:6851:19) at at process._tickCallback (internal/process/next_tick.js:188:7) name: 'jsonld.InvalidUrl', message: 'Dereferencing a URL did not result in a JSON object. The response was valid JSON, but it was not a JSON object.', details: { code: 'invalid remote context', url: 'https://demo.api-platform.com/contexts/Entrypoint', cause: null } } ``` Check access to the specified URL, in this case `https://demo.api-platform.com/contexts/Entrypoint`, use curl to check access and the response `curl https://demo.api-platform.com/contexts/Entrypoint`. In the above case an "Access Denied" message in JSON format was being returned. ## Docker distribution on Windows and hot-reloading Due to [a long-time known Docker for Windows issue](https://forums.docker.com/t/file-system-watch-does-not-work-with-mounted-volumes/12038), the file changes on the host are not notified on the `pwa` container. It causes the hot-reloading feature to not working properly for Windows users. --- # Deploying API Platform Applications Source: https://api-platform.com/docs/deployment/ # Deploying API Platform Applications API Platform apps are super easy to deploy in production thanks to the [Docker Compose definition](docker-compose.md) and to the [Kubernetes chart](kubernetes.md) we provide. We strongly recommend using Kubernetes or Docker Compose to deploy your apps. If you want to play with a local Kubernetes cluster, read [how to deploy an API Platform project on Minikube](minikube.md). If you don't want to use Docker, keep in mind that the server application of API Platform is a standard Symfony project, while the Progressive Web Application is a standard Next.js project:

JWT screencast
Watch the Animated Deployment with Ansistrano screencast

- [Deploying the Symfony application](https://symfony.com/doc/current/deployment.html) - [Deploying the Next.js application](https://nextjs.org/docs/deployment) Alternatively, you may want to deploy API Platform on a PaaS (Platform as a Service): - [Deploying the server application of API Platform on Heroku](heroku.md) - [Deploying API Platform on Platform.sh (outdated)](https://platform.sh/blog/deploy-api-platform-on-platformsh) --- # Deploying with Docker Compose Source: https://api-platform.com/docs/deployment/docker-compose/ # Deploying with Docker Compose While [Docker Compose](https://docs.docker.com/compose/) is mainly known and used in a development environment, it [can be used in production too](https://docs.docker.com/compose/production/). This is especially suitable for prototyping or small-scale deployments, where the robustness (and the associated complexity) of [Kubernetes](kubernetes.md) is not required. API Platform provides Docker images and a Docker Compose definition optimized for production usage. In this tutorial, we will learn how to deploy our Symfony application on a single server using Docker Compose. Note: this tutorial has been adapted from [the Symfony Docker documentation](https://github.com/dunglas/symfony-docker/blob/main/docs/production.md). ## Preparing a Server To deploy your application in production, you need a server. In this tutorial, we will use a virtual machine provided by DigitalOcean, but any Linux server can work. If you already have a Linux server with Docker Compose installed, you can skip straight to [the next section](#configuring-a-domain-name). Otherwise, use [this affiliate link](https://m.do.co/c/5d8aabe3ab80) to get $100 of free credit, create an account, then click on "Create a Droplet". Then, click on the "Marketplace" tab under the "Choose an image" section and search for the app named "Docker". This will provision an Ubuntu server with the latest versions of Docker and Docker Compose already installed! For test purposes, the cheapest plans will be enough, even though you might want at least 2GB of RAM to execute Docker Compose for the first time. For real production usage, you'll probably want to pick a plan in the "general purpose" section to fit your needs. ![Deploying an API Platform project on DigitalOcean with Docker Compose](images/digitalocean-droplet.png) You can keep the defaults for other settings, or tweak them according to your needs. Don't forget to add your SSH key or create a password then press the "Finalize and create" button. Then, wait a few seconds while your Droplet is provisioning. When your Droplet is ready, use SSH to connect: ```console ssh root@ ``` ## Configuring a Domain Name In most cases, you'll want to associate a domain name with your site. If you don't own a domain name yet, you'll have to buy one through a registrar. Then create a DNS record of type `A` for your domain name pointing to the IP address of your server: ```dns your-domain-name.example.com. IN A 207.154.233.113 ``` Example with the DigitalOcean Domains service ("Networking" > "Domains"): ![Configuring DNS on DigitalOcean](../deployment/images/digitalocean-dns.png) > [!NOTE] Let's Encrypt, the service used by default by API Platform to automatically generate a TLS > certificate, doesn't support using bare IP addresses. Using a domain name is mandatory to use > Let's Encrypt. ## Deploying Copy your project on the server using `git clone`, `scp`, or any other tool that may fit your needs. If you use GitHub, you may want to use [a deploy key](https://docs.github.com/en/developers/overview/managing-deploy-keys#deploy-keys). Deploy keys are also [supported by GitLab](https://docs.gitlab.com/ee/user/project/deploy_keys/). Example with Git: ```console git clone git@github.com:/.git ``` Go into the directory containing your project (``), and start the app in production mode: ```console SERVER_NAME=your-domain-name.example.com \ APP_SECRET=ChangeMe \ POSTGRES_PASSWORD=ChangeMe \ CADDY_MERCURE_JWT_SECRET=ChangeThisMercureHubJWTSecretKey \ docker compose -f compose.yaml -f compose.prod.yaml up --wait ``` Be sure to replace `your-domain-name.example.com` with your actual domain name and to set the values of `APP_SECRET`, `CADDY_MERCURE_JWT_SECRET` to cryptographically secure random values. > [!CAUTION] Docker can have a cache layer, make sure you have the right build for each deployment > or rebuild your project with --no-cache option to avoid cache issue. Your server is up and running, and a Let's Encrypt HTTPS certificate has been automatically generated for you. Go to `https://your-domain-name.example.com` and enjoy! > [!NOTE] The worker mode of FrankenPHP is enabled by default in prod. To disable it, add the env > var FRANKENPHP_CONFIG as empty to the compose.prod.yaml file. ## Disabling HTTPS Alternatively, if you don't want to expose an HTTPS server but only an HTTP one, run the following command: ```console SERVER_NAME=http://localhost \ MERCURE_PUBLIC_URL=http://localhost/.well-known/mercure \ TRUSTED_HOSTS='^localhost|php$' \ APP_SECRET=ChangeMe \ CADDY_MERCURE_JWT_SECRET=ChangeThisMercureHubJWTSecretKey \ docker compose -f -compose.yaml -f compose.prod.yaml up --wait ``` ## Deploying on Multiple Nodes If you want to deploy your app on a cluster of machines, we recommend using [Kubernetes](kubernetes.md). You can use [Docker Swarm](https://docs.docker.com/engine/swarm/stack-deploy/), which is compatible with the provided Compose files. ## Configuring a Load Balancer or a Reverse Proxy Since Caddy 2.5, XFF values of incoming requests will be ignored to prevent spoofing. So if Caddy is not the first server being connected to by your clients (for example when a CDN is in front of Caddy), you may configure `trusted_proxies` with a list of IP ranges (CIDRs) from which incoming requests are trusted to have sent good values for these headers. As a shortcut, `private_ranges` may be configured to trust all private IP ranges. ```diff -php_fastcgi unix//var/run/php/php-fpm.sock +php_fastcgi unix//var/run/php/php-fpm.sock { + trusted_proxies private_ranges +} ``` ## Building Next.js client locally with SSG When deploying API Platform with Docker Compose and you need to build a Next.js client that utilizes Static Site Generation (SSG), a specific setup is required. This setup ensures the Next.js client can access the API at build time to generate static pages. ### Configuration Steps #### 1. Adjust the compose.prod.yaml file Modify the pwa service to ensure network communication between the pwa and php services during the build: ```yaml pwa: build: context: ./pwa target: prod network: host extra_hosts: - php=127.0.0.1 ``` #### 2. Build and start the php service Begin by starting the php service container: ```console SERVER_NAME=http://localhost \ MERCURE_PUBLIC_URL=http://localhost/.well-known/mercure \ TRUSTED_HOSTS='^localhost|php$' \ APP_SECRET=!ChangeMe! \ CADDY_MERCURE_JWT_SECRET=ChangeThisMercureHubJWTSecretKey \ POSTGRES_PASSWORD=!ChangeMe! \ docker compose -f compose.yaml -f compose.prod.yaml up -d --build --wait php ``` #### 3. Optional: Env file with create-client If your are using the [create-client](../create-client/nextjs.md) generator inside your Next.js client, you need to create a `.env` file in the `pwa` directory with the `NEXT_PUBLIC_ENTRYPOINT` environment variable to ensure the Next.js client knows where to find the API: ```dotenv NEXT_PUBLIC_ENTRYPOINT=http://php ``` #### 4. Build the pwa service ```console docker compose -f compose.yaml -f compose.prod.yaml build pwa ``` #### 5. Finally, bring up the full project ```console SERVER_NAME=http://localhost \ MERCURE_PUBLIC_URL=http://localhost/.well-known/mercure \ TRUSTED_HOSTS='^localhost|php$' \ APP_SECRET=!ChangeMe! \ CADDY_MERCURE_JWT_SECRET=ChangeThisMercureHubJWTSecretKey \ POSTGRES_PASSWORD=!ChangeMe! \ docker compose -f compose.yaml -f compose.prod.yaml up -d --wait ``` These steps ensure the Next.js client can statically generate pages by accessing the API during the build process. --- # Deploying to a Kubernetes Cluster Source: https://api-platform.com/docs/deployment/kubernetes/ # Deploying to a Kubernetes Cluster [Kubernetes](https://kubernetes.io/) has become the most popular way to deploy, run and manage containers in production. [Google Cloud Platform](https://cloud.google.com/kubernetes-engine/), [Microsoft Azure](https://azure.microsoft.com/en-us/services/container-service/kubernetes/) and [Amazon Web Services](https://aws.amazon.com/eks/) and many more companies provide managed Kubernetes environment. API Platform can be deployed to any of these platforms with a [Helm](https://helm.sh/) (the k8s package manager) chart. > [!NOTE] > > The Helm chart is no longer bundled with the project scaffolded by the `api-platform` installer. A > reworked chart is in the works. In the meantime, you can use the chart preserved in the `4.1` > release of the distribution: copy the > [`helm/api-platform`](https://github.com/api-platform/api-platform/tree/v4.1.0/helm/api-platform) > directory from that tag into your project, and adapt it to your image names and project layout. > The commands below assume it is available at `./helm/api-platform`. This guide is based on Helm 3. If you want to deploy API Platform on a local Kubernetes cluster, check out [our Minikube tutorial](minikube.md)! ## Preparing Your Cluster and Your Local Machine 1. Create a Kubernetes cluster on your preferred Cloud provider or install Kubernetes locally on your server, for example with [kubeadm](https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/install-kubeadm/) 2. Install [Helm 3](https://helm.sh/) `locally` following their [documentation](https://helm.sh/docs/intro/install/) 3. Be sure to be connected to the right Kubernetes cluster `kubectl config view` [Details](https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/) e.g. for Google Cloud running: `gcloud config get-value core/project` Working-Dir: Your local installation of api-platform. Default /api-platform/ ## Creating and Publishing the Docker Images ### Example with the [Google Container Registry](https://cloud.google.com/container-registry/) and [Google Cloud Platform](https://cloud.google.com/kubernetes-engine/) Change the name "test-api-platform" to your Google project ID (not the project name). [Quickstart Google Cloud](https://cloud.google.com/sdk/docs/quickstart?hl=de) If you do not have gcloud yet, install it with these command. ```console curl https://sdk.cloud.google.com | bash ``` #### 1. Build the PHP and PWA Docker images and tag them Versioning: The 0.1.0 is the version. This value should be the same as the attribute `appVersion` in `Chart.yaml`. Infos for [Google Container pulling and pushing](https://cloud.google.com/container-registry/docs/pushing-and-pulling) ```console docker build -t gcr.io/test-api-platform/php:0.1.0 -t gcr.io/test-api-platform/php:latest api --target frankenphp_prod docker build -t gcr.io/test-api-platform/pwa:0.1.0 -t gcr.io/test-api-platform/pwa:latest pwa --target prod ``` Optional: If your pwa project use Static Site Generation (SSG) and you need to build it against the API running locally, you can build the pwa with the command below. ```console docker build -t gcr.io/test-api-platform/pwa:0.1.0 -t gcr.io/test-api-platform/pwa:latest pwa --target prod --network=host --add-host php=127.0.0.1 ``` #### 2. Push your images to your Docker registry ```console gcloud auth configure-docker docker push gcr.io/test-api-platform/php docker push gcr.io/test-api-platform/pwa ``` Optional: push the version images: ```console docker push gcr.io/test-api-platform/php:0.1.0 docker push gcr.io/test-api-platform/pwa:0.1.0 ``` The result should look similar to these images. ![Example of Google Images - Overview](images/google-image-overview.png) ## Deploying with Helm 3 ### 1. Check the Helm version ```console helm version ``` If you are using version 2.x follow this [guide to migrate Helm to v3](https://helm.sh/docs/topics/v2_v3_migration/#helm) ### 2. Firstly you need to update helm dependencies by running ```console helm dependency update ./helm/api-platform ``` This will create a folder helm/api-platform/charts/ and add all dependencies there. Actual this is [bitnami/PostgreSQL](https://bitnami.com/stack/postgresql/helm), a file postgresql-[VERSION].tgz is created. ### 3. Optional: If you made changes to the Helm chart, check if its format is correct ```console helm lint ./helm/api-platform ``` ### 4. Deploy your API to the container ```console helm upgrade main ./helm/api-platform --namespace=default --create-namespace --wait \ --install \ --set "php.image.repository=gcr.io/test-api-platform/php" \ --set php.image.tag=latest \ --set "pwa.image.repository=gcr.io/test-api-platform/pwa" \ --set pwa.image.tag=latest \ --set php.appSecret='!ChangeMe!' \ --set postgresql.postgresqlPassword='!ChangeMe!' \ --set postgresql.persistence.enabled=true \ --set "corsAllowOrigin=^https?:\/\/[a-z]*\.mywebsite.com$" ``` The `"` are necessary for Windows. Use ^ on Windows instead of \ to split commands into multiple lines. You can add the parameter `--dry-run` to check upfront if anything is correct. Replace the values with the image parameters from the stage above. The parameter `php.appSecret` is the `AppSecret` from ./.env Fill the rest of the values with the correct settings. For available options see /helm/api-platform/values.yaml. If you want a test deploy you can set corsAllowOrigin='\*' After a successful installation, there is a message at the end. You can copy these commands and execute them to set a port-forwarding and get access on your local machine to the deploy. See image below. ![Deploy Result](images/deploy-result.png) If you prefer to use a managed DBMS like [Heroku Postgres](https://www.heroku.com/postgres) or [Google Cloud SQL](https://cloud.google.com/sql/docs/postgres/) (recommended): ```console helm upgrade api-platform ./helm/api-platform \ # ... --set postgresql.enabled=false \ --set postgresql.url=pgsql://username:password@host/database?serverVersion=13 ``` Finally, build the `pwa` (client and admin) JavaScript apps and [deploy them on a static site hosting service](https://create-react-app.dev/docs/deployment/). ## Access the container You can access the php container of the pod with the following command. In this example the symfony console is called. ```console CADDY_PHP_POD=$(kubectl --namespace=default get pods -l app.kubernetes.io/name=api-platform -o jsonpath="{.items[0].metadata.name}") kubectl --namespace=default exec -it $CADDY_PHP_POD -c api-platform-php -- bin/console ``` ## Caution for system architecture If the pods do not run, and you get the following error from google Kubernetes engine logs, there is probably a problem with the system architecture. `standard_init_linux.go:211: exec user process caused "exec format error` Build the images with the same system architecture as the cluster runs. Example: Building with Mac M1 with arm64 leads to problems. Most cluster will run with x86_64. Solution: [https://blog.jaimyn.dev/how-to-build-multi-architecture-docker-images-on-an-m1-mac](https://blog.jaimyn.dev/how-to-build-multi-architecture-docker-images-on-an-m1-mac) ## Updates There are 2 main upgrade strategies. ### 1. Always version your images (recommended) Change the version in the attribute "appVersion" in Chart.yaml and tag the images with this version. You can upgrade with the same command from the installation and pass all parameters. ### 2. Use :latest tags Infos about [best practices for tagging images for Kubernetes](https://kubernetes.io/docs/concepts/containers/images/) You have to use the \*.image.pullPolicy=Always see the last 3 parameters. ```console PHP_POD=$(kubectl --namespace=bar get pods -l app=php -o jsonpath="{.items[0].metadata.name}") kubectl --namespace=bar exec -it $PHP_POD -- bin/console doctrine:schema:create helm upgrade api-platform ./helm/api-platform --namespace=default \ --set "php.image.repository=gcr.io/test-api-platform/php" \ --set php.image.tag=latest \ --set "pwa.image.repository=gcr.io/test-api-platform/pwa" \ --set pwa.image.tag=latest \ --set php.appSecret='!ChangeMe!' \ --set postgresql.postgresqlPassword='!ChangeMe!' \ --set postgresql.persistence.enabled=true \ --set "corsAllowOrigin=^https?://[a-z\]*\.mywebsite.com$" \ --set php.image.pullPolicy=Always \ --set pwa.image.pullPolicy=Always ``` ## GitHub Actions Example for deployment You can find a [complete deploy command for GKE](https://github.com/api-platform/demo/blob/4.1/.github/workflows/cd.yml) on the [demo project](https://github.com/api-platform/demo/): ## Symfony Messenger Running Pods with the Messenger Component to consume queues requires additions to the Helm chart. Start by creating a new template for the queue-worker-deployment. The `deployment.yaml` can be used as template, the caddy container and all unused ENV variables should be removed. Add the following lines under `containers` to overwrite the command. ```yaml command: {{ range .Values.queue_worker.command }} - {{ . | quote }} {{ end }} args: {{ range .Values.queue_worker.commandArgs }} - {{ . | quote }} {{ end }} ``` Here is an example on how to use it from your `values.yaml`: ```yaml command: ["bin/console"] commandArgs: ["messenger:consume", "async", "--memory-limit=100M"] ``` The `readinessProbe` and the `livenessProbe` can not use the default `docker-healthcheck` but should test if the command is running. First, make sure to install the `/bin/ps` binary, otherwise the `readinessProbe` and `livenessProbe` will fail: ```patch # api/Dockerfile RUN apt-get update && apt-get install --no-install-recommends -y \ + procps \ # ... ``` Then, update the probes: ```yaml readinessProbe: exec: command: ["/bin/sh", "-c", "/usr/bin/pgrep -af '^php.*bin/console.*messenger:consume'"] initialDelaySeconds: 120 periodSeconds: 3 livenessProbe: exec: command: ["/bin/sh", "-c", "/usr/bin/pgrep -af '^php.*bin/console.*messenger:consume'"] initialDelaySeconds: 120 periodSeconds: 3 ``` --- # Deploying to minikube Source: https://api-platform.com/docs/deployment/minikube/ # Deploying to minikube ## Install minikube If you have no existing installation of minikube on your computer, [follow the official tutorial](https://minikube.sigs.k8s.io/docs/start/). When Minikube is installed, start the cluster: ```console minikube start --addons registry --addons dashboard ``` The previous command starts minikube with a Docker registry (we'll use it in the next step) and with the Kubernetes dashboard. Finally, [install Helm](https://helm.sh/docs/intro/install/). We'll use it to deploy the application in the cluster thanks to a Helm chart (see the [Kubernetes guide](kubernetes.md) for how to obtain it). ## Building and Pushing Docker Images On GNU/Linux and macOS, run the following command to point your terminal's docker-cli to the Docker Engine inside minikube: ```console eval $(minikube docker-env) ``` Now any `docker` command you run in this current terminal will run against the Docker Engine inside the minikube cluster. For detailed explanation and instructions for Windows [visit official minikube documentation](https://minikube.sigs.k8s.io/docs/handbook/pushing/#1-pushing-directly-to-the-in-cluster-docker-daemon-docker-env). Build the images in minikube: ```console docker build -t localhost:5000/php api --target frankenphp_prod docker build -t localhost:5000/pwa pwa --target prod ``` Then push the images in the registry available in minikube: ```console docker push localhost:5000/php docker push localhost:5000/pwa ``` ## Deploying Fetch Helm chart dependencies: ```console helm repo add bitnami https://charts.bitnami.com/bitnami/ helm dependency build helm/api-platform ``` Finally, deploy the project using the Helm chart: ```console helm upgrade --install my-project helm/api-platform \ --set php.image.repository=localhost:5000/php \ --set php.image.tag=latest \ --set pwa.image.repository=localhost:5000/pwa \ --set pwa.image.tag=latest ``` Copy and paste the commands displayed in the terminal to enable the port forwarding then go to `http://localhost:8080` to access your application! Run `minikube dashboard` at any moment to see the state of your deployments. ## Using Skaffold Skaffold is a tool for Kubernetes development: [https://skaffold.dev/](https://skaffold.dev/). It will build and deploy automatically your app in Kubernetes and apply every changes. The default configuration use minikube and helm. More configurations are available in Skaffold documentation. First, install the [skaffold CLI](https://skaffold.dev/docs/install/#standalone-binary). Then, run minikube: ```console minikube start ``` Add Skaffold configuration in the file `./helm/skaffold.yaml`. You can find a [complete configuration file for minikube](https://github.com/api-platform/api-platform/blob/main/helm/skaffold.yaml) with its [Helm values override](https://github.com/api-platform/api-platform/blob/main/helm/skaffold-values.yaml). Finally, go to the helm folder, and run skaffold in dev mode: ```console cd ./helm skaffold dev ``` --- # Deploying an API Platform App on Heroku Source: https://api-platform.com/docs/deployment/heroku/ # Deploying an API Platform App on Heroku [Heroku](https://www.heroku.com) is a popular, fast, scalable and reliable _Platform As A Service_ (PaaS). As Heroku offers a free plan including database support through [Heroku Postgres](https://www.heroku.com/postgres), it's a convenient way to experiment with API Platform. The API Platform Heroku integration also supports MySQL databases provided by [the ClearDB add-on](https://addons.heroku.com/cleardb). Deploying API Platform applications on Heroku is straightforward and you will learn how to do it in this tutorial. _Note: this tutorial works perfectly well with API Platform but also with any Symfony application based on the Symfony Standard Edition._ If you don't already have one, [create an account on Heroku](https://signup.heroku.com/signup/dc). Then install [the Heroku toolbelt](https://devcenter.heroku.com/articles/getting-started-with-php#set-up). We're guessing you already have a working install of [Composer](https://getcomposer.org/). Perfect, we will need it. Create a new [API Platform Symfony project](symfony/index.md) which will be used in the rest of this example. Heroku relies on [environment variables](https://devcenter.heroku.com/articles/config-vars) for its configuration. Regardless of what provider you choose for hosting your application, using environment variables to configure your production environment is a best practice promoted by API Platform. Create a Heroku `app.json` file at the root of the `api/` directory to configure the deployment: ```json { "success_url": "/", "env": { "APP_ENV": "prod", "APP_SECRET": { "generator": "secret" }, "CORS_ALLOW_ORIGIN": "https://your-client-url.com" }, "addons": ["heroku-postgresql"], "buildpacks": [ { "url": "https://github.com/heroku/heroku-buildpack-php" } ], "scripts": { "postdeploy": "php bin/console doctrine:schema:create" } } ``` The file also tells the Heroku deployment system to build a PHP container and to add the Postgres add-on. We are almost done, but API Platform (and Symfony) has a particular directory structure which requires further configuration. We must tell Heroku that the document root is `public/`, and that all other directories must be private. Create a new file named `Procfile` in the `api/` directory with the following content: ```yaml web: vendor/bin/heroku-php-apache2 public/ ``` Be sure to add the Apache Pack to your dependencies: ```console composer require symfony/apache-pack ``` As Heroku doesn't support Varnish out of the box, let's disable its integration: ```diff # api/config/packages/api_platform.yaml - http_cache: - invalidation: - enabled: true - varnish_urls: ['%env(VARNISH_URL)%'] - max_age: 0 - shared_max_age: 3600 - vary: ['Content-Type', 'Authorization', 'Origin'] - public: true ``` Heroku provides another free service, [Logplex](https://devcenter.heroku.com/articles/logplex), which allows us to centralize and persist application logs. Because API Platform writes logs on `STDERR`, it will work seamlessly. However, if you use Monolog instead of the default logger, you'll need to configure it to output to `STDERR` instead of in a file. Open `api/config/packages/prod/monolog.yaml` and apply the following patch: ```diff handlers: nested: type: stream - path: "%kernel.logs_dir%/%kernel.environment%.log" + path: php://stderr level: debug ``` We are now ready to deploy our app! Go to the `api/` directory, then 1. Initialize a Git repository: ```console git init ``` 1. Add all existing files: ```console git add --all ``` 1. Commit: ```console git commit -a -m "My first API Platform app running on Heroku!" ``` 1. Create the Heroku application: ```console heroku create ``` 1. And deploy for the first time: ```console git push heroku master ``` **We're done.** You can play with the demo API provided with API Platform. It is ready for production and you can scale it in one click from the Heroku interface. To see your logs, run `heroku logs --tail`. --- # Implement Træfik Into API Platform Dockerized Source: https://api-platform.com/docs/deployment/traefik/ # Implement Træfik Into API Platform Dockerized > An open-source reverse proxy and load balancer for HTTP and TCP-based applications that is easy, > dynamic, automatic, fast, full-featured, production proven, provides metrics and integrates with > every major cluster technology. > > —[https://traefik.io](https://traefik.io) ## Basic Implementation This tutorial will help you to define your own routes for your client, API and more generally for your containers. Use this custom API Platform `compose.yaml` file which implements ready-to-use Træfik container configuration. Override ports and add labels to tell Træfik to listen on the routes mentioned and redirect routes to a specified container. A few points to note: - `--api.insecure=true` Tells Træfik to generate a browser view to watch containers and IP/DNS associated easier - `--providers.docker` Tells Træfik to listen on Docker API - `labels:` Key for Træfik configuration into Docker integration ```yaml services: # ... api: labels: - traefik.http.routers.api.rule=Host(`api.localhost`) ``` The API DNS will be specified with ``traefik.http.routers.api.rule=Host(`your.host`)`` (here api.localhost) - `--traefik.routers.clientloadbalancer.server.port=3000` The port specified to Træfik will be exposed by the container (here the React app exposes the 3000 port), but if your container exposes only one port, it can be ignored We assume that you've generated a SSL `localhost.crt` and associated `localhost.key` combo under `./certs` folder Then you edited your `admin/Dockerfile` and `client/Dockerfile` like this: ```diff -ENV HTTPS true +EXPOSE 3000 ``` After that, don't forget to re-build your containers ```yaml # compose.yaml version: "3.4" x-cache-from: - &cache cache_from: - ${NGINX_IMAGE:-quay.io/api-platform/nginx} - ${PHP_IMAGE:-quay.io/api-platform/php} services: traefik: image: traefik:latest command: --api.insecure=true --providers.docker ports: - target: 80 published: 80 protocol: tcp - target: 443 published: 443 protocol: tcp - target: 8080 published: 8080 protocol: tcp volumes: - /var/run/docker.sock:/var/run/docker.sock php: build: context: ./api target: api_platform_php <<: *cache image: ${PHP_IMAGE:-quay.io/api-platform/php} healthcheck: interval: 10s timeout: 3s retries: 3 start_period: 30s depends_on: - db volumes: - ./api:/srv/api:rw,cached - ./api/docker/php/conf.d/api-platform.dev.ini:/usr/local/etc/php/conf.d/api-platform.ini api: build: context: ./api target: api_platform_nginx <<: *cache image: ${NGINX_IMAGE:-quay.io/api-platform/nginx} depends_on: - php volumes: - ./api/public:/srv/api/public:ro vulcain: image: dunglas/vulcain environment: - CERT_FILE=/certs/localhost.crt - KEY_FILE=/certs/localhost.key - UPSTREAM=http://api depends_on: - api volumes: - ./certs:/certs:ro labels: - traefik.http.routers.vulcain.rule=Host(`vulcain.localhost`) db: image: postgres:12-alpine environment: - POSTGRES_DB=api - POSTGRES_PASSWORD=!ChangeMe! - POSTGRES_USER=api-platform volumes: - db-data:/var/lib/postgresql/data:rw labels: - traefik.http.routers.db.rule=Host(`db.localhost`) mercure: image: dunglas/mercure environment: # - ACME_HOSTS=${DOMAIN_NAME} # - CERT_FILE=/certs/localhost.crt # - KEY_FILE=/certs/localhost.key - JWT_KEY=${JWT_KEY} - ALLOW_ANONYMOUS=1 - USE_FORWARDED_HEADERS=true - CORS_ALLOWED_ORIGINS=* - READ_TIMEOUT=0s - WRITE_TIMEOUT=0s - PUBLISH_ALLOWED_ORIGINS=* volumes: - ./certs:/certs:ro labels: - traefik.http.routers.mercure.rule=Host(`mercure.localhost`) client: build: context: ./client target: api_platform_client_development cache_from: - ${CLIENT_IMAGE:-quay.io/api-platform/client} image: ${CLIENT_IMAGE:-quay.io/api-platform/client} tty: true environment: - API_PLATFORM_CLIENT_GENERATOR_ENTRYPOINT=http://api - API_PLATFORM_CLIENT_GENERATOR_OUTPUT=src volumes: - ./client:/usr/src/client:rw,cached labels: - traefik.http.routers.client.rule=Host(`client.localhost`) - traefik.http.services.client.loadbalancer.server.port=3000 admin: build: context: ./admin target: api_platform_admin_development cache_from: - ${ADMIN_IMAGE:-quay.io/api-platform/admin} image: ${ADMIN_IMAGE:-quay.io/api-platform/admin} tty: true volumes: - ./admin:/usr/src/admin:rw,cached labels: - traefik.http.routers.admin.rule=Host(`admin.localhost`) - traefik.http.services.admin.loadbalancer.server.port=3000 volumes: db-data: {} ``` Don't forget the db-data, or the database won't work in this dockerized solution. `localhost` is a reserved domain referred to in your `/etc/hosts`. If you want to implement custom DNS such as production DNS in local, just add them at the end of your `/etc/host` file like that: ```csv # /etc/hosts # ... 127.0.0.1 your.domain.com ``` If you do that, you'll have to update the `CORS_ALLOW_ORIGIN` environment variable `api/.env` to accept the specified URL. ## Known Issues If your network is of type B, it may conflict with the Træfik sub-network. ## Going Further As this Træfik configuration listens on 80 and 443 ports, you can run only 1 Træfik instance per server. However, you may want to run multiple API Platform projects on the same server. To deal with it, you'll have to externalize the Træfik configuration to another `compose.yaml` file, anywhere on your server. Here is a working example: ```yaml # /somewhere/compose.yaml version: "3.4" services: traefik: image: traefik:latest command: --api.insecure=true --providers.docker ports: - target: 80 published: 80 protocol: tcp - target: 443 published: 443 protocol: tcp - target: 8080 published: 8080 protocol: tcp volumes: - /var/run/docker.sock:/var/run/docker.sock networks: - api_platform_network # Add other networks here networks: api_platform_network: external: true # Add other networks here ``` Then update the `compose.yaml` file belonging to your API Platform projects: ```diff # /anywhere/api-platform/compose.yaml version: '3.4' x-cache: &cache cache_from: - ${CONTAINER_REGISTRY_BASE}/php - ${CONTAINER_REGISTRY_BASE}/nginx - ${CONTAINER_REGISTRY_BASE}/varnish +x-network: + &network + networks: + - api_platform_network services: - traefik: - image: traefik:latest - command: --api.insecure=true --providers.docker - ports: - - target: 80 - published: 80 - protocol: tcp - - target: 443 - published: 443 - protocol: tcp - - target: 8080 - published: 8080 - protocol: tcp - volumes: - - /var/run/docker.sock:/var/run/docker.sock php: build: context: ./api target: api_platform_php <<: *cache image: ${PHP_IMAGE:-quay.io/api-platform/php} + environment: + # You should remove these variables from .env into api folder + - TRUSTED_HOSTS=^(((${SUBDOMAINS_LIST}\.)?${DOMAIN_NAME})|api)$$ + - CORS_ALLOW_ORIGIN=^${HTTP_OR_SSL}(${SUBDOMAINS_LIST}.)?${DOMAIN_NAME}$$ + - DATABASE_URL=postgres://${DB_USER}:${DB_PASS}@db/${DB_NAME} + - MERCURE_SUBSCRIBE_URL=${HTTP_OR_SSL}mercure.${DOMAIN_NAME}$$ + - MERCURE_PUBLISH_URL=${HTTP_OR_SSL}mercure.${DOMAIN_NAME}$$ + - MERCURE_JWT_TOKEN=${JWT_KEY} healthcheck: interval: 10s timeout: 3s retries: 3 start_period: 30s depends_on: - db - - dev-tls volumes: - ./api:/srv/api:rw,cached - ./api/docker/php/conf.d/api-platform.dev.ini:/usr/local/etc/php/conf.d/api-platform.ini - ./certs:/certs:ro + <<: *network api: build: context: ./api target: api_platform_nginx <<: *cache image: ${NGINX_IMAGE:-quay.io/api-platform/nginx} depends_on: - php volumes: - ./api/public:/srv/api/public:ro labels: - traefik.http.routers.api.rule=Host(`api.${DOMAIN_NAME}`) + <<: *network vulcain: image: dunglas/vulcain environment: - CERT_FILE=/certs/localhost.crt - KEY_FILE=/certs/localhost.key - UPSTREAM=http://api depends_on: - api - - dev-tls volumes: - ./certs:/certs:ro labels: - traefik.http.routers.vulcain.rule=Host(`vulcain.${DOMAIN_NAME}`) + <<: *network db: image: postgres:12-alpine environment: - POSTGRES_DB=${DB_NAME} - POSTGRES_PASSWORD=${DB_PASS} - POSTGRES_USER=${DB_USER} volumes: - db-data:/var/lib/postgresql/data:rw labels: - traefik.http.routers.db.rule=Host(`db.${DOMAIN_NAME}`) + <<: *network mercure: image: dunglas/mercure environment: # - ACME_HOSTS=${DOMAIN_NAME} # - CERT_FILE=/certs/localhost.crt # - KEY_FILE=/certs/localhost.key - JWT_KEY=${JWT_KEY} - ALLOW_ANONYMOUS=1 - USE_FORWARDED_HEADERS=true - CORS_ALLOWED_ORIGINS=* - READ_TIMEOUT=0s - WRITE_TIMEOUT=0s - PUBLISH_ALLOWED_ORIGINS=* - depends_on: - - dev-tls volumes: - ./certs:/certs:ro labels: - traefik.http.routers.mercure.rule=Host(`mercure.${DOMAIN_NAME}`) + <<: *network client: build: context: ./client target: api_platform_client_development cache_from: - ${CLIENT_IMAGE:-quay.io/api-platform/client} image: ${CLIENT_IMAGE:-quay.io/api-platform/client} tty: true environment: - API_PLATFORM_CLIENT_GENERATOR_ENTRYPOINT=http://api - API_PLATFORM_CLIENT_GENERATOR_OUTPUT=src + # You should remove this variable from .env into client folder + - REACT_APP_API_ENTRYPOINT=${HTTP_OR_SSL}api.${DOMAIN_NAME} - depends_on: - - dev-tls volumes: - ./client:/usr/src/client:rw,cached expose: - 3000 labels: - traefik.http.routers.client.rule=Host(`client.${DOMAIN_NAME}`) - traefik.http.services.client.loadbalancer.server.port=3000 + <<: *network admin: build: context: ./admin target: api_platform_admin_development cache_from: - ${ADMIN_IMAGE:-quay.io/api-platform/admin} image: ${ADMIN_IMAGE:-quay.io/api-platform/admin} tty: true - depends_on: - - dev-tls + environment: + # You should remove this variable from .env into client folder + - REACT_APP_API_ENTRYPOINT=${HTTP_OR_SSL}api.${DOMAIN_NAME} volumes: - ./admin:/usr/src/admin:rw,cached expose: - 3000 labels: - traefik.http.routers.admin.rule=Host(`admin.${DOMAIN_NAME}`) - traefik.http.services.admin.loadbalancer.server.port=3000 + <<: *network volumes: db-data: {} +networks: + api_platform_network: + external: true ``` Finally, some environment variables must be defined, here is an example of a `.env` file to set them: ```dotenv CONTAINER_REGISTRY_BASE=quay.io/api-platform DOMAIN_NAME=localhost HTTP_OR_SSL=https:// DB_NAME=api-platform-db-name DB_PASS=YouMustChangeThisPassword DB_USER=api-platform JWT_KEY=!UnsecureChangeMe! SUBDOMAINS_LIST=(admin|api|mercure) ``` This way, you can configure your main variables into one single file. ## Multiple Instances If you want to run multiple API Platform instances on the same server and behind only one Træfik instance, you'll have to define different service names for each service to avoid named conflicts error since Træfik v2.0. To achieve that, by setting only one more environment variable, you'll be able to make each instance unique. Here is a working example below: ```dotenv # /anywhere/first/api-platform/.env #... RANDOM_UNIQUE_KEY=yourUniqueKeyForYourFirstInstance ``` ```dotenv # /anywhere/second/api-platform/.env #... RANDOM_UNIQUE_KEY=yourUniqueKeyForYourSecondInstance ``` Then update each traefik http routers names and services following this sample for admin ```yaml # /anywhere/first/api-plaform/compose.yaml # ... labels: - traefik.http.routers.admin-${RANDOM_UNIQUE_KEY}.rule=Host(`admin.${DOMAIN_NAME}`) - traefik.http.services.admin-${RANDOM_UNIQUE_KEY}.loadbalancer.server.port=3000 ``` ## More Generic Approach Here is a fully working sample for Træfik generic config with a little script using docker-compose override approach. We assume that you've set `EXPOSE 3000` in your client and admin Dockerfile. Create a new `init-dc.sh` which contains the generation code that will be written in `compose.override.yaml` file. ```console #!/bin/sh # /anywhere/api-platform/init-dc.sh services=("admin:admin." "api:api." "mercure:mercure.") # Define your services keys following this format: "{container key}:{sub DNS}". To define root DNS write nothing after the colon text="version: '3.4' services:" for k in "${services[@]}" ; do key=${k%%:*} value=${k#*:} text+=" $key: labels: - traefik.http.routers.$key-\${RANDOM_UNIQUE_KEY}.entrypoints=web-secure - traefik.http.routers.$key-\${RANDOM_UNIQUE_KEY}.rule=Host(\`${value}\${DOMAIN_NAME}\`) - traefik.http.routers.$key-\${RANDOM_UNIQUE_KEY}.tls=true - traefik.http.routers.$key-\${RANDOM_UNIQUE_KEY}.tls.domains[0].main=\${DOMAIN_NAME} - traefik.http.routers.$key-\${RANDOM_UNIQUE_KEY}.tls.domains[0].sans=*.\${DOMAIN_NAME} - traefik.http.routers.$key-\${RANDOM_UNIQUE_KEY}.tls.certresolver=sample " done echo "$text" > ./compose.override.yaml ``` Write this minimal configuration into your `traefik.toml` file: ```toml # /anywhere/traefik/traefik.toml [providers.docker] endpoint = "unix:///var/run/docker.sock" [api] insecure = true dashboard = true debug = true [entryPoints.web] address = ":80" [entryPoints.web.http] [entryPoints.web.http.redirections] [entryPoints.web.http.redirections.entryPoint] to = "web-secure" scheme = "https" [entryPoints.web-secure] address = ":443" ``` Then after that update respectively your API Platform and Træfik `compose.yaml` following these examples below. ```yaml # /anywhere/api-platform/compose.yaml version: "3.4" x-cache: &cache cache_from: - ${CONTAINER_REGISTRY_BASE}/php - ${CONTAINER_REGISTRY_BASE}/nginx - ${CONTAINER_REGISTRY_BASE}/varnish x-network: &network networks: - api_platform_network services: php: build: context: ./api target: api_platform_php <<: *cache image: ${PHP_IMAGE:-quay.io/api-platform/php} environment: # You should remove these variables from .env into api folder - TRUSTED_HOSTS=^(((${SUBDOMAINS_LIST}\.)?${DOMAIN_NAME})|api)$$ - CORS_ALLOW_ORIGIN=^${HTTP_OR_SSL}(${SUBDOMAINS_LIST}.)?${DOMAIN_NAME}$$ - DATABASE_URL=postgres://${DB_USER}:${DB_PASS}@db/${DB_NAME} - MERCURE_SUBSCRIBE_URL=http://mercure/.well-known/mercure - MERCURE_PUBLISH_URL=http://mercure/.well-known/mercure - MERCURE_JWT_TOKEN=${JWT_KEY} healthcheck: interval: 10s timeout: 3s retries: 3 start_period: 30s depends_on: - db volumes: - ./api:/srv/api:rw,cached - ./api/docker/php/conf.d/api-platform.dev.ini:/usr/local/etc/php/conf.d/api-platform.ini - ./certs:/certs:ro <<: *network api: build: context: ./api target: api_platform_nginx <<: *cache image: ${NGINX_IMAGE:-quay.io/api-platform/nginx} depends_on: - php volumes: - ./api/public:/srv/api/public:ro <<: *network vulcain: image: dunglas/vulcain environment: - CERT_FILE=/certs/localhost.crt - KEY_FILE=/certs/localhost.key - UPSTREAM=http://api depends_on: - api volumes: - ./certs:/certs:ro <<: *network db: image: postgres:12-alpine environment: - POSTGRES_DB=${DB_NAME} - POSTGRES_PASSWORD=${DB_PASS} - POSTGRES_USER=${DB_USER} volumes: - db-data:/var/lib/postgresql/data:rw <<: *network mercure: image: dunglas/mercure environment: # - ACME_HOSTS=${DOMAIN_NAME} # - CERT_FILE=/certs/localhost.crt # - KEY_FILE=/certs/localhost.key - JWT_KEY=${JWT_KEY} - ALLOW_ANONYMOUS=1 - USE_FORWARDED_HEADERS=true - CORS_ALLOWED_ORIGINS=* - READ_TIMEOUT=0s - WRITE_TIMEOUT=0s - PUBLISH_ALLOWED_ORIGINS=* volumes: - ./certs:/certs:ro <<: *network client: build: context: ./client target: api_platform_client_development cache_from: - ${CLIENT_IMAGE:-quay.io/api-platform/client} image: ${CLIENT_IMAGE:-quay.io/api-platform/client} tty: true environment: - API_PLATFORM_CLIENT_GENERATOR_ENTRYPOINT=http://api - API_PLATFORM_CLIENT_GENERATOR_OUTPUT=src # You should remove this variable from .env into client folder - REACT_APP_API_ENTRYPOINT=${HTTP_OR_SSL}api.${DOMAIN_NAME} volumes: - ./client:/usr/src/client:rw,cached <<: *network admin: build: context: ./admin target: api_platform_admin_development cache_from: - ${ADMIN_IMAGE:-quay.io/api-platform/admin} image: ${ADMIN_IMAGE:-quay.io/api-platform/admin} tty: true environment: # You should remove this variable from .env into client folder - REACT_APP_API_ENTRYPOINT=${HTTP_OR_SSL}api.${DOMAIN_NAME} volumes: - ./admin:/usr/src/admin:rw,cached <<: *network volumes: db-data: {} networks: api_platform_network: external: true ``` ```yaml # /anywhere/traefik/compose.yaml version: "3.4" x-network: &network networks: - api_platform_network services: traefik: image: traefik:latest ports: - target: 80 published: 80 protocol: tcp - target: 443 published: 443 protocol: tcp - target: 8080 published: 8080 protocol: tcp volumes: - /var/run/docker.sock:/var/run/docker.sock - ./traefik.toml:/etc/traefik/traefik.toml - ./acme.json:/acme.json <<: *network networks: api_platform_network: external: true ``` For a more detailed step-by-step configuration, take a look at [this repository](https://github.com/darkweak/WorkshopContainous) which include Fail2ban link to Træfik instance. --- # API Platform’s Philosophy Source: https://api-platform.com/docs/extra/philosophy/ # API Platform's Philosophy In 25 years of PHP, the web changed dramatically and is now evolving faster than ever: - Thanks to awesome frontend technologies such as [React](https://reactjs.org/) or [Vue.js](https://vuejs.org/), [full-JavaScript Progressive Web Apps](https://en.wikipedia.org/wiki/Progressive_web_application) **are becoming the standard**. - [Internet users spend more time on their mobile devices than on desktops](https://www.broadbandsearch.net/blog/mobile-desktop-internet-usage-statistics): having a mobile-first website is mandatory and **native mobile apps are a must have**. - [The semantic web](https://en.wikipedia.org/wiki/Semantic_Web) and **especially [Linked Data](https://en.wikipedia.org/wiki/Linked_data) is a reality**: with the [Schema.org](https://schema.org/) initiative and new open web standards such as [JSON-LD](https://json-ld.org/), search engines (among a bunch of other services and software) consume structured and machine-readable data at web scale. Not exposing such data decrease interoperability and search engine ranking/efficiency (think rich snippets). - HTTP/2 and HTTP/3 [dramatically improve the performance of web applications](https://vulcain.rocks) thanks to multiplexing, Server Push and their other new capabilities. [PHP.net](https://www.php.net), [Symfony](https://symfony.com), [Facebook](https://hhvm.com/) and many others have worked hard to improve and professionalize the PHP ecosystem. The PHP world has closed the gap with most backend solutions and is often [more innovative](https://wiki.php.net/rfc) and [faster](https://benchmarksgame-team.pages.debian.net/benchmarksgame/fastest/php-python3.html) than them. However in critical areas I've described previously, many things can be improved. Almost all existing solutions are still [designed and documented](https://symfony.com/doc/current/book/page_creation.html) to create websites the old way: a server generates then sends plain-old HTML documents to browsers. [API Platform](https://api-platform.com) is a set of tools for building modern web projects. It is a framework for API-first projects built on top of [Symfony components](https://symfony.com/projects/apiplatform). Like other modern frameworks such as Laravel and Symfony, it's both a full-stack all-in-one framework and a set of independent PHP components and bundles that can be used separately. API Platform makes modern development easy and fun again: - [Start by **creating a web API**](../symfony/index.md) exposing structured data that can be understood by any compliant client such as your apps but also search engines (JSON-LD with Schema.org vocabulary). This API is the central and unique entry point to access and modify data. It also encapsulates the whole business logic. - [Then **create as many clients as you want using frontend technologies you love**](../create-client/index.md): a JavaScript webapp built with React or with Vue querying the API but also a native iOS or Android app, or even a desktop application. Clients only display data and forms. See also [the general design](../core/design.md) of the framework. --- # The Release Process Source: https://api-platform.com/docs/extra/releases/ # The Release Process API Platform follows the [Semantic Versioning](https://semver.org) strategy. A new minor version is released every six months, and a new major version is released every two years, along with a last minor version on the previous major one with the same features and an upgrade path. For example: - version 3.0 has been released on 15 September 2022; - version 3.1 has been released on 23 January 2023; - version 3.2 has been released on 12 October 2023; - version 3.3 has been released on 9 April 2024 (we were a little late, it should have been published in March); - versions 3.4 has been released on 18 September 2024; - versions 4.0 has been released on 27 September 2024; - versions 4.1 has been released on 28 February 2025; - versions 4.2 has been released on 18 September 2025; - versions 4.3 has been released on 13 March 2026; ## Maintenance 3 versions are maintained at the same time: - **stable** (currently the **4.3** branch): regular bugfixes are integrated in this version - **old-stable** (are the last branch: **4.2**): [security fixes](security.md) are integrated in this version, regular bugfixes are **not** backported in it - **development** (**main** branch): new features target this branch Older versions (1.x, 2.6..., 3.0..., 4.0, 4.1) **are not maintained**. If you still use them, you must upgrade as soon as possible. The **old-stable** branch is merged in the **stable** branch on a regular basis to propagate [security fixes](security.md). The **stable** branch is merged in the **development** branch on a regular basis to propagate [security](security.md) and regular bugfixes. New major versions of API Platform are released every 2 years. New minor versions of API Platform are released every 6 months. The latest minor version of a major branch contains all the new features introduced in the first version of the next major, but also contains deprecated features which are removed in the next major branch. --- # API Platform for Enterprise Source: https://api-platform.com/docs/extra/enterprise/ # API Platform for Enterprise API Platform is available as part of [the Tidelift Subscription](https://tidelift.com/subscription/pkg/packagist-api-platform-core?utm_source=packagist-api-platform-core&utm_medium=referral&utm_campaign=enterprise). [Tidelift](https://tidelift.com/subscription/pkg/packagist-api-platform-core?utm_source=packagist-api-platform-core&utm_medium=referral&utm_campaign=enterprise) is working with the maintainers of API Platform and thousands of other open source projects to deliver commercial support and maintenance for the open source dependencies you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact dependencies you use. - [Learn more](https://tidelift.com/subscription/pkg/packagist-api-platform-core?utm_source=packagist-api-platform-core&utm_medium=referral&utm_campaign=enterprise) - [Request a demo](https://tidelift.com/subscription/request-a-demo?utm_source=packagist-api-platform-core&utm_medium=referral&utm_campaign=enterprise) ## Enterprise-ready open source software—managed for you [The Tidelift Subscription](https://tidelift.com/subscription/pkg/packagist-api-platform-core?utm_source=packagist-api-platform-core&utm_medium=referral&utm_campaign=enterprise) is a managed open source subscription for application dependencies covering millions of open source projects across JavaScript, Python, Java, PHP, Ruby, .NET, and more. Your subscription includes: - **Security updates**: Tidelift’s security response team coordinates patches for new breaking security vulnerabilities and alerts immediately through a private channel, so your software supply chain is always secure. - **Licensing verification and indemnification**: Tidelift verifies license information to enable easy policy enforcement and adds intellectual property indemnification to cover creators and users in case something goes wrong. You always have a 100% up-to-date bill of materials for your dependencies to share with your legal team, customers, or partners. - **Maintenance and code improvement**: Tidelift ensures the software you rely on keeps working as long as you need it to work. Your managed dependencies are actively maintained and we recruit additional maintainers where required. - **Package selection and version guidance**: We help you choose the best open source packages from the start—and then guide you through updates to stay on the best releases as new issues arise. - **Roadmap input**: Take a seat at the table with the creators behind the software you use. Tidelift’s participating maintainers earn more income as their software is used by more subscribers, so they’re interested in knowing what you need. - **Tooling and cloud integration**: Tidelift works with GitHub, GitLab, BitBucket, and more. We support every cloud platform (and other deployment targets, too). The end result? All of the capabilities you expect from commercial-grade software, for the full breadth of open source you use. That means less time grappling with esoteric open source trivia, and more time building your own applications—and your business. - [Learn more](https://tidelift.com/subscription/pkg/packagist-api-platform-core?utm_source=packagist-api-platform-core&utm_medium=referral&utm_campaign=enterprise) - [Request a demo](https://tidelift.com/subscription/request-a-demo?utm_source=packagist-api-platform-core&utm_medium=referral&utm_campaign=enterprise) --- # Security Policy Source: https://api-platform.com/docs/extra/security/ # Security Policy This document explains how API Platform security issues are handled by the API Platform core team (API Platform being the code hosted in [the `api-platform` GitHub organization](https://github.com/api-platform)). ## Reporting a Security Issue If you think that you have found a security issue in API Platform, don't use the bug tracker and don't publish it publicly. Instead, all security issues must be sent to kevin+api-platform-security [at] dunglas.fr. ## Resolving Process For each report, we first try to confirm the vulnerability. When it is confirmed, the core team works on a solution following these steps: 1. Send an acknowledgment to the reporter; 2. Work on a patch; 3. Get a CVE identifier from [mitre.org](https://cveform.mitre.org); 4. Send the patch to the reporter for review; 5. Apply the patch to all [maintained versions](releases.md) of API Platform; 6. Package new versions for all affected versions; 7. If the affected package is written in PHP, update the public [security advisories database](https://github.com/FriendsOfPHP/security-advisories) maintained by the FriendsOfPHP organization and which is used by the `check:security` command. While we are working on a patch, please do not reveal the issue publicly. The resolution takes anywhere between a couple of days to some months depending on its complexity and the coordination with the downstream projects (see next paragraph). ## Security Updates With Tidelift API Platform Core is part of [the Tidelift subscription](https://tidelift.com/subscription/pkg/packagist-api-platform-core?utm_source=packagist-api-platform-core&utm_medium=referral&utm_campaign=enterprise): verified updates for zero-day vulnerabilities, coordinated security responses, and immediate notifications of which of your applications are impacted, with the fix prepared for you! - [Learn more](https://tidelift.com/subscription/pkg/packagist-api-platform-core?utm_source=packagist-api-platform-core&utm_medium=referral&utm_campaign=enterprise) - [Request a demo](https://tidelift.com/subscription/request-a-demo?utm_source=packagist-api-platform-core&utm_medium=referral&utm_campaign=enterprise) ## Issue Severity In order to determine the severity of a security issue we take into account the complexity of any potential attack, the impact of the vulnerability and also how many projects it is likely to affect. This score out of 15 is then converted into a level of: Low, Medium, High, Critical, or Exceptional. ### Attack Complexity Score of between 1 and 5 depending on how complex it is to exploit the vulnerability - 4 - 5 Basic: attacker must follow a set of simple steps - 2 - 3 Complex: attacker must follow non-intuitive steps with a high level of dependencies - 1 - 2 High: A successful attack depends on conditions beyond the attacker's control. That is, a successful attack cannot be accomplished at will, but requires the attacker to invest in some measurable amount of effort in preparation or execution against the vulnerable component before a successful attack can be expected. ### Impact Scores from the following areas are added together to produce a score. The score for Impact is capped at 6. Each area is scored between 0 and 4. - Integrity: Does this vulnerability cause non-public data to be accessible? If so, does the attacker have control over the data disclosed? (0-4) - Disclosure: Can this exploit allow system data (or data handled by the system) to be compromised? If so, does the attacker have control over modification? (0-4) - Code Execution: Does the vulnerability allow arbitrary code to be executed on an end users system, or the server that it runs on? (0-4) - Availability: Is the availability of a service or application affected? Is it reduced availability or total loss of availability of a service / application? Availability includes networked services (e.g., databases) or resources such as consumption of network bandwidth, processor cycles, or disk space. (0-4) ### Affected Projects Scores from the following areas are added together to produce a score. The score for Affected Projects is capped at 4. - Will it affect some or all projects using a component? (1-2) - Is the usage of the component that would cause such a thing already considered bad practice? (0-1) - How common/popular is the component (e.g. Core vs Distribution vs Schema Generator)? (0-2) - Are a number of well-known FOSS projects using API Platform affected that requires coordinated releases? (0-1) ### Score Totals - Attack Complexity: 1 - 5 - Impact: 1 - 6 - Affected Projects: 1 - 4 ### Severity levels - Low: 1 - 5 - Medium: 6 - 10 - High: 11 - 12 - Critical: 13 - 14 - Exceptional: 15 ## Credits This document has been adapted from the [Symfony's security policy](https://symfony.com/doc/current/contributing/code/security.html). --- # Troubleshooting Source: https://api-platform.com/docs/extra/troubleshooting/ # Troubleshooting This is a list of common pitfalls while using API Platform, and how to avoid them. ## Using Docker ### With Docker Toolbox on Windows Docker Toolbox is not supported anymore by API Platform. Please upgrade to [Docker for Windows](https://www.docker.com/docker-windows). ### Error Starting The Web Server If the `php` container cannot start and display this `Error starting userland proxy: Bind for 0.0.0.0:80`, it means that port 80 is already in use. You can check to see which processes are currently listening on certain ports. Find out if any service listens on port 80. You can use this command on UNIX-based OSes like macOS and Linux: ```console sudo lsof -n -i :80 | grep LISTEN ``` On Windows, you can use `netstat`. This will give you all TCP/IP network connections and not just processes listening to port 80. ```console netstat -a -b ``` The same problem may occur for port 443. In this case, follow the same steps but replace 80 by 443. You can change the port to be used in the `compose.yaml` file (default ports are 443 and 80). ## Using API Platform and JMS Serializer in the same project For the latest versions of [JMSSerializerBundle](https://jmsyst.com/bundles/JMSSerializerBundle), there is no conflict so everything should work out of the box. If you are still using the old, unmaintained v1 of JMSSerializerBundle, the best way would be to [upgrade to v2](https://github.com/schmittjoh/JMSSerializerBundle/blob/2.4.2/UPGRADING.md#upgrading-from-1x-to-20) of JMSSerializerBundle. In v1 of JMSSerializerBundle, the `serializer` alias is registered for the JMS Serializer service by default. However, API Platform requires the Symfony Serializer (and not the JMS one) to work properly. If you cannot upgrade for some reason, this behavior can be deactivated using the following configuration: ```yaml # api/config/packages/jms_serializer.yaml jms_serializer: enable_short_alias: false ``` The JMS Serializer service is available as `jms_serializer`. **Note:** if you are using JMSSerializerBundle along with FOSRestBundle and considering migrating to API Platform, you might want to take a look at [this guide](migrate-from-fosrestbundle.md) too. ## "upstream sent too big header while reading response header from upstream" NGINX 502 Error Some of your API calls fail with a 502 error and the logs for the API container shows the following error message `upstream sent too big header while reading response header from upstream`. This can be due to the cache invalidation headers that are too big for NGINX. When you query the API, API Platform adds the IDs of all returned entities and their dependencies in the headers like so : `Cache-Tags: /entity/1,/dependent_entity/1,/entity/2`. This can overflow the default header size (4k) when your API gets larger and more complex. You can modify the PHP FPM configuration file and set values to `fastcgi_buffer_size` and `fastcgi_buffers` that suit your needs, like so: ```nginx server { root /app/public; location / { # try to serve file directly, fallback to index.php try_files $uri /index.php$is_args$args; } location ~ ^/index\.php(/|$) { # Comment the next line and uncomment the next to enable dynamic resolution (incompatible with Kubernetes) fastcgi_pass php:9000; #resolver 127.0.0.11; #set $upstream_host php; #fastcgi_pass $upstream_host:9000; # Bigger buffer size to handle cache invalidation headers expansion fastcgi_buffer_size 32k; fastcgi_buffers 8 16k; fastcgi_split_path_info ^(.+\.php)(/.*)$; include fastcgi_params; # When you are using symlinks to link the document root to the # current version of your application, you should pass the real # application path instead of the path to the symlink to PHP # FPM. # Otherwise, PHP's OPcache may not properly detect changes to # your PHP files (see https://github.com/zendtech/ZendOptimizerPlus/issues/126 # for more information). fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; fastcgi_param DOCUMENT_ROOT $realpath_root; # Prevents URIs that include the front controller. This will 404: # http://domain.tld/index.php/some-path # Remove the internal directive to allow URIs like this internal; } # return 404 for all other php files not matching the front controller # this prevents access to other php files you don't want to be accessible. location ~ \.php$ { return 404; } } ``` --- # Contribution Guides Source: https://api-platform.com/docs/extra/contribution-guides/ # Contribution Guides ## API Platform Core - [General Contribution Guide](https://github.com/api-platform/core/blob/main/CONTRIBUTING.md) - [Laravel-Specific Contribution Guide](https://github.com/api-platform/core/blob/main/src/Laravel/CONTRIBUTING.md) ## API Platform Documentation - [General Contribution Guide](https://github.com/api-platform/docs/blob/main/CONTRIBUTING.md) ## API Platform Tools - [Schema Generator Contribution Guide](https://github.com/api-platform/schema-generator/blob/main/CONTRIBUTING.md) - [Admin Contribution Guide](https://github.com/api-platform/admin/blob/master/CONTRIBUTING.md) - [CRUD Generator Contribution Guide](https://github.com/api-platform/create-client/blob/master/CONTRIBUTING.md) **To report a security issue, please take a look at [the dedicated document](security.md).**

JWT screencast
Watch the Contributing back to Symfony screencast (free)

--- # Contributor Code of Conduct Source: https://api-platform.com/docs/extra/conduct/ # Contributor Code of Conduct As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, ethnic group, ethnicity, age, religion, or nationality. Examples of unacceptable behavior by participants include: - The use of sexualized language or imagery - Personal attacks - Trolling or insulting/derogatory comments - Public or private harassment - Publishing other's private information, such as physical or electronic addresses, without explicit permission - Other unethical or unprofessional conduct Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. By adopting this Code of Conduct, project maintainers commit themselves to fairly and consistently applying these principles to every aspect of managing this project. Project maintainers who do not follow or enforce the Code of Conduct may be permanently removed from the project team. This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting a project maintainer at kevin+api-platform-coc [at] dunglas.fr. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. Maintainers are obligated to maintain confidentiality regarding the reporter of an incident. This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.3.0, available at [http://contributor-covenant.org/version/1/3/0/][version] [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/3/0/