# 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

> _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:

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/`:

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).

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.

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:

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:

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:

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
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.
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
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!
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).
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
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.
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.

### 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`

### 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:

## 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.

[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!

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!

## 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** 🎉:

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

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

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
{
}
```

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
{
}
```

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:

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

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)!

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

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).

## 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
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
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).
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.
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
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.
Watch the Serializer screencast
The main serialization process has two stages:

> 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
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
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
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
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.
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
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)
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.

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:

## 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!

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
```

## 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).

## 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 :

---
# 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
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
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!

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
Sorry, your browser doesn't support HTML5 video.
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.
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).

Here is what it looks like with a simple API exposing a `Greetings` resource:

## 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!

**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!

**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.

**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.

**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 |
| ---------------------------------------------------------------- | ---------------------------------------------------------------------------- |
|  |  |
## 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.

`` 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:

## 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.

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:

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:

## 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:

## 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:

**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"
+ />
+
);
```

> [!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.

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:

## 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 = () => (
-
+
);
```

**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:

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:

**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:

## 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:

**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:

**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:

**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:

## 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.

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 = () => (
);
```

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.

**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.

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.

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.

**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.

## 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.

## 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

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


---
# 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
 
---
# 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:
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.

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"):

> [!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.

## 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.

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).**
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/