From 2408e64405bd3150ee68af3e8b8e7977b9fdfe7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20B=C3=BChlmann?= Date: Mon, 15 Sep 2025 09:40:27 +0200 Subject: [PATCH 01/49] graphql.md: add "MediaObject Normalization" chapter (#2109) * graphql.md: add "MediaObject Normalization" chapter graphql-normalizer needs to be injected * graphql.md: add "MediaObject Normalization" chapter optimization from @vinceAmstoutz Co-authored-by: Vincent Amstoutz * graphql.md: add "MediaObject Normalization" chapter optimization from @vinceAmstoutz Co-authored-by: Vincent Amstoutz --------- Co-authored-by: Vincent Amstoutz --- core/graphql.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/core/graphql.md b/core/graphql.md index bd02fbf380e..ef36ea901d9 100644 --- a/core/graphql.md +++ b/core/graphql.md @@ -2942,6 +2942,29 @@ final class CreateMediaObjectResolver implements MutationResolverInterface 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 + Date: Tue, 16 Sep 2025 22:57:04 +0200 Subject: [PATCH 02/49] docs: fix small typo (#2205) Corrected the phrasing from "Checkout" to "Check out" for clarity. ("to check something out", in this case the code, VS "checkout" as the place where someone pays for their purchases) --- symfony/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/symfony/index.md b/symfony/index.md index 1333f139cec..fbf3ca9f404 100644 --- a/symfony/index.md +++ b/symfony/index.md @@ -815,7 +815,7 @@ Keep in mind that you can use your favorite client-side technology: API Platform 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. [Checkout the demo code source on GitHub](https://github.com/api-platform/demo) +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 From d42309599309ed69a033b0fa329ae20011a25f8b Mon Sep 17 00:00:00 2001 From: Sarah Ember Date: Wed, 17 Sep 2025 02:23:09 -0700 Subject: [PATCH 03/49] Update openapi.md (#2042) Added 'responses' config to the example: Changing Operations in the OpenAPI Documentation. --- core/openapi.md | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/core/openapi.md b/core/openapi.md index f472a3b2f14..80685b63675 100644 --- a/core/openapi.md +++ b/core/openapi.md @@ -497,7 +497,27 @@ use App\Controller\RandomRabbit; ] ] ]) - ) + ), + 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 From 8410d87bf05726026f69d78f1b4bcf6a0df28fb4 Mon Sep 17 00:00:00 2001 From: Vincent Amstoutz Date: Wed, 17 Sep 2025 11:38:09 +0200 Subject: [PATCH 04/49] Merge 4.1 (#2206) * docs: fix small typo (#2205) Corrected the phrasing from "Checkout" to "Check out" for clarity. ("to check something out", in this case the code, VS "checkout" as the place where someone pays for their purchases) * Update openapi.md (#2042) Added 'responses' config to the example: Changing Operations in the OpenAPI Documentation. --------- Co-authored-by: Florent Drousset Co-authored-by: Sarah Ember --- core/openapi.md | 22 +++++++++++++++++++++- symfony/index.md | 2 +- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/core/openapi.md b/core/openapi.md index f472a3b2f14..80685b63675 100644 --- a/core/openapi.md +++ b/core/openapi.md @@ -497,7 +497,27 @@ use App\Controller\RandomRabbit; ] ] ]) - ) + ), + 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 diff --git a/symfony/index.md b/symfony/index.md index 1333f139cec..fbf3ca9f404 100644 --- a/symfony/index.md +++ b/symfony/index.md @@ -815,7 +815,7 @@ Keep in mind that you can use your favorite client-side technology: API Platform 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. [Checkout the demo code source on GitHub](https://github.com/api-platform/demo) +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 From 0f9047385a296c7abb305ae6ae1c549635cf5d20 Mon Sep 17 00:00:00 2001 From: Antoine Bluchet Date: Wed, 17 Sep 2025 15:18:24 +0200 Subject: [PATCH 05/49] 4.2 branch --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fbe77f16102..527d094cb6f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,7 +30,7 @@ jobs: VALIDATE_EDITORCONFIG: false VALIDATE_JSCPD: false VALIDATE_MARKDOWN_PRETTIER: false - DEFAULT_BRANCH: "origin/4.1" + DEFAULT_BRANCH: "origin/4.2" GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - uses: actions/cache@v4 From d840a9cc0a69a71eec0c8a3718c16987d75cb1b7 Mon Sep 17 00:00:00 2001 From: Kent Richards Date: Fri, 19 Sep 2025 06:43:22 -0300 Subject: [PATCH 06/49] Update from 'attributes' to 'properties' (#2195) --- core/serialization.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/serialization.md b/core/serialization.md index 703870c0b19..1cb8c480813 100644 --- a/core/serialization.md +++ b/core/serialization.md @@ -37,7 +37,7 @@ JSON-LD, or JavaScript Object Notation for Linked Data, is a method of encoding

Serialization Groups screencast
Watch the Serialization Groups screencast

-API Platform allows you to specify the `$context` variable used by the Symfony Serializer. This variable is an associative array that has a handy `groups` key allowing you to choose which attributes of the resource are exposed during the normalization (read) and denormalization (write) processes. +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. From 17d371cc09b58bded94f1aca5e59274ccf711d14 Mon Sep 17 00:00:00 2001 From: Maxime Helias Date: Mon, 22 Sep 2025 10:25:18 +0200 Subject: [PATCH 07/49] docs(openapi): refacto override OpenAPI section (#2207) --- core/openapi.md | 98 ++++++++++++++++++++----------------------------- 1 file changed, 39 insertions(+), 59 deletions(-) diff --git a/core/openapi.md b/core/openapi.md index 80685b63675..b132ba5e7a9 100644 --- a/core/openapi.md +++ b/core/openapi.md @@ -69,24 +69,17 @@ To produce a specification including only the operation matching your tag. ## Overriding the OpenAPI Specification -### Overriding the OpenAPI Specification with Symfony - -Symfony allows to [decorate services](https://symfony.com/doc/current/service_container/service_decoration.html), here we -need to decorate `api_platform.openapi.factory`. +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 `/foos` path. +the `GET` operation of `/api/grumpy_pizzas/{id}` path. -```yaml -# api/config/services.yaml -App\OpenApi\OpenApiFactory: - decorates: 'api_platform.openapi.factory' - arguments: ['@App\OpenApi\OpenApiFactory.inner'] - autoconfigure: false -``` +First, create a custom OpenAPI factory that decorates the original service: ```php ```php app->extend(OpenApiFactoryInterface::class, function (OpenApiFactoryInterface $factory) { - return new OpenApiFactory($factory); - }); - } + // ... } ``` +```yaml +# api/config/services.yaml +services: + # ... + App\OpenApi\OpenApiFactory: + decorates: 'api_platform.openapi.factory' + arguments: ['@App\OpenApi\OpenApiFactory.inner'] + autoconfigure: false +``` + + + +### Decorate with Laravel + +Laravel allows to [decorate services](https://laravel.com/docs/container#extending-bindings), as following: + ```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; + $this->app->extend(OpenApiFactoryInterface::class, function (OpenApiFactoryInterface $factory) { + return new OpenApiFactory($factory); + }); } } ``` From df2f50210896e61a851188d45e5bb3e592af988f Mon Sep 17 00:00:00 2001 From: Takashi Kanemoto <4360663+ttskch@users.noreply.github.com> Date: Wed, 8 Oct 2025 17:19:12 +0900 Subject: [PATCH 08/49] docs(core/operations): update explanation about omitting GET item operation (#2212) --- core/operations.md | 121 ++++++++++++++++++++++++------------------- symfony/messenger.md | 9 +--- 2 files changed, 68 insertions(+), 62 deletions(-) diff --git a/core/operations.md b/core/operations.md index 529fed04190..86f57c331c4 100644 --- a/core/operations.md +++ b/core/operations.md @@ -189,7 +189,18 @@ resources: 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. -If you do not want to allow access to the resource item (i.e. you don't want a `GET` item operation), instead of omitting it altogether, you should instead declare a `GET` item operation which returns HTTP 404 (Not Found), so that the resource item can still be identified by an IRI. For example: +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`: @@ -198,22 +209,27 @@ If you do not want to allow access to the resource item (i.e. you don't want a ` // api/src/Entity/Book.php namespace App\Entity; -use ApiPlatform\Action\NotFoundAction; -use ApiPlatform\Metadata\Get; -use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\Post; #[ApiResource(operations: [ new Get( - controller: NotFoundAction::class, - read: false, - output: false + uriTemplate: '/grimoire/{id}', + requirements: ['id' => '\d+'], + defaults: ['color' => 'brown'], + options: ['my_option' => 'my_option_value'], + schemes: ['https'], + host: '{subdomain}.api-platform.com' ), - new GetCollection() + new Post( + uriTemplate: '/grimoire', + status: 301 + ) ])] class Book { - // ... + //... } ``` @@ -222,11 +238,19 @@ class Book resources: App\Entity\Book: operations: - ApiPlatform\Metadata\GetCollection: ~ + ApiPlatform\Metadata\Post: + uriTemplate: '/grimoire' + status: 301 ApiPlatform\Metadata\Get: - controller: ApiPlatform\Action\NotFoundAction - read: false - output: false + uriTemplate: '/grimoire/{id}' + requirements: + id: '\d+' + defaults: + color: 'brown' + host: '{subdomain}.api-platform.com' + schemes: ['https'] + options: + my_option: 'my_option_value' ``` ```xml @@ -239,9 +263,25 @@ resources: https://api-platform.com/schema/metadata/resources-3.0.xsd"> - - + + + + \d+ + + + + brown + + + + https + + + + brown + + + @@ -249,12 +289,7 @@ resources: -## 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`: +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: @@ -263,6 +298,7 @@ In addition to that, we require the `id` parameter in the URL of the `GET` opera // api/src/Entity/Book.php namespace App\Entity; +use ApiPlatform\Action\NotFoundAction; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\Post; @@ -270,11 +306,9 @@ use ApiPlatform\Metadata\Post; #[ApiResource(operations: [ new Get( uriTemplate: '/grimoire/{id}', - requirements: ['id' => '\d+'], - defaults: ['color' => 'brown'], - options: ['my_option' => 'my_option_value'], - schemes: ['https'], - host: '{subdomain}.api-platform.com' + controller: NotFoundAction::class, + read: false, + output: false ), new Post( uriTemplate: '/grimoire', @@ -283,7 +317,7 @@ use ApiPlatform\Metadata\Post; ])] class Book { - //... + // ... } ``` @@ -297,14 +331,9 @@ resources: 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' + controller: ApiPlatform\Action\NotFoundAction + read: false + output: false ``` ```xml @@ -318,24 +347,8 @@ resources: - - - \d+ - - - - brown - - - - https - - - - brown - - - + diff --git a/symfony/messenger.md b/symfony/messenger.md index 735e1815195..726e8e68636 100644 --- a/symfony/messenger.md +++ b/symfony/messenger.md @@ -28,13 +28,10 @@ namespace App\Entity; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\ApiProperty; -use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\Post; use Symfony\Component\Validator\Constraints as Assert; -use ApiPlatform\Action\NotFoundAction; #[ApiResource(operations: [ - new Get(controller: NotFoundAction::class, read: false, status: 404), new Post(messenger: true, output: false, status: 202) ])] final class Person @@ -56,17 +53,13 @@ resources: status: 202 messenger: true output: false - ApiPlatform\Metadata\Get: - status: 404 - controller: ApiPlatform\Action\NotFoundAction - read: false ``` 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. We disabled the item operation using the `NotFoundAction`. A resource must have at least one item operation as it must be identified by an IRI, here the route `/people/1` exists, eventhough it returns a 404 status code. +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. From ed00f676164c85236e95e9d878dd80b4b173e5aa Mon Sep 17 00:00:00 2001 From: Takashi Kanemoto <4360663+ttskch@users.noreply.github.com> Date: Wed, 8 Oct 2025 17:20:11 +0900 Subject: [PATCH 09/49] docs(core/extensions): add eloquent support (#2208) Co-authored-by: Vincent Amstoutz --- core/extensions.md | 60 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 55 insertions(+), 5 deletions(-) diff --git a/core/extensions.md b/core/extensions.md index 258d37cbac7..f1453732a87 100644 --- a/core/extensions.md +++ b/core/extensions.md @@ -1,11 +1,8 @@ -# Extensions for Doctrine and Elasticsearch - -> [!WARNING] -> This is not yet available with [Eloquent](https://laravel.com/docs/eloquent), you're welcome to contribute [on GitHub](https://github.com/api-platform/core) +# Extensions for Doctrine, Eloquent and Elasticsearch API Platform provides a system to extend queries on items and collections. -Extensions are specific to Doctrine and Elasticsearch-PHP, and therefore, the Doctrine ORM / MongoDB ODM support or the Elasticsearch +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. @@ -160,6 +157,59 @@ The tags are `api_platform.doctrine_mongodb.odm.aggregation_extension.item` and 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) From 263c804ecf6614f65e4fd0b1e541baf85f80e9fc Mon Sep 17 00:00:00 2001 From: Richard van Laak Date: Wed, 8 Oct 2025 10:20:51 +0200 Subject: [PATCH 10/49] Remove invalid tip about #[GetCollection] alias (#2215) --- core/operations.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/core/operations.md b/core/operations.md index 86f57c331c4..f2d819edee2 100644 --- a/core/operations.md +++ b/core/operations.md @@ -72,9 +72,6 @@ for the `GET` method for both `collection` and `item` to create a readonly endpo If the operation's name matches a supported HTTP method (`GET`, `POST`, `PUT`, `PATCH` or `DELETE`), the corresponding `method` property will be automatically added. -> [!TIP] -> The `#[GetCollection]` attribute is an alias for `#[Get(collection: true)]` - --- > [!NOTE] From e39b73135bf0dc9ea029688b7df1bbf1c31677c2 Mon Sep 17 00:00:00 2001 From: Nathan Pesneau <129308244+NathanPesneau@users.noreply.github.com> Date: Tue, 25 Nov 2025 08:44:17 +0100 Subject: [PATCH 11/49] docs: metadata mutators (#2228) --- core/extending.md | 1 + core/operations.md | 81 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+) diff --git a/core/extending.md b/core/extending.md index 9bf25614d43..0bb05f67d44 100644 --- a/core/extending.md +++ b/core/extending.md @@ -20,6 +20,7 @@ The following tables summarizes which extension point to use depending on what y | [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 diff --git a/core/operations.md b/core/operations.md index f2d819edee2..c3e00d0c95e 100644 --- a/core/operations.md +++ b/core/operations.md @@ -589,3 +589,84 @@ class Weather ``` That's it! + +## Customize Operation and Resource Metadata + +Metadata mutators allow a dynamic control over resources and operations, by programmatically altering metadata before they are exposed as endpoints. Providing a way to modify, add or remove operations, adjust serialization groups or pagination settings. + +It also makes it possible to customize built-in endpoints from a third-party API, such as Sylius. + +### Resource Mutator + +Use the resource mutator to modify the entire resource metadata by adding the attribute and target resource class as argument: + +```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. + +--- From 6f09119d1ad69249e6c088979f4d8a5650063840 Mon Sep 17 00:00:00 2001 From: Alexander Kochetov Date: Tue, 25 Nov 2025 10:46:45 +0300 Subject: [PATCH 12/49] docs: add missing "docs_formats" configuration for Symfony (#2226) --- core/configuration.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/core/configuration.md b/core/configuration.md index 0d55bd5c506..961f31af3bd 100644 --- a/core/configuration.md +++ b/core/configuration.md @@ -265,6 +265,13 @@ api_platform: # 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: From e98c7fb811ce54ee4d001a1609e3aaee94fc47ac Mon Sep 17 00:00:00 2001 From: xalopp Date: Tue, 25 Nov 2025 08:50:15 +0100 Subject: [PATCH 13/49] Fix invalid YAML in example in core/state-processors.md and state-providers.md, fixes #2213 (#2223) --- core/state-processors.md | 2 +- core/state-providers.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/state-processors.md b/core/state-processors.md index 28137a95fd1..bf52a79b5d4 100644 --- a/core/state-processors.md +++ b/core/state-processors.md @@ -324,7 +324,7 @@ If you disabled this feature, you need to register the services by yourself and services: # ... - App\State\BlogPostProcessor: ~ + App\State\BlogPostProcessor: tags: [ 'api_platform.state_processor' ] App\State\UserProcessor: diff --git a/core/state-providers.md b/core/state-providers.md index 49f6b603d0b..cbb88f0c360 100644 --- a/core/state-providers.md +++ b/core/state-providers.md @@ -404,7 +404,7 @@ To declare the service explicitly, you can use the following snippet: services: # ... - App\State\BlogPostProvider: ~ + App\State\BlogPostProvider: tags: [ 'api_platform.state_provider' ] # api/config/services.yaml From e34dfd7adca99ab57fc1f885e452f6fc127173fe Mon Sep 17 00:00:00 2001 From: OlivierBarbier Date: Tue, 25 Nov 2025 09:17:48 +0100 Subject: [PATCH 14/49] docs: update UUID recommendation to v7 for better database performance (#2219) Co-authored-by: Antoine Bluchet --- symfony/index.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/symfony/index.md b/symfony/index.md index fbf3ca9f404..e69b698dd8e 100644 --- a/symfony/index.md +++ b/symfony/index.md @@ -361,8 +361,7 @@ The framework also uses these metadata to serialize and deserialize data from JS 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. [You should -probably use them instead of auto-incremented IDs](https://www.clever-cloud.com/blog/engineering/2015/05/20/why-auto-increment-is-a-terrible-idea/). +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! From 97b274e2f437fecc7c0ff1d98d4e3650cc064a4c Mon Sep 17 00:00:00 2001 From: Antoine Bluchet Date: Tue, 25 Nov 2025 09:48:56 +0100 Subject: [PATCH 15/49] document property filter back (#2220) --- core/filters.md | 54 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 51 insertions(+), 3 deletions(-) diff --git a/core/filters.md b/core/filters.md index 9a53f11bf4a..8bbaf1e9706 100644 --- a/core/filters.md +++ b/core/filters.md @@ -154,7 +154,10 @@ public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $q } ``` -Note that we're 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`. +> [!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 @@ -244,7 +247,8 @@ class Book { This approach is recommended for new filters as it's more flexible and allows true property restriction via the parameter configuration. -Note that invalid values are usually ignored by our filters, use [validation](#parameter-validation) to trigger errors for wrong parameter values. +> [!NOTE] +> Invalid values are usually ignored by our filters, use [validation](#parameter-validation) to trigger errors for wrong parameter values. ## OpenAPI and JSON Schema @@ -338,7 +342,8 @@ use Symfony\Component\Validator\Constraints as Assert; class User {} ``` -Note that when `castToNativeType` is enabled, API Platform infers type validation from the JSON Schema. +> [!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. @@ -396,6 +401,49 @@ 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 distribution. +> [!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`. From a0e1317dbfbf60de549eb3ca8da7bfcd45510ef1 Mon Sep 17 00:00:00 2001 From: Max <28672138+xammmue@users.noreply.github.com> Date: Thu, 4 Dec 2025 16:11:16 +0100 Subject: [PATCH 16/49] Update releases information (#2232) --- extra/releases.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/extra/releases.md b/extra/releases.md index 37f68ddaadd..a486f736271 100644 --- a/extra/releases.md +++ b/extra/releases.md @@ -12,16 +12,17 @@ For example: - 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; ## Maintenance 3 versions are maintained at the same time: -- **stable** (currently the **4.1** branch): regular bugfixes are integrated in this version -- **old-stable** (are the last branch: **4.0**): [security fixes](security.md) are integrated in this version, regular bugfixes are **not** backported in it +- **stable** (currently the **4.2** branch): regular bugfixes are integrated in this version +- **old-stable** (are the last branch: **4.1**): [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...) **are not maintained**. If you still use them, you must upgrade as soon as possible. +Older versions (1.x, 2.6..., 3.0..., 4.0) **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. From 323e97780cb8d215db13e96076b3759cc541fcd0 Mon Sep 17 00:00:00 2001 From: Antoine Bluchet Date: Thu, 4 Dec 2025 17:51:17 +0100 Subject: [PATCH 17/49] improve dto documentation (#2231) --- core/dto.md | 400 +++++++++++++++++++++++++++++++++------------------- 1 file changed, 258 insertions(+), 142 deletions(-) diff --git a/core/dto.md b/core/dto.md index ca47d83cc14..bf6633bff8b 100644 --- a/core/dto.md +++ b/core/dto.md @@ -1,215 +1,331 @@ # Using Data Transfer Objects (DTOs) -

Custom Resources screencast
Watch the Custom Resources screencast

+

Custom Resources screencast -As stated in [the general design considerations](design.md), in most cases [the DTO pattern](https://en.wikipedia.org/wiki/Data_transfer_object) should be implemented using an API Resource class representing the public data model exposed through the API and [a custom State Provider](state-providers.md). In such cases, the class marked with `#[ApiResource]` will act as a DTO. +Watch the Custom Resources screencast

-However, it's sometimes useful to use a specific class to represent the input or output data structure related to an operation. These techniques are useful to document your API properly (using Hydra or OpenAPI) and will often be used on `POST` operations. +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. -## Implementing a Write Operation With an Input Different From the Resource +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. -Using an input, the request body will be denormalized to the input instead of your resource class. +This reference covers three implementation strategies: + + * [State Options: Linking a DTO Resource to an Entity for automated CRUD operations.](#1-the-dto-resource-state-options) + * [Automated Mapped Inputs: Using input DTOs with stateOptions for automated Write operations.](#2-automated-mapped-inputs-outputs) + * [Custom Business Logic: Using input DTOs with custom State Processors for specific business actions.](#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 the symfony/object-mapper:^7.4 + +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 - - */ -final class UserResetPasswordProcessor implements ProcessorInterface -{ - /** - * @param UserResetPasswordDto $data - * - * @throws NotFoundHttpException - */ - public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): User + 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 { - if ('user@example.com' === $data->email) { - return new User(email: $data->email, id: 1); + // Entity (int) -> DTO (string) + if ($source instanceof BookEntity) { + return number_format($price / 100, 2).'$'; } - - throw new NotFoundHttpException(); + // DTO (string) -> Entity (int) + if ($source instanceof self) { + return 100 * (int) str_replace('$', '', $price); + } + throw new \LogicException(\sprintf('Unexpected "%s" source.', $source::class)); } } ``` -In some cases, using an input DTO is a way to avoid serialization groups. +### Implementation Details: The Object Mapper Magic -## Use Symfony Messenger With an Input DTO +Automated mapping relies on two internal classes: `ApiPlatform\State\Provider\ObjectMapperProvider` and `ApiPlatform\State\Processor\ObjectMapperProcessor`. -Let's use a message that will be processed by [Symfony Messenger](https://symfony.com/components/Messenger). API Platform has an [integration with messenger](../symfony/messenger.md), to use a DTO as input you need to specify the `input` attribute: +These classes act as decorators around the standard Provider/Processor chain. They are activated when: -```php -map($entity, $resourceClass)` to transform the Entity into your DTO Resource. -This will dispatch the `App\Dto\Message` via [Symfony Messenger](https://symfony.com/components/Messenger). +**Write (POST/PUT/PATCH):** -## Implementing a Read Operation With an Output Different From the Resource +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. -To return another representation of your data in a [State Provider](./state-providers.md) we advise to specify the `output` attribute of the resource. Note that this technique works without any changes to the resource but your API documentation would be wrong. + +## 2. Automated Mapped Inputs & 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 - - */ -final class BookRepresentationProvider implements ProviderInterface +#[Map(target: BookEntity::class)] +final class UpdateBook { - public function provide(Operation $operation, array $uriVariables = [], array $context = []): AnotherRepresentation - { - return new AnotherRepresentation(); - } + #[Assert\NotBlank, Assert\Length(max: 255)] + #[Map(target: 'title')] + public string $name; + + #[Assert\NotBlank, Assert\Length(max: 255)] + public string $description; } ``` -## Implementing a Write Operation With an Output Different From the Resource +#### Output DTO (Collection Read) -For returning another representation of your data in a [State Processor](./state-processors.md), you should specify your processor class in -the `processor` attribute and same for your `output`. - - +For the `GetCollection` operation, we use a lighter DTO that exposes only essential fields. ```php - - - - - - - - - - - +#### Wiring it all together in the Resource + +In your Book resource, configure the operations to use these classes via input and output. + +```php +// src/Api/Resource/Book.php + +#[ApiResource( + stateOptions: new Options(entityClass: BookEntity::class), + operations: [ + new Get(), + // Use the specialized Output DTO for collections + new GetCollection( + output: BookCollection::class + ), + // Use the specialized Input DTO for creation + new Post( + input: CreateBook::class + ), + // Use the specialized Input DTO for updates + new Patch( + input: UpdateBook::class + ), + ] +)] +final class Book { /* ... */ } ``` - +## 3. Custom Business Logic (Custom Processor) + +For complex business actions (like applying a discount), standard CRUD mapping isn't enough. You should use a custom Processor paired with a specific Input DTO. -Here the `$data` attribute represents an instance of your resource. +### The Input DTO + +This DTO holds the data required for the specific action. ```php - - */ -final class BookRepresentationProcessor implements ProcessorInterface +final readonly class DiscountBookProcessor implements ProcessorInterface { - /** - * @param Book $data - */ - public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): AnotherRepresentation + public function __construct( + // Inject the built-in Doctrine persist processor to handle saving + #[Autowire(service: 'api_platform.doctrine.orm.state.persist_processor')] + private ProcessorInterface $persistProcessor, + private ObjectMapperInterface $objectMapper, + ) { + } + + public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): Book { - return new AnotherRepresentation( - $data->getId(), - $data->getTitle(), - // etc. - ); + // 1. Retrieve the Entity loaded by API Platform (via stateOptions) + if (!$entity = $context['request']->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 { /* ... */ } +``` From ff2ccb4ebea9834c96a686757c73a5ed146a9507 Mon Sep 17 00:00:00 2001 From: soyuka Date: Thu, 4 Dec 2025 21:17:34 +0100 Subject: [PATCH 18/49] update docs --- core/filters.md | 64 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/core/filters.md b/core/filters.md index 8bbaf1e9706..29f2213d3eb 100644 --- a/core/filters.md +++ b/core/filters.md @@ -25,6 +25,34 @@ The recommended way to define parameters is by using Parameter attributes direct * `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: + +* **`DateFilter`**: For filtering by date intervals (e.g., `?createdAt[after]=...`). + * Usage: `new QueryParameter(filter: DateFilter::class)` +* **`ExactFilter`**: For exact value matching. + * Usage: `new QueryParameter(filter: ExactFilter::class)` +* **`PartialSearchFilter`**: For partial string matching (SQL `LIKE %...%`). + * Usage: `new QueryParameter(filter: PartialSearchFilter::class)` +* **`IriFilter`**: For filtering by IRIs (e.g., relations). + * Usage: `new QueryParameter(filter: IriFilter::class)` +* **`BooleanFilter`**: For boolean field filtering. + * Usage: `new QueryParameter(filter: BooleanFilter::class)` +* **`NumericFilter`**: For numeric field filtering. + * Usage: `new QueryParameter(filter: NumericFilter::class)` +* **`RangeFilter`**: For range-based filtering (e.g., prices between X and Y). + * Usage: `new QueryParameter(filter: RangeFilter::class)` +* **`ExistsFilter`**: For checking existence of nullable values. + * Usage: `new QueryParameter(filter: ExistsFilter::class)` +* **`OrderFilter`**: For sorting results. + * 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. + You can declare a parameter on the resource class to make it available for all its operations: ```php @@ -71,6 +99,42 @@ class Friend } ``` +### Using Filters with DateTime Properties + +When working with `DateTime` or `DateTimeImmutable` properties, the system might default to exact matching. To enable date ranges (e.g., `after`, `before`), you must explicitly use the `DateFilter`: + +```php + new QueryParameter( + // Use the class string to leverage the service container (recommended) + filter: DateFilter::class, + properties: ['startDate', 'endDate'] + ) + ] + ) +])] +class Event +{ + // ... +} +``` + +This configuration allows clients to filter events by date ranges using queries like: + + * `/events?date[startDate][after]=2023-01-01` + * `/events?date[endDate][before]=2023-12-31` + ### 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. From ba8d387296a185cb41c6bfa9d75cab8a8579452c Mon Sep 17 00:00:00 2001 From: soyuka Date: Fri, 5 Dec 2025 07:43:39 +0100 Subject: [PATCH 19/49] fix #1749 --- symfony/testing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/symfony/testing.md b/symfony/testing.md index 2b658bf4edc..f6ed87b22a7 100644 --- a/symfony/testing.md +++ b/symfony/testing.md @@ -149,7 +149,7 @@ Now that you have some data fixtures for your API, you are ready to write functi 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 distribution. 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. -If you don't use the distribution, run `composer require --dev symfony/test-pack symfony/http-client` to install them. +Run `composer require --dev symfony/test-pack symfony/http-client` to install the testing tools (when using the distribution they're already installed). Install [DAMADoctrineTestBundle](https://github.com/dmaicher/doctrine-test-bundle) to reset the database automatically before each test: From 9e67aec47ef6581a0644dba1b52af96b2c161ce2 Mon Sep 17 00:00:00 2001 From: soyuka Date: Fri, 5 Dec 2025 07:56:52 +0100 Subject: [PATCH 20/49] document enums as resource --- core/enums.md | 281 ++++++++++++++++++++++++++++++++++++++++++ core/openapi.md | 1 + core/serialization.md | 2 + 3 files changed, 284 insertions(+) create mode 100644 core/enums.md diff --git a/core/enums.md b/core/enums.md new file mode 100644 index 00000000000..1afb2441678 --- /dev/null +++ b/core/enums.md @@ -0,0 +1,281 @@ +# Enums as API Resources + +API Platform provides support for PHP 8.1+ `BackedEnum`s, allowing them to be exposed as first-class API resources. This enables clients to discover available enum cases and their associated metadata directly through your API. + +## Exposing BackedEnums + +To expose a `BackedEnum` as an API resource, simply apply the `#[ApiResource]` attribute to your enum class: + +```php +name; + } +} +``` +#### Adding Custom Properties + +You can add custom properties to your enum resource by defining public methods and marking them with `#[ApiProperty]`: + +```php + 'Article is not ready for public consumption', + self::PUBLISHED => 'Article is publicly available', + self::ARCHIVED => 'Article content is outdated or superseded', + }; + } +} +``` + +With the above, `GET /statuses/0` might return: + +```json +{ + "name": "DRAFT", + "value": 0, + "description": "Article is not ready for public consumption" +} +``` + +#### Custom State Providers + +For more advanced customization, you can implement custom state providers for your enum resources. +A common pattern is to use a trait to provide common functionality: + +```php +value; + } + + public function getValue(): int|string + { + return $this->value; + } + + public static function getCases(): array + { + return self::cases(); + } + + public static function getCase(Operation $operation, array $uriVariables): ?BackedEnum + { + $id = is_numeric($uriVariables['id']) ? (int) $uriVariables['id'] : $uriVariables['id']; + + return array_reduce(self::cases(), static fn($c, BackedEnum $case) => $case->name === $id || $case->value === $id ? $case : $c, null); + } +} +``` + +Then, apply the trait and specify the providers in your enum: + +```php + Date: Fri, 5 Dec 2025 10:55:55 +0100 Subject: [PATCH 21/49] fix #1856 --- core/bootstrap.md | 4 ++ core/form-data.md | 149 +++++++++++++++------------------------------- 2 files changed, 51 insertions(+), 102 deletions(-) diff --git a/core/bootstrap.md b/core/bootstrap.md index 68c6906ba8c..dd1966db0ef 100644 --- a/core/bootstrap.md +++ b/core/bootstrap.md @@ -3,6 +3,10 @@ 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 outdated 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. + ```console composer require \ api-platform/core \ diff --git a/core/form-data.md b/core/form-data.md index df616effe5f..27e9318ec93 100644 --- a/core/form-data.md +++ b/core/form-data.md @@ -16,128 +16,73 @@ of the framework. > If you're working with Laravel, refer to the [Laravel CSRF documentation](https://laravel.com/docs/csrf) to ensure > adequate protection against such attacks. -In this tutorial, we will decorate the default `DeserializeListener` class to handle form data if applicable, and delegate to the built-in listener for other cases. +## Configuration -## Create your `FormRequestProcessorDecorator` processor +First, you must register the form format and map it to the `application/x-www-form-urlencoded` MIME type in your API Platform configuration: -This decorator is able to denormalize posted form data to the target object. In case of other format, it fallbacks to the original [DeserializeListener](https://github.com/api-platform/core/blob/91dc2a4d6eeb79ea8dec26b41e800827336beb1a/src/Bridge/Symfony/Bundle/Resources/config/api.xml#L85-L91). - -```php -getContentType()) { - return $this->handleFormRequest($data); - } - - // Delegate the processing to the original processor for other cases - return $this->decorated->process($data, $operation, $uriVariables, $context); - } +```yaml +# api\_platform.yaml +api_platform: + formats: + jsonld: ['application/ld+json'] + form: ['application/x-www-form-urlencoded'] +``` - /** - * Handle form requests by deserializing the data into the correct entity - */ - private function handleFormRequest(Request $request) - { - $attributes = $request->attributes->get('_api_attributes'); - if (!$attributes) { - return null; - } +## Creating a Decoder - $context = $this->serializerContextBuilder->createFromRequest($request, false, $attributes); +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. - // Deserialize the form data into an entity - $data = $request->request->all(); - - return $this->denormalizer->denormalize($data, 'App\Entity\SomeEntity', null, $context); - } -} -``` +```php +app->bind(ProcessorInterface::class, function ($app) { - $decoratedProcessor = $app->make(ProcessorInterface::class); - - return new FormRequestProcessorDecorator( - $decoratedProcessor, - $app->make(DenormalizerInterface::class), - $app->make(SerializerContextBuilderInterface::class) - ); - }); + return $parsedData; } + + public function supportsDecoding(string $format): bool + { + return $format === 'form'; + } } ``` -## Using your `FormRequestProcessorDecorator` processor +## **Usage in a Resource** -Finally, you can use the processor in your API Resource like this: +You can now configure your API Resource to accept the form input format. In this example, we define a FormData resource and restrict the inputFormats for the Post operation. ```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; + } +} ``` From d7c1d742a12347113d70212c2136c6db035a759c Mon Sep 17 00:00:00 2001 From: soyuka Date: Fri, 5 Dec 2025 11:03:20 +0100 Subject: [PATCH 22/49] bootstrap --- core/bootstrap.md | 652 +--------------------------------------------- 1 file changed, 9 insertions(+), 643 deletions(-) diff --git a/core/bootstrap.md b/core/bootstrap.md index dd1966db0ef..b7e43655fb4 100644 --- a/core/bootstrap.md +++ b/core/bootstrap.md @@ -4,657 +4,23 @@ You may want to run a minimal version of API Platform. This one file runs API Pl It requires the following Composer packages: > [!NOTE] -> This documentation is outdated we're working on improving it, in the mean time we declare +> 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/core \ + api-platform/serializer \ + api-platform/metadata \ + api-platform/state \ + api-platform/jsonld \ phpdocumentor/reflection-docblock \ symfony/property-info \ symfony/routing \ symfony/validator ``` -The minimal version of API Platform: - -```php - ['application/merge-patch+json'], 'jsonapi' => ['application/vnd.api+json']]; -$formats = ['jsonld' => ['application/ld+json']]; -$errorFormats = [ - 'jsonproblem' => ['application/problem+json'], - 'jsonld' => ['application/ld+json'], - 'jsonapi' => ['application/vnd.api+json'] -]; - -$configuration = [ - 'collection' => [ - 'pagination' => [ - 'page_parameter_name' => 'page', - 'enabled_parameter_name' => 'pagination' - ] - ] -]; - -$exceptionToStatus = [ - # The 4 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 => 400, - \ApiPlatform\Exception\FilterValidationException::class => 400, - \Doctrine\ORM\OptimisticLockException::class => 409, -]; - -$logger = new Logger(); - -$phpDocExtractor = new PhpDocExtractor(); -$reflectionExtractor = new ReflectionExtractor(); - -$propertyInfo = new PropertyInfoExtractor( - [$reflectionExtractor], - [$phpDocExtractor, $reflectionExtractor], - [$phpDocExtractor], - [$reflectionExtractor], - [$reflectionExtractor] -); - - -$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader()); - -final class FilterLocator implements ContainerInterface -{ - private $filters = []; - public function get(string $id) { - return $this->filters[$id] ?? null; - } - - public function has(string $id): bool { - return isset($this->filter[$id]); - } -} - -$filterLocator = new FilterLocator(); -$pathSegmentNameGenerator = new UnderscorePathSegmentNameGenerator(); - -$resourceNameCollectionFactory = new AttributesResourceNameCollectionFactory([__DIR__.'/../src/']); -$resourceClassResolver = new ResourceClassResolver($resourceNameCollectionFactory); -$propertyMetadataFactory = new PropertyInfoPropertyMetadataFactory($propertyInfo); -$propertyMetadataFactory = new SerializerPropertyMetadataFactory(new ApiClassMetadataFactory($classMetadataFactory), $propertyMetadataFactory, $resourceClassResolver); - -$propertyNameCollectionFactory = new PropertyInfoPropertyNameCollectionFactory($propertyInfo); -$linkFactory = new LinkFactory( - $propertyNameCollectionFactory, - $propertyMetadataFactory, - $resourceClassResolver -); - -// CachedResourceMetadataCollectionFactory decoration-priority="-10" -// MessengerResourceMetadataCollectionFactory decoration-priority="50" -// AlternateUriResourceMetadataCollectionFactory decoration-priority="200" -// FiltersResourceMetadataCollectionFactory decoration-priority="200" -// FormatsResourceMetadataCollectionFactory decoration-priority="200" -// InputOutputResourceMetadataCollectionFactory decoration-priority="200" -// PhpDocResourceMetadataCollectionFactory decoration-priority="200" -// OperationNameResourceMetadataCollectionFactory decoration-priority="200" -// LinkResourceMetadataCollectionFactory decoration-priority="500" -// UriTemplateResourceMetadataCollectionFactory decoration-priority="500" -// NotExposedOperationResourceMetadataCollectionFactory decoration-priority="700" -// ExtractorResourceMetadataCollectionFactory decoration-priority="800" - -// AttributesResourceMetadataCollectionFactory decorated - -$resourceMetadataFactory = new MessengerResourceMetadataCollectionFactory( - new AlternateUriResourceMetadataCollectionFactory( - new FiltersResourceMetadataCollectionFactory( - new FormatsResourceMetadataCollectionFactory( - new InputOutputResourceMetadataCollectionFactory( - new PhpDocResourceMetadataCollectionFactory( - new OperationNameResourceMetadataCollectionFactory( - new LinkResourceMetadataCollectionFactory( - $linkFactory, - new UriTemplateResourceMetadataCollectionFactory( - $linkFactory, - $pathSegmentNameGenerator, - new NotExposedOperationResourceMetadataCollectionFactory( - $linkFactory, - new AttributesResourceMetadataCollectionFactory(null, $logger, [], false) - ) - ) - ) - ) - ) - ), - $formats, - $patchFormats, - ) - ) - ) -); - -$providerCollection = new class implements ContainerInterface { - public array $providers = []; - public function get($id) { - return $this->providers[$id]; - } - - public function has($id): bool { - return isset($this->providers[$id]); - } -}; -$stateProviders = new CallableProvider($providerCollection); - -$processorCollection = new class implements ContainerInterface { - public array $processors = []; - public function get($id) { - return $this->processors[$id]; - } - - public function has($id): bool { - return isset($this->processors['id']); - } -}; -$stateProcessors = new CallableProcessor($processorCollection); - -class Validator implements ValidatorInterface { - private $validator; - public function __construct($validator) - { - $this->validator = $validator; - } - - public function validate(object $data, array $context = []): void { - $this->validator->validate($data, $context); - } -} - -$validator = new Validator(Validation::createValidator()); -$validateListener = new ValidateListener($validator, $resourceMetadataFactory); - -#[ApiResource(provider: \BookProvider::class)] -class Book -{ - public int $id; -} - -class BookProvider implements ProviderInterface -{ - public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null - { - $book = new Book(); - if ($operation instanceof CollectionOperationInterface) { - return [$book]; - } - - $book->id = $uriVariables['id']; - return $book; - } -} - -$dataProvider = new BookProvider(); -$providerCollection->providers[BookProvider::class] = $dataProvider; - -class BookProcessor implements ProcessorInterface { - public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []) {} -} - -$bookProcessor = new BookProcessor(); -$processorCollection->processors[BookProcessor::class] = $bookProcessor; - -$propertyAccessor = PropertyAccess::createPropertyAccessor(); -$identifiersExtractor = new IdentifiersExtractor($resourceMetadataFactory, $resourceClassResolver, $propertyNameCollectionFactory, $propertyMetadataFactory, $propertyAccessor); - -class ApiLoader { - private $resourceNameCollectionFactory; - private $resourceMetadataFactory; - - public function __construct( - ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, - ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory - ) { - $this->resourceNameCollectionFactory = $resourceNameCollectionFactory; - $this->resourceMetadataFactory = $resourceMetadataFactory; - } - - public function load(): RouteCollection - { - $routeCollection = new RouteCollection(); - - foreach ($this->resourceNameCollectionFactory->create() as $resourceClass) { - foreach ($this->resourceMetadataFactory->create($resourceClass) as $resourceMetadata) { - foreach ($resourceMetadata->getOperations() as $operationName => $operation) { - if ($operation->getRouteName()) { - continue; - } - - if (SkolemIriConverter::$skolemUriTemplate === $operation->getUriTemplate()) { - continue; - } - - $path = ($operation->getRoutePrefix() ?? '').$operation->getUriTemplate(); - foreach ($operation->getUriVariables() ?? [] as $parameterName => $link) { - if (!$expandedValue = $link->getExpandedValue()) { - continue; - } - - $path = str_replace(sprintf('{%s}', $parameterName), $expandedValue, $path); - } - - if (($controller = $operation->getController()) && !$this->container->has($controller)) { - throw new RuntimeException(sprintf('There is no builtin action for the "%s" operation. You need to define the controller yourself.', $operationName)); - } - - $route = new Route( - $path, - [ - '_controller' => $controller ?? PlaceholderAction::class, - '_format' => null, - '_stateless' => $operation->getStateless(), - '_api_resource_class' => $resourceClass, - '_api_operation_name' => $operationName, - ] + ($operation->getDefaults() ?? []), - $operation->getRequirements() ?? [], - $operation->getOptions() ?? [], - $operation->getHost() ?? '', - $operation->getSchemes() ?? [], - [$operation->getMethod() ?? HttpOperation::METHOD_GET], - $operation->getCondition() ?? '' - ); - - $routeCollection->add($operationName, $route); - } - } - } - - return $routeCollection; - } -} - -$apiLoader = new ApiLoader($resourceNameCollectionFactory, $resourceMetadataFactory); -$routes = $apiLoader->load(); -$requestContext = new RequestContext(); -$matcher = new UrlMatcher($routes, $requestContext); -$generator = new UrlGenerator($routes, $requestContext); - -class Router implements RouterInterface -{ - private $routes; - private $context; - private $matcher; - private $generator; - - public function __construct(RouteCollection $routes, UrlMatcherInterface $matcher, UrlGeneratorInterface $generator, RequestContext $requestContext) - { - $this->routes = $routes; - $this->matcher = $matcher; - $this->generator = $generator; - $this->context = $requestContext; - } - - public function getRouteCollection() { - return $this->routes; - } - - public function match(string $pathinfo): array { - return $this->matcher->match($pathinfo); - } - - public function setContext(RequestContext $context) { - $this->context = $context; - } - - public function getContext(): RequestContext { - return $this->context; - } - - public function generate(string $name, array $parameters = [], int $referenceType = self::ABSOLUTE_PATH): string { - return $this->generator->generate($name, $parameters, $referenceType); - } -} - -class ApiUrlGenerator implements ApiUrlGeneratorInterface { - private $generator; - - public function __construct(UrlGeneratorInterface $generator) - { - $this->generator = $generator; - } - - public function generate($name, $parameters = [], $referenceType = self::ABS_PATH): string { - return $this->generator->generate($name, $parameters, $referenceType ?: self::ABS_PATH); - } -} - -$apiUrlGenerator = new ApiUrlGenerator($generator); - -$router = new Router($routes, $matcher, $generator, $requestContext); - -$uriVariableTransformers = [ - new IntegerUriVariableTransformer(), - new DateTimeUriVariableTransformer(), -]; - -$iriConverter = new IriConverter( - $stateProviders, - $router, - $identifiersExtractor, - $resourceClassResolver, - $resourceMetadataFactory, - new UriVariablesConverter($propertyMetadataFactory, $resourceMetadataFactory, $uriVariableTransformers), - new SkolemIriConverter($router) -); - -$writeListener = new WriteListener( - $stateProcessors, - $iriConverter, - $resourceClassResolver, - $resourceMetadataFactory, - /**new UriVariablesConverter($propertyMetadataFactory, $resourceMetadataFactory, $uriVariableTransformers)*/ null, -); - -$serializerContextBuilder = new SerializerContextBuilder($resourceMetadataFactory); - -$objectNormalizer = new ObjectNormalizer(); - -$nameConverter = new MetadataAwareNameConverter($classMetadataFactory); -$jsonLdContextBuilder = new JsonLdContextBuilder($resourceNameCollectionFactory, $resourceMetadataFactory, $propertyNameCollectionFactory, $propertyMetadataFactory, $apiUrlGenerator, $iriConverter, $nameConverter); -$jsonLdItemNormalizer = new JsonLdItemNormalizer($resourceMetadataFactory, $propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $jsonLdContextBuilder, $propertyAccessor, $nameConverter, $classMetadataFactory, $defaultContext, /** resource access checker **/ null); -$jsonLdObjectNormalizer = new JsonLdObjectNormalizer($objectNormalizer, $iriConverter, $jsonLdContextBuilder); -$jsonLdEncoder = new JsonLdEncoder('jsonld', new JsonEncoder()); - -$problemConstraintViolationListNormalizer = new ProblemConstraintViolationListNormalizer([], $nameConverter, $defaultContext); - -$hydraCollectionNormalizer = new HydraCollectionNormalizer($jsonLdContextBuilder, $resourceClassResolver, $iriConverter, $resourceMetadataFactory, $defaultContext); -$hydraPartialCollectionNormalizer = new PartialCollectionViewNormalizer($hydraCollectionNormalizer, $configuration['collection']['pagination']['page_parameter_name'], $configuration['collection']['pagination']['enabled_parameter_name'], $resourceMetadataFactory, $propertyAccessor); -$hydraCollectionFiltersNormalizer = new CollectionFiltersNormalizer($hydraPartialCollectionNormalizer, $resourceMetadataFactory, $resourceClassResolver, $filterLocator); -$hydraErrorNormalizer = new HydraErrorNormalizer($apiUrlGenerator, $debug, $defaultContext); -$hydraEntrypointNormalizer = new HydraEntrypointNormalizer($resourceMetadataFactory, $iriConverter, $apiUrlGenerator); -$hydraDocumentationNormalizer = new HydraDocumentationNormalizer($resourceMetadataFactory, $propertyNameCollectionFactory, $propertyMetadataFactory, $resourceClassResolver, $apiUrlGenerator, $nameConverter); -$hydraConstraintViolationNormalizer = new HydraConstraintViolationListNormalizer($apiUrlGenerator, [], $nameConverter); - -$problemErrorNormalizer = new ErrorNormalizer($debug, $defaultContext); - -// $expressionLanguage = new ExpressionLanguage(); -// $resourceAccessChecker = new ResourceAccessChecker( -// $expressionLanguage, -// ); - -$itemNormalizer = new ItemNormalizer( - $propertyNameCollectionFactory, - $propertyMetadataFactory, - $iriConverter, - $resourceClassResolver, - $propertyAccessor, - $nameConverter, - $classMetadataFactory, - $logger, - $resourceMetadataFactory, - /**$resourceAccessChecker **/ null, - $defaultContext -); - -$arrayDenormalizer = new ArrayDenormalizer(); -$problemNormalizer = new ProblemNormalizer($debug, $defaultContext); -$jsonserializableNormalizer = new JsonSerializableNormalizer($classMetadataFactory, $nameConverter, $defaultContext); -$dateTimeNormalizer = new DateTimeNormalizer($defaultContext); -$dataUriNormalizer = new DataUriNormalizer(); -$dateIntervalNormalizer = new DateIntervalNormalizer($defaultContext); -$dateTimeZoneNormalizer = new DateTimeZoneNormalizer(); -$constraintViolationListNormalizer = new ConstraintViolationListNormalizer($defaultContext, $nameConverter); -$unwrappingDenormalizer = new UnwrappingDenormalizer($propertyAccessor); - -$halItemNormalizer = new HalItemNormalizer($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, $classMetadataFactory, $defaultContext, $resourceMetadataFactory, /** resourceAccessChecker **/ null); -$halItemNormalizer = new HalItemNormalizer($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, $classMetadataFactory, $defaultContext, $resourceMetadataFactory, /** resourceAccessChecker **/ null); - -$halEntrypointNormalizer = new HalEntrypointNormalizer($resourceMetadataFactory, $iriConverter, $apiUrlGenerator); -$halCollectionNormalizer = new HalCollectionNormalizer($resourceClassResolver, $configuration['collection']['pagination']['page_parameter_name'], $resourceMetadataFactory); -$halObjectNormalizer = new HalObjectNormalizer($objectNormalizer, $iriConverter); - -$openApiNormalizer = new OpenApiNormalizer($objectNormalizer); - -$list = new \SplPriorityQueue(); -$list->insert($unwrappingDenormalizer, 1000); -$list->insert($halItemNormalizer, -890); -$list->insert($hydraConstraintViolationNormalizer, -780); -$list->insert($hydraEntrypointNormalizer, -800); -$list->insert($hydraErrorNormalizer, -800); -$list->insert($hydraCollectionFiltersNormalizer, -800); -$list->insert($halEntrypointNormalizer, -800); -$list->insert($halCollectionNormalizer, -985); -$list->insert($halObjectNormalizer, -995); -$list->insert($jsonLdItemNormalizer, -890); -$list->insert($problemConstraintViolationListNormalizer, -780); -$list->insert($problemErrorNormalizer, -810); -$list->insert($jsonLdObjectNormalizer, -995); -$list->insert($constraintViolationListNormalizer, -915); -$list->insert($arrayDenormalizer, -990); -$list->insert($dateTimeZoneNormalizer, -915); -$list->insert($dateIntervalNormalizer, -915); -$list->insert($dataUriNormalizer, -920); -$list->insert($dateTimeNormalizer, -910); -$list->insert($jsonserializableNormalizer, -900); -$list->insert($problemNormalizer, -890); -$list->insert($objectNormalizer, -1000); -$list->insert($itemNormalizer, -895); -// $list->insert($uuidDenormalizer, -895); //Todo ramsey uuid support ? -$list->insert($openApiNormalizer, -780); - -// TODO: JSON-API support -/** - * api_platform.jsonapi.normalizer.error -790 ApiPlatform\JsonApi\Serializer\ErrorNormalizer - * api_platform.jsonapi.normalizer.constraint_violation_list -780 ApiPlatform\JsonApi\Serializer\ConstraintViolationListNormalizer - * api_platform.openapi.normalizer.api_gateway -780 ApiPlatform\Swagger\Serializer\ApiGatewayNormalizer - * api_platform.jsonapi.normalizer.entrypoint -800 ApiPlatform\JsonApi\Serializer\EntrypointNormalizer - * api_platform.jsonapi.normalizer.collection -985 ApiPlatform\JsonApi\Serializer\CollectionNormalizer - * api_platform.jsonapi.normalizer.item -890 ApiPlatform\JsonApi\Serializer\ItemNormalizer - * api_platform.jsonapi.normalizer.object -995 ApiPlatform\JsonApi\Serializer\ObjectNormalizer - */ - -$encoders = [new JsonEncoder(), $jsonLdEncoder]; -$serializer = new Serializer(iterator_to_array($list), $encoders); - -$serializeListener = new SerializeListener($serializer, $serializerContextBuilder, $resourceMetadataFactory); -$respondListener = new RespondListener($resourceMetadataFactory); -$formatListener = new AddFormatListener(new Negotiator(), $resourceMetadataFactory, $formats); -$readListener = new ReadListener($stateProviders, $resourceMetadataFactory, $serializerContextBuilder); -$deserializeListener = new DeserializeListener($serializer, $serializerContextBuilder, $resourceMetadataFactory); -$addLinkHeaderListener = new AddLinkHeaderListener($apiUrlGenerator); -$validationExceptionListener = new ValidationExceptionListener($serializer, $errorFormats, $exceptionToStatus); - -$controller = new ExceptionAction($serializer, $errorFormats, $exceptionToStatus); -$errorListener = new ErrorListener($controller); -$exceptionListener = new ExceptionListener($errorListener); - -$dispatcher = new EventDispatcher(); -$dispatcher->addSubscriber(new RouterListener($matcher, new RequestStack())); -$dispatcher->addListener('kernel.view', [$validateListener, 'onKernelView'], 64); -$dispatcher->addListener('kernel.view', [$writeListener, 'onKernelView'], 32); -$dispatcher->addListener('kernel.view', [$serializeListener, 'onKernelView'], 16); -// TODO: ApiPlatform\EventListener\QueryParameterValidateListener, prio 16 -$dispatcher->addListener('kernel.view', [$respondListener, 'onKernelView'], 8); -$dispatcher->addListener('kernel.request', [$formatListener, 'onKernelRequest'], 28); -$dispatcher->addListener('kernel.request', [$readListener, 'onKernelRequest'], 4); -$dispatcher->addListener('kernel.request', [$deserializeListener, 'onKernelRequest'], 2); -$dispatcher->addListener('kernel.exception', [$validationExceptionListener, 'onKernelException'], 2); -// $dispatcher->addListener('kernel.exception', [$exceptionListener, 'onKernelException'], -96); -$dispatcher->addListener('kernel.response', [$addLinkHeaderListener, 'onKernelResponse'], 2); - -/* - * TODO: - * api_platform.security.listener.request.deny_access kernel.request onSecurity 3 ApiPlatform\Security\EventListener\DenyAccessListener - * " kernel.request onSecurityPostDenormalize 1 - * api_platform.swagger.listener.ui kernel.request onKernelRequest ApiPlatform\Bridge\Symfony\Bundle\EventListener\SwaggerUiListener - * api_platform.http_cache.listener.response.configure kernel.response onKernelResponse -1 ApiPlatform\HttpCache\EventListener\AddHeadersListener -*/ - -final class DocumentationAction -{ - private $openApiFactory; - public function __construct(OpenApiFactoryInterface $openApiFactory) - { - $this->openApiFactory = $openApiFactory; - } - - public function __invoke(Request $request): DocumentationInterface - { - $context = ['base_url' => $request->getBaseUrl(), 'spec_version' => 3]; - if ($request->query->getBoolean('api_gateway')) { - $context['api_gateway'] = true; - } - - return $this->openApiFactory->__invoke($context); - } -} - -$paginationOptions = new PaginationOptions(); -$openApiOptions = new OpenApiOptions('API Platform'); -$jsonSchemaTypeFactory = new TypeFactory($resourceClassResolver); -$jsonSchemaFactory = new SchemaFactory($jsonSchemaTypeFactory, $resourceMetadataFactory, $propertyNameCollectionFactory, $propertyMetadataFactory, $nameConverter, $resourceClassResolver); - -$openApiFactory = new OpenApiFactory($resourceNameCollectionFactory, $resourceMetadataFactory, $propertyNameCollectionFactory, $propertyMetadataFactory, $jsonSchemaFactory, $jsonSchemaTypeFactory, $filterLocator); -$documentationAction = new DocumentationAction($openApiFactory); -$routes->add('api_doc', new Route('/docs.{_format}', ['_controller' => $documentationAction, '_format' => null, '_api_respond' => true])); - -$entryPointAction = new EntrypointAction($resourceNameCollectionFactory); -$routes->add('api_entrypoint', new Route('/{index}.{_format}', ['_controller' => $entryPointAction, '_format' => null, '_api_respond' => true, 'index' => 'index'], ['index' => 'index'])); - -$contextAction = new ContextAction($jsonLdContextBuilder, $resourceNameCollectionFactory, $resourceMetadataFactory); -$routes->add('api_jsonld_context', new Route('/contexts/{shortName}.{_format}', ['_controller' => $contextAction, '_format' => 'jsonld', '_api_respond' => true], ['shortName' => '.+'])); - -$notExposedAction = new NotExposedAction(); -$routes->add('api_genid', new Route('/.well-known/genid/{id}', ['_controller' => $notExposedAction, '_format' => 'text', '_api_respond' => true])); - -$controllerResolver = new ControllerResolver(); -$argumentResolver = new ArgumentResolver(); - -$kernel = new HttpKernel($dispatcher, $controllerResolver, new RequestStack(), $argumentResolver); - -$request = Request::createFromGlobals(); -$response = $kernel->handle($request); -$response->send(); -$kernel->terminate($request, $response); -``` From 8aaa843a2dbd8c5aac434e29975b02457fc94b33 Mon Sep 17 00:00:00 2001 From: soyuka Date: Fri, 5 Dec 2025 11:08:44 +0100 Subject: [PATCH 23/49] fix #2230 --- core/content-negotiation.md | 10 ++++++++++ core/serialization.md | 12 ++++++++++++ 2 files changed, 22 insertions(+) diff --git a/core/content-negotiation.md b/core/content-negotiation.md index d7f1edb5ae9..5eb06705f29 100644 --- a/core/content-negotiation.md +++ b/core/content-negotiation.md @@ -309,6 +309,14 @@ final class CustomItemNormalizer implements NormalizerInterface, DenormalizerInt { return $this->normalizer->supportsNormalization($data, $format); } + + public function getSupportedTypes(?string $format): array + { + return [ + 'object' => null, + '*' => false, + ]; + } } ``` @@ -348,6 +356,8 @@ class CustomItemNormalizer implements NormalizerInterface, DenormalizerInterface } ``` +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 diff --git a/core/serialization.md b/core/serialization.md index 26ccfe807f1..deb79b0a1d0 100644 --- a/core/serialization.md +++ b/core/serialization.md @@ -1042,6 +1042,13 @@ class BookAttributeNormalizer implements NormalizerInterface, NormalizerAwareInt // for the current $object (book) and // return true or false } + + public function getSupportedTypes(?string $format): array + { + return [ + Book::class => true + ]; + } } ``` @@ -1149,6 +1156,11 @@ final class ApiNormalizer implements NormalizerInterface, DenormalizerInterface, $this->decorated->setSerializer($serializer); } } + + public function getSupportedTypes(?string $format): array + { + return $this->decorated->getSupportedTypes($format); + } } ``` From 5dd857bead6034e1679b2af1f58c85f98c935264 Mon Sep 17 00:00:00 2001 From: soyuka Date: Fri, 5 Dec 2025 11:12:13 +0100 Subject: [PATCH 24/49] fix #2221 --- core/configuration.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/core/configuration.md b/core/configuration.md index 961f31af3bd..e658c8af5fd 100644 --- a/core/configuration.md +++ b/core/configuration.md @@ -298,8 +298,7 @@ api_platform: description: ~ iri: ~ short_name: ~ - item_operations: ~ - collection_operations: ~ + operations: ~ graphql: ~ @@ -725,8 +724,7 @@ return [ 'description' => null, 'iri' => null, 'short_name' => null, - 'item_operations' => null, - 'collection_operations' => null, + 'operations' => null, 'graphql' => null, From 812d718c5e6521a745afe5217e5ecdeea1b62dcb Mon Sep 17 00:00:00 2001 From: soyuka Date: Fri, 5 Dec 2025 11:20:58 +0100 Subject: [PATCH 25/49] fix #2189 --- symfony/index.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/symfony/index.md b/symfony/index.md index e69b698dd8e..c9a93268884 100644 --- a/symfony/index.md +++ b/symfony/index.md @@ -94,6 +94,8 @@ docker compose up --wait > 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 | From 54ef2ace06387f9b98730103e1bb3a420fcc6487 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my?= Date: Fri, 5 Dec 2025 11:22:59 +0100 Subject: [PATCH 26/49] doc(provider): show how to enable link security to be able to use ReadLinkParameterProvider (#2217) --- core/filters.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/core/filters.md b/core/filters.md index 29f2213d3eb..639b75f03c2 100644 --- a/core/filters.md +++ b/core/filters.md @@ -561,6 +561,12 @@ The `IriConverterParameterProvider` supports the following options in `extraProp ### `ReadLinkParameterProvider` +This provider must be enabled before it can be used. +``` +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. From 3011261bfe09328cd1146962f9fe63535a7ce5d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=91=A8=E2=80=8D=F0=9F=92=BB=F0=9F=93=8A=20Vincent=20?= =?UTF-8?q?Amstoutz?= Date: Fri, 5 Dec 2025 12:16:58 +0100 Subject: [PATCH 27/49] docs(filters): document enhanced QueryParameter syntax on old filters (#2227) Co-authored-by: Nicolas LAURENT Co-authored-by: Antoine Bluchet --- core/doctrine-filters.md | 931 ++++++++++++--------------------------- core/filters.md | 133 +----- 2 files changed, 311 insertions(+), 753 deletions(-) diff --git a/core/doctrine-filters.md b/core/doctrine-filters.md index 72a99476729..62490615a1d 100644 --- a/core/doctrine-filters.md +++ b/core/doctrine-filters.md @@ -6,8 +6,7 @@ For further documentation on filters (including for Eloquent and Elasticsearch), > [!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 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. @@ -54,11 +53,6 @@ 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). - ## Basic Knowledge Filters are services (see the section on [custom filters](../core/filters.md#creating-custom-filters)), and they can be linked @@ -68,23 +62,6 @@ to a Resource in two ways: For example, having a filter service declaration in `services.yaml`: -```yaml -# api/config/services.yaml -services: - # ... - offer.date_filter: - parent: 'api_platform.doctrine.orm.date_filter' - arguments: [{ dateProperty: ~ }] - 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. - autowire: false - autoconfigure: false - public: false -``` - -Alternatively, you can choose to use a dedicated file to gather filters together: - ```yaml # api/config/filters.yaml services: @@ -94,91 +71,37 @@ services: tags: ['api_platform.filter'] ``` -We're linking the filter `offer.date_filter` with the resource like this: - - - -```php - - - - - - - - - offer.date_filter - - - - - - -``` - - - -2. By using the `#[ApiFilter]` attribute. +> [!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. -This attribute automatically declares the service, and you just have to use the filter class you want: +We're linking the filter `offer.date_filter` with the resource like this: ```php new QueryParameter(filter: 'offer.date_filter')])] class Offer { // ... } ``` -Learn more on how the [ApiFilter attribute](../core/filters.md#1-legacy-filters-searchfilter-etc---not-recommended) works. - -For the sake of consistency, we're using the attribute in the below documentation. - 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 (not recommended) +## Search Filter > [!WARNING] -> Instead of using the deprecated `SearchFilter` its recommended to use the new search filters with QueryParameter attributes +> The SearchFilter is a multi-type filter that may have inconsistencies (eg: you can search a partial date with LIKE) +> we recommend to use type-specific filters such as `PartialSearchFilter` or `DateFilter` instead. -### Built-in new Search Filters (API Platform >= 4.2) +### Built-in new Search Filters since API Platform >= 4.2 To add some search filters, choose over this new list: - [IriFilter](#iri-filter) (filter on IRIs) @@ -187,10 +110,17 @@ To add some search filters, choose over this new list: - [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` ) -### Legacy SearchFilter (API Platform < 4.2)) +### 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: -If Doctrine ORM or MongoDB ODM support is enabled, adding filters is as easy as 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: @@ -210,9 +140,7 @@ Note: Search filters with the `exact` strategy can have multiple values for the Syntax: `?property[]=foo&property[]=bar` -In the following example, we will see how to allow the filtering of a list of e-commerce offers: - - +In the following example, we will see how to allow the filtering of e-commerce offers (a list): ```php 'exact', 'price' => 'exact', 'description' => 'partial'])] +#[GetCollection( + parameters: [ + new QueryParameter( + filter: 'app_search_filter_via_parameter', // the previously declared filter + properties: ['id', 'price', 'description'], + ) + ] +)] class Offer { // ... } ``` -```yaml -# config/services.yaml -services: - offer.search_filter: - parent: 'api_platform.doctrine.orm.search_filter' - arguments: [{ id: 'exact', price: 'exact', description: 'partial' }] - 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.search_filter'] -``` - - - `http://localhost:8000/api/offers?price=10` will return all offers with a price being exactly `10`. `http://localhost:8000/api/offers?description=shirt` will return all offers with a description containing the word "shirt". Filters can be combined: `http://localhost:8000/api/offers?price=10&description=shirt` -It is possible to filter on relations too, if `Offer` has a `Product` relation: - - - -```php - 'exact'])] -class Offer -{ - // ... -} -``` - -```yaml -# config/services.yaml -services: - offer.search_filter: - parent: 'api_platform.doctrine.orm.search_filter' - arguments: [{ product: '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.search_filter'] -``` - - - -With this service definition, it is possible to find all offers belonging to the product identified by a given IRI. -Try the following: `http://localhost:8000/api/offers?product=/api/products/12`. -Using a numeric ID is also supported: `http://localhost:8000/api/offers?product=12` - -The above URLs will return all offers for the product having the following IRI as JSON-LD identifier (`@id`): `http://localhost:8000/api/products/12`. - ## Iri Filter The iri filter allows filtering a resource using IRIs. @@ -401,7 +262,7 @@ 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 -> (e.g., by converting them to lowercase) before making the comparison. +> (for e.g., by converting them to lowercase) before making the comparison. ## Free Text Query Filter @@ -413,7 +274,7 @@ Syntax: `?property=value` The value can take any scalar value or array of values. Like other [new search filters](#built-in-new-search-filters-api-platform--42) it can be used on the ApiResource attribute -or in the operation attribute, for e.g. the `#GetCollection()` attribute: +or in the operation attribute, for e.g., the `#GetCollection()` attribute: ```php // api/src/ApiResource/Chicken.php @@ -501,14 +362,42 @@ The value can take any date format supported by the [`\DateTime` constructor](ht 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: +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 [here](#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`. @@ -564,48 +455,56 @@ Four behaviors are available at the property level of the filter: 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 DateFilterInterface::EXCLUDE_NULL])] +#[Get(parameters: [ + 'dateProperty' => new QueryParameter( + filter: new DateFilter(), + filterContext: DateFilterInterface::EXCLUDE_NULL, + ), +])] class Offer { // ... } ``` -```yaml -# config/services.yaml -services: - offer.date_filter: - parent: 'api_platform.doctrine.orm.date_filter' - arguments: [{ dateProperty: exclude_null }] - 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 +Or you can also use the `properties` attribute on the `DateFilter` to apply your [`null` strategy](#managing-null-values): -# config/api/Offer.yaml -App\Entity\Offer: - # ... - operations: - ApiPlatform\Metadata\GetCollection: - filters: ['offer.date_filter'] +```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 [here](#introduction). + ## Boolean Filter @@ -615,47 +514,33 @@ Syntax: `?property=` Enable the filter: - +### Boolean Filter using the QueryParameter Syntax ```php new QueryParameter(filter: new BooleanFilter()), + ] +)] class Offer { // ... } ``` -```yaml -# config/services.yaml -services: - offer.boolean_filter: - parent: 'api_platform.doctrine.orm.boolean_filter' - arguments: [{ isAvailableGenericallyInMyCountry: ~ }] - 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.boolean_filter'] -``` +> [!TIP] +> For other syntaxes, for e.g., if you want to new syntax with the ApiResource attribute take a look [here](#introduction). - +### Result using the Boolean Filter Given that the collection endpoint is `/offers`, you can filter offers with the following query: `/offers?isAvailableGenericallyInMyCountry=true`. @@ -669,47 +554,33 @@ Syntax: `?property=` Enable the filter: - +### Numeric Filter using the QueryParameter Syntax ```php new QueryParameter(filter: new NumericFilter()), + ] +)] class Offer { // ... } ``` -```yaml -# config/services.yaml -services: - offer.numeric_filter: - parent: 'api_platform.doctrine.orm.numeric_filter' - arguments: [{ sold: ~ }] - 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 +> [!TIP] +> For other syntaxes, for e.g., if you want to new syntax with the ApiResource attribute take a look [here](#introduction). -# config/api/Offer.yaml -App\Entity\Offer: - # ... - operations: - ApiPlatform\Metadata\GetCollection: - filters: ['offer.numeric_filter'] -``` - - +### Result using the Numeric Filter Given that the collection endpoint is `/offers`, you can filter offers with the following query: `/offers?sold=1`. @@ -723,47 +594,33 @@ Syntax: `?property[]=value` Enable the filter: - +### Range Filter using the QueryParameter Syntax ```php new QueryParameter(filter: new RangeFilter()), + ] +)] class Offer { // ... } ``` -```yaml -# config/services.yaml -services: - offer.range_filter: - parent: 'api_platform.doctrine.orm.range_filter' - arguments: [{ price: ~ }] - 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.range_filter'] -``` +> [!TIP] +> For other syntaxes, for e.g., if you want to new syntax with the ApiResource attribute take a look [here](#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`. @@ -780,53 +637,42 @@ Syntax: `?exists[property]=` Enable the filter: - +### Exists Filter using the QueryParameter Syntax ```php new QueryParameter(filter: new ExistsFilter()), + ] +)] class Offer { // ... } ``` -```yaml -# config/services.yaml -services: - offer.exists_filter: - parent: 'api_platform.doctrine.orm.exists_filter' - arguments: [{ transportFees: ~ }] - 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.exists_filter'] -``` +> [!TIP] +> For other syntaxes, for e.g., if you want to new syntax with the ApiResource attribute take a look [here](#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 +### 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: @@ -846,99 +692,130 @@ 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 'order'])] +#[GetCollection( + parameters: [ + 'order[:property]' => new QueryParameter(filter: new OrderFilter()), + ] +)] class Offer { // ... } ``` -```yaml -# config/services.yaml -services: - offer.order_filter: - parent: 'api_platform.doctrine.orm.order_filter' - arguments: - $properties: { id: ~, name: ~ } - $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 +After that, you can use it with the following query: `/offers?order[name]=desc&order[id]=asc`. -# config/api/Offer.yaml -App\Entity\Offer: - # ... - operations: - ApiPlatform\Metadata\GetCollection: - filters: ['offer.order_filter'] -``` +> [!TIP] +> For other syntaxes, for e.g., if you want to new syntax with the ApiResource attribute take a look [here](#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 'ASC', 'name' => 'DESC'])] +#[GetCollection( + parameters: [ + 'id' => new QueryParameter(filter: new OrderFilter(), filterContext: OrderFilterInterface::DIRECTION_ASC ), + 'name' => new QueryParameter(filter: new OrderFilter(), filterContext: OrderFilterInterface::DIRECTION_DESC), + ] +)] class Offer { // ... } ``` -```yaml -# config/services.yaml -services: - offer.order_filter: - parent: 'api_platform.doctrine.orm.order_filter' - arguments: [{ id: 'ASC', name: 'DESC' }] - 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 +Or you can also use the `properties` attribute on the `OrderFilter` to apply your [`direction` strategy](#basic-directions-strategies-with-the-order-filter): -# config/api/Offer.yaml -App\Entity\Offer: - # ... - operations: - ApiPlatform\Metadata\GetCollection: - filters: ['offer.order_filter'] -``` +```php + +use ApiPlatform\Doctrine\Common\Filter\OrderFilterInterface; +use ApiPlatform\Metadata\QueryParameter; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Doctrine\Orm\Filter\OrderFilter; + +#[GetCollection( + parameters: [ + 'id' => 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 +### 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: @@ -951,80 +828,51 @@ the comparison: | 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 ['nulls_comparison' => OrderFilterInterface::NULLS_SMALLEST, 'default_direction' => 'DESC']])] +#[GetCollection( + parameters: [ + 'nulls_comparison' => 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 { // ... } ``` -```yaml -# config/services.yaml -services: - offer.order_filter: - parent: 'api_platform.doctrine.orm.order_filter' - arguments: - [ - { - validFrom: - { nulls_comparison: 'nulls_smallest', default_direction: 'DESC' }, - }, - ] - 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'] -``` - - - -The strategy to use by default can be configured globally: - -```yaml -# api/config/packages/api_platform.yaml -api_platform: - collection: - order_nulls_comparison: 'nulls_smallest' -``` - -### Using a Custom Order Query Parameter Name - -A conflict will occur if `order` 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: - order_parameter_name: '_order' # the URL query parameter to use is now "_order" -``` - ## Filtering on Nested Properties +> [!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). +> For nested properties support we recommend to use a custom filter. + Sometimes, you need to be able to perform filtering based on some linked resources (on the other side of a relation). All built-in filters support nested properties using the dot (`.`) syntax, e.g.: @@ -1057,7 +905,7 @@ services: 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) + # 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 @@ -1066,7 +914,7 @@ services: 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) + # 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 @@ -1086,8 +934,13 @@ or order offers by the product's release date: `http://localhost:8000/api/offers ## 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 (e.g. an API with restricted access), it is also possible to enable built-in filters +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: @@ -1117,7 +970,7 @@ services: arguments: [~] # Pass null to enable the filter for all properties 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) + # 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 @@ -1132,7 +985,7 @@ App\Entity\Offer: -**Note: Filters on nested properties must still be enabled explicitly, in order to keep things sane.** +**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: @@ -1141,8 +994,8 @@ Regardless of this option, filters can be applied on a property only if: It means that the filter will be **silently** ignored if the property: -- does not exist -- is not enabled +- it does not exist +- it is not enabled - has an invalid value @@ -1188,8 +1041,8 @@ final class SearchTextAndDateFilter implements FilterInterface $this->dateFilter->setProperties($this->dateFilterProperties); } - $this->searchFilter->apply($queryBuilder, $queryNameGenerator, $resourceClass, $operation, ['filters' => $context['filters']['searchOnTextAndDate']] + $context); - $this->dateFilter->apply($queryBuilder, $queryNameGenerator, $resourceClass, $operation, ['filters' => $context['filters']['searchOnTextAndDate']] + $context); + $this->searchFilter->apply($queryBuilder, $queryNameGenerator, $resourceClass, $operation, $context); + $this->dateFilter->apply($queryBuilder, $queryNameGenerator, $resourceClass, $operation, $context); } } ``` @@ -1254,7 +1107,7 @@ class SearchFilterParameter ## 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 (e.g. from a DQL query, or by loading associated entities). +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). @@ -1458,7 +1311,7 @@ class MyCustomFilter implements FilterInterface ``` #### Implementing a Custom ORM Filter -Let's create a concrete filter that allows fetching entities based on the month of a date field (e.g., `createdAt`). +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. @@ -1658,209 +1511,6 @@ final class MyComplexFilter implements FilterInterface, OpenApiParameterFilterIn } ``` -### Creating Custom Doctrine ORM Filters With The Old Syntax (API Platform < 4.2) - - -API Platform includes a convenient abstract class implementing this interface and providing utility methods: `ApiPlatform\Doctrine\Orm\Filter\AbstractFilter`. - -In the following example, we create a class to filter a collection by applying a regular expression to a property. -The `REGEXP` DQL function used in this example can be found in the [`DoctrineExtensions`](https://github.com/beberlei/DoctrineExtensions) -library. This library must be properly installed and registered to use this example (works only with MySQL). - -```php -isPropertyEnabled($property, $resourceClass) || - !$this->isPropertyMapped($property, $resourceClass) - ) { - return; - } - - $parameterName = $queryNameGenerator->generateParameterName($property); // Generate a unique parameter name to avoid collisions with other filters - $queryBuilder - ->andWhere(sprintf('REGEXP(o.%s, :%s) = 1', $property, $parameterName)) - ->setParameter($parameterName, $value); - } - - // This function is only used to hook in documentation generators (supported by Swagger and Hydra) - public function getDescription(string $resourceClass): array - { - if (!$this->properties) { - return []; - } - - $description = []; - foreach ($this->properties as $property => $strategy) { - $description["regexp_$property"] = [ - 'property' => $property, - 'type' => Type::BUILTIN_TYPE_STRING, - 'required' => false, - 'description' => 'Filter using a regex. This will appear in the OpenApi documentation!', - 'openapi' => new Parameter( - name: $property, - in: 'query', - allowEmptyValue: true, - explode: false, // to be true, the type must be Type::BUILTIN_TYPE_ARRAY, ?product=blue,green will be ?product=blue&product=green - allowReserved: false, // if true, query parameters will be not percent-encoded - example: 'Custom example that will be in the documentation and be the default value of the sandbox', - ), - ]; - } - - return $description; - } -} -``` - -Thanks to [Symfony's automatic service loading](https://symfony.com/doc/current/service_container.html#service-container-services-load-example), which is enabled by default in the API Platform distribution, the filter is automatically registered as a service! - -Finally, add this filter to resources you want to be filtered by using the `ApiFilter` attribute: - -```php -getRootAliases()[0]; - foreach(array_keys($this->getProperties()) as $prop) { // we use array_keys() because getProperties() returns a map of property => strategy - if (!$this->isPropertyEnabled($prop, $resourceClass) || !$this->isPropertyMapped($prop, $resourceClass)) { - return; - } - $parameterName = $queryNameGenerator->generateParameterName($prop); - $queryBuilder - ->andWhere(sprintf('%s.%s LIKE :%s', $rootAlias, $prop, $parameterName)) - ->setParameter($parameterName, "%" . $value . "%"); - } -} -``` - -### Manual Service and Attribute Registration - -If you don't use Symfony's automatic service loading, you have to register the filter as a service by yourself. -Use the following service definition (remember, by default, this isn't needed!): - -```yaml -# api/config/services.yaml -services: - # ... - # This whole definition can be omitted if automatic service loading is enabled - 'App\Filter\RegexpFilter': - # The "arguments" key can be omitted if the autowiring is enabled - arguments: ['@doctrine', '@?logger'] - # The "tags" key can be omitted if the autoconfiguration is enabled - tags: ['api_platform.filter'] -``` - -In the previous example, the filter can be applied to any property. However, thanks to the `AbstractFilter` class, -it can also be enabled for some properties: - -```yaml -# api/config/services.yaml -services: - 'App\Filter\RegexpFilter': - arguments: ['@doctrine', '@?logger', { email: ~, anOtherProperty: ~ }] - tags: ['api_platform.filter'] -``` - -Finally, if you don't want to use the `#[ApiFilter]` attribute, you can register the filter on an API resource class using the `filters` attribute: - -```php - [!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 parameters attribute using `QueryParameter`. The legacy method using the `ApiFilter` attribute is not recommended. ## Declaring Parameters @@ -205,16 +204,14 @@ 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 populates the parameter's `extraProperties` with a `_properties` array containing all the available properties for the filter. Your filter can access this information: +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; - $properties = $parameter?->getExtraProperties()['_properties'] ?? []; - - // $properties contains: ['title' => 'title', 'description' => 'description'] - // This allows your filter to know which properties are available for filtering + dump(key: $parameter->getKey(), value: $parameter->getValue()); + // shows key: search[title], value: Ring } ``` @@ -225,35 +222,28 @@ public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $q ### Restricting Properties with `:property` Placeholders -There are two different approaches to property restriction depending on your filter design: - -#### 1. Legacy Filters (SearchFilter, etc.) - Not Recommended - -> [!WARNING] -> Filters that extend `AbstractFilter` with pre-configured properties are considered legacy. They don't support property restriction via parameters and may be deprecated in future versions. Consider using per-parameter filters instead for better flexibility and performance. - -For existing filters that extend `AbstractFilter` and have pre-configured properties, the parameter's `properties` does **not** restrict the filter's behavior. These filters use their own internal property configuration: +Filters that work on a per-parameter basis can also use the `:property` placeholde and use the parameter's `properties` configuration: ```php new QueryParameter( - properties: ['title', 'author'], // Only affects _properties, doesn't restrict filter - filter: new SearchFilter(properties: ['title' => 'partial', 'description' => 'partial']) -) - -// To restrict legacy filters, configure them with only the desired properties: -'search[:property]' => new QueryParameter( - filter: new SearchFilter(properties: ['title' => 'partial', 'author' => 'exact']) -) +// api/src/Resource/Book.php +#[ApiResource(operations: [ + new GetCollection( + parameters: [ + // This WILL restrict to only title and author properties + 'search[:property]' => new QueryParameter( + properties: ['title', 'author'], // Only these properties get parameters created, defaults to all properties + filter: new PartialSearchFilter() + ) + ] + ) +])] +class Book { + // ... +} ``` -#### 2. Per-Parameter Filters (Recommended) - -> [!NOTE] -> Per-parameter filters are the modern approach. They provide better performance (only process requested properties), cleaner code, and full support for parameter-based property restriction. - -Modern filters that work on a per-parameter basis can be effectively restricted using the parameter's `properties`: +This will create 2 parameters: `search[title]` and `search[author]`, here is an example of the associated filter for Doctrine ORM: ```php new QueryParameter( - properties: ['title', 'author'], // Only these properties get parameters created - filter: new PartialSearchFilter() - ) - ] - ) -])] -class Book { - // ... -} -``` - **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) @@ -761,66 +732,8 @@ use App\Filter\RegexpFilter; class User {} ``` -### Advanced Use Case: Composing Filters - -You can create complex filters by composing existing ones. This is useful when you want to apply multiple filtering logics based on a single parameter. - -```php -getValue(); - if ($value instanceof ParameterNotFound) { - return; - } - - // Create a new context for the sub-filters, passing the value. - $subContext = ['filters' => ['searchOnTextAndDate' => $value]] + $context; - - $this->searchFilter->apply($queryBuilder, $queryNameGenerator, $resourceClass, $operation, $subContext); - $this->dateFilter->apply($queryBuilder, $queryNameGenerator, $resourceClass, $operation, $subContext); - } -} -``` - -To use this composite filter, register it as a service and reference it by its ID: - -```yaml -# config/services.yaml -services: - 'app.filter_date_and_search': - class: App\Filter\SearchTextAndDateFilter - autowire: true -```php - new QueryParameter(filter: 'app.filter_date_and_search') - ] - ) -])] -class LogEntry {} -``` +> [!NOTE] +> A `filter` is either an instanceof `FilterInterface` or a string referencing a filter service. ## Parameter Attribute Reference From 2e12e2444201bb089650b347ffb134df6d6de3e9 Mon Sep 17 00:00:00 2001 From: soyuka Date: Fri, 5 Dec 2025 12:30:13 +0100 Subject: [PATCH 28/49] default fixup --- .github/workflows/ci.yml | 14 +++--- .markdownlint.yaml | 9 ++++ .markdownlint.yml | 5 -- CONTRIBUTING.md | 4 ++ README.md | 1 - admin/components.md | 2 - admin/customizing.md | 1 - admin/getting-started.md | 2 +- admin/index.md | 2 +- admin/validation.md | 18 +++---- core/bootstrap.md | 2 - core/client-integration.md | 1 + core/doctrine-filters.md | 18 ++++--- core/dto.md | 2 - core/elasticsearch-filters.md | 1 + core/elasticsearch.md | 1 + core/enums.md | 5 +- core/filters.md | 88 +++++++++++++++++---------------- core/form-data.md | 2 +- core/pagination.md | 1 - core/performance.md | 8 +-- core/state-processors.md | 5 ++ core/state-providers.md | 3 +- core/url-generation-strategy.md | 1 + create-client/nextjs.md | 21 +++++++- deployment/heroku.md | 8 +-- extra/contribution-guides.md | 3 ++ laravel/filters.md | 4 ++ laravel/jwt.md | 2 - laravel/testing.md | 1 + symfony/file-upload.md | 1 + symfony/jwt.md | 1 + symfony/security.md | 1 - 33 files changed, 138 insertions(+), 100 deletions(-) create mode 100644 .markdownlint.yaml delete mode 100644 .markdownlint.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 527d094cb6f..eb4e3530c2b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,14 +24,12 @@ jobs: with: fetch-depth: 0 - - name: Lint - uses: super-linter/super-linter/slim@v7 - env: - VALIDATE_EDITORCONFIG: false - VALIDATE_JSCPD: false - VALIDATE_MARKDOWN_PRETTIER: false - DEFAULT_BRANCH: "origin/4.2" - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - uses: articulate/actions-markdownlint@v1 + with: + config: .markdownlint.yaml + files: '**/*.md' + ignore: node_modules + version: 0.46.1 - uses: actions/cache@v4 with: diff --git a/.markdownlint.yaml b/.markdownlint.yaml new file mode 100644 index 00000000000..8e7a2ff30ac --- /dev/null +++ b/.markdownlint.yaml @@ -0,0 +1,9 @@ +--- +MD013: + line_length: 400 +no-inline-html: + allowed_elements: [a, p, img, br, code-selector] +MD046: + style: fenced +MD004: + style: dash diff --git a/.markdownlint.yml b/.markdownlint.yml deleted file mode 100644 index 754eda95a84..00000000000 --- a/.markdownlint.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -MD013: - line_length: 400 -no-inline-html: - allowed_elements: [a, p, img, br] diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ad1e6404e08..dfc0c7cd5e4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,13 +24,17 @@ By contributing to this project, you agree to abide by our [Code of Conduct](htt 1. Fork this repository by clicking the "Fork" button at the top right of the `api-platform/docs` repository page. 2. Clone the forked repository to your local machine: + ```console git clone https://github.com/your-username/repository-name.git ``` + 3. Create a new branch for your contribution: + ```console git switch -c docs-your-branch-name ``` + 4. Commit and push your changes 5. Submit a Pull Request. You must decide on what branch your changes will be based depending of the nature of the change. See [the dedicated documentation entry](https://api-platform.com/docs/extra/releases/). diff --git a/README.md b/README.md index f32cb8d650a..0267b5e84ea 100644 --- a/README.md +++ b/README.md @@ -11,4 +11,3 @@ This repository contains all the API Platform documentation resources. ## Contributing Please check our [CONTRIBUTING file](/CONTRIBUTING.md) to contribute. - diff --git a/admin/components.md b/admin/components.md index 5aca722b8cf..4ffd161b7ce 100644 --- a/admin/components.md +++ b/admin/components.md @@ -379,7 +379,6 @@ export const BooksShow = () => ( `` 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). @@ -445,4 +444,3 @@ You can also pass props that are specific to a certain input component. For exam ```tsx ``` - diff --git a/admin/customizing.md b/admin/customizing.md index 83001b3540e..4d80cd5a0f1 100644 --- a/admin/customizing.md +++ b/admin/customizing.md @@ -277,7 +277,6 @@ Let's customize this `ReviewEdit` component to: - make the `body` input multiline - mark the `publicationDate` input as read-only - ```diff export const ReviewEdit = () => ( diff --git a/admin/getting-started.md b/admin/getting-started.md index cbd2c535fe7..d979763fa0a 100644 --- a/admin/getting-started.md +++ b/admin/getting-started.md @@ -125,4 +125,4 @@ bin/console cache:clear --env=prod ## Next Step -Learn how to add more features to your generated Admin by [Customizing the Schema](./schema.md). \ No newline at end of file +Learn how to add more features to your generated Admin by [Customizing the Schema](./schema.md). diff --git a/admin/index.md b/admin/index.md index 6330913770d..e39d45a6565 100644 --- a/admin/index.md +++ b/admin/index.md @@ -56,4 +56,4 @@ By [leveraging React Admin components](./advanced-customization.md), you can fur ## Next Step -Get your Admin up and running by following the [Getting Started guide](./getting-started.md). \ No newline at end of file +Get your Admin up and running by following the [Getting Started guide](./getting-started.md). diff --git a/admin/validation.md b/admin/validation.md index 777ecdd318d..121f73b1bf6 100644 --- a/admin/validation.md +++ b/admin/validation.md @@ -82,15 +82,15 @@ 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 +- `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). diff --git a/core/bootstrap.md b/core/bootstrap.md index b7e43655fb4..40c781c745c 100644 --- a/core/bootstrap.md +++ b/core/bootstrap.md @@ -22,5 +22,3 @@ composer require \ symfony/routing \ symfony/validator ``` - - diff --git a/core/client-integration.md b/core/client-integration.md index 0d3ab5ad7c6..78fe20d34d6 100644 --- a/core/client-integration.md +++ b/core/client-integration.md @@ -137,6 +137,7 @@ 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. diff --git a/core/doctrine-filters.md b/core/doctrine-filters.md index 62490615a1d..1414d1523be 100644 --- a/core/doctrine-filters.md +++ b/core/doctrine-filters.md @@ -32,6 +32,7 @@ class Book { // ... } ``` + > [!TIP] > This filter can be also defined directly on a specific operation like `#[GetCollection(...)])` for finer > control, like the following code: @@ -73,7 +74,7 @@ services: > [!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 +> `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. We're linking the filter `offer.date_filter` with the resource like this: @@ -104,6 +105,7 @@ services all begin with `api_platform.doctrine_mongodb.odm`. ### Built-in new Search Filters since API Platform >= 4.2 To add some search filters, choose over this new list: + - [IriFilter](#iri-filter) (filter on IRIs) - [ExactFilter](#exact-filter) (filter with exact value) - [PartialSearchFilter](#partial-search-filter) (filter using a `LIKE %value%`) @@ -505,7 +507,6 @@ class Offer > [!TIP] > For other syntaxes, for e.g., if you want to new syntax with the ApiResource attribute take a look [here](#introduction). - ## Boolean Filter The boolean filter allows you to search on boolean fields and values. @@ -594,7 +595,7 @@ Syntax: `?property[]=value` Enable the filter: -### Range Filter using the QueryParameter Syntax +### Range Filter using the QueryParameter Syntax ```php > You can see how this works directly in our code components: > -> * The `ParameterValidatorProvider` for **Symfony** can be found [here](https://github.com/api-platform/core/blob/c9692b509d5b641104addbadb349b9bcab83e251/src/Symfony/Validator/State/ParameterValidatorProvider.php). -> * The `ParameterValidatorProvider` for **Laravel** is located [here](https://github.com/api-platform/core/blob/c9692b509d5b641104addbadb349b9bcab83e251/src/Laravel/State/ParameterValidatorProvider.php). +> - The `ParameterValidatorProvider` for **Symfony** can be found [here](https://github.com/api-platform/core/blob/c9692b509d5b641104addbadb349b9bcab83e251/src/Symfony/Validator/State/ParameterValidatorProvider.php). +> - The `ParameterValidatorProvider` for **Laravel** is located [here](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). @@ -1576,6 +1577,7 @@ class MonthFilter implements FilterInterface } } ``` + #### 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`). @@ -1666,8 +1668,8 @@ This allows delegating validation to API Platform, respecting the [SOLID Princip > > You can see how this works directly in our code components: > -> * The `ParameterValidatorProvider` for **Symfony** can be found [here](https://github.com/api-platform/core/blob/c9692b509d5b641104addbadb349b9bcab83e251/src/Symfony/Validator/State/ParameterValidatorProvider.php). -> * The `ParameterValidatorProvider` for **Laravel** is located [here](https://github.com/api-platform/core/blob/c9692b509d5b641104addbadb349b9bcab83e251/src/Laravel/State/ParameterValidatorProvider.php). +> - The `ParameterValidatorProvider` for **Symfony** can be found [here](https://github.com/api-platform/core/blob/c9692b509d5b641104addbadb349b9bcab83e251/src/Symfony/Validator/State/ParameterValidatorProvider.php). +> - The `ParameterValidatorProvider` for **Laravel** is located [here](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). diff --git a/core/dto.md b/core/dto.md index bf6633bff8b..96511287d25 100644 --- a/core/dto.md +++ b/core/dto.md @@ -14,7 +14,6 @@ This reference covers three implementation strategies: * [Automated Mapped Inputs: Using input DTOs with stateOptions for automated Write operations.](#2-automated-mapped-inputs-outputs) * [Custom Business Logic: Using input DTOs with custom State Processors for specific business actions.](#3-custom-business-logic-custom-processor) - ## 1. The DTO Resource (State Options) > [!WARNING] @@ -131,7 +130,6 @@ The `ObjectMapperProvider` delegates fetching the data to the underlying Doctrin 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 & 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). diff --git a/core/elasticsearch-filters.md b/core/elasticsearch-filters.md index efad12f70a1..28e950b8e3f 100644 --- a/core/elasticsearch-filters.md +++ b/core/elasticsearch-filters.md @@ -30,6 +30,7 @@ class Book { // ... } ``` + > [!TIP] > This filter can be also defined directly on a specific operation like `#[GetCollection(...)])` for finer > control, like the following code: diff --git a/core/elasticsearch.md b/core/elasticsearch.md index 53b9db80951..a7c26a8611c 100644 --- a/core/elasticsearch.md +++ b/core/elasticsearch.md @@ -21,6 +21,7 @@ composer require elasticsearch/elasticsearch:^8.4 ``` For Elasticsearch 7: + ```console composer require elasticsearch/elasticsearch:^7.11 ``` diff --git a/core/enums.md b/core/enums.md index 1afb2441678..0d658bf7717 100644 --- a/core/enums.md +++ b/core/enums.md @@ -26,8 +26,8 @@ By default, API Platform will automatically generate `GET` and `GET Collection` ### Default Operations and Identifiers -- **Collection**: `GET /statuses` will return a collection of all enum cases. -- **Item**: `GET /statuses/{value}` will return a single enum case based on its `value`. +- **Collection**: `GET /statuses` will return a collection of all enum cases. +- **Item**: `GET /statuses/{value}` will return a single enum case based on its `value`. Example `GET /statuses` response: @@ -87,6 +87,7 @@ enum Audit: string } } ``` + #### Adding Custom Properties You can add custom properties to your enum resource by defining public methods and marking them with `#[ApiProperty]`: diff --git a/core/filters.md b/core/filters.md index 39dfc1c32cb..63e9572f380 100644 --- a/core/filters.md +++ b/core/filters.md @@ -21,33 +21,33 @@ You can declare parameters on a resource class to apply them to all operations, 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`). +- `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. +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: -* **`DateFilter`**: For filtering by date intervals (e.g., `?createdAt[after]=...`). - * Usage: `new QueryParameter(filter: DateFilter::class)` -* **`ExactFilter`**: For exact value matching. - * Usage: `new QueryParameter(filter: ExactFilter::class)` -* **`PartialSearchFilter`**: For partial string matching (SQL `LIKE %...%`). - * Usage: `new QueryParameter(filter: PartialSearchFilter::class)` -* **`IriFilter`**: For filtering by IRIs (e.g., relations). - * Usage: `new QueryParameter(filter: IriFilter::class)` -* **`BooleanFilter`**: For boolean field filtering. - * Usage: `new QueryParameter(filter: BooleanFilter::class)` -* **`NumericFilter`**: For numeric field filtering. - * Usage: `new QueryParameter(filter: NumericFilter::class)` -* **`RangeFilter`**: For range-based filtering (e.g., prices between X and Y). - * Usage: `new QueryParameter(filter: RangeFilter::class)` -* **`ExistsFilter`**: For checking existence of nullable values. - * Usage: `new QueryParameter(filter: ExistsFilter::class)` -* **`OrderFilter`**: For sorting results. - * Usage: `new QueryParameter(filter: OrderFilter::class)` +- **`DateFilter`**: For filtering by date intervals (e.g., `?createdAt[after]=...`). + - Usage: `new QueryParameter(filter: DateFilter::class)` +- **`ExactFilter`**: For exact value matching. + - Usage: `new QueryParameter(filter: ExactFilter::class)` +- **`PartialSearchFilter`**: For partial string matching (SQL `LIKE %...%`). + - Usage: `new QueryParameter(filter: PartialSearchFilter::class)` +- **`IriFilter`**: For filtering by IRIs (e.g., relations). + - Usage: `new QueryParameter(filter: IriFilter::class)` +- **`BooleanFilter`**: For boolean field filtering. + - Usage: `new QueryParameter(filter: BooleanFilter::class)` +- **`NumericFilter`**: For numeric field filtering. + - Usage: `new QueryParameter(filter: NumericFilter::class)` +- **`RangeFilter`**: For range-based filtering (e.g., prices between X and Y). + - Usage: `new QueryParameter(filter: RangeFilter::class)` +- **`ExistsFilter`**: For checking existence of nullable values. + - Usage: `new QueryParameter(filter: ExistsFilter::class)` +- **`OrderFilter`**: For sorting results. + - 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. @@ -131,8 +131,8 @@ class Event This configuration allows clients to filter events by date ranges using queries like: - * `/events?date[startDate][after]=2023-01-01` - * `/events?date[endDate][before]=2023-12-31` +- `/events?date[startDate][after]=2023-01-01` +- `/events?date[endDate][before]=2023-12-31` ### Filtering a Single Property @@ -158,6 +158,7 @@ class Issue {} ``` This will generate the following Hydra `IriTemplateMapping`: + ```json { "@context": "http://www.w3.org/ns/hydra/context.jsonld", @@ -275,6 +276,7 @@ final class PartialSearchFilter implements FilterInterface ``` **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()` @@ -386,30 +388,30 @@ Here is the list of validation constraints that are automatically inferred from ### From OpenAPI Definition -* **`allowEmptyValue`**: If set to `false`, a `Symfony\Component\Validator\Constraints\NotBlank` constraint is added. +- **`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`). +- **`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. +- **`required`**: If set to `true`, a `Symfony\Component\Validator\Constraints\NotNull` constraint is added. ### Strict Parameter Validation @@ -533,6 +535,7 @@ The `IriConverterParameterProvider` supports the following options in `extraProp ### `ReadLinkParameterProvider` This provider must be enabled before it can be used. + ``` api_platform: enable_link_security: true @@ -580,6 +583,7 @@ class WithParameter ``` 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 @@ -790,8 +794,8 @@ 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` - diff --git a/core/form-data.md b/core/form-data.md index 27e9318ec93..803f4443eab 100644 --- a/core/form-data.md +++ b/core/form-data.md @@ -30,7 +30,7 @@ api_platform: ## Creating a Decoder -The Symfony Serializer (used by API Platform) does not decode `application/x-www-form-urlencoded` by default. +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 diff --git a/core/pagination.md b/core/pagination.md index 6c9018ed965..a863d683074 100644 --- a/core/pagination.md +++ b/core/pagination.md @@ -748,4 +748,3 @@ class BookRepository extends ServiceEntityRepository } } ``` - diff --git a/core/performance.md b/core/performance.md index 26e9ccaff71..bfacb28b524 100644 --- a/core/performance.md +++ b/core/performance.md @@ -224,7 +224,6 @@ return [ ]; ``` - 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). @@ -341,6 +340,7 @@ with a huge collection. [Here are some examples to index LIKE filters](https://u ### 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). @@ -530,7 +530,7 @@ services: - BLACKFIRE_DISABLE_LEGACY_PORT=1 ``` -2. 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): +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 @@ -544,7 +544,7 @@ export BLACKFIRE_SERVER_ID=xxxxxxxxxx export BLACKFIRE_SERVER_TOKEN=xxxxxxxxxx ``` -3. Install and configure the Blackfire probe in the app container, by adding the following to your `./Dockerfile`: +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' : '');") \ @@ -557,7 +557,7 @@ export BLACKFIRE_SERVER_TOKEN=xxxxxxxxxx && rm -rf /tmp/blackfire /tmp/blackfire-probe.tar.gz ``` -4. Rebuild and restart all your containers +1. Rebuild and restart all your containers ```console docker compose build diff --git a/core/state-processors.md b/core/state-processors.md index bf52a79b5d4..c43b2b89e14 100644 --- a/core/state-processors.md +++ b/core/state-processors.md @@ -24,6 +24,7 @@ process the data for a given resource will be used. ## Creating a Custom State Processor ### Custom State Processor with Symfony + If the [Symfony MakerBundle](https://symfony.com/doc/current/bundles/SymfonyMakerBundle) is installed in your project, you can use the following command to generate a custom state processor easily: ```console @@ -80,7 +81,9 @@ class BlogPost {} ``` ### Custom State Processor with Laravel + Using [Laravel Artisan Console](https://laravel.com/docs/artisan), you can generate a custom state processor easily with the following command: + ```console php artisan make:state-processor ``` @@ -137,6 +140,7 @@ class BlogPost {} ## 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: @@ -212,6 +216,7 @@ class User {} ``` ### Laravel 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 [Laravel Mail](https://laravel.com/docs/mail) to send new users a welcome email after a REST `POST` or GraphQL `create` operation, in a project using the native Eloquent ORM state processor: diff --git a/core/state-providers.md b/core/state-providers.md index cbb88f0c360..ced1e0ff588 100644 --- a/core/state-providers.md +++ b/core/state-providers.md @@ -11,7 +11,6 @@ With the Laravel variant, a state provider using [Eloquent ORM](https://laravel. The ORM providers are enabled by default, based on your framework variant (Eloquent or Doctrine will be set up). - These state providers natively support paged collections and filters. They can be used as-is and are perfectly suited to common uses. However, you sometimes want to retrieve data from other sources such as another persistence layer or a webservice. @@ -21,6 +20,7 @@ retrieve data for a given resource will be used. To do so you need to implement the `ApiPlatform\State\ProviderInterface`. In the following examples we will create custom state providers for Symfony entities and Laravel models: + - For Symfony we will create an entity class called `App\Entity\BlogPost`. - For Laravel, we will create a model class called `App\Models\BlogPost`. @@ -391,7 +391,6 @@ use App\State\BookRepresentationProvider; class Book {} ``` - ## Registering Services Without Autowiring (only for the Symfony variant) The services in the previous examples are automatically registered because diff --git a/core/url-generation-strategy.md b/core/url-generation-strategy.md index b997b85d64a..98efcb34d6e 100644 --- a/core/url-generation-strategy.md +++ b/core/url-generation-strategy.md @@ -34,6 +34,7 @@ api_platform: defaults: url_generation_strategy: !php/const ApiPlatform\Metadata\UrlGeneratorInterface::ABS_URL ``` + ### Configure URL Generation Globally using Laravel ```php diff --git a/create-client/nextjs.md b/create-client/nextjs.md index b82942ede2e..84263fee8cf 100644 --- a/create-client/nextjs.md +++ b/create-client/nextjs.md @@ -16,30 +16,39 @@ 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 ``` @@ -62,14 +71,19 @@ Omit the resource flag to generate files for all resource types exposed by the A 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 ``` @@ -101,14 +115,19 @@ export default App; You can launch the server with: - Pnpm (recommended) + ```console pnpm dev ``` + - Npm + ```console npm run dev ``` + - Yarn + ```console yarn dev ``` diff --git a/deployment/heroku.md b/deployment/heroku.md index 4536de6e583..b988b8f7dc7 100644 --- a/deployment/heroku.md +++ b/deployment/heroku.md @@ -101,25 +101,25 @@ Go to the `api/` directory, then git init ``` -2. Add all existing files: +1. Add all existing files: ```console git add --all ``` -3. Commit: +1. Commit: ```console git commit -a -m "My first API Platform app running on Heroku!" ``` -4. Create the Heroku application: +1. Create the Heroku application: ```console heroku create ``` -5. And deploy for the first time: +1. And deploy for the first time: ```console git push heroku master diff --git a/extra/contribution-guides.md b/extra/contribution-guides.md index 7c9c9a20502..ee0a3b95bb0 100644 --- a/extra/contribution-guides.md +++ b/extra/contribution-guides.md @@ -1,13 +1,16 @@ # 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) diff --git a/laravel/filters.md b/laravel/filters.md index 433f99c4034..06b9f1ee8c9 100644 --- a/laravel/filters.md +++ b/laravel/filters.md @@ -244,7 +244,9 @@ class Book extends Model } } ``` + Examples: + - `/books?published=true` - `/books?published=1` - `/books?published=false` @@ -362,6 +364,7 @@ final class MonthFilter implements FilterInterface ``` We can now use it in our resources and model like other filters, for example, as follows: + ```php 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 = { diff --git a/symfony/jwt.md b/symfony/jwt.md index 5f25ae75192..cfb114dbf76 100644 --- a/symfony/jwt.md +++ b/symfony/jwt.md @@ -17,6 +17,7 @@ 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: diff --git a/symfony/security.md b/symfony/security.md index 1f7c61b3b35..cd7d471d2b9 100644 --- a/symfony/security.md +++ b/symfony/security.md @@ -225,7 +225,6 @@ resources: - ## 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. From 9bb47383415263e3b804f5b06e6a2af8a1f93da5 Mon Sep 17 00:00:00 2001 From: soyuka Date: Fri, 5 Dec 2025 15:23:35 +0100 Subject: [PATCH 29/49] lint --- .github/workflows/ci.yml | 7 +-- .markdownlint.yaml | 2 +- core/doctrine-filters.md | 74 +++++++++++++++++---------- core/dto.md | 14 ++--- core/elasticsearch-filters.md | 2 +- core/filters.md | 4 +- core/graphql.md | 2 +- laravel/filters.md | 6 +-- symfony/migrate-from-fosrestbundle.md | 36 ++++++------- 9 files changed, 81 insertions(+), 66 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eb4e3530c2b..e06543e8b66 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,12 +24,9 @@ jobs: with: fetch-depth: 0 - - uses: articulate/actions-markdownlint@v1 + - uses: DavidAnson/markdownlint-cli2-action@v21 with: - config: .markdownlint.yaml - files: '**/*.md' - ignore: node_modules - version: 0.46.1 + globs: '**/*.md' - uses: actions/cache@v4 with: diff --git a/.markdownlint.yaml b/.markdownlint.yaml index 8e7a2ff30ac..8dd37022cff 100644 --- a/.markdownlint.yaml +++ b/.markdownlint.yaml @@ -2,7 +2,7 @@ MD013: line_length: 400 no-inline-html: - allowed_elements: [a, p, img, br, code-selector] + allowed_elements: [a, p, img, br, code-selector, video, source, iframe, h1] MD046: style: fenced MD004: diff --git a/core/doctrine-filters.md b/core/doctrine-filters.md index 1414d1523be..1748a8dc1ea 100644 --- a/core/doctrine-filters.md +++ b/core/doctrine-filters.md @@ -56,10 +56,7 @@ class Book { ## Basic Knowledge -Filters are services (see the section on [custom filters](../core/filters.md#creating-custom-filters)), and they can be linked -to a Resource in two ways: - -1. Through the resource declaration, as the `filters` attribute. +Filters are services (see the section on [custom filters](../core/filters.md#creating-custom-filters)), the can be linked to an API Platform Operation throuh [parameters](./filters.md): For example, having a filter service declaration in `services.yaml`: @@ -72,11 +69,6 @@ services: tags: ['api_platform.filter'] ``` -> [!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. - We're linking the filter `offer.date_filter` with the resource like this: ```php @@ -93,6 +85,32 @@ 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`. @@ -102,7 +120,7 @@ services all begin with `api_platform.doctrine_mongodb.odm`. > The SearchFilter is a multi-type filter that may have inconsistencies (eg: you can search a partial date with LIKE) > we recommend to use type-specific filters such as `PartialSearchFilter` or `DateFilter` instead. -### Built-in new Search Filters since API Platform >= 4.2 +### Built-in Search Filters since API Platform >= 4.2 To add some search filters, choose over this new list: @@ -132,7 +150,7 @@ The search filter supports `exact`, `partial`, `start`, `end`, and `word_start` - `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 [if there is no proper index](performance.md#search-filter). +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`) @@ -180,7 +198,7 @@ Syntax: `?property=value` The value can take any [IRI(Internationalized Resource Identifier)](https://en.wikipedia.org/wiki/Internationalized_Resource_Identifier). -Like other [new search filters](#built-in-new-search-filters-api-platform--42) it can be used on the ApiResource attribute +This filter can be used on the ApiResource attribute or in the operation attribute, for e.g., the `#GetCollection()` attribute: ```php @@ -210,7 +228,7 @@ Syntax: `?property=value` The value can take any scalar value or array of values. -Like other [new search filters](#built-in-new-search-filters-api-platform--42) it can be used on the ApiResource attribute +This filter can be used on the ApiResource attribute or in the operation attribute, for e.g., the `#GetCollection()` attribute: ```php @@ -240,7 +258,7 @@ Syntax: `?property=value` The value can take any scalar value or array of values. -Like other [new search filters](#built-in-new-search-filters-api-platform--42) it can be used on the ApiResource attribute +This filter can be used on the ApiResource attribute or in the operation attribute, for e.g., the `#GetCollection()` attribute: ```php @@ -275,7 +293,7 @@ Syntax: `?property=value` The value can take any scalar value or array of values. -Like other [new search filters](#built-in-new-search-filters-api-platform--42) it can be used on the ApiResource attribute +This filter can be used on the ApiResource attribute or in the operation attribute, for e.g., the `#GetCollection()` attribute: ```php @@ -388,7 +406,7 @@ class Offer ``` > [!TIP] -> For other syntaxes, for e.g., if you want to new syntax with the ApiResource attribute take a look [here](#introduction). +> 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) @@ -505,7 +523,7 @@ class Offer ``` > [!TIP] -> For other syntaxes, for e.g., if you want to new syntax with the ApiResource attribute take a look [here](#introduction). +> 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 @@ -539,7 +557,7 @@ class Offer ``` > [!TIP] -> For other syntaxes, for e.g., if you want to new syntax with the ApiResource attribute take a look [here](#introduction). +> 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 @@ -579,7 +597,7 @@ class Offer ``` > [!TIP] -> For other syntaxes, for e.g., if you want to new syntax with the ApiResource attribute take a look [here](#introduction). +> 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 @@ -619,7 +637,7 @@ class Offer ``` > [!TIP] -> For other syntaxes, for e.g., if you want to new syntax with the ApiResource attribute take a look [here](#introduction). +> 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 @@ -662,7 +680,7 @@ class Offer ``` > [!TIP] -> For other syntaxes, for e.g., if you want to new syntax with the ApiResource attribute take a look [here](#introduction). +> 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 @@ -743,7 +761,7 @@ 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 [here](#introduction). +> 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 @@ -755,14 +773,14 @@ order with the following query: `/offers?order[name]=desc&order[id]=asc`. 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** +#### 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** +#### 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). @@ -1397,8 +1415,8 @@ This allows delegating validation to API Platform, respecting the [SOLID Princip > > You can see how this works directly in our code components: > -> - The `ParameterValidatorProvider` for **Symfony** can be found [here](https://github.com/api-platform/core/blob/c9692b509d5b641104addbadb349b9bcab83e251/src/Symfony/Validator/State/ParameterValidatorProvider.php). -> - The `ParameterValidatorProvider` for **Laravel** is located [here](https://github.com/api-platform/core/blob/c9692b509d5b641104addbadb349b9bcab83e251/src/Laravel/State/ParameterValidatorProvider.php). +> - 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). @@ -1668,8 +1686,8 @@ This allows delegating validation to API Platform, respecting the [SOLID Princip > > You can see how this works directly in our code components: > -> - The `ParameterValidatorProvider` for **Symfony** can be found [here](https://github.com/api-platform/core/blob/c9692b509d5b641104addbadb349b9bcab83e251/src/Symfony/Validator/State/ParameterValidatorProvider.php). -> - The `ParameterValidatorProvider` for **Laravel** is located [here](https://github.com/api-platform/core/blob/c9692b509d5b641104addbadb349b9bcab83e251/src/Laravel/State/ParameterValidatorProvider.php). +> - 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). diff --git a/core/dto.md b/core/dto.md index 96511287d25..8c96944d251 100644 --- a/core/dto.md +++ b/core/dto.md @@ -10,9 +10,9 @@ In API Platform, [the general design considerations](design.md) recommended patt This reference covers three implementation strategies: - * [State Options: Linking a DTO Resource to an Entity for automated CRUD operations.](#1-the-dto-resource-state-options) - * [Automated Mapped Inputs: Using input DTOs with stateOptions for automated Write operations.](#2-automated-mapped-inputs-outputs) - * [Custom Business Logic: Using input DTOs with custom State Processors for specific business actions.](#3-custom-business-logic-custom-processor) +- [State Options: Linking a DTO Resource to an Entity for automated CRUD operations.](#1-the-dto-resource-state-options) +- [Automated Mapped Inputs: Using input DTOs with stateOptions for automated Write operations.](#2-automated-mapped-inputs-and-outputs) +- [Custom Business Logic: Using input DTOs with custom State Processors for specific business actions.](#3-custom-business-logic-custom-processor) ## 1. The DTO Resource (State Options) @@ -116,9 +116,9 @@ Automated mapping relies on two internal classes: `ApiPlatform\State\Provider\Ob 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. +- 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 @@ -130,7 +130,7 @@ The `ObjectMapperProvider` delegates fetching the data to the underlying Doctrin 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 & Outputs +## 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). diff --git a/core/elasticsearch-filters.md b/core/elasticsearch-filters.md index 28e950b8e3f..01c10c7401d 100644 --- a/core/elasticsearch-filters.md +++ b/core/elasticsearch-filters.md @@ -52,7 +52,7 @@ class Book { } ``` -**Further Reading** +## 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). diff --git a/core/filters.md b/core/filters.md index 63e9572f380..04a68bdc17f 100644 --- a/core/filters.md +++ b/core/filters.md @@ -536,7 +536,7 @@ The `IriConverterParameterProvider` supports the following options in `extraProp This provider must be enabled before it can be used. -``` +```yaml api_platform: enable_link_security: true ``` @@ -742,7 +742,7 @@ class User {} ## 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. | diff --git a/core/graphql.md b/core/graphql.md index ef36ea901d9..f7551cbf3ac 100644 --- a/core/graphql.md +++ b/core/graphql.md @@ -680,7 +680,7 @@ Your custom queries will be available like this: ## Mutations -If you don't know what mutations are yet, the documentation about them is [here](https://graphql.org/learn/queries/#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: diff --git a/laravel/filters.md b/laravel/filters.md index 06b9f1ee8c9..aa8f1326b87 100644 --- a/laravel/filters.md +++ b/laravel/filters.md @@ -153,19 +153,19 @@ 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** +#### 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** +#### 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** +#### 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 diff --git a/symfony/migrate-from-fosrestbundle.md b/symfony/migrate-from-fosrestbundle.md index 8f24457c3bf..0360b9bc836 100644 --- a/symfony/migrate-from-fosrestbundle.md +++ b/symfony/migrate-from-fosrestbundle.md @@ -13,13 +13,13 @@ The table below provides a list of the main features you can find in FOSRestBund ### Make CRUD endpoints -**In FOSRestBundle** +#### 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** +#### 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. @@ -27,11 +27,11 @@ See [Operations](../core/operations.md). ### Make custom controllers -**In FOSRestBundle** +#### In FOSRestBundle (Custom controllers) Same as above. -**In API Platform** +#### 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. @@ -41,13 +41,13 @@ See [General Design Considerations](../core/design.md). ### Routing system (with native documentation support) -**In FOSRestBundle** +#### 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** +#### 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. @@ -55,13 +55,13 @@ See [Operations](../core/operations.md). ### Hook into the handling of the requests -**In FOSRestBundle** +#### 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** +#### 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. @@ -69,7 +69,7 @@ See [Extending API Platform](../core/extending.md) for more details. ### Customize the formats of the requests and the responses -**In FOSRestBundle** +#### In FOSRestBundle (Formats) Only the request body's format can be customized. @@ -77,7 +77,7 @@ Use body listeners to use either FOSRest's own decoders or your own ones. FOSRes See [Body Listener](https://github.com/FriendsOfSymfony/FOSRestBundle/blob/3.x/Resources/doc/body_listener.rst). -**In API Platform** +#### In API Platform (Formats) Both the request and the response body's format can be customized. @@ -87,7 +87,7 @@ See [Content negotiation](../core/content-negotiation.md). ### Name conversion -**In FOSRestBundle** +#### In FOSRestBundle (Name conversion) Only request bodies can be converted before entering into your controller. @@ -95,7 +95,7 @@ FOSRest provides two native normalizers for converting the names of your JSON ke See [Body Listeners](https://github.com/FriendsOfSymfony/FOSRestBundle/blob/3.x/Resources/doc/body_listener.rst). -**In API Platform** +#### In API Platform (Name conversion) Both request and response bodies can be converted. @@ -105,13 +105,13 @@ See [_Name Conversion_ in The Serialization Process](../core/serialization.md#na ### Handle errors -**In FOSRestBundle** +#### 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** +#### In API Platform (Error handling) Map the exceptions to HTTP statuses in the `api_platform.exception_to_status` parameter. @@ -119,11 +119,11 @@ See [Errors Handling](../core/errors.md). ### Security -**In FOSRestBundle** +#### In FOSRestBundle (Security) Use [Symfony's Security component](https://symfony.com/doc/current/security) to control your API access. -**In API Platform** +#### 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. @@ -133,13 +133,13 @@ See [Security](../core/security.md). ### API versioning -**In FOSRestBundle** +#### 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** +#### 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. From c6712471233398c75f0b5d55cd920a1ac56a6bba Mon Sep 17 00:00:00 2001 From: soyuka Date: Fri, 5 Dec 2025 15:39:28 +0100 Subject: [PATCH 30/49] restore max depth + prettier --- .editorconfig | 1 + .github/workflows/ci.yml | 55 +++++++++++++++++++++++++++++----------- .markdownlint.yaml | 4 ++- .prettierrc | 4 +++ 4 files changed, 48 insertions(+), 16 deletions(-) create mode 100644 .prettierrc diff --git a/.editorconfig b/.editorconfig index cde45ed3717..dd4b2e5e3f5 100644 --- a/.editorconfig +++ b/.editorconfig @@ -20,6 +20,7 @@ indent_style = space indent_size = 2 [*.md] +max_line_length = 80 trim_trailing_whitespace = false [*.neon] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e06543e8b66..92672d1eb4f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,24 +9,12 @@ permissions: contents: read jobs: - build: - name: Lint + proselint: + name: Prose Lint runs-on: ubuntu-latest - - permissions: - contents: read - packages: read - statuses: write - steps: - - name: Checkout + - name: Checkout code uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - uses: DavidAnson/markdownlint-cli2-action@v21 - with: - globs: '**/*.md' - uses: actions/cache@v4 with: @@ -40,3 +28,40 @@ jobs: - name: Run Proselint run: find . -name '*.md' -exec proselint {} \; + lint: + name: Lint + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - uses: DavidAnson/markdownlint-cli2-action@v21 + with: + globs: '**/*.md' + + prettier-format: + name: Prettier + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Prettier Dry Run + run: | + npx prettier --check "**/*.md" || { + echo "::error title=Formatting Failed::Some files are not formatted correctly." + echo "-------------------------------------------------------" + echo "❌ CHECK FAILED" + echo "To fix these issues, run the following command locally:" + echo "" + echo " npx prettier --write \"**/*.md\" --prose-wrap always" + echo "" + echo "-------------------------------------------------------" + exit 1 + } diff --git a/.markdownlint.yaml b/.markdownlint.yaml index 8dd37022cff..f003365cbcc 100644 --- a/.markdownlint.yaml +++ b/.markdownlint.yaml @@ -1,6 +1,8 @@ --- MD013: - line_length: 400 + line_length: 80 + code_blocks: false + tables: false no-inline-html: allowed_elements: [a, p, img, br, code-selector, video, source, iframe, h1] MD046: diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000000..ac50a21a10c --- /dev/null +++ b/.prettierrc @@ -0,0 +1,4 @@ +{ + "printWidth": 100, + "proseWrap": "always" +} From a6375a4b3099adbc5ce155e777de8fb01c419f1e Mon Sep 17 00:00:00 2001 From: soyuka Date: Fri, 5 Dec 2025 15:43:10 +0100 Subject: [PATCH 31/49] max-length 100 prettier --- .github/ISSUE_TEMPLATE/1_Support_question.md | 6 +- CONTRIBUTING.md | 36 +- README.md | 3 +- admin/advanced-customization.md | 366 ++++-- admin/authentication-support.md | 151 ++- admin/components.md | 421 +++--- admin/customizing.md | 152 ++- admin/file-upload.md | 48 +- admin/getting-started.md | 92 +- admin/handling-relations.md | 145 ++- admin/index.md | 29 +- admin/performance.md | 15 +- admin/real-time-mercure.md | 36 +- admin/schema.md | 83 +- admin/validation.md | 41 +- core/bootstrap.md | 11 +- core/client-integration.md | 123 +- core/configuration.md | 218 ++-- core/content-negotiation.md | 216 ++-- core/default-order.md | 43 +- core/deprecations.md | 73 +- core/design.md | 87 +- core/doctrine-filters.md | 613 +++++---- core/dto.md | 53 +- core/elasticsearch-filters.md | 118 +- core/elasticsearch.md | 189 +-- core/enums.md | 81 +- core/errors.md | 147 ++- core/events.md | 51 +- core/extending-jsonld-context.md | 41 +- core/extending.md | 35 +- core/extensions.md | 88 +- core/external-vocabularies.md | 59 +- core/filters.md | 318 +++-- core/form-data.md | 91 +- core/getting-started.md | 153 ++- core/graphql.md | 1216 ++++++++++-------- core/identifiers.md | 57 +- core/index.md | 33 +- core/json-schema.md | 71 +- core/jwt.md | 14 +- core/mercure.md | 84 +- core/mongodb.md | 74 +- core/openapi.md | 356 ++--- core/operation-path-naming.md | 25 +- core/operations.md | 264 ++-- core/pagination.md | 205 +-- core/performance.md | 260 ++-- core/push-relations.md | 46 +- core/security.md | 9 +- core/serialization.md | 465 ++++--- core/state-processors.md | 110 +- core/state-providers.md | 90 +- core/subresources.md | 122 +- core/testing.md | 17 +- core/upgrade-guide.md | 104 +- core/url-generation-strategy.md | 55 +- core/validation.md | 3 +- create-client/custom.md | 110 +- create-client/index.md | 30 +- create-client/nextjs.md | 27 +- create-client/nuxt.md | 52 +- create-client/quasar.md | 16 +- create-client/react-native.md | 78 +- create-client/react.md | 8 +- create-client/troubleshooting.md | 36 +- create-client/typescript.md | 16 +- create-client/vuejs.md | 18 +- create-client/vuetify.md | 17 +- deployment/docker-compose.md | 113 +- deployment/heroku.md | 84 +- deployment/index.md | 11 +- deployment/kubernetes.md | 130 +- deployment/minikube.md | 27 +- deployment/traefik.md | 647 +++++----- extra/conduct.md | 51 +- extra/contribution-guides.md | 14 +- extra/enterprise.md | 43 +- extra/philosophy.md | 65 +- extra/releases.md | 27 +- extra/security.md | 58 +- extra/troubleshooting.md | 41 +- laravel/filters.md | 71 +- laravel/index.md | 324 +++-- laravel/jwt.md | 41 +- laravel/security.md | 18 +- laravel/testing.md | 145 ++- laravel/validation.md | 3 +- schema-generator/configuration.md | 538 ++++---- schema-generator/getting-started.md | 94 +- schema-generator/index.md | 96 +- symfony/caddy.md | 38 +- symfony/controllers.md | 239 ++-- symfony/debugging.md | 72 +- symfony/file-upload.md | 120 +- symfony/fosuser-bundle.md | 40 +- symfony/index.md | 677 ++++++---- symfony/jwt.md | 274 ++-- symfony/messenger.md | 94 +- symfony/migrate-from-fosrestbundle.md | 90 +- symfony/nelmio-api-doc.md | 37 +- symfony/security.md | 216 ++-- symfony/testing.md | 173 ++- symfony/user.md | 25 +- symfony/validation.md | 182 +-- 105 files changed, 7668 insertions(+), 5770 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/1_Support_question.md b/.github/ISSUE_TEMPLATE/1_Support_question.md index 1b9f449db0c..2ef79d289ef 100644 --- a/.github/ISSUE_TEMPLATE/1_Support_question.md +++ b/.github/ISSUE_TEMPLATE/1_Support_question.md @@ -5,8 +5,8 @@ about: See https://api-platform.com/support/ for questions about using API Platf # Support question -We use GitHub issues only to discuss bugs and new features. -For this kind of questions about using API Platform, please use -any of the support alternatives shown in [API Platform support](https://api-platform.com/support/). +We use GitHub issues only to discuss bugs and new features. For this kind of questions about using +API Platform, please use any of the support alternatives shown in +[API Platform support](https://api-platform.com/support/). Thanks! diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dfc0c7cd5e4..7741a60c55a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,8 @@ First of all, thank you for contributing, you're awesome! -To have your code integrated in the API Platform documentation project, there are some rules to follow, but don't panic, it's easy! +To have your code integrated in the API Platform documentation project, there are some rules to +follow, but don't panic, it's easy! ## Reporting Bugs in the documentation @@ -12,33 +13,36 @@ Before submitting your issue: - A clear title to resume the issue - A description of the workflow needed to reproduce the bug -> [!NOTE] -> Don't hesitate giving as much information as you can. +> [!NOTE] Don't hesitate giving as much information as you can. ## Code of Conduct -By contributing to this project, you agree to abide by our [Code of Conduct](https://github.com/api-platform/docs#coc-ov-file). We expect all contributors to foster a welcoming and inclusive environment. +By contributing to this project, you agree to abide by our +[Code of Conduct](https://github.com/api-platform/docs#coc-ov-file). We expect all contributors to +foster a welcoming and inclusive environment. ## How to Contribute -1. Fork this repository by clicking the "Fork" button at the top right of the `api-platform/docs` repository page. +1. Fork this repository by clicking the "Fork" button at the top right of the `api-platform/docs` + repository page. 2. Clone the forked repository to your local machine: - ```console - git clone https://github.com/your-username/repository-name.git - ``` + ```console + git clone https://github.com/your-username/repository-name.git + ``` 3. Create a new branch for your contribution: - ```console - git switch -c docs-your-branch-name - ``` + ```console + git switch -c docs-your-branch-name + ``` 4. Commit and push your changes -5. Submit a Pull Request. You must decide on what branch your changes will be based depending of the nature of the change. - See [the dedicated documentation entry](https://api-platform.com/docs/extra/releases/). +5. Submit a Pull Request. You must decide on what branch your changes will be based depending of the + nature of the change. See + [the dedicated documentation entry](https://api-platform.com/docs/extra/releases/). -> [!TIP] -> You can also contribute to improving the documentation directly by clicking on the -> **"You can also help us improve the documentation of this page."** link, located at the end of each documentation page. +> [!TIP] You can also contribute to improving the documentation directly by clicking on the **"You +> can also help us improve the documentation of this page."** link, located at the end of each +> documentation page. diff --git a/README.md b/README.md index 0267b5e84ea..268e2e68d2f 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,8 @@ [![Lint](https://github.com/api-platform/docs/actions/workflows/ci.yml/badge.svg)](https://github.com/api-platform/docs/actions/workflows/ci.yml) -Welcome to the official documentation for [API Platform](https://api-platform.com), a powerful framework for building APIs and web applications. +Welcome to the official documentation for [API Platform](https://api-platform.com), a powerful +framework for building APIs and web applications. This repository contains all the API Platform documentation resources. diff --git a/admin/advanced-customization.md b/admin/advanced-customization.md index f714af21e9e..61db70c7ae2 100644 --- a/admin/advanced-customization.md +++ b/admin/advanced-customization.md @@ -1,8 +1,10 @@ # 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). +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. +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: @@ -19,15 +21,19 @@ 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. +API Platform comes with its own [layout](https://marmelab.com/react-admin/Admin.html#layout) and +[themes](https://marmelab.com/react-admin/Admin.html#theme) by default. ![Admin with default API Platform theme and layout](./images/api-platform-admin-theme.png) -However you may not find them to your liking, or you may want to remove the API Platform logo from the top bar. +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. +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`. +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"; @@ -42,7 +48,10 @@ 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. +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`. @@ -53,8 +62,8 @@ import { HydraAdmin, ResourceGuesser } from "@api-platform/admin"; export const App = () => ( - -+ @@ -70,11 +79,14 @@ 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. +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. +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: +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'; @@ -94,19 +106,24 @@ const BookList = () => ( ![Admin with number of related records](./images/admin-reference-record-count.png) -**Tip:** It is recommended to also set a custom `label` to the column, as the label is otherwise humanized from the `source` prop, which is no longer suitable with a source like `reviews.length`. +**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. +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`. +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. +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. +`` 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'; @@ -133,7 +150,8 @@ The UI is now more concise: ![Admin with hidden show and edit buttons](./images/admin-datagrid.png) -If you want, you can use the [`rowClick`](https://marmelab.com/react-admin/Datagrid.html#rowclick) prop to customize the row click action, for instance to redirect to the book edit view instead: +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 = () => ( @@ -150,19 +168,26 @@ const BookList = () => ( ); ``` -**Tip:** Check out the [`` documentation](https://marmelab.com/react-admin/Datagrid.html) for more customization options. +**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. +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. +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"`. +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. +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"; @@ -184,17 +209,25 @@ That's enough to display an undoable notification when updating a book: ![Admin with undoable mutations](./images/admin-undoable-mutation.png) -**Tip:** The default `mutationMode` set by `` is `"pessimistic"`, however the default `mutationMode` set by React Admin's `` component is `"undoable"`. +**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. +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. +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. +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. +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"; @@ -218,10 +251,15 @@ Now, if the user tries to navigate away from the form with unsaved changes, they ## 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. +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). +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 ``. @@ -246,11 +284,17 @@ 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. +**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). +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: @@ -273,22 +317,22 @@ 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'; +import { InputGuesser } from "@api-platform/admin"; +import { Edit, SimpleForm } from "react-admin"; +import { Stack } from "@mui/material"; export const BookEdit = () => ( - - - - - - - - - - - + + + + + + + + + + + ); ``` @@ -296,50 +340,67 @@ With these simple changes we already get a more appealing form layout: ![Admin with customized form layout](./images/admin-form-layout.png) -**Tip:** Feel free to look at the [``](https://marmelab.com/react-admin/Edit.html) and [``](https://marmelab.com/react-admin/SimpleForm.html) documentation pages to learn more about the customization options they offer. +**Tip:** 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). +**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. +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. +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. +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'; +import { + Show, + TabbedShowLayout, + TextField, + DateField, + ReferenceArrayField, + SimpleList, +} from "react-admin"; const BookShow = () => ( - - - - - - - - - - - - - review.author - .split(' ') - .map((name: string) => name[0]) - .join('') - } - /> - - - - + + + + + + + + + + + + + review.author + .split(" ") + .map((name: string) => name[0]) + .join("") + } + /> + + + + ); ``` @@ -347,41 +408,53 @@ Here is the result: ![Admin with tabbed show layout](./images/admin-tabbed-show-layout.png) -**Tip:** Feel free to look at the [``](https://marmelab.com/react-admin/TabbedShowLayout.html), [``](https://marmelab.com/react-admin/ReferenceArrayField.html) and [``](https://marmelab.com/react-admin/SimpleList.html) documentation pages to learn more about the customization options they offer. +**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. +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. +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. +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. +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. +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. +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'; +import { ShowGuesser } from "@api-platform/admin"; +import { FieldGuesser, WithRecord, Labeled } from "react-admin"; +import { Rating } from "@mui/material"; const ReviewShow = () => ( - - - - - - ( - - )} - /> - - - + + + + + + } + /> + + + ); ``` @@ -389,51 +462,61 @@ Here is the result: ![Admin with custom field component](./images/admin-custom-field.png) -**Tip:** For a more complex field component, the preferred approach would probably be to create a dedicated React component. You can then leverage the [`useRecordContext`](https://marmelab.com/react-admin/useRecordContext.html) hook to achieve the same result. +**Tip:** 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. +**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. +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. +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'; +import { useInput } from "react-admin"; +import { Rating } from "@mui/material"; const RatingInput = (props: InputProps) => { - const { field } = useInput(props); - return ( - { - field.onChange(value); - }} - /> - ); + 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. +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'; +import { Edit, SimpleForm, InputGuesser, Labeled } from "react-admin"; +import { RatingInput } from "./RatingInput"; const ReviewEdit = () => ( - - - - - - - - - - - + + + + + + + + + + + ); ``` @@ -443,9 +526,13 @@ 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. +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. +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: @@ -459,4 +546,7 @@ Here are some examples, from the simplest to the most complete solutions: - [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. +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. diff --git a/admin/authentication-support.md b/admin/authentication-support.md index 024af85a507..f435a65a7c0 100644 --- a/admin/authentication-support.md +++ b/admin/authentication-support.md @@ -2,13 +2,18 @@ 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. +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. +**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. +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: @@ -18,52 +23,54 @@ In the following steps, we will see how to: ### 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. +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")}` } - : {}; + 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. +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"; +import { fetchHydra as baseFetchHydra } from "@api-platform/admin"; const fetchHydra = (url, options = {}) => - baseFetchHydra(url, { - ...options, - headers: getHeaders, - }); - + 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. +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(); + const introspect = useIntrospection(); - if (localStorage.getItem("token")) { - introspect(); - return <>; - } - return ; + 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). +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. @@ -72,39 +79,38 @@ 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; - } + 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); + localStorage.removeItem("token"); + setRedirectToLogin(true); - return { api, response, status }; - } + 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. +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 { hydraDataProvider as baseHydraDataProvider } from "@api-platform/admin"; import { ENTRYPOINT } from "config/entrypoint"; const dataProvider = (setRedirectToLogin) => - baseHydraDataProvider({ - entrypoint: ENTRYPOINT, - httpClient: fetchHydra, - apiDocumentationParser: apiDocumentationParser(setRedirectToLogin), - }); + baseHydraDataProvider({ + entrypoint: ENTRYPOINT, + httpClient: fetchHydra, + apiDocumentationParser: apiDocumentationParser(setRedirectToLogin), + }); ``` ### Update The Admin Component @@ -168,22 +174,28 @@ export const Admin = () => { ### 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). +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. +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 +- 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. +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. +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 @@ -193,7 +205,7 @@ const getAccessToken = () => localStorage.getItem("token"); const httpClient = async (url: string, options: fetchUtils.Options = {}) => { options.headers = new Headers({ ...options.headers, - Accept: 'application/json', + Accept: "application/json", }) as Headers; const token = getAccessToken(); @@ -203,19 +215,23 @@ const httpClient = async (url: string, options: fetchUtils.Options = {}) => { }; const dataProvider = openApiDataProvider({ - dataProvider: simpleRestProvider(API_ENTRYPOINT_PATH, httpClient), - entrypoint: API_ENTRYPOINT_PATH, - docEntrypoint: API_DOCS_PATH, + 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 `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. +**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. +Now let's create and export an `authProvider` object that handles authentication and authorization +logic. ```typescript // src/authProvider.ts @@ -228,7 +244,7 @@ interface JwtPayload { const getAccessToken = () => localStorage.getItem("token"); const authProvider = { - login: async ({username, password}: { username: string; password: string }) => { + login: async ({ username, password }: { username: string; password: string }) => { const request = new Request(API_AUTH_PATH, { method: "POST", body: JSON.stringify({ email: username, password }), @@ -248,7 +264,7 @@ const authProvider = { localStorage.removeItem("token"); return Promise.resolve(); }, - checkAuth: () => getAccessToken() ? Promise.resolve() : Promise.reject(), + checkAuth: () => (getAccessToken() ? Promise.resolve() : Promise.reject()), checkError: (error: { status: number }) => { const status = error.status; if (status === 401 || status === 403) { @@ -279,22 +295,23 @@ export default authProvider; ### Updating The Admin Component -Finally, we can update the `Admin` component to use the `authProvider` and `dataProvider` we created earlier. +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 { 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 () => ( - + ); ``` diff --git a/admin/components.md b/admin/components.md index 4ffd161b7ce..aeeafa93cab 100644 --- a/admin/components.md +++ b/admin/components.md @@ -2,21 +2,24 @@ ## HydraAdmin -Creates a complete Admin, using [``](./components.md#adminguesser), but configured specially for [Hydra](https://www.hydra-cg.com/). +Creates a complete Admin, using [``](./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:** 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. +**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'; +import { HydraAdmin, ResourceGuesser } from "@api-platform/admin"; const App = () => ( - - - {/* ... */} - + + + {/* ... */} + ); export default App; @@ -30,7 +33,8 @@ export default App; | 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: +\* `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 @@ -38,7 +42,9 @@ export default App; ### 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`. +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. @@ -48,26 +54,26 @@ 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. +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/). +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. +**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'; +import { OpenApiAdmin, ResourceGuesser } from "@api-platform/admin"; const App = () => ( - - - {/* ... */} - + + + {/* ... */} + ); export default App; @@ -82,7 +88,8 @@ export default App; | 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: +\* `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 @@ -90,7 +97,9 @@ export default App; ### 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`. +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. @@ -100,56 +109,62 @@ 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. +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`. +`` 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'; +import { AdminGuesser } from "@api-platform/admin"; +import dataProvider from "./dataProvider"; +import schemaAnalyzer from "./schemaAnalyzer"; -const App = () => ( - -); +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`. +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 Hydra documented APIs, use the [`` component](./components.md#hydraadmin) +instead. -**Tip:** For OpenAPI documented APIs, use the [`` component](./components.md#openapiadmin) 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. +`` 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'; +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. +**Tip:** Deprecated resources are hidden by default, but you can add them back using an explicit +`` component. ### AdminGuesser Props @@ -161,25 +176,33 @@ export default App; | 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. +`` 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). +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. +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'; +import { AdminGuesser, ResourceGuesser } from "@api-platform/admin"; const App = () => ( - - {/* Uses the default guesser components for each CRUD view */} - - {/* Overrides only the list view */} - - + + {/* Uses the default guesser components for each CRUD view */} + + {/* Overrides only the list view */} + + ); export default App; @@ -195,209 +218,250 @@ export default App; | 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). +`` 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). +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'; +import { ListGuesser } from "@api-platform/admin"; export const BooksList = () => ( - /* Will display fields guessed from the schema */ - + /* 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. +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'; +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 */} - - - + + {/* 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). +`` 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). +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. +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'; +import { CreateGuesser } from "@api-platform/admin"; export const BooksCreate = () => ( - /* Will display inputs guessed from the schema */ - + /* 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. +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'; +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 */} - - - + + {/* 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). +`` 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). +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. +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'; +import { EditGuesser } from "@api-platform/admin"; export const BooksEdit = () => ( - /* Will display inputs guessed from the schema */ - + /* 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. +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'; +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 */} - - - + + {/* 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). +`` 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). +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. +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'; +import { ShowGuesser } from "@api-platform/admin"; export const BooksShow = () => ( - /* Will display fields guessed from the schema */ - + /* 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. +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'; +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 */} - - - + + {/* 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). +`` 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). +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). +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'; +import { FieldGuesser, ShowGuesser } from "@api-platform/admin"; export const BooksShow = () => ( - - {/* Renders a TextField */} - - {/* Renders a NumberField */} - - {/* Renders a DateField */} - - + + {/* Renders a TextField */} + + {/* Renders a NumberField */} + + {/* Renders a DateField */} + + ); ``` @@ -407,27 +471,33 @@ export const BooksShow = () => ( | ------ | ------ | ----- | -------- | ------------------------------------ | | 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. +`` 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). +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). +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'; +import { CreateGuesser, InputGuesser } from "@api-platform/admin"; export const BooksCreate = () => ( - - {/* Renders a TextInput */} - - {/* Renders a NumberInput */} - - {/* Renders a DateInput */} - - + + {/* Renders a TextInput */} + + {/* Renders a NumberInput */} + + {/* Renders a DateInput */} + + ); ``` @@ -437,9 +507,16 @@ export const BooksCreate = () => ( | ------ | ------ | ----- | -------- | ------------------------------------ | | 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). +`` 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. +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 diff --git a/admin/customizing.md b/admin/customizing.md index 4d80cd5a0f1..c23ce687878 100644 --- a/admin/customizing.md +++ b/admin/customizing.md @@ -1,23 +1,31 @@ # 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. +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: +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 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. +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. +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: @@ -29,9 +37,11 @@ If you want to override at least one resource, paste this content in the ``` -This message tells you which resources are exposed by your API and how to customize the generated pages for each of them. +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 ``: +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"; @@ -50,7 +60,10 @@ Now the `greetings` resource will no longer be displayed in the menu. ![Customized Admin menu](./images/admin-menu.png) -`` also accepts all props react-admin's [``](https://marmelab.com/react-admin/Resource.html) component accepts. This means that, for instance, you can use the `list` prop to use your own list component, but keep using the create, edit and show components introspected by ``: +`` 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"; @@ -88,15 +101,18 @@ Here is the result: ## Customizing the `` -By default, `` will render a `` component as the list view for a resource. +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 component will automatically introspect the API schema and generate a list view with all the +fields of the resource. ![Admin default generated list view](./images/admin-default-list.png) This is already usable, but may not provide the best user experience yet. -To start customizing the list view, you can look at the DevTools console. You will see messages like this: +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: @@ -127,9 +143,11 @@ const App = () => ( ); ``` -If you follow these instructions, you will end up with the same view as before, but now you can start customizing it. +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. +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: @@ -155,13 +173,17 @@ 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. +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). +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. +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: +For instance, let's add a `label` prop to customize the label of the ISBN column to be all +uppercase: ```diff export const BookList = () => ( @@ -182,34 +204,31 @@ 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. +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: +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'; +import { HydraAdmin, ResourceGuesser, ShowGuesser, FieldGuesser } from "@api-platform/admin"; const BookShow = () => ( - - - - - - - - + + + + + + + + ); export default () => ( - - - - + + + + ); ``` @@ -219,11 +238,22 @@ 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. +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. +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: +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'; @@ -239,7 +269,8 @@ const ReviewShow = () => ( ## Customizing the `` and `` -Customizing the `` and `` is very similar to customizing the ``. +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. @@ -298,17 +329,30 @@ Here is the result: ![Admin with customized edit guesser](./images/admin-custom-edit-guesser.png) -**Tip:** Here, we leveraged the `multiline` and `readOnly` props of the `` component. But you can use any [common input prop](https://marmelab.com/react-admin/Inputs.html#common-input-props) supported by React Admin [Inputs](https://marmelab.com/react-admin/Inputs.html) on them. +**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. +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. +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). +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: +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'; @@ -330,13 +374,19 @@ const ReviewsEdit = () => ( ![Admin With AutocompleteInput](./images/AutocompleteInput.png) -> [!WARNING] -> When replacing `` with a React Admin input component, the validation rules are not automatically applied. You will need to manually add them back. Fortunately, this is very easy to do. Read the [Validation With React Admin Inputs](./validation.md#validation-with-react-admin-inputs) section to learn more. +> [!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. +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. +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. +Head to the next section, [Customizing the Admin](./advanced-customization.md), for step-by-step +examples. diff --git a/admin/file-upload.md b/admin/file-upload.md index 4de9702a3a2..adf5fd8c59b 100644 --- a/admin/file-upload.md +++ b/admin/file-upload.md @@ -1,39 +1,41 @@ # Handling File Upload -If you need to handle the file upload in the server part, please follow [the related documentation](../symfony/file-upload.md). +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. +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). +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: +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'; +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. +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). +**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). diff --git a/admin/getting-started.md b/admin/getting-started.md index d979763fa0a..c8d429d2812 100644 --- a/admin/getting-started.md +++ b/admin/getting-started.md @@ -2,11 +2,13 @@ ## API Platform Symfony variant -If you use the [API Platform Symfony variant](../symfony/), good news, API Platform Admin is already installed! 🎉 +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). +When running locally, you can also click on the "Admin" button of the welcome page at +[https://localhost](https://localhost). ![API Platform welcome page](./images/api-platform-welcome-page.png) @@ -16,9 +18,11 @@ 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. +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: +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 @@ -33,12 +37,15 @@ 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 +- [``](./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. +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: @@ -50,21 +57,27 @@ import { HydraAdmin } from "@api-platform/admin"; 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). +**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. +Your new administration interface is ready! `HydraAdmin` will automatically fetch the Hydra +documentation of your API and generate CRUD pages for all the resources it exposes. Type `npm run dev` to try it! ![Basic admin with the Greetings resource](./images/basic-admin-greetings.png) -**Tip:** There are more props you can pass to the `HydraAdmin` component to customize the dataProvider or the connection to Mercure. Check the [API documentation](./components.md#hydraadmin) for more information. +**Tip:** 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. +**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. +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: @@ -73,48 +86,59 @@ 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). +**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. +Your new administration interface is ready! `OpenApiAdmin` will automatically fetch the Hydra +documentation of your API and generate CRUD pages for all the resources it exposes. Type `npm run dev` to try it! ![Basic admin with the Greetings resource](./images/basic-admin-greetings.png) -**Tip:** There are more props you can pass to the `OpenApiAdmin` component to customize the dataProvider or the connection to Mercure. Check the [API documentation](./components.md#openapiadmin) for more information. +**Tip:** 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. +**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. +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). +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: +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 + 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: diff --git a/admin/handling-relations.md b/admin/handling-relations.md index 7eebda4f133..36a565d55a4 100644 --- a/admin/handling-relations.md +++ b/admin/handling-relations.md @@ -2,11 +2,13 @@ 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. +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. +You can configure your API to [embed the related data](../core/serialization.md#embedding-relations) +in the serialized response. ```js // Without Embedded Book Data @@ -35,18 +37,23 @@ You can configure your API to [embed the related data](../core/serialization.md# } ``` -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. +If you do so, by default the admin will render the full object as text field and text input, which +is not very user-friendly. ![Embedded Relation With Full Object](images/embedded-relation-full-object.png) There are two ways you can handle this situation: -1. Change the Field and Input components to [display the fields you want](./handling-relations.md#displaying-a-field-of-an-embedded-relation) -2. Ask the admin to [return the embedded resources' IRI instead of the full record](./handling-relations.md#return-the-embedded-resources-iri-instead-of-the-full-record), by leveraging the `useEmbedded` parameter +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. +React Admin fields allow to use the dot notation (e.g. `book.title`) to target a field from an +embedded relation. ```jsx import { @@ -76,11 +83,15 @@ export const App = () => ( ![Embedded Relation With Dot Notation](images/embedded-relation-dot-notation.png) -Allowing to edit the relation, on the other hand, is a little trickier, as it requires transforming the record to replace the nested object by its IRI. +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. +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 { @@ -121,27 +132,32 @@ This offers a nice and convenient way to edit the relation. ![Embedded Relation With ReferenceInput](images/embedded-relation-ReferenceInput.png) -**Tip:** We also had to customize ``'s child [``](https://marmelab.com/react-admin/AutocompleteInput.html) component to override its `label` and `filterToQuery` props. You can learn more about why that's necessary in the [Using an AutoComplete Input for Relations](./handling-relations.md#using-an-autocomplete-input-for-relations) section. +**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`. +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'; +import { HydraAdmin, dataProvider } from "@api-platform/admin"; const entrypoint = process.env.ENTRYPOINT; export const App = () => ( - + ); ``` @@ -150,35 +166,36 @@ This tells the dataProvider to return only the IRI in the record, discarding the ```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", - } -} + "@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" -} + "@id": "/reviews/15", + id: 15, + rating: 5, + body: "A must-read for any software developer. Martin's insights are invaluable.", + author: "Alice Smith", + book: "/books/7", +}; ``` This way, the related record's IRI is returned and can be displayed. ![Embedded Relation With useEmbedded To False](images/embedded-relation-useEmbedded-false.png) -We can improve the UI further by leveraging React Admin's [``](https://marmelab.com/react-admin/ReferenceField.html) component: +We can improve the UI further by leveraging React Admin's +[``](https://marmelab.com/react-admin/ReferenceField.html) component: ```jsx import { @@ -211,7 +228,10 @@ This allows to display the title of the related book instead of its IRI. ![Embedded Relation With ReferenceField](images/embedded-relation-ReferenceField.png) -Lastly, this also allows to easily edit the relation by leveraging either [``](https://marmelab.com/react-admin/ReferenceInput.html) for a `to-one` relation or [``](https://marmelab.com/react-admin/ReferenceArrayInput.html) for a `to-many` relation. +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 { @@ -246,17 +266,25 @@ This offers a nice and convenient way to edit the relation. ![Embedded Relation With ReferenceInput](images/embedded-relation-ReferenceInput.png) -**Tip:** We also had to customize ``'s child [``](https://marmelab.com/react-admin/AutocompleteInput.html) component to override its `filterToQuery` props. You can learn more about why that's necessary in the [Using an AutoComplete Input for Relations](./handling-relations.md#using-an-autocomplete-input-for-relations) section. +**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. +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. +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. +`` 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). +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: @@ -313,9 +341,13 @@ class Book } ``` -Notice the "partial search" [filter](../core/filters.md) on the `title` property of the `Book` resource class. +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: +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 { @@ -353,8 +385,10 @@ 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 +- 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. @@ -362,12 +396,19 @@ 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. +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. +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. +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. +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. diff --git a/admin/index.md b/admin/index.md index e39d45a6565..327733ce03f 100644 --- a/admin/index.md +++ b/admin/index.md @@ -6,24 +6,33 @@ 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). +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. +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. +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. +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. +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. @@ -39,11 +48,13 @@ Simply by reading your API documentation, API Platform Admin provides the follow - 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)) +- 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: +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 diff --git a/admin/performance.md b/admin/performance.md index b856ab16ae4..d931186bd2c 100644 --- a/admin/performance.md +++ b/admin/performance.md @@ -4,15 +4,17 @@ 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. +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. +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: +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 ` or `` component: +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'; +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'; +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' }, + 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) +- `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) diff --git a/admin/schema.md b/admin/schema.md index 18618db1bbe..11b336cbe70 100644 --- a/admin/schema.md +++ b/admin/schema.md @@ -1,16 +1,23 @@ # 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**. +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. +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. +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. +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: +For instance, here is how configure filtering on the `id`, `title` and `author` properties of a +`Book` resource: ```php 'exact', - 'title' => 'ipartial', + 'id' => 'exact', + 'title' => 'ipartial', 'author' => 'ipartial' ])] class Book @@ -35,17 +42,21 @@ 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. +If you are using the guessers, the Admin will automatically update the Book list view to include a +filter on the selected properties. ![Filtering on the title property](./images/admin-filter.png) -**Tip:** Learn more about the [`ApiFilter` attribute](../core/filters.md#apifilter-attribute) in the core documentation. +**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. +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: +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', + 'id' => 'ASC', + 'isbn' => 'ASC', + 'title' => 'ASC', + 'author' => 'ASC', 'publicationDate' => 'DESC' ])] class Book @@ -72,15 +83,18 @@ class Book } ``` -If you are using the guessers, the Admin will automatically update the Book list view to make the selected columns sortable. +If you are using the guessers, the Admin will automatically update the Book list view to make the +selected columns sortable. ![Sorting by the title property](./images/admin-sort.png) -**Tip:** Learn more about the [`ApiFilter` attribute](../core/filters.md#apifilter-attribute) in the core documentation. +**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: +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. +> 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. +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. +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: +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 @@ -132,15 +152,20 @@ private $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. +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/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))! +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 diff --git a/admin/validation.md b/admin/validation.md index 121f73b1bf6..16675f0bfd1 100644 --- a/admin/validation.md +++ b/admin/validation.md @@ -1,11 +1,13 @@ # Validation -API Platform Admin manages automatically two types of validation: client-side validation and server-side (or submission) 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). +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: @@ -31,12 +33,16 @@ If you create a new book and try to submit without filling the "Title" field, yo ## Server-side Validation -When the form is submitted and if submission errors are received, -API Platform Admin will automatically show the errors for the corresponding fields. +When the form is submitted and if submission errors are received, API Platform Admin will +automatically show the errors for the corresponding fields. -To do so, it uses the [Server-Side Validation](https://marmelab.com/react-admin/Validation.html#server-side-validation) feature of React Admin, and the mapping between the response and the fields is done by the [schema analyzer](components.md#hydra-schema-analyzer) with its method `getSubmissionErrors`. +To do so, it uses the +[Server-Side Validation](https://marmelab.com/react-admin/Validation.html#server-side-validation) +feature of React Admin, and the mapping between the response and the fields is done by the +[schema analyzer](components.md#hydra-schema-analyzer) with its method `getSubmissionErrors`. -API Platform is supported by default, but if you use another backend, you will need to override the `getSubmissionErrors` method. +API Platform is supported by default, but if you use another backend, you will need to override the +`getSubmissionErrors` method. For example if you have this code: @@ -62,9 +68,15 @@ If you submit the form with an invalid ISBN, you will see: ## Validation With React Admin Inputs -If you replace an `` 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**. +If you replace an `` 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. +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: @@ -80,7 +92,9 @@ 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: +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, @@ -92,6 +106,9 @@ React Admin already comes with several [built-in validators](https://marmelab.co - `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). +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. +Check out the [Form Validation](https://marmelab.com/react-admin/Validation.html) documentation to +learn more. diff --git a/core/bootstrap.md b/core/bootstrap.md index 40c781c745c..89331af4a95 100644 --- a/core/bootstrap.md +++ b/core/bootstrap.md @@ -1,11 +1,12 @@ # 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: +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. +> [!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 diff --git a/core/client-integration.md b/core/client-integration.md index 78fe20d34d6..34c21da4382 100644 --- a/core/client-integration.md +++ b/core/client-integration.md @@ -2,38 +2,42 @@ ## 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. +> [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. +> 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)* +> — _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. +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! +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. +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) @@ -41,12 +45,13 @@ integrates seamlessly with your API endpoints, reducing development time and err #### 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. +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: +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 { @@ -57,53 +62,56 @@ Given an API referencing books and their authors, where `GET /books/1` returns: } ``` -Use an [URLPattern](https://urlpattern.spec.whatwg.org/) to load authors automatically when fetching an author property -such as `books.author?.name`: +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' +import ld from "@api-platform/ld"; const pattern = new URLPattern("/authors/:id", "https://localhost"); -const books = await ld('/books', { +const books = await ld("/books", { urlPattern: pattern, onUpdate: (newBooks) => { - log() - } -}) + log(); + }, +}); function log() { - console.log(books.author?.name) + console.log(books.author?.name); } -log() +log(); ``` -With [api-platform/ld](https://edge-side-api.rocks/linked-data), authors are automatically loaded when needed. +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. +[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: +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 res = await mercure("https://localhost/authors/1", { + onUpdate: (author) => console.log(author), +}); -const author = res.then(res => res.json()) +const author = res.then((res) => res.json()); -// Close if you need to -history.onpushstate = function(e) { - close('https://localhost/authors/1') -} +// Close if you need to +history.onpushstate = function (e) { + close("https://localhost/authors/1"); +}; ``` Assuming `/authors/1` returned the following: @@ -113,32 +121,37 @@ Link: ; rel="self" Link: ; rel="mercure" ``` -An `EventSource` subscribes to the topic `https://localhost/authors/1` on the hub `https://localhost/.well-known/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. +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. +- 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. +- 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'; +import { parseHydraDocumentation } from "@api-platform/api-doc-parser"; -parseHydraDocumentation('https://demo.api-platform.com').then(({api}) => console.log(api)); +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. +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). diff --git a/core/configuration.md b/core/configuration.md index e658c8af5fd..51e08d033df 100644 --- a/core/configuration.md +++ b/core/configuration.md @@ -2,7 +2,8 @@ ## Symfony Configuration -Here's the complete configuration of [API Platform for Symfony](../symfony/index.md) including default values: +Here's the complete configuration of [API Platform for Symfony](../symfony/index.md) including +default values: ```yaml # api/config/packages/api_platform.yaml @@ -289,108 +290,110 @@ api_platform: ### 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: +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: ~ - - # 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 - - # ... + 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: ~ + + # 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: +Here's the complete configuration of [API Platform for Laravel](../laravel/index.md) including +default values: ```php [ // 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 @@ -668,14 +671,14 @@ return [ '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'], @@ -688,12 +691,12 @@ return [ '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, @@ -711,7 +714,8 @@ return [ ### 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: +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 Formats screencast
Watch the Formats screencast

@@ -25,8 +22,8 @@ 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 + [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: @@ -42,71 +39,67 @@ Available formats are: | [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. +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). +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"] + 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. +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: +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"] + 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. +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 +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: @@ -114,21 +107,20 @@ 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"] + 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. +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. +The `format` attribute can be used as a shortcut, it sets both the `inputFormats` and +`outputFormats` in one time. ```php @@ -180,16 +172,14 @@ class Book ```yaml resources: - App\Entity\Book: - formats: - 0: "jsonld" # format already defined in the config - csv: "text/csv" - operations: - ApiPlatform\Metadata\Get: + App\Entity\Book: formats: - json: [ - "application/merge-patch+json", - ] # works also with "application/merge-patch+json" + 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 @@ -218,9 +208,9 @@ resources: ## 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 +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: @@ -228,45 +218,44 @@ Then, register the new format in the configuration: ```yaml # api/config/packages/api_platform.yaml api_platform: - formats: - # ... - myformat: ["application/vnd.myformat"] + 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' } + 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, +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`: +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' ] + 'App\Serializer\CustomItemNormalizer': + arguments: ["@api_platform.serializer.normalizer.item"] + # Uncomment if you don't use the autoconfigure feature + #tags: [ 'serializer.normalizer' ] - # ... + # ... ``` ```php @@ -320,8 +309,8 @@ final class CustomItemNormalizer implements NormalizerInterface, DenormalizerInt } ``` -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: +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 [!WARNING] -> The SortFilter is for Eloquent, the Doctrine equivalent is the OrderFilter. -> The config/api-platform.php is Laravel specific. +> 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: +When working with JSON:API you may want to declare the `SparseFieldset` and the `SortFilter` +globally: ```php @@ -32,14 +33,15 @@ class Book # api/config/api_platform/resources/Book.yaml # The YAML syntax is only supported for Symfony App\ApiResource\Book: - order: - foo: ASC + 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`: +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`: @@ -73,7 +75,7 @@ class Book # api/config/api_platform/resources/Book.yaml # The YAML syntax is only supported for Symfony App\ApiResource\Book: - order: ['foo', 'bar'] + order: ["foo", "bar"] ``` @@ -107,12 +109,13 @@ class Book # api/config/api_platform/resources/Book.yaml # The YAML syntax is only supported for Symfony App\ApiResource\Book: - order: ['author.username'] + order: ["author.username"] ``` -Another possibility is to apply the default order for a specific collection operation, which will override the global default order configuration. +Another possibility is to apply the default order for a specific collection operation, which will +override the global default order configuration. @@ -146,17 +149,17 @@ class Book # 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 + 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 ``` diff --git a/core/deprecations.md b/core/deprecations.md index 98a98b917eb..08c9c2af756 100644 --- a/core/deprecations.md +++ b/core/deprecations.md @@ -1,21 +1,24 @@ # 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. +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. +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. +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. +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. +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: @@ -33,10 +36,12 @@ class Parchment } ``` -As you can see, to deprecate a resource, we just have to explain what the client should do to upgrade in the dedicated attribute. +As you can see, to deprecate a resource, we just have to explain what the client should do to +upgrade in the dedicated attribute. -The deprecation will automatically be taken into account by clients supporting the previously mentioned format, including -[Admin](../admin/index.md), clients created with [Create Client](../create-client/index.md) and the lower level +The deprecation will automatically be taken into account by clients supporting the previously +mentioned format, including [Admin](../admin/index.md), clients created with +[Create Client](../create-client/index.md) and the lower level [api-doc-parser](https://github.com/api-platform/api-doc-parser) library. Here is how it renders for OpenAPI in the built-in Swagger UI shipped with the framework: @@ -47,7 +52,8 @@ And now in the built-in version of GraphiQL (for GraphQL APIs): ![Deprecation shown in GraphiQL](images/deprecated-graphiql.png) -You can also use this new `deprecationReason` attribute to deprecate specific [operations](operations.md): +You can also use this new `deprecationReason` attribute to deprecate specific +[operations](operations.md): ```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 +- 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. +[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: +Thanks to the `sunset` attribute, API Platform makes it easy to set this header for all URLs related +to a resource class: ```php [!NOTE] -> REST and GraphQL architectures recommend to use deprecations instead of 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: @@ -181,5 +195,4 @@ class Parchment } ``` -> [!NOTE] -> It's also possible to use the configuration `route_prefix` to prefix all your operations. +> [!NOTE] It's also possible to use the configuration `route_prefix` to prefix all your operations. diff --git a/core/design.md b/core/design.md index 199376426fc..e838769dd97 100644 --- a/core/design.md +++ b/core/design.md @@ -1,23 +1,27 @@ # 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). +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: @@ -26,27 +30,34 @@ This class will read the API resource object (the one marked with `#[ApiResource - 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 +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 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]` +- 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. +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. diff --git a/core/doctrine-filters.md b/core/doctrine-filters.md index 1748a8dc1ea..1bb5951b495 100644 --- a/core/doctrine-filters.md +++ b/core/doctrine-filters.md @@ -2,16 +2,18 @@ ## Introduction -For further documentation on filters (including for Eloquent and Elasticsearch), please see the [Filters documentation](filters.md). +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. +> [!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. +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. +Here is the recommended approach to apply a `PartialSearchFilter` only to the title and author +properties of a Book resource. ```php [!TIP] -> This filter can be also defined directly on a specific operation like `#[GetCollection(...)])` for finer -> control, like the following code: +> [!TIP] This filter can be also defined directly on a specific operation like +> `#[GetCollection(...)])` for finer control, like the following code: ```php [!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. +> [!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)) +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 [!WARNING] -> The SearchFilter is a multi-type filter that may have inconsistencies (eg: you can search a partial date with LIKE) -> we recommend to use type-specific filters such as `PartialSearchFilter` or `DateFilter` instead. +> [!WARNING] The SearchFilter is a multi-type filter that may have inconsistencies (eg: you can +> search a partial date with LIKE) we recommend to use type-specific filters such as +> `PartialSearchFilter` or `DateFilter` instead. ### Built-in Search Filters since API Platform >= 4.2 @@ -127,19 +131,21 @@ To add some search filters, choose over this new list: - [IriFilter](#iri-filter) (filter on IRIs) - [ExactFilter](#exact-filter) (filter with exact value) - [PartialSearchFilter](#partial-search-filter) (filter using a `LIKE %value%`) -- [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) +- [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: +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' } ] +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: @@ -147,16 +153,20 @@ The search filter supports `exact`, `partial`, `start`, `end`, and `word_start` - `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`. +- `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). +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. +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). +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` @@ -186,7 +196,8 @@ class Offer ``` `http://localhost:8000/api/offers?price=10` will return all offers with a price being exactly `10`. -`http://localhost:8000/api/offers?description=shirt` will return all offers with a description containing the word "shirt". +`http://localhost:8000/api/offers?description=shirt` will return all offers with a description +containing the word "shirt". Filters can be combined: `http://localhost:8000/api/offers?price=10&description=shirt` @@ -196,10 +207,11 @@ The iri filter allows filtering a resource using IRIs. Syntax: `?property=value` -The value can take any [IRI(Internationalized Resource Identifier)](https://en.wikipedia.org/wiki/Internationalized_Resource_Identifier). +The value can take any +[IRI(Internationalized Resource Identifier)](https://en.wikipedia.org/wiki/Internationalized_Resource_Identifier). -This filter can be used on the ApiResource attribute -or in the operation attribute, for e.g., the `#GetCollection()` attribute: +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 @@ -215,8 +227,8 @@ class Chicken } ``` -Given that the endpoint is `/chickens`, you can filter chickens by chicken coop with the following query: -`/chikens?chickenCoop=/chickenCoop/1`. +Given that the endpoint is `/chickens`, you can filter chickens by chicken coop with the following +query: `/chikens?chickenCoop=/chickenCoop/1`. It will return all the chickens that live the chicken coop number 1. @@ -228,8 +240,8 @@ 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: +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 @@ -258,8 +270,8 @@ 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: +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 @@ -280,21 +292,21 @@ Given that the endpoint is `/chickens`, you can filter chickens by name with the 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. +> [!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. ## 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. +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: +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 @@ -302,7 +314,7 @@ or in the operation attribute, for e.g., the `#GetCollection()` attribute: #[GetCollection( parameters: [ 'q' => new QueryParameter( - filter: new FreeTextQueryFilter(new PartialSearchFilter()), + filter: new FreeTextQueryFilter(new PartialSearchFilter()), properties: ['name', 'ean'] ), ], @@ -328,21 +340,21 @@ 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. +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. +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 `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: +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 @@ -350,7 +362,7 @@ exact matches in one of several fields. Example of usage: #[GetCollection( parameters: [ 'autocomplete' => new QueryParameter( - filter: new FreeTextQueryFilter(new OrFilter(new ExactFilter())), + filter: new FreeTextQueryFilter(new OrFilter(new ExactFilter())), properties: ['name', 'ean'] ), ], @@ -378,9 +390,11 @@ 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 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. +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: @@ -405,12 +419,13 @@ 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). +> [!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. +Basically the ApiFilter declares the correct service under the hood. We recommend to use +`QueryParameter` as they're more declarative and hide less complexity. @@ -434,39 +449,40 @@ class Offer ```yaml # config/services.yaml services: - offer.date_filter: - parent: 'api_platform.doctrine.orm.date_filter' - arguments: [{ createdAt: ~ }] - 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.date_filter: + parent: "api_platform.doctrine.orm.date_filter" + arguments: [{ createdAt: ~ }] + 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\ApiResource\Offer: - # ... - operations: - ApiPlatform\Metadata\GetCollection: - filters: ['offer.date_filter'] + # ... + operations: + ApiPlatform\Metadata\GetCollection: + filters: ["offer.date_filter"] ``` ### 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`. +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: +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`) | @@ -499,7 +515,8 @@ class Offer } ``` -Or you can also use the `properties` attribute on the `DateFilter` to apply your [`null` strategy](#managing-null-values): +Or you can also use the `properties` attribute on the `DateFilter` to apply your +[`null` strategy](#managing-null-values): ```php [!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). +> [!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 @@ -556,12 +573,13 @@ 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). +> [!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`. +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`. @@ -596,18 +614,20 @@ 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). +> [!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`. +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 -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. +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` @@ -636,12 +656,13 @@ 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). +> [!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`. +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. @@ -649,8 +670,8 @@ You can filter offers by joining two values, for example: `/offers?price[gt]=12. ## 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. +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]=` @@ -679,19 +700,20 @@ 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). +> [!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`. +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. +> [!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: @@ -699,8 +721,8 @@ 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" + collection: + exists_parameter_name: "not_null" # the URL query parameter to use is now "not_null" ``` ## Order Filter (Sorting) @@ -734,7 +756,8 @@ 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: +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 [!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). +> [!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`. +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: +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). +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 @@ -809,7 +835,8 @@ 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): +Or you can also use the `properties` attribute on the `OrderFilter` to apply your +[`direction` strategy](#basic-directions-strategies-with-the-order-filter): ```php [!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). +> [!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: +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 @@ -887,13 +915,14 @@ class Offer ## Filtering on Nested Properties -> [!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). -> For nested properties support we recommend to use a custom filter. +> [!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). For nested properties support we +> recommend to use a custom filter. -Sometimes, you need to be able to perform filtering based on some linked resources (on the other side of a relation). All -built-in filters support nested properties using the dot (`.`) syntax, e.g.: +Sometimes, you need to be able to perform filtering based on some linked resources (on the other +side of a relation). All built-in filters support nested properties using the dot (`.`) syntax, +e.g.: @@ -919,48 +948,49 @@ 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 + 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'] + # ... + 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` +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. +> [!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: +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: @@ -984,22 +1014,22 @@ class Offer ```yaml # config/services.yaml services: - offer.order_filter: - parent: 'api_platform.doctrine.orm.order_filter' - arguments: [~] # Pass null to enable the filter for all properties - 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.order_filter: + parent: "api_platform.doctrine.orm.order_filter" + arguments: [~] # Pass null to enable the filter for all properties + 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'] + # ... + operations: + ApiPlatform\Metadata\GetCollection: + filters: ["offer.order_filter"] ``` @@ -1019,7 +1049,8 @@ It means that the filter will be **silently** ignored if the property: ## Decorate a Doctrine filter using Symfony -A filter that implements the `ApiPlatform\Doctrine\Common\Filter\PropertyAwareFilterInterface` interface can be decorated: +A filter that implements the `ApiPlatform\Doctrine\Common\Filter\PropertyAwareFilterInterface` +interface can be decorated: ```php namespace App\Doctrine\Filter; @@ -1125,12 +1156,17 @@ class SearchFilterParameter ## 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. +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). +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. +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 = 4.2) Advantages of the new approach: -- Simplicity: No more need to extend `AbstractFilter`. A simple implementation of `FilterInterface` is all it takes. +- 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: +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: +Then, provide the name of your filter, for example `MonthFilter`, or pass it directly as an +argument: ```console make:filter orm MyCustomFilter @@ -1330,9 +1373,11 @@ class MyCustomFilter implements FilterInterface #### 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`). +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. +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: @@ -1361,7 +1406,7 @@ class MonthFilter implements FilterInterface $parameterName = $queryNameGenerator->generateParameterName($property); $alias = $queryBuilder->getRootAliases()[0]; - + $queryBuilder ->andWhere(sprintf('MONTH(%s.%s) = :%s', $alias, $property, $parameterName)) ->setParameter($parameterName, $monthValue); @@ -1369,8 +1414,9 @@ class MonthFilter implements FilterInterface } ``` -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). +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 [!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. +> [!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). +> - 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). +> 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 getValue();` knowing it's already a +- 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. +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). +> [!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`. +If your filter expects a simple type (`int`, `string`, `bool`, or arrays of these types), the +quickest way is to use the `OpenApiFilterTrait`. ```php */ @@ -1519,10 +1573,10 @@ final class MyComplexFilter implements FilterInterface, OpenApiParameterFilterIn // like ?myParam[key1]=value1&myParam[key2]=value2 return [ new OpenApiParameter( - name: $parameter->getKey(), - in: 'query', + name: $parameter->getKey(), + in: 'query', description: 'A custom filter for complex objects.', - style: 'deepObject', + style: 'deepObject', explode: true ) ]; @@ -1532,31 +1586,39 @@ final class MyComplexFilter implements FilterInterface, OpenApiParameterFilterIn ## Creating Custom Doctrine MongoDB ODM Filters -For `Doctrine ORM` filters, please refer to [Creating Custom Doctrine ORM Filters documentation](#creating-custom-doctrine-orm-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. +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`. +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. +- 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: +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: +Then, provide the name of your filter, for example `MonthFilter`, or pass it directly as an +argument: ```console make:filter orm MyCustomFilter @@ -1598,9 +1660,11 @@ class MonthFilter implements FilterInterface #### 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`). +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. +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: @@ -1640,8 +1704,9 @@ class MonthFilter implements FilterInterface } ``` -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). +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(), + filter: new MonthFilter(), property: 'createdAt' ), ] @@ -1675,22 +1740,26 @@ A request like `GET /invoices?createdAtMonth=7` will now correctly return the in #### 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`. +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). +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. +> [!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). +> - 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). +> 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 getValue();` knowing it's already a +- 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. +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). +> [!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`. +If your filter expects a simple type (`int`, `string`, `bool`, or arrays of these types), the +quickest way is to use the `OpenApiFilterTrait`. ```php */ @@ -1790,10 +1863,10 @@ final class MyComplexFilter implements FilterInterface, OpenApiParameterFilterIn // like ?myParam[key1]=value1&myParam[key2]=value2 return [ new OpenApiParameter( - name: $parameter->getKey(), - in: 'query', + name: $parameter->getKey(), + in: 'query', description: 'A custom filter for complex objects.', - style: 'deepObject', + style: 'deepObject', explode: true ) ]; diff --git a/core/dto.md b/core/dto.md index 8c96944d251..7a57998e341 100644 --- a/core/dto.md +++ b/core/dto.md @@ -4,9 +4,13 @@ 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. +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. +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: @@ -16,13 +20,15 @@ This reference covers three implementation strategies: ## 1. The DTO Resource (State Options) -> [!WARNING] -> This is a Symfony only feature in 4.2 and is not working properly without the symfony/object-mapper:^7.4 +> [!WARNING] This is a Symfony only feature in 4.2 and is not working properly without the +> symfony/object-mapper:^7.4 -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. +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. +> [!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 @@ -61,7 +67,9 @@ class Book ### 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`). +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 @@ -75,7 +83,7 @@ use Symfony\Component\ObjectMapper\Attribute\Map; #[ApiResource( shortName: 'Book', // 1. Link this DTO to the Doctrine Entity - stateOptions: new Options(entityClass: BookEntity::class), + stateOptions: new Options(entityClass: BookEntity::class), operations: [ /* ... defined in next sections ... */ ] )] #[Map(source: BookEntity::class)] @@ -112,9 +120,11 @@ final class Book ### Implementation Details: The Object Mapper Magic -Automated mapping relies on two internal classes: `ApiPlatform\State\Provider\ObjectMapperProvider` and `ApiPlatform\State\Processor\ObjectMapperProcessor`. +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: +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). @@ -124,19 +134,26 @@ These classes act as decorators around the standard Provider/Processor chain. Th **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. +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. +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). +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. +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 @@ -242,7 +259,8 @@ final class Book { /* ... */ } ## 3. Custom Business Logic (Custom Processor) -For complex business actions (like applying a discount), standard CRUD mapping isn't enough. You should use a custom Processor paired with a specific Input DTO. +For complex business actions (like applying a discount), standard CRUD mapping isn't enough. You +should use a custom Processor paired with a specific Input DTO. ### The Input DTO @@ -263,7 +281,8 @@ final class DiscountBook ### The Processor -The processor handles the business logic. It receives the DiscountBook DTO as $data and the loaded Entity (retrieved automatically via stateOptions) in the context. +The processor handles the business logic. It receives the DiscountBook DTO as $data and the loaded +Entity (retrieved automatically via stateOptions) in the context. ```php // src/State/DiscountBookProcessor.php diff --git a/core/elasticsearch-filters.md b/core/elasticsearch-filters.md index 01c10c7401d..94bca801121 100644 --- a/core/elasticsearch-filters.md +++ b/core/elasticsearch-filters.md @@ -1,16 +1,17 @@ # Elasticsearch Filters -For further documentation on filters (including for Eloquent and Doctrine), please see the [Filters documentation](filters.md). +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**. +> [!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. +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. +Here is the recommended approach to apply a `MatchFilter` only to the title and author properties of +a Book resource. ```php [!TIP] -> This filter can be also defined directly on a specific operation like `#[GetCollection(...)])` for finer -> control, like the following code: +> [!TIP] This filter can be also defined directly on a specific operation like +> `#[GetCollection(...)])` for finer control, like the following code: ```php ` @@ -89,31 +92,32 @@ 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 + 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'] + # ... + 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`. +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: +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 = 7.11.0 < 8.0 and Elasticsearch >= 8.4 < 9.0. Support for -Elasticsearch 8 was introduced in API Platform 3.2. +Be careful, API Platform only supports Elasticsearch >= 7.11.0 < 8.0 and Elasticsearch >= 8.4 < 9.0. +Support for Elasticsearch 8 was introduced in API Platform 3.2. ## Enabling Reading Support -To enable the reading support for Elasticsearch, simply require the Elasticsearch-PHP package using Composer. For -Elasticsearch 8: +To enable the reading support for Elasticsearch, simply require the Elasticsearch-PHP package using +Composer. For Elasticsearch 8: ```console composer require elasticsearch/elasticsearch:^8.4 @@ -33,19 +34,19 @@ Then, enable it inside the API Platform configuration, using one of the configur ```yaml # api/config/packages/api_platform.yaml parameters: - # ... - env(ELASTICSEARCH_HOST): 'http://localhost:9200' + # ... + env(ELASTICSEARCH_HOST): "http://localhost:9200" api_platform: - # ... + # ... - mapping: - paths: ['%kernel.project_dir%/src/Model'] + mapping: + paths: ["%kernel.project_dir%/src/Model"] - elasticsearch: - hosts: ['%env(ELASTICSEARCH_HOST)%'] + elasticsearch: + hosts: ["%env(ELASTICSEARCH_HOST)%"] - #... + #... ``` ### Enabling Reading Support using Laravel @@ -72,8 +73,8 @@ return [ 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); +- 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. @@ -86,44 +87,44 @@ Here is an example of mappings for 2 resources, `User` and `Tweet`, and their mo ```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" + "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" + } }, - "date": { - "type": "date", - "format": "yyyy-MM-dd HH:mm:ss" - }, - "message": { - "type": "text" - } - }, - "dynamic": "strict" + "dynamic": "strict" } - }, - "dynamic": "strict" } - } } ``` @@ -131,43 +132,43 @@ Here is an example of mappings for 2 resources, `User` and `Tweet`, and their mo ```json { - "mappings": { - "_doc": { - "properties": { - "id": { - "type": "keyword" - }, - "author": { - "properties": { - "id": { - "type": "keyword" - }, - "gender": { - "type": "keyword" - }, - "age": { - "type": "integer" - }, - "first_name": { - "type": "text" + "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" + } }, - "last_name": { - "type": "text" - } - }, - "dynamic": "strict" - }, - "date": { - "type": "date", - "format": "yyyy-MM-dd HH:mm:ss" - }, - "message": { - "type": "text" + "dynamic": "strict" } - }, - "dynamic": "strict" } - } } ``` @@ -242,12 +243,13 @@ class Tweet } ``` -API Platform will automatically disable write operations and snake_case document fields will automatically be converted to -camelCase object properties during serialization. +API Platform will automatically disable write operations and snake_case document fields will +automatically be converted to camelCase object properties during serialization. -Keep in mind that it is your responsibility to populate your Elasticsearch index. To do so, you can use [Logstash](https://www.elastic.co/products/logstash), -a custom [state processors](state-processors.md#creating-a-custom-state-processor) or any other mechanism that suits your -project (such as an [ETL](https://en.wikipedia.org/wiki/Extract,_transform,_load)). +Keep in mind that it is your responsibility to populate your Elasticsearch index. To do so, you can +use [Logstash](https://www.elastic.co/products/logstash), a custom +[state processors](state-processors.md#creating-a-custom-state-processor) or any other mechanism +that suits your project (such as an [ETL](https://en.wikipedia.org/wiki/Extract,_transform,_load)). You're done! The API is now ready to use. @@ -258,4 +260,5 @@ See how to use Elasticsearch filters and how to create Elasticsearch custom filt ## Creating Custom Extensions -See how to create Elasticsearch custom extensions in [the Extensions chapter](extensions.md#custom-elasticsearch-extension). +See how to create Elasticsearch custom extensions in +[the Extensions chapter](extensions.md#custom-elasticsearch-extension). diff --git a/core/enums.md b/core/enums.md index 0d658bf7717..604e1ed56d2 100644 --- a/core/enums.md +++ b/core/enums.md @@ -1,10 +1,13 @@ # Enums as API Resources -API Platform provides support for PHP 8.1+ `BackedEnum`s, allowing them to be exposed as first-class API resources. This enables clients to discover available enum cases and their associated metadata directly through your API. +API Platform provides support for PHP 8.1+ `BackedEnum`s, allowing them to be exposed as first-class +API resources. This enables clients to discover available enum cases and their associated metadata +directly through your API. ## Exposing BackedEnums -To expose a `BackedEnum` as an API resource, simply apply the `#[ApiResource]` attribute to your enum class: +To expose a `BackedEnum` as an API resource, simply apply the `#[ApiResource]` attribute to your +enum class: ```php 400 @@ -43,8 +48,10 @@ The decision works like this, if you are using API Platform with Symfony: 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. +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 @@ -54,8 +61,8 @@ And like this, if you are using API Platform with Laravel: ## 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. +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: @@ -109,30 +116,30 @@ final class ProductManager implements EventSubscriberInterface } ``` -If you use the standard distribution of API Platform, this event listener will be automatically registered. If you use a -custom installation, [learn how to extend API Platform](extending.md). +If you use the standard distribution of API Platform, 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: +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: + # 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 @@ -150,35 +157,36 @@ return [ //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 ], ]; ``` -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: +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." + "@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. +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. @@ -212,15 +220,17 @@ class Book } ``` -Exceptions mappings defined on operations take precedence over mappings defined on resources, which take precedence over -the global config. +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`. +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: +To customize the API Platform response, replace the `api_platform.state.error_provider` with your +own provider: ```php ['trace', 'file', 'line', 'code', 'message', 'traceAsString']]` because you usually don't want these. You can override -this context value if you want. +`\ApiPlatform\Metadata\Exception\HttpExceptionInterface`. For security reasons we add: +`normalizationContext: ['ignored_attributes' => ['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 ! +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: +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; @@ -368,8 +378,8 @@ class MyDomainException extends \Exception implements ProblemExceptionInterface } ``` -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: +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; @@ -385,6 +395,7 @@ class Greeting } ``` -This will automatically document your potential domain exception as a Response in the OpenAPI definition, and show it in the UI : +This will automatically document your potential domain exception as a Response in the OpenAPI +definition, and show it in the UI : ![Swagger UI](images/open-api-documented-error.png) diff --git a/core/events.md b/core/events.md index 6ceee75229c..a9ae9396796 100644 --- a/core/events.md +++ b/core/events.md @@ -1,27 +1,33 @@ # The Event System -> [!WARNING] -> In API Platform 4.0 with Symfony, you need `use_symfony_listeners: true` to activate event listeners. +> [!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. +> [!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) +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. +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. +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)) +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 @@ -54,8 +60,8 @@ Some of these built-in listeners can be enabled/disabled by setting operation at | `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)): +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 | | -------------- | ------ | ------- | ----------------------------------------------------------------------------- | @@ -67,7 +73,9 @@ attribute of an operation](operations.md#recommended-method)): 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: +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 | | ------------------ | ----------------- | -------- | @@ -134,7 +142,10 @@ final class BookMailSubscriber implements EventSubscriberInterface } ``` -If you use the official API Platform distribution, 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). +If you use the official API Platform distribution, 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). +Alternatively, +[the subscriber must be registered manually](https://symfony.com/doc/current/components/event_dispatcher.html#connecting-listeners). diff --git a/core/extending-jsonld-context.md b/core/extending-jsonld-context.md index 1c341c08ec8..af54bfa9886 100644 --- a/core/extending-jsonld-context.md +++ b/core/extending-jsonld-context.md @@ -4,10 +4,10 @@

JSON-LD screencast
Watch the JSON-LD screencast

-API Platform provides the possibility to extend the JSON-LD context of properties. This allows you to describe JSON-LD-typed -values, inverse properties using the `@reverse` keyword, and you can even overwrite the `@id` property this way. -Everything you define within the following annotation will be passed to the context. This provides a generic way to -extend the context. +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 Service Decoration screencast
Watch the Service Decoration screencast

@@ -67,9 +74,9 @@ When using Symfony, the access checker provider is used at three different stage - `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 +> [!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 @@ -125,9 +132,9 @@ or in the `services.yaml` by defining: ```yaml # api/config/services.yaml services: - # ... - App\State\CustomRespondProcessor: - decorates: api_platform.state.processor.respond_processor + # ... + App\State\CustomRespondProcessor: + decorates: api_platform.state.processor.respond_processor ``` And that's it! diff --git a/core/extensions.md b/core/extensions.md index f1453732a87..65c35f2cbd6 100644 --- a/core/extensions.md +++ b/core/extensions.md @@ -2,21 +2,28 @@ 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. +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). +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. +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. +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`. +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: @@ -98,25 +105,29 @@ final readonly class CurrentUserExtension implements QueryCollectionExtensionInt ``` -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. +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: +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 } + '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. +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: +Note that your extensions should have a positive priority if defined. Internal extensions have +negative priorities, for reference: | Service name | Priority | Class | | ---------------------------------------------------------------------- | -------- | -------------------------------------------------------------- | @@ -129,18 +140,21 @@ Note that your extensions should have a positive priority if defined. Internal e #### 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. +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 } + access_control: + # ... + - { path: ^/offers, roles: IS_AUTHENTICATED_FULLY } + - { path: ^/users, roles: IS_AUTHENTICATED_FULLY } ``` ## Custom Doctrine MongoDB ODM Extension @@ -149,17 +163,25 @@ 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. +- `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 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), +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. +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); } } @@ -212,6 +234,8 @@ class AppServiceProvider extends ServiceProvider ## 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. +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. diff --git a/core/external-vocabularies.md b/core/external-vocabularies.md index 593430fa891..5590013c312 100644 --- a/core/external-vocabularies.md +++ b/core/external-vocabularies.md @@ -1,9 +1,11 @@ # 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/). +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). +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 Filtering and Searching screencast
Watch the Filtering & Searching screencast

-> [!WARNING] -> For maximum flexibility and to ensure future compatibility, it is strongly recommended to configure your filters via -> the parameters attribute using `QueryParameter`. The legacy method using the `ApiFilter` attribute is not recommended. +> [!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): +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`). @@ -28,29 +37,30 @@ The recommended way to define parameters is by using Parameter attributes direct 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: +Here is a list of available filters you can use. You can pass the filter class name (recommended) or +a new instance: - **`DateFilter`**: For filtering by date intervals (e.g., `?createdAt[after]=...`). - - Usage: `new QueryParameter(filter: DateFilter::class)` + - Usage: `new QueryParameter(filter: DateFilter::class)` - **`ExactFilter`**: For exact value matching. - - Usage: `new QueryParameter(filter: ExactFilter::class)` + - Usage: `new QueryParameter(filter: ExactFilter::class)` - **`PartialSearchFilter`**: For partial string matching (SQL `LIKE %...%`). - - Usage: `new QueryParameter(filter: PartialSearchFilter::class)` + - Usage: `new QueryParameter(filter: PartialSearchFilter::class)` - **`IriFilter`**: For filtering by IRIs (e.g., relations). - - Usage: `new QueryParameter(filter: IriFilter::class)` + - Usage: `new QueryParameter(filter: IriFilter::class)` - **`BooleanFilter`**: For boolean field filtering. - - Usage: `new QueryParameter(filter: BooleanFilter::class)` + - Usage: `new QueryParameter(filter: BooleanFilter::class)` - **`NumericFilter`**: For numeric field filtering. - - Usage: `new QueryParameter(filter: NumericFilter::class)` + - Usage: `new QueryParameter(filter: NumericFilter::class)` - **`RangeFilter`**: For range-based filtering (e.g., prices between X and Y). - - Usage: `new QueryParameter(filter: RangeFilter::class)` + - Usage: `new QueryParameter(filter: RangeFilter::class)` - **`ExistsFilter`**: For checking existence of nullable values. - - Usage: `new QueryParameter(filter: ExistsFilter::class)` + - Usage: `new QueryParameter(filter: ExistsFilter::class)` - **`OrderFilter`**: For sorting results. - - Usage: `new QueryParameter(filter: OrderFilter::class)` + - 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. +> [!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. You can declare a parameter on the resource class to make it available for all its operations: @@ -100,7 +110,8 @@ class Friend ### Using Filters with DateTime Properties -When working with `DateTime` or `DateTimeImmutable` properties, the system might default to exact matching. To enable date ranges (e.g., `after`, `before`), you must explicitly use the `DateFilter`: +When working with `DateTime` or `DateTimeImmutable` properties, the system might default to exact +matching. To enable date ranges (e.g., `after`, `before`), you must explicitly use the `DateFilter`: ```php new QueryParameter( // Use the class string to leverage the service container (recommended) - filter: DateFilter::class, + filter: DateFilter::class, properties: ['startDate', 'endDate'] ) ] @@ -136,9 +147,12 @@ This configuration allows clients to filter events by date ranges using queries ### 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. +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. +For Hydra, you can map a query parameter to `hydra:freetextQuery` to indicate a general-purpose +search query. ```php [!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`. +> [!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` placeholde and use the parameter's `properties` configuration: +Filters that work on a per-parameter basis can also use the `:property` placeholde and use the +parameter's `properties` configuration: ```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).'%'); @@ -282,10 +302,11 @@ final class PartialSearchFilter implements FilterInterface 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. +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. +> [!NOTE] Invalid values are usually ignored by our filters, use [validation](#parameter-validation) +> to trigger errors for wrong parameter values. ## OpenAPI and JSON Schema @@ -293,7 +314,8 @@ 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. +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 '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`. +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 [!NOTE] -> When `castToNativeType` is enabled, API Platform infers type validation from the JSON Schema. +> [!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. +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. +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. +- **`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. + - 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. + - 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. +- **`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). +- **`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`). + - 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. +- **`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. +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 [!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 distribution. -> [!NOTE] -> When unsing JSON:API check out the [specific SparseFieldset and Sort filters](./content-negotiation/#jsonapi-sparse-fieldset-and-sort-parameters) +> [!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 +> distribution. [!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). @@ -475,19 +520,26 @@ 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) +- `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`. +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`. +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. +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 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; } } @@ -530,7 +582,8 @@ class WithParameter 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. +- **`fetch_data`**: Boolean (default: `false`) - When `true`, forces the IRI converter to fetch the + actual entity data instead of just creating a reference. ### `ReadLinkParameterProvider` @@ -538,11 +591,14 @@ This provider must be enabled before it can be used. ```yaml api_platform: - enable_link_security: true + 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. +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 QueryParameter( - provider: ReadLinkParameterProvider::class, + provider: ReadLinkParameterProvider::class, extraProperties: [ 'resource_class' => Dummy::class, 'uri_template' => '/dummies/{id}' // Optional: specify the template for the linked resource @@ -571,7 +627,7 @@ use App\Entity\Dummy; ], provider: [self::class, 'provideDummyFromParameter'], )] -class WithParameter +class WithParameter { public static function provideDummyFromParameter(Operation $operation, array $uriVariables = [], array $context = []): object|array { @@ -586,7 +642,8 @@ 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 +- 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 @@ -596,11 +653,12 @@ You can control the behavior of `ReadLinkParameterProvider` with these `extraPro - **`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 +- **`throw_not_found`**: Boolean (default: `true`) - Whether to throw `NotFoundHttpException` when + resource is not found ```php 'dummy' => new QueryParameter( - provider: ReadLinkParameterProvider::class, + provider: ReadLinkParameterProvider::class, extraProperties: [ 'resource_class' => Dummy::class, 'throw_not_found' => false, // Won't throw NotFoundHttpException if resource is missing @@ -611,7 +669,9 @@ You can control the behavior of `ReadLinkParameterProvider` with these `extraPro ### 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: +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 @@ -624,9 +684,12 @@ Both `IriConverterParameterProvider` and `ReadLinkParameterProvider` support pro ### 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. +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: +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(); @@ -661,12 +724,16 @@ final class DynamicGroupProvider implements ParameterProviderInterface ### 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. +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 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`: @@ -697,10 +764,10 @@ final class RegexpFilter implements FilterInterface $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; @@ -736,30 +803,31 @@ use App\Filter\RegexpFilter; class User {} ``` -> [!NOTE] -> A `filter` is either an instanceof `FilterInterface` or a string referencing a filter service. +> [!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. | +| 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. +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 [!WARNING] -> Adding support for `application/x-www-form-urlencoded` makes your API vulnerable to [CSRF (Cross-Site Request Forgery)](https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)) attacks. +> 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 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. +> 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: +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'] +# 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. +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 { +#[Post( + inputFormats: ['form' => ['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; - } -} + public static function process(mixed $data, Operation $operation, array $uriVariables, array $context) { + return $data; + } +} ``` diff --git a/core/getting-started.md b/core/getting-started.md index 7ea79cd42d5..b0034f0b68e 100644 --- a/core/getting-started.md +++ b/core/getting-started.md @@ -2,52 +2,65 @@ ## Installing API Platform -You can choose your preferred stack between Symfony, Laravel, or bootstrapping the API Platform core library manually. +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). +> [!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). +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), +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). +[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. +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: +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). +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. +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). +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)... +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). +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. +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 @@ -55,9 +68,9 @@ and what [JSON-LD](https://json-ld.org/) and [Hydra](https://www.hydra-cg.com/)

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

-API Platform can automatically expose entities mapped as "API resources" through a REST API supporting CRUD -operations. -To expose your entities, you can use attributes, XML, and YAML configuration files. +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: @@ -157,18 +170,20 @@ class Offer } ``` -It is the minimal configuration required to expose `Product` and `Offer` entities as JSON-LD documents through an hypermedia -web API. +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. +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: +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 @@ -182,11 +197,11 @@ for resources of the product type: > [!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). +> `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: @@ -195,12 +210,12 @@ 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 + 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 @@ -226,26 +241,29 @@ resources: -If you prefer to use YAML or XML files instead of attributes, you must configure API Platform to load the appropriate files: +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 + 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`. +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. +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 @@ -255,7 +273,8 @@ First, create a migration class for the `products` table: php artisan make:migration create_products_table ``` -Open the generated migration class (`database/migrations/_create_products_table.php`) and add some columns: +Open the generated migration class (`database/migrations/_create_products_table.php`) and +add some columns: ```patch public function up(): void @@ -294,7 +313,8 @@ use Illuminate\Database\Eloquent\Model; class Product extends Model {} ``` -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. +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. ```patch [!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`. +> [!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. +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 @@ -33,8 +40,8 @@ Using the Symfony variant we can do this modification by adding the following co ```yaml # api/config/routes.yaml api_graphql_entrypoint: - path: /graphql - controller: api_platform.graphql.action.entrypoint + path: /graphql + controller: api_platform.graphql.action.entrypoint # ... ``` @@ -57,7 +64,8 @@ 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. +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`. @@ -68,9 +76,9 @@ If you need to disable it, it can be done in the configuration: ```yaml # api/config/packages/api_platform.yaml api_platform: - graphql: - graphiql: - enabled: false + graphql: + graphiql: + enabled: false # ... ``` @@ -91,22 +99,25 @@ return [ ### 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. +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: +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 + 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: +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 @@ -130,16 +141,16 @@ You can disable it if you want in the configuration. ```yaml # api/config/packages/api_platform.yaml api_platform: - graphql: - graphql_playground: - enabled: false + 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) +> [!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 @@ -152,8 +163,8 @@ Using the Symfony variant we can do this modification by adding the following co ```yaml # app/config/routes.yaml graphql_playground: - path: /docs/graphql_playground - controller: api_platform.graphql.action.graphql_playground + path: /docs/graphql_playground + controller: api_platform.graphql.action.graphql_playground ``` ### Laravel config routes for GraphQL Playground @@ -178,9 +189,9 @@ When going to the GraphQL endpoint, you can choose to launch the IDE you want. ```yaml # api/config/packages/api_platform.yaml api_platform: - graphql: - # Choose between graphiql or graphql-playground - default_ide: graphql-playground + graphql: + # Choose between graphiql or graphql-playground + default_ide: graphql-playground # ... ``` @@ -205,8 +216,8 @@ You can also disable this feature by setting the configuration value to `false`. ```yaml # api/config/packages/api_platform.yaml api_platform: - graphql: - default_ide: false + graphql: + default_ide: false # ... ``` @@ -234,8 +245,8 @@ 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 + graphql: + introspection: false # ... ``` @@ -256,17 +267,19 @@ return [ ## 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): +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'] + formats: + # ... + graphql: ["application/graphql"] ``` ### Laravel config for GraphQL Content-Type @@ -286,12 +299,13 @@ return [ ## Operations -> [!NOTE] -> In Symfony we use the term “entities”, while the following documentation is mostly for Laravel “models”. +> [!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. +For GraphQL, the operations are defined by using the `Query`, `QueryCollection`, `Mutation`, +`DeleteMutation` and `Subscription` attributes. By default, the following operations are enabled: @@ -303,7 +317,8 @@ By default, the following operations are enabled: 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: +For instance, in the following example, only the query of an item and the create mutation are +enabled: @@ -329,11 +344,11 @@ class Book ```yaml #The YAML syntax is only supported for Symfony resources: - App\Entity\Book: - graphQlOperations: - ApiPlatform\Metadata\GraphQl\Query: ~ - ApiPlatform\Metadata\GraphQl\Mutation: - name: create + App\Entity\Book: + graphQlOperations: + ApiPlatform\Metadata\GraphQl\Query: ~ + ApiPlatform\Metadata\GraphQl\Mutation: + name: create ``` ```xml @@ -355,24 +370,27 @@ resources: ## Queries -If you don't know what queries are yet, please [read the documentation about them](https://graphql.org/learn/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. +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). +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 - } + book(id: "/books/89") { + title + isbn + } } ``` @@ -414,17 +432,18 @@ final class BookCollectionResolver implements QueryCollectionResolverInterface ### Custom Queries config for Symfony -If you use autoconfiguration (the default Symfony configuration) in your application, then you are done! +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 } + # ... + App\Resolver\BookCollectionResolver: + tags: + - { name: api_platform.graphql.query_resolver } ``` ### Custom Queries config for Laravel @@ -482,11 +501,14 @@ final class BookResolver implements QueryItemResolverInterface } ``` -Note that you will receive the retrieved item or not in this resolver depending on how you configure your query in your resource. +Note that you will receive the retrieved item or not in this resolver depending on how you configure +your query in your resource. -Since the resolver is a service, you can inject some dependencies and fetch your item in the resolver if you want. +Since the resolver is a service, you can inject some dependencies and fetch your item in the +resolver if you want. -Now that your resolver is created and registered, you can configure your custom query and link its resolver. +Now that your resolver is created and registered, you can configure your custom query and link its +resolver. In your resource, add the following: @@ -543,42 +565,42 @@ 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 + 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 @@ -628,59 +650,65 @@ resources: -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). +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. +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. +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. +- 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. +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 - } + retrievedQueryBook(id: "/books/56") { + title + } - withDefaultArgsNotRetrievedQueryBook(id: "/books/56") { - title - } + notRetrievedQueryBook { + title + } - withCustomArgsQueryBook(id: "/books/23", log: true, logDate: "2019-12-20") { - title - } + withDefaultArgsNotRetrievedQueryBook(id: "/books/56") { + title + } - collectionQueryBooks { - edges { - node { + 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). +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: @@ -688,20 +716,22 @@ For each resource, three mutations are available: - `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. +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), +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 - } + deleteBook(input: { id: $id, clientMutationId: $clientMutationId }) { + clientMutationId + } } ``` @@ -739,37 +769,47 @@ final class BookMutationResolver implements MutationResolverInterface } ``` -As you can see, depending on how you configure your custom mutation in the resource, the item is retrieved or not. -For instance, if you don't set an `id` argument or if you disable the `read` or the `deserialize` providers (other state providers and state processors [can also be disabled](#disabling-system-providers-and-processors)), -the received item will be `null`. +As you can see, depending on how you configure your custom mutation in the resource, the item is +retrieved or not. For instance, if you don't set an `id` argument or if you disable the `read` or +the `deserialize` providers (other state providers and state processors +[can also be disabled](#disabling-system-providers-and-processors)), the received item will be +`null`. -Likewise, if you don't want your item to be persisted by API Platform, -you can return `null` instead of the mutated item (be careful: the response will also be `null`) or disable the `write` provider. +Likewise, if you don't want your item to be persisted by API Platform, you can return `null` instead +of the mutated item (be careful: the response will also be `null`) or disable the `write` provider. Don't forget the resolver is a service and you can inject the dependencies you want. -If you don't use autoconfiguration, add the tag `api_platform.graphql.mutation_resolver` to the resolver service. -If you're using Laravel, don't forget to tag the resolver service with the `ApiPlatform\GraphQl\Resolver\MutationResolverInterface`. +If you don't use autoconfiguration, add the tag `api_platform.graphql.mutation_resolver` to the +resolver service. If you're using Laravel, don't forget to tag the resolver service with the +`ApiPlatform\GraphQl\Resolver\MutationResolverInterface`. -Note that you need to explicitly add the auto-generated queries and mutations if they are needed when configuring custom mutations, like it's done for the [operations](#operations). +Note that you need to explicitly add the auto-generated queries and mutations if they are needed +when configuring custom mutations, like it's done for the [operations](#operations). -As the custom queries, you can define your own arguments if you don't want to use the default ones (extracted from your resource). -The only difference with them is that, even if you define your own arguments, the `clientMutationId` will always be set. -You can also use the `extraArgs` property in case you need to add additional arguments (for instance to add the `id` argument since it is not added by default for a custom mutation). +As the custom queries, you can define your own arguments if you don't want to use the default ones +(extracted from your resource). The only difference with them is that, even if you define your own +arguments, the `clientMutationId` will always be set. You can also use the `extraArgs` property in +case you need to add additional arguments (for instance to add the `id` argument since it is not +added by default for a custom mutation). The arguments will be in `$context['args']['input']` of your resolvers. ## Subscriptions -Subscriptions are an [RFC](https://github.com/graphql/graphql-spec/blob/master/rfcs/Subscriptions.md#rfc-graphql-subscriptions) to allow a client to receive pushed realtime data from the server. +Subscriptions are an +[RFC](https://github.com/graphql/graphql-spec/blob/master/rfcs/Subscriptions.md#rfc-graphql-subscriptions) +to allow a client to receive pushed realtime data from the server. -In API Platform, the built-in subscription support is handled by using [Mercure](https://mercure.rocks/) as its underlying protocol. +In API Platform, the built-in subscription support is handled by using +[Mercure](https://mercure.rocks/) as its underlying protocol. ### Enable Update Subscriptions for a Resource To enable update subscriptions for a resource, these conditions have to be met: -- the [Mercure hub and bundle need to be installed and configured](mercure.md#installing-mercure-support). +- the + [Mercure hub and bundle need to be installed and configured](mercure.md#installing-mercure-support). - Mercure needs to be enabled for the resource. - the `update` mutation needs to be enabled for the resource. - the subscription needs to be enabled for the resource. @@ -800,11 +840,11 @@ class Book ```yaml #The YAML syntax is only supported for Symfony resources: - App\Entity\Book: - graphQlOperations: - ApiPlatform\Metadata\GraphQl\Mutation: - name: update - ApiPlatform\Metadata\GraphQl\Subscription: ~ + App\Entity\Book: + graphQlOperations: + ApiPlatform\Metadata\GraphQl\Mutation: + name: update + ApiPlatform\Metadata\GraphQl\Subscription: ~ ``` ```xml @@ -830,50 +870,56 @@ Doing a subscription is very similar to doing a query: ```graphql { - subscription { - updateBookSubscribe( - input: { id: "/books/1", clientSubscriptionId: "myId" } - ) { - book { - title - isbn - } - mercureUrl - clientSubscriptionId + 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. +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. +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. +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. +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" - } + "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. +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. +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. @@ -881,16 +927,19 @@ It's recommended to use an adapter like Redis for this cache. 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. +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. +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. +The following table lists the system states providers and states processors you can disable in your +resource configuration. | Attribute | Type | Default | Description | | -------------------------- | ------ | ------- | ----------------------------------------- | @@ -929,13 +978,13 @@ class Book ```yaml #The YAML syntax is only supported for Symfony resources: - App\Entity\Book: - graphQlOperations: - ApiPlatform\Metadata\GraphQl\Query: ~ - ApiPlatform\Metadata\GraphQl\QueryCollection: ~ - ApiPlatform\Metadata\GraphQl\Mutation: - name: create - write: false + App\Entity\Book: + graphQlOperations: + ApiPlatform\Metadata\GraphQl\Query: ~ + ApiPlatform\Metadata\GraphQl\QueryCollection: ~ + ApiPlatform\Metadata\GraphQl\Mutation: + name: create + write: false ``` ```xml @@ -985,13 +1034,13 @@ class Book #The YAML syntax is only supported for Symfony resources: - App\Entity\Book: - write: false - graphQlOperations: - ApiPlatform\Metadata\GraphQl\Query: ~ - ApiPlatform\Metadata\GraphQl\QueryCollection: ~ - ApiPlatform\Metadata\GraphQl\Mutation: - name: create + App\Entity\Book: + write: false + graphQlOperations: + ApiPlatform\Metadata\GraphQl\Query: ~ + ApiPlatform\Metadata\GraphQl\QueryCollection: ~ + ApiPlatform\Metadata\GraphQl\Mutation: + name: create ``` ```xml @@ -1014,17 +1063,20 @@ resources: ## 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. +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. +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`). +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: @@ -1057,18 +1109,18 @@ class Offer ```yaml #The YAML syntax is only supported for Symfony resources: - App\Entity\Book: - filters: ['offer.search_filter'] - graphQlOperations: - - class: ApiPlatform\Metadata\GraphQl\Query - - class: ApiPlatform\Metadata\GraphQl\QueryCollection - filters: ['offer.date_filter'] - - class: ApiPlatform\Metadata\GraphQl\Mutation - name: create - - class: ApiPlatform\Metadata\GraphQl\Mutation - name: update - - class: ApiPlatform\Metadata\GraphQl\Mutation - name: delete + App\Entity\Book: + filters: ["offer.search_filter"] + graphQlOperations: + - class: ApiPlatform\Metadata\GraphQl\Query + - class: ApiPlatform\Metadata\GraphQl\QueryCollection + filters: ["offer.date_filter"] + - class: ApiPlatform\Metadata\GraphQl\Mutation + name: create + - class: ApiPlatform\Metadata\GraphQl\Mutation + name: update + - class: ApiPlatform\Metadata\GraphQl\Mutation + name: delete ``` ```xml @@ -1101,38 +1153,39 @@ resources: ### 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. +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 - } + 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. +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 - } + offers(order: [{ id: "ASC" }, { name: "DESC" }]) { + edges { + node { + id + name + } + } } - } } ``` @@ -1140,7 +1193,8 @@ 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.: +Unlike for REST, all built-in filters support nested properties using the underscore (`_`) syntax +instead of the dot (`.`) syntax, e.g.: ```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. +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). +- `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. @@ -1404,17 +1469,17 @@ An example of a query: ```graphql { - offers(page: 3, itemsPerPage: 15) { - collection { - id - } - paginationInfo { - itemsPerPage - lastPage - totalCount - hasNextPage + offers(page: 3, itemsPerPage: 15) { + collection { + id + } + paginationInfo { + itemsPerPage + lastPage + totalCount + hasNextPage + } } - } } ``` @@ -1431,10 +1496,10 @@ The pagination can be disabled for all GraphQL resources using this configuratio ```yaml # api/config/packages/api_platform.yaml api_platform: - graphql: - collection: - pagination: - enabled: false + graphql: + collection: + pagination: + enabled: false ``` ##### Disable pagination for all GraphQL resources with Laravel @@ -1477,8 +1542,8 @@ class Book ```yaml #The YAML syntax is only supported for Symfony resources: - App\Entity\Book: - paginationEnabled: false + App\Entity\Book: + paginationEnabled: false ``` ```xml @@ -1517,10 +1582,10 @@ class Book ```yaml #The YAML syntax is only supported for Symfony resources: - App\Entity\Book: - graphQlOperations: - ApiPlatform\Metadata\GraphQl\QueryCollection: - paginationEnabled: false + App\Entity\Book: + graphQlOperations: + ApiPlatform\Metadata\GraphQl\QueryCollection: + paginationEnabled: false ``` ```xml @@ -1543,18 +1608,22 @@ resources: [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`. +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. +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. +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. +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. @@ -1593,26 +1662,26 @@ class Book ```yaml #The YAML syntax is only supported for Symfony resources: - App\Entity\Book: - security: "is_granted('ROLE_USER')" - operations: - ApiPlatform\Metadata\Get: - security: "is_granted('ROLE_USER') and object.owner == user" - securityMessage: 'Sorry, but you are not the book owner.' - ApiPlatform\Metadata\Post: - security: "is_granted('ROLE_ADMIN')" - securityMessage: 'Only admins can add books.' - graphQlOperations: - ApiPlatform\Metadata\GraphQl\Query: - security: "is_granted('ROLE_USER') and object.owner == user" - ApiPlatform\Metadata\GraphQl\QueryCollection: - security: "is_granted('ROLE_ADMIN')" - ApiPlatform\Metadata\GraphQl\DeleteMutation: - name: delete - security: "is_granted('ROLE_ADMIN')" - ApiPlatform\Metadata\GraphQl\Mutation: - name: create - security: "is_granted('ROLE_ADMIN')" + App\Entity\Book: + security: "is_granted('ROLE_USER')" + operations: + ApiPlatform\Metadata\Get: + security: "is_granted('ROLE_USER') and object.owner == user" + securityMessage: "Sorry, but you are not the book owner." + ApiPlatform\Metadata\Post: + security: "is_granted('ROLE_ADMIN')" + securityMessage: "Only admins can add books." + graphQlOperations: + ApiPlatform\Metadata\GraphQl\Query: + security: "is_granted('ROLE_USER') and object.owner == user" + ApiPlatform\Metadata\GraphQl\QueryCollection: + security: "is_granted('ROLE_ADMIN')" + ApiPlatform\Metadata\GraphQl\DeleteMutation: + name: delete + security: "is_granted('ROLE_ADMIN')" + ApiPlatform\Metadata\GraphQl\Mutation: + name: create + security: "is_granted('ROLE_ADMIN')" ``` ```xml @@ -1640,23 +1709,30 @@ resources: ### 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. +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. +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. +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. +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. +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`). +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: @@ -1701,19 +1777,19 @@ class User ```yaml #The YAML syntax is only supported for Symfony resources: - App\Entity\User: - graphQlOperations: - ApiPlatform\Metadata\GraphQl\Query: - security: "is_granted('VIEW', object)" - ApiPlatform\Metadata\GraphQl\QueryCollection: - security: "is_granted('ROLE_ADMIN')" + App\Entity\User: + graphQlOperations: + ApiPlatform\Metadata\GraphQl\Query: + security: "is_granted('VIEW', object)" + ApiPlatform\Metadata\GraphQl\QueryCollection: + security: "is_granted('ROLE_ADMIN')" properties: - App\Entity\User: - viewableDocuments: - security: "is_granted('VIEW', object)" - email: - security: "is_granted('ROLE_ADMIN')" + App\Entity\User: + viewableDocuments: + security: "is_granted('VIEW', object)" + email: + security: "is_granted('ROLE_ADMIN')" ``` ```xml @@ -1784,19 +1860,19 @@ class Document ```yaml #The YAML syntax is only supported for Symfony resources: - App\Entity\Document: - graphQlOperations: - ApiPlatform\Metadata\GraphQl\Query: - security: "is_granted('VIEW', object)" - ApiPlatform\Metadata\GraphQl\QueryCollection: - security: "is_granted('ROLE_ADMIN')" + App\Entity\Document: + graphQlOperations: + ApiPlatform\Metadata\GraphQl\Query: + security: "is_granted('VIEW', object)" + ApiPlatform\Metadata\GraphQl\QueryCollection: + security: "is_granted('ROLE_ADMIN')" properties: - App\Entity\Document: - viewers: - security: "is_granted('VIEW', object)" - createdBy: - security: "is_granted('VIEW', object)" + App\Entity\Document: + viewers: + security: "is_granted('VIEW', object)" + createdBy: + security: "is_granted('VIEW', object)" ``` ```xml @@ -1826,26 +1902,34 @@ properties: -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. +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). +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. +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. +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. +- 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: @@ -1892,24 +1976,24 @@ class Book ```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: + App\Entity\Book: normalizationContext: - groups: ['query_collection'] - ApiPlatform\Metadata\GraphQl\Mutation: - name: create - normalizationContext: - groups: ['query_collection'] + groups: ["read"] denormalizationContext: - groups: ['mutation'] + 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 @@ -1987,29 +2071,38 @@ resources: -In this case, the REST endpoint will be able to get the two attributes of the book and to modify only its title. +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). +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). +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). +- 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. +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: @@ -2039,10 +2132,10 @@ class Book ```yaml #The YAML syntax is only supported for Symfony resources: - App\Entity\Book: - graphQlOperations: - ApiPlatform\Metadata\GraphQl\Mutation: - name: create + App\Entity\Book: + graphQlOperations: + ApiPlatform\Metadata\GraphQl\Mutation: + name: create ``` ```xml @@ -2061,28 +2154,27 @@ resources: -Creating a book with its author will be done like this, where `/authors/32` is the IRI of an existing resource: +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 + 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): +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): @@ -2113,12 +2205,12 @@ class Book ```yaml #The YAML syntax is only supported for Symfony resources: - App\Entity\Book: - graphQlOperations: - ApiPlatform\Metadata\GraphQl\Mutation: - name: create - denormalizationContext: - groups: ['book:create'] + App\Entity\Book: + graphQlOperations: + ApiPlatform\Metadata\GraphQl\Mutation: + name: create + denormalizationContext: + groups: ["book:create"] ``` ```xml @@ -2173,21 +2265,16 @@ In this case, creating a book with its author can now be done like this: ```graphql { - mutation { - createBook( - input: { - title: "The Name of the Wind" - author: { name: "Patrick Rothfuss" } - } - ) { - book { - title - author { - name + mutation { + createBook(input: { title: "The Name of the Wind", author: { name: "Patrick Rothfuss" } }) { + book { + title + author { + name + } + } } - } } - } } ``` @@ -2195,8 +2282,9 @@ In this case, creating a book with its author can now be done like this: ### Handling Exceptions and Errors (Logging, Filtering, ...) -When there are errors (GraphQL ones, or if an exception is sent), a default error handler (`api_platform.graphql.error_handler`) is called. -Its main responsibility is to apply a formatter to them. +When there are errors (GraphQL ones, or if an exception is sent), a default error handler +(`api_platform.graphql.error_handler`) is called. Its main responsibility is to apply a formatter to +them. If you need to log the errors, or if you want to filter them, you have to decorate this service. @@ -2236,9 +2324,9 @@ Then register the service: ```yaml # api/config/services.yaml services: - # ... - App\Error\ErrorHandler: - decorates: api_platform.graphql.error_handler + # ... + App\Error\ErrorHandler: + decorates: api_platform.graphql.error_handler ``` ```xml @@ -2298,25 +2386,37 @@ class AppServiceProvider extends ServiceProvider ### 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). +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). +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. +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). +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. +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. +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). +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: @@ -2350,26 +2450,30 @@ final class MyExceptionNormalizer implements NormalizerInterface } ``` -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. +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. +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: +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 } + 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. +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). +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: @@ -2398,13 +2502,13 @@ By default, with the search filter, the query to retrieve a collection will be: ```graphql { - books(publicationDate: "2010") { - edges { - node { - publicationDate - } + books(publicationDate: "2010") { + edges { + node { + publicationDate + } + } } - } } ``` @@ -2412,19 +2516,20 @@ But if you use the `CamelCaseToSnakeCaseNameConverter`, it will be: ```graphql { - books(publication_date: "2010") { - edges { - node { - publication_date - } + 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. +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: @@ -2456,25 +2561,26 @@ You would need to use the search filter like this: ```graphql { - books(related_books_title: "The Fitz and the Fool") { - edges { - node { - title - } + 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 `_`: +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: __ + graphql: + nesting_separator: __ # ... ``` @@ -2482,13 +2588,13 @@ In this case, your query will be: ```graphql { - books(related_books__title: "The Fitz and the Fool") { - edges { - node { - title - } + books(related_books__title: "The Fitz and the Fool") { + edges { + node { + title + } + } } - } } ``` @@ -2509,13 +2615,13 @@ In this case, your query will be: ```graphql { - books(related_books__title: "The Fitz and the Fool") { - edges { - node { - title - } + books(related_books__title: "The Fitz and the Fool") { + edges { + node { + title + } + } } - } } ``` @@ -2525,9 +2631,11 @@ Much better, isn't it? 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`. +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. +You should extend the `GraphQL\Type\Definition\ScalarType` class too to take advantage of its useful +methods. For instance, to create a custom `DateType`: @@ -2596,29 +2704,33 @@ final class DateTimeType extends ScalarType implements TypeInterface } ``` -You can also check the documentation of [graphql-php](https://webonyx.github.io/graphql-php/type-definitions/scalars/#writing-custom-scalar-types). +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. +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! +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 } + # ... + 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). +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 @@ -2644,12 +2756,13 @@ class AppServiceProvider extends ServiceProvider 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). +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. +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: @@ -2658,9 +2771,9 @@ To do so, you need to decorate the `api_platform.graphql.type_converter` service ```yaml # api/config/services.yaml services: - # ... - 'App\Type\TypeConverter': - decorates: api_platform.graphql.type_converter + # ... + 'App\Type\TypeConverter': + decorates: api_platform.graphql.type_converter ``` ### Laravel TypeConverter Decoration @@ -2722,7 +2835,8 @@ final class TypeConverter implements TypeConverterInterface 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: +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() @@ -2736,11 +2850,13 @@ 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. +[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 service is `api_platform.graphql.serializer.context_builder` and the method to override is +`create`. The decorator could be like this: @@ -2813,8 +2929,8 @@ final class BookContextBuilder implements SerializerContextBuilderInterface ## 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) +> [!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. @@ -2824,7 +2940,8 @@ The `api:graphql:export` command is provided to do so: 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: +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 @@ -2832,10 +2949,11 @@ Since the command prints the schema to the output if you don't use the `-o` opti ## 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. +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). +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. @@ -2904,9 +3022,10 @@ class MediaObject } ``` -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']`. +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. @@ -2944,10 +3063,10 @@ For handling the upload of multiple files, iterate over `$context['args']['input ### 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: +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 +````php [!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. +> [!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. +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. @@ -37,12 +39,12 @@ final class Person # api/config/api_platform/resources/Person.yaml # The YAML syntax is only supported for Symfony properties: - App\ApiResource\Person: - code: - identifier: true + App\ApiResource\Person: + code: + identifier: true resource: - App\ApiResource\Person: - provider: App\State\PersonProvider + App\ApiResource\Person: + provider: App\State\PersonProvider ``` ```xml @@ -57,8 +59,9 @@ resource: -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`. +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: @@ -88,8 +91,8 @@ final class PersonProvider implements ProviderInterface } ``` -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: +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 SymfonyCasts, API Platform screencasts

-The easiest and funniest way to learn how to use API Platform for Symfony is to watch [the more than 60 screencasts available on SymfonyCasts](https://symfonycasts.com/tracks/rest?cid=apip#api-platform-3)! +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)! diff --git a/core/json-schema.md b/core/json-schema.md index d751441d87d..c90cd12ca33 100644 --- a/core/json-schema.md +++ b/core/json-schema.md @@ -1,17 +1,18 @@ # 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). +[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). +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) +> [!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: @@ -27,32 +28,38 @@ 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. +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 +> [!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. +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. +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. +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. +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: +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 [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. +> [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). +- For Laravel users, explore the [JWT Authentication with Laravel documentation](/laravel/jwt.md). diff --git a/core/mercure.md b/core/mercure.md index a8748656a38..7cea94354e5 100644 --- a/core/mercure.md +++ b/core/mercure.md @@ -1,32 +1,41 @@ # 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). +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. +> _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). +API Platform detects changes made to your Doctrine entities, and sends the updated resources to the +Mercure hub. Then, the Mercure hub dispatches the updates to all connected clients using +[Server-sent Events (SSE)](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events). ![Mercure subscriptions](images/mercure-subscriptions.png) ## Installing Mercure Support -Mercure support is already installed, configured and enabled in [the API Platform Symfony variant](../symfony/index.md). -If you use the distribution, you have nothing more to do, and you can skip to the next section. +Mercure support is already installed, configured and enabled in +[the API Platform Symfony variant](../symfony/index.md). If you use the distribution, 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: +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). + - 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: +Use the `mercure` attribute to hint API Platform that it must dispatch the updates regarding the +given resources to the Mercure hub: ```php 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. +> 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}`. +> 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. +> 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) @@ -201,11 +220,10 @@ In this case, the JWT Token for the subscriber should contain: ```json { - "mercure": { - "subscribe": ["https://example.com/users/foo/{?topic}"] - } + "mercure": { + "subscribe": ["https://example.com/users/foo/{?topic}"] + } } ``` -The subscribe topic should be: -`https://example.com/books/{id}` +The subscribe topic should be: `https://example.com/books/{id}` diff --git a/core/mongodb.md b/core/mongodb.md index 76822bdfbf8..f6e48eed9f9 100644 --- a/core/mongodb.md +++ b/core/mongodb.md @@ -2,23 +2,28 @@ ## Overview -[MongoDB](https://www.mongodb.com/) is one of the most popular NoSQL document-oriented database, used for its high -write load (useful for analytics or IoT) and high availability (easy to set replica sets with automatic failover). It -can also shard the database easily for horizontal scalability and has a powerful query language for doing aggregation, -text search or geospatial queries. - -API Platform uses [Doctrine MongoDB ODM 2](https://www.doctrine-project.org/projects/mongodb-odm.html) and in particular -its [aggregation builder](https://www.doctrine-project.org/projects/doctrine-mongodb-odm/en/current/reference/aggregation-builder.html) +[MongoDB](https://www.mongodb.com/) is one of the most popular NoSQL document-oriented database, +used for its high write load (useful for analytics or IoT) and high availability (easy to set +replica sets with automatic failover). It can also shard the database easily for horizontal +scalability and has a powerful query language for doing aggregation, text search or geospatial +queries. + +API Platform uses +[Doctrine MongoDB ODM 2](https://www.doctrine-project.org/projects/mongodb-odm.html) and in +particular its +[aggregation builder](https://www.doctrine-project.org/projects/doctrine-mongodb-odm/en/current/reference/aggregation-builder.html) to leverage all the possibilities of the database. -Doctrine MongoDB ODM 2 relies on the [mongodb](https://secure.php.net/manual/en/set.mongodb.php) PHP extension and not on -the legacy [mongo](https://secure.php.net/manual/en/book.mongo.php) extension. +Doctrine MongoDB ODM 2 relies on the [mongodb](https://secure.php.net/manual/en/set.mongodb.php) PHP +extension and not on the legacy [mongo](https://secure.php.net/manual/en/book.mongo.php) extension. ## Enabling MongoDB Support -If the `mongodb` PHP extension is not installed yet, [install it beforehand](https://secure.php.net/manual/en/mongodb.installation.pecl.php). +If the `mongodb` PHP extension is not installed yet, +[install it beforehand](https://secure.php.net/manual/en/mongodb.installation.pecl.php). -Or if you are using the [API Platform Distribution with Symfony](../symfony/index.md), modify the `Dockerfile` to add the extension: +Or if you are using the [API Platform Distribution with Symfony](../symfony/index.md), modify the +`Dockerfile` to add the extension: ```diff # api/Dockerfile @@ -46,25 +51,28 @@ Add a MongoDB image to the docker-compose file: # compose.yaml services: - # ... - db-mongodb: - # In production, you may want to use a managed database service - image: mongodb/mongodb-community-server:latest - environment: - - MONGODB_INITDB_DATABASE=api - - MONGODB_INITDB_ROOT_USERNAME=api-platform - # You should definitely change the password in production - - MONGODB_INITDB_ROOT_PASSWORD=!ChangeMe! - volumes: - - db-data:/data/db:rw - # You may use a bind-mounted host directory instead, so that it is harder to accidentally remove the volume and lose all your data! - # - ./docker/db/data:/data/db:rw - ports: - - '27017:27017' + # ... + db-mongodb: + # In production, you may want to use a managed database service + image: mongodb/mongodb-community-server:latest + environment: + - MONGODB_INITDB_DATABASE=api + - MONGODB_INITDB_ROOT_USERNAME=api-platform + # You should definitely change the password in production + - MONGODB_INITDB_ROOT_PASSWORD=!ChangeMe! + volumes: + - db-data:/data/db:rw + # You may use a bind-mounted host directory instead, so that it is harder to accidentally remove the volume and lose all your data! + # - ./docker/db/data:/data/db:rw + ports: + - "27017:27017" # ... ``` -In all cases, enable the MongoDB support by requiring the [Doctrine MongoDB ODM bundle](https://github.com/doctrine/DoctrineMongoDBBundle) and [MongoDB ODM for API Platform](https://github.com/api-platform/doctrine-odm/) packages using Composer: +In all cases, enable the MongoDB support by requiring the +[Doctrine MongoDB ODM bundle](https://github.com/doctrine/DoctrineMongoDBBundle) and +[MongoDB ODM for API Platform](https://github.com/api-platform/doctrine-odm/) packages using +Composer: ```console composer require doctrine/mongodb-odm-bundle api-platform/doctrine-odm @@ -171,8 +179,10 @@ class Offer } ``` -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/). +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 @@ -182,11 +192,13 @@ See how to use them and how to create custom ones in the [filters documentation] ## Creating Custom Extensions -See how to create Doctrine MongoDB ODM custom extensions in the [extensions documentation](extensions.md). +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)), +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: diff --git a/core/openapi.md b/core/openapi.md index 48a79825e87..0f829740688 100644 --- a/core/openapi.md +++ b/core/openapi.md @@ -6,12 +6,13 @@ API Platform natively supports the [OpenAPI](https://www.openapis.org/) API spec

OpenAPI screencast
Watch the OpenAPI screencast

-The specification of the API is available at the `/docs.jsonopenapi` path. -By default, OpenAPI v3 is used. -You can also get an OpenAPI v3-compliant version thanks to the `spec_version` query parameter: `/docs.jsonopenapi?spec_version=3` +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. +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 @@ -49,7 +50,8 @@ 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: +You can now decline a same OpenAPI specification in multiple versions using the `x-apiplatform-tags` +tag: ```php use ApiPlatform\OpenApi\Factory\OpenApiFactory; @@ -59,7 +61,8 @@ use ApiPlatform\OpenApi\Factory\OpenApiFactory; class Book {} ``` -Then, either use the query parameter for the web version such as `/docs?filter_tags[]=customer` or through the command line: +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 @@ -69,11 +72,12 @@ 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. +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. +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: @@ -122,7 +126,9 @@ 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: +Symfony allows to +[decorate services](https://symfony.com/doc/current/service_container/service_decoration.html) as +following: @@ -144,22 +150,23 @@ class OpenApiFactory implements OpenApiFactoryInterface ```yaml # api/config/services.yaml services: - # ... - App\OpenApi\OpenApiFactory: - decorates: 'api_platform.openapi.factory' - arguments: ['@App\OpenApi\OpenApiFactory.inner'] - autoconfigure: false + # ... + App\OpenApi\OpenApiFactory: + decorates: "api_platform.openapi.factory" + arguments: ['@App\OpenApi\OpenApiFactory.inner'] + autoconfigure: false ``` ### Decorate with Laravel -Laravel allows to [decorate services](https://laravel.com/docs/container#extending-bindings), as following: +Laravel allows to [decorate services](https://laravel.com/docs/container#extending-bindings), as +following: ```php @@ -377,10 +389,10 @@ class Product # api/config/api_platform/resources.yaml # The YAML syntax is only supported for Symfony resources: - App\Entity\Product: - operations: - ApiPlatform\Metadata\GetCollection: - openapi: false + App\Entity\Product: + operations: + ApiPlatform\Metadata\GetCollection: + openapi: false ``` ```xml @@ -402,12 +414,14 @@ resources:
-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. +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: +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; @@ -510,29 +524,30 @@ class Rabbit ```yaml # The YAML syntax is only supported for Symfony resources: - App\ApiResource\Rabbit: - operations: - create_rabbit: - class: ApiPlatform\Metadata\Post - path: '/rabbit/create' - controller: App\Controller\RandomRabbit - openapi: - summary: Random rabbit picture - description: > - # Pop a great rabbit picture by color! - - ![A great rabbit](https://rabbit.org/graphics/fun/netbunnies/jellybean1-brennan1.jpg) - requestBody: - content: - application/json: - schema: - type: object - properties: - name: { type: string } - description: { type: string } - example: - name: Mr. Rabbit - description: Pink rabbit + App\ApiResource\Rabbit: + operations: + create_rabbit: + class: ApiPlatform\Metadata\Post + path: "/rabbit/create" + controller: App\Controller\RandomRabbit + openapi: + summary: Random rabbit picture + description: > + # Pop a great rabbit picture by color! + + ![A great + rabbit](https://rabbit.org/graphics/fun/netbunnies/jellybean1-brennan1.jpg) + requestBody: + content: + application/json: + schema: + type: object + properties: + name: { type: string } + description: { type: string } + example: + name: Mr. Rabbit + description: Pink rabbit ``` ```xml @@ -602,8 +617,8 @@ To disable Swagger UI (ReDoc will be shown by default): ```yaml # api/config/packages/api_platform.yaml api_platform: - # ... - enable_swagger_ui: false + # ... + enable_swagger_ui: false ``` To disable ReDoc: @@ -611,8 +626,8 @@ To disable ReDoc: ```yaml # api/config/packages/api_platform.yaml api_platform: - # ... - enable_re_doc: false + # ... + enable_re_doc: false ``` ### Disabling Swagger UI or ReDoc with Laravel @@ -641,7 +656,8 @@ return [ ## 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`. +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. @@ -654,8 +670,8 @@ Manually register the Swagger UI controller: ```yaml # app/config/routes.yaml api_doc: - path: /api_documentation - controller: api_platform.action.documentation + path: /api_documentation + controller: api_platform.action.documentation ``` Change `/api_documentation` to the URI you wish Swagger UI to be accessible on. @@ -684,9 +700,9 @@ With Symfony use: ```yaml # api/config/packages/api_platform.yaml api_platform: - # ... - enable_swagger_ui: false - enable_re_doc: false + # ... + enable_swagger_ui: false + enable_re_doc: false ``` Or with Laravel use: @@ -701,15 +717,17 @@ return [ ]; ``` -If you have manually registered the Swagger UI controller, the Swagger UI will still be accessible at the route you have chosen. +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) +> [!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. +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: @@ -717,7 +735,7 @@ Specify a custom asset package name: ```yaml # config/packages/api_platform.yaml api_platform: - asset_package: 'api_platform' + asset_package: "api_platform" ``` Set or override asset properties per package: @@ -725,12 +743,12 @@ 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: '/' + # ... + assets: + base_path: "/custom_base_path" # the default + packages: + api_platform: + base_path: "/" ``` ## Overriding the UI Template @@ -739,7 +757,9 @@ You can extend the default UI Template using the Symfony and Laravel instruction ### 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: +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 #} @@ -752,11 +772,15 @@ As described [in the Symfony documentation](https://symfony.com/doc/current/temp ``` -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. +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: +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 --}} @@ -765,7 +789,7 @@ As described [in the Laravel documentation](https://laravel.com/docs/packages#ov - @if(isset($title)) + @if(isset($title)) {{ $title }} @endif My custom template @@ -774,56 +798,62 @@ As described [in the Laravel documentation](https://laravel.com/docs/packages#ov </html> ``` -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. +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). +[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. +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: +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 + oauth: + # To enable or disable OAuth. + enabled: false - # The OAuth client ID. - clientId: '' + # The OAuth client ID. + clientId: "" - # The OAuth client secret. - clientSecret: '' + # The OAuth client secret. + clientSecret: "" - # The OAuth type. - type: 'oauth2' + # The OAuth type. + type: "oauth2" - # The OAuth flow grant type. - flow: 'application' + # The OAuth flow grant type. + flow: "application" - # The OAuth token url. - tokenUrl: '/oauth/v2/token' + # The OAuth token url. + tokenUrl: "/oauth/v2/token" - # The OAuth authentication url. - authorizationUrl: '/oauth/v2/auth' + # The OAuth authentication url. + authorizationUrl: "/oauth/v2/auth" - # The OAuth scopes. - scopes: [] + # 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: +If you implemented OAuth on your API, you should configure OpenApi's authorization using API +Platform's configuration: ```php <?php @@ -833,7 +863,7 @@ return [ 'oauth' => [ 'enabled' => false, // To enable or disable OAuth. 'clientId' => '', // The OAuth client ID. - 'clientSecret' => '', // The OAuth client secret. + 'clientSecret' => '', // The OAuth client secret. 'type' => 'oauth2', // The OAuth flow grant type. 'authorizationUrl' => '/oauth/v2/auth' // The OAuth authentication url. 'scopes' => [], // The OAuth scopes. @@ -847,25 +877,32 @@ Note that `clientId` and `clientSecret` are being used by the SwaggerUI if enabl #### 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: +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). + 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: +The `api_platform.oauth.scopes` option requires an array value with the scopes name and description. +For example: ```php <?php @@ -883,45 +920,46 @@ return [ ]; ``` -> [!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). +> [!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: +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 +### 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: + # 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 diff --git a/core/operation-path-naming.md b/core/operation-path-naming.md index def62e3426b..60b9bb9073e 100644 --- a/core/operation-path-naming.md +++ b/core/operation-path-naming.md @@ -8,7 +8,7 @@ Pre-registered resolvers are available and can easily be overridden. There are two pre-registered operation path naming services: | Service name | Entity name | Path result | -|----------------------------------------------------------------|--------------|-----------------| +| -------------------------------------------------------------- | ------------ | --------------- | | `api_platform.metadata.path_segment_name_generator.underscore` | `MyResource` | `/my_resources` | | `api_platform.metadata.path_segment_name_generator.dash` | `MyResource` | `/my-resources` | @@ -16,12 +16,13 @@ The default resolver is `api_platform.metadata.path_segment_name_generator.under ### Configuration using Symfony -To change it to the dash resolver, add the following lines to `api/config/packages/api_platform.yaml`: +To change it to the dash resolver, add the following lines to +`api/config/packages/api_platform.yaml`: ```yaml # api/config/packages/api_platform.yaml api_platform: - path_segment_name_generator: api_platform.metadata.path_segment_name_generator.dash + path_segment_name_generator: api_platform.metadata.path_segment_name_generator.dash ``` ### Configuration using Laravel @@ -43,7 +44,8 @@ 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): +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 <?php @@ -75,19 +77,20 @@ class SingularPathSegmentNameGenerator implements PathSegmentNameGeneratorInterf } ``` -Note that `$name` contains a camelCase string, by default the resource class name (e.g. `MyResource`). +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: +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': ~ + # ... + 'App\Operation\SingularPathSegmentNameGenerator': ~ ``` ### Configuring the Service @@ -97,7 +100,7 @@ services: ```yaml # api/config/packages/api_platform.yaml api_platform: - path_segment_name_generator: 'App\Operation\SingularPathSegmentNameGenerator' + path_segment_name_generator: 'App\Operation\SingularPathSegmentNameGenerator' ``` #### Configuring It using Laravel diff --git a/core/operations.md b/core/operations.md index c3e00d0c95e..1224f1e3e52 100644 --- a/core/operations.md +++ b/core/operations.md @@ -1,31 +1,36 @@ # 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. +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. <p align="center" class="symfonycasts"><a href="https://symfonycasts.com/screencast/api-platform/operations?cid=apip"><img src="../symfony/images/symfonycasts-player.png" alt="Operations screencast"><br>Watch the Operations screencast</a></p> -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. +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 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. +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. +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: +When the `ApiPlatform\Metadata\ApiResource` annotation is applied to an entity class, the following +built-in CRUD operations are automatically enabled: Collection operations: @@ -43,39 +48,41 @@ Item operations: | `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] 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] 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`. +> [!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. +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. +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. +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. +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”. +> [!NOTE] In Symfony we use the term “entities”, while the following documentation is mostly for +> Laravel “models”. <code-selector> @@ -103,10 +110,10 @@ class Book ```yaml # api/config/api_platform/resources.yaml resources: - App\Entity\Book: - operations: - ApiPlatform\Metadata\GetCollection: ~ # nothing more to add if we want to keep the default controller - ApiPlatform\Metadata\Get: ~ + App\Entity\Book: + operations: + ApiPlatform\Metadata\GetCollection: ~ # nothing more to add if we want to keep the default controller + ApiPlatform\Metadata\Get: ~ ``` ```xml @@ -156,12 +163,12 @@ class Book ```yaml # api/config/api_platform/resources.yaml resources: - App\Entity\Book: - operations: - ApiPlatform\Metadata\GetCollection: - method: GET - ApiPlatform\Metadata\Get: - method: GET + App\Entity\Book: + operations: + ApiPlatform\Metadata\GetCollection: + method: GET + ApiPlatform\Metadata\Get: + method: GET ``` ```xml @@ -183,21 +190,28 @@ resources: </code-selector> -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. +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. +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. +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. +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`: +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`: <code-selector> @@ -233,21 +247,21 @@ 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' + 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 @@ -286,7 +300,10 @@ resources: </code-selector> -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: +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: <code-selector> @@ -321,16 +338,16 @@ 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}' - controller: ApiPlatform\Action\NotFoundAction - read: false - output: false + App\Entity\Book: + operations: + ApiPlatform\Metadata\Post: + uriTemplate: "/grimoire" + status: 301 + ApiPlatform\Metadata\Get: + uriTemplate: "/grimoire/{id}" + controller: ApiPlatform\Action\NotFoundAction + read: false + output: false ``` ```xml @@ -355,9 +372,10 @@ resources: ## 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: +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: <code-selector> @@ -378,8 +396,8 @@ class Book ```yaml # api/config/api_platform/resources.yaml resources: - App\Entity\Book: - routePrefix: /library + App\Entity\Book: + routePrefix: /library ``` ```xml @@ -398,20 +416,20 @@ resources: ## 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. +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: +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. +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: @@ -441,22 +459,22 @@ class User ```yaml # api/config/api_platform/resources.yaml resources: - App\Entity\User: - - operations: - ApiPlatform\Metadata\GetCollection: ~ - ApiPlatform\Metadata\Get: ~ - - operations: - ApiPlatform\Metadata\GetCollection: - uriTemplate: /companies/{companyId}/users - itemUriTemplate: /companies/{companyId}/users/{id} - # ... - ApiPlatform\Metadata\Post: - uriTemplate: /companies/{companyId}/users - itemUriTemplate: /companies/{companyId}/users/{id} - # ... - ApiPlatform\Metadata\Get: - uriTemplate: /companies/{companyId}/users/{id} - # ... + App\Entity\User: + - operations: + ApiPlatform\Metadata\GetCollection: ~ + ApiPlatform\Metadata\Get: ~ + - operations: + ApiPlatform\Metadata\GetCollection: + uriTemplate: /companies/{companyId}/users + itemUriTemplate: /companies/{companyId}/users/{id} + # ... + ApiPlatform\Metadata\Post: + uriTemplate: /companies/{companyId}/users + itemUriTemplate: /companies/{companyId}/users/{id} + # ... + ApiPlatform\Metadata\Get: + uriTemplate: /companies/{companyId}/users/{id} + # ... ``` ```xml @@ -492,9 +510,9 @@ 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. +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: @@ -539,8 +557,9 @@ class Weather } ``` -We don't save the `Weather` entity in the database, since we want to return the weather in real time when it is queried. -Because we want to get the weather for a known place, it is more reasonable to query it through a subresource of the `Place` entity, so let's do this: +We don't save the `Weather` entity in the database, since we want to return the weather in real time +when it is queried. Because we want to get the weather for a known place, it is more reasonable to +query it through a subresource of the `Place` entity, so let's do this: ```php <?php @@ -572,8 +591,9 @@ class Place // ... ``` -The `GetWeather` controller fetches the weather for the given city and returns an instance of the `Weather` entity. -This implies that API Platform has to know about this entity, so we will need to make it an API resource too: +The `GetWeather` controller fetches the weather for the given city and returns an instance of the +`Weather` entity. This implies that API Platform has to know about this entity, so we will need to +make it an API resource too: ```php <?php @@ -592,13 +612,16 @@ That's it! ## Customize Operation and Resource Metadata -Metadata mutators allow a dynamic control over resources and operations, by programmatically altering metadata before they are exposed as endpoints. Providing a way to modify, add or remove operations, adjust serialization groups or pagination settings. +Metadata mutators allow a dynamic control over resources and operations, by programmatically +altering metadata before they are exposed as endpoints. Providing a way to modify, add or remove +operations, adjust serialization groups or pagination settings. It also makes it possible to customize built-in endpoints from a third-party API, such as Sylius. ### Resource Mutator -Use the resource mutator to modify the entire resource metadata by adding the attribute and target resource class as argument: +Use the resource mutator to modify the entire resource metadata by adding the attribute and target +resource class as argument: ```php <?php @@ -623,7 +646,7 @@ final readonly class ApiPrefixMutator implements ResourceMutatorInterface public function __invoke(ApiResource $resource): ApiResource { $operations = $resource->getOperations(); - + if (null !== $operations) { return $resource; } @@ -641,7 +664,8 @@ final readonly class ApiPrefixMutator implements ResourceMutatorInterface ### Operation Mutator -The operation mutator will modify a specific operation's metadata, by using the attribute and passing the operation name: +The operation mutator will modify a specific operation's metadata, by using the attribute and +passing the operation name: ```php <?php @@ -660,13 +684,13 @@ final class BookOperationMutator implements OperationMutatorInterface $context = $operation->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. +> [!NOTE] Operation mutators are executed during metadata loading, the result is stored in cache so +> runtime logic is prohibited. --- diff --git a/core/pagination.md b/core/pagination.md index a863d683074..65fed93ca38 100644 --- a/core/pagination.md +++ b/core/pagination.md @@ -2,44 +2,45 @@ <p align="center" class="symfonycasts"><a href="https://symfonycasts.com/screencast/api-platform/pagination?cid=apip"><img src="../symfony/images/symfonycasts-player.png" alt="Pagination screencast"><br>Watch the Pagination screencast</a></p> -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: +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. +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..." + "@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" } - ], - "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. +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: @@ -48,9 +49,9 @@ The name of the page parameter can be changed with the following configuration: ```yaml # api/config/packages/api_platform.yaml api_platform: - collection: - pagination: - page_parameter_name: _page + collection: + pagination: + page_parameter_name: _page ``` ## Changing page parameter name with Laravel @@ -70,9 +71,10 @@ return [ ## 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. +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 @@ -83,8 +85,8 @@ The pagination can be disabled for all resources using this configuration: ```yaml # api/config/packages/api_platform.yaml api_platform: - defaults: - pagination_enabled: false + defaults: + pagination_enabled: false ``` #### Disabling the Pagination Globally with Laravel @@ -126,8 +128,8 @@ class Book # api/config/api_platform/resources.yaml # The YAML syntax is only supported for Symfony resources: - App\ApiResource\Book: - paginationEnabled: false + App\ApiResource\Book: + paginationEnabled: false ``` </code-selector> @@ -163,10 +165,10 @@ class Book # api/config/api_platform/resources.yaml # The YAML syntax is only supported for Symfony resources: - App\ApiResource\Book: - operations: - ApiPlatform\Metadata\GetCollection: - paginationEnabled: false + App\ApiResource\Book: + operations: + ApiPlatform\Metadata\GetCollection: + paginationEnabled: false ``` ```xml @@ -202,11 +204,11 @@ To configure this feature globally, use the following configuration: ```yaml # api/config/packages/api_platform.yaml api_platform: - defaults: - pagination_client_enabled: false - collection: - pagination: - enabled_parameter_name: pagination # optional + defaults: + pagination_client_enabled: false + collection: + pagination: + enabled_parameter_name: pagination # optional ``` The pagination can now be enabled or disabled by adding a query parameter named `pagination`: @@ -214,7 +216,8 @@ The pagination can now be enabled or disabled by adding a query parameter named - `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 +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. ##### Disabling the Pagination Client-side Globally with Laravel @@ -242,7 +245,8 @@ The pagination can now be enabled or disabled by adding a query parameter named - `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 +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. #### Disabling the Pagination Client-side For a Specific Resource @@ -276,8 +280,8 @@ The number of items per page can be configured for all resources. ```yaml # api/config/packages/api_platform.yaml api_platform: - defaults: - pagination_items_per_page: 30 # Default value + defaults: + pagination_items_per_page: 30 # Default value ``` #### Changing the Number of Items per Page Globally with Laravel @@ -318,14 +322,15 @@ class Book ```yaml # api/config/packages/api_platform.yaml api_platform: - defaults: - pagination_client_items_per_page: true - collection: - pagination: - items_per_page_parameter_name: itemsPerPage # Default value + defaults: + pagination_client_items_per_page: true + collection: + pagination: + items_per_page_parameter_name: itemsPerPage # Default value ``` -The number of items per page can now be changed adding a query parameter named `itemsPerPage`: `GET /books?itemsPerPage=20`. +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 Globally using Laravel @@ -345,7 +350,8 @@ return [ ]; ``` -The number of items per page can now be changed adding a query parameter named `itemsPerPage`: `GET /books?itemsPerPage=20`. +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 @@ -376,8 +382,8 @@ The number of maximum items per page can be configured for all resources. ```yaml # api/config/packages/api_platform.yaml api_platform: - defaults: - pagination_maximum_items_per_page: 50 + defaults: + pagination_maximum_items_per_page: 50 ``` #### Changing Maximum Items Per Page Globally using Laravel @@ -429,8 +435,9 @@ class Book ## Partial Pagination -When using the default pagination, a `COUNT` query will be issued against the current requested collection. This may have a -performance impact on huge collections. The downside is that the information about the last page is lost (ie: `last`). +When using the default pagination, a `COUNT` query will be issued against the current requested +collection. This may have a performance impact on huge collections. The downside is that the +information about the last page is lost (ie: `last`). ### Partial Pagination Globally @@ -442,8 +449,8 @@ The partial pagination retrieval can be configured for all resources. # api/config/packages/api_platform.yaml api_platform: - defaults: - pagination_partial: true # Disabled by default + defaults: + pagination_partial: true # Disabled by default ``` #### Partial Pagination Globally using Laravel @@ -485,14 +492,15 @@ class Book # api/config/packages/api_platform.yaml api_platform: - defaults: - pagination_client_partial: true # Disabled by default - collection: - pagination: - partial_parameter_name: 'partial' # Default value + defaults: + 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`. +The partial pagination retrieval can now be changed by toggling a query parameter named `partial`: +`GET /books?partial=true`. ##### Partial Pagination Client-side Globally using Laravel @@ -512,7 +520,8 @@ return [ ]; ``` -The partial pagination retrieval can now be changed by toggling a query parameter named `partial`: `GET /books?partial=true`. +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 @@ -532,9 +541,10 @@ class Book ## Cursor-Based Pagination -To configure your resource to use the cursor-based pagination, select your unique sorted field as well as the direction -you’ll like the pagination to go via filters and enable the `paginationViaCursor` option. -Note that for now you have to declare a `RangeFilter` and an `OrderFilter` on the property used for the cursor-based pagination. +To configure your resource to use the cursor-based pagination, select your unique sorted field as +well as the direction you’ll like the pagination to go via filters and enable the +`paginationViaCursor` option. Note that for now you have to declare a `RangeFilter` and an +`OrderFilter` on the property used for the cursor-based pagination. The following configuration also works on a specific operation: @@ -562,28 +572,33 @@ class Book } ``` -To know more about cursor-based pagination take a look at [this blog post on medium (draft)](https://medium.com/@sroze/74fd1d324723). +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: +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: +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. +- `$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: +You can configure this using the `paginationFetchJoinCollection` attribute on a resource or on a +per-operation basis: ```php <?php @@ -602,9 +617,11 @@ class Book } ``` -- `setUseOutputWalkers` setter: Whether to use output walkers. When set to `true`, the Doctrine ORM Paginator will use output walkers, which are compulsory for some types of queries. +- `setUseOutputWalkers` setter: Whether to use output walkers. When set to `true`, the Doctrine ORM + Paginator will use output walkers, which are compulsory for some types of queries. -You can configure this using the `paginationUseOutputWalkers` attribute on a resource or on a per-operation basis: +You can configure this using the `paginationUseOutputWalkers` attribute on a resource or on a +per-operation basis: ```php <?php @@ -623,12 +640,17 @@ class Book } ``` -For more information, please see the [Pagination](https://www.doctrine-project.org/projects/doctrine-orm/en/current/tutorials/pagination.html) entry in the Doctrine ORM documentation. +For more information, please see the +[Pagination](https://www.doctrine-project.org/projects/doctrine-orm/en/current/tutorials/pagination.html) +entry in the Doctrine ORM documentation. ## Custom Symfony Controller Action -In case you're using a custom controller action, make sure you return the `Paginator` object to get the full Hydra response with `view` (which contains information about first, last, next and previous page). The following examples show how to handle it within a repository method. -The controller needs to pass through the page number. You will need to use the Doctrine Paginator and pass it to the API Platform Paginator. +In case you're using a custom controller action, make sure you return the `Paginator` object to get +the full Hydra response with `view` (which contains information about first, last, next and previous +page). The following examples show how to handle it within a repository method. The controller needs +to pass through the page number. You will need to use the Doctrine Paginator and pass it to the API +Platform Paginator. First example: @@ -710,8 +732,9 @@ class GetBooksByFavoriteAuthorAction extends AbstractController } ``` -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: +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 <?php diff --git a/core/performance.md b/core/performance.md index bfacb28b524..2b40a4933d4 100644 --- a/core/performance.md +++ b/core/performance.md @@ -2,31 +2,38 @@ ## 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 +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. +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 API Platform distribution 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 API Platform distribution 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 +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 @@ -67,20 +74,24 @@ Update your Caddyfile with the following configuration: # ... ``` -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. +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: +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 + 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 @@ -131,15 +142,15 @@ 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'] + 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 @@ -169,29 +180,32 @@ return [ ## 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: +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: ', ' + 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 @@ -224,13 +238,13 @@ return [ ]; ``` -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). +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: +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 <?php @@ -316,40 +330,48 @@ 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 distribution), it automatically enables support for the best cache adapter available. +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 distribution), +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 distribution). -API Platform will automatically use it. +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 distribution). 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 distribution. +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 distribution. ## 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. +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) +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). +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: +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 ... @@ -360,54 +382,57 @@ public $foo; ... ``` -> **Warning**: to trigger the `EagerLoadingExtension` you must use [Serializer groups](serialization.md) on relations properties. +> **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: +`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 + 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. +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: +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 + 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. +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: +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 + 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: +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 <?php @@ -479,40 +504,44 @@ class Group } ``` -Be careful, the operation level has a higher priority than the resource level but both are higher priority than the global -configuration. +Be careful, the operation level has a higher priority than the resource level but both are higher +priority than the global configuration. #### Disable Eager Loading -If for any reason you don't want the eager loading feature, you can turn it off in the configuration: +If for any reason you don't want the eager loading feature, you can turn it off in the +configuration: ```yaml # api/config/packages/api_platform.yaml api_platform: - eager_loading: - enabled: false + eager_loading: + enabled: false ``` -The whole configuration described before will no longer work and Doctrine will recover its default behavior. +The whole configuration described before will no longer work and Doctrine will recover its default +behavior. ### Partial Pagination -When using the default pagination, the Doctrine paginator will execute a `COUNT` query on the collection. The result of the -`COUNT` query is used to compute the last page available. With big collections, this can lead to quite long response times. -If you don't mind not having the last page available, you can enable partial pagination and avoid the `COUNT` query: +When using the default pagination, the Doctrine paginator will execute a `COUNT` query on the +collection. The result of the `COUNT` query is used to compute the last page available. With big +collections, this can lead to quite long response times. If you don't mind not having the last page +available, you can enable partial pagination and avoid the `COUNT` query: ```yaml # api/config/packages/api_platform.yaml api_platform: - defaults: - pagination_partial: true # Disabled by default + defaults: + pagination_partial: true # Disabled by default ``` More details are available on the [pagination documentation](pagination.md#partial-pagination). ## 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/). +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: @@ -520,17 +549,18 @@ To configure Blackfire.io follow these steps: ```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 + # ... + 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): +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 @@ -544,7 +574,8 @@ 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`: +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' : '');") \ @@ -564,4 +595,5 @@ 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). +For details on how to perform profiling, see +[the Blackfire.io documentation](https://blackfire.io/docs/integrations/docker#using-the-client-for-http-profiling). diff --git a/core/push-relations.md b/core/push-relations.md index eb7826185ee..d91a8a1a606 100644 --- a/core/push-relations.md +++ b/core/push-relations.md @@ -1,16 +1,17 @@ # 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. +> 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 distribution](../symfony/index.md). +> [!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 distribution](../symfony/index.md). ```php <?php @@ -30,16 +31,23 @@ class Book } ``` -By setting the `push` attribute to `true` on a property holding a relation, API Platform will automatically add a valid -`Link` HTTP header with the `preload` relation. According to the [Preload W3C Candidate Recommendation](https://www.w3.org/TR/preload/#server-push-http-2), -web servers and proxy servers can read this header, fetch the related resource and send it to the client using Server Push. - -With the Caddy web server (the server shipped as part of the [distribution](../symfony/index.md)), you must add -[the `push` directive](https://caddyserver.com/docs/caddyfile/directives/push) to your `Caddyfile` to be able to use this feature. - -[NGINX](https://www.nginx.com/blog/nginx-1-13-9-http2-server-push/), [Apache](https://httpd.apache.org/docs/current/howto/http2.html#push), -[Cloudflare](https://www.cloudflare.com/website-optimization/http2/serverpush/), [Fastly](https://docs.fastly.com/guides/performance-tuning/http2-server-push) -and [Akamai](https://blogs.akamai.com/2017/03/http2-server-push-the-what-how-and-why.html) honor this header. - -Using this feature maximises HTTP cache hits for your API resources. -For best performance, this feature should be used in conjunction with [the built-in HTTP cache invalidation system (based on Varnish)](performance.md#enabling-the-built-in-http-cache-invalidation-system). +By setting the `push` attribute to `true` on a property holding a relation, API Platform will +automatically add a valid `Link` HTTP header with the `preload` relation. According to the +[Preload W3C Candidate Recommendation](https://www.w3.org/TR/preload/#server-push-http-2), web +servers and proxy servers can read this header, fetch the related resource and send it to the client +using Server Push. + +With the Caddy web server (the server shipped as part of the [distribution](../symfony/index.md)), +you must add [the `push` directive](https://caddyserver.com/docs/caddyfile/directives/push) to your +`Caddyfile` to be able to use this feature. + +[NGINX](https://www.nginx.com/blog/nginx-1-13-9-http2-server-push/), +[Apache](https://httpd.apache.org/docs/current/howto/http2.html#push), +[Cloudflare](https://www.cloudflare.com/website-optimization/http2/serverpush/), +[Fastly](https://docs.fastly.com/guides/performance-tuning/http2-server-push) and +[Akamai](https://blogs.akamai.com/2017/03/http2-server-push-the-what-how-and-why.html) honor this +header. + +Using this feature maximises HTTP cache hits for your API resources. For best performance, this +feature should be used in conjunction with +[the built-in HTTP cache invalidation system (based on Varnish)](performance.md#enabling-the-built-in-http-cache-invalidation-system). diff --git a/core/security.md b/core/security.md index ed9eef1b7da..502ddd7e57c 100644 --- a/core/security.md +++ b/core/security.md @@ -2,11 +2,12 @@ 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 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. +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). diff --git a/core/serialization.md b/core/serialization.md index deb79b0a1d0..82f21619a1f 100644 --- a/core/serialization.md +++ b/core/serialization.md @@ -1,10 +1,12 @@ # The Serialization Process -For documentation on how to expose PHP 8.1+ Enums as API resources, refer to the [Enums documentation](enums.md). +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. +API Platform embraces and extends the Symfony Serializer Component to transform PHP entities in +(hypermedia) API responses. <p align="center" class="symfonycasts"><a href="https://symfonycasts.com/screencast/api-platform/serializer?cid=apip"><img src="../symfony/images/symfonycasts-player.png" alt="Serializer screencast"><br>Watch the Serializer screencast</a></p> @@ -12,22 +14,25 @@ The main serialization process has two stages: ![Serializer workflow](images/SerializerWorkflow.png) -> As you can see in the picture above, an array is used as a man-in-the-middle. This way, Encoders will only deal with turning specific formats into arrays and vice versa. The same way, Normalizers will deal with turning specific objects into arrays and vice versa. -> -- [The Symfony documentation](https://symfony.com/doc/current/components/serializer.html) +> 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. +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. +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](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. +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` @@ -39,21 +44,26 @@ JSON-LD, or JavaScript Object Notation for Linked Data, is a method of encoding <p align="center" class="symfonycasts"><a href="https://symfonycasts.com/screencast/api-platform/serialization-groups?cid=apip"><img src="../symfony/images/symfonycasts-player.png" alt="Serialization Groups screencast"><br>Watch the Serialization Groups screencast</a></p> -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) +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) +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. +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 distribution, you need to enable annotation support in the serializer -> configuration as outlined below, depending on your Symfony version. +> [!NOTE] If you are not using the API Platform distribution, you need to enable annotation support +> in the serializer configuration as outlined below, depending on your Symfony version. #### Configuration for Symfony `<= 6.4` @@ -64,12 +74,13 @@ Add the following configuration to your `framework.yaml` file: ```yaml # api/config/packages/framework.yaml framework: - serializer: { enable_annotations: true } + 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: +If you use [Symfony Flex](https://github.com/symfony/flex) and Symfony `<= 6.4`, simply run the +following command: ```console composer req doctrine/annotations @@ -79,7 +90,8 @@ 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). +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: @@ -93,21 +105,22 @@ framework: #### Additional Syntax Configuration for All Versions -If you want to use YAML or XML for serialization, add the mapping path to the serializer configuration: +If you want to use YAML or XML for serialization, add the mapping path to the serializer +configuration: <code-selector> ```yaml # api/config/packages/framework.yaml framework: - serializer: - mapping: - paths: ['%kernel.project_dir%/config/serialization'] + serializer: + mapping: + paths: ["%kernel.project_dir%/config/serialization"] ``` ```xml <!-- api/config/packages/framework.xml --> -<framework> +<framework> <!-- ... --> <serializer> <mapping> @@ -124,8 +137,9 @@ framework: 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. +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. <code-selector> @@ -158,20 +172,20 @@ class Book # The YAML syntax is only supported for Symfony # api/config/api_platform/resources.yaml resources: - App\ApiResource\Book: - normalizationContext: - groups: ['read'] - denormalizationContext: - groups: ['write'] + 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'] + attributes: + name: + groups: ["read", "write"] + author: + groups: ["write"] ``` ```xml @@ -203,7 +217,7 @@ App\ApiResource\Book: </denormalizationContext> </resource> </resources> - + <!--The XML syntax is only supported for Symfony --> <!-- api/config/serialization/Book.xml --> <?xml version="1.0" encoding="UTF-8" ?> @@ -225,33 +239,40 @@ App\ApiResource\Book: </code-selector> -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. +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). +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). +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)). +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. +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 <p class="symfonycasts" align="center"><a href="https://symfonycasts.com/screencast/api-platform/relations?cid=apip"><img src="../symfony/images/symfonycasts-player.png" alt="Relations screencast"><br>Watch the Relations screencast</a></p> -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. +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. +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: @@ -361,9 +382,10 @@ App\ApiResource\Book: </code-selector> -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. +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. @@ -371,29 +393,36 @@ Refer to the [operations](operations.md) documentation to learn more. <p align="center" class="symfonycasts"><a href="https://symfonycasts.com/screencast/api-platform/relations?cid=apip"><img src="../symfony/images/symfonycasts-player.png" alt="Relations screencast"><br>Watch the Relations screencast</a></p> -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. +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 distribution. +**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 distribution. ### Normalization -In the following JSON document, the relation from a book to an author is by default represented by an URI: +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" + "@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. +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. <code-selector> @@ -461,9 +490,9 @@ class Person # The YAML syntax is only supported for Symfony # api/config/serializer/Person.yaml App\ApiResource\Person: - attributes: - name: - groups: ['book'] + attributes: + name: + groups: ["book"] ``` </code-selector> @@ -472,28 +501,30 @@ 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" - } + "@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). +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). +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: +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: <code-selector> @@ -515,17 +546,19 @@ class Book # The YAML syntax is only supported for Symfony # api/config/api_platform/resources/Book.yaml App\ApiResource\Book: - denormalizationContext: - groups: ['book'] + denormalizationContext: + groups: ["book"] ``` </code-selector> 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. +- 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. @@ -580,9 +613,11 @@ App\ApiResource\Person: </code-selector> -The problem here is that the **$parent** property become automatically an embedded object. Besides, the property won't be shown on the OpenAPI view. +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: +To force the **$parent** property to be used as an IRI, add an +`#[ApiProperty(readableLink: false, writableLink: false)]` annotation: <code-selector> @@ -617,32 +652,33 @@ class Person # 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'] + App\ApiResource\Person: + normalizationContext: + groups: ["person"] + denormalizationContext: + groups: ["person"] properties: - App\ApiResource\Person: - parent: - readableLink: false - writableLink: false + 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'] + attributes: + name: + groups: ["person"] + parent: + groups: ["person"] ``` </code-selector> ### 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: +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 <?php @@ -693,8 +729,9 @@ class PlainIdentifierDenormalizer implements DenormalizerInterface, Denormalizer ## 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. +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: @@ -723,10 +760,10 @@ 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" + "@context": "/contexts/Book", + "@id": "/books/3", + "@type": "https://schema.org/Book", + "publicationDate": "1989-06-16" } ``` @@ -784,7 +821,8 @@ class Book ## 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. +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. <code-selector> @@ -854,7 +892,8 @@ App\Entity\Greeting: <p align="center" class="symfonycasts"><a href="https://symfonycasts.com/screencast/api-platform-security/service-decoration?cid=apip"><img src="../symfony/images/symfonycasts-player.png" alt="Context Builder & Service Decoration screencast"><br>Watch the Context Builder & Service Decoration screencast</a></p> -Let's imagine a resource where most fields can be managed by any user, but some can be managed only by admin users: +Let's imagine a resource where most fields can be managed by any user, but some can be managed only +by admin users: <code-selector> @@ -911,22 +950,24 @@ App\ApiResource\Book: </code-selector> -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. +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: +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 + # ... + 'App\Serializer\BookContextBuilder': + decorates: "api_platform.serializer.context_builder" + arguments: ['@App\Serializer\BookContextBuilder.inner'] + autoconfigure: false ``` ```php @@ -964,31 +1005,34 @@ final class BookContextBuilder implements SerializerContextBuilderInterface } ``` -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`). +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. +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): +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 } + '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. +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: @@ -1052,53 +1096,60 @@ class BookAttributeNormalizer implements NormalizerInterface, NormalizerAwareInt } ``` -This will add the serialization group `can_retrieve_book` only if the currently logged-in user has access to the given book -instance. +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). +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). +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: +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': ~ + 'Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter': ~ ``` ```yaml # api/config/packages/api_platform.yaml api_platform: - name_converter: 'Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter' + 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. +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`: +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' + '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: +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' +"app.serializer.normalizer.item.json": + class: 'App\Serializer\ApiNormalizer' + decorates: "api_platform.serializer.normalizer.item" ``` ```php @@ -1166,15 +1217,18 @@ final class ApiNormalizer implements NormalizerInterface, DenormalizerInterface, ## 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), +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) +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: +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: <code-selector> @@ -1212,9 +1266,9 @@ class Book # The YAML syntax is only supported for Symfony # api/config/api_platform/properties.yaml properties: - App\ApiResource\Book: - id: - identifier: true + App\ApiResource\Book: + id: + identifier: true ``` ```xml @@ -1232,32 +1286,36 @@ properties: </code-selector> -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: +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) +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: +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" + "@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: +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: <code-selector> @@ -1279,8 +1337,8 @@ class Book # The YAML syntax is only supported for Symfony # api/config/api_platform/resources/Book.yaml App\ApiResource\Book: - normalizationContext: - jsonldEmbedContext: true + normalizationContext: + jsonldEmbedContext: true ``` </code-selector> @@ -1289,23 +1347,28 @@ 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" + "@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. +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 <?php diff --git a/core/state-processors.md b/core/state-processors.md index c43b2b89e14..948335a0024 100644 --- a/core/state-processors.md +++ b/core/state-processors.md @@ -1,38 +1,49 @@ # State Processors -To mutate the application states during `POST`, `PUT`, `PATCH` or `DELETE` [operations](operations.md), API Platform uses -classes called **state processors**. State processors receive an instance of the class marked as an API resource (usually using -the `#[ApiResource]` attribute). This instance contains data submitted by the client during [the deserialization -process](serialization.md). - -With the Symfony variant, a state processor using [Doctrine ORM](https://www.doctrine-project.org/projects/orm.html) is included with the library and -is enabled by default. It is able to persist and delete objects that are also mapped as [Doctrine entities](https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/basic-mapping.html). -A [Doctrine MongoDB ODM](https://www.doctrine-project.org/projects/mongodb-odm.html) state processor is also included and can be enabled by following the [MongoDB documentation](mongodb.md). - -With the Laravel variant, a state processor using [Eloquent ORM](https://laravel.com/docs/eloquent) is included with the library and -is enabled by default. It is able to persist and delete objects that are also mapped as [Related Models](https://laravel.com/docs/eloquent-relationships#inserting-and-updating-related-models). +To mutate the application states during `POST`, `PUT`, `PATCH` or `DELETE` +[operations](operations.md), API Platform uses classes called **state processors**. State processors +receive an instance of the class marked as an API resource (usually using the `#[ApiResource]` +attribute). This instance contains data submitted by the client during +[the deserialization process](serialization.md). + +With the Symfony variant, a state processor using +[Doctrine ORM](https://www.doctrine-project.org/projects/orm.html) is included with the library and +is enabled by default. It is able to persist and delete objects that are also mapped as +[Doctrine entities](https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/basic-mapping.html). +A [Doctrine MongoDB ODM](https://www.doctrine-project.org/projects/mongodb-odm.html) state processor +is also included and can be enabled by following the [MongoDB documentation](mongodb.md). + +With the Laravel variant, a state processor using [Eloquent ORM](https://laravel.com/docs/eloquent) +is included with the library and is enabled by default. It is able to persist and delete objects +that are also mapped as +[Related Models](https://laravel.com/docs/eloquent-relationships#inserting-and-updating-related-models). However, you may want to: - store data to other persistence layers (Elasticsearch, external web services...) - not publicly expose the internal model mapped with the database through the API -- use a separate model for [read operations](state-providers.md) and for updates by implementing patterns such as [CQRS](https://martinfowler.com/bliki/CQRS.html) +- use a separate model for [read operations](state-providers.md) and for updates by implementing + patterns such as [CQRS](https://martinfowler.com/bliki/CQRS.html) -Custom state processors can be used to do so. A project can include as many state processors as needed. The first able to -process the data for a given resource will be used. +Custom state processors can be used to do so. A project can include as many state processors as +needed. The first able to process the data for a given resource will be used. ## Creating a Custom State Processor ### Custom State Processor with Symfony -If the [Symfony MakerBundle](https://symfony.com/doc/current/bundles/SymfonyMakerBundle) is installed in your project, you can use the following command to generate a custom state processor easily: +If the [Symfony MakerBundle](https://symfony.com/doc/current/bundles/SymfonyMakerBundle) is +installed in your project, you can use the following command to generate a custom state processor +easily: ```console bin/console make:state-processor ``` -To create a state processor, you have to implement the [`ProcessorInterface`](https://github.com/api-platform/core/blob/main/src/State/ProcessorInterface.php). -This interface defines a method `process`: to create, delete, update, or alter the given data in any ways. +To create a state processor, you have to implement the +[`ProcessorInterface`](https://github.com/api-platform/core/blob/main/src/State/ProcessorInterface.php). +This interface defines a method `process`: to create, delete, update, or alter the given data in any +ways. Here is an implementation example: @@ -62,8 +73,10 @@ final class BlogPostProcessor implements ProcessorInterface } ``` -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. +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: @@ -82,14 +95,17 @@ class BlogPost {} ### Custom State Processor with Laravel -Using [Laravel Artisan Console](https://laravel.com/docs/artisan), you can generate a custom state processor easily with the following command: +Using [Laravel Artisan Console](https://laravel.com/docs/artisan), you can generate a custom state +processor easily with the following command: ```console php artisan make:state-processor ``` -To create a state processor, you have to implement the [`ProcessorInterface`](https://github.com/api-platform/core/blob/main/src/State/ProcessorInterface.php). -This interface defines a method `process`: to create, delete, update, or alter the given data in any ways. +To create a state processor, you have to implement the +[`ProcessorInterface`](https://github.com/api-platform/core/blob/main/src/State/ProcessorInterface.php). +This interface defines a method `process`: to create, delete, update, or alter the given data in any +ways. Here is an implementation example: @@ -119,8 +135,10 @@ final class BlogPostProcessor implements ProcessorInterface } ``` -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. +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: @@ -141,9 +159,13 @@ class BlogPost {} ### 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). +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: +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 <?php @@ -196,9 +218,11 @@ final class UserProcessor implements ProcessorInterface } ``` -The `Autowire` attribute is used to inject the built-in processor services registered by API Platform. +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. +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: @@ -217,9 +241,12 @@ class User {} ### Laravel 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). +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 [Laravel Mail](https://laravel.com/docs/mail) to send new users a welcome email after a REST `POST` or GraphQL `create` operation, in a project using the native Eloquent ORM state processor: +Here is an implementation example which uses [Laravel Mail](https://laravel.com/docs/mail) to send +new users a welcome email after a REST `POST` or GraphQL `create` operation, in a project using the +native Eloquent ORM state processor: ```php <?php @@ -254,7 +281,7 @@ final class UserProcessor implements ProcessorInterface if ($operation instanceof DeleteOperationInterface) { return $this->removeProcessor->process($data, $operation, $uriVariables, $context); } - + $result = $this->persistProcessor->process($data, $operation, $uriVariables, $context); $this->sendWelcomeEmail($data); @@ -269,7 +296,11 @@ final class UserProcessor implements ProcessorInterface } ``` -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: +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 <?php @@ -321,8 +352,9 @@ class User {} ## Registering Services Without Autowiring (only for the Symfony variant) -The previous examples work because service autowiring and autoconfiguration are enabled by default in Symfony and API Platform. -If you disabled this feature, you need to register the services by yourself and add the `api_platform.state_processor` tag. +The previous examples work because service autowiring and autoconfiguration are enabled by default +in Symfony and API Platform. If you disabled this feature, you need to register the services by +yourself and add the `api_platform.state_processor` tag. ```yaml # api/config/services.yaml @@ -330,15 +362,15 @@ If you disabled this feature, you need to register the services by yourself and services: # ... App\State\BlogPostProcessor: - tags: [ 'api_platform.state_processor' ] + tags: ["api_platform.state_processor"] App\State\UserProcessor: arguments: - $persistProcessor: '@api_platform.doctrine.orm.state.persist_processor' - $removeProcessor: '@api_platform.doctrine.orm.state.remove_processor' + $persistProcessor: "@api_platform.doctrine.orm.state.persist_processor" + $removeProcessor: "@api_platform.doctrine.orm.state.remove_processor" # If you're using Doctrine MongoDB ODM, you can use the following code: # $persistProcessor: '@api_platform.doctrine_mongodb.odm.state.persist_processor' # $removeProcessor: '@api_platform.doctrine_mongodb.odm.state.remove_processor' - $mailer: '@mailer' - tags: [ 'api_platform.state_processor' ] + $mailer: "@mailer" + tags: ["api_platform.state_processor"] ``` diff --git a/core/state-providers.md b/core/state-providers.md index ced1e0ff588..e653875d775 100644 --- a/core/state-providers.md +++ b/core/state-providers.md @@ -2,37 +2,44 @@ To retrieve data exposed by the API, API Platform uses classes called **state providers**. -With the Symfony variant, a state provider using [Doctrine -ORM](https://www.doctrine-project.org/projects/orm.html) is ready to retrieve data from a database and a state provider using -[Doctrine MongoDB ODM](https://www.doctrine-project.org/projects/mongodb-odm.html) to retrieve data from a document -database. +With the Symfony variant, a state provider using +[Doctrine ORM](https://www.doctrine-project.org/projects/orm.html) is ready to retrieve data from a +database and a state provider using +[Doctrine MongoDB ODM](https://www.doctrine-project.org/projects/mongodb-odm.html) to retrieve data +from a document database. -With the Laravel variant, a state provider using [Eloquent ORM](https://laravel.com/docs/eloquent) to retrieve data from a relational database and a state provider. +With the Laravel variant, a state provider using [Eloquent ORM](https://laravel.com/docs/eloquent) +to retrieve data from a relational database and a state provider. -The ORM providers are enabled by default, based on your framework variant (Eloquent or Doctrine will be set up). +The ORM providers are enabled by default, based on your framework variant (Eloquent or Doctrine will +be set up). -These state providers natively support paged collections and filters. They can be used as-is and are perfectly suited to common uses. +These state providers natively support paged collections and filters. They can be used as-is and are +perfectly suited to common uses. -However, you sometimes want to retrieve data from other sources such as another persistence layer or a webservice. -Custom state providers can be used to do so. A project can include as many state providers as needed. The first able to -retrieve data for a given resource will be used. +However, you sometimes want to retrieve data from other sources such as another persistence layer or +a webservice. Custom state providers can be used to do so. A project can include as many state +providers as needed. The first able to retrieve data for a given resource will be used. To do so you need to implement the `ApiPlatform\State\ProviderInterface`. -In the following examples we will create custom state providers for Symfony entities and Laravel models: +In the following examples we will create custom state providers for Symfony entities and Laravel +models: - For Symfony we will create an entity class called `App\Entity\BlogPost`. - For Laravel, we will create a model class called `App\Models\BlogPost`. -Note, that if your entity is not Doctrine-related or Eloquent-related, you need to flag the identifier property by using -`#[ApiProperty(identifier: true)` for things to work properly (see also [Entity Identifier Case](serialization.md#entity-identifier-case)). +Note, that if your entity is not Doctrine-related or Eloquent-related, you need to flag the +identifier property by using `#[ApiProperty(identifier: true)` for things to work properly (see also +[Entity Identifier Case](serialization.md#entity-identifier-case)). ## Creating a Custom State Provider ### Custom State Provider with Symfony -If the [Symfony MakerBundle](https://symfony.com/doc/current/bundles/SymfonyMakerBundle) is installed in your project, -you can use the following command to generate a custom state provider easily: +If the [Symfony MakerBundle](https://symfony.com/doc/current/bundles/SymfonyMakerBundle) is +installed in your project, you can use the following command to generate a custom state provider +easily: ```console bin/console make:state-provider @@ -65,7 +72,7 @@ final class BlogPostProvider implements ProviderInterface 'ab' => new BlogPost('ab'), 'cd' => new BlogPost('cd'), ]; - } + } public function provide(Operation $operation, array $uriVariables = [], array $context = []): BlogPost|null { @@ -76,7 +83,10 @@ final class BlogPostProvider implements ProviderInterface 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`. +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. @@ -95,8 +105,9 @@ use App\State\BlogPostProvider; class BlogPost {} ``` -Now let's say that we also want to handle the `/blog_posts` URI which returns a collection. We can change the Provider into -supporting a wider range of operations. Then we can provide a collection of blog posts when the operation is a `CollectionOperationInterface`: +Now let's say that we also want to handle the `/blog_posts` URI which returns a collection. We can +change the Provider into supporting a wider range of operations. Then we can provide a collection of +blog posts when the operation is a `CollectionOperationInterface`: ```php <?php @@ -127,7 +138,8 @@ final class BlogPostProvider implements ProviderInterface } ``` -We then need to configure this same provider on the BlogPost `GetCollection` operation, or for every operation via the `ApiResource` attribute: +We then need to configure this same provider on the BlogPost `GetCollection` operation, or for every +operation via the `ApiResource` attribute: ```php <?php @@ -144,7 +156,8 @@ class BlogPost {} #### Custom State Provider with Laravel -Using [Laravel Artisan Console](https://laravel.com/docs/artisan), you can generate a custom state provider easily with the following command: +Using [Laravel Artisan Console](https://laravel.com/docs/artisan), you can generate a custom state +provider easily with the following command: ```console php artisan make:state-provider @@ -181,7 +194,10 @@ final class BlogPostProvider implements ProviderInterface 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`. +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. @@ -200,8 +216,9 @@ use App\State\BlogPostProvider; class BlogPost {} ``` -Now let's say that we also want to handle the `/blog_posts` URI which returns a collection. We can change the Provider into -supporting a wider range of operations. Then we can provide a collection of blog posts when the operation is a `CollectionOperationInterface`: +Now let's say that we also want to handle the `/blog_posts` URI which returns a collection. We can +change the Provider into supporting a wider range of operations. Then we can provide a collection of +blog posts when the operation is a `CollectionOperationInterface`: ```php <?php @@ -232,7 +249,8 @@ final class BlogPostProvider implements ProviderInterface } ``` -We then need to configure this same provider on the BlogPost `GetCollection` operation, or for every operation via the `ApiResource` attribute: +We then need to configure this same provider on the BlogPost `GetCollection` operation, or for every +operation via the `ApiResource` attribute: ```php <?php @@ -249,9 +267,13 @@ class BlogPost {} ## Hooking into the Built-In State Provider -If you want to execute custom business logic before or after retrieving data, this can be achieved by [decorating](https://symfony.com/doc/current/service_container/service_decoration.html) the built-in state providers or using [composition](https://en.wikipedia.org/wiki/Object_composition). +If you want to execute custom business logic before or after retrieving data, this can be achieved +by [decorating](https://symfony.com/doc/current/service_container/service_decoration.html) the +built-in state providers or using [composition](https://en.wikipedia.org/wiki/Object_composition). -The next examples (one for Symfony and one for Laravel) uses a [DTO](https://api-platform.com/docs/core/dto/#using-data-transfer-objects-dtos) to change the presentation for data originally retrieved by the default state provider. +The next examples (one for Symfony and one for Laravel) uses a +[DTO](https://api-platform.com/docs/core/dto/#using-data-transfer-objects-dtos) to change the +presentation for data originally retrieved by the default state provider. ### Symfony State Provider mechanism @@ -332,11 +354,11 @@ final class BookRepresentationProvider implements ProviderInterface ) { } - + 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(), @@ -345,7 +367,9 @@ final class BookRepresentationProvider implements ProviderInterface } ``` -And we bind the [ItemProvider](https://github.com/api-platform/core/blob/main/src/Laravel/Eloquent/State/ItemProvider.php) in our Service Provider +And we bind the +[ItemProvider](https://github.com/api-platform/core/blob/main/src/Laravel/Eloquent/State/ItemProvider.php) +in our Service Provider ```php <?php @@ -394,9 +418,9 @@ class Book {} ## Registering Services Without Autowiring (only for the Symfony variant) The services in the previous examples are automatically registered because -[autowiring](https://symfony.com/doc/current/service_container/autowiring.html) -and autoconfiguration are enabled by default in API Platform. -To declare the service explicitly, you can use the following snippet: +[autowiring](https://symfony.com/doc/current/service_container/autowiring.html) and +autoconfiguration are enabled by default in API Platform. To declare the service explicitly, you can +use the following snippet: ```yaml # api/config/services.yaml diff --git a/core/subresources.md b/core/subresources.md index f631224e3cf..6516380c151 100644 --- a/core/subresources.md +++ b/core/subresources.md @@ -1,28 +1,29 @@ # 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. +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. +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. +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. +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”. +> [!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`. +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: @@ -90,8 +91,8 @@ class Question # The YAML syntax is only supported for Symfony # api/config/api_platform/resources.yaml resources: - App\Entity\Answer: ~ - App\Entity\Question: ~ + App\Entity\Answer: ~ + App\Entity\Question: ~ ``` ```xml @@ -111,9 +112,9 @@ resources: </code-selector> -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`: +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`: <code-selector> @@ -148,16 +149,16 @@ class Answer # 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: ~ + App\Entity\Answer: + uriTemplate: /questions/{id}/answer + uriVariables: + id: + fromClass: App\Entity\Question + fromProperty: answer + operations: + ApiPlatform\Metadata\Get: ~ + + App\Entity\Question: ~ ``` ```xml @@ -186,12 +187,14 @@ resources: </code-selector> -In this example, we instructed API Platform that the `Answer` we retrieve comes **from** the **class** `Question` -**from** the **property** `answer` of that class. +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. +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: +If we had a `relatedQuestions` property on the `Answer` we could retrieve the collection of related +questions via the following definition: <code-selector> @@ -209,14 +212,14 @@ If we had a `relatedQuestions` property on the `Answer` we could retrieve the co #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: ~ + App\Entity\Question: + uriTemplate: /answers/{id}/related_questions.{_format} + uriVariables: + id: + fromClass: App\Entity\Answer + fromProperty: relatedQuestions + operations: + ApiPlatform\Metadata\GetCollection: ~ ``` ```xml @@ -237,12 +240,15 @@ resources: ### Company Employee's -> [!NOTE] -> In Symfony we use the term “entities”, while the following documentation is mostly for Laravel “models”. +> [!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. +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 +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 <?php @@ -320,11 +326,15 @@ class Company } ``` -We did not define any Doctrine or Eloquent annotation here and if we want things to work properly with GraphQL, we need to map the `employees` field as a Link to the class `Employee` using the property `company`. +We did not define any Doctrine or Eloquent annotation here and if we want things to work properly +with GraphQL, we need to map the `employees` field as a Link to the class `Employee` using the +property `company`. -As a general rule, if the property we want to create a link from is in the `fromClass`, use `fromProperty`, if not, use `toProperty`. +As a general rule, if the property we want to create a link from is in the `fromClass`, use +`fromProperty`, if not, use `toProperty`. -For example, we could add a subresource fetching an employee's company. The `company` property belongs to the `Employee` class we can use `fromProperty`: +For example, we could add a subresource fetching an employee's company. The `company` property +belongs to the `Employee` class we can use `fromProperty`: ```php <?php @@ -345,12 +355,15 @@ class Company { ## Security -> [!WARNING] -> This is not yet available with Laravel, you're welcome to contribute [on GitHub](https://github.com/api-platform/core) +> [!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. +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. +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. @@ -371,10 +384,11 @@ class Company { } ``` -This is currently an experimental feature disabled by default. To enable it please set `enable_link_security` to true: +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 + enable_link_security: true ``` diff --git a/core/testing.md b/core/testing.md index 480ae28fa24..d598b0264a1 100644 --- a/core/testing.md +++ b/core/testing.md @@ -1,13 +1,16 @@ # 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. +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). +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. +- 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. diff --git a/core/upgrade-guide.md b/core/upgrade-guide.md index 25c4688dc67..63290cf1c1b 100644 --- a/core/upgrade-guide.md +++ b/core/upgrade-guide.md @@ -2,7 +2,8 @@ ## API Platform 3.4 -Remove the `keep_legacy_inflector`, the `event_listeners_backward_compatibility_layer` and the `rfc_7807_compliant_errors` flag: +Remove the `keep_legacy_inflector`, the `event_listeners_backward_compatibility_layer` and the +`rfc_7807_compliant_errors` flag: ```diff api_platform: @@ -17,43 +18,48 @@ If you use a custom normalizer for validation exception use: ```yaml api_platform: - validator: - legacy_validation_exception: true + 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` +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: +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 + 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: +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 + 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: +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 + 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 + defaults: + extra_properties: + standard_put: true ``` We recommend using the standalone API Platform packages instead of the Core monolithic repository. @@ -72,36 +78,39 @@ Update your `composer.json` like that: ## 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. +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 + 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: +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'] + json: ["application/json"] ``` You can also remove documentations you're not using via the new `docs_formats`. @@ -116,31 +125,42 @@ For new users we recommend to use 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. +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: +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). +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 + 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/). +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: +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) diff --git a/core/url-generation-strategy.md b/core/url-generation-strategy.md index 98efcb34d6e..8def1ac86f4 100644 --- a/core/url-generation-strategy.md +++ b/core/url-generation-strategy.md @@ -6,21 +6,22 @@ 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 + "@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. +You may want to use absolute URLs (for instance if resources are used in another API) or network +paths instead. ## Configure URL Generation Globally @@ -31,8 +32,8 @@ It can be configured globally using one of the configurations below: ```yaml # api/config/packages/api_platform.yaml api_platform: - defaults: - url_generation_strategy: !php/const ApiPlatform\Metadata\UrlGeneratorInterface::ABS_URL + defaults: + url_generation_strategy: !php/const ApiPlatform\Metadata\UrlGeneratorInterface::ABS_URL ``` ### Configure URL Generation Globally using Laravel @@ -74,8 +75,8 @@ class Book # api/config/api_platform/resources.yaml # The YAML syntax is only supported for Symfony resources: - App\ApiResource\Book: - urlGenerationStrategy: !php/const ApiPlatform\Api\UrlGeneratorInterface::ABS_URL + App\ApiResource\Book: + urlGenerationStrategy: !php/const ApiPlatform\Api\UrlGeneratorInterface::ABS_URL ``` ```xml @@ -98,16 +99,16 @@ 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 + "@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 } ``` diff --git a/core/validation.md b/core/validation.md index 42533bab891..dc5d149a762 100644 --- a/core/validation.md +++ b/core/validation.md @@ -1,6 +1,7 @@ # Validation -API Platform takes care of validating the data sent to the API by the client (usually user data entered through forms). +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). diff --git a/create-client/custom.md b/create-client/custom.md index b24d23b7de0..3fb75cdd22e 100644 --- a/create-client/custom.md +++ b/create-client/custom.md @@ -1,8 +1,14 @@ # 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. +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. +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 @@ -10,65 +16,69 @@ You will probably want to extend or, at least, take a look at [BaseGenerator.js] 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. +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. +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`. +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'; +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); - } + 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); + } } ``` diff --git a/create-client/index.md b/create-client/index.md index 9280dc765f5..1858c5e930e 100644 --- a/create-client/index.md +++ b/create-client/index.md @@ -1,7 +1,7 @@ # 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. +Create Client is the fastest way to scaffold fully featured webapps and native mobile apps from APIs +supporting the [Hydra](https://www.hydra-cg.com/) or [OpenAPI](https://www.openapis.org/) format. ![Screencast](images/create-client-demo.gif) @@ -18,21 +18,27 @@ It is able to generate apps using the following frontend stacks: - [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. +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 + - 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) +- 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) +- 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) diff --git a/create-client/nextjs.md b/create-client/nextjs.md index 84263fee8cf..c0007a7d08a 100644 --- a/create-client/nextjs.md +++ b/create-client/nextjs.md @@ -2,14 +2,15 @@ ![List screenshot](images/nextjs/create-client-nextjs-list.png) -The Next.js generator scaffolds components for server-side rendered (SSR) applications using [Next.js](https://nextjs.org/). +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. +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! @@ -53,13 +54,14 @@ Install the required dependencies: 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) +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 Distribution with Symfony](../symfony/index.md) generating all the code you need for a given resource is as simple as running the following command: +If you are using the [API Platform Distribution with Symfony](../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 \ @@ -88,8 +90,8 @@ Or if you don't use the standalone installation, run the following command inste 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`. +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! @@ -131,12 +133,13 @@ You can launch the server with: ```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). +If you want to generate a production build locally with docker compose, follow +[these instructions](../deployment/docker-compose.md). ## Screenshots diff --git a/create-client/nuxt.md b/create-client/nuxt.md index 6406d5301b2..113453581e3 100644 --- a/create-client/nuxt.md +++ b/create-client/nuxt.md @@ -19,7 +19,8 @@ To generate the code you need for a given resource, run the following command: 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`. +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. @@ -28,9 +29,9 @@ Add Pinia module in `nuxt.config.ts`: ```typescript // https://nuxt.com/docs/api/configuration/nuxt-config export default defineNuxtConfig({ - // ... - modules: ['@pinia/nuxt'], - // ... + // ... + modules: ["@pinia/nuxt"], + // ... }); ``` @@ -48,15 +49,15 @@ 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: {}, + // ... + css: ["~/assets/css/main.css"], + postcss: { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, }, - }, - // ... + // ... }); ``` @@ -65,18 +66,18 @@ 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: [], + content: [ + "./components/**/*.{js,vue,ts}", + "./layouts/**/*.vue", + "./pages/**/*.vue", + "./plugins/**/*.{js,ts}", + "./nuxt.config.{js,ts}", + "./app.vue", + ], + theme: { + extend: {}, + }, + plugins: [], }; ``` @@ -98,5 +99,4 @@ Go to `https://localhost:3000/books/` to start using your app. ## Screenshots -![List](images/nuxt/create-client-nuxt-list.png) -![Edit](images/nuxt/create-client-nuxt-edit.png) +![List](images/nuxt/create-client-nuxt-list.png) ![Edit](images/nuxt/create-client-nuxt-edit.png) diff --git a/create-client/quasar.md b/create-client/quasar.md index 356a6ef4e51..5d50f0e2787 100644 --- a/create-client/quasar.md +++ b/create-client/quasar.md @@ -1,7 +1,6 @@ # Quasar Framework Generator -Create a Quasar Framework application using -[Quasar CLI](https://quasar.dev/start/quasar-cli): +Create a Quasar Framework application using [Quasar CLI](https://quasar.dev/start/quasar-cli): ```console npm i -g @quasar/cli @@ -35,22 +34,23 @@ In the app directory, generate the files for the resource you want: npm init @api-platform/client https://demo.api-platform.com src/ -- --generator quasar --resource foo ``` -Replace the URL by 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`. +Replace the URL by 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. +**Note:** Make sure to follow the result indications of the command to register the routes and the +translations. Import common translations: ```ts // src/i18n/en-US/index.ts -import common from './common'; +import common from "./common"; export default { - // ... - ...common, + // ... + ...common, }; ``` diff --git a/create-client/react-native.md b/create-client/react-native.md index 281c895dfd0..3fe31a678a7 100644 --- a/create-client/react-native.md +++ b/create-client/react-native.md @@ -33,8 +33,8 @@ In the app directory, generate the files for the resource you want: npm init @api-platform/client https://demo.api-platform.com . -- --generator react-native --resource book ``` -Replace the URL with the entrypoint of your Hydra-enabled API. -You can also use an OpenAPI documentation with `-f openapi3`. +Replace the URL with the entrypoint of your Hydra-enabled API. You can also use an OpenAPI +documentation with `-f openapi3`. Omit the resource flag to generate files for all resource types exposed by the API. @@ -42,15 +42,15 @@ Create a `Router.js` file to import all routes: ```javascript // Router.js -import React from 'react'; -import { Router, Stack } from 'react-native-router-flux'; +import React from "react"; +import { Router, Stack } from "react-native-router-flux"; // Replace "book" with the name of the resource type -import BookRoutes from './routes/book'; +import BookRoutes from "./routes/book"; const RouterComponent = () => ( - <Router> - <Stack key="root">{BookRoutes}</Stack> - </Router> + <Router> + <Stack key="root">{BookRoutes}</Stack> + </Router> ); export default RouterComponent; @@ -60,48 +60,48 @@ Here is an example of an `App.js` file: ```javascript // App.js -import React, { Component } from 'react'; -import { Provider } from 'react-redux'; -import thunk from 'redux-thunk'; -import { createStore, applyMiddleware, combineReducers } from 'redux'; -import { View } from 'react-native'; -import { reducer as form } from 'redux-form'; +import React, { Component } from "react"; +import { Provider } from "react-redux"; +import thunk from "redux-thunk"; +import { createStore, applyMiddleware, combineReducers } from "redux"; +import { View } from "react-native"; +import { reducer as form } from "redux-form"; // see https://github.com/facebook/react-native/issues/14796 -import { Buffer } from 'buffer'; +import { Buffer } from "buffer"; global.Buffer = Buffer; // see https://github.com/facebook/react-native/issues/16434 -import { URL, URLSearchParams } from 'whatwg-url'; +import { URL, URLSearchParams } from "whatwg-url"; global.URL = URL; global.URLSearchParams = URLSearchParams; // see https://github.com/facebook/react-native/issues/12890 -import RNEventSource from 'react-native-event-source'; +import RNEventSource from "react-native-event-source"; global.EventSource = RNEventSource; // Replace "book" with the name of resource type -import book from './reducers/book'; -import Router from './Router'; +import book from "./reducers/book"; +import Router from "./Router"; export default class App extends Component { - render() { - const store = createStore( - combineReducers({ - book, - form, - }), - {}, - applyMiddleware(thunk) - ); - return ( - <Provider store={store}> - <View style={{ flex: 1 }}> - <Router /> - </View> - </Provider> - ); - } + render() { + const store = createStore( + combineReducers({ + book, + form, + }), + {}, + applyMiddleware(thunk), + ); + return ( + <Provider store={store}> + <View style={{ flex: 1 }}> + <Router /> + </View> + </Provider> + ); + } } ``` @@ -113,5 +113,7 @@ expo start ## Screenshots in iOS Simulator -![List](images/react-native/create-client-react-native-list.png) ![Show](images/react-native/create-client-react-native-show.png) -![Add](images/react-native/create-client-react-native-add.png) ![Delete](images/react-native/create-client-react-native-delete.png) +![List](images/react-native/create-client-react-native-list.png) +![Show](images/react-native/create-client-react-native-show.png) +![Add](images/react-native/create-client-react-native-add.png) +![Delete](images/react-native/create-client-react-native-delete.png) diff --git a/create-client/react.md b/create-client/react.md index 8722776af42..0622c5ddb4b 100644 --- a/create-client/react.md +++ b/create-client/react.md @@ -2,8 +2,8 @@ ![List screenshot](images/react/create-client-react-list.png) -The React generator scaffolds a Single Page Application or a Progressive Web App built with battle-tested libraries -from the ecosystem: +The React generator scaffolds a Single Page Application or a Progressive Web App built with +battle-tested libraries from the ecosystem: - [React](https://reactjs.org/) - [React Router](https://reactrouter.com/) @@ -42,8 +42,8 @@ npm run dev npm init @api-platform/client https://demo.api-platform.com src/ -- --generator react --resource book ``` -Replace the URL by the entrypoint of your Hydra-enabled API. -You can also use an OpenAPI documentation with `-f openapi3`. +Replace the URL by the entrypoint of your Hydra-enabled API. You can also use an OpenAPI +documentation with `-f openapi3`. Omit the resource flag to generate files for all resource types exposed by the API. diff --git a/create-client/troubleshooting.md b/create-client/troubleshooting.md index 2e743041048..7c65171b3f8 100644 --- a/create-client/troubleshooting.md +++ b/create-client/troubleshooting.md @@ -11,8 +11,9 @@ NODE_TLS_REJECT_UNAUTHORIZED=0 npm init @api-platform/client --generator typescr ## 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: +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} @@ -22,18 +23,20 @@ api_jsonld_context ANY ANY ANY /contexts/{s ## 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 +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/`. +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 @@ -52,12 +55,13 @@ 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. +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. +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. diff --git a/create-client/typescript.md b/create-client/typescript.md index 3771f2832c6..bfd94ebb17a 100644 --- a/create-client/typescript.md +++ b/create-client/typescript.md @@ -1,6 +1,7 @@ # TypeScript Interfaces -The TypeScript Generator allows you to create [TypeScript interfaces](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#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: @@ -12,10 +13,11 @@ npm init @api-platform/client https://demo.api-platform.com src/ -- --generator `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`. +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. +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). @@ -30,6 +32,6 @@ npm init @api-platform/client https://demo.api-platform.com src/ -- --generator You will obtain 2 `.ts` files arranged as following: - src/ - - interfaces/ - - foo.ts - - bar.ts + - interfaces/ + - foo.ts + - bar.ts diff --git a/create-client/vuejs.md b/create-client/vuejs.md index 8d303e3fd7d..da795c2fc31 100644 --- a/create-client/vuejs.md +++ b/create-client/vuejs.md @@ -19,8 +19,8 @@ To generate all the code you need for a given resource run the following command npm init @api-platform/client https://demo.api-platform.com src/ -- --generator vue --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`. +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. @@ -31,11 +31,11 @@ Replace the content of `App.vue` with the following code: ```html // src/App.vue <template> - <RouterView /> + <RouterView /> </template> <script setup lang="ts"> - import { RouterView } from 'vue-router'; + import { RouterView } from "vue-router"; </script> ``` @@ -52,11 +52,11 @@ Replace the content of `tailwind.config.js` by: // tailwind.config.js /** @type {import('tailwindcss').Config} */ module.exports = { - content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'], - theme: { - extend: {}, - }, - plugins: [], + content: ["./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}"], + theme: { + extend: {}, + }, + plugins: [], }; ``` diff --git a/create-client/vuetify.md b/create-client/vuetify.md index fdede0382c7..f64b259c6ab 100644 --- a/create-client/vuetify.md +++ b/create-client/vuetify.md @@ -19,18 +19,19 @@ To generate all the code you need for a given resource run the following command 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`. +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. +**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'; +import { VDataTableServer } from "vuetify/labs/VDataTable"; ``` In the same file replace the export with: @@ -38,9 +39,9 @@ In the same file replace the export with: ```typescript // src/plugins/vuetify.ts export default createVuetify({ - components: { - VDataTableServer, - }, + components: { + VDataTableServer, + }, }); ``` @@ -48,7 +49,7 @@ In `src/plugins/index.ts` add this import: ```typescript // src/plugins/index.ts -import i18n from '@/plugins/i18n'; +import i18n from "@/plugins/i18n"; ``` In the same file add `.use(i18n)` chained with the other `use()` functions. diff --git a/deployment/docker-compose.md b/deployment/docker-compose.md index 2e31b9fde67..7b96dd629c9 100644 --- a/deployment/docker-compose.md +++ b/deployment/docker-compose.md @@ -1,33 +1,40 @@ # 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. +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. +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). +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). +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! +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. +For test purposes, the cheapest plans will be enough, even though you might want at least 2GB of RAM +to execute Docker Compose for the first time. For real production usage, you'll probably want to +pick a plan in the "general purpose" section to fit your needs. ![Deploying an API Platform project on DigitalOcean with Docker Compose](images/digitalocean-droplet.png) -You can keep the defaults for other settings, or tweak them according to your needs. -Don't forget to add your SSH key or create a password then press the "Finalize and create" button. +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: +Then, wait a few seconds while your Droplet is provisioning. When your Droplet is ready, use SSH to +connect: ```console ssh root@<droplet-ip> @@ -35,8 +42,8 @@ ssh root@<droplet-ip> ## 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. +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: @@ -48,14 +55,15 @@ Example with the DigitalOcean Domains service ("Networking" > "Domains"): ![Configuring DNS on DigitalOcean](../deployment/images/digitalocean-dns.png) -> [!NOTE] -> Let's Encrypt, the service used by default by API Platform to automatically generate a TLS certificate, doesn't support using bare IP addresses. -> Using a domain name is mandatory to use Let's Encrypt. +> [!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). +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: @@ -64,7 +72,8 @@ Example with Git: git clone git@github.com:<username>/<project-name>.git ``` -Go into the directory containing your project (`<project-name>`), and start the app in production mode: +Go into the directory containing your project (`<project-name>`), and start the app in production +mode: ```console SERVER_NAME=your-domain-name.example.com \ @@ -74,20 +83,22 @@ 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. +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. +> [!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! +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. +> [!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: +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 \ @@ -100,15 +111,18 @@ 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. +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. +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 @@ -119,23 +133,25 @@ As a shortcut, `private_ranges` may be configured to trust all private IP 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. +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: +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 + build: + context: ./pwa + target: prod + network: host + extra_hosts: + - php=127.0.0.1 ``` #### 2. Build and start the php service @@ -154,7 +170,9 @@ 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: +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 @@ -178,4 +196,5 @@ 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. +These steps ensure the Next.js client can statically generate pages by accessing the API during the +build process. diff --git a/deployment/heroku.md b/deployment/heroku.md index b988b8f7dc7..09baa188001 100644 --- a/deployment/heroku.md +++ b/deployment/heroku.md @@ -1,52 +1,61 @@ # 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. +[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). +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. +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._ +_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. +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. +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. +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" + "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" } - ], - "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. +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. +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: @@ -74,11 +83,12 @@ As Heroku doesn't support Varnish out of the box, let's disable its integration: - 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. +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. +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: @@ -125,7 +135,7 @@ heroku create 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. +**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`. diff --git a/deployment/index.md b/deployment/index.md index e594468f5e9..eceb65611ed 100644 --- a/deployment/index.md +++ b/deployment/index.md @@ -1,13 +1,16 @@ # 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. +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 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: +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: <p align="center" class="symfonycasts"><a href="https://symfonycasts.com/screencast/ansistrano?cid=apip"><img src="../symfony/images/symfonycasts-player.png" alt="JWT screencast"><br>Watch the Animated Deployment with Ansistrano screencast</a></p> diff --git a/deployment/kubernetes.md b/deployment/kubernetes.md index 2c5cffcc730..79decd69618 100644 --- a/deployment/kubernetes.md +++ b/deployment/kubernetes.md @@ -1,22 +1,28 @@ # 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. +[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. -[The API Platform symfony variant](../symfony/index.md) contains a built-in [Helm](https://helm.sh/) (the k8s -package manager) chart to deploy in a wink on any of these platforms. +[The API Platform symfony variant](../symfony/index.md) contains a built-in [Helm](https://helm.sh/) +(the k8s package manager) chart to deploy in a wink on any of these platforms. 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)! +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/) +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/ @@ -26,8 +32,8 @@ Working-Dir: Your local installation of api-platform. Default /api-platform/ ### 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. +[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 @@ -35,15 +41,17 @@ 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) +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. +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 @@ -76,7 +84,8 @@ The result should look similar to these images. 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) +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 @@ -84,8 +93,9 @@ If you are using version 2.x follow this [guide to migrate Helm to v3](https://h 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. +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 @@ -108,17 +118,15 @@ helm upgrade main ./helm/api-platform --namespace=default --create-namespace --w --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='\*' +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. +After a successful installation, there is a message at the end. You can copy these commands and +execute them to set a port-forwarding and get access on your local machine to the deploy. See image +below. ![Deploy Result](images/deploy-result.png) @@ -132,13 +140,13 @@ helm upgrade api-platform ./helm/api-platform \ --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/). +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. +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}") @@ -147,12 +155,12 @@ kubectl --namespace=default exec -it $CADDY_PHP_POD -c api-platform-php -- bin/c ## 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) +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 @@ -165,7 +173,8 @@ You can upgrade with the same command from the installation and pass all paramet ### 2. Use :latest tags -Infos about [best practices for tagging images for Kubernetes](https://kubernetes.io/docs/concepts/containers/images/) +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 @@ -186,13 +195,16 @@ helm upgrade api-platform ./helm/api-platform --namespace=default \ ## 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/): +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. +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. @@ -210,13 +222,15 @@ args: Here is an example on how to use it from your `values.yaml`: ```yaml -command: ['bin/console'] -commandArgs: ['messenger:consume', 'async', '--memory-limit=100M'] +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. +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: +First, make sure to install the `/bin/ps` binary, otherwise the `readinessProbe` and `livenessProbe` +will fail: <!-- markdownlint-disable no-hard-tabs --> @@ -234,23 +248,13 @@ Then, update the probes: ```yaml readinessProbe: - exec: - command: - [ - '/bin/sh', - '-c', - "/usr/bin/pgrep -af '^php.*bin/console.*messenger:consume'", - ] - initialDelaySeconds: 120 - periodSeconds: 3 + 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 + exec: + command: ["/bin/sh", "-c", "/usr/bin/pgrep -af '^php.*bin/console.*messenger:consume'"] + initialDelaySeconds: 120 + periodSeconds: 3 ``` diff --git a/deployment/minikube.md b/deployment/minikube.md index 83eb3cac86b..8839396bc22 100644 --- a/deployment/minikube.md +++ b/deployment/minikube.md @@ -2,7 +2,8 @@ ## Install minikube -If you have no existing installation of minikube on your computer, [follow the official tutorial](https://minikube.sigs.k8s.io/docs/start/). +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: @@ -10,19 +11,24 @@ When Minikube is installed, start the cluster: 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. +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 the chart provided in the API Platform distribution. +Finally, [install Helm](https://helm.sh/docs/intro/install/). We'll use it to deploy the application +in the cluster thanks to the chart provided in the API Platform distribution. ## 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: +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). +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: @@ -57,7 +63,8 @@ helm upgrade --install my-project helm/api-platform \ --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! +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. @@ -65,7 +72,8 @@ Run `minikube dashboard` at any moment to see the state of your deployments. 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. +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). @@ -75,7 +83,10 @@ Then, run minikube: 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). +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: diff --git a/deployment/traefik.md b/deployment/traefik.md index dabdaa548ef..ce7a88c2c0f 100644 --- a/deployment/traefik.md +++ b/deployment/traefik.md @@ -1,36 +1,44 @@ # 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. +> 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. +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. +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 +- `--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`) - ``` + ```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) + 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 +- `--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: +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 @@ -41,140 +49,140 @@ After that, don't forget to re-build your containers ```yaml # compose.yaml -version: '3.4' +version: "3.4" x-cache-from: - - &cache - cache_from: - - ${NGINX_IMAGE:-quay.io/api-platform/nginx} - - ${PHP_IMAGE:-quay.io/api-platform/php} + - &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 + 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 + 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: {} + 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: +`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 @@ -183,7 +191,8 @@ If you want to implement custom DNS such as production DNS in local, just add th 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. +If you do that, you'll have to update the `CORS_ALLOW_ORIGIN` environment variable `api/.env` to +accept the specified URL. ## Known Issues @@ -191,38 +200,41 @@ 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. +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' +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 + 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 + api_platform_network: + external: true + # Add other networks here ``` Then update the `compose.yaml` file belonging to your API Platform projects: @@ -404,7 +416,8 @@ volumes: + external: true ``` -Finally, some environment variables must be defined, here is an example of a `.env` file to set them: +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 @@ -421,8 +434,11 @@ 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: +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 @@ -442,16 +458,18 @@ Then update each traefik http routers names and services following this sample f # /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 + - 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. +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. +Create a new `init-dc.sh` which contains the generation code that will be written in +`compose.override.yaml` file. ```console #!/bin/sh @@ -503,172 +521,175 @@ Write this minimal configuration into your `traefik.toml` file: address = ":443" ``` -Then after that update respectively your API Platform and Træfik `compose.yaml` following these examples below. +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' +version: "3.4" x-cache: &cache - cache_from: - - ${CONTAINER_REGISTRY_BASE}/php - - ${CONTAINER_REGISTRY_BASE}/nginx - - ${CONTAINER_REGISTRY_BASE}/varnish + cache_from: + - ${CONTAINER_REGISTRY_BASE}/php + - ${CONTAINER_REGISTRY_BASE}/nginx + - ${CONTAINER_REGISTRY_BASE}/varnish x-network: &network - networks: - - api_platform_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 + 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 - 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 + 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: {} + db-data: {} networks: - api_platform_network: - external: true + api_platform_network: + external: true ``` ```yaml # /anywhere/traefik/compose.yaml -version: '3.4' +version: "3.4" x-network: &network - networks: - - api_platform_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 + 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 + 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. +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. diff --git a/extra/conduct.md b/extra/conduct.md index 0a1058106df..e02b53b0603 100644 --- a/extra/conduct.md +++ b/extra/conduct.md @@ -1,14 +1,13 @@ # 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. +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. +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: @@ -16,34 +15,30 @@ Examples of unacceptable behavior by participants include: - 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 +- 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, +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. +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. +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 +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] +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/ diff --git a/extra/contribution-guides.md b/extra/contribution-guides.md index ee0a3b95bb0..b3c4761c2f5 100644 --- a/extra/contribution-guides.md +++ b/extra/contribution-guides.md @@ -1,19 +1,19 @@ # Contribution Guides -## API Platform Core +## API Platform Core -- [General Contribution Guide](https://github.com/api-platform/core/blob/main/CONTRIBUTING.md) +- [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) +- [General Contribution Guide](https://github.com/api-platform/docs/blob/main/CONTRIBUTING.md) -## API Platform Tools +## 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) +- [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).** diff --git a/extra/enterprise.md b/extra/enterprise.md index b4e351a5e11..81fee1551df 100644 --- a/extra/enterprise.md +++ b/extra/enterprise.md @@ -1,27 +1,46 @@ # 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). +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. +[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. +[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. +- **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) diff --git a/extra/philosophy.md b/extra/philosophy.md index 07c71404681..a48f4036cd6 100644 --- a/extra/philosophy.md +++ b/extra/philosophy.md @@ -2,34 +2,47 @@ 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. +- 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. +- [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. diff --git a/extra/releases.md b/extra/releases.md index a486f736271..f6f5938ee0c 100644 --- a/extra/releases.md +++ b/extra/releases.md @@ -1,14 +1,16 @@ # 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. +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); +- 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; @@ -19,15 +21,20 @@ For example: 3 versions are maintained at the same time: - **stable** (currently the **4.2** branch): regular bugfixes are integrated in this version -- **old-stable** (are the last branch: **4.1**): [security fixes](security.md) are integrated in this version, regular bugfixes are **not** backported in it +- **old-stable** (are the last branch: **4.1**): [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) **are not maintained**. If you still use them, you must upgrade as soon as possible. +Older versions (1.x, 2.6..., 3.0..., 4.0) **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. +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. +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. +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. diff --git a/extra/security.md b/extra/security.md index 5c5dbbbc9e4..505f5cd03a5 100644 --- a/extra/security.md +++ b/extra/security.md @@ -1,14 +1,19 @@ # 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)). +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. +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: +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; @@ -16,22 +21,30 @@ For each report, we first try to confirm the vulnerability. When it is confirmed 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. +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). +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! +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. +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 @@ -39,25 +52,37 @@ Score of between 1 and 5 depending on how complex it is to exploit the vulnerabi - 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. +- 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. +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) +- 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. +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) +- Are a number of well-known FOSS projects using API Platform affected that requires coordinated + releases? (0-1) ### Score Totals @@ -75,4 +100,5 @@ Scores from the following areas are added together to produce a score. The score ## Credits -This document has been adapted from the [Symfony's security policy](https://symfony.com/doc/current/contributing/code/security.html). +This document has been adapted from the +[Symfony's security policy](https://symfony.com/doc/current/contributing/code/security.html). diff --git a/extra/troubleshooting.md b/extra/troubleshooting.md index f49e84b796a..8afb4e6bc65 100644 --- a/extra/troubleshooting.md +++ b/extra/troubleshooting.md @@ -6,19 +6,24 @@ This is a list of common pitfalls while using API Platform, and how to avoid the ### 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). +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. +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: +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. +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 @@ -30,29 +35,41 @@ You can change the port to be used in the `compose.yaml` file (default ports are ## 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. +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. +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: +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 + 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. +**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`. +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. +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: +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 { diff --git a/laravel/filters.md b/laravel/filters.md index aa8f1326b87..09118b28d49 100644 --- a/laravel/filters.md +++ b/laravel/filters.md @@ -1,10 +1,15 @@ # 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. +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`: +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 @@ -20,7 +25,8 @@ class Book extends Model } ``` -The `key` option specifies the query parameter and the `filter` applies the given value to a where clause: +The `key` option specifies the query parameter and the `filter` applies the given value to a where +clause: ```php namespace ApiPlatform\Laravel\Eloquent\Filter; @@ -42,7 +48,9 @@ final class EqualsFilter implements FilterInterface } ``` -You can create your own filters by implementing the `ApiPlatform\Laravel\Eloquent\Filter\FilterInterface`. API Platform provides several eloquent filters for a RAD approach. +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 @@ -64,7 +72,8 @@ class Book extends Model ### Parameter Validation -You can add [validation rules](https://laravel.com/docs/validation) to parameters within the `constraints` attribute: +You can add [validation rules](https://laravel.com/docs/validation) to parameters within the +`constraints` attribute: ```php // app/Models/Book.php @@ -84,7 +93,8 @@ 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: +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 @@ -102,7 +112,9 @@ 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`. +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 @@ -151,7 +163,8 @@ The `DateFilter` allows to filter dates with an operator (`eq`, `lt`, `gt`, `lte Usage Examples -With the `DateFilter` applied, you can now filter dates between 2024-01-01 and 2024-01-31 using these API calls: +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 @@ -189,7 +202,8 @@ class Book extends Model } ``` -Our default strategy is to exclude null values, just remove the `filterContext` if you want to exclude nulls. +Our default strategy is to exclude null values, just remove the `filterContext` if you want to +exclude nulls. ### Or @@ -217,7 +231,8 @@ class Book extends Model } ``` -This allows to query multiple `isbn` values with a `q` query parameter: `/books?q[]=9781784043735&q[]=9780369406361`. +This allows to query multiple `isbn` values with a `q` query parameter: +`/books?q[]=9781784043735&q[]=9780369406361`. <!-- ### Number @@ -225,7 +240,8 @@ TODO --> ### BooleanFilter -The `BooleanFilter` allows to filter using an `WHERE` clause on a boolean field with (`true`, `false`, `0`, `1`): +The `BooleanFilter` allows to filter using an `WHERE` clause on a boolean field with (`true`, +`false`, `0`, `1`): ```php // app/Models/Book.php @@ -254,7 +270,9 @@ Examples: ### 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 distribution. +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 +distribution. The property filter adds the possibility to select the properties to serialize (sparse fieldsets). @@ -279,22 +297,27 @@ class Book extends Model 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) +- `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`. +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: +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: +Then, provide the name of your filter, for example `MonthFilter`, or pass it directly as an +argument: ```console make:filter MyCustomFilter @@ -331,9 +354,11 @@ final class MonthFilter implements FilterInterface #### 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`). +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. +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: @@ -357,7 +382,7 @@ final class MonthFilter implements FilterInterface * @param array<string, mixed> $context */ public function apply(Builder $builder, mixed $values, Parameter $parameter, array $context = []): Builder - { + { return $builder->->whereMonth($parameter->getProperty(), $values); } } @@ -378,7 +403,7 @@ use App\Filters\MonthFilter; #[ApiResource] #[QueryParameter( key: 'createdAtMonth', - filter: new MonthFilter(), + filter: new MonthFilter(), property: 'createdAt' )] class Invoice @@ -387,5 +412,5 @@ 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! +And that's it! ✅ Your filter is operational. A request like `GET /invoices?createdAtMonth=7` will +now correctly return the invoices from July! diff --git a/laravel/index.md b/laravel/index.md index 24cf4c0bd8c..1ef51ac47c1 100644 --- a/laravel/index.md +++ b/laravel/index.md @@ -1,35 +1,46 @@ # API Platform for Laravel Projects -API Platform is **the easiest way** to create **state-of-the-art** web APIs -using Laravel! +API Platform is **the easiest way** to create **state-of-the-art** web APIs using Laravel! ![Basic REST API](images/basic-rest.png) With API Platform, you can: - [expose your Eloquent](#exposing-a-model) models in minutes as: - - a REST API implementing the industry-leading standards, formats and best practices: [JSON-LD](https://en.wikipedia.org/wiki/JSON-LD)/[RDF](https://en.wikipedia.org/wiki/Resource_Description_Framework), [JSON:API](https://jsonapi.org), [HAL](https://stateless.group/hal_specification.html), and many RFCs... - - a [GraphQL](#enabling-graphql) API - - or both at the same time, with the same code! -- automatically expose an [OpenAPI](https://www.openapis.org) specification (formerly Swagger), dynamically generated from your Eloquent models and always up to date -- automatically expose nice UIs and playgrounds to develop using your API ([Swagger UI](https://swagger.io/tools/swagger-ui/) and [GraphiQL](https://github.com/graphql/graphiql)) + - 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 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 + <!--* 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! ## 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 can be installed easily on new and existing Laravel projects. If you already have an +existing project, skip directly to the next section. 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: @@ -65,7 +76,8 @@ Open `http://127.0.0.1:8000/api/`, your API is already active and documented... ## Publishing the Config File and Assets -After installing API Platform, you can publish its assets and config using the `api-platform:install` Artisan command. +After installing API Platform, you can publish its assets and config using the +`api-platform:install` Artisan command. ```console php artisan api-platform:install @@ -81,7 +93,9 @@ Let's start by creating a `Book` model: 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. +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. @@ -93,7 +107,8 @@ First, create a migration class for the `books` table: php artisan make:migration create_books_table ``` -Open the generated migration class (`database/migrations/<timestamp>_create_books_table.php`) and add some columns: +Open the generated migration class (`database/migrations/<timestamp>_create_books_table.php`) and +add some columns: ```patch public function up(): void @@ -122,7 +137,8 @@ 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: +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; @@ -144,42 +160,56 @@ 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! +1. Registered the standard REST routes in Laravel's router and a controller that implements a + state-of-the-art, fully-featured, and secure API endpoint using the services provided by the + [API Platform Core library](../core/index.md) +2. Used its built-in Eloquent [state provider](../core/state-providers.md) to introspect the + database and gather metadata about all columns to expose through the API +3. Generated machine-readable documentations of the API in the + [OpenAPI (formerly known as Swagger)](../core/openapi.md) (available at + `http://127.0.0.1:8000/api/docs.json`) and + [JSON-LD](https://json-ld.org)/[Hydra](https://www.hydra-cg.com) formats using this metadata +4. Generated nice human-readable documentation and a sandbox for the API with + [SwaggerUI](https://swagger.io/tools/swagger-ui/) (Redoc is also available out-of-the-box) + +Imagine doing it all again, properly, by hand? How much time have you saved? Weeks, months? And +you've seen nothing yet! ## Playing With The API ![View of an item](images/books-collection.png) -If you access any API URL with the `.html` extension appended, API Platform displays -the corresponding API request in the UI. Try it yourself by browsing to `http://127.0.0.1:8000/api/books.html`. If no extension is present, API Platform will use the `Accept` header to select the format to use. +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 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. +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. +> [!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. +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. +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! +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: @@ -254,7 +284,8 @@ class Book ## Content Negotiation -By default, a JSON-LD response is sent [but many other formats, including CSV and JSON:API are supported](../core/content-negotiation.md). +By default, a JSON-LD response is sent +[but many other formats, including CSV and JSON:API are supported](../core/content-negotiation.md). You can enable or disable formats in `config/api-platform.php`: ```php @@ -287,7 +318,8 @@ return [ ![GraphQL in Laravel](images/graphql.png) -By default, only the REST endpoints are enabled, but API Platform also [supports GraphQL](../core/graphql.md)! +By default, only the REST endpoints are enabled, but API Platform also +[supports GraphQL](../core/graphql.md)! Install the GraphQL support package: @@ -307,25 +339,27 @@ Then open `http://127.0.0.1:8000/api/graphql` and replace the default GraphQL qu ```graphql { - books(first: 3) { - edges { - node { - title - author - publicationDate - } + 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. +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): +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; @@ -389,18 +423,18 @@ Let's replace our author column with a relation to a new `author` table: } ``` -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: +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" + "@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" } ``` @@ -415,9 +449,12 @@ Content-Type: application/merge-patch+json } ``` -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). +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`: +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; @@ -438,7 +475,8 @@ class Book extends Model } ``` -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): +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; @@ -457,23 +495,23 @@ 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" - } + "@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...). +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! @@ -481,10 +519,11 @@ 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. +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). +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: @@ -492,7 +531,8 @@ First, create a factory class for our `Book` model: php artisan make:factory BookFactory ``` -Then, edit `database/factories/BookFactory.php` to specify which generator to use for each property of the model: +Then, edit `database/factories/BookFactory.php` to specify which generator to use for each property +of the model: ```patch namespace Database\Factories; @@ -574,11 +614,12 @@ Finally, seed the database: 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! +> [!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 @@ -611,7 +652,8 @@ Read the [pagination documentation](../core/pagination.md) to learn all you can 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): +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 @@ -636,17 +678,22 @@ For instance, here is how to make your API read-only by enabling only the `GET` ![Read-only](images/read-only.png) -We'll use configuration options provided by API Platform all along this getting started guide, but there are tons of features! +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). +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`). +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). +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: +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 @@ -688,30 +735,35 @@ It's also possible to enable filters on every exposed property: 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: +The `:property` placeholder gives the ability to create a parameter for each exposed property. These +filters will be automatically documented: ![Filters documentation](images/filters-documentation.png) -On top of that, some validation rules are automatically added based on the given JSON Schema. You can customize the set of rules inside the `constraints` option of a `QueryParameter`. +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). +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 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 +- [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. +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: @@ -747,8 +799,8 @@ Or if you are using Laravel Passport (or any other OAuth server): ] ``` -A combination of both is also possible. -For more information, you can also check the [Swagger UI documentation](https://swagger.io/docs/specification/authentication/). +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 @@ -770,13 +822,15 @@ return [ ![Form Request](images/form-request.png) -To authorize write operations (`POST`, `PATCH`, `PUT`) and validate user input, you may generate a [Form Request class](https://laravel.com/docs/validation#creating-form-requests): +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): +Then, add validation rules to the generated class (`app/Http/Requests/BookFormRequest.php` in our +example): ```patch namespace App\Http\Requests; @@ -811,10 +865,11 @@ Then, add validation rules to the generated class (`app/Http/Requests/BookFormRe } ``` -In this example, we only authorize admin users to do write operations, and we add some validation rules. +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: +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 @@ -832,14 +887,16 @@ Otherwise, reference it explicitly in the `rules` parameter: } ``` -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/). +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): +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 @@ -847,52 +904,65 @@ 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). +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)! +Wouldn't it be nice to have an administration backend to manage the data exposed by your API? +Checkout [API Platform Admin](../admin/index.md)! ![The admin](../symfony/images/api-platform-2.6-admin.png) -This [Material Design](https://material.io/guidelines/) admin is a Single Page App built with [React Admin](https://marmelab.com/react-admin/). It is powerful and fully customizable. +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**. +It leverages the Hydra documentation exposed by the API component to build itself. It's 100% +dynamic - **no code generation occurs**. ### SPA/PWA Scaffolding ![The Next.js Progressive Web App](../symfony/images/api-platform-2.6-pwa-react.png) -API Platform also has an awesome [client generator](../create-client/index.md) able to scaffold fully working [Next.js](../create-client/nextjs.md), [Nuxt.js](../create-client/nuxt.md), [React/Redux](../create-client/react.md), [Vue.js](../create-client/vuejs.md), [Quasar](../create-client/quasar.md), -and [Vuetify](../create-client/vuetify.md) Progressive Web Apps/Single Page Apps that you can easily tune and customize. The generator also supports -[React Native](../create-client/react-native.md) if you prefer to leverage all capabilities of mobile devices. +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. +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`). +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. +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! +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. +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: @@ -922,7 +992,8 @@ class Book extends Model } ``` -When using the `IsApiResourceTrait`, it's also possible to return advanced configuration by defining an `apiResource()` static method. +When using the `IsApiResourceTrait`, it's also possible to return advanced configuration by defining +an `apiResource()` static method. These two classes are strictly equivalent: @@ -969,8 +1040,9 @@ class Book extends Model } ``` -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. +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: diff --git a/laravel/jwt.md b/laravel/jwt.md index 1dbfbc7edd5..f20cb257fe9 100644 --- a/laravel/jwt.md +++ b/laravel/jwt.md @@ -1,37 +1,40 @@ # 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. +> [!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). +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. + 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. + 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. +> [!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. +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**: +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. +> [!NOTE] Ensure your routes (/api/auth) and authentication mechanisms are configured to match your +> application's implementation. ### Test with Pest diff --git a/laravel/security.md b/laravel/security.md index cb8d59f1b93..388252420fb 100644 --- a/laravel/security.md +++ b/laravel/security.md @@ -2,10 +2,11 @@ ## Policies -API Platform is compatible with Laravel's [authorization](https://laravel.com/docs/authorization) mechanism. +API Platform is compatible with Laravel's [authorization](https://laravel.com/docs/authorization) +mechanism. -To utilize policies in API Platform, it is essential to have Laravel's authentication system initialized. -See the [Authentication section](#authentication) for more information. +To utilize policies in API Platform, it is essential to have Laravel's authentication system +initialized. See the [Authentication section](#authentication) for more information. Once a gate is defined, API Platform will automatically detect your policy. @@ -20,8 +21,8 @@ class Book extends Model } ``` -API Platform will detect the operation and map it to a specific method in your policy according to the rules defined in -this table: +API Platform will detect the operation and map it to a specific method in your policy according to +the rules defined in this table: | Operation | Policy | | -------------- | ---------------------------------------------------------- | @@ -32,8 +33,8 @@ this table: | DELETE | `delete` | | PUT | `update` or `create` if the resource doesn't already exist | -If your policy methods do not match Laravel's conventions, you can always use the `policy` property on an operation -attribute to enforce this policy: +If your policy methods do not match Laravel's conventions, you can always use the `policy` property +on an operation attribute to enforce this policy: ```php // app/Models/Book.php @@ -73,7 +74,8 @@ Gate::guessPolicyNamesUsing(function (string $modelClass): ?string { ## Authentication -Usually, you will use [Sanctum](https://laravel.com/docs/sanctum) and add a middleware on secured routes: +Usually, you will use [Sanctum](https://laravel.com/docs/sanctum) and add a middleware on secured +routes: ```php // app/Models/Book.php diff --git a/laravel/testing.md b/laravel/testing.md index 691a2e848bd..7d4f48dae19 100644 --- a/laravel/testing.md +++ b/laravel/testing.md @@ -1,31 +1,35 @@ # 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 +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). +- **[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. +> [!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. +> [!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. +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: @@ -33,7 +37,8 @@ To check the Pest installation, run the following command: php artisan test ``` -If for some reason, Pest is not installed refer to the [Pest Installation Guide](https://pestphp.com/docs/installation). +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: @@ -45,17 +50,19 @@ In that case, you can run Pest using: #### 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. +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: +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). +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. @@ -208,32 +215,36 @@ it('deletes a book') }); ``` -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. +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. +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! +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. +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). +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`. +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: @@ -241,11 +252,13 @@ You can test the PHPUnit installation by running: ./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). +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). +For instructions on generating the factory, please refer to the +[Generate The Factory section](#generate-the-factory). #### Writing PHPUnit tests @@ -417,51 +430,67 @@ class BooksTest extends TestCase } ``` -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. +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. +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! +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. +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 +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. +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 distribution is [shipped with a GitHub Actions workflow](https://github.com/api-platform/api-platform/blob/main/.github/workflows/ci.yml) that builds the Docker images, does a [smoke test](<https://en.wikipedia.org/wiki/Smoke_testing_(software)>) to check that the application's entrypoint is accessible, and runs PHPUnit. +The API Platform distribution is +[shipped with a GitHub Actions workflow](https://github.com/api-platform/api-platform/blob/main/.github/workflows/ci.yml) +that builds the Docker images, does a +[smoke test](<https://en.wikipedia.org/wiki/Smoke_testing_(software)>) 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 [the Helm chart provided with the distribution](../deployment/kubernetes.md) to deploy the app on a Kubernetes cluster. +The API Platform Demo +[contains a CD workflow](https://github.com/api-platform/demo/tree/main/.github/workflows) that uses +[the Helm chart provided with the distribution](../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 in the API Platform distribution): +You may also be interested in these alternative testing tools (not included in the API Platform +distribution): - [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; +- [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: +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 <?php @@ -475,7 +504,7 @@ use Tests\TestCase; class MyTest extends TestCase { use ApiTestAssertionsTrait; - + public function testSomething(): void { // $response = $this->get('/'); @@ -498,7 +527,7 @@ There is also a method to find the IRI matching a given resource: namespace App\Tests; use ApiPlatform\Laravel\Test\ApiTestAssertionsTrait; -use App\Models\Book; +use App\Models\Book; use Tests\TestCase; class BooksTest extends TestCase @@ -509,9 +538,9 @@ class BooksTest extends TestCase { $book = Book::firstOrFail(); $iri = $this->getIriFromResource($book); - + $response = $this->get($iri); - + $response->assertStatus(200); } } diff --git a/laravel/validation.md b/laravel/validation.md index 6ec5e72168e..6c32d37db9a 100644 --- a/laravel/validation.md +++ b/laravel/validation.md @@ -1,6 +1,7 @@ # Validation with Laravel -API Platform simplifies the validation of data sent by clients to the API, typically user inputs submitted through forms. +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: diff --git a/schema-generator/configuration.md b/schema-generator/configuration.md index 9007618fb21..cae9e5efa88 100644 --- a/schema-generator/configuration.md +++ b/schema-generator/configuration.md @@ -4,8 +4,9 @@ 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)). +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: @@ -16,7 +17,8 @@ namespaces: interface: "App\ECommerce\Model" ``` -Namespaces can also be specified for a specific type. It will take precedence over any globally configured namespace. +Namespaces can also be specified for a specific type. It will take precedence over any globally +configured namespace. Example: @@ -30,29 +32,28 @@ types: ## 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). +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) + 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. + 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: +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 @@ -62,16 +63,17 @@ Supported cardinalities are: - `(*..1)` - `(*..*)` -Cardinalities are enforced by the class generator, the Doctrine ORM generator and the Symfony validation generator. +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)' + Product: + properties: + sku: + cardinality: "(0..1)" ``` ## Changing the Default Cardinality @@ -82,38 +84,38 @@ By default, the cardinality `(1..1)` is used, but you can change it like this: ```yaml relations: - defaultCardinality: '(1..*)' + 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! +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')" } + 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: ~ + 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 + 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 @@ -124,24 +126,24 @@ 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 + 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. +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 + Person: + abstract: true ``` ## Define API Platform Operations @@ -150,26 +152,26 @@ API Platform operations can be added this way: ```yaml types: - Person: - operations: - Get: ~ - GetCollection: - routeName: get_person_collection + 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`. +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 } + properties: + name: { nullable: false } ``` The `#[Assert\NotNull]` constraint is automatically added. @@ -195,8 +197,8 @@ Example: ```yaml Person: - properties: - email: { unique: true } + properties: + email: { unique: true } ``` Output: @@ -240,8 +242,8 @@ A property can be marked read-only with the following configuration: ```yaml Person: - properties: - email: { writable: false } + properties: + email: { writable: false } ``` In such case, no mutator method will be generated. @@ -252,8 +254,8 @@ A property can be marked write-only with the following configuration: ```yaml Person: - properties: - email: { readable: false } + properties: + email: { readable: false } ``` In this case, no getter method will be generated. @@ -266,10 +268,10 @@ Example: ```yaml QuantitativeValue: - embeddable: true + embeddable: true Product: - properties: - weight: { range: 'QuantitativeValue', embedded: true } + properties: + weight: { range: "QuantitativeValue", embedded: true } ``` Output: @@ -308,8 +310,8 @@ class Product ## Skipping Accessor Method Generation -It's possible to skip the generation of accessor methods. This is particularly useful combined with the `visibility: public` -option. +It's possible to skip the generation of accessor methods. This is particularly useful combined with +the `visibility: public` option. To skip the generation of accessor methods, use the following config: @@ -338,22 +340,24 @@ 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: +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 + 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: +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 + generationStrategy: uuid ``` ## User-submitted UUIDs @@ -362,63 +366,65 @@ To manually set a UUID instead of letting the DBMS generate it, use the followin ```yaml id: - generationStrategy: uuid - writable: true + 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. +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 + 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: +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 + 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. +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' +fieldVisibility: "protected" ``` ## Generating `Assert\Type` Attributes -It's possible to automatically generate Symfony validator's `#[Assert\Type]` attributes using the following config: +It's possible to automatically generate Symfony validator's `#[Assert\Type]` attributes using the +following config: ```yaml validator: - assertType: true + 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)), +- 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. @@ -426,33 +432,34 @@ If this behaviour does not suit you, the inheritance attribute can be forced in ```yaml doctrine: - inheritanceType: SINGLE_TABLE # Default: JOINED - inheritanceAttributes: - CustomInheritanceAttribute: [] + 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. +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. +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: +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 + 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 + resolveTargetEntityConfigPath: path/to/doctrine.yaml + resolveTargetEntityConfigType: YAML # Supports XML & YAML ``` ### Doctrine Resolve Target Entity Config Type @@ -461,69 +468,72 @@ By default, the mapping file is in XML. If you want to have a YAML file, add the ```yaml doctrine: - resolveTargetEntityConfigPath: path/to/doctrine.yaml - resolveTargetEntityConfigType: yaml + 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 +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 + - 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. +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: +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 + - http://vocab.linkeddata.es/vgo/GameOntologyv3.owl # The URL of the vocabulary definition types: - Session: - vocabularyNamespace: http://purl.org/net/VideoGameOntology# + 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: +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 + # 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 + 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. +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. @@ -534,7 +544,7 @@ Add a `@author` PHPDoc annotation to class DocBlock. Example: ```yaml -author: 'Kévin Dunglas <kevin@les-tilleuls.coop>' +author: "Kévin Dunglas <kevin@les-tilleuls.coop>" ``` ## PHP File Header @@ -545,32 +555,32 @@ Example: ```yaml header: | - /* - * This file is part of the Ecommerce package. - * - * (c) Kévin Dunglas <kevin@dunglas.fr> - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ + /* + * This file is part of the Ecommerce package. + * + * (c) Kévin Dunglas <kevin@dunglas.fr> + * + * 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. +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 + - 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. +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: @@ -585,36 +595,36 @@ attributeGenerators ```yaml openApi: - file: null + file: null # RDF vocabularies vocabularies: - # Prototype - uri: - # RDF vocabulary to use - uri: ~ # Example: 'https://schema.org/version/latest/schemaorg-current-https.rdf' + # Prototype + uri: + # RDF vocabulary to use + uri: ~ # Example: 'https://schema.org/version/latest/schemaorg-current-https.rdf' - # RDF vocabulary format - format: null # Example: rdfxml + # 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 + # 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: [] + # Attributes (merged with generated attributes) + attributes: [] # Namespace of the vocabulary to import -vocabularyNamespace: 'https://schema.org/' # Example: 'http://www.w3.org/ns/activitystreams#' +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 + # 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)"; "(*..*)" + # 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 @@ -624,14 +634,14 @@ apiPlatformOldAttributes: false # IDs configuration id: - # Automatically add an ID field to entities - generate: true + # 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" + # 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 + # Is the ID writable? Only applicable if "generationStrategy" is "uuid". + writable: false # Generate interfaces and use Doctrine's Resolve Target Entity feature useInterface: false @@ -644,52 +654,52 @@ header: null # Example: '// (c) Kévin Dunglas <dunglas@gmail.com>' # PHP namespaces namespaces: - # The global namespace's prefix - prefix: null # Example: App\ + # 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 entities + entity: App\Entity # Example: App\Entity - # The namespace of the generated enumerations - enum: App\Enum # Example: App\Enum + # The namespace of the generated enumerations + enum: App\Enum # Example: App\Enum - # The namespace of the generated interfaces - interface: App\Model # Example: App\Model + # 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 + # Prototype + name: + # Name of this use + name: ~ # Example: App\Attributes\MyAttribute - # The alias to use for this use - alias: null + # The alias to use for this use + alias: null # Doctrine doctrine: - # Use Doctrine's ArrayCollection instead of standard arrays - useCollection: true + # 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 path + resolveTargetEntityConfigPath: null - # The Resolve Target Entity Listener config file type - resolveTargetEntityConfigType: XML # One of "XML"; "yaml" + # 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: [] + # 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" + # 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 + # Maximum length of any given database identifier, like tables or column names + maxIdentifierLength: 63 # Symfony Validator Component validator: - # Generate @Assert\Type annotation - assertType: false + # Generate @Assert\Type annotation + assertType: false # The value of the phpDoc's @author annotation author: false # Example: 'Kévin Dunglas <dunglas@gmail.com>' @@ -703,8 +713,8 @@ accessorMethods: true # Set this flag to true to generate fluent setter, adder and remover methods fluentMutatorMethods: false rangeMapping: - # Prototype - name: ~ + # Prototype + name: ~ # Generate all types, even if an explicit configuration exists allTypes: false @@ -714,97 +724,97 @@ resolveTypes: false # Types to import from the vocabulary types: - # Prototype - id: - # Exclude this type, even if "allTypes" is set to true" - exclude: false + # 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#' + # 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 abstract? (null to guess) + abstract: null - # Is the class embeddable? - embeddable: false + # Is the class embeddable? + embeddable: false - # Type namespaces - namespaces: - # The namespace for the generated class (override any other defined namespace) - class: null + # 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 + # The namespace for the generated interface (override any other defined namespace) + interface: null - # Attributes (merged with generated attributes) - attributes: [] + # Attributes (merged with generated attributes) + attributes: [] - # The parent class, set to false for a top level class - parent: false + # 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 + # If declaring a custom class, this will be the class from which properties type will be guessed + guessFrom: Thing - # Operations for the class - operations: [] + # Operations for the class + operations: [] - # Import all existing properties - allProperties: false + # 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 + # 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" + # The property range + range: null # Example: Offer + cardinality: unknown # One of "(0..1)"; "(0..*)"; "(1..1)"; "(1..*)"; "(*..0)"; "(*..1)"; "(*..*)"; "unknown" - # Symfony Serialization Groups - groups: [] + # Symfony Serialization Groups + groups: [] - # The doctrine mapped by attribute - mappedBy: null # Example: partOfSeason + # The doctrine mapped by attribute + mappedBy: null # Example: partOfSeason - # The doctrine inversed by attribute - inversedBy: null # Example: episodes + # The doctrine inversed by attribute + inversedBy: null # Example: episodes - # Is the property readable? - readable: true + # Is the property readable? + readable: true - # Is the property writable? - writable: 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 nullable? (if null, cardinality will be used: will be true if no cardinality found) + nullable: null - # Is the property required? - required: true + # Is the property required? + required: true - # The property unique - unique: false + # The property unique + unique: false - # Is the property embedded? - embedded: false + # Is the property embedded? + embedded: false - # Attributes (merged with generated attributes) - attributes: [] + # Attributes (merged with generated attributes) + attributes: [] # Annotation generators to use annotationGenerators: - # Default: - - ApiPlatform\SchemaGenerator\AnnotationGenerator\PhpDocAnnotationGenerator + # 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 + # 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: [] diff --git a/schema-generator/getting-started.md b/schema-generator/getting-started.md index f8ff1453baf..c5e5621af83 100644 --- a/schema-generator/getting-started.md +++ b/schema-generator/getting-started.md @@ -2,14 +2,15 @@ ## Installation -If you use [the API Platform Distribution with Symfony](../symfony/index.md), the Schema Generator is already installed -as a development dependency of your project and can be invoked with: +If you use [the API Platform Distribution with Symfony](../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) +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 @@ -18,16 +19,19 @@ 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)). +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/). +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. @@ -40,42 +44,44 @@ Here we will generate a data model for an address book with the following data: # 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: ~ + # 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. +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 + OfferItemCondition: # The generator will automatically guess that OfferItemCondition is subclass of Enum + properties: {} # Remove all properties of the parent class ``` ### OpenAPI Generation @@ -90,7 +96,7 @@ Write the following config file: ```yaml # api/config/schema.yaml openApi: - file: '../openapi.yaml' + file: "../openapi.yaml" ``` ## Usage @@ -107,8 +113,8 @@ Using [the API Platform Symfony variant](../symfony/index.md): 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. +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. @@ -116,7 +122,8 @@ Without configuration file, the tool will build the entire Schema.org vocabulary 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. +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. @@ -126,10 +133,11 @@ 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`. +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: diff --git a/schema-generator/index.md b/schema-generator/index.md index 365674309a2..e18cff64fc5 100644 --- a/schema-generator/index.md +++ b/schema-generator/index.md @@ -1,70 +1,90 @@ # 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! +`schema` is a command-line tool part of [the API Platform framework](https://api-platform.com) that +instantly generates a set of PHP classes from +[RDF](https://en.wikipedia.org/wiki/Resource_Description_Framework) vocabularies such as (but not +limited to) [Schema.org](https://schema.org) or +[ActivityStreams](https://www.w3.org/TR/activitystreams-core/). Alternatively, it can generate PHP +classes from an [OpenAPI](https://www.openapis.org/) documentation. + +[Find and browse](https://lov.linkeddata.es/) (or create) a vocabulary that fits your needs, choose +the types and properties you need, run our code generator and you're done! Alternatively, design +your API with tools like [Stoplight](https://stoplight.io/), export the OpenAPI documentation, run +our code generator and your API is ready! ![Stoplight](images/stoplight.png) You get a fully featured PHP data model including: -- A set of PHP entities with properties, constants (enum values), getters, setters, adders and removers. The class - hierarchy provided by the vocabulary will be translated to a PHP class hierarchy with parents as `abstract` classes. - The generated code complies with [PSR](https://www.php-fig.org/) coding standards; -- Full, high-quality PHPDoc and type declarations for classes, properties, constants and methods extracted from the vocabulary; -- Doctrine ORM or MongoDB ODM attributes mapping including database columns / fields with type guessing, relations with cardinality guessing, - smart class inheritance (through the `#[MappedSuperclass]` or `#[InheritanceType]` attributes depending on if the resource is used in a relation); -- Data validation through [Symfony Validator](https://symfony.com/doc/current/book/validation.html) attributes including enum support (choices) and check for required properties; +- 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) +- 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. +- 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/)). +- 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](<https://en.wikipedia.org/wiki/Microdata_(HTML)>) 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. +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](<https://en.wikipedia.org/wiki/Microdata_(HTML)>) 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. +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. +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). +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. +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. +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 diff --git a/symfony/caddy.md b/symfony/caddy.md index ce701f9cf90..9e10127cfa9 100644 --- a/symfony/caddy.md +++ b/symfony/caddy.md @@ -1,28 +1,32 @@ # Configuring the Caddy Web Server with Symfony -[The API Platform distribution](index.md) 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. +[The API Platform distribution](index.md) 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. -It routes requests to either service depending on the value of the `Accept` HTTP header or the extension -of the requested file. +Caddy is positioned in front of the web API and of the Progressive Web App. It routes requests to +either service depending on the value of the `Accept` HTTP header or the extension of the requested +file. -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/). +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/). -By default, requests having an `Accept` request header containing the `text/html` media type are routed to the Next.js application, -except for some paths known to be resources served by the API (e.g. the Swagger UI documentation, static files provided by bundles...). -Other requests are routed to the API. +By default, requests having an `Accept` request header containing the `text/html` media type are +routed to the Next.js application, except for some paths known to be resources served by the API +(e.g. the Swagger UI documentation, static files provided by bundles...). Other requests are routed +to the API. -Sometimes, you may want to let the PHP application generate HTML responses. -For instance, when you create your own Symfony controllers serving HTML pages, -or when using bundles such as EasyAdmin or SonataAdmin. +Sometimes, you may want to let the PHP application generate HTML responses. For instance, when you +create your own Symfony controllers serving HTML pages, or when using bundles such as EasyAdmin or +SonataAdmin. -To do so, you have to tweak the rules used to route the requests. -Open `api-platform/api/frankenphp/Caddyfile` and modify the expression. -You can use [any CEL (Common Expression Language) expression](https://caddyserver.com/docs/caddyfile/matchers#expression) supported by Caddy. +To do so, you have to tweak the rules used to route the requests. Open +`api-platform/api/frankenphp/Caddyfile` and modify the expression. You can use +[any CEL (Common Expression Language) expression](https://caddyserver.com/docs/caddyfile/matchers#expression) +supported by Caddy. -For instance, if you want to route all requests to a path starting with `/admin` to the API, modify the existing expression like this: +For instance, if you want to route all requests to a path starting with `/admin` to the API, modify +the existing expression like this: ```patch # Matches requests for HTML documents, for static files and for Next.js files, diff --git a/symfony/controllers.md b/symfony/controllers.md index caf6613fe7d..301ccc4937b 100644 --- a/symfony/controllers.md +++ b/symfony/controllers.md @@ -1,47 +1,57 @@ # Creating Custom Operations and Symfony Controllers -> [!NOTE] -> Using custom Symfony controllers with API Platform is **discouraged**. Also, GraphQL is **not supported**. +> [!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) +> 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 + 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 +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 distribution of API Platform also eases the implementation of the ADR pattern: it automatically registers action classes -stored in `api/src/Controller` as autowired services. +The distribution of API Platform 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. +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`. +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. +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 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. +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: @@ -70,31 +80,35 @@ class CreateBookPublication extends AbstractController } ``` -This custom operation behaves exactly like the built-in operation: it returns a JSON-LD document corresponding to the ID -passed in the URL. +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 distribution). -This action will be automatically registered as a service (the service name is the same as the class name: +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 distribution). 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. +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)`. +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. +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 `__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: @@ -127,14 +141,14 @@ 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: App\Controller\CreateBookPublication + App\Entity\Book: + operations: + ApiPlatform\Metadata\Get: ~ + post_publication: + class: ApiPlatform\Metadata\Post + method: POST + uriTemplate: /books/{id}/publication + controller: App\Controller\CreateBookPublication ``` ```xml @@ -158,16 +172,18 @@ resources: </code-selector> -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. +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. +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). +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: @@ -199,14 +215,14 @@ 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 + App\Entity\Book: + operations: + ApiPlatform\Metadata\Get: ~ + post_publication: + class: ApiPlatform\Metadata\Post + method: POST + uriTemplate: /books/{id}/publication + controller: ApiPlatform\Action\PlaceholderAction ``` ```xml @@ -232,7 +248,8 @@ resources: ## Using Serialization Groups -You may want different serialization groups for your custom operations. Just configure the proper `normalizationContext` and/or `denormalizationContext` in your operation: +You may want different serialization groups for your custom operations. Just configure the proper +`normalizationContext` and/or `denormalizationContext` in your operation: <code-selector> @@ -270,15 +287,15 @@ class Book ```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'] + 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 @@ -309,8 +326,8 @@ resources: ## 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: +If you want to bypass the automatic retrieval of the entity in your custom operation, you can set +`read: false` in the operation attribute: <code-selector> @@ -342,14 +359,14 @@ class Book ```yaml # api/config/api_platform/resources.yaml resources: - App\Entity\Book: - operations: - ApiPlatform\Metadata\Get: ~ - post_publication: - class: ApiPlatform\Metadata\Post - uriTemplate: /books/{id}/publication - controller: App\Controller\CreateBookPublication - read: false + App\Entity\Book: + operations: + ApiPlatform\Metadata\Get: ~ + post_publication: + class: ApiPlatform\Metadata\Post + uriTemplate: /books/{id}/publication + controller: App\Controller\CreateBookPublication + read: false ``` ```xml @@ -372,21 +389,22 @@ resources: </code-selector> -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. +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)`. +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. +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. +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: @@ -415,14 +433,14 @@ class Book ```yaml # api/config/api_platform/resources.yaml resources: - App\Entity\Book: - operations: - ApiPlatform\Metadata\Get: ~ - post_publication: - class: ApiPlatform\Metadata\Post - routeName: book_post_publication - book_post_discontinuation: - class: ApiPlatform\Metadata\Post + App\Entity\Book: + operations: + ApiPlatform\Metadata\Get: ~ + post_publication: + class: ApiPlatform\Metadata\Post + routeName: book_post_publication + book_post_discontinuation: + class: ApiPlatform\Metadata\Post ``` ```xml @@ -445,8 +463,8 @@ resources: </code-selector> -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: +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 <?php @@ -483,10 +501,11 @@ class CreateBookPublication extends AbstractController } ``` -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. +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: +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 <?php @@ -510,10 +529,10 @@ class BookController extends AbstractController ```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 + path: /books/{id}/publication + methods: ["POST"] + defaults: + _controller: App\Controller\BookController::createPublication + _api_resource_class: App\Entity\Book + _api_operation_name: post_publication ``` diff --git a/symfony/debugging.md b/symfony/debugging.md index e69008eedf1..9e8045d93d5 100644 --- a/symfony/debugging.md +++ b/symfony/debugging.md @@ -15,63 +15,69 @@ 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): +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 + - 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. +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. +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: + Example: - ```console - XDEBUG_SESSION=1 PHP_IDE_CONFIG="serverName=api" php bin/console ... - ``` + ```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. +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" - } - } - ] + "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. +> 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. +Inspect the installation with the following command. The requested Xdebug version should be +displayed in the output. ```console $ docker compose exec php \ diff --git a/symfony/file-upload.md b/symfony/file-upload.md index 471c5288af4..208a1498276 100644 --- a/symfony/file-upload.md +++ b/symfony/file-upload.md @@ -1,22 +1,22 @@ # 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) +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. +> [!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'] + formats: + jsonld: ["application/ld+json"] + multipart: ["multipart/form-data"] ``` ## Installing VichUploaderBundle @@ -27,29 +27,28 @@ Install the bundle with the help of Composer: 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. +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 + 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`). +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 @@ -128,14 +127,17 @@ class MediaObject } ``` -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. +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. +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: +A [normalizer](../core/serialization.md#normalization) could be used to set the `contentUrl` +property: ```php <?php @@ -191,8 +193,8 @@ class MediaObjectNormalizer implements NormalizerInterface ### 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: +By default, Symfony is not able to decode `multipart/form-data`-encoded data. We need to create our +own decoder to do it: ```php <?php @@ -232,7 +234,8 @@ final class MultipartDecoder implements DecoderInterface } ``` -If you're not using `autowiring` and `autoconfiguring`, don't forget to register the service and tag it as `serializer.encoder`. +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: @@ -266,26 +269,28 @@ final class UploadedFileDenormalizer implements DenormalizerInterface } ``` -If you're not using `autowiring` and `autoconfiguring`, don't forget to register the service and tag it as `serializer.normalizer`. +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: +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/<id>", - "contentUrl": "<url>" + "@type": "https://schema.org/MediaObject", + "@id": "/media_objects/<id>", + "contentUrl": "<url>" } ``` ### 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/|`: +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/|`: <!-- markdownlint-disable no-hard-tabs --> @@ -307,8 +312,8 @@ You will need to modify your `Caddyfile` to allow the above `contentUrl` to be a ### 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 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`. @@ -339,20 +344,19 @@ class Book } ``` -By sending a POST request to create a new book, linked with the previously -uploaded cover, you can have a nice illustrated book record! +By sending a POST request to create a new book, linked with the previously uploaded cover, you can +have a nice illustrated book record! `POST /books` ```json { - "name": "The name", - "image": "/media_objects/<id>" + "name": "The name", + "image": "/media_objects/<id>" } ``` -Voilà! You can now send files to your API, and link them to any other resource -in your app. +Voilà! You can now send files to your API, and link them to any other resource in your app. ### Testing @@ -402,13 +406,13 @@ class MediaObjectTest extends ApiTestCase ## 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. +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. +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. +> [!WARNING] Make sure to encode the fields in JSON before sending them. For instance, you could do something like this: @@ -416,18 +420,18 @@ For instance, you could do something like this: async function uploadBook(file) { const bookMetadata = { title: "API Platform Best Practices", - genre: "Programming" + genre: "Programming", }; const formData = new FormData(); for (const [name, value] of Object.entries(bookMetadata)) { formData.append(name, JSON.stringify(value)); } - formData.append('file', file); + formData.append("file", file); - const response = await fetch('https://my-api.com/books', { - method: 'POST', - body: formData + const response = await fetch("https://my-api.com/books", { + method: "POST", + body: formData, }); const result = await response.json(); diff --git a/symfony/fosuser-bundle.md b/symfony/fosuser-bundle.md index ce8378ad8b4..1dc12951ca5 100644 --- a/symfony/fosuser-bundle.md +++ b/symfony/fosuser-bundle.md @@ -1,37 +1,45 @@ # 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). +> [!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). +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) +- 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) +- 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: +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 } + 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). +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: diff --git a/symfony/index.md b/symfony/index.md index c9a93268884..47f252b19d1 100644 --- a/symfony/index.md +++ b/symfony/index.md @@ -6,71 +6,108 @@ > > —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. +[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 download the API Platform distribution](https://github.com/api-platform/api-platform/releases). It contains: - -- 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)) -- [the client scaffolding tool](../create-client/index.md) to generate [Next.js](../create-client/index.md) web applications from the API documentation ([Nuxt](https://nuxt.com/), [Vue](https://vuejs.org/), [Create React App](https://reactjs.org), [React Native](https://reactnative.dev/), [Quasar](https://quasar.dev/) and [Vuetify](https://vuetifyjs.com/) are also supported) -- [a beautiful admin interface](../admin/index.md), built on top of React Admin, dynamically created by parsing the API documentation +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 download the API Platform distribution](https://github.com/api-platform/api-platform/releases). +It contains: + +- 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)) +- [the client scaffolding tool](../create-client/index.md) to generate + [Next.js](../create-client/index.md) web applications from the API documentation + ([Nuxt](https://nuxt.com/), [Vue](https://vuejs.org/), [Create React App](https://reactjs.org), + [React Native](https://reactnative.dev/), [Quasar](https://quasar.dev/) and + [Vuetify](https://vuetifyjs.com/) are also supported) +- [a beautiful admin interface](../admin/index.md), built on top of React Admin, dynamically created + by parsing the API documentation - 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 [Helm](https://helm.sh/) chart to deploy the API in any [Kubernetes](../deployment/kubernetes.md) cluster +- 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 [Helm](https://helm.sh/) chart to deploy the API in any + [Kubernetes](../deployment/kubernetes.md) cluster ## 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_. +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: +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)...) +- 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) +- 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)) +- 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 API Platform distribution includes [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. +One more thing, before we start: as the API Platform distribution includes +[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 Distribution (Recommended) -Start by [downloading the API Platform distribution](https://github.com/api-platform/api-platform/releases/latest), or [generate a GitHub repository from the template we provide](https://github.com/new?template_name=api-platform&template_owner=api-platform). +Start by +[downloading the API Platform distribution](https://github.com/api-platform/api-platform/releases/latest), +or +[generate a GitHub repository from the template we provide](https://github.com/new?template_name=api-platform&template_owner=api-platform). You will add your own code and configuration inside this skeleton. -**Note**: Avoid downloading the `.zip` archive, as it may cause potential [permission](https://github.com/api-platform/api-platform/issues/319#issuecomment-307037562) [issues](https://github.com/api-platform/api-platform/issues/777#issuecomment-412515342), prefer the `.tar.gz` archive. +**Note**: Avoid downloading the `.zip` archive, as it may cause potential +[permission](https://github.com/api-platform/api-platform/issues/319#issuecomment-307037562) +[issues](https://github.com/api-platform/api-platform/issues/777#issuecomment-412515342), prefer the +`.tar.gz` archive. -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/). +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. +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 directory containing your project skeleton. Run the following command to start all -services using [Docker Compose](https://docs.docker.com/compose/): +Open a terminal, and navigate to the directory containing your project skeleton. Run the following +command to start all services using [Docker Compose](https://docs.docker.com/compose/): Build the images: @@ -86,15 +123,19 @@ 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. +> 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: +> 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. +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: @@ -120,53 +161,60 @@ 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 API Platform distribution comes with a dummy entity for test purposes: `api/src/Entity/Greeting.php`. We will remove -it later. - -If you're used to the PHP ecosystem, you probably guessed that this test entity uses the industry-leading [Doctrine ORM](https://www.doctrine-project.org/projects/orm.html) -library as a persistence system. It is shipped, in the API Platform distribution. - -Doctrine ORM is the easiest way to persist and query data in an API Platform project thanks to the bridge shipped with the -distribution, 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 <command>`. +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 API Platform distribution comes with a dummy entity for test purposes: +`api/src/Entity/Greeting.php`. We will remove it later. + +If you're used to the PHP ecosystem, you probably guessed that this test entity uses the +industry-leading [Doctrine ORM](https://www.doctrine-project.org/projects/orm.html) library as a +persistence system. It is shipped, in the API Platform distribution. + +Doctrine ORM is the easiest way to persist and query data in an API Platform project thanks to the +bridge shipped with the distribution, 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 <command>`. ### 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.** +**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 without the distribution on SymfonyCasts](https://symfonycasts.com/screencast/api-platform/install?cid=apip). -The rest of this tutorial assumes that you have installed API Platform using the official distribution. Go straight to the -next section if it's your case. +The rest of this tutorial assumes that you have installed API Platform using the official +distribution. 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): +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: @@ -199,11 +247,16 @@ And start the built-in PHP server: 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). +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. +**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 @@ -211,39 +264,47 @@ Open `https://localhost` in your favorite web browser: ![The welcome page](images/api-platform-3.0-welcome.png) -You'll need to add a security exception in your browser to accept the self-signed TLS certificate that has been generated -for this container when installing the framework. +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. -Later you will probably replace this welcome screen by the homepage of your Next.js application. If you don't plan to create -a Progressive Web App, you can remove the `pwa/` directory as well as the related lines in `docker-compose*.yml` and in `api/frankenphp/Caddyfile` (don't do it -now, we'll use this container later in this tutorial). +Later you will probably replace this welcome screen by the homepage of your Next.js application. If +you don't plan to create a Progressive Web App, you can remove the `pwa/` directory as well as the +related lines in `docker-compose*.yml` and in `api/frankenphp/Caddyfile` (don't do it now, we'll use +this container later in this tutorial). Click on the "API" button, or go to `https://localhost/docs/`: ![The API](images/api-platform-2.6-api.png) -API Platform exposes a description of the API in the [OpenAPI](https://www.openapis.org/) format (formerly known as Swagger). -It also integrates a customized version of [Swagger UI](https://swagger.io/swagger-ui/), a nice interface rendering the -OpenAPI documentation. -Click on an operation to display its details. You can also send requests to the API directly from the UI. -Try to create a new _Greeting_ resource using the `POST` operation, then access it using the `GET` operation and, finally, -delete it by executing the `DELETE` operation. -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)). +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 create a new _Greeting_ resource using the `POST` operation, then access it using the +`GET` operation and, finally, delete it by executing the `DELETE` operation. 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 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. +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. +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 Project in Sync with the API Platform Template -You have started a project with the API Platform template and you would like to benefit from the latest enhancements introduced since you created your project (i.e. [FrankenPHP](https://frankenphp.dev/)). Just use this Git based tool +You have started a project with the API Platform template and you would like to benefit from the +latest enhancements introduced since you created your project (i.e. +[FrankenPHP](https://frankenphp.dev/)). Just use this Git based tool [The _template-sync_ project](https://github.com/coopTilleuls/template-sync) got you covered. Run the following command to import the changes since your last update: @@ -254,15 +315,17 @@ curl -sSL https://raw.githubusercontent.com/coopTilleuls/template-sync/main/temp 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) +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. +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. +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): @@ -347,23 +410,33 @@ class Review } ``` -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. +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). +Reload `https://localhost/docs/`: API Platform used these classes to generate an OpenAPI +documentation (a Hydra documentation is also exposed), and registered for us +[the typical REST routes](../core/operations.md). ![The bookshop API](images/api-platform-2.6-bookshop-api.png) -Operations available for our 2 resource types appear in the UI. We can also see the awesome [Web Debug Toolbar](https://symfonycasts.com/screencast/symfony/profiler?cid=apip). +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. +Note that the entities' and properties' descriptions are present in the API documentation, and that +API Platform uses PHP types to generate the appropriate JSON Schemas. ![Bookshop JSON Schemas](images/api-platform-2.6-bookshop-json-schemas.png) -The framework also uses these metadata to serialize and deserialize data from JSON (and other formats) to PHP objects (back and forth)! +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. +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! @@ -373,12 +446,23 @@ The only remaining task to have a working API is to be able to query and persist 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. +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. @@ -469,22 +553,32 @@ Modify these files as described in these patches: 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: +**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 ``` -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: +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 @@ -493,72 +587,88 @@ 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: +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" + "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. +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)! +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. +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" + "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"` +**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. +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: @@ -574,19 +684,23 @@ Now try to add another book by issuing a `POST` request to `/books` with the fol ```json { - "isbn": "2815840053", - "description": "Hello", - "author": "Me", - "publicationDate": "today" + "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. +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. +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: @@ -649,45 +763,46 @@ Modify the following files as described in these patches: 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. +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." - } - ] + "@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... +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: +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: +You now have a GraphQL API! Open `https://localhost/graphql` (or `https://localhost/api/graphql` if +you used Symfony Flex to install API Platform) to play with it using the nice +[GraphiQL](https://github.com/graphql/graphiql) UI that is shipped with API Platform: ![GraphQL endpoint](images/api-platform-2.6-graphql.png) @@ -695,18 +810,20 @@ 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 + 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 + } } - } } ``` @@ -714,11 +831,11 @@ And by reading out the book: ```graphql { - book(id: "/books/2") { - id - title - _id - } + book(id: "/books/2") { + id + title + _id + } } ``` @@ -726,32 +843,33 @@ You can also try things a bit more complex: ```graphql { - books { - totalCount - edges { - node { - id - title - reviews { - totalCount - edges { + books { + totalCount + edges { node { - author - rating + 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 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 @@ -762,19 +880,25 @@ Open `https://localhost/admin/` in your browser: ![The admin](images/api-platform-2.6-admin.png) -This [Material Design](https://material.io/guidelines/) admin is a [Progressive Web App](https://developers.google.com/web/progressive-web-apps/) -built with [API Platform Admin](../admin/index.md) ([React Admin](https://marmelab.com/react-admin/) inside!). It is powerful and fully customizable. -Refer to its documentation to learn more. -It leverages the Hydra documentation exposed by the API component to build itself. It's 100% dynamic - **no code generation -occurs**. +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. +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 distribution comes with a skeleton ready to welcome the [Next.js](https://nextjs.org/) flavor of the generated code. To bootstrap your app, run: +The distribution comes with a skeleton ready to welcome the [Next.js](https://nextjs.org/) flavor of +the generated code. To bootstrap your app, run: ```console docker compose exec pwa \ @@ -785,42 +909,53 @@ Open `https://localhost/books/` in your browser: ![The Next.js Progressive Web App](images/api-platform-2.6-pwa-react.png) -You can also choose to generate the code for a specific resource with the `--resource` argument (example: -`pnpm create @api-platform/client --resource books`). +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. +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). +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! +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). +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. +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 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). +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). +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 <p align="center" class="symfonycasts"><a href="https://symfonycasts.com/tracks/rest?cid=apip#api-platform-3"><img src="images/symfonycasts-player.png" alt="SymfonyCasts, API Platform screencasts"></a></p> -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)! +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)! diff --git a/symfony/jwt.md b/symfony/jwt.md index cfb114dbf76..fd1d3fda593 100644 --- a/symfony/jwt.md +++ b/symfony/jwt.md @@ -1,16 +1,16 @@ # 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. +> [!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. <p class="symfonycasts" align="center"><a href="https://symfonycasts.com/screencast/symfony-rest4/json-web-token?cid=apip"><img src="../symfony/images/symfonycasts-player.png" alt="JWT screencast"><br>Watch the LexikJWTAuthenticationBundle screencast</a></p> ## Installing LexikJWTAuthenticationBundle -> [!NOTE] -> API Platform makes it easy to add JWT-based authentication to your API using [LexikJWTAuthenticationBundle](https://github.com/lexik/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: @@ -26,7 +26,8 @@ You can generate them by using this command: php bin/console lexik:jwt:generate-keypair ``` -Or if you're using the [API Platform distribution with Symfony](../symfony/index.md), you may run this from the project's root directory: +Or if you're using the [API Platform distribution with Symfony](../symfony/index.md), you may run +this from the project's root directory: ```console docker compose exec php sh -c ' @@ -38,77 +39,84 @@ docker compose exec php sh -c ' ' ``` -Note that the `setfacl` command relies on the `acl` package. This is installed by default when using the API Platform -docker distribution but may need to be installed in your working environment in order to execute the `setfacl` command. +Note that the `setfacl` command relies on the `acl` package. This is installed by default when using +the API Platform docker distribution 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. +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. +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. +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) +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**). +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). +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 } + # 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`: @@ -116,57 +124,63 @@ You must also declare the route used for `/auth`: ```yaml # api/config/routes.yaml auth: - path: /auth - methods: ['POST'] + 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). +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). +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: +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 } + # 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 @@ -174,9 +188,9 @@ security: ```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)%' + 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 @@ -188,11 +202,11 @@ Want to test the routes of your JWT-authentication-protected API? ```yaml # api/config/packages/api_platform.yaml api_platform: - swagger: - api_keys: - JWT: - name: Authorization - type: header + swagger: + api_keys: + JWT: + name: Authorization + type: header ``` The "Authorize" button will automatically appear in Swagger UI. @@ -201,9 +215,11 @@ 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. +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` @@ -211,19 +227,19 @@ You must set the [JWT token](https://github.com/lexik/LexikJWTAuthenticationBund ### 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. +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 + # ... + api_platform: + check_path: /auth + username_path: email + password_path: password ``` You will see something like this in Swagger UI: @@ -291,23 +307,29 @@ Refer to [Testing the API](../symfony/testing.md) for more information about tes ### 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. +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')`) +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. +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. +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 + password_hashers: + App\Entity\User: + algorithm: md5 + encode_as_base64: false + iterations: 0 ``` diff --git a/symfony/messenger.md b/symfony/messenger.md index 726e8e68636..9076597ba24 100644 --- a/symfony/messenger.md +++ b/symfony/messenger.md @@ -1,11 +1,15 @@ # 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). +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. +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. +Many transports are supported to dispatch messages to async consumers, including RabbitMQ, Apache +Kafka, Amazon SQS and Google Pub/Sub. ## Installing Symfony Messenger @@ -17,7 +21,9 @@ 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: +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: <code-selector> @@ -47,28 +53,38 @@ final class Person ```yaml # api/config/api_platform/resources.yaml resources: - App\Entity\Person: - operations: - ApiPlatform\Metadata\Post: - status: 202 - messenger: true - output: false + App\Entity\Person: + operations: + ApiPlatform\Metadata\Post: + status: 202 + messenger: true + output: false ``` </code-selector> -Because the `messenger` attribute is `true`, when a `POST` is handled by API Platform, the corresponding instance of the `Person` will be dispatched. +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. +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. +**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: +To process the message that will be dispatched, +[a handler](https://symfony.com/doc/current/messenger.html#registering-handlers) must be created: ```php <?php @@ -91,25 +107,36 @@ final class PersonHandler That's all! -By default, the handler will process your message synchronously. -If you want it to be consumed asynchronously (e.g. by a worker machine), [configure a transport and the consumer](https://symfony.com/doc/current/messenger.html#transports). +By default, the handler will process your message synchronously. If you want it to be consumed +asynchronously (e.g. by a worker machine), +[configure a transport and the consumer](https://symfony.com/doc/current/messenger.html#transports). ## Accessing the Data Returned by the Handler -API Platform automatically uses the `Symfony\Component\Messenger\Stamp\HandledStamp` when set. -It means that if you use a synchronous handler, the data returned by the `__invoke` method replaces the original data. +API Platform automatically uses the `Symfony\Component\Messenger\Stamp\HandledStamp` when set. It +means that if you use a synchronous handler, the data returned by the `__invoke` method replaces the +original data. -In cases where multiple handlers are registered, the last handler return value will be used as output. If none are returned, ensure resource configuration defines no output with `output=false`. -Handler ordering can be configured [using messenger priority tag](https://symfony.com/doc/current/messenger.html#manually-configuring-handlers). +In cases where multiple handlers are registered, the last handler return value will be used as +output. If none are returned, ensure resource configuration defines no output with `output=false`. +Handler ordering can be configured +[using messenger priority tag](https://symfony.com/doc/current/messenger.html#manually-configuring-handlers). ## Detecting Removals -When a `DELETE` operation occurs, API Platform automatically adds a `ApiPlatform\Symfony\Messenger\RemoveStamp` ["stamp"](https://symfony.com/doc/current/components/messenger.html#adding-metadata-to-messages-envelopes) instance to the "envelope". -To differentiate typical persists calls (create and update) and removal calls, check for the presence of this stamp using [a custom "middleware"](https://symfony.com/doc/current/components/messenger.html#adding-metadata-to-messages-envelopes). +When a `DELETE` operation occurs, API Platform automatically adds a +`ApiPlatform\Symfony\Messenger\RemoveStamp` +["stamp"](https://symfony.com/doc/current/components/messenger.html#adding-metadata-to-messages-envelopes) +instance to the "envelope". To differentiate typical persists calls (create and update) and removal +calls, check for the presence of this stamp using +[a custom "middleware"](https://symfony.com/doc/current/components/messenger.html#adding-metadata-to-messages-envelopes). ## Using Messenger with an Input Object -Set the `messenger` attribute to `input`, and API Platform will automatically dispatch the given Input as a message instead of the Resource. Indeed, it'll add a default `DataTransformer` ([see input/output documentation](../core/dto.md)) that handles the given `input`. In this example, we'll handle a `ResetPasswordRequest` on a custom operation on our `User` resource: +Set the `messenger` attribute to `input`, and API Platform will automatically dispatch the given +Input as a message instead of the Resource. Indeed, it'll add a default `DataTransformer` +([see input/output documentation](../core/dto.md)) that handles the given `input`. In this example, +we'll handle a `ResetPasswordRequest` on a custom operation on our `User` resource: ```php <?php @@ -154,11 +181,16 @@ final class ResetPasswordRequest } ``` -As above, 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. +As above, 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. -In this case, when a `POST` request is issued on `/users/reset_password` the message handler will receive an `App\Dto\ResetPasswordRequest` object instead of a `User` because we specified it as `input` and set `messenger=input`: +In this case, when a `POST` request is issued on `/users/reset_password` the message handler will +receive an `App\Dto\ResetPasswordRequest` object instead of a `User` because we specified it as +`input` and set `messenger=input`: ```php <?php diff --git a/symfony/migrate-from-fosrestbundle.md b/symfony/migrate-from-fosrestbundle.md index 0360b9bc836..d5cda0f0d60 100644 --- a/symfony/migrate-from-fosrestbundle.md +++ b/symfony/migrate-from-fosrestbundle.md @@ -1,27 +1,34 @@ # 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. +[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**. +> 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. +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`. +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). +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. +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). @@ -33,9 +40,13 @@ 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. +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. +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). @@ -45,11 +56,13 @@ See [General Design Considerations](../core/design.md). 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). +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. +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). @@ -57,13 +70,16 @@ See [Operations](../core/operations.md). #### 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. +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). +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. +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. @@ -73,15 +89,18 @@ See [Extending API Platform](../core/extending.md) for more details. 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. +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). +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. +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). @@ -91,17 +110,23 @@ See [Content negotiation](../core/content-negotiation.md). 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`. +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). +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. +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). +See +[_Name Conversion_ in The Serialization Process](../core/serialization.md#name-conversion-for-symfony). ### Handle errors @@ -109,7 +134,8 @@ See [_Name Conversion_ in The Serialization Process](../core/serialization.md#na 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). +See +[ExceptionController support](https://github.com/FriendsOfSymfony/FOSRestBundle/blob/3.x/Resources/doc/4-exception-controller-support.rst). #### In API Platform (Error handling) @@ -121,11 +147,15 @@ See [Errors Handling](../core/errors.md). #### In FOSRestBundle (Security) -Use [Symfony's Security component](https://symfony.com/doc/current/security) to control your API access. +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. +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. @@ -135,12 +165,16 @@ See [Security](../core/security.md). #### 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. +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). +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. +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). diff --git a/symfony/nelmio-api-doc.md b/symfony/nelmio-api-doc.md index b9d86373982..456861ce10a 100644 --- a/symfony/nelmio-api-doc.md +++ b/symfony/nelmio-api-doc.md @@ -1,36 +1,37 @@ # NelmioApiDocBundle Integration with Symfony -> [!WARNING] -> For new projects, prefer using the built-in Swagger support and/or NelmioApiDoc 3. +> [!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. +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. +As NelmioApiDocBundle 3+ has built-in support for API Platform, this documentation is only relevant +for people using NelmioApiDocBundle between version 2.9 and 3.0. ![Screenshot of API Platform integrated with NelmioApiDocBundle](images/NelmioApiDocBundle.png) -[NelmioApiDocBundle](https://github.com/nelmio/NelmioApiDocBundle) is supported by API Platform since version 2.9. +[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 + 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' + 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. +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. diff --git a/symfony/security.md b/symfony/security.md index cd7d471d2b9..005ff7d6b7e 100644 --- a/symfony/security.md +++ b/symfony/security.md @@ -1,8 +1,12 @@ # Security with Symfony -The API Platform security layer is built on top of the [Symfony Security component](https://symfony.com/doc/current/security.html). -All its features, including [global access control directives](https://symfony.com/doc/current/security.html#securing-url-patterns-access-control) are supported. -API Platform also provides convenient [access control expressions](https://symfony.com/doc/current/expressions.html#security-complex-access-controls-with-expressions) which you can apply at resource and operation level. +The API Platform security layer is built on top of the +[Symfony Security component](https://symfony.com/doc/current/security.html). All its features, +including +[global access control directives](https://symfony.com/doc/current/security.html#securing-url-patterns-access-control) +are supported. API Platform also provides convenient +[access control expressions](https://symfony.com/doc/current/expressions.html#security-complex-access-controls-with-expressions) +which you can apply at resource and operation level. <p align="center" class="symfonycasts"><a href="https://symfonycasts.com/screencast/api-platform-security/?cid=apip"><img src="../symfony/images/symfonycasts-player.png" alt="Security screencast"><br>Watch the Security screencast</a></p> @@ -49,15 +53,15 @@ class Book ```yaml # api/config/api_platform/resources.yaml resources: - App\Entity\Book: - security: 'is_granted("ROLE_USER")' - operations: - ApiPlatform\Metadata\GetCollection: ~ - ApiPlatform\Metadata\Post: - security: 'is_granted("ROLE_ADMIN")' - ApiPlatform\Metadata\Get: ~ - ApiPlatform\Metadata\Put: - security: 'is_granted("ROLE_ADMIN") or object.owner == user' + App\Entity\Book: + security: 'is_granted("ROLE_USER")' + operations: + ApiPlatform\Metadata\GetCollection: ~ + ApiPlatform\Metadata\Post: + security: 'is_granted("ROLE_ADMIN")' + ApiPlatform\Metadata\Get: ~ + ApiPlatform\Metadata\Put: + security: 'is_granted("ROLE_ADMIN") or object.owner == user' ``` </code-selector> @@ -88,9 +92,9 @@ class Book ```yaml # api/config/api_platform/resources/Book.yaml properties: - App\Entity\Book: - adminOnlyProperty: - security: 'is_granted("ROLE_ADMIN")' + App\Entity\Book: + adminOnlyProperty: + security: 'is_granted("ROLE_ADMIN")' ``` </code-selector> @@ -98,25 +102,33 @@ properties: 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 +- 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 +- `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). +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: +In some cases, it might be useful to execute a security after the denormalization step. To do so, +use the `securityPostDenormalize` attribute: <code-selector> @@ -141,31 +153,40 @@ 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)" - # ... + App\Entity\Book: + operations: + ApiPlatform\Metadata\Get: ~ + ApiPlatform\Metadata\GetCollectionPut: + securityPostDenormalize: + "is_granted('ROLE_ADMIN') or (object.owner == user and previous_object.owner == + user)" + # ... ``` </code-selector> -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. +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. +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 +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. +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`: +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`: <code-selector> @@ -191,14 +212,16 @@ 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 - # ... + 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 @@ -227,7 +250,9 @@ resources: ## 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. +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)` @@ -262,23 +287,26 @@ class Book ```yaml # api/config/api_platform/resources/Book.yaml App\Entity\Book: - security: 'is_granted("ROLE_USER")' - operations: - ApiPlatform\Metadata\GetCollection: ~ - ApiPlatform\Metadata\Post: - securityPostDenormalize: 'is_granted("BOOK_CREATE", object)' - ApiPlatform\Metadata\Get: - security: 'is_granted("BOOK_READ", object)' - ApiPlatform\Metadata\Put: - security: 'is_granted("BOOK_EDIT", object)' - ApiPlatform\Metadata\Delete: - security: 'is_granted("BOOK_DELETE", object)' + security: 'is_granted("ROLE_USER")' + operations: + ApiPlatform\Metadata\GetCollection: ~ + ApiPlatform\Metadata\Post: + securityPostDenormalize: 'is_granted("BOOK_CREATE", object)' + ApiPlatform\Metadata\Get: + security: 'is_granted("BOOK_READ", object)' + ApiPlatform\Metadata\Put: + security: 'is_granted("BOOK_EDIT", object)' + ApiPlatform\Metadata\Delete: + security: 'is_granted("BOOK_DELETE", object)' ``` </code-selector> -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. +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: @@ -334,14 +362,19 @@ class BookVoter extends Voter } ``` -_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 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._ +_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. +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: @@ -383,36 +416,47 @@ class Book ```yaml # api/config/api_platform/resources.yaml resources: - App\Entity\Book: - security: 'is_granted("ROLE_USER")' - operations: - ApiPlatform\Metadata\Post: - security: 'is_granted("ROLE_ADMIN")' - securityMessage: 'Only admins can add books.' - ApiPlatform\Metadata\Get: - security: 'is_granted("ROLE_USER") and object.owner == user' - securityMessage: 'Sorry, but you are not the book owner.' - ApiPlatform\Metadata\Put: - securityPostDenormalize: "is_granted('ROLE_ADMIN') or (object.owner == user and previous_object.owner == user)" - securityPostDenormalizeMessage: 'Sorry, but you are not the actual book owner.' - # ... + App\Entity\Book: + security: 'is_granted("ROLE_USER")' + operations: + ApiPlatform\Metadata\Post: + security: 'is_granted("ROLE_ADMIN")' + securityMessage: "Only admins can add books." + ApiPlatform\Metadata\Get: + security: 'is_granted("ROLE_USER") and object.owner == user' + securityMessage: "Sorry, but you are not the book owner." + ApiPlatform\Metadata\Put: + securityPostDenormalize: + "is_granted('ROLE_ADMIN') or (object.owner == user and previous_object.owner == + user)" + securityPostDenormalizeMessage: "Sorry, but you are not the actual book owner." + # ... ``` </code-selector> ## 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. +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. +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. +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. +See +[how to dynamically change](../core/serialization.md#changing-the-serialization-context-dynamically) +the current Serializer context according to the current logged-in user. diff --git a/symfony/testing.md b/symfony/testing.md index f6ed87b22a7..a1e4c7acdd8 100644 --- a/symfony/testing.md +++ b/symfony/testing.md @@ -1,6 +1,7 @@ # 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 +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! @@ -9,22 +10,28 @@ Let's learn how to use tests with Symfony! 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. +- [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. +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): +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! +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): @@ -140,18 +147,28 @@ You can now load your fixtures in the database with the following command: 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. +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). +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 distribution. 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. +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 distribution. 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 distribution they're already installed). +Run `composer require --dev symfony/test-pack symfony/http-client` to install the testing tools +(when using the distribution they're already installed). -Install [DAMADoctrineTestBundle](https://github.com/dmaicher/doctrine-test-bundle) to reset the database automatically before each test: +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 @@ -170,16 +187,18 @@ And activate it in the `phpunit.xml.dist` file: </phpunit> ``` -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: +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. +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): +Here is an example of functional tests specifying the behavior of +[the bookstore API you created in the tutorial](index.md): ```php <?php @@ -326,12 +345,16 @@ publicationDate: This value should not be null.', } ``` -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. +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. +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: @@ -339,63 +362,92 @@ All you have to do now is to run your tests: bin/phpunit ``` -If everything is working properly, you should see `OK (5 tests, 17 assertions)`. -Your REST API is now properly tested! +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. +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). +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. +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 distribution is [shipped with a GitHub Actions workflow](https://github.com/api-platform/api-platform/blob/main/.github/workflows/ci.yml) that builds the Docker images, does a [smoke test](<https://en.wikipedia.org/wiki/Smoke_testing_(software)>) to check that the application's entrypoint is accessible, and runs PHPUnit. +The API Platform distribution is +[shipped with a GitHub Actions workflow](https://github.com/api-platform/api-platform/blob/main/.github/workflows/ci.yml) +that builds the Docker images, does a +[smoke test](<https://en.wikipedia.org/wiki/Smoke_testing_(software)>) 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 [the Helm chart provided with the distribution](../deployment/kubernetes.md) to deploy the app on a Kubernetes cluster. +The API Platform Demo +[contains a CD workflow](https://github.com/api-platform/demo/tree/main/.github/workflows) that uses +[the Helm chart provided with the distribution](../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 in the API Platform distribution): +You may also be interested in these alternative testing tools (not included in the API Platform +distribution): - [Hoppscotch](https://docs.hoppscotch.io/features/tests), create functional test for your API -- [Hoppscotch](https://docs.hoppscotch.io/documentation/features/rest-api-testing/), create functional test 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/cli); -- [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. +- [Hoppscotch](https://docs.hoppscotch.io/documentation/features/rest-api-testing/), create + functional test 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/cli); +- [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. ## Using the API Platform Distribution for 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. +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). +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). +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). <p align="center" class="symfonycasts"><a href="https://symfonycasts.com/screencast/api-platform-security/api-tests?cid=apip"><img src="images/symfonycasts-player.png" alt="Test and Assertions screencast"><br>Watch the API Tests & Assertions screencast</a></p> ### 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). +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: +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 @@ -421,9 +473,13 @@ class BooksTest extends ApiTestCase } ``` -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). +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: +Note that you can create your own test case class extending the ApiTestCase. For example to set up a +Json Web Token authentication: ```php <?php @@ -475,7 +531,8 @@ abstract class AbstractTest extends ApiTestCase } ``` -Use it by extending the `AbstractTest` class. For example this class tests the `/users` resource accessibility where only the admin can retrieve the collection: +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 <?php @@ -505,7 +562,8 @@ final class UsersTest extends AbstractTest ### 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: +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 <?php @@ -566,7 +624,8 @@ class BooksTest extends ApiTestCase ### 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: +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 <?php diff --git a/symfony/user.md b/symfony/user.md index 484b8cf621b..25c30b6f6ba 100644 --- a/symfony/user.md +++ b/symfony/user.md @@ -1,10 +1,15 @@ # User Entity with Symfony -This documentation is based on the [official Symfony Documentation](https://symfony.com/doc/current/security/user_providers.html) with some API Platform integrations. +This documentation is based on the +[official Symfony Documentation](https://symfony.com/doc/current/security/user_providers.html) with +some API Platform integrations. ## Creating the Entity and Repository -You can follow the [official Symfony Documentation](https://symfony.com/doc/current/security/user_providers.html) and add the API Platform attributes (e.g. `#[ApiResource]`) by your own, or just use the following entity file and modify it to your needs: +You can follow the +[official Symfony Documentation](https://symfony.com/doc/current/security/user_providers.html) and +add the API Platform attributes (e.g. `#[ApiResource]`) by your own, or just use the following +entity file and modify it to your needs: ```php <?php @@ -219,8 +224,9 @@ class UserRepository extends ServiceEntityRepository implements PasswordUpgrader ## 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. +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: @@ -276,11 +282,11 @@ Then bind it to the ORM persist processor: # api/config/services.yaml services: - # ... + # ... - App\State\UserPasswordHasher: - bind: - $processor: '@api_platform.doctrine.orm.state.persist_processor' + 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: @@ -295,4 +301,5 @@ You may have wondered about the following lines in our entity file we created be ], ``` -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! +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! diff --git a/symfony/validation.md b/symfony/validation.md index a72e656d818..d4e733b171e 100644 --- a/symfony/validation.md +++ b/symfony/validation.md @@ -1,16 +1,19 @@ # 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. +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. <p align="center" class="symfonycasts"><a href="https://symfonycasts.com/screencast/api-platform/validation?cid=apip"><img src="../symfony/images/symfonycasts-player.png" alt="Validation screencast"><br>Watch the Validation screencast</a></p> ## 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: +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 <?php @@ -85,34 +88,37 @@ final class MinimalPropertiesValidator extends ConstraintValidator } ``` -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): +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\")" - } - ] + "@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. +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). +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: +You can configure the groups you want to use when the validation occurs directly through the +`ApiResource` attribute: ```php <?php @@ -134,19 +140,22 @@ class Book } ``` -With the previous configuration, the validation groups `a` and `b` will be used when validation is performed. +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. +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. +You can have different validation for each [operation](../core/operations.md) related to your +resource. ```php <?php @@ -194,9 +203,10 @@ With this configuration, there are three validation groups: ## 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). +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: @@ -264,9 +274,11 @@ final class AdminGroupsGenerator implements ValidationGroupsGeneratorInterface } ``` -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 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). +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: @@ -294,8 +306,9 @@ class Book ## 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. +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 <?php @@ -312,7 +325,8 @@ class MySequencedGroup } ``` -Just creating the class is not enough because Symfony does not see this service as being used. Therefore to prevent the service to be removed, you need to enforce it to be public. +Just creating the class is not enough because Symfony does not see this service as being used. +Therefore to prevent the service to be removed, you need to enforce it to be public. ```yaml # api/config/services.yaml @@ -362,7 +376,8 @@ class Greeting ## 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. +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: @@ -427,15 +442,16 @@ final readonly class MyEntityRemoveProcessor implements ProcessorInterface ## 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: +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: ~ + validator: + serialize_payload_fields: ~ ``` Then, the serializer will return all payload values in the error response. @@ -446,8 +462,8 @@ If you want to serialize only some payload fields, define them in the config lik # api/config/packages/api_platform.yaml api_platform: - validator: - serialize_payload_fields: [severity, anotherPayloadField] + validator: + serialize_payload_fields: [severity, anotherPayloadField] ``` In this example, only `severity` and `anotherPayloadField` will be serialized. @@ -456,8 +472,12 @@ In this example, only `severity` and `anotherPayloadField` will be serialized. 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. +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: @@ -495,7 +515,9 @@ final class Brand ## 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. +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: @@ -520,13 +542,15 @@ The following validation constraints are covered: 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 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. +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). +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 @@ -558,23 +582,25 @@ final class CustomPropertySchemaRestriction implements PropertySchemaRestriction } ``` -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. +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' ] + # ... + '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). +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): @@ -596,27 +622,29 @@ class Book } ``` -If the submitted data has denormalization errors, the HTTP status code will be set to `422 Unprocessable Content` and the response body will contain the list of errors: +If the submitted data has denormalization errors, the HTTP status code will be set to +`422 Unprocessable Content` and the response body will contain the list of errors: ```json { - "@context": "/api/contexts/ConstraintViolationList", - "@type": "ConstraintViolationList", - "title": "An error occurred", - "description": "boolean: This value should be of type bool.\nproperty1: This value should be of type string.", - "violations": [ - { - "propertyPath": "boolean", - "message": "This value should be of type bool.", - "code": "0" - }, - { - "propertyPath": "property1", - "message": "This value should be of type string.", - "code": "0" - } - ] + "@context": "/api/contexts/ConstraintViolationList", + "@type": "ConstraintViolationList", + "title": "An error occurred", + "description": "boolean: This value should be of type bool.\nproperty1: This value should be of type string.", + "violations": [ + { + "propertyPath": "boolean", + "message": "This value should be of type bool.", + "code": "0" + }, + { + "propertyPath": "property1", + "message": "This value should be of type string.", + "code": "0" + } + ] } ``` -You can also enable collecting of denormalization errors globally in the [Global Resources Defaults](https://api-platform.com/docs/core/configuration/#global-resources-defaults). +You can also enable collecting of denormalization errors globally in the +[Global Resources Defaults](https://api-platform.com/docs/core/configuration/#global-resources-defaults). From d0a27173f7a8e7c693648138b54ac5a8ec19af38 Mon Sep 17 00:00:00 2001 From: soyuka <soyuka@users.noreply.github.com> Date: Fri, 5 Dec 2025 15:48:13 +0100 Subject: [PATCH 32/49] max-length prettier --- laravel/index.md | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/laravel/index.md b/laravel/index.md index 1ef51ac47c1..8ebdaed5b3a 100644 --- a/laravel/index.md +++ b/laravel/index.md @@ -7,12 +7,12 @@ API Platform is **the easiest way** to create **state-of-the-art** web APIs usin 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! +- 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 @@ -24,11 +24,13 @@ With API Platform, you can: - 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--> +- 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) --> + [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 From c619f82cd30499a94001ba3f7c5896cc029e83e5 Mon Sep 17 00:00:00 2001 From: soyuka <soyuka@users.noreply.github.com> Date: Fri, 5 Dec 2025 15:48:56 +0100 Subject: [PATCH 33/49] max-length markdown --- .editorconfig | 2 +- .markdownlint.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.editorconfig b/.editorconfig index dd4b2e5e3f5..7d3fe6f018a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -20,7 +20,7 @@ indent_style = space indent_size = 2 [*.md] -max_line_length = 80 +max_line_length = 100 trim_trailing_whitespace = false [*.neon] diff --git a/.markdownlint.yaml b/.markdownlint.yaml index f003365cbcc..1213c33fdd8 100644 --- a/.markdownlint.yaml +++ b/.markdownlint.yaml @@ -1,6 +1,6 @@ --- MD013: - line_length: 80 + line_length: 100 code_blocks: false tables: false no-inline-html: From 17aac721d0639c3283c335e059ab40606897392f Mon Sep 17 00:00:00 2001 From: soyuka <soyuka@users.noreply.github.com> Date: Fri, 5 Dec 2025 16:41:22 +0100 Subject: [PATCH 34/49] lint --- .editorconfig | 10 +--------- .markdownlint.yaml | 4 +++- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/.editorconfig b/.editorconfig index 7d3fe6f018a..cc1d0c71d30 100644 --- a/.editorconfig +++ b/.editorconfig @@ -22,35 +22,27 @@ indent_size = 2 [*.md] max_line_length = 100 trim_trailing_whitespace = false +indent_size = 4 [*.neon] indent_style = tab indent_size = 4 [*.xml] -indent_style = space indent_size = 4 [*.{yaml,yml}] -indent_style = space indent_size = 2 trim_trailing_whitespace = false [.circleci/config.yml] -indent_style = space indent_size = 2 [.github/workflows/*.yml] -indent_style = space indent_size = 2 [.gitmodules] indent_style = tab [.proselintrc] -indent_style = space -indent_size = 2 - -[.travis.yml] -indent_style = space indent_size = 2 diff --git a/.markdownlint.yaml b/.markdownlint.yaml index 1213c33fdd8..79b434ac8da 100644 --- a/.markdownlint.yaml +++ b/.markdownlint.yaml @@ -1,6 +1,6 @@ --- MD013: - line_length: 100 + line_length: 1000 code_blocks: false tables: false no-inline-html: @@ -9,3 +9,5 @@ MD046: style: fenced MD004: style: dash +MD007: + indent: 4 From 68dfcfeef6daba8845c8d66fc01e42a54ab86ff4 Mon Sep 17 00:00:00 2001 From: aaa2000 <adev2000@gmail.com> Date: Mon, 8 Dec 2025 17:15:52 +0100 Subject: [PATCH 35/49] Document Deprecation HTTP Header usage (#2233) --- core/deprecations.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/core/deprecations.md b/core/deprecations.md index 08c9c2af756..313438a1885 100644 --- a/core/deprecations.md +++ b/core/deprecations.md @@ -169,6 +169,41 @@ class Parchment } ``` +### Setting the Deprecation HTTP Header to Indicate When an Operation Is Deprecated + +[The Deprecation HTTP Response Header Field (RFC 9745)](https://datatracker.ietf.org/doc/rfc9745/) +indicates that a URI will be or has been deprecated. This header can be used alongside the `Sunset` +header to provide a deprecation date and a link to relevant documentation. Additionally, the +`deprecation` link relation can be used to point to a resource that provides further information +about the planned or existing deprecation. + +To set a `Deprecation` header, use the `headers` option in the operation definition. The value of +the `deprecation` header should be a Unix timestamp. You can also provide a `Link` header with a +`deprecation` relation to give users more context about the deprecation: + +```php +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Link; + +#[ApiResource( + operations: [ + new GetCollection( + headers: [ + 'deprecation' => '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. From ffef0940c254ac04bcdb274adaa012660d82654e Mon Sep 17 00:00:00 2001 From: Hubert Lenoir <Jean-Beru@users.noreply.github.com> Date: Fri, 19 Dec 2025 16:46:27 +0100 Subject: [PATCH 36/49] doc(mutator): fix typo (#2234) Invert null check for operations in __invoke method --- core/operations.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/operations.md b/core/operations.md index 1224f1e3e52..8849678ae7b 100644 --- a/core/operations.md +++ b/core/operations.md @@ -647,7 +647,7 @@ final readonly class ApiPrefixMutator implements ResourceMutatorInterface { $operations = $resource->getOperations(); - if (null !== $operations) { + if (null === $operations) { return $resource; } From 4775684887d1e8710b4312f2891882b2fc268346 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=91=A8=E2=80=8D=F0=9F=92=BB=F0=9F=93=8A=20Vincent=20?= =?UTF-8?q?Amstoutz?= <vincent.amstoutz@les-tilleuls.coop> Date: Fri, 19 Dec 2025 17:06:55 +0100 Subject: [PATCH 37/49] fix(core/bootstrap): note formatting (#2235) * fix(core/bootstrap): note formatting * fix(core/bootstrap): use correct GitHub alert syntax with empty line separator (#2236) * Initial plan * fix: apply prettier formatting to bootstrap.md Co-authored-by: vinceAmstoutz <46444652+vinceAmstoutz@users.noreply.github.com> * fix: use correct GitHub alert syntax with empty line separator Co-authored-by: vinceAmstoutz <46444652+vinceAmstoutz@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: vinceAmstoutz <46444652+vinceAmstoutz@users.noreply.github.com> --------- Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> --- core/bootstrap.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/core/bootstrap.md b/core/bootstrap.md index 89331af4a95..5e65f9cbe7a 100644 --- a/core/bootstrap.md +++ b/core/bootstrap.md @@ -3,8 +3,10 @@ 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 +> [!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. From e61eec17673ec000c34e50835ceb14f19de8eb42 Mon Sep 17 00:00:00 2001 From: soyuka <soyuka@users.noreply.github.com> Date: Mon, 26 Jan 2026 10:38:48 +0100 Subject: [PATCH 38/49] fix api-platform/core/pull/5606 --- core/identifiers.md | 65 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/core/identifiers.md b/core/identifiers.md index 046940f684b..297095d57d3 100644 --- a/core/identifiers.md +++ b/core/identifiers.md @@ -172,6 +172,71 @@ services: 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 +<?php +// api/src/Identifier/DateTimeIdentifiersExtractor.php +namespace App\Identifier; + +use ApiPlatform\Api\IdentifiersExtractorInterface; +use DateTimeInterface; + +final class DateTimeIdentifiersExtractor implements IdentifiersExtractorInterface +{ + public function __construct(private IdentifiersExtractorInterface $decorated) + { + } + + /** + * {@inheritdoc} + */ + public function getIdentifiersFromItem(object $item, array $options = []): array + { + $identifiers = $this->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`: + +<code-selector> + +```yaml +# api/config/services.yaml +services: + App\Identifier\DateTimeIdentifiersExtractor: + decorates: 'api_platform.identifiers.identifiers_extractor' + arguments: ['@.inner'] + public: false +``` + +```xml +<!-- The XML syntax is only supported for Symfony --> + <service id="App\Identifier\DateTimeIdentifiersExtractor" class="App\Identifier\DateTimeIdentifiersExtractor" public="false"> + <decorate id="api_platform.identifiers.identifiers_extractor" /> + <argument type="service" id="App\Identifier\DateTimeIdentifiersExtractor.inner" /> + </service> +``` + +</code-selector> + +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 From bb66780baacd270120747e50bd3715c77e9da918 Mon Sep 17 00:00:00 2001 From: Alexis Lefebvre <alexislefebvre+github@gmail.com> Date: Tue, 27 Jan 2026 11:33:46 +0100 Subject: [PATCH 39/49] =?UTF-8?q?fix:=20typo=20=E2=80=9Cthrough=E2=80=9D?= =?UTF-8?q?=20(#2243)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/doctrine-filters.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/doctrine-filters.md b/core/doctrine-filters.md index 1bb5951b495..db4695cd89b 100644 --- a/core/doctrine-filters.md +++ b/core/doctrine-filters.md @@ -59,7 +59,7 @@ class Book { Filters are services (see the section on [custom filters](../core/filters.md#creating-custom-filters)), the can be linked to an API Platform -Operation throuh [parameters](./filters.md): +Operation through [parameters](./filters.md): For example, having a filter service declaration in `services.yaml`: From f1c33c35134034b429d0155a0714fda0fd0b2e47 Mon Sep 17 00:00:00 2001 From: Alexis Lefebvre <alexislefebvre+github@gmail.com> Date: Tue, 27 Jan 2026 11:34:36 +0100 Subject: [PATCH 40/49] docs: mention symfony/object-mapper:^8.0 (#2238) --- core/dto.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/dto.md b/core/dto.md index 7a57998e341..2bc80bb8c4f 100644 --- a/core/dto.md +++ b/core/dto.md @@ -20,8 +20,8 @@ This reference covers three implementation strategies: ## 1. The DTO Resource (State Options) -> [!WARNING] This is a Symfony only feature in 4.2 and is not working properly without the -> symfony/object-mapper:^7.4 +> [!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 From d724e5a99e170f14e28965b200e82b4f75137b8d Mon Sep 17 00:00:00 2001 From: mdavid-dev <59739365+mdavid-dev@users.noreply.github.com> Date: Tue, 27 Jan 2026 11:36:07 +0100 Subject: [PATCH 41/49] docs: clarify pagination_client_enabled configuration (#2237) Co-authored-by: david.merle <david.merle@cbainfo.fr> --- core/pagination.md | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/core/pagination.md b/core/pagination.md index 65fed93ca38..851450429e8 100644 --- a/core/pagination.md +++ b/core/pagination.md @@ -191,21 +191,23 @@ resources: </code-selector> -### Disabling the Pagination Client-side +### Allowing the Client to Control Pagination -#### Disabling the Pagination Client-side Globally +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`. -You can configure API Platform to let the client enable or disable the pagination. +#### Allowing the Client to Control Pagination Globally -##### Disabling the Pagination Client-side Globally with Symfony +##### Allowing the Client to Control Pagination Globally with Symfony -To configure this feature globally, use the following configuration: +To enable this feature globally, use the following configuration: ```yaml # api/config/packages/api_platform.yaml api_platform: defaults: - pagination_client_enabled: false + pagination_client_enabled: true collection: pagination: enabled_parameter_name: pagination # optional @@ -220,9 +222,9 @@ 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. -##### Disabling the Pagination Client-side Globally with Laravel +##### Allowing the Client to Control Pagination Globally with Laravel -To configure this feature globally, use the following configuration: +To enable this feature globally, use the following configuration: ```php <?php @@ -230,7 +232,7 @@ To configure this feature globally, use the following configuration: return [ // .... 'defaults' => [ - 'pagination_client_enabled' => false, + 'pagination_client_enabled' => true, ], 'collection' => [ 'pagination' => [ @@ -249,9 +251,9 @@ 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. -#### Disabling the Pagination Client-side For a Specific Resource +#### Allowing the Client to Control Pagination For a Specific Resource -The client ability to disable the pagination can also be set in the resource configuration: +The client ability to control pagination can also be enabled for a specific resource: ```php <?php From fa2534e2228c6ae64d6b2662c8df6a747353f8cc Mon Sep 17 00:00:00 2001 From: Alexis Lefebvre <alexislefebvre+github@gmail.com> Date: Tue, 27 Jan 2026 16:35:15 +0100 Subject: [PATCH 42/49] docs: rewrite intro to DTO (#2244) --- core/dto.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/dto.md b/core/dto.md index 2bc80bb8c4f..ccdad0ded8c 100644 --- a/core/dto.md +++ b/core/dto.md @@ -14,9 +14,9 @@ In API Platform, [the general design considerations](design.md) recommended patt This reference covers three implementation strategies: -- [State Options: Linking a DTO Resource to an Entity for automated CRUD operations.](#1-the-dto-resource-state-options) -- [Automated Mapped Inputs: Using input DTOs with stateOptions for automated Write operations.](#2-automated-mapped-inputs-and-outputs) -- [Custom Business Logic: Using input DTOs with custom State Processors for specific business actions.](#3-custom-business-logic-custom-processor) +- 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) From 2d2a5dd578716a9be8f558a1841c5b1fb345ab5a Mon Sep 17 00:00:00 2001 From: Antoine Bluchet <soyuka@users.noreply.github.com> Date: Tue, 27 Jan 2026 16:38:37 +0100 Subject: [PATCH 43/49] cs (#2246) --- core/dto.md | 9 ++++++--- core/identifiers.md | 9 ++++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/core/dto.md b/core/dto.md index ccdad0ded8c..bf8c36067aa 100644 --- a/core/dto.md +++ b/core/dto.md @@ -14,9 +14,12 @@ In API Platform, [the general design considerations](design.md) recommended patt 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) +- 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) diff --git a/core/identifiers.md b/core/identifiers.md index 297095d57d3..66ed192155c 100644 --- a/core/identifiers.md +++ b/core/identifiers.md @@ -174,7 +174,10 @@ 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`. +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`. @@ -220,8 +223,8 @@ Then, configure the service decoration in your `services.yaml`: # api/config/services.yaml services: App\Identifier\DateTimeIdentifiersExtractor: - decorates: 'api_platform.identifiers.identifiers_extractor' - arguments: ['@.inner'] + decorates: "api_platform.identifiers.identifiers_extractor" + arguments: ["@.inner"] public: false ``` From 04bdd638af5d5a7f93740862cec33486e74ff97e Mon Sep 17 00:00:00 2001 From: Alexis Lefebvre <alexislefebvre+github@gmail.com> Date: Tue, 27 Jan 2026 11:34:16 +0100 Subject: [PATCH 44/49] doc: refactor BlogPostProvider examples (#2239) --- core/state-providers.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/core/state-providers.md b/core/state-providers.md index e653875d775..a50db2766c3 100644 --- a/core/state-providers.md +++ b/core/state-providers.md @@ -125,7 +125,7 @@ use ApiPlatform\Metadata\CollectionOperationInterface; */ final class BlogPostProvider implements ProviderInterface { - private array $data; + // … public function provide(Operation $operation, array $uriVariables = [], array $context = []): iterable|BlogPost|null { @@ -185,6 +185,13 @@ 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; @@ -236,7 +243,7 @@ use ApiPlatform\Metadata\CollectionOperationInterface; */ final class BlogPostProvider implements ProviderInterface { - private array $data; + // … public function provide(Operation $operation, array $uriVariables = [], array $context = []): iterable|BlogPost|null { From 6efe91c5d6ea40d6f37dba5fc6a92e1efd3a14cb Mon Sep 17 00:00:00 2001 From: Alexis Lefebvre <alexislefebvre+github@gmail.com> Date: Tue, 27 Jan 2026 16:35:43 +0100 Subject: [PATCH 45/49] docs: fix links to Hoppscotch and remove duplicated line (#2245) --- symfony/testing.md | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/symfony/testing.md b/symfony/testing.md index a1e4c7acdd8..88c976ba375 100644 --- a/symfony/testing.md +++ b/symfony/testing.md @@ -398,18 +398,13 @@ Kubernetes cluster. You may also be interested in these alternative testing tools (not included in the API Platform distribution): -- [Hoppscotch](https://docs.hoppscotch.io/features/tests), create functional test for your API -- [Hoppscotch](https://docs.hoppscotch.io/documentation/features/rest-api-testing/), create - functional test 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/cli); -- [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. +- [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. ## Using the API Platform Distribution for End-to-End Testing From eb4a0062095ecc91e956f2b2588cafd160aa4323 Mon Sep 17 00:00:00 2001 From: soyuka <soyuka@users.noreply.github.com> Date: Tue, 27 Jan 2026 16:41:28 +0100 Subject: [PATCH 46/49] cs --- symfony/testing.md | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/symfony/testing.md b/symfony/testing.md index 88c976ba375..222ca2fa0ff 100644 --- a/symfony/testing.md +++ b/symfony/testing.md @@ -398,13 +398,18 @@ Kubernetes cluster. You may also be interested in these alternative testing tools (not included in the API Platform distribution): -- [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. +- [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. ## Using the API Platform Distribution for End-to-End Testing From 0e3611e2d28af78cc8cf175cad19786c82a7e6bf Mon Sep 17 00:00:00 2001 From: ginifizz <laury@les-tilleuls.coop> Date: Thu, 29 Jan 2026 13:59:35 +0100 Subject: [PATCH 47/49] feat: update php version on workflow --- .github/workflows/cd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 6a4f0a6eb92..a9127a3d145 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -45,7 +45,7 @@ jobs: - name: Install php uses: shivammathur/setup-php@v2 with: - php-version: "8.2" + php-version: "8.4" tools: phive - name: Auth gcloud From badbbc1585bd08a905d4152e0b8f19b7282b9f61 Mon Sep 17 00:00:00 2001 From: Johannes Przymusinski <johannes@przymusinski.de> Date: Mon, 9 Feb 2026 15:09:22 +0100 Subject: [PATCH 48/49] docs: update vich example to use the Attribute namespace instead (#2250) --- symfony/file-upload.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/symfony/file-upload.md b/symfony/file-upload.md index 208a1498276..58ac5d242cd 100644 --- a/symfony/file-upload.md +++ b/symfony/file-upload.md @@ -70,7 +70,7 @@ use Doctrine\ORM\Mapping as ORM; use Symfony\Component\HttpFoundation\File\File; use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Validator\Constraints as Assert; -use Vich\UploaderBundle\Mapping\Annotation as Vich; +use Vich\UploaderBundle\Mapping\Attribute as Vich; #[Vich\Uploadable] #[ORM\Entity] From 7cd27ed9c699609b1b26fae40f8e42604f761956 Mon Sep 17 00:00:00 2001 From: Olivier Massot <olivier.massot@2iopenservice.fr> Date: Tue, 17 Feb 2026 17:21:13 +0100 Subject: [PATCH 49/49] feat: add global defaults.normalization_context.gen_id configuration option --- core/configuration.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/configuration.md b/core/configuration.md index 51e08d033df..605c4ade382 100644 --- a/core/configuration.md +++ b/core/configuration.md @@ -331,6 +331,10 @@ api_platform: normalization_context: # Default value to omit null values in conformance with the JSON Merge Patch RFC. skip_null_values: true + + # Set the genId property globally + gen_id: true + denormalization_context: ~ swagger_context: ~ openapi_context: ~