From 8742b52bc0780760c1b0dfcaf5d22a7e9431c798 Mon Sep 17 00:00:00 2001 From: Stephane DECOCK Date: Thu, 3 Oct 2024 19:19:01 +0200 Subject: [PATCH 01/80] Change composer require package name --- core/getting-started.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/getting-started.md b/core/getting-started.md index bd51b40289d..232d05a5728 100644 --- a/core/getting-started.md +++ b/core/getting-started.md @@ -23,7 +23,7 @@ Alternatively, you can use [Composer](https://getcomposer.org/) to install the s project: ```console -composer require API +composer require api ``` There are no mandatory configuration options although [many settings are available](configuration.md). From c287c4f75ac6d95962059945da1a8b42cda8b6f6 Mon Sep 17 00:00:00 2001 From: Daniel West Date: Fri, 4 Oct 2024 08:42:45 +0100 Subject: [PATCH 02/80] Update performance.md (#2016) Remove duplicate cache-handler in the xcaddy build and replace the frankenphp module lines. The previous lines resulted in messages that php_server is not ordered. Perhaps these lines were outdated? This currently appears to work for me. --- core/performance.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/core/performance.md b/core/performance.md index c45a4667c31..eff7f1d1e10 100644 --- a/core/performance.md +++ b/core/performance.md @@ -41,8 +41,7 @@ The integration using the cache handler is quite simple. You just have to update +ENV CGO_ENABLED=1 XCADDY_SETCAP=1 XCADDY_GO_BUILD_FLAGS="-ldflags \"-w -s -extldflags '-Wl,-z,stack-size=0x80000'\"" +RUN xcaddy build \ + --output /usr/local/bin/frankenphp \ -+ --with github.com/dunglas/frankenphp=./ \ -+ --with github.com/dunglas/frankenphp/caddy=./caddy/ \ ++ --with github.com/dunglas/frankenphp/caddy \ + --with github.com/dunglas/mercure/caddy \ + --with github.com/dunglas/vulcain/caddy \ + --with github.com/dunglas/caddy-cbrotli \ From 4881e9efab9bc0d67269be8189618f9b6fd1c3d5 Mon Sep 17 00:00:00 2001 From: Vincent Amstoutz Date: Fri, 4 Oct 2024 09:59:55 +0200 Subject: [PATCH 03/80] docs: add CONTRIBUTING.md file (#2029) --- CONTRIBUTING.md | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000000..2be61f563bd --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,40 @@ +# Contributing to the API Platform Documentation + +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! + +## Reporting Bugs in the documentation + +Before submitting your issue: + +* Check if the bug is not already reported! +* 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. + +## Code of Conduct + +By contributing to this project, you agree to abide by our [Code of Conduct](https://github.com/api-platform/docs#contributor-code-of-conduct). 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. + +2. Clone the forked repository to your local machine: + ```bash + git clone https://github.com/your-username/repository-name.git + ``` +3. Create a new branch for your contribution: + ```bash + 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/). + +> [!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. From 6d118f486544defb0cb95de1133218a56d5fca0c Mon Sep 17 00:00:00 2001 From: Vincent Amstoutz Date: Fri, 4 Oct 2024 14:57:40 +0200 Subject: [PATCH 04/80] fix: removes a deprecated link in releases documentation (#2030) --- extra/releases.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/extra/releases.md b/extra/releases.md index 1881c7365c5..2fb74338a16 100644 --- a/extra/releases.md +++ b/extra/releases.md @@ -21,8 +21,6 @@ For example: Older versions (1.x, 2.6...) **are not maintained**. If you still use them, you must upgrade as soon as possible. -There's a [crowdfunding for a 2.7 LTS version](https://opencollective.com/api-platform/projects/27-lts). - 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 d84cd2fb49e0db0b1ec0e1303daa78628508206f Mon Sep 17 00:00:00 2001 From: Vincent Amstoutz Date: Fri, 4 Oct 2024 15:00:01 +0200 Subject: [PATCH 05/80] Update graphql, events & extending docs for Laravel and v4 (#2026) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update event listener configuration for API Platform 4.0 Updated the documentation to reflect the requirement of `use_symfony_listeners: true` for activating event listeners in API Platform 4.0. Also reformatted existing notes for better clarity and added separation lines for improved readability. * Add documentation on system providers and processors This commit introduces detailed documentation on the workflow of state providers and processors in the system. It includes a schema, examples of decorating providers and processors, and specific implementations for both Symfony and Laravel frameworks. * Update GraphQL docs to include Laravel-specific instructions This update adds detailed instructions for enabling and configuring GraphQL in a Laravel environment, complementing the existing Symfony guidance. It covers installation, route configurations, disabling features, custom resolvers, and altering default settings specific to Laravel, ensuring comprehensive and platform-specific documentation. * Remove specific framework & Docker for GraphQL installation commands * Refactor GraphQL & extending documentations for clarity and accuracy Updated terminology from "stages" to "providers and processors" for better clarity. Removed redundant sections to streamline information on custom mutations and configuration examples. * Fix capitalize title Co-authored-by: Kévin Dunglas * Fix capitalize title Co-authored-by: Kévin Dunglas * Fix capitalize title Co-authored-by: Kévin Dunglas * Fix capitalize title Co-authored-by: Kévin Dunglas * Fix title Co-authored-by: Kévin Dunglas * Fix title Co-authored-by: Kévin Dunglas * Fix titles and coding style * Fix wrong implementation & rm duplicate code * Fix spelling and class rendering Co-authored-by: Antoine Bluchet * Fix title with a better title name Co-authored-by: Antoine Bluchet * Fix title with a better title name Co-authored-by: Antoine Bluchet * Fix remove unused method Co-authored-by: Antoine Bluchet * Fix remove unused method Co-authored-by: Antoine Bluchet * Fix remove unused method Co-authored-by: Antoine Bluchet * Fix by rephrasing not available commands message Co-authored-by: Antoine Bluchet * Fix better explanations to enable GraphQL Co-authored-by: Antoine Bluchet * Fix remove unused declaration in the Laravel container Co-authored-by: Antoine Bluchet * Fix typo Co-authored-by: Alan Poulain * Fix typo Co-authored-by: Alan Poulain * Fix spelling Co-authored-by: Alan Poulain * Fix typo Co-authored-by: Alan Poulain * Fix typo Co-authored-by: Alan Poulain * Fix missing var assignation Co-authored-by: Alan Poulain * Add recommendation for using system providers and processors * Add GraphQL state provider access checker notes & cleanup * Completes GraphQL doc for Laravel support & clarified some points reviewed * Fix string syntax error Co-authored-by: Alan Poulain * Apply suggestions from code review --------- Co-authored-by: Kévin Dunglas Co-authored-by: Antoine Bluchet Co-authored-by: Alan Poulain --- core/events.md | 10 +- core/extending.md | 112 ++++++++ core/graphql.md | 668 ++++++++++++++++++++++++++++------------------ 3 files changed, 531 insertions(+), 259 deletions(-) diff --git a/core/events.md b/core/events.md index 1c535d79d4a..ee5319f7c83 100644 --- a/core/events.md +++ b/core/events.md @@ -1,9 +1,13 @@ # The Event System -In API Platform 3.2 you may need `event_listeners_backward_compatibility_layer: true` to keep event listeners activated. +> [!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**. -[For most use cases, better extension points, working both with REST and GraphQL, are available](extending.md). +--- + +> [!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) diff --git a/core/extending.md b/core/extending.md index 73e393654c4..47f37af53e9 100644 --- a/core/extending.md +++ b/core/extending.md @@ -36,3 +36,115 @@ For instance, if you want to send a mail after a resource has been persisted, bu To replace existing API Platform services with your decorators, [check out how to decorate services](https://symfony.com/doc/current/service_container/service_decoration.html).

Service Decoration screencast
Watch the Service Decoration screencast

+ +## System Providers and Processors + +The system is based on a workflow composed of **state providers** and **state processors**. + +The schema below describes them: + +```mermaid +--- +title: System providers and processors +--- +flowchart TB + C1(ReadProvider) --> C2(AccessCheckerProvider) + C2 --> C3(DeserializeProvider) + C3 --> C4(ParameterProvider) + C4 --> C5(ValidateProcessor) + C5 --> C6(WriteProcessor) + C6 --> C7(SerializeProcessor) +``` + +### Symfony Access Checker Provider + +When using Symfony, the access checker provider is used at three different stages: +- `api_platform.state_provider.access_checker.post_validate` decorates the `ValidateProvider` +- `api_platform.state_provider.access_checker.post_deserialize` decorates the `DeserializeProvider` +- `api_platform.state_provider.access_checker` decorates the `ReadProvider` + +> [!NOTE] +> For graphql use: `api_platform.graphql.state_provider.access_checker.post_deserialize`, +> `api_platform.graphql.state_provider.access_checker.post_validate`, `api_platform.graphql.state_provider.validate` and +> `api_platform.graphql.state_provider.access_checker.after_resolver` + +### Decoration Example + +Here is an example of the decoration of the RespondProcessor: + +Starts by creating your `CustomRespondProcessor`: + +```php +processor->process($data, $operation, $uriVariables, $context); + + // You can add post-write code here. + + return $writtenObject; + } +} +``` + +Now decorate the `RespondProcessor` with the `CustomRespondProcessor` using Symfony or Laravel: + +### Symfony Processor Decoration + +With Symfony you can simply do that by adding the `#[AsDecorator]` attribute as following: + +```php +namespace App\State; + +use ApiPlatform\State\ProcessorInterface; + +#[AsDecorator(decorates: 'api_platform.state.processor.respond_processor')] +final class CustomRespondProcessor implements ProcessorInterface +{ + // ... +} +``` + +or in the `services.yaml` by defining: + +```yaml +# api/config/services.yaml +services: + # ... + App\State\CustomRespondProcessor: + decorates: api_platform.state.processor.respond_processor +``` + +And that's it! + +### Laravel Processor Decoration +```php +app->extend(RespondProcessor::class, function (RespondProcessor $respondProcessor) { + return new CustomRespondProcessor($respondProcessor); + }); + } +} +``` diff --git a/core/graphql.md b/core/graphql.md index 3392112f33c..b3125977e24 100644 --- a/core/graphql.md +++ b/core/graphql.md @@ -10,23 +10,25 @@ Once enabled, you have nothing to do: your schema describing your API is automat ## Enabling GraphQL -To enable GraphQL and its IDE (GraphiQL and GraphQL Playground) in your API, simply require the [graphql-php](https://webonyx.github.io/graphql-php/) package using Composer and clear the cache one more time: +To enable GraphQL and its IDE (GraphiQL and GraphQL Playground) in your API, simply require the `api-platform/graphql` package using Composer: ```console -docker compose exec php sh -c ' - composer require webonyx/graphql-php - bin/console cache:clear -' + composer require api-platform/graphql ``` You can now use GraphQL at the endpoint: `https://localhost:8443/graphql`. -*Note:* If you used [Symfony Flex to install API Platform](../symfony/index.md#using-symfony-flex-and-composer-advanced-users), URLs will be prefixed with `/api` by default. For example, the GraphQL endpoint will be: `https://localhost:8443/api/graphql`. +> [!NOTE] +> If you used [the Symfony Variant thanks to Symfony Flex](../symfony/index.md#using-symfony-flex-and-composer-advanced-users) or the Laravel variant, URLs will be prefixed with `/api` by default. For example, the GraphQL endpoint will be: `https://localhost:8443/api/graphql`. ## Changing Location of the GraphQL Endpoint Sometimes you may want to have the GraphQL endpoint at a different location. This can be done by manually configuring the GraphQL controller. +### Symfony Routes + +Using the Symfony variant we can do this modification by adding the following code: + ```yaml # api/config/routes.yaml api_graphql_entrypoint: @@ -37,14 +39,29 @@ api_graphql_entrypoint: Change `/api/graphql` to the URI you wish the GraphQL endpoint to be accessible on. +### Laravel Routes + +Using the Laravel variant we can do this modification by adding the following code: +```php +// routes/web.php +use Illuminate\Support\Facades\Route; +use ApiPlatform\GraphQL\Action\EntrypointAction; + +Route::post('/api/graphql', EntrypointAction::class) + ->name('api_graphql_entrypoint'); +``` + +Change `/api/graphql` to the URI you wish the GraphQL endpoint to be accessible on. + ## GraphiQL -If Twig is installed in your project, 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`. If you need to disable it, it can be done in the configuration: +### Disabling GraphiQL with Symfony ```yaml # api/config/packages/api_platform.yaml api_platform: @@ -54,9 +71,27 @@ api_platform: # ... ``` +### Disabling GraphiQL with Laravel + +```php + [ + 'graphiql' => [ + 'enabled' => false, + ] + ], +]; +``` + ### Add another Location for GraphiQL -If you want to add a different location besides `/graphql/graphiql`, you can do it like this: +Sometimes you may want to have the GraphiQL at a different location. This can be done by manually configuring the GraphiQL controller. + +### Symfony config routes for GraphiQL +If you want to add a different location besides `/graphql/graphiql`, you can do it like this if you are using the Symfony variant: ```yaml # app/config/routes.yaml @@ -65,13 +100,27 @@ graphiql: controller: api_platform.graphql.action.graphiql ``` +### Laravel config routes for GraphiQL + +If you want to add a different location besides `/graphql/graphiql`, you can do it like this if you are using the Laravel variant: +```php +// routes/web.php +use Illuminate\Support\Facades\Route; +use ApiPlatform\GraphQL\Action\GraphiQlAction; + +Route::post('/docs/graphiql', GraphiQlAction::class) + ->name('graphiql'); +``` + ## GraphQL Playground Another IDE is by default included in API Platform: GraphQL Playground. It can be found at `/graphql/graphql_playground`. -You can disable it if you want in the configuration: +You can disable it if you want in the configuration. + +### Disable GraphQL Playground with Symfony ```yaml # api/config/packages/api_platform.yaml @@ -82,9 +131,17 @@ api_platform: # ... ``` +### Disable GraphQL Playground with Laravel + +> [!WARNING] +> This is not yet available with Laravel, you're welcome to contribute [on Github](github.com/api-platform/core) + ### Add another Location for GraphQL Playground +You can add a different location besides `/graphql/graphql_playground`. + +### Symfony config routes for GraphQL Playground -You can add a different location besides `/graphql/graphql_playground`: +Using the Symfony variant we can do this modification by adding the following code: ```yaml # app/config/routes.yaml @@ -93,10 +150,24 @@ graphql_playground: controller: api_platform.graphql.action.graphql_playground ``` +### Laravel config routes for GraphQL Playground + +Using the Laravel variant we can do this modification by adding the following code: +```php +// routes/web.php +use Illuminate\Support\Facades\Route; +use ApiPlatform\GraphQL\Action\GraphQlPlaygroundAction; + +Route::post('/docs/graphql_playground', GraphQlPlaygroundAction::class) + ->name('graphql_playground'); +``` + ## Modifying or Disabling the Default IDE When going to the GraphQL endpoint, you can choose to launch the IDE you want. +### Symfony config to modifying the default IDE + ```yaml # api/config/packages/api_platform.yaml api_platform: @@ -106,8 +177,23 @@ api_platform: # ... ``` +### Laravel config to modifying the default IDE + +```php + [ + // Choose between graphiql or graphql-playground + 'default_ide' => 'graphql-playground', + ], +]; +``` + You can also disable this feature by setting the configuration value to `false`. +### Symfony config to disable default IDE ```yaml # api/config/packages/api_platform.yaml api_platform: @@ -116,10 +202,24 @@ api_platform: # ... ``` +### Laravel config to disable default IDE +```php + [ + 'default_ide' => false, + ], +]; +``` + ## Disabling the Introspection Query For security reason, the introspection query should be disabled to not expose the GraphQL schema. + +### Symfony config to disable the Introspection Query If you need to disable it, it can be done in the configuration: ```yaml @@ -130,11 +230,27 @@ api_platform: # ... ``` +### Laravel config to disable the Introspection Query +If you need to disable it, it can be done in the configuration: + +```php + [ + 'introspection' => false, + ], +]; +``` + ## Request with `application/graphql` Content-Type If you wish to send a [POST request using the `application/graphql` Content-Type](https://graphql.org/learn/serving-over-http/#post-request), you need to enable it in the [allowed formats of API Platform](content-negotiation.md#configuring-formats-globally): +### Symfony config for GraphQL Content-Type + ```yaml # api/config/packages/api_platform.yaml api_platform: @@ -143,8 +259,26 @@ api_platform: graphql: ['application/graphql'] ``` +### Laravel config for GraphQL Content-Type + +```php + [ + 'graphql' => [ + 'application/graphql', + ], + ], +]; +``` + ## Operations +> [!NOTE] +> In Symfony we use the term “entities”, while the following documentation is mostly for Laravel “models”. + To understand what an operation is, please refer to the [operations documentation](operations.md). For GraphQL, the operations are defined by using the `Query`, `QueryCollection`, `Mutation`, `DeleteMutation` and `Subscription` attributes. @@ -183,6 +317,7 @@ class Book ``` ```yaml +#The YAML syntax is only supported for Symfony resources: App\Entity\Book: graphQlOperations: @@ -192,6 +327,7 @@ resources: ``` ```xml + - - - - - - - - - - - - - ID! - - - - - - - - - Boolean! - Send a mail? - - - - - - - - -``` - - +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). @@ -709,38 +748,6 @@ You can also use the `extraArgs` property in case you need to add additional arg The arguments will be in `$context['args']['input']` of your resolvers. -Your custom mutations will be available like this: - -```graphql -{ - mutation { - mutationBook(input: {id: "/books/18", title: "The Fitz and the Fool"}) { - book { - title - } - } - } - - mutation { - withCustomArgsMutationBook(input: {sendMail: true, clientMutationId: "myId"}) { - book { - title - } - clientMutationId - } - } - - mutation { - disabledStagesMutationBook(input: {title: "The Fitz and the Fool"}) { - book { - title - } - clientMutationId - } - } -} -``` - ## 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. @@ -780,6 +787,7 @@ class Book ``` ```yaml +#The YAML syntax is only supported for Symfony resources: App\Entity\Book: graphQlOperations: @@ -789,6 +797,7 @@ resources: ``` ```xml + [ + 'nesting_separator' => '__' + ], +]; +``` In this case, your query will be: @@ -2521,9 +2580,11 @@ You can also check the documentation of [graphql-php](https://webonyx.github.io/ The big difference in API Platform is that the value is already serialized when it's received in your type class. Similarly, you would not want to denormalize your parsed value since it will be done by API Platform later. +### Custom Types config for Symfony + If you use autoconfiguration (the default Symfony configuration) in your application, then you are done! -Else, you need to tag your type class like this: +Else, you need to tag your type class like this, if you're using Symfony : ```yaml # api/config/services.yaml @@ -2538,6 +2599,33 @@ Your custom type is now registered and is available in the `TypesContainer`. To use it please [modify the extracted types](#modify-the-extracted-types) or use it directly in [custom queries](#custom-queries) or [custom mutations](#custom-mutations). + +### Custom Types config for Laravel + +If you are using Laravel tag your type with: + +```php +app->tag([DateTimeType::class], TypeInterface::class); + } +} +``` + +Your custom type is now registered and is available in the `TypesContainer`. + +To use it please [modify the extracted types](#modify-the-extracted-types) or use it directly in [custom queries](#custom-queries) or [custom mutations](#custom-mutations). + ## Modify the Extracted Types The GraphQL schema and its types are extracted from your resources. @@ -2545,6 +2633,8 @@ In some cases, you would want to modify the extracted types for instance to use To do so, you need to decorate the `api_platform.graphql.type_converter` service: +### Symfony TypeConverter Decoration + ```yaml # api/config/services.yaml services: @@ -2553,7 +2643,29 @@ services: decorates: api_platform.graphql.type_converter ``` -Your class needs to look like this: +### Laravel TypeConverter Decoration + +```php +app->extend(TypeConverterInterface::class, function (TypeConverterInterface $typeConverter) { + return new TypeConverter($typeConverter); + }); + } +} +``` + +Then, your class needs to look like this: ```php defaultTypeConverter = $defaultTypeConverter; - } + public function __construct(private readonly TypeConverterInterface $defaultTypeConverter) {} public function convertType(Type $type, bool $input, Operation $rootOperation, string $resourceClass, string $rootResource, ?string $property, int $depth) { @@ -2616,6 +2724,8 @@ The service is `api_platform.graphql.serializer.context_builder` and the method The decorator could be like this: +### Symfony Serialization Context Decoration + ```php decorated = $decorated; - $this->authorizationChecker = $authorizationChecker; + $context = $this->decorated->create($resourceClass, $operationName, $resolverContext, $normalization); + $resourceClass = $context['resource_class'] ?? null; + + if ($resourceClass === Book::class && isset($context['groups']) && $this->authorizationChecker->isGranted('ROLE_ADMIN') && false === $normalization) { + $context['groups'][] = 'admin:input'; + } + + return $context; } +} +``` + +### Laravel Serialization Context Decoration +```php +decorated->create($resourceClass, $operationName, $resolverContext, $normalization); $resourceClass = $context['resource_class'] ?? null; - if ($resourceClass === Book::class && isset($context['groups']) && $this->authorizationChecker->isGranted('ROLE_ADMIN') && false === $normalization) { + if ($resourceClass === Book::class && isset($context['groups']) && $this->isAdmin() && !$normalization) { $context['groups'][] = 'admin:input'; } return $context; } + + private function isAdmin(): bool + { + $user = Auth::user(); + + return $user && $user->role === 'admin'; + } } ``` ## Export the Schema in SDL +> [!WARNING] +> This command is not yet available with Laravel, you're welcome to contribute [on Github](github.com/api-platform/core) + You may need to export your schema in SDL (Schema Definition Language) to import it in some tools. The `api:graphql:export` command is provided to do so: ```shell-session -docker compose exec php \ bin/console api:graphql:export -o path/to/your/volume/schema.graphql ``` Since the command prints the schema to the output if you don't use the `-o` option, you can also use this command: ```shell-session -docker compose exec php \ bin/console api:graphql:export > path/in/host/schema.graphql ``` @@ -2784,6 +2926,7 @@ Following the specification, the upload must be done with a `multipart/form-data You need to enable it in the [allowed formats of API Platform](content-negotiation.md#configuring-formats-globally): +#### Modifying allowed formats with Symfony ```yaml # api/config/packages/api_platform.yaml api_platform: @@ -2792,6 +2935,19 @@ api_platform: multipart: ['multipart/form-data'] ``` +#### Modifying allowed formats with Laravel +```php + [ + // ... + 'multipart' => ['multipart/form-data'] + ], +]; +``` + You can now upload files using the `createMediaObject` mutation, for details check [GraphQL multipart request specification](https://github.com/jaydenseric/graphql-multipart-request-spec) and for an example implementation for the Apollo client check out [Apollo Upload Client](https://github.com/jaydenseric/apollo-upload-client). From 3d444f61686185c5b5ced4e52e8d310e87ef1bad Mon Sep 17 00:00:00 2001 From: soyuka Date: Fri, 4 Oct 2024 15:46:43 +0200 Subject: [PATCH 06/80] doc: path versioning --- core/deprecations.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/core/deprecations.md b/core/deprecations.md index 3d13cab0dec..d476e6abbb8 100644 --- a/core/deprecations.md +++ b/core/deprecations.md @@ -152,3 +152,33 @@ class Parchment // ... } ``` + +## Path versioning + +> [!NOTE] +> REST and GraphQL architectures recommend to use deprecations instead of path versioning. + +You can prefix your URI Templates and change the representation using serialization groups: + +```php + ['v1']])] +#[Get(uriTemplate: '/v2/books/{id}', normalizationContext: ['groups' => ['v2']])] +class Parchment +{ + #[Groups(['v1'])] + public $name; + + #[Groups(['v2'])] + public $title; +} +``` + +> [!NOTE] +> It's also possible to use the configuration `route_prefix` to prefix all your operations. From 4669b2b19bf95799b117ecf372cf33aea35619e9 Mon Sep 17 00:00:00 2001 From: soyuka Date: Fri, 4 Oct 2024 15:46:43 +0200 Subject: [PATCH 07/80] doc: path versioning --- core/deprecations.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/core/deprecations.md b/core/deprecations.md index 3cbff17f3a4..b155d939503 100644 --- a/core/deprecations.md +++ b/core/deprecations.md @@ -152,3 +152,33 @@ class Parchment // ... } ``` + +## Path versioning + +> [!NOTE] +> REST and GraphQL architectures recommend to use deprecations instead of path versioning. + +You can prefix your URI Templates and change the representation using serialization groups: + +```php + ['v1']])] +#[Get(uriTemplate: '/v2/books/{id}', normalizationContext: ['groups' => ['v2']])] +class Parchment +{ + #[Groups(['v1'])] + public $name; + + #[Groups(['v2'])] + public $title; +} +``` + +> [!NOTE] +> It's also possible to use the configuration `route_prefix` to prefix all your operations. From de8478d155f70104b4db55bbd59d57e5b9cecb6d Mon Sep 17 00:00:00 2001 From: soyuka Date: Fri, 4 Oct 2024 15:53:49 +0200 Subject: [PATCH 08/80] add provider --- laravel/index.md | 56 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/laravel/index.md b/laravel/index.md index 5b93da39d2f..7e28d1e647d 100644 --- a/laravel/index.md +++ b/laravel/index.md @@ -165,9 +165,65 @@ So, if you want to access the raw data, you have two alternatives: For instance, go to `http://127.0.0.1:8000/api/books.jsonld` to retrieve the list of `Book` resources in JSON-LD. +> [!NOTE] Read the next parameter if you want to use JSON:API instead! + 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. +As recommended by our [design considerations](../core/design.md), you can totally use the data source of your choice using a [provider](../core/state-providers.md): + +```php +app->singleton(BookProvider::class, function (Application $app) { + return new BookProvider(); + }); + + $this->app->tag([BookProvider::class], ProviderInterface::class); + } +} +``` + +Apply the provider to your operation: + +```php + Date: Fri, 4 Oct 2024 17:00:09 +0200 Subject: [PATCH 09/80] chore: lint --- core/deprecations.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/deprecations.md b/core/deprecations.md index b155d939503..7565c3f738f 100644 --- a/core/deprecations.md +++ b/core/deprecations.md @@ -156,7 +156,7 @@ class Parchment ## Path versioning > [!NOTE] -> REST and GraphQL architectures recommend to use deprecations instead of path versioning. +> 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: From d34d996592c4ed463e3e1e38d414ed5d72a0d6fb Mon Sep 17 00:00:00 2001 From: soyuka Date: Fri, 4 Oct 2024 17:00:09 +0200 Subject: [PATCH 10/80] chore: lint --- core/deprecations.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/deprecations.md b/core/deprecations.md index d476e6abbb8..91acde9a0fc 100644 --- a/core/deprecations.md +++ b/core/deprecations.md @@ -156,7 +156,7 @@ class Parchment ## Path versioning > [!NOTE] -> REST and GraphQL architectures recommend to use deprecations instead of path versioning. +> 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: From 6f225ca371914c0b825365301cc1ff8288cc3c04 Mon Sep 17 00:00:00 2001 From: Antoine Bluchet Date: Fri, 4 Oct 2024 17:14:02 +0200 Subject: [PATCH 11/80] Document laravel provider (#2031) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add provider * Update laravel/index.md Co-authored-by: Kévin Dunglas * Update laravel/index.md Co-authored-by: Kévin Dunglas * review --------- Co-authored-by: Kévin Dunglas --- laravel/index.md | 59 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 55 insertions(+), 4 deletions(-) diff --git a/laravel/index.md b/laravel/index.md index 7e28d1e647d..07d504c05bf 100644 --- a/laravel/index.md +++ b/laravel/index.md @@ -165,12 +165,55 @@ So, if you want to access the raw data, you have two alternatives: For instance, go to `http://127.0.0.1:8000/api/books.jsonld` to retrieve the list of `Book` resources in JSON-LD. -> [!NOTE] Read the next parameter if you want to use JSON:API instead! +> [!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. -As recommended by our [design considerations](../core/design.md), you can totally use the data source of your choice using a [provider](../core/state-providers.md): + +## Using Data Transfer Objects and Hooking Custom Logic + +While exposing directly the data in the database is convenient for Rapid Application Development, using different classes for the internal data and the public data is a good practice for more complex projects. + +As explained in our [general design considerations](../core/design.md), API Platform allows us to use the data source of our choice using a [provider](../core/state-providers.md) and Data Transfer Objects (DTOs) are first-class citizens! + +Let's create our DTO: + +```php + [ + app_path('ApiResource'), + app_path('Models'), + ], + + // ... +]; +``` + +Then we can create the logic to retrieve the state of our `Book` DTO: ```php id, title: $book->title); } } ``` @@ -217,10 +262,16 @@ Apply the provider to your operation: ```php Date: Thu, 10 Oct 2024 07:39:10 +0200 Subject: [PATCH 12/80] Bump super-linter from v4 to v7 and fix all files (#2035) --- .github/ISSUE_TEMPLATE/1_Support_question.md | 7 +- .../ISSUE_TEMPLATE/2_Documentation_issue.md | 1 - .github/workflows/cd.yml | 46 +- .github/workflows/ci.yml | 10 +- .markdownlint.yml | 1 + CONTRIBUTING.md | 18 +- admin/components.md | 85 ++- admin/customizing.md | 28 +- admin/file-upload.md | 6 +- admin/getting-started.md | 22 +- admin/handling-relations.md | 115 ++-- admin/index.md | 26 +- admin/openapi.md | 13 +- admin/real-time-mercure.md | 11 +- admin/schema.org.md | 6 +- core/angularjs-integration.md | 116 ++-- core/bootstrap.md | 46 +- core/configuration.md | 137 ++-- core/content-negotiation.md | 92 +-- core/controllers.md | 124 ++-- core/default-order.md | 38 +- core/deprecations.md | 16 +- core/design.md | 14 +- core/dto.md | 16 +- core/elasticsearch.md | 30 +- core/errors.md | 50 +- core/events.md | 82 +-- core/extending-jsonld-context.md | 12 +- core/extending.md | 16 +- core/extensions.md | 41 +- core/external-vocabularies.md | 34 +- core/file-upload.md | 47 +- core/filters.md | 591 +++++++++--------- core/form-data.md | 25 +- core/fosuser-bundle.md | 12 +- core/getting-started.md | 68 +- core/graphql.md | 553 ++++++++-------- core/identifiers.md | 24 +- core/index.md | 32 +- core/json-schema.md | 2 +- core/jwt.md | 178 +++--- core/mercure.md | 20 +- core/messenger.md | 30 +- core/migrate-from-fosrestbundle.md | 1 - core/mongodb.md | 35 +- core/nelmio-api-doc.md | 20 +- core/openapi.md | 190 +++--- core/operation-path-naming.md | 18 +- core/operations.md | 141 +++-- core/pagination.md | 92 +-- core/performance.md | 111 ++-- core/push-relations.md | 2 +- core/security.md | 114 ++-- core/serialization.md | 150 ++--- core/state-processors.md | 8 +- core/state-providers.md | 6 +- core/subresources.md | 64 +- core/upgrade-guide.md | 58 +- core/url-generation-strategy.md | 8 +- core/user.md | 8 +- core/validation.md | 87 +-- create-client/custom.md | 90 +-- create-client/index.md | 2 +- create-client/nextjs.md | 2 +- create-client/nuxt.md | 18 +- create-client/quasar.md | 2 +- create-client/react-native.md | 24 +- create-client/react.md | 6 +- create-client/troubleshooting.md | 2 +- create-client/typescript.md | 8 +- create-client/vuejs.md | 7 +- create-client/vuetify.md | 4 +- deployment/docker-compose.md | 14 +- deployment/heroku.md | 34 +- deployment/index.md | 8 +- deployment/kubernetes.md | 28 +- deployment/minikube.md | 10 +- deployment/traefik.md | 66 +- extra/conduct.md | 12 +- extra/enterprise.md | 20 +- extra/philosophy.md | 12 +- extra/releases.md | 6 +- extra/security.md | 42 +- extra/troubleshooting.md | 2 +- laravel/filters.md | 16 +- laravel/index.md | 92 ++- laravel/security.md | 7 +- laravel/validation.md | 2 +- outline.yaml | 1 + schema-generator/configuration.md | 485 +++++++------- schema-generator/getting-started.md | 52 +- schema-generator/index.md | 40 +- symfony/debugging.md | 50 +- symfony/index.md | 153 ++--- symfony/testing.md | 30 +- 95 files changed, 2668 insertions(+), 2633 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/1_Support_question.md b/.github/ISSUE_TEMPLATE/1_Support_question.md index df80ae425b5..1b9f449db0c 100644 --- a/.github/ISSUE_TEMPLATE/1_Support_question.md +++ b/.github/ISSUE_TEMPLATE/1_Support_question.md @@ -1,11 +1,12 @@ --- name: ⛔ Support Question about: See https://api-platform.com/support/ for questions about using API Platform - --- -We use GitHub issues only to discuss about bugs and new features. +# 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 https://api-platform.com/support/ +any of the support alternatives shown in [API Platform support](https://api-platform.com/support/). Thanks! diff --git a/.github/ISSUE_TEMPLATE/2_Documentation_issue.md b/.github/ISSUE_TEMPLATE/2_Documentation_issue.md index 78f759d8a21..6ae294a09c1 100644 --- a/.github/ISSUE_TEMPLATE/2_Documentation_issue.md +++ b/.github/ISSUE_TEMPLATE/2_Documentation_issue.md @@ -1,5 +1,4 @@ --- name: 📄 Documentation issue about: Report a documentation issue - --- diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 192fabacdf7..7f64e18f0e9 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -1,10 +1,19 @@ +--- name: Deploy Website on: push: branches: - main - - '*.*' + - "*.*" + +permissions: + contents: read + id-token: read + actions: read + checks: write + deployments: write + pull-requests: read jobs: deploy: @@ -28,57 +37,52 @@ jobs: restore-keys: | ${{ runner.os }}-yarn- - - name: Setup Hugo - uses: peaceiris/actions-hugo@v2 + uses: peaceiris/actions-hugo@v3 with: - hugo-version: '0.134.2' + hugo-version: "0.134.2" extended: true - name: Install php uses: shivammathur/setup-php@v2 with: - php-version: '8.2' + php-version: "8.2" tools: phive - name: Auth gcloud - uses: google-github-actions/auth@v1 + uses: google-github-actions/auth@v2 with: credentials_json: ${{ secrets.BUCKET_CREDS }} - - - name: 'Set up Cloud SDK' - uses: 'google-github-actions/setup-gcloud@v1' + + - name: Set up Cloud SDK + uses: google-github-actions/setup-gcloud@v2 - name: Clone website - uses: actions/checkout@v2 + uses: actions/checkout@v4 with: repository: api-platform/docs-website path: docs-website + - name: Install javascript packages working-directory: docs-website run: npm install + - name: Fetch API Platform docs working-directory: docs-website run: tools/get-docs.sh + - name: Fetch API Platform references and guides working-directory: docs-website run: tools/get-core-docs.sh - - name: Build menu + + - name: Build menu working-directory: docs-website run: node tools/menu.mjs + - name: Hugo working-directory: docs-website run: hugo --minify + - name: Deploy working-directory: docs-website run: gsutil -q -m rsync -d -r ./public gs://api-platform-website-v3/ - # This need to move to website - # env: - # GITHUB_KEY: ${{ secrets.CONTRIBUTORS_GITHUB_TOKEN }} - # NODE_OPTIONS: --openssl-legacy-provider - # - name: Deploy - # uses: peaceiris/actions-gh-pages@v3 - # with: - # github_token: ${{ secrets.GITHUB_TOKEN }} - # publish_dir: ./public - # cname: api-platform.com diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0e457f53c4d..f845ae2b77e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,10 +1,12 @@ +--- name: Lint on: push: pull_request: -permissions: {} +permissions: + contents: read jobs: build: @@ -23,12 +25,12 @@ jobs: fetch-depth: 0 - name: Lint - uses: github/super-linter/slim@v4 + uses: super-linter/super-linter/slim@v7 env: - VALIDATE_ALL_CODEBASE: false VALIDATE_EDITORCONFIG: false VALIDATE_JSCPD: false - DEFAULT_BRANCH: "4.0" + VALIDATE_MARKDOWN_PRETTIER: false + DEFAULT_BRANCH: "origin/4.0" GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - uses: actions/cache@v4 diff --git a/.markdownlint.yml b/.markdownlint.yml index 1b3df3ad86f..754eda95a84 100644 --- a/.markdownlint.yml +++ b/.markdownlint.yml @@ -1,3 +1,4 @@ +--- MD013: line_length: 400 no-inline-html: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2be61f563bd..2458ebb6e2e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,9 +8,9 @@ To have your code integrated in the API Platform documentation project, there ar Before submitting your issue: -* Check if the bug is not already reported! -* A clear title to resume the issue -* A description of the workflow needed to reproduce the bug +- Check if the bug is not already reported! +- 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. @@ -24,13 +24,13 @@ 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: - ```bash - git clone https://github.com/your-username/repository-name.git - ``` + ```bash + git clone https://github.com/your-username/repository-name.git + ``` 3. Create a new branch for your contribution: - ```bash - git switch -c docs-your-branch-name - ``` + ```bash + 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/admin/components.md b/admin/components.md index 5ff0ac5db96..ec3d634465e 100644 --- a/admin/components.md +++ b/admin/components.md @@ -14,21 +14,20 @@ Deprecated resources are hidden by default, but you can add them back using an e ```javascript // App.js -import { AdminGuesser, ResourceGuesser } from "@api-platform/admin"; +import { AdminGuesser, ResourceGuesser } from '@api-platform/admin'; const App = () => ( - + + create={BooksCreate} + /> -) +); export default App; ``` @@ -36,7 +35,7 @@ export default App; #### Props | Name | Type | Value | required | Description | -|-------------------|---------|----------------|----------|----------------------------------------------------------------------------------| +| ----------------- | ------- | -------------- | -------- | -------------------------------------------------------------------------------- | | dataProvider | object | dataProvider | yes | communicates with your API | | schemaAnalyzer | object | schemaAnalyzer | yes | retrieves resource type according to [Schema.org](https://schema.org) vocabulary | | theme | object | theme | no | theme of your Admin App | @@ -50,19 +49,17 @@ Otherwise, you can pass it your own CRUD components using `create`, `list`, `edi ```javascript // App.js -import { AdminGuesser, ResourceGuesser } from "@api-platform/admin"; +import { AdminGuesser, ResourceGuesser } from '@api-platform/admin'; const App = () => ( - + + edit={BooksEdit} + /> ); @@ -73,7 +70,7 @@ export default App; #### ResourceGuesser Props | Name | Type | Value | required | Description | -|------|--------|-------|----------|--------------------------| +| ---- | ------ | ----- | -------- | ------------------------ | | name | string | - | yes | endpoint of the resource | You can also use props accepted by React Admin [Resource component](https://marmelab.com/react-admin/Resource.html). For example, the props `list`, `show`, `create` or `edit`. @@ -91,10 +88,10 @@ By default, `` comes with [Pagination](components.md#pagination). ```javascript // BooksList.js -import { FieldGuesser, ListGuesser } from "@api-platform/admin"; -import { ReferenceField, TextField } from "react-admin"; +import { FieldGuesser, ListGuesser } from '@api-platform/admin'; +import { ReferenceField, TextField } from 'react-admin'; -export const BooksList = props => ( +export const BooksList = (props) => ( @@ -107,9 +104,9 @@ export const BooksList = props => ( #### ListGuesser Props -| Name | Type | Value | required | Description | -|----------|---------|-------|----------|-----------------------------------------| -| filters | element | - | no | filters that can be applied to the list | +| Name | Type | Value | required | Description | +| ------- | ------- | ----- | -------- | --------------------------------------- | +| filters | element | - | no | filters that can be applied to the list | You can also use props accepted by React Admin [List](https://marmelab.com/react-admin/List.html). @@ -120,9 +117,9 @@ For simple inputs, you can pass as children API Platform Admin [InputGuesser](co ```javascript // BooksCreate.js -import { CreateGuesser, InputGuesser } from "@api-platform/admin"; +import { CreateGuesser, InputGuesser } from '@api-platform/admin'; -export const BooksCreate = props => ( +export const BooksCreate = (props) => ( @@ -144,9 +141,9 @@ For simple inputs, you can use API Platform Admin [InputGuesser](components.md#i ```javascript // BooksEdit.js -import { EditGuesser, InputGuesser } from "@api-platform/admin"; +import { EditGuesser, InputGuesser } from '@api-platform/admin'; -export const BooksEdit = props => ( +export const BooksEdit = (props) => ( @@ -167,9 +164,9 @@ Displays a detailed page for one item. Based on React Admin [Show component](htt ```javascript // BooksShow.js -import { FieldGuesser, ShowGuesser } from "@api-platform/admin"; +import { FieldGuesser, ShowGuesser } from '@api-platform/admin'; -export const BooksShow = props => ( +export const BooksShow = (props) => ( @@ -193,16 +190,16 @@ If you want to use other formats (see supported formats: `@api-platform/api-doc- ```javascript // App.js -import { HydraAdmin, ResourceGuesser } from "@api-platform/admin"; +import { HydraAdmin, ResourceGuesser } from '@api-platform/admin'; const App = () => ( - - { /* ... */ } + > + + {/* ... */} ); @@ -212,12 +209,13 @@ export default App; #### HydraAdmin Props | Name | Type | Value | required | Description | -|--------------|---------------------|--------------|----------|------------------------------| +| ------------ | ------------------- | ------------ | -------- | ---------------------------- | | entrypoint | string | - | yes | entrypoint of the API | -| mercure | object|boolean | * | no | configuration to use Mercure | +| mercure | object|boolean | \* | no | configuration to use Mercure | | dataProvider | object | dataProvider | no | hydra data provider to use | \* `false` to explicitly disable, `true` to enable with default parameters or an object with the following properties: + - `hub`: the URL to your Mercure hub - `jwt`: a subscriber JWT to access your Mercure hub - `topicUrl`: the topic URL of your resources @@ -241,7 +239,7 @@ If you want to use other formats (see supported formats: `@api-platform/api-doc- ```javascript // App.js -import { OpenApiAdmin, ResourceGuesser } from "@api-platform/admin"; +import { OpenApiAdmin, ResourceGuesser } from '@api-platform/admin'; const App = () => ( ( docEntrypoint={docEntrypoint} dataProvider={dataProvider} authProvider={authProvider} - > - - { /* ... */ } + > + + {/* ... */} ); @@ -261,13 +259,14 @@ export default App; #### OpenApiAdmin Props | Name | Type | Value | required | Description | -|---------------|---------------------|-------|----------|------------------------------| +| ------------- | ------------------- | ----- | -------- | ---------------------------- | | dataProvider | dataProvider | - | yes | data provider to use | | docEntrypoint | string | - | yes | doc entrypoint of the API | | entrypoint | string | - | yes | entrypoint of the API | -| mercure | object|boolean | * | no | configuration to use Mercure | +| mercure | object|boolean | \* | no | configuration to use Mercure | \* `false` to explicitly disable, `true` to enable with default parameters or an object with the following properties: + - `hub`: the URL to your Mercure hub - `jwt`: a subscriber JWT to access your Mercure hub - `topicUrl`: the topic URL of your resources @@ -291,9 +290,9 @@ Based on React Admin [field components](https://marmelab.com/react-admin/Fields. ```javascript // BooksShow.js -import { FieldGuesser, ShowGuesser } from "@api-platform/admin"; +import { FieldGuesser, ShowGuesser } from '@api-platform/admin'; -export const BooksShow = props => ( +export const BooksShow = (props) => ( @@ -301,13 +300,13 @@ export const BooksShow = props => ( -) +); ``` #### FieldGuesser Props | Name | Type | Value | required | Description | -|--------|--------|-------|----------|--------------------------------------| +| ------ | ------ | ----- | -------- | ------------------------------------ | | source | string | - | yes | name of the property of the resource | You can also use props accepted by React Admin [basic fields](https://marmelab.com/react-admin/Fields.html#basic-fields). @@ -319,5 +318,5 @@ Uses React Admin [input components](https://marmelab.com/react-admin/Inputs.html #### InputGuesser Props | Name | Type | Value | required | Description | -|--------|--------|-------|----------|--------------------------------------| +| ------ | ------ | ----- | -------- | ------------------------------------ | | source | string | - | yes | name of the property of the resource | diff --git a/admin/customizing.md b/admin/customizing.md index f2f155cc4b0..eac82936902 100644 --- a/admin/customizing.md +++ b/admin/customizing.md @@ -16,7 +16,7 @@ However, it's also possible to display only specific resources, and to order the To cherry-pick the resources to make available through the admin, pass a list of `` components as children of the root component: ```javascript -import { HydraAdmin, ResourceGuesser } from "@api-platform/admin"; +import { HydraAdmin, ResourceGuesser } from '@api-platform/admin'; export default () => ( @@ -40,10 +40,10 @@ import { HydraAdmin, ResourceGuesser, ListGuesser, - FieldGuesser -} from "@api-platform/admin"; + FieldGuesser, +} from '@api-platform/admin'; -const ReviewsList = props => ( +const ReviewsList = (props) => ( @@ -74,10 +74,10 @@ import { HydraAdmin, ResourceGuesser, ShowGuesser, - FieldGuesser -} from "@api-platform/admin"; + FieldGuesser, +} from '@api-platform/admin'; -const ReviewsShow = props => ( +const ReviewsShow = (props) => ( @@ -110,10 +110,10 @@ import { HydraAdmin, ResourceGuesser, CreateGuesser, - InputGuesser -} from "@api-platform/admin"; + InputGuesser, +} from '@api-platform/admin'; -const ReviewsCreate = props => ( +const ReviewsCreate = (props) => ( @@ -148,10 +148,10 @@ import { HydraAdmin, ResourceGuesser, EditGuesser, - InputGuesser -} from "@api-platform/admin"; + InputGuesser, +} from '@api-platform/admin'; -const ReviewsEdit = props => ( +const ReviewsEdit = (props) => ( @@ -167,7 +167,7 @@ const ReviewsEdit = props => ( export default () => ( - + {/* ... */} ); diff --git a/admin/file-upload.md b/admin/file-upload.md index bcc0c974001..6cc50907f57 100644 --- a/admin/file-upload.md +++ b/admin/file-upload.md @@ -13,10 +13,10 @@ import { HydraAdmin, ResourceGuesser, CreateGuesser, -} from "@api-platform/admin"; -import { FileField, FileInput } from "react-admin"; +} from '@api-platform/admin'; +import { FileField, FileInput } from 'react-admin'; -const MediaObjectsCreate = props => ( +const MediaObjectsCreate = (props) => ( diff --git a/admin/getting-started.md b/admin/getting-started.md index bb8c8c89baa..2eda179a5b1 100644 --- a/admin/getting-started.md +++ b/admin/getting-started.md @@ -25,13 +25,11 @@ To initialize API Platform Admin, register it in your application. For instance, if you used Create React App, replace the content of `src/App.js` by: ```javascript -import { HydraAdmin } from "@api-platform/admin"; +import { HydraAdmin } from '@api-platform/admin'; // Replace with your own API entrypoint // For instance if https://example.com/api/books is the path to the collection of book resources, then the entrypoint is https://example.com/api -export default () => ( - -); +export default () => ; ``` Be sure to make your API send proper [CORS HTTP headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) to allow @@ -47,14 +45,14 @@ Here is a sample configuration: # 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 3fe9d63c49c..c98641f6a8f 100644 --- a/admin/handling-relations.md +++ b/admin/handling-relations.md @@ -18,24 +18,21 @@ Embedded data is inserted to a local cache: it will not be necessary to make mor ```javascript // admin/src/App.js -import { HydraAdmin, fetchHydra, hydraDataProvider } from "@api-platform/admin"; -import { parseHydraDocumentation } from "@api-platform/api-doc-parser"; +import { HydraAdmin, fetchHydra, hydraDataProvider } from '@api-platform/admin'; +import { parseHydraDocumentation } from '@api-platform/api-doc-parser'; const entrypoint = process.env.REACT_APP_API_ENTRYPOINT; const dataProvider = hydraDataProvider({ - entrypoint, - httpClient: fetchHydra, - apiDocumentationParser: parseHydraDocumentation, - mercure: true, - useEmbedded: false, + entrypoint, + httpClient: fetchHydra, + apiDocumentationParser: parseHydraDocumentation, + mercure: true, + useEmbedded: false, }); export default () => ( - + ); ``` @@ -50,9 +47,9 @@ import { HydraAdmin, FieldGuesser, ListGuesser, - ResourceGuesser -} from "@api-platform/admin"; -import { TextField } from "react-admin"; + ResourceGuesser, +} from '@api-platform/admin'; +import { TextField } from 'react-admin'; const BooksList = (props) => ( @@ -64,17 +61,14 @@ const BooksList = (props) => ( export default () => ( - + ); ``` If `useEmbedded` is explicitly set to `false`, make sure you write the code as if the relation needs to be fetched as a reference. -In this case, you *cannot* use the dot separator to do so. +In this case, you _cannot_ use the dot separator to do so. Note that you cannot edit the embedded data directly with this behavior. @@ -109,15 +103,19 @@ import { HydraAdmin, FieldGuesser, ListGuesser, - ResourceGuesser -} from "@api-platform/admin"; -import { ReferenceField, TextField } from "react-admin"; + ResourceGuesser, +} from '@api-platform/admin'; +import { ReferenceField, TextField } from 'react-admin'; const BooksList = (props) => ( {/* Use react-admin components directly when you want complex fields. */} - + @@ -125,10 +123,7 @@ const BooksList = (props) => ( export default () => ( - + ); @@ -181,11 +176,11 @@ class Book #[ORM\Id, ORM\Column, ORM\GeneratedValue] public ?int $id = null; - #[ORM\Column] + #[ORM\Column] #[ApiFilter(SearchFilter::class, strategy: 'ipartial')] public string $title; - #[ORM\OneToMany(targetEntity: Review::class, mappedBy: 'book')] + #[ORM\OneToMany(targetEntity: Review::class, mappedBy: 'book')] public $reviews; public function __construct() @@ -205,19 +200,16 @@ import { ResourceGuesser, CreateGuesser, EditGuesser, - InputGuesser -} from "@api-platform/admin"; -import { ReferenceInput, AutocompleteInput } from "react-admin"; + InputGuesser, +} from '@api-platform/admin'; +import { ReferenceInput, AutocompleteInput } from 'react-admin'; -const ReviewsCreate = props => ( +const ReviewsCreate = (props) => ( - + ({ title: searchText })} + filterToQuery={(searchText) => ({ title: searchText })} optionText="title" label="Books" /> @@ -229,16 +221,13 @@ const ReviewsCreate = props => ( ); -const ReviewsEdit = props => ( +const ReviewsEdit = (props) => ( - + ({ title: searchText })} + filterToQuery={(searchText) => ({ title: searchText })} optionText="title" label="Books" /> @@ -252,11 +241,7 @@ const ReviewsEdit = props => ( export default () => ( - + ); ``` @@ -270,19 +255,16 @@ import { ResourceGuesser, CreateGuesser, EditGuesser, - InputGuesser -} from "@api-platform/admin"; -import { ReferenceInput, AutocompleteInput } from "react-admin"; + InputGuesser, +} from '@api-platform/admin'; +import { ReferenceInput, AutocompleteInput } from 'react-admin'; -const ReviewsCreate = props => ( +const ReviewsCreate = (props) => ( - + ({ title: searchText })} + filterToQuery={(searchText) => ({ title: searchText })} optionText="title" label="Books" /> @@ -294,17 +276,14 @@ const ReviewsCreate = props => ( ); -const ReviewsEdit = props => ( +const ReviewsEdit = (props) => ( - + ({ title: searchText })} - format={v => v['@id'] || v} + filterToQuery={(searchText) => ({ title: searchText })} + format={(v) => v['@id'] || v} optionText="title" label="Books" /> @@ -318,11 +297,7 @@ const ReviewsEdit = props => ( export default () => ( - + ); ``` diff --git a/admin/index.md b/admin/index.md index 37b7bc981d6..0349b705bcf 100644 --- a/admin/index.md +++ b/admin/index.md @@ -21,16 +21,16 @@ You can **customize everything** by using provided React Admin and [MUI](https:/ ## Features -* Automatically generates an admin interface for all the resources of the API thanks to the hypermedia features of Hydra or to the OpenAPI documentation -* Generates 'list', 'create', 'show', and 'edit' screens, as well as a delete button -* Generates suitable inputs and fields according to the API doc (e.g. number HTML input for numbers, checkbox for booleans, selectbox for relationships...) -* Generates suitable inputs and fields according to Schema.org types if available (e.g. email field for `https://schema.org/email`) -* Handles relationships -* Supports pagination -* Supports filters and ordering -* Automatically validates whether a field is mandatory client-side according to the API description -* Sends proper HTTP requests to the API and decodes them using Hydra and JSON-LD formats if available -* Nicely displays server-side errors (e.g. advanced validation) -* Supports real-time updates with [Mercure](https://mercure.rocks) -* All the [features provided by React-admin](https://marmelab.com/react-admin/Tutorial.html) can also be used -* **100% customizable** +- Automatically generates an admin interface for all the resources of the API thanks to the hypermedia features of Hydra or to the OpenAPI documentation +- Generates 'list', 'create', 'show', and 'edit' screens, as well as a delete button +- Generates suitable inputs and fields according to the API doc (e.g. number HTML input for numbers, checkbox for booleans, selectbox for relationships...) +- Generates suitable inputs and fields according to Schema.org types if available (e.g. email field for `https://schema.org/email`) +- Handles relationships +- Supports pagination +- Supports filters and ordering +- Automatically validates whether a field is mandatory client-side according to the API description +- Sends proper HTTP requests to the API and decodes them using Hydra and JSON-LD formats if available +- Nicely displays server-side errors (e.g. advanced validation) +- Supports real-time updates with [Mercure](https://mercure.rocks) +- All the [features provided by React-admin](https://marmelab.com/react-admin/Tutorial.html) can also be used +- **100% customizable** diff --git a/admin/openapi.md b/admin/openapi.md index 59fa6f19c47..cfba6580cfd 100644 --- a/admin/openapi.md +++ b/admin/openapi.md @@ -5,10 +5,13 @@ API Platform Admin has native support for API exposing an [OpenAPI documentation To use it, use the `OpenApiAdmin` component, with the entry point of the API and the entry point of the OpenAPI documentation in JSON: ```javascript -import { OpenApiAdmin } from "@api-platform/admin"; +import { OpenApiAdmin } from '@api-platform/admin'; export default () => ( - + ); ``` @@ -24,12 +27,12 @@ By default, the component will use a basic data provider, without pagination sup If you want to use [another data provider](https://marmelab.com/react-admin/DataProviderList.html), pass the `dataProvider` prop to the component: ```javascript -import { OpenApiAdmin } from "@api-platform/admin"; -import drfProvider from "ra-data-django-rest-framework"; +import { OpenApiAdmin } from '@api-platform/admin'; +import drfProvider from 'ra-data-django-rest-framework'; export default () => ( diff --git a/admin/real-time-mercure.md b/admin/real-time-mercure.md index 5368610b4c2..02eb83c0554 100644 --- a/admin/real-time-mercure.md +++ b/admin/real-time-mercure.md @@ -13,13 +13,13 @@ Once enabled, API Platform Admin for Hydra will automatically detect that Mercur 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 () => ( ); ``` @@ -27,18 +27,19 @@ 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" }, + mercure: { hub: 'https://mercure.rocks/hub' }, }); ``` The `mercure` object can take the following properties: + - `hub`: the URL to your Mercure hub (default value: null ; when null it will be discovered by using API responses) - `jwt`: a subscriber JWT to access your Mercure hub (default value: null) - `topicUrl`: the topic URL of your resources (default value: entrypoint) diff --git a/admin/schema.org.md b/admin/schema.org.md index feb76d1b561..f8ecc7b7c75 100644 --- a/admin/schema.org.md +++ b/admin/schema.org.md @@ -30,8 +30,8 @@ Besides, it is also possible to use the documentation to customize some fields a The following Schema.org types are currently supported by API Platform Admin: -* `https://schema.org/email`: the field will be rendered using the `` React Admin component -* `https://schema.org/url`: the field will be rendered using the `` React Admin component -* `https://schema.org/identifier`: the field will be formatted properly in inputs +- `https://schema.org/email`: the field will be rendered using the `` React Admin component +- `https://schema.org/url`: the field will be rendered using the `` 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))! diff --git a/core/angularjs-integration.md b/core/angularjs-integration.md index 63aec43ddfb..a0147a9f2de 100644 --- a/core/angularjs-integration.md +++ b/core/angularjs-integration.md @@ -1,11 +1,11 @@ -# AngularJS Integration +# Angular Integration Warning: for a new project, you should consider using [the API Platform's Progressive Web App generator](../create-client/index.md) (that supports React and Vue.js) instead of this Angular v1 integration. ## Restangular -API Platform works fine with [AngularJS v1](https://angularjs.org/). The popular [Restangular](https://github.com/mgonto/restangular) +API Platform works fine with [Angular v1](https://angularjs.org/). The popular [Restangular](https://github.com/mgonto/restangular) REST client library for Angular can easily be configured to handle the API format. Here is a working Restangular config: @@ -13,53 +13,54 @@ Here is a working Restangular config: ```javascript 'use strict'; -var app = angular - .module('myAngularjsApp') - .config(['RestangularProvider', function(RestangularProvider) { - // The URL of the API endpoint - RestangularProvider.setBaseUrl('http://localhost:8000'); - - // JSON-LD @id support - RestangularProvider.setRestangularFields({ - id: '@id', - selfLink: '@id' +var app = angular.module('myAngularjsApp').config([ + 'RestangularProvider', + function (RestangularProvider) { + // The URL of the API endpoint + RestangularProvider.setBaseUrl('http://localhost:8000'); + + // JSON-LD @id support + RestangularProvider.setRestangularFields({ + id: '@id', + selfLink: '@id', + }); + RestangularProvider.setSelfLinkAbsoluteUrl(false); + + // Hydra collections support + RestangularProvider.addResponseInterceptor(function (data, operation) { + // Remove trailing slash to make Restangular working + function populateHref(data) { + if (data['@id']) { + data.href = data['@id'].substring(1); + } + } + + // Populate href property for the collection + populateHref(data); + + if ('getList' === operation) { + var collectionResponse = data['member']; + collectionResponse.metadata = {}; + + // Put metadata in a property of the collection + angular.forEach(data, function (value, key) { + if ('member' !== key) { + collectionResponse.metadata[key] = value; + } }); - RestangularProvider.setSelfLinkAbsoluteUrl(false); - - // Hydra collections support - RestangularProvider.addResponseInterceptor(function(data, operation) { - // Remove trailing slash to make Restangular working - function populateHref(data) { - if (data['@id']) { - data.href = data['@id'].substring(1); - } - } - - // Populate href property for the collection - populateHref(data); - - if ('getList' === operation) { - var collectionResponse = data['member']; - collectionResponse.metadata = {}; - - // Put metadata in a property of the collection - angular.forEach(data, function(value, key) { - if ('member' !== key) { - collectionResponse.metadata[key] = value; - } - }); - - // Populate href property for all elements of the collection - angular.forEach(collectionResponse, function(value) { - populateHref(value); - }); - - return collectionResponse; - } - - return data; + + // Populate href property for all elements of the collection + angular.forEach(collectionResponse, function (value) { + populateHref(value); }); - }]); + + return collectionResponse; + } + + return data; + }); + }, +]); ``` ## ng-admin @@ -73,25 +74,22 @@ then create your entities like in the following example : var nga = NgAdminConfigurationProvider; var admin = nga - .application('My First Admin') - .baseApiUrl('http://localhost:8000'); + .application('My First Admin') + .baseApiUrl('http://localhost:8000'); var article = nga.entity('articles'); article.identifier(nga.field('@id')); -article.url(function(entityName, viewType, identifierValue) { - var url = '/' + entityName; +article.url(function (entityName, viewType, identifierValue) { + var url = '/' + entityName; - if (viewType === 'ListView' || viewType === 'CreateView') { - return url; - } + if (viewType === 'ListView' || viewType === 'CreateView') { + return url; + } - return identifierValue ? decodeURIComponent(identifierValue) : url; + return identifierValue ? decodeURIComponent(identifierValue) : url; }); -article.listView().fields([ - nga.field('title'), - nga.field('content') -]); +article.listView().fields([nga.field('title'), nga.field('content')]); admin.addEntity(article); nga.configure(admin); diff --git a/core/bootstrap.md b/core/bootstrap.md index 025128425bb..556a581deda 100644 --- a/core/bootstrap.md +++ b/core/bootstrap.md @@ -181,7 +181,7 @@ $propertyInfo = new PropertyInfoExtractor( $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader()); -final class FilterLocator implements ContainerInterface +final class FilterLocator implements ContainerInterface { private $filters = []; public function get(string $id) { @@ -189,12 +189,12 @@ final class FilterLocator implements ContainerInterface } public function has(string $id): bool { - return isset($this->filter[$id]); + return isset($this->filter[$id]); } } $filterLocator = new FilterLocator(); -$pathSegmentNameGenerator = new UnderscorePathSegmentNameGenerator(); +$pathSegmentNameGenerator = new UnderscorePathSegmentNameGenerator(); $resourceNameCollectionFactory = new AttributesResourceNameCollectionFactory([__DIR__.'/../src/']); $resourceClassResolver = new ResourceClassResolver($resourceNameCollectionFactory); @@ -277,7 +277,7 @@ $stateProcessors = new CallableProcessor($processorCollection); class Validator implements ValidatorInterface { private $validator; - public function __construct($validator) + public function __construct($validator) { $this->validator = $validator; } @@ -328,7 +328,7 @@ class ApiLoader { private $resourceMetadataFactory; public function __construct( - ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, + ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory ) { $this->resourceNameCollectionFactory = $resourceNameCollectionFactory; @@ -396,14 +396,14 @@ $requestContext = new RequestContext(); $matcher = new UrlMatcher($routes, $requestContext); $generator = new UrlGenerator($routes, $requestContext); -class Router implements RouterInterface +class Router implements RouterInterface { private $routes; private $context; private $matcher; private $generator; - public function __construct(RouteCollection $routes, UrlMatcherInterface $matcher, UrlGeneratorInterface $generator, RequestContext $requestContext) + public function __construct(RouteCollection $routes, UrlMatcherInterface $matcher, UrlGeneratorInterface $generator, RequestContext $requestContext) { $this->routes = $routes; $this->matcher = $matcher; @@ -455,9 +455,9 @@ $uriVariableTransformers = [ ]; $iriConverter = new IriConverter( - $stateProviders, - $router, - $identifiersExtractor, + $stateProviders, + $router, + $identifiersExtractor, $resourceClassResolver, $resourceMetadataFactory, new UriVariablesConverter($propertyMetadataFactory, $resourceMetadataFactory, $uriVariableTransformers), @@ -466,9 +466,9 @@ $iriConverter = new IriConverter( $writeListener = new WriteListener( $stateProcessors, - $iriConverter, - $resourceClassResolver, - $resourceMetadataFactory, + $iriConverter, + $resourceClassResolver, + $resourceMetadataFactory, /**new UriVariablesConverter($propertyMetadataFactory, $resourceMetadataFactory, $uriVariableTransformers)*/ null, ); @@ -500,14 +500,14 @@ $problemErrorNormalizer = new ErrorNormalizer($debug, $defaultContext); // ); $itemNormalizer = new ItemNormalizer( - $propertyNameCollectionFactory, - $propertyMetadataFactory, - $iriConverter, - $resourceClassResolver, + $propertyNameCollectionFactory, + $propertyMetadataFactory, + $iriConverter, + $resourceClassResolver, $propertyAccessor, - $nameConverter, + $nameConverter, $classMetadataFactory, - $logger, + $logger, $resourceMetadataFactory, /**$resourceAccessChecker **/ null, $defaultContext @@ -590,7 +590,7 @@ $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 +// 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); @@ -600,14 +600,14 @@ $dispatcher->addListener('kernel.exception', [$validationExceptionListener, 'onK $dispatcher->addListener('kernel.response', [$addLinkHeaderListener, 'onKernelResponse'], 2); /* - * TODO: + * TODO: * api_platform.security.listener.request.deny_access kernel.request onSecurity 3 ApiPlatform\Security\EventListener\DenyAccessListener - * " kernel.request onSecurityPostDenormalize 1 + * " 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 +final class DocumentationAction { private $openApiFactory; public function __construct(OpenApiFactoryInterface $openApiFactory) diff --git a/core/configuration.md b/core/configuration.md index b461b08f9bd..922a799ae54 100644 --- a/core/configuration.md +++ b/core/configuration.md @@ -292,95 +292,94 @@ If you need to globally configure all the resources instead of adding configurat ```yaml # api/config/packages/api_platform.yaml api_platform: + defaults: + description: ~ + iri: ~ + short_name: ~ + item_operations: ~ + collection_operations: ~ - defaults: - description: ~ - iri: ~ - short_name: ~ - item_operations: ~ - collection_operations: ~ - - graphql: ~ + graphql: ~ - elasticsearch: ~ + elasticsearch: ~ - security: ~ - security_message: ~ - security_post_denormalize: ~ - security_post_denormalize_message: ~ + security: ~ + security_message: ~ + security_post_denormalize: ~ + security_post_denormalize_message: ~ - cache_headers: - # Automatically generate etags for API responses. - etag: true + 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 max age. + max_age: 3600 - # Default value for the response shared (proxy) max age. - shared_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'] + # Default values of the "Vary" HTTP header. + vary: ['Accept'] - invalidation: - xkey: - glue: ' ' + 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: ~ + 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 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 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 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 + # 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 default number of items per page. + pagination_items_per_page: 30 - # The maximum number of items per page. - pagination_maximum_items_per_page: ~ + # 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 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: ~ + # To use cursor-based pagination. + pagination_via_cursor: ~ - pagination_fetch_join_collection: ~ + pagination_fetch_join_collection: ~ - route_prefix: ~ - validation_groups: ~ - sunset: ~ - input: ~ - output: ~ - stateless: ~ + route_prefix: ~ + validation_groups: ~ + sunset: ~ + input: ~ + output: ~ + stateless: ~ - # The URL generation strategy to use for IRIs - url_generation_strategy: !php/const ApiPlatform\Api\UrlGeneratorInterface::ABS_PATH + # The URL generation strategy to use for IRIs + url_generation_strategy: !php/const ApiPlatform\Api\UrlGeneratorInterface::ABS_PATH - # To enable collecting denormalization errors - collectDenormalizationErrors: false + # To enable collecting denormalization errors + collectDenormalizationErrors: false - # ... + # ... ``` diff --git a/core/content-negotiation.md b/core/content-negotiation.md index b1ed788485e..568ff186afa 100644 --- a/core/content-negotiation.md +++ b/core/content-negotiation.md @@ -13,22 +13,22 @@ API Platform also supports [JSON Merge Patch (RFC 7396)](https://tools.ietf.org/ 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 +- enabled formats (see below) +- the requested format, specified in either [the `Accept` HTTP header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept) or as an extension appended to the URL Available formats are: -Format | Format name | MIME types | Backward Compatibility guaranteed -----------------------------------------------------------------|--------------|-------------------------------|---------------------------------------- -[JSON-LD](https://json-ld.org) | `jsonld` | `application/ld+json` | yes -[GraphQL](graphql.md) | n/a | n/a | yes -[JSON:API](https://jsonapi.org/) | `jsonapi` | `application/vnd.api+json` | yes -[HAL](https://stateless.group/hal_specification.html) | `jsonhal` | `application/hal+json` | yes -[YAML](https://yaml.org/) | `yaml` | `application/x-yaml` | no -[CSV](https://tools.ietf.org/html/rfc4180) | `csv` | `text/csv` | no -[HTML](https://whatwg.org/) (API docs) | `html` | `text/html` | no -[XML](https://www.w3.org/XML/) | `xml` | `application/xml`, `text/xml` | no -[JSON](https://www.json.org/) | `json` | `application/json` | no +| Format | Format name | MIME types | Backward Compatibility guaranteed | +| ----------------------------------------------------- | ----------- | ----------------------------- | --------------------------------- | +| [JSON-LD](https://json-ld.org) | `jsonld` | `application/ld+json` | yes | +| [GraphQL](graphql.md) | n/a | n/a | yes | +| [JSON:API](https://jsonapi.org/) | `jsonapi` | `application/vnd.api+json` | yes | +| [HAL](https://stateless.group/hal_specification.html) | `jsonhal` | `application/hal+json` | yes | +| [YAML](https://yaml.org/) | `yaml` | `application/x-yaml` | no | +| [CSV](https://tools.ietf.org/html/rfc4180) | `csv` | `text/csv` | no | +| [HTML](https://whatwg.org/) (API docs) | `html` | `text/html` | no | +| [XML](https://www.w3.org/XML/) | `xml` | `application/xml`, `text/xml` | no | +| [JSON](https://www.json.org/) | `json` | `application/json` | no | If the client's requested format is not specified, the response format will be the first format defined in the `formats` configuration key (see below). If the request format is not supported, an [Unsupported Media Type](https://developer.mozilla.org/fr/docs/Web/HTTP/Status/415) error will be returned. @@ -43,16 +43,16 @@ and of a custom format called `myformat` and having `application/vnd.myformat` a ```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). @@ -70,9 +70,9 @@ 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. @@ -86,10 +86,10 @@ 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 @@ -146,14 +146,14 @@ class Book ```yaml resources: - App\Entity\Book: + App\Entity\Book: + formats: + 0: 'jsonld' # format already defined in the config + csv: 'text/csv' + operations: + ApiPlatform\Metadata\Get: formats: - 0: 'jsonld' # format already defined in the config - csv: 'text/csv' - operations: - ApiPlatform\Metadata\Get: - formats: - json: ['application/merge-patch+json'] # works also with "application/merge-patch+json" + json: ['application/merge-patch+json'] # works also with "application/merge-patch+json" ``` ```xml @@ -191,9 +191,9 @@ 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'] ``` API Platform will automatically call the serializer with your defined format name as `format` parameter during the deserialization process (`myformat` in the example). @@ -208,12 +208,12 @@ 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 diff --git a/core/controllers.md b/core/controllers.md index 77d249ac1a6..283b4bf78ca 100644 --- a/core/controllers.md +++ b/core/controllers.md @@ -12,11 +12,11 @@ To enable this feature use `use_symfony_listeners: true` in your `api_platform` ```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 @@ -111,8 +111,8 @@ use App\Controller\CreateBookPublication; #[ApiResource(operations: [ new Get(), new Post( - name: 'publication', - uriTemplate: '/books/{id}/publication', + name: 'publication', + uriTemplate: '/books/{id}/publication', controller: CreateBookPublication::class ) ])] @@ -125,14 +125,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 @@ -183,8 +183,8 @@ use ApiPlatform\Metadata\Post; #[ApiResource(operations: [ new Get(), new Post( - name: 'publication', - uriTemplate: '/books/{id}/publication', + name: 'publication', + uriTemplate: '/books/{id}/publication', controller: PlaceholderAction::class ) ])] @@ -197,14 +197,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 @@ -248,9 +248,9 @@ use Symfony\Component\Serializer\Annotation\Groups; #[ApiResource(operations: [ new Get(), new Post( - name: 'publication', - uriTemplate: '/books/{id}/publication', - controller: CreateBookPublication::class, + name: 'publication', + uriTemplate: '/books/{id}/publication', + controller: CreateBookPublication::class, normalizationContext: ['groups' => ['publication']], ) ])] @@ -268,15 +268,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 @@ -325,9 +325,9 @@ use App\Controller\CreateBookPublication; #[ApiResource(operations: [ new Get(), new Post( - name: 'publication', - uriTemplate: '/books/{id}/publication', - controller: CreateBookPublication::class, + name: 'publication', + uriTemplate: '/books/{id}/publication', + controller: CreateBookPublication::class, read: false ) ])] @@ -340,14 +340,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 @@ -413,14 +413,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 @@ -508,10 +508,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/core/default-order.md b/core/default-order.md index b216610837a..0489add6811 100644 --- a/core/default-order.md +++ b/core/default-order.md @@ -23,7 +23,7 @@ class Book * ... */ public $foo; - + // ... } ``` @@ -31,8 +31,8 @@ class Book ```yaml # api/config/api_platform/resources/Book.yaml App\Entity\Book: - order: - foo: ASC + order: + foo: ASC ``` @@ -63,7 +63,7 @@ class Book * ... */ public $bar; - + // ... } ``` @@ -71,7 +71,7 @@ class Book ```yaml # api/config/api_platform/resources/Book.yaml App\Entity\Book: - order: ['foo', 'bar'] + order: ['foo', 'bar'] ``` @@ -96,7 +96,7 @@ class Book * @var User */ public $author; - + // ... } ``` @@ -104,7 +104,7 @@ class Book ```yaml # api/config/api_platform/resources/Book.yaml App\Entity\Book: - order: ['author.username'] + order: ['author.username'] ``` @@ -134,7 +134,7 @@ class Book * @var string */ public $name; - + // ... } ``` @@ -142,17 +142,17 @@ class Book ```yaml # api/config/api_platform/resources/Book.yaml App\Entity\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 91acde9a0fc..df648023268 100644 --- a/core/deprecations.md +++ b/core/deprecations.md @@ -84,7 +84,7 @@ class Review #[ApiProperty(deprecationReason: "Use the rating property instead")] public $letter; - + // ... } ``` @@ -92,18 +92,18 @@ class Review ```yaml # api/config/api_platform/resources/Review.yaml properties: + # ... + App\Entity\Review: # ... - App\Entity\Review: - # ... - letter: - deprecationReason: 'Use the rating property instead' + letter: + deprecationReason: 'Use the rating property instead' ``` -* 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 diff --git a/core/design.md b/core/design.md index 83cb29b3836..8253465ecf2 100644 --- a/core/design.md +++ b/core/design.md @@ -21,10 +21,10 @@ To do so, there is another interface to implement: [`ProcessorInterface`](state- This class will read the API resource object (the one marked with `#[ApiResource]`) and: -* persist it directly in the database; -* or hydrate a DTO then trigger a command; -* or populate an event store; -* or persist the data in any other useful way. +- persist it directly in the database; +- or hydrate a DTO then trigger a command; +- or populate an event store; +- or persist the data in any other useful way. The logic of state processors is the responsibility of application developers, and is **out of the API Platform's scope**. @@ -45,8 +45,8 @@ or [CQRS](https://martinfowler.com/bliki/CQRS.html) thanks to [the Messenger Com Last but not least, to create [Event Sourcing](https://martinfowler.com/eaaDev/EventSourcing.html)-based systems, a convenient approach is: -* to persist data in an event store using a Messenger handler or a custom [state processor](state-processors.md) -* to create projections in standard RDBMS (PostgreSQL, MariaDB...) tables or views -* to map those projections with read-only Doctrine entity classes **and** to mark those classes with `#[ApiResource]` +- to persist data in an event store using a Messenger handler or a custom [state processor](state-processors.md) +- to create projections in standard RDBMS (PostgreSQL, MariaDB...) tables or views +- to map those projections with read-only Doctrine entity classes **and** to mark those classes with `#[ApiResource]` You can then benefit from the built-in Doctrine filters, sorting, pagination, auto-joins and all of [the extension points](extending.md) provided by API Platform. diff --git a/core/dto.md b/core/dto.md index e9bc870b146..8eeeeb94281 100644 --- a/core/dto.md +++ b/core/dto.md @@ -153,15 +153,17 @@ use App\State\BookRepresentationProcessor; #[Post(output: AnotherRepresentation::class, processor: BookRepresentationProcessor::class)] class Book {} ``` + ```yaml # api/config/api_platform/resources.yaml resources: - App\Entity\Book: - operations: - ApiPlatform\Metadata\Post: - output: App\Dto\AnotherRepresentation - processor: App\State\BookRepresentationProcessor + App\Entity\Book: + operations: + ApiPlatform\Metadata\Post: + output: App\Dto\AnotherRepresentation + processor: App\State\BookRepresentationProcessor ``` + ```xml @@ -172,9 +174,9 @@ resources: https://api-platform.com/schema/metadata/resources-3.0.xsd"> - + output="App\Dto\AnotherRepresentation" /> diff --git a/core/elasticsearch.md b/core/elasticsearch.md index b41fcb28406..7e5f931720b 100644 --- a/core/elasticsearch.md +++ b/core/elasticsearch.md @@ -23,30 +23,30 @@ Then, enable it inside the API Platform configuration: ```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)%'] - #... + #... ``` ## Creating Models API Platform follows the best practices of Elasticsearch: -* a single index per resource should be used because Elasticsearch is going to [drop support for index types and will allow only a single type per -index](https://www.elastic.co/guide/en/elasticsearch/reference/current/removal-of-types.html); -* index name should be the short resource name in lower snake case; -* the default `_doc` type should be used; -* all fields should be lower case and should use camel case for combining words. +- a single index per resource should be used because Elasticsearch is going to [drop support for index types and will allow only a single type per + index](https://www.elastic.co/guide/en/elasticsearch/reference/current/removal-of-types.html); +- index name should be the short resource name in lower snake_case; +- the default `_doc` type should be used; +- all fields should be lower case and should use camelCase for combining words. This involves having mappings and models which absolutely match each other. @@ -212,8 +212,8 @@ class Tweet } ``` -API Platform will automatically disable write operations and snake case document fields will automatically be converted to -camel case 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 diff --git a/core/errors.md b/core/errors.md index 5d8ffd6be0d..a65c33c4065 100644 --- a/core/errors.md +++ b/core/errors.md @@ -12,9 +12,9 @@ Use the following configuration: ```yaml api_platform: - defaults: - extra_properties: - rfc_7807_compliant_errors: false + defaults: + extra_properties: + rfc_7807_compliant_errors: false ``` This can also be configured on an `ApiResource` or in an `HttpOperation`, for example: @@ -100,19 +100,19 @@ errors: ```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 ``` 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 @@ -206,27 +206,27 @@ final class ErrorProvider implements ProviderInterface if ($status >= 500) { $error->setDetail('Something went wrong'); } - + return $error; } } ``` ```yaml - api_platform.state.error_provider: - class: 'App\State\ErrorProvider' - tags: - - key: 'api_platform.state.error_provider' - name: 'api_platform.state_provider' +api_platform.state.error_provider: + class: 'App\State\ErrorProvider' + tags: + - key: 'api_platform.state.error_provider' + name: 'api_platform.state_provider' ``` Note that our validation exception have their own error provider at: ```yaml api_platform.validator.state.error_provider: - tags: - - key: 'api_platform.validator.state.error_provider' - name: 'api_platform.state_provider' + tags: + - key: 'api_platform.validator.state.error_provider' + name: 'api_platform.state_provider' ``` ## Domain exceptions diff --git a/core/events.md b/core/events.md index ee5319f7c83..0efc1d5926b 100644 --- a/core/events.md +++ b/core/events.md @@ -28,40 +28,40 @@ are also available if you want to hook into the persistence layer's object lifec These built-in event listeners are registered for routes managed by API Platform: -Name | Event | [Pre & Post hooks](#custom-event-listeners) | Priority | Description -------------------------------|--------------------|---------------------------------------------|----------|------------- -`AddFormatListener` | `kernel.request` | None | 28 | Guesses the best response format ([content negotiation](content-negotiation.md)) -`ReadListener` | `kernel.request` | `PRE_READ`, `POST_READ` | 4 | Retrieves data from the persistence system using the [state providers](state-providers.md) (`GET`, `PUT`, `PATCH`, `DELETE`) -`QueryParameterValidateListener` | `kernel.request` | None | 2 | Validates query parameters -`DeserializeListener` | `kernel.request` | `PRE_DESERIALIZE`, `POST_DESERIALIZE` | 2 | Deserializes data into a PHP entity (`POST`); updates the entity retrieved using the state provider (`PUT`, `PATCH`) -`DenyAccessListener` | `kernel.request` | None | 1 | Enforces [access control](security.md) using Security expressions -`ValidateListener` | `kernel.view` | `PRE_VALIDATE`, `POST_VALIDATE` | 64 | [Validates data](validation.md) (`POST`, `PUT`, `PATCH`) -`WriteListener` | `kernel.view` | `PRE_WRITE`, `POST_WRITE` | 32 | Persists changes in the persistence system using the [state processors](state-processors.md) (`POST`, `PUT`, `PATCH`, `DELETE`) -`SerializeListener` | `kernel.view` | `PRE_SERIALIZE`, `POST_SERIALIZE` | 16 | Serializes the PHP entity in string [according to the request format](content-negotiation.md) -`RespondListener` | `kernel.view` | `PRE_RESPOND`, `POST_RESPOND` | 8 | Transforms serialized to a `Symfony\Component\HttpFoundation\Response` instance -`AddLinkHeaderListener` | `kernel.response` | None | 0 | Adds a `Link` HTTP header pointing to the Hydra documentation -`ValidationExceptionListener` | `kernel.exception` | None | 0 | Serializes validation exceptions in the Hydra format -`ExceptionListener` | `kernel.exception` | None | -96 | Serializes PHP exceptions in the Hydra format (including the stack trace in debug mode) +| Name | Event | [Pre & Post hooks](#custom-event-listeners) | Priority | Description | +| -------------------------------- | ------------------ | ------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------- | +| `AddFormatListener` | `kernel.request` | None | 28 | Guesses the best response format ([content negotiation](content-negotiation.md)) | +| `ReadListener` | `kernel.request` | `PRE_READ`, `POST_READ` | 4 | Retrieves data from the persistence system using the [state providers](state-providers.md) (`GET`, `PUT`, `PATCH`, `DELETE`) | +| `QueryParameterValidateListener` | `kernel.request` | None | 2 | Validates query parameters | +| `DeserializeListener` | `kernel.request` | `PRE_DESERIALIZE`, `POST_DESERIALIZE` | 2 | Deserializes data into a PHP entity (`POST`); updates the entity retrieved using the state provider (`PUT`, `PATCH`) | +| `DenyAccessListener` | `kernel.request` | None | 1 | Enforces [access control](security.md) using Security expressions | +| `ValidateListener` | `kernel.view` | `PRE_VALIDATE`, `POST_VALIDATE` | 64 | [Validates data](validation.md) (`POST`, `PUT`, `PATCH`) | +| `WriteListener` | `kernel.view` | `PRE_WRITE`, `POST_WRITE` | 32 | Persists changes in the persistence system using the [state processors](state-processors.md) (`POST`, `PUT`, `PATCH`, `DELETE`) | +| `SerializeListener` | `kernel.view` | `PRE_SERIALIZE`, `POST_SERIALIZE` | 16 | Serializes the PHP entity in string [according to the request format](content-negotiation.md) | +| `RespondListener` | `kernel.view` | `PRE_RESPOND`, `POST_RESPOND` | 8 | Transforms serialized to a `Symfony\Component\HttpFoundation\Response` instance | +| `AddLinkHeaderListener` | `kernel.response` | None | 0 | Adds a `Link` HTTP header pointing to the Hydra documentation | +| `ValidationExceptionListener` | `kernel.exception` | None | 0 | Serializes validation exceptions in the Hydra format | +| `ExceptionListener` | `kernel.exception` | None | -96 | Serializes PHP exceptions in the Hydra format (including the stack trace in debug mode) | Some of these built-in listeners can be enabled/disabled by setting operation attributes: -Attribute | Type | Default | Description ----------------------------|--------|---------|------------- -`query_parameter_validate` | `bool` | `true` | Enables or disables `QueryParameterValidateListener` -`read` | `bool` | `true` | Enables or disables `ReadListener` -`deserialize` | `bool` | `true` | Enables or disables `DeserializeListener` -`validate` | `bool` | `true` | Enables or disables `ValidateListener` -`write` | `bool` | `true` | Enables or disables `WriteListener` -`serialize` | `bool` | `true` | Enables or disables `SerializeListener` +| Attribute | Type | Default | Description | +| -------------------------- | ------ | ------- | ---------------------------------------------------- | +| `query_parameter_validate` | `bool` | `true` | Enables or disables `QueryParameterValidateListener` | +| `read` | `bool` | `true` | Enables or disables `ReadListener` | +| `deserialize` | `bool` | `true` | Enables or disables `DeserializeListener` | +| `validate` | `bool` | `true` | Enables or disables `ValidateListener` | +| `write` | `bool` | `true` | Enables or disables `WriteListener` | +| `serialize` | `bool` | `true` | Enables or disables `SerializeListener` | Some of these built-in listeners can be enabled/disabled by setting request attributes (for instance in the [`defaults` attribute of an operation](operations.md#recommended-method)): -Attribute | Type | Default | Description ----------------|--------|---------|------------- -`_api_receive` | `bool` | `true` | Enables or disables `ReadListener`, `DeserializeListener`, `ValidateListener` -`_api_respond` | `bool` | `true` | Enables or disables `SerializeListener`, `RespondListener` -`_api_persist` | `bool` | `true` | Enables or disables `WriteListener` +| Attribute | Type | Default | Description | +| -------------- | ------ | ------- | ----------------------------------------------------------------------------- | +| `_api_receive` | `bool` | `true` | Enables or disables `ReadListener`, `DeserializeListener`, `ValidateListener` | +| `_api_respond` | `bool` | `true` | Enables or disables `SerializeListener`, `RespondListener` | +| `_api_persist` | `bool` | `true` | Enables or disables `WriteListener` | ## Custom Event Listeners @@ -69,20 +69,20 @@ Registering your own event listeners to add extra logic is convenient. The [`ApiPlatform\Symfony\EventListener\EventPriorities`](https://github.com/api-platform/core/blob/main/src/Symfony/EventListener/EventPriorities.php) class comes with a convenient set of class constants corresponding to commonly used priorities: -Constant | Event | Priority | --------------------|-------------------|----------| -`PRE_READ` | `kernel.request` | 5 | -`POST_READ` | `kernel.request` | 3 | -`PRE_DESERIALIZE` | `kernel.request` | 3 | -`POST_DESERIALIZE` | `kernel.request` | 1 | -`PRE_VALIDATE` | `kernel.view` | 65 | -`POST_VALIDATE` | `kernel.view` | 63 | -`PRE_WRITE` | `kernel.view` | 33 | -`POST_WRITE` | `kernel.view` | 31 | -`PRE_SERIALIZE` | `kernel.view` | 17 | -`POST_SERIALIZE` | `kernel.view` | 15 | -`PRE_RESPOND` | `kernel.view` | 9 | -`POST_RESPOND` | `kernel.response` | 0 | +| Constant | Event | Priority | +| ------------------ | ----------------- | -------- | +| `PRE_READ` | `kernel.request` | 5 | +| `POST_READ` | `kernel.request` | 3 | +| `PRE_DESERIALIZE` | `kernel.request` | 3 | +| `POST_DESERIALIZE` | `kernel.request` | 1 | +| `PRE_VALIDATE` | `kernel.view` | 65 | +| `POST_VALIDATE` | `kernel.view` | 63 | +| `PRE_WRITE` | `kernel.view` | 33 | +| `POST_WRITE` | `kernel.view` | 31 | +| `PRE_SERIALIZE` | `kernel.view` | 17 | +| `POST_SERIALIZE` | `kernel.view` | 15 | +| `PRE_RESPOND` | `kernel.view` | 9 | +| `POST_RESPOND` | `kernel.response` | 0 | In the following example, we will send a mail each time a new book is created using the API: diff --git a/core/extending-jsonld-context.md b/core/extending-jsonld-context.md index b15763a886f..aa595bfdd98 100644 --- a/core/extending-jsonld-context.md +++ b/core/extending-jsonld-context.md @@ -33,7 +33,7 @@ class Book ] )] public $name; - + // ... } ``` @@ -89,10 +89,10 @@ class Book ```yaml # api/config/api_platform/resources.yaml resources: - App\Entity\Book: - operations: - ApiPlatform\Metadata\Get: - hydraContext: { foo: 'bar' } + App\Entity\Book: + operations: + ApiPlatform\Metadata\Get: + hydraContext: { foo: 'bar' } ``` ```xml @@ -105,7 +105,7 @@ resources: https://api-platform.com/schema/metadata/resources-3.0.xsd"> - + bar diff --git a/core/extending.md b/core/extending.md index 47f37af53e9..adfd4b39a80 100644 --- a/core/extending.md +++ b/core/extending.md @@ -7,7 +7,7 @@ Those extensions points are taken into account both by the REST and [GraphQL](gr The following tables summarizes which extension point to use depending on what you want to do: | Extension Point | Usage | -|------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| ---------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [State Providers](state-providers.md) | adapters for custom persistence layers, virtual fields, custom hydration | | [Denormalizers](serialization.md) | post-process objects created from the payload sent in the HTTP request body | | [Voters](security.md#hooking-custom-permission-checks-using-voters) | custom authorization logic | @@ -15,15 +15,15 @@ The following tables summarizes which extension point to use depending on what y | [State Processors](state-processors) | custom business logic and computations to trigger before or after persistence (ex: mail, call to an external API...) | | [Normalizers](serialization.md#decorating-a-serializer-and-adding-extra-data) | customize the resource sent to the client (add fields in JSON documents, encode codes, dates...) | | [Filters](filters.md) | create filters for collections and automatically document them (OpenAPI, GraphQL, Hydra) | -| [Serializer Context Builders](serialization.md#changing-the-serialization-context-dynamically) | change the Serialization context (e.g. groups) dynamically | +| [Serializer Context Builders](serialization.md#changing-the-serialization-context-dynamically) | change the Serialization context (e.g. groups) dynamically | | [Messenger Handlers](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 | +| [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) | ## Doctrine Specific Extension Points | Extension Point | Usage | -|------------------------------------------------------------|----------------------------------------------------------------------------------------------------| +| ---------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | | [Extensions](extensions.md) | Access to the query builder to change the DQL query | | [Filters](filters.md#doctrine-orm-and-mongodb-odm-filters) | Add filters documentations (OpenAPI, GraphQL, Hydra) and automatically apply them to the DQL query | @@ -59,6 +59,7 @@ flowchart TB ### Symfony Access Checker Provider When using Symfony, the access checker provider is used at three different stages: + - `api_platform.state_provider.access_checker.post_validate` decorates the `ValidateProvider` - `api_platform.state_provider.access_checker.post_deserialize` decorates the `DeserializeProvider` - `api_platform.state_provider.access_checker` decorates the `ReadProvider` @@ -121,14 +122,15 @@ 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! ### Laravel Processor Decoration + ```php ['media_object:read']], + normalizationContext: ['groups' => ['media_object:read']], types: ['https://schema.org/MediaObject'], outputFormats: ['jsonld' => ['application/ld+json']], operations: [ @@ -92,10 +92,10 @@ use Vich\UploaderBundle\Mapping\Annotation as Vich; content: new \ArrayObject([ 'multipart/form-data' => [ 'schema' => [ - 'type' => 'object', + 'type' => 'object', 'properties' => [ 'file' => [ - 'type' => 'string', + 'type' => 'string', 'format' => 'binary' ] ] @@ -121,7 +121,7 @@ class MediaObject public ?File $file = null; #[ApiProperty(writable: false)] - #[ORM\Column(nullable: true)] + #[ORM\Column(nullable: true)] public ?string $filePath = null; public function getId(): ?int @@ -130,6 +130,7 @@ 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. ### Resolving the File URL @@ -211,6 +212,7 @@ your data, you will get a response looking like this: You will need to modify your `Caddyfile` to allow the above `contentUrl` to be accessed directly. If you followed the above configuration for the VichUploaderBundle, that will be in `api/public/media`. Add your folder to the list of path matches, e.g. `|^/media/|`: + ```patch # Matches requests for HTML documents, for static files and for Next.js files, # except for known API paths and paths with extensions handled by API Platform @@ -224,6 +226,7 @@ You will need to modify your `Caddyfile` to allow the above `contentUrl` to be a ) || path('/favicon.ico', '/manifest.json', '/robots.txt', '/_next*', '/sitemap*')` ``` + ### Linking a MediaObject Resource to Another Resource @@ -255,7 +258,7 @@ class Book #[ORM\JoinColumn(nullable: true)] #[ApiProperty(types: ['https://schema.org/image'])] public ?MediaObject $image = null; - + // ... } ``` @@ -351,8 +354,8 @@ use Vich\UploaderBundle\Mapping\Annotation as Vich; */ #[ORM\Entity] #[ApiResource( - normalizationContext: ['groups' => ['book:read']], - denormalizationContext: ['groups' => ['book:write']], + normalizationContext: ['groups' => ['book:read']], + denormalizationContext: ['groups' => ['book:write']], types: ['https://schema.org/Book'], operations: [ new GetCollection(), @@ -376,9 +379,9 @@ class Book #[Groups(['book:write'])] public ?File $file = null; - #[ORM\Column(nullable: true)] + #[ORM\Column(nullable: true)] public ?string $filePath = null; - + // ... } ``` diff --git a/core/filters.md b/core/filters.md index 828afe81a9c..18f35216761 100644 --- a/core/filters.md +++ b/core/filters.md @@ -43,7 +43,7 @@ A parameter can alter the current Operation context, to do so use a `ApiPlatform ```php class GroupsParameterProvider implements ParameterProviderInterface { - public function provide(Parameter $parameter, array $uriVariables = [], array $context = []): HttpOperation + public function provide(Parameter $parameter, array $uriVariables = [], array $context = []): HttpOperation { $request = $context['request']; return $context['operation']->withNormalizationContext(['groups' => $request->query->all('groups')]); @@ -68,10 +68,10 @@ If you don't have autoconfiguration enabled, declare the parameter as a tagged s ```yaml services: - ApiPlatform\Tests\Fixtures\TestBundle\Parameter\CustomGroupParameterProvider: - tags: - - name: 'api_platform.parameter_provider' - key: 'ApiPlatform\Tests\Fixtures\TestBundle\Parameter\CustomGroupParameterProvider' + ApiPlatform\Tests\Fixtures\TestBundle\Parameter\CustomGroupParameterProvider: + tags: + - name: 'api_platform.parameter_provider' + key: 'ApiPlatform\Tests\Fixtures\TestBundle\Parameter\CustomGroupParameterProvider' ``` ### Call a filter @@ -81,12 +81,12 @@ A Parameter can also call a filter and works on filters that impact the data per ```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' ] + offer.order_filter: + parent: 'api_platform.doctrine.orm.order_filter' + arguments: + $properties: { id: ~, name: ~ } + $orderParameterName: order + tags: ['api_platform.filter'] ``` We can use this filter specifying we want a query parameter with the `:property` placeholder: @@ -230,12 +230,11 @@ class ValidateParameter {} You can also use your own constraint by setting the `constraints` option on a Parameter. In that case we won't setup the automatic validation for you and it'll replace our defaults. - ### Parameter security Parameters may have security checks: -``` php +```php [!WARNING] + > Prefer using QueryParameter instead of ApiFilter for more flexibility, this is subject to change in the next major version. ### Basic Knowledge @@ -373,16 +374,16 @@ 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 + # ... + 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: @@ -390,10 +391,10 @@ Alternatively, you can choose to use a dedicated file to gather filters together ```yaml # api/config/filters.yaml services: - offer.date_filter: - parent: 'api_platform.doctrine.orm.date_filter' - arguments: [ { dateProperty: ~ } ] - tags: [ 'api_platform.filter' ] + offer.date_filter: + parent: 'api_platform.doctrine.orm.date_filter' + arguments: [{ dateProperty: ~ }] + tags: ['api_platform.filter'] ``` We're linking the filter `offer.date_filter` with the resource like this: @@ -417,11 +418,11 @@ class Offer ```yaml # api/config/api_platform/resources.yaml resources: - App\Entity\Offer: - operations: - ApiPlatform\Metadata\GetCollection: - filters: ['offer.date_filter'] - # ... + App\Entity\Offer: + operations: + ApiPlatform\Metadata\GetCollection: + filters: ['offer.date_filter'] + # ... ``` ```xml @@ -482,10 +483,10 @@ If Doctrine ORM or MongoDB ODM support is enabled, adding filters is as easy as The search filter supports `exact`, `partial`, `start`, `end`, and `word_start` matching strategies: -* `partial` strategy uses `LIKE %text%` to search for fields that contain `text`. -* `start` strategy uses `LIKE text%` to search for fields that start with `text`. -* `end` strategy uses `LIKE %text` to search for fields that end with `text`. -* `word_start` strategy uses `LIKE text% OR LIKE % text%` to search for fields that contain words starting with `text`. +- `partial` strategy uses `LIKE %text%` to search for fields that contain `text`. +- `start` strategy uses `LIKE text%` to search for fields that start with `text`. +- `end` strategy uses `LIKE %text` to search for fields that end with `text`. +- `word_start` strategy uses `LIKE text% OR LIKE % text%` to search for fields that contain words starting with `text`. Prepend the letter `i` to the filter if you want it to be case insensitive. For example `ipartial` or `iexact`. Note that this will use the `LOWER` function and **will** impact performance [if there is no proper index](performance.md#search-filter). @@ -522,22 +523,22 @@ 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 + 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'] + # ... + operations: + ApiPlatform\Metadata\GetCollection: + filters: ['offer.search_filter'] ``` @@ -571,22 +572,22 @@ 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 + 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'] + # ... + operations: + ApiPlatform\Metadata\GetCollection: + filters: ['offer.search_filter'] ``` @@ -631,22 +632,22 @@ 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\Entity\Offer: - # ... - operations: - ApiPlatform\Metadata\GetCollection: - filters: ['offer.date_filter'] + # ... + operations: + ApiPlatform\Metadata\GetCollection: + filters: ['offer.date_filter'] ``` @@ -660,13 +661,13 @@ It will return all offers where `createdAt` is superior or equal to `2018-03-19` 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\Orm\Filter\DateFilter::EXCLUDE_NULL` (`exclude_null`) -Consider items as oldest | `ApiPlatform\Doctrine\Orm\Filter\DateFilter::INCLUDE_NULL_BEFORE` (`include_null_before`) -Consider items as youngest | `ApiPlatform\Doctrine\Orm\Filter\DateFilter::INCLUDE_NULL_AFTER` (`include_null_after`) -Always include items | `ApiPlatform\Doctrine\Orm\Filter\DateFilter::INCLUDE_NULL_BEFORE_AND_AFTER` (`include_null_before_and_after`) +| Description | Strategy to set | +| ------------------------------------ | ------------------------------------------------------------------------------------------------------------- | +| Use the default behavior of the DBMS | `null` | +| Exclude items | `ApiPlatform\Doctrine\Orm\Filter\DateFilter::EXCLUDE_NULL` (`exclude_null`) | +| Consider items as oldest | `ApiPlatform\Doctrine\Orm\Filter\DateFilter::INCLUDE_NULL_BEFORE` (`include_null_before`) | +| Consider items as youngest | `ApiPlatform\Doctrine\Orm\Filter\DateFilter::INCLUDE_NULL_AFTER` (`include_null_after`) | +| Always include items | `ApiPlatform\Doctrine\Orm\Filter\DateFilter::INCLUDE_NULL_BEFORE_AND_AFTER` (`include_null_before_and_after`) | For instance, exclude entries with a property value of `null` with the following service definition: @@ -692,22 +693,22 @@ 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 + 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 # config/api/Offer.yaml App\Entity\Offer: - # ... - operations: - ApiPlatform\Metadata\GetCollection: - filters: ['offer.date_filter'] + # ... + operations: + ApiPlatform\Metadata\GetCollection: + filters: ['offer.date_filter'] ``` @@ -742,22 +743,22 @@ 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 + 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'] + # ... + operations: + ApiPlatform\Metadata\GetCollection: + filters: ['offer.boolean_filter'] ``` @@ -796,22 +797,22 @@ 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 + 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 # config/api/Offer.yaml App\Entity\Offer: - # ... - operations: - ApiPlatform\Metadata\GetCollection: - filters: ['offer.numeric_filter'] + # ... + operations: + ApiPlatform\Metadata\GetCollection: + filters: ['offer.numeric_filter'] ``` @@ -850,22 +851,22 @@ 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 + 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'] + # ... + operations: + ApiPlatform\Metadata\GetCollection: + filters: ['offer.range_filter'] ``` @@ -907,22 +908,22 @@ 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 + 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'] + # ... + operations: + ApiPlatform\Metadata\GetCollection: + filters: ['offer.exists_filter'] ``` @@ -939,8 +940,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) @@ -973,24 +974,24 @@ 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 + 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 # config/api/Offer.yaml App\Entity\Offer: - # ... - operations: - ApiPlatform\Metadata\GetCollection: - filters: ['offer.order_filter'] + # ... + operations: + ApiPlatform\Metadata\GetCollection: + filters: ['offer.order_filter'] ``` @@ -1023,22 +1024,22 @@ 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 + 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 # config/api/Offer.yaml App\Entity\Offer: - # ... - operations: - ApiPlatform\Metadata\GetCollection: - filters: ['offer.order_filter'] + # ... + operations: + ApiPlatform\Metadata\GetCollection: + filters: ['offer.order_filter'] ``` @@ -1048,13 +1049,13 @@ App\Entity\Offer: When the property used for ordering can contain `null` values, you may want to specify how `null` values are treated in the comparison: -Description | Strategy to set --------------------------------------|--------------------------------------------------------------------------------------------- -Use the default behavior of the DBMS | `null` -Consider items as smallest | `ApiPlatform\Doctrine\Orm\Filter\OrderFilter::NULLS_SMALLEST` (`nulls_smallest`) -Consider items as largest | `ApiPlatform\Doctrine\Orm\Filter\OrderFilter::NULLS_LARGEST` (`nulls_largest`) -Order items always first | `ApiPlatform\Doctrine\Orm\Filter\OrderFilter::NULLS_ALWAYS_FIRST` (`nulls_always_first`) -Order items always last | `ApiPlatform\Doctrine\Orm\Filter\OrderFilter::NULLS_ALWAYS_LAST` (`nulls_always_last`) +| Description | Strategy to set | +| ------------------------------------ | ---------------------------------------------------------------------------------------- | +| Use the default behavior of the DBMS | `null` | +| Consider items as smallest | `ApiPlatform\Doctrine\Orm\Filter\OrderFilter::NULLS_SMALLEST` (`nulls_smallest`) | +| Consider items as largest | `ApiPlatform\Doctrine\Orm\Filter\OrderFilter::NULLS_LARGEST` (`nulls_largest`) | +| Order items always first | `ApiPlatform\Doctrine\Orm\Filter\OrderFilter::NULLS_ALWAYS_FIRST` (`nulls_always_first`) | +| Order items always last | `ApiPlatform\Doctrine\Orm\Filter\OrderFilter::NULLS_ALWAYS_LAST` (`nulls_always_last`) | For instance, treat entries with a property value of `null` as the smallest, with the following service definition: @@ -1080,22 +1081,28 @@ 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 + 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'] + # ... + operations: + ApiPlatform\Metadata\GetCollection: + filters: ['offer.order_filter'] ``` @@ -1105,8 +1112,8 @@ 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' + collection: + order_nulls_comparison: 'nulls_smallest' ``` #### Using a Custom Order Query Parameter Name @@ -1117,8 +1124,8 @@ 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" + collection: + order_parameter_name: '_order' # the URL query parameter to use is now "_order" ``` ### Filtering on Nested Properties @@ -1150,31 +1157,31 @@ 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'] ``` @@ -1210,22 +1217,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'] ``` @@ -1234,14 +1241,14 @@ App\Entity\Offer: Regardless of this option, filters can be applied on a property only if: -* the property exists -* the value is supported (ex: `asc` or `desc` for the order filters). +- the property exists +- the value is supported (ex: `asc` or `desc` for the order filters). It means that the filter will be **silently** ignored if the property: -* does not exist -* is not enabled -* has an invalid value +- does not exist +- is not enabled +- has an invalid value ## Elasticsearch Filters @@ -1277,22 +1284,22 @@ 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'] ``` @@ -1329,8 +1336,8 @@ 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" + collection: + order_parameter_name: '_order' # the URL query parameter to use is now "_order" ``` ### Match Filter @@ -1457,9 +1464,9 @@ class Book Three arguments are available to configure the filter: -* `parameterName` is the query parameter name (default `groups`) -* `overrideDefaultGroups` allows to override the default serialization groups (default `false`) -* `whitelist` groups whitelist to avoid uncontrolled data exposure (default `null` to allow all groups) +- `parameterName` is the query parameter name (default `groups`) +- `overrideDefaultGroups` allows to override the default serialization groups (default `false`) +- `whitelist` groups whitelist to avoid uncontrolled data exposure (default `null` to allow all groups) Given that the collection endpoint is `/books`, you can filter by serialization groups with the following query: `/books?groups[]=read&groups[]=write`. @@ -1495,9 +1502,9 @@ 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) +- `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`. @@ -1630,6 +1637,7 @@ class Offer ``` When creating a custom filter you can specify multiple properties of a resource using the usual filter syntax: + ```php getRootAliases()[0]; +protected function filterProperty(string $property, $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, Operation $operation = null, array $context = []): void { + $rootAlias = $queryBuilder->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 . "%"); - } + 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 . "%"); + } } ``` @@ -1675,13 +1685,13 @@ 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' ] + # ... + # 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, @@ -1690,9 +1700,9 @@ 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' ] + '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: @@ -1754,9 +1764,9 @@ class AndOperatorFilterExtension implements RequestBodySearchCollectionExtension 'query' => $context['filters']['fullName'], 'operator' => 'and', ]; - + $requestBody['query']['constant_score']['filter']['bool']['must'][0]['match']['full_name'] = $andQuery; - + return $requestBody; } } @@ -1801,7 +1811,7 @@ class Order #[ORM\ManyToOne(User::class)] #[ORM\JoinColumn(name: 'user_id', referencedColumnName: 'id')] public User $user; - + // ... } ``` @@ -1887,11 +1897,11 @@ Now, we must configure the Doctrine filter. ```yaml # api/config/packages/api_platform.yaml doctrine: - orm: - filters: - user_filter: - class: App\Filter\UserFilter - enabled: true + orm: + filters: + user_filter: + class: App\Filter\UserFilter + enabled: true ``` Done: Doctrine will automatically filter all `UserAware`entities! @@ -2008,4 +2018,3 @@ The next filters are not related to how the data is fetched but rather to how th #[ApiFilter(PropertyFilter::class, arguments: ['parameterName' => 'foobar'])] #[ApiFilter(GroupFilter::class, arguments: ['parameterName' => 'foobargroups'])] ``` - diff --git a/core/form-data.md b/core/form-data.md index b159e6f0b2b..d1e8e04fd1c 100644 --- a/core/form-data.md +++ b/core/form-data.md @@ -3,7 +3,7 @@ API Platform only supports raw documents as request input (encoded in JSON, XML, YAML...). This has many advantages including support of types and the ability to send back to the API documents originally retrieved through a `GET` request. However, sometimes - for instance, to support legacy clients - it is necessary to accept inputs encoded in the traditional [`application/x-www-form-urlencoded`](https://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.1) format (HTML form content type). This can easily be done using [the powerful event system](events.md) of the framework. -**⚠ Adding support for `application/x-www-form-urlencoded` makes your API vulnerable to [CSRF attacks](https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)). Be sure to enable proper countermeasures [such as DunglasAngularCsrfBundle](https://github.com/dunglas/DunglasAngularCsrfBundle).** +**⚠ Adding support for `application/x-www-form-urlencoded` makes your API vulnerable to [CSRF attacks](). Be sure to enable proper countermeasures [such as DunglasAngularCsrfBundle](https://github.com/dunglas/DunglasAngularCsrfBundle).** 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. @@ -74,13 +74,18 @@ final class DeserializeListener ```yaml # api/config/services.yaml services: - # ... - 'App\EventListener\DeserializeListener': - tags: - - { name: 'kernel.event_listener', event: 'kernel.request', method: 'onKernelRequest', priority: 2 } - # Autoconfiguration must be disabled to set a custom priority - autoconfigure: false - decorates: 'api_platform.listener.request.deserialize' - arguments: - $decorated: '@App\EventListener\DeserializeListener.inner' + # ... + 'App\EventListener\DeserializeListener': + tags: + - { + name: 'kernel.event_listener', + event: 'kernel.request', + method: 'onKernelRequest', + priority: 2, + } + # Autoconfiguration must be disabled to set a custom priority + autoconfigure: false + decorates: 'api_platform.listener.request.deserialize' + arguments: + $decorated: '@App\EventListener\DeserializeListener.inner' ``` diff --git a/core/fosuser-bundle.md b/core/fosuser-bundle.md index aef69474121..dc5cbdb8c81 100644 --- a/core/fosuser-bundle.md +++ b/core/fosuser-bundle.md @@ -6,10 +6,10 @@ The installation procedure of the FOSUserBundle is described [in the main Symfon You can: -* Skip [step 3 (Create your User class)](https://symfony.com/doc/master/bundles/FOSUserBundle/index.html#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://symfony.com/doc/master/bundles/FOSUserBundle/index.html#step-4-configure-your-application-s-security-yml) -if you are planning to [use a JWT-based authentication using `LexikJWTAuthenticationBundle`](jwt.md) +- Skip [step 3 (Create your User class)](https://symfony.com/doc/master/bundles/FOSUserBundle/index.html#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://symfony.com/doc/master/bundles/FOSUserBundle/index.html#step-4-configure-your-application-s-security-yml) + if you are planning to [use a JWT-based authentication using `LexikJWTAuthenticationBundle`](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: @@ -17,7 +17,7 @@ configuration options: ```yaml # api/config/packages/framework.yaml framework: - form: { enabled: true } + form: { enabled: true } ``` ## Creating a `User` Entity with Serialization Groups @@ -55,7 +55,7 @@ class User extends BaseUser #[Groups("user")] protected string $email; - #[ORM\Column(nullable: true)] + #[ORM\Column(nullable: true)] #[Groups("user")] protected string $fullname; diff --git a/core/getting-started.md b/core/getting-started.md index 232d05a5728..8d60d799197 100644 --- a/core/getting-started.md +++ b/core/getting-started.md @@ -81,7 +81,7 @@ class Product // The class name will be used to name exposed resources * A name property - this description will be available in the API documentation too. * */ - #[ORM\Column] + #[ORM\Column] #[Assert\NotBlank] public string $name = ''; @@ -90,7 +90,7 @@ class Product // The class name will be used to name exposed resources * @var Offer[]|ArrayCollection * */ - #[ORM\OneToMany(targetEntity: Offer::class, mappedBy: 'product', cascade: ['persist'])] + #[ORM\OneToMany(targetEntity: Offer::class, mappedBy: 'product', cascade: ['persist'])] public iterable $offers; public function __construct() @@ -173,21 +173,21 @@ for resources of the product type: ### Product API using Symfony -Method | URL | Description --------|----------------|-------------------------------- -GET | /products | Retrieve the (paginated) collection -POST | /products | Create a new product -GET | /products/{id} | Retrieve a product -PATCH | /products/{id} | Apply a partial modification to a product -DELETE | /products/{id} | Delete a product +| Method | URL | Description | +| ------ | -------------- | ----------------------------------------- | +| GET | /products | Retrieve the (paginated) collection | +| POST | /products | Create a new product | +| GET | /products/{id} | Retrieve a product | +| PATCH | /products/{id} | Apply a partial modification to a product | +| DELETE | /products/{id} | Delete a product | > [!NOTE] > > `PUT` (replace or create) isn't registered automatically, > but is entirely supported by API Platform and can be added explicitly. -The same operations are available for the offer method (routes will start with the `/offers` pattern). -Route prefixes are built by pluralizing the name of the mapped entity class. -It is also possible to override the naming convention using [operation path namings](operation-path-naming.md). +> 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: @@ -196,13 +196,14 @@ 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 @@ -231,11 +232,12 @@ If you prefer to use YAML or XML files instead of attributes, you must configure ```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. @@ -288,12 +290,13 @@ namespace App\Models; //app/Models/Product.php +use ApiPlatform\Metadata\ApiResource; use Illuminate\Database\Eloquent\Model; - + +#[ApiResource] 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. + ```patch This is not yet available with Laravel, you're welcome to contribute [on Github](github.com/api-platform/core) ### Add another Location for GraphQL Playground + You can add a different location besides `/graphql/graphql_playground`. ### Symfony config routes for GraphQL Playground @@ -146,13 +151,14 @@ 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 Using the Laravel variant we can do this modification by adding the following code: + ```php // routes/web.php use Illuminate\Support\Facades\Route; @@ -171,9 +177,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 # ... ``` @@ -194,15 +200,17 @@ return [ You can also disable this feature by setting the configuration value to `false`. ### Symfony config to disable default IDE + ```yaml # api/config/packages/api_platform.yaml api_platform: - graphql: - default_ide: false + graphql: + default_ide: false # ... ``` ### Laravel config to disable default IDE + ```php ['type' => 'ID!'], - 'log' => ['type' => 'Boolean!', 'description' => 'Is logging activated?'], + 'id' => ['type' => 'ID!'], + 'log' => ['type' => 'Boolean!', 'description' => 'Is logging activated?'], 'logDate' => ['type' => 'DateTime'] ] ), @@ -533,42 +542,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 @@ -633,8 +642,8 @@ Conversely, if you need to add custom arguments, make sure `id` is added among t 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. @@ -673,9 +682,10 @@ Your custom queries will be available like this: If you don't know what mutations are yet, the documentation about them is [here](https://graphql.org/learn/queries/#mutations). For each resource, three mutations are available: -* `Mutation(name: 'create')` for creating a new resource -* `Mutation(name: 'update')` for updating an existing resource -* `DeleteMutation(name: 'delete')` for deleting an existing resource + +- `Mutation(name: 'create')` for creating a new resource +- `Mutation(name: 'update')` for updating an existing resource +- `DeleteMutation(name: 'delete')` for deleting an existing resource When updating or deleting a resource, you need to pass the **IRI** of the resource as argument. See [Global Object Identifier](#global-object-identifier) for more information. @@ -688,7 +698,7 @@ For example, if you delete a book: ```graphql mutation DeleteBook($id: ID!, $clientMutationId: String!) { - deleteBook(input: {id: $id, clientMutationId: $clientMutationId}) { + deleteBook(input: { id: $id, clientMutationId: $clientMutationId }) { clientMutationId } } @@ -758,10 +768,10 @@ In API Platform, the built-in subscription support is handled by using [Mercure] 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). -* 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. +- 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. For instance, your resource should look like this: @@ -789,11 +799,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 @@ -820,7 +830,9 @@ Doing a subscription is very similar to doing a query: ```graphql { subscription { - updateBookSubscribe(input: {id: "/books/1", clientSubscriptionId: "myId"}) { + updateBookSubscribe( + input: { id: "/books/1", clientSubscriptionId: "myId" } + ) { book { title isbn @@ -880,7 +892,7 @@ If you need to, you can disable some states providers and state processors, for The following table lists the system states providers and states processors you can disable in your resource configuration. | Attribute | Type | Default | Description | -|----------------------------|--------|---------|-------------------------------------------| +| -------------------------- | ------ | ------- | ----------------------------------------- | | `query_parameter_validate` | `bool` | `true` | Enables or disables `QueryParameter` | | `read` | `bool` | `true` | Enables or disables `ReadProvider` | | `deserialize` | `bool` | `true` | Enables or disables `DeserializeProvider` | @@ -916,13 +928,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 @@ -972,13 +984,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 @@ -1044,18 +1056,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 @@ -1094,7 +1106,7 @@ The first syntax coming to mind to use them is to write: ```graphql { - offers(order: {id: "ASC", name: "DESC"}) { + offers(order: { id: "ASC", name: "DESC" }) { edges { node { id @@ -1112,7 +1124,7 @@ That's why this syntax needs to be used instead: ```graphql { - offers(order: [{id: "ASC"}, {name: "DESC"}]) { + offers(order: [{ id: "ASC" }, { name: "DESC" }]) { edges { node { id @@ -1148,7 +1160,7 @@ class Offer } ``` -The above allows you to find offers by their respective product's color like for the REST Api. +The above allows you to find offers by their respective product's color like for the REST API. You can then filter using the following syntax: ```graphql @@ -1171,7 +1183,7 @@ Or order your results like: ```graphql { - offers(order: [{product_releaseDate: "DESC"}]) { + offers(order: [{ product_releaseDate: "DESC" }]) { edges { node { id @@ -1236,16 +1248,16 @@ Here is an example query leveraging the pagination system: Two pairs of parameters work with the query: -* `first` and `after`; -* `last` and `before`. +- `first` and `after`; +- `last` and `before`. More precisely: -* `first` corresponds to the items per page starting from the beginning; -* `after` corresponds to the `cursor` from which the items are returned. +- `first` corresponds to the items per page starting from the beginning; +- `after` corresponds to the `cursor` from which the items are returned. -* `last` corresponds to the items per page starting from the end; -* `before` corresponds to the `cursor` from which the items are returned, from a backwards point of view. +- `last` corresponds to the items per page starting from the end; +- `before` corresponds to the `cursor` from which the items are returned, from a backwards point of view. The current page always has a `startCursor` and an `endCursor`, present in the `pageInfo` field. @@ -1305,17 +1317,17 @@ class Offer ```yaml #The YAML syntax is only supported for Symfony resources: - App\Entity\Book: - graphQlOperations: - - class: ApiPlatform\Metadata\GraphQl\Query - - class: ApiPlatform\Metadata\GraphQl\QueryCollection - paginationType: page - - class: ApiPlatform\Metadata\GraphQl\Mutation - name: create - - class: ApiPlatform\Metadata\GraphQl\Mutation - name: update - - class: ApiPlatform\Metadata\GraphQl\Mutation - name: delete + App\Entity\Book: + graphQlOperations: + - class: ApiPlatform\Metadata\GraphQl\Query + - class: ApiPlatform\Metadata\GraphQl\QueryCollection + paginationType: page + - class: ApiPlatform\Metadata\GraphQl\Mutation + name: create + - class: ApiPlatform\Metadata\GraphQl\Mutation + name: update + - class: ApiPlatform\Metadata\GraphQl\Mutation + name: delete ``` ```xml @@ -1359,8 +1371,8 @@ class Offer ```yaml #The YAML syntax is only supported for Symfony resources: - App\Entity\Book: - paginationType: page + App\Entity\Book: + paginationType: page ``` ```xml @@ -1379,10 +1391,10 @@ Once enabled, a `page` filter will be available in the collection query (its nam A `paginationInfo` field can be queried to obtain the following information: -* `itemsPerPage`: the number of items per page. To change it, follow the [pagination documentation](pagination.md#changing-the-number-of-items-per-page). -* `lastPage`: the last page of the collection. -* `totalCount`: the total number of items in the collection. -* `hasNextPage`: does the current collection offers a next page. +- `itemsPerPage`: the number of items per page. To change it, follow the [pagination documentation](pagination.md#changing-the-number-of-items-per-page). +- `lastPage`: the last page of the collection. +- `totalCount`: the total number of items in the collection. +- `hasNextPage`: does the current collection offers a next page. The collection items data are available in the `collection` field. @@ -1417,10 +1429,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 @@ -1463,8 +1475,8 @@ class Book ```yaml #The YAML syntax is only supported for Symfony resources: - App\Entity\Book: - paginationEnabled: false + App\Entity\Book: + paginationEnabled: false ``` ```xml @@ -1503,10 +1515,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 @@ -1558,7 +1570,7 @@ use ApiPlatform\Metadata\GraphQl\QueryCollection; use ApiPlatform\Metadata\Post; #[ApiResource( - security: "is_granted('ROLE_USER')", + security: "is_granted('ROLE_USER')", operations: [ new Get(security: "is_granted('ROLE_USER') and object.owner == user", securityMessage: 'Sorry, but you are not the book owner.'), new Post(security: "is_granted('ROLE_ADMIN')", securityMessage: 'Only admins can add books.') @@ -1579,26 +1591,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 @@ -1636,7 +1648,7 @@ For example, associations can be made with Doctrine ORM's `OneToMany`, `ManyToOn 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. +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. @@ -1675,7 +1687,7 @@ class User */ #[ApiProperty(security: 'is_granted("VIEW", object)')] private Collection $viewableDocuments; - + /** * @ORM\Column(type="string", length=180, unique=true) */ @@ -1687,19 +1699,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 @@ -1770,19 +1782,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 @@ -1815,7 +1827,7 @@ 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. -Similar to `Query`, all associations are secured, requiring `VIEW` access on the parent object (*not* on the association). +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). @@ -1830,8 +1842,8 @@ If the (de)normalization context between GraphQL and REST is different, use the 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 **query** is only using the normalization context. +- A **mutation** is using the denormalization context for its input and the normalization context for its output. The following example shows you what can be done: @@ -1849,7 +1861,7 @@ use ApiPlatform\Metadata\GraphQl\QueryCollection; use Symfony\Component\Serializer\Annotation\Groups; #[ApiResource( - normalizationContext: ['groups' => ['read']], + normalizationContext: ['groups' => ['read']], denormalizationContext: ['groups' => ['write']], graphQlOperations: [ new Query(normalizationContext: ['groups' => ['query']]), @@ -1878,24 +1890,24 @@ class Book ```yaml #The YAML syntax is only supported for Symfony resources: - App\Entity\Book: + App\Entity\Book: + normalizationContext: + groups: ['read'] + denormalizationContext: + groups: ['write'] + graphQlOperations: + ApiPlatform\Metadata\GraphQl\Query: + normalizationContext: + groups: ['query'] + ApiPlatform\Metadata\GraphQl\QueryCollection: + normalizationContext: + groups: ['query_collection'] + ApiPlatform\Metadata\GraphQl\Mutation: + name: create normalizationContext: - groups: ['read'] + groups: ['query_collection'] denormalizationContext: - groups: ['write'] - graphQlOperations: - ApiPlatform\Metadata\GraphQl\Query: - normalizationContext: - groups: ['query'] - ApiPlatform\Metadata\GraphQl\QueryCollection: - normalizationContext: - groups: ['query_collection'] - ApiPlatform\Metadata\GraphQl\Mutation: - name: create - normalizationContext: - groups: ['query_collection'] - denormalizationContext: - groups: ['mutation'] + groups: ['mutation'] ``` ```xml @@ -1988,8 +2000,8 @@ Make sure you understand the implications when doing this: having different type 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) @@ -2025,10 +2037,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 @@ -2052,7 +2064,9 @@ Creating a book with its author will be done like this, where `/authors/32` is t ```graphql { mutation { - createBook(input: {title: "The Name of the Wind", author: "/authors/32"}) { + createBook( + input: { title: "The Name of the Wind", author: "/authors/32" } + ) { book { title author { @@ -2097,12 +2111,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 @@ -2158,7 +2172,12 @@ 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"}}) { + createBook( + input: { + title: "The Name of the Wind" + author: { name: "Patrick Rothfuss" } + } + ) { book { title author { @@ -2215,9 +2234,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 @@ -2339,9 +2358,9 @@ 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 @@ -2447,16 +2466,16 @@ You would need to use the search filter like this: 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: __ # ... ``` + In this case, your query will be: ```graphql @@ -2589,17 +2608,16 @@ 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). - ### Custom Types config for Laravel If you are using Laravel tag your type with: @@ -2638,9 +2656,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 @@ -2756,6 +2774,7 @@ final class BookContextBuilder implements SerializerContextBuilderInterface ``` ### Laravel Serialization Context Decoration + ```php ['media_object_read']], + normalizationContext: ['groups' => ['media_object_read']], types: ['https://schema.org/MediaObject'], graphQlOperations: [ new Mutation( - name: 'upload', - resolver: CreateMediaObjectResolver::class, - deserialize: false, + name: 'upload', + resolver: CreateMediaObjectResolver::class, + deserialize: false, args: [ 'file' => [ - 'type' => 'Upload!', + 'type' => 'Upload!', 'description' => 'The file to upload' ] ] @@ -2869,7 +2888,7 @@ class MediaObject /** * @Vich\UploadableField(mapping="media_object", fileNameProperty="filePath") */ - #[Assert\NotNull(groups: ['media_object_create'])] + #[Assert\NotNull(groups: ['media_object_create'])] public ?File $file = null; #[ORM\Column(nullable: true)] @@ -2927,15 +2946,17 @@ Following the specification, the upload must be done with a `multipart/form-data You need to enable it in the [allowed formats of API Platform](content-negotiation.md#configuring-formats-globally): #### Modifying allowed formats with Symfony + ```yaml # api/config/packages/api_platform.yaml api_platform: - formats: - # ... - multipart: ['multipart/form-data'] + formats: + # ... + multipart: ['multipart/form-data'] ``` #### Modifying allowed formats with Laravel + ```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. +> 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) @@ -60,41 +60,41 @@ 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: ^/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: ^/auth, roles: PUBLIC_ACCESS } + - { path: ^/, roles: IS_AUTHENTICATED_FULLY } ``` You must also declare the route used for `/auth`: @@ -102,8 +102,8 @@ 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 @@ -119,39 +119,39 @@ If your API uses a [path prefix](https://symfony.com/doc/current/routing/externa ```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: ^/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: ^/auth, roles: PUBLIC_ACCESS } + - { path: ^/, roles: IS_AUTHENTICATED_FULLY } ``` ### Be sure to have lexik_jwt_authentication configured on your user_identity_field @@ -159,9 +159,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 @@ -173,11 +173,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. @@ -204,11 +204,11 @@ If you need to modify the default configuration, you can do it in the dedicated ```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: @@ -290,9 +290,9 @@ To significantly improve the test suite speed, we can use more simple password h ```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/core/mercure.md b/core/mercure.md index 900f05ea9c5..fb64a2f1182 100644 --- a/core/mercure.md +++ b/core/mercure.md @@ -2,7 +2,7 @@ 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) @@ -55,7 +55,7 @@ Clients generated using [Create Client](../create-client/index.md) will use this ## Dispatching Private Updates (Authorized Mode) Mercure allows dispatching [private updates, that will be received only by authorized clients](https://mercure.rocks/spec#authorization). -To receive this kind of updates, the client must hold a JWT containing at least one *target selector* matched by the update. +To receive this kind of updates, the client must hold a JWT containing at least one _target selector_ matched by the update. Then, use options to mark the published updates as privates: @@ -73,7 +73,7 @@ class Book } ``` -It's also possible to execute an *expression* (using the [Symfony Expression Language component](https://symfony.com/doc/current/components/expression_language.html)), to generate the options dynamically: +It's also possible to execute an _expression_ (using the [Symfony Expression Language component](https://symfony.com/doc/current/components/expression_language.html)), to generate the options dynamically: ```php @@ -133,11 +133,11 @@ use App\Dto\ResetPasswordRequest; new GetCollection(), new Post(), new Post( - name: 'reset_password', - status: 202, - messenger: 'input', - input: ResetPasswordRequest::class, - output: false, + name: 'reset_password', + status: 202, + messenger: 'input', + input: ResetPasswordRequest::class, + output: false, uriTemplate: '/users/reset_password' ) ])] diff --git a/core/migrate-from-fosrestbundle.md b/core/migrate-from-fosrestbundle.md index 469a4e56e8c..d1ad2ac2ce5 100644 --- a/core/migrate-from-fosrestbundle.md +++ b/core/migrate-from-fosrestbundle.md @@ -37,7 +37,6 @@ You can use them as you migrate from FOSRestBundle, but you should consider [swi See [General Design Considerations](design.md). - ### Routing system (with native documentation support) **In FOSRestBundle** diff --git a/core/mongodb.md b/core/mongodb.md index 7af5c726494..fd0460e33d6 100644 --- a/core/mongodb.md +++ b/core/mongodb.md @@ -48,19 +48,19 @@ Add a MongoDB image to the docker-compose file: services: # ... db-mongodb: - # In production, you may want to use a managed database service - image: mongo - environment: - - MONGO_INITDB_DATABASE=api - - MONGO_INITDB_ROOT_USERNAME=api-platform - # You should definitely change the password in production - - MONGO_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 production, you may want to use a managed database service + image: mongo + environment: + - MONGO_INITDB_DATABASE=api + - MONGO_INITDB_ROOT_USERNAME=api-platform + # You should definitely change the password in production + - MONGO_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' # ... ``` @@ -87,12 +87,13 @@ Change the configuration of API Platform to add the right mapping path: ```yaml # api/config/packages/api_platform.yaml api_platform: - # ... + # ... - mapping: - paths: ['%kernel.project_dir%/src/Entity', '%kernel.project_dir%/src/Document'] + mapping: + paths: + ['%kernel.project_dir%/src/Entity', '%kernel.project_dir%/src/Document'] - # ... + # ... ``` ## Creating Documents diff --git a/core/nelmio-api-doc.md b/core/nelmio-api-doc.md index f31636953fd..1fb8944ad08 100644 --- a/core/nelmio-api-doc.md +++ b/core/nelmio-api-doc.md @@ -16,19 +16,19 @@ 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 diff --git a/core/openapi.md b/core/openapi.md index cd88e2fb346..7b310b9ea1f 100644 --- a/core/openapi.md +++ b/core/openapi.md @@ -62,10 +62,10 @@ the `GET` operation of `/foos` path. ```yaml # api/config/services.yaml - 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 ``` ```php @@ -154,7 +154,7 @@ class Product // The class name will be used to name exposed resources )] public string $name; - #[ORM\Column(type: "datetime")] + #[ORM\Column(type: "datetime")] #[Assert\DateTime] #[ApiProperty( openapiContext: [ @@ -171,16 +171,16 @@ class Product // The class name will be used to name exposed resources ```yaml # api/config/api_platform/properties.yaml properties: - App\Entity\Product: - name: - openapiContext: - type: string - enum: ['one', 'two'] - example: one - timestamp: - openapiContext: - type: string - format: date-time + App\Entity\Product: + name: + openapiContext: + type: string + enum: ['one', 'two'] + example: one + timestamp: + openapiContext: + type: string + format: date-time ``` ```xml @@ -292,10 +292,10 @@ class Product ```yaml # api/config/api_platform/resources.yaml resources: - App\Entity\Product: - operations: - ApiPlatform\Metadata\GetCollection: - openapi: false + App\Entity\Product: + operations: + ApiPlatform\Metadata\GetCollection: + openapi: false ``` ```xml @@ -371,24 +371,24 @@ use App\Controller\RandomRabbit; #[ApiResource] #[Post( - name: 'create_rabbit', - uriTemplate: '/rabbit/create', - controller: RandomRabbit::class, + name: 'create_rabbit', + uriTemplate: '/rabbit/create', + controller: RandomRabbit::class, openapi: new Model\Operation( - summary: 'Create a rabbit picture', - description: '# Pop a great rabbit picture by color!\n\n![A great rabbit](https://rabbit.org/graphics/fun/netbunnies/jellybean1-brennan1.jpg)', + summary: 'Create a rabbit picture', + description: '# Pop a great rabbit picture by color!\n\n![A great rabbit](https://rabbit.org/graphics/fun/netbunnies/jellybean1-brennan1.jpg)', requestBody: new Model\RequestBody( content: new \ArrayObject([ 'application/json' => [ 'schema' => [ - 'type' => 'object', + 'type' => 'object', 'properties' => [ - 'name' => ['type' => 'string'], + 'name' => ['type' => 'string'], 'description' => ['type' => 'string'] ] - ], + ], 'example' => [ - 'name' => 'Mr. Rabbit', + 'name' => 'Mr. Rabbit', 'description' => 'Pink Rabbit' ] ] @@ -427,7 +427,6 @@ resources: example: name: Mr. Rabbit description: Pink rabbit - ``` ```xml @@ -442,7 +441,7 @@ resources: controller="App\Controller\RandomRabbit"> @@ -493,8 +492,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: @@ -502,8 +501,8 @@ To disable ReDoc: ```yaml # api/config/packages/api_platform.yaml api_platform: - # ... - enable_re_doc: false + # ... + enable_re_doc: false ``` ## Changing the Location of Swagger UI @@ -519,8 +518,8 @@ Manually register the Swagger UI controller: ```yaml # app/config/routes.yaml api_doc: - path: /api_documentation - controller: api_platform.swagger_ui.action + path: /api_documentation + controller: api_platform.swagger_ui.action ``` Change `/api_documentation` to the URI you wish Swagger UI to be accessible on. @@ -532,9 +531,9 @@ To disable the Swagger UI at the API location, disable both Swagger UI and ReDoc ```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 ``` If you have manually registered the Swagger UI controller, the Swagger UI will still be accessible at the route you have chosen. @@ -550,7 +549,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: @@ -558,12 +557,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 @@ -597,30 +596,30 @@ If you implemented OAuth on your API, you should configure OpenApi's authorizati ```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. @@ -631,12 +630,12 @@ The `api_platform.oauth.scopes` option requires an array value with the scopes n ```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." + 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 @@ -648,31 +647,30 @@ The [info object](https://swagger.io/specification/#info-object) provides metada ```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: ``` diff --git a/core/operation-path-naming.md b/core/operation-path-naming.md index a4ef6a93715..1433933ae3a 100644 --- a/core/operation-path-naming.md +++ b/core/operation-path-naming.md @@ -7,10 +7,10 @@ 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` +| 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` | The default resolver is `api_platform.metadata.path_segment_name_generator.underscore`. To change it to the dash resolver, add the following lines to `api/config/packages/api_platform.yaml`: @@ -18,7 +18,7 @@ To change it to the dash resolver, add the following lines to `api/config/packag ```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 ``` ## Create a Custom Operation Path Resolver @@ -59,7 +59,7 @@ class SingularPathSegmentNameGenerator implements PathSegmentNameGeneratorInterf } ``` -Note that `$name` contains a camel case 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 @@ -70,8 +70,8 @@ Otherwise, you must register this class as a service like in the following examp ```yaml # api/config/services.yaml services: - # ... - 'App\Operation\SingularPathSegmentNameGenerator': ~ + # ... + 'App\Operation\SingularPathSegmentNameGenerator': ~ ``` ### Configuring It @@ -79,5 +79,5 @@ services: ```yaml # api/config/packages/api_platform.yaml api_platform: - path_segment_name_generator: 'App\Operation\SingularPathSegmentNameGenerator' + path_segment_name_generator: 'App\Operation\SingularPathSegmentNameGenerator' ``` diff --git a/core/operations.md b/core/operations.md index 191f4580113..8e637c2d473 100644 --- a/core/operations.md +++ b/core/operations.md @@ -29,19 +29,19 @@ operations are automatically enabled: Collection operations: -Method | Mandatory | Description | Registered by default --------|-----------|-------------------------------------------|---------------------- -`GET` | yes | Retrieve the (paginated) list of elements | yes -`POST` | no | Create a new element | yes +| Method | Mandatory | Description | Registered by default | +| ------ | --------- | ----------------------------------------- | --------------------- | +| `GET` | yes | Retrieve the (paginated) list of elements | yes | +| `POST` | no | Create a new element | yes | Item operations: -Method | Mandatory | Description | Registered by default ----------|-----------|--------------------------------------------|---------------------- -`GET` | yes | Retrieve an element | yes -`PUT` | no | Replace an element | no -`PATCH` | no | Apply a partial modification to an element | yes -`DELETE` | no | Delete an element | yes +| Method | Mandatory | Description | Registered by default | +| -------- | --------- | ------------------------------------------ | --------------------- | +| `GET` | yes | Retrieve an element | yes | +| `PUT` | no | Replace an element | no | +| `PATCH` | no | Apply a partial modification to an element | yes | +| `DELETE` | no | Delete an element | yes | > [!NOTE] > The `PATCH` method must be enabled explicitly in the configuration, refer to the [Content Negotiation](content-negotiation.md) section for more information. @@ -106,10 +106,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 @@ -131,7 +131,6 @@ resources: - The previous example can also be written with an explicit method definition: @@ -160,12 +159,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 @@ -206,8 +205,8 @@ use ApiPlatform\Metadata\ApiResource; #[ApiResource(operations: [ new Get( - controller: NotFoundAction::class, - read: false, + controller: NotFoundAction::class, + read: false, output: false ), new GetCollection() @@ -221,13 +220,13 @@ class Book ```yaml # api/config/api_platform/resources.yaml resources: - App\Entity\Book: - operations: - ApiPlatform\Metadata\GetCollection: ~ - ApiPlatform\Metadata\Get: - controller: ApiPlatform\Action\NotFoundAction - read: false - output: false + App\Entity\Book: + operations: + ApiPlatform\Metadata\GetCollection: ~ + ApiPlatform\Metadata\Get: + controller: ApiPlatform\Action\NotFoundAction + read: false + output: false ``` ```xml @@ -270,15 +269,15 @@ 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'], + uriTemplate: '/grimoire/{id}', + requirements: ['id' => '\d+'], + defaults: ['color' => 'brown'], + options: ['my_option' => 'my_option_value'], + schemes: ['https'], host: '{subdomain}.api-platform.com' ), new Post( - uriTemplate: '/grimoire', + uriTemplate: '/grimoire', status: 301 ) ])] @@ -291,21 +290,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 @@ -369,8 +368,8 @@ class Book ```yaml # api/config/api_platform/resources.yaml resources: - App\Entity\Book: - routePrefix: /library + App\Entity\Book: + routePrefix: /library ``` ```xml @@ -432,22 +431,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 @@ -502,13 +501,13 @@ class Place #[ORM\Id, ORM\Column, ORM\GeneratedValue] private ?int $id = null; - #[ORM\Column] + #[ORM\Column] private string $name = ''; #[ORM\Column(type: 'float')] private float $latitude = 0; - #[ORM\Column(type: 'float')] + #[ORM\Column(type: 'float')] private float $longitude = 0; // ... diff --git a/core/pagination.md b/core/pagination.md index c569a3296a6..5afdefa7ee1 100644 --- a/core/pagination.md +++ b/core/pagination.md @@ -6,8 +6,8 @@ API Platform has native support for paged collections. Pagination is enabled by 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) +- 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. @@ -24,8 +24,8 @@ is returned. It's a valid JSON(-LD) document containing items of the requested p "name": "My awesome book" }, { - "_": "Other items in the collection..." - }, + "_": "Other items in the collection..." + } ], "totalItems": 50, "view": { @@ -46,9 +46,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 ``` ## Disabling the Pagination @@ -64,8 +64,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 For a Specific Resource @@ -91,9 +91,10 @@ class Book ```yaml # api/config/api_platform/resources.yaml resources: - App\Entity\Book: - paginationEnabled: false + App\Entity\Book: + paginationEnabled: false ``` + ### Disabling the Pagination For a Specific Operation @@ -114,7 +115,7 @@ use ApiPlatform\Metadata\GetCollection; operations: [ new GetCollection( paginationEnabled: false - ) + ) ] )] class Book @@ -126,10 +127,10 @@ class Book ```yaml # api/config/api_platform/resources.yaml resources: - App\Entity\Book: - operations: - ApiPlatform\Metadata\GetCollection: - paginationEnabled: false + App\Entity\Book: + operations: + ApiPlatform\Metadata\GetCollection: + paginationEnabled: false ``` ```xml @@ -143,11 +144,12 @@ resources: + paginationEnabled="false" /> ``` + ### Disabling the Pagination Client-side @@ -160,17 +162,17 @@ use the following configuration: ```yaml # api/config/packages/api_platform.yaml api_platform: - defaults: - pagination_client_enabled: true - collection: - pagination: - enabled_parameter_name: pagination # optional + defaults: + pagination_client_enabled: true + collection: + pagination: + enabled_parameter_name: pagination # optional ``` The pagination can now be enabled or disabled by adding a query parameter named `pagination`: -* `GET /books?pagination=false`: disabled -* `GET /books?pagination=true`: enabled +- `GET /books?pagination=false`: disabled +- `GET /books?pagination=true`: enabled Any value accepted by the [`FILTER_VALIDATE_BOOLEAN`](https://www.php.net/manual/en/filter.filters.validate.php) filter can be used as the value. @@ -204,8 +206,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 For a Specific Resource @@ -231,11 +233,11 @@ 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`. @@ -267,8 +269,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 For a Specific Resource @@ -318,8 +320,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 For a Specific Resource @@ -346,11 +348,11 @@ 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`. @@ -389,7 +391,7 @@ use ApiPlatform\Doctrine\Odm\Filter\OrderFilter; use ApiPlatform\Doctrine\Odm\Filter\RangeFilter; #[ApiResource( - paginationPartial: true, + paginationPartial: true, paginationViaCursor: [ ['field' => 'id', 'direction' => 'DESC'] ] @@ -408,7 +410,7 @@ To know more about cursor-based pagination take a look at [this blog post on med 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: @@ -429,7 +431,7 @@ 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: @@ -584,5 +586,5 @@ and if you want your results to be paginated, you will need to return an instanc `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` +- `ApiPlatform\State\Pagination\ArrayPaginator` +- `ApiPlatform\State\Pagination\TraversablePaginator` diff --git a/core/performance.md b/core/performance.md index 36d802eef2a..acf278e9c7f 100644 --- a/core/performance.md +++ b/core/performance.md @@ -47,7 +47,7 @@ The integration using the cache handler is quite simple. You just have to update + --with github.com/dunglas/vulcain/caddy \ + --with github.com/dunglas/caddy-cbrotli \ + --with github.com/caddyserver/cache-handler -+ # You should use another storage than the default one (e.g. otter). ++ # You should use another storage than the default one (e.g. otter). + # The list of the available storages can be find either on the documentation website (https://docs.souin.io/docs/storages/) or on the storages repository https://github.com/darkweak/storages + --with github.com/caddyserver/cache-handler + # Or use the following lines instead of the cache-handler one for the latest improvements @@ -68,17 +68,19 @@ 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. Setup the HTTP cache invalidation in your API Platform project + ```yaml api_platform: - http_cache: - invalidation: - # We assume that your API can reach your caddy instance by the hostname http://caddy. - # The endpoint /souin-api/souin is the default path to the invalidation API. - urls: [ 'http://caddy/souin-api/souin' ] - purger: api_platform.http_cache.purger.souin + http_cache: + invalidation: + # We assume that your API can reach your caddy instance by the hostname http://caddy. + # The endpoint /souin-api/souin is the default path to the invalidation API. + urls: ['http://caddy/souin-api/souin'] + purger: api_platform.http_cache.purger.souin ``` Don't forget to set your `Cache-Control` directive to enable caching on your API resource class. @@ -90,7 +92,7 @@ use ApiPlatform\Metadata\ApiResource; #[ApiResource( cacheHeaders: [ 'public' => true, - 'max_age' => 60, + 'max_age' => 60, ] )] class Book @@ -98,6 +100,7 @@ class Book // ... } ``` + And voilà, you have a fully working HTTP cache with an invalidation API. #### Varnish @@ -108,15 +111,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'] ``` ## Configuration @@ -128,20 +131,20 @@ for example, to use the `xkey` implementation: ```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: ', ' ``` In addition to the cache invalidation mechanism, you may want to [use HTTP/2 Server Push to pre-emptively send relations @@ -197,8 +200,8 @@ use ApiPlatform\Metadata\ApiResource; #[ApiResource( cacheHeaders: [ - 'max_age' => 60, - 'shared_max_age' => 120, + 'max_age' => 60, + 'shared_max_age' => 120, 'vary' => ['Authorization', 'Accept-Language'], ] )] @@ -224,7 +227,7 @@ use ApiPlatform\Metadata\Get; #[ApiResource] #[Get( cacheHeaders: [ - 'max_age' => 60, + 'max_age' => 60, 'shared_max_age' => 120, ] )] @@ -249,7 +252,7 @@ API Platform will automatically use it. 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 Indexes +## Doctrine Queries and Index ### Search Filter @@ -292,8 +295,8 @@ 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) @@ -306,8 +309,8 @@ If you want to fetch only partial data according to serialization groups, you ca ```yaml # api/config/packages/api_platform.yaml api_platform: - eager_loading: - fetch_partial: true + eager_loading: + fetch_partial: true ``` It is disabled by default. @@ -321,8 +324,8 @@ 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 @@ -393,7 +396,7 @@ class Group /** * @var User[] */ - #[ORM\ManyToMany(targetEntity: User::class, mappedBy: 'groups')] + #[ORM\ManyToMany(targetEntity: User::class, mappedBy: 'groups')] public $users; // ... @@ -410,8 +413,8 @@ If for any reason you don't want the eager loading feature, you can turn it off ```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. @@ -425,8 +428,8 @@ If you don't mind not having the last page available, you can enable partial pag ```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). @@ -441,14 +444,14 @@ 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 ``` 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): diff --git a/core/push-relations.md b/core/push-relations.md index 1012a9d4d20..e3f52c6cd28 100644 --- a/core/push-relations.md +++ b/core/push-relations.md @@ -22,7 +22,7 @@ class Book { #[ApiProperty(push: true)] public Author $author; - + // ... } ``` diff --git a/core/security.md b/core/security.md index 150bbcfa23c..5dadaeb716f 100644 --- a/core/security.md +++ b/core/security.md @@ -49,15 +49,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' ``` @@ -88,27 +88,27 @@ 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")' ``` 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 +- The user must be logged in to interact with `Book` resources (configured at the resource level) +- Only users having [the role](https://symfony.com/doc/current/security.html#roles) `ROLE_ADMIN` can create a new resource (configured on the `post` operation) +- Only users having the `ROLE_ADMIN` or owning the current object can replace an existing book (configured on the `put` operation) +- Only users having the `ROLE_ADMIN` can view or modify the `adminOnlyProperty` property. Only users having the `ROLE_ADMIN` can create a new resource specifying `adminOnlyProperty` value. +- Only users that are granted the `UPDATE` attribute on the book (via a voter) can write to the field Available variables are: -* `user`: the current logged in object, if any -* `object`: the current resource class during denormalization, the current resource during normalization, or collection of resources for collection operations -* `previous_object`: (`securityPostDenormalize` only) a clone of `object`, before modifications were made - this is `null` for create operations -* `request` (only at the resource level): the current request +- `user`: the current logged in object, if any +- `object`: the current resource class during denormalization, the current resource during normalization, or collection of resources for collection operations +- `previous_object`: (`securityPostDenormalize` only) a clone of `object`, before modifications were made - this is `null` for create operations +- `request` (only at the resource level): the current request Access control checks in the `security` attribute are always executed before the [denormalization step](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](state-processors.md). @@ -141,12 +141,12 @@ 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)" + # ... ``` @@ -197,17 +197,17 @@ 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)' ``` @@ -215,7 +215,7 @@ App\Entity\Book: 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: +Create a _BookVoter_ with the `bin/console make:voter` command: ```php diff --git a/core/serialization.md b/core/serialization.md index 863c9976d4f..fcfd3150b5b 100644 --- a/core/serialization.md +++ b/core/serialization.md @@ -11,7 +11,7 @@ The main serialization process has two stages: ![Serializer workflow](/docs/core/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) +> -- [The Symfony documentation](https://symfony.com/doc/current/components/serializer.html) Unlike Symfony itself, 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. @@ -19,16 +19,16 @@ The API Platform Serializer is extendable. You can register custom normalizers a ## 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. -* [HAL](https://en.wikipedia.org/wiki/Hypertext_Application_Language) serializer -`api_platform.hal.normalizer.item` +- [HAL](https://en.wikipedia.org/wiki/Hypertext_Application_Language) serializer + `api_platform.hal.normalizer.item` -* JSON, XML, CSV, YAML serializer (using the Symfony serializer) -`api_platform.serializer.normalizer.item` +- JSON, XML, CSV, YAML serializer (using the Symfony serializer) + `api_platform.serializer.normalizer.item` ## The Serialization Context, Groups and Relations @@ -51,7 +51,7 @@ Note: if you aren't using the API Platform distribution, you will need to enable ```yaml # api/config/packages/framework.yaml framework: - serializer: { enable_annotations: true } + serializer: { enable_annotations: true } ``` If you use [Symfony Flex](https://github.com/symfony/flex), just execute `composer req doctrine/annotations` and you are @@ -62,9 +62,9 @@ If you want to use YAML or XML, please add the mapping path in the serializer co ```yaml # api/config/packages/framework.yaml framework: - serializer: - mapping: - paths: ['%kernel.project_dir%/config/serialization'] + serializer: + mapping: + paths: ['%kernel.project_dir%/config/serialization'] ``` ## Using Serialization Groups @@ -103,19 +103,19 @@ class Book ```yaml # api/config/api_platform/resources.yaml resources: - App\Entity\Book: - normalizationContext: - groups: ['read'] - denormalizationContext: - groups: ['write'] + App\Entity\Book: + normalizationContext: + groups: ['read'] + denormalizationContext: + groups: ['write'] # api/config/serialization/Book.yaml App\Entity\Book: - attributes: - name: - groups: ['read', 'write'] - author: - groups: ['write'] + attributes: + name: + groups: ['read', 'write'] + author: + groups: ['write'] ``` ```xml @@ -389,9 +389,9 @@ class Person ```yaml # api/config/serializer/Person.yaml App\Entity\Person: - attributes: - name: - groups: ['book'] + attributes: + name: + groups: ['book'] ``` @@ -442,17 +442,17 @@ class Book ```yaml # api/config/api_platform/resources/Book.yaml App\Entity\Book: - denormalizationContext: - groups: ['book'] + denormalizationContext: + groups: ['book'] ``` The following rules apply when denormalizing embedded relations: -* If an `@id` key is present in the embedded resource, then the object corresponding to the given URI will be retrieved through -the state provider. Any changes in the embedded relation will also be applied to that object. -* If no `@id` key exists, a new object will be created containing state provided in the embedded JSON document. +- 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. @@ -484,7 +484,7 @@ class Person */ #[Groups('person')] public $parent; // Note that a Person instance has a relation with another Person. - + // ... } ``` @@ -535,7 +535,7 @@ class Person #[Groups('person')] #[ApiProperty(readableLink: false, writableLink: false)] public Person $parent; // This property is now serialized/deserialized as an IRI. - + // ... } @@ -544,24 +544,24 @@ class Person ```yaml # api/config/api_platform/resources/Person.yaml resources: - App\Entity\Person: - normalizationContext: - groups: ['person'] - denormalizationContext: - groups: ['person'] + App\Entity\Person: + normalizationContext: + groups: ['person'] + denormalizationContext: + groups: ['person'] properties: - App\Entity\Person: - parent: - readableLink: false - writableLink: false + App\Entity\Person: + parent: + readableLink: false + writableLink: false # api/config/serializer/Person.yaml App\Entity\Person: - attributes: - name: - groups: ['person'] - parent: - groups: ['person'] + attributes: + name: + groups: ['person'] + parent: + groups: ['person'] ``` @@ -639,7 +639,7 @@ use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer; #[ApiResource] class Book { - #[ORM\Column] + #[ORM\Column] #[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])] public ?\DateTimeInterface $publicationDate = null; } @@ -732,9 +732,9 @@ class Greeting #[ORM\Id, ORM\Column, ORM\GeneratedValue] #[Groups("greeting:collection:get")] private ?int $id = null; - + private $a = 1; - + private $b = 2; #[ORM\Column] @@ -817,7 +817,7 @@ class Book ```yaml # api/config/api_platform/resources/Book.yaml -App\Entity\Book: +App\Entity\Book: normalizationContext: groups: ['book:output'] denormalizationContext: @@ -844,11 +844,11 @@ API Platform implements a `ContextBuilder`, which prepares the context for seria ```yaml # 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 @@ -903,10 +903,10 @@ 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. @@ -983,13 +983,13 @@ To use this feature, declare a new name converter service. For example, you can ```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. @@ -1002,18 +1002,18 @@ 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: ```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' +# Need a different name to avoid duplicate YAML key +'app.serializer.normalizer.item.json': + class: 'App\Serializer\ApiNormalizer' + decorates: 'api_platform.serializer.normalizer.item' ``` ```php @@ -1117,9 +1117,9 @@ class Book ```yaml # api/config/api_platform/properties.yaml properties: - App\Entity\Book: - id: - identifier: true + App\Entity\Book: + id: + identifier: true ``` ```xml @@ -1143,7 +1143,7 @@ must do the following: 1. create a setter for the identifier of the entity (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/latest/reference/basic-mapping.html#identifier-generation-strategies) - or use the `NONE` value + or use the `NONE` value ## Embedding the JSON-LD Context @@ -1182,8 +1182,8 @@ class Book ```yaml # api/config/api_platform/resources/Book.yaml App\Entity\Book: - normalizationContext: - jsonldEmbedContext: true + normalizationContext: + jsonldEmbedContext: true ``` @@ -1207,8 +1207,8 @@ The JSON output will now include the embedded context: ## Collection Relation -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 indexes 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 removeProcessor->process($data, $operation, $uriVariables, $context); } - + $result = $this->persistProcessor->process($data, $operation, $uriVariables, $context); $this->sendWelcomeEmail($data); diff --git a/core/state-providers.md b/core/state-providers.md index b5de9769c54..af275568a95 100644 --- a/core/state-providers.md +++ b/core/state-providers.md @@ -158,11 +158,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(), @@ -191,7 +191,7 @@ class Book {} 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. +and autoconfiguration are enabled by default in API Platform. To declare the service explicitly, you can use the following snippet: ```yaml diff --git a/core/subresources.md b/core/subresources.md index 36734c61335..5c9e571337a 100644 --- a/core/subresources.md +++ b/core/subresources.md @@ -10,7 +10,9 @@ subresources providing you add the correct configuration for URI Variables. ## 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 implementation, queries are automatically built using the provided links. @@ -84,8 +86,8 @@ class Question ```yaml # api/config/api_platform/resources.yaml resources: - App\Entity\Answer: ~ - App\Entity\Question: ~ + App\Entity\Answer: ~ + App\Entity\Question: ~ ``` ```xml @@ -122,13 +124,13 @@ use Doctrine\ORM\Mapping as ORM; #[ORM\Entity] #[ApiResource] #[ApiResource( - uriTemplate: '/questions/{id}/answer', + uriTemplate: '/questions/{id}/answer', uriVariables: [ 'id' => new Link( fromClass: Question::class, fromProperty: 'answer' ) - ], + ], operations: [new Get()] )] class Answer @@ -140,16 +142,16 @@ class Answer ```yaml # 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 @@ -157,7 +159,7 @@ resources: xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://api-platform.com/schema/metadata/resources-3.0 https://api-platform.com/schema/metadata/resources-3.0.xsd"> - + @@ -171,7 +173,7 @@ resources: - + ``` @@ -191,7 +193,7 @@ If we had a `relatedQuestions` property on the `Answer` we could retrieve the co uriTemplate: '/answers/{id}/related_questions.{_format}', uriVariables: [ 'id' => new Link(fromClass: Answer::class, fromProperty: 'relatedQuestions') - ], + ], operations: [new GetCollection()] )] ``` @@ -199,14 +201,14 @@ If we had a `relatedQuestions` property on the `Answer` we could retrieve the co ```yaml # 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 @@ -228,9 +230,7 @@ resources: 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 ( - - {BookRoutes} - + {BookRoutes} ); @@ -67,7 +65,7 @@ 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 { reducer as form } from 'redux-form'; // see https://github.com/facebook/react-native/issues/14796 import { Buffer } from 'buffer'; @@ -88,14 +86,18 @@ import Router from './Router'; export default class App extends Component { render() { - const store = createStore(combineReducers({ - book, - form - }), {}, applyMiddleware(thunk)); + const store = createStore( + combineReducers({ + book, + form, + }), + {}, + applyMiddleware(thunk) + ); return ( - - + + ); diff --git a/create-client/react.md b/create-client/react.md index ba183eb8a2e..8722776af42 100644 --- a/create-client/react.md +++ b/create-client/react.md @@ -5,9 +5,9 @@ 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/) -* [React Hook Form](https://react-hook-form.com/) +- [React](https://reactjs.org/) +- [React Router](https://reactrouter.com/) +- [React Hook Form](https://react-hook-form.com/) ## Install diff --git a/create-client/troubleshooting.md b/create-client/troubleshooting.md index 72a16eddd7b..2e743041048 100644 --- a/create-client/troubleshooting.md +++ b/create-client/troubleshooting.md @@ -54,7 +54,7 @@ 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. +message in JSON format was being returned. ## Docker distribution on Windows and hot-reloading diff --git a/create-client/typescript.md b/create-client/typescript.md index 30283195026..f974044db45 100644 --- a/create-client/typescript.md +++ b/create-client/typescript.md @@ -29,7 +29,7 @@ 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 +- src/ + - interfaces/ + - foo.ts + - bar.ts diff --git a/create-client/vuejs.md b/create-client/vuejs.md index 4deecadd0b8..8d303e3fd7d 100644 --- a/create-client/vuejs.md +++ b/create-client/vuejs.md @@ -35,7 +35,7 @@ Replace the content of `App.vue` with the following code: ``` @@ -52,10 +52,7 @@ 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}", - ], + content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'], theme: { extend: {}, }, diff --git a/create-client/vuetify.md b/create-client/vuetify.md index 48e3ed8ea20..fdede0382c7 100644 --- a/create-client/vuetify.md +++ b/create-client/vuetify.md @@ -30,7 +30,7 @@ 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: @@ -48,7 +48,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 4b7798336d1..4fee630a451 100644 --- a/deployment/docker-compose.md +++ b/deployment/docker-compose.md @@ -129,13 +129,13 @@ This setup ensures the Next.js client can access the API at build time to genera 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 +pwa: + build: + context: ./pwa + target: prod + network: host + extra_hosts: + - php=127.0.0.1 ``` #### 2. Build and start the php service diff --git a/deployment/heroku.md b/deployment/heroku.md index 170d1edaa7f..a39f79fdb13 100644 --- a/deployment/heroku.md +++ b/deployment/heroku.md @@ -1,6 +1,6 @@ # 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 +[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. @@ -8,8 +8,8 @@ The API Platform Heroku integration also supports MySQL databases provided by [t 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 @@ -28,12 +28,10 @@ Create a Heroku `app.json` file at the root of the `api/` directory to configure "success_url": "/", "env": { "APP_ENV": "prod", - "APP_SECRET": {"generator": "secret"}, + "APP_SECRET": { "generator": "secret" }, "CORS_ALLOW_ORIGIN": "https://your-client-url.com" }, - "addons": [ - "heroku-postgresql" - ], + "addons": ["heroku-postgresql"], "buildpacks": [ { "url": "https://github.com/heroku/heroku-buildpack-php" @@ -97,25 +95,35 @@ We are now ready to deploy our app! Go to the `api/` directory, then -1. Initialize a git repository: +1. Initialize a Git repository: - git init +```bash +git init +``` 2. Add all existing files: - git add --all +```bash +git add --all +``` 3. Commit: - git commit -a -m "My first API Platform app running on Heroku!" +```bash +git commit -a -m "My first API Platform app running on Heroku!" +``` 4. Create the Heroku application: - heroku create +```bash +heroku create +``` 5. And deploy for the first time: - git push heroku master +```bash +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. diff --git a/deployment/index.md b/deployment/index.md index e89162fd785..e594468f5e9 100644 --- a/deployment/index.md +++ b/deployment/index.md @@ -11,10 +11,10 @@ while the Progressive Web Application is a standard Next.js project:

JWT screencast
Watch the Animated Deployment with Ansistrano screencast

-* [Deploying the Symfony application](https://symfony.com/doc/current/deployment.html) -* [Deploying the Next.js application](https://nextjs.org/docs/deployment) +- [Deploying the Symfony application](https://symfony.com/doc/current/deployment.html) +- [Deploying the Next.js application](https://nextjs.org/docs/deployment) Alternatively, you may want to deploy API Platform on a PaaS (Platform as a Service): -* [Deploying the server application of API Platform on Heroku](heroku.md) -* [Deploying API Platform on Platform.sh (outdated)](https://platform.sh/blog/deploy-api-platform-on-platformsh) +- [Deploying the server application of API Platform on Heroku](heroku.md) +- [Deploying API Platform on Platform.sh (outdated)](https://platform.sh/blog/deploy-api-platform-on-platformsh) diff --git a/deployment/kubernetes.md b/deployment/kubernetes.md index fc6f41fd907..5601e84534a 100644 --- a/deployment/kubernetes.md +++ b/deployment/kubernetes.md @@ -85,7 +85,7 @@ 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. +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 @@ -234,13 +234,23 @@ 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 c68c7e91de1..9e669799c28 100644 --- a/deployment/minikube.md +++ b/deployment/minikube.md @@ -62,11 +62,15 @@ First, install the [skaffold CLI](https://skaffold.dev/docs/install/#standalone- Then, run minikube: - minikube start +```bash +minikube start +``` Add Skaffold configuration in the file `./helm/skaffold.yaml`. You can find a [complete configuration file for minikube](https://github.com/api-platform/api-platform/blob/main/helm/skaffold.yaml) with its [Helm values override](https://github.com/api-platform/api-platform/blob/main/helm/skaffold-values.yaml). Finally, go to the helm folder, and run skaffold in dev mode: - cd ./helm - skaffold dev +```bash +cd ./helm +skaffold dev +``` diff --git a/deployment/traefik.md b/deployment/traefik.md index 5c6af0fc1eb..6053ba21415 100644 --- a/deployment/traefik.md +++ b/deployment/traefik.md @@ -13,20 +13,21 @@ ports and add labels to tell Træfik to listen on the routes mentioned and redir A few points to note: -* `--api.insecure=true` Tells Træfik to generate a browser view to watch containers and IP/DNS associated easier -* `--providers.docker` Tells Træfik to listen on Docker API -* `labels:` Key for Træfik configuration into Docker integration +- `--api.insecure=true` Tells Træfik to generate a browser view to watch containers and IP/DNS associated easier +- `--providers.docker` Tells Træfik to listen on Docker API +- `labels:` Key for Træfik configuration into Docker integration ```yaml services: - # ... + # ... api: labels: - traefik.http.routers.api.rule=Host(`api.localhost`) ``` The API DNS will be specified with ``traefik.http.routers.api.rule=Host(`your.host`)`` (here api.localhost) -* `--traefik.routers.clientloadbalancer.server.port=3000` The port specified to Træfik will be exposed by the container (here the React app exposes the 3000 port), but if your container exposes only one port, it can be ignored + +- `--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: @@ -50,20 +51,20 @@ x-cache-from: 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 + 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: @@ -120,9 +121,9 @@ services: mercure: image: dunglas/mercure environment: -# - ACME_HOSTS=${DOMAIN_NAME} -# - CERT_FILE=/certs/localhost.crt -# - KEY_FILE=/certs/localhost.key + # - 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 @@ -440,9 +441,9 @@ Then update each traefik http routers names and services following this sample f ```yaml # /anywhere/first/api-plaform/compose.yaml # ... - labels: - - traefik.http.routers.admin-${RANDOM_UNIQUE_KEY}.rule=Host(`admin.${DOMAIN_NAME}`) - - traefik.http.services.admin-${RANDOM_UNIQUE_KEY}.loadbalancer.server.port=3000 +labels: + - traefik.http.routers.admin-${RANDOM_UNIQUE_KEY}.rule=Host(`admin.${DOMAIN_NAME}`) + - traefik.http.services.admin-${RANDOM_UNIQUE_KEY}.loadbalancer.server.port=3000 ``` ## More Generic Approach @@ -508,15 +509,13 @@ Then after that update respectively your API Platform and Træfik `compose.yaml` # /anywhere/api-platform/compose.yaml version: '3.4' -x-cache: - &cache +x-cache: &cache cache_from: - ${CONTAINER_REGISTRY_BASE}/php - ${CONTAINER_REGISTRY_BASE}/nginx - ${CONTAINER_REGISTRY_BASE}/varnish -x-network: - &network +x-network: &network networks: - api_platform_network @@ -585,9 +584,9 @@ services: mercure: image: dunglas/mercure environment: -# - ACME_HOSTS=${DOMAIN_NAME} -# - CERT_FILE=/certs/localhost.crt -# - KEY_FILE=/certs/localhost.key + # - 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 @@ -643,8 +642,7 @@ networks: # /anywhere/traefik/compose.yaml version: '3.4' -x-network: - &network +x-network: &network networks: - api_platform_network diff --git a/extra/conduct.md b/extra/conduct.md index 9fc70ff2c40..0a1058106df 100644 --- a/extra/conduct.md +++ b/extra/conduct.md @@ -12,13 +12,13 @@ body size, ethnic group, ethnicity, age, religion, or nationality. Examples of unacceptable behavior by participants include: -* The use of sexualized language or imagery -* Personal attacks -* Trolling or insulting/derogatory comments -* Public or private harassment -* Publishing other's private information, such as physical or electronic +- The use of sexualized language or imagery +- Personal attacks +- Trolling or insulting/derogatory comments +- Public or private harassment +- Publishing other's private information, such as physical or electronic addresses, without explicit permission -* Other unethical or unprofessional conduct +- 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 diff --git a/extra/enterprise.md b/extra/enterprise.md index 909732cc150..b4e351a5e11 100644 --- a/extra/enterprise.md +++ b/extra/enterprise.md @@ -5,8 +5,8 @@ API Platform is available as part of [the Tidelift Subscription](https://tidelif [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) +- [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 @@ -14,14 +14,14 @@ open source projects to deliver commercial support and maintenance for the open 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). +- **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) +- [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 3f39538ef83..df22dcc7ea6 100644 --- a/extra/philosophy.md +++ b/extra/philosophy.md @@ -2,14 +2,14 @@ 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/), +- 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) +- [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. +- 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 @@ -25,10 +25,10 @@ Like other modern frameworks such as Laravel and Symfony, it's both a full-stack API Platform makes modern development easy and fun again: -* [Start by **creating a web API**](../symfony/index.md) exposing structured data that can +- [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 +- [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. diff --git a/extra/releases.md b/extra/releases.md index 1881c7365c5..18848ff0cfa 100644 --- a/extra/releases.md +++ b/extra/releases.md @@ -15,9 +15,9 @@ For example: 3 versions are maintained at the same time: -* **stable** (currently the **4.0** branch): regular bugfixes are integrated in this version -* **old-stable** (are the last 2 minor branches: currently **3.4** and **3.3** branches): [security fixes](security.md) are integrated in this version, regular bugfixes are **not** backported in it -* **development** (**main** branch): new features target this branch +- **stable** (currently the **4.0** branch): regular bugfixes are integrated in this version +- **old-stable** (are the last 2 minor branches: currently **3.4** and **3.3** branches): [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...) **are not maintained**. If you still use them, you must upgrade as soon as possible. diff --git a/extra/security.md b/extra/security.md index df722d517fa..5c5dbbbc9e4 100644 --- a/extra/security.md +++ b/extra/security.md @@ -26,8 +26,8 @@ The resolution takes anywhere between a couple of days to some months depending 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) +- [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 @@ -37,41 +37,41 @@ In order to determine the severity of a security issue we take into account the Score of between 1 and 5 depending on how complex it is to exploit the vulnerability -* 4 - 5 Basic: attacker must follow a set of simple steps -* 2 - 3 Complex: attacker must follow non-intuitive steps with a high level of dependencies -* 1 - 2 High: A successful attack depends on conditions beyond the attacker's control. That is, a successful attack cannot be accomplished at will, but requires the attacker to invest in some measurable amount of effort in preparation or execution against the vulnerable component before a successful attack can be expected. +- 4 - 5 Basic: attacker must follow a set of simple steps +- 2 - 3 Complex: attacker must follow non-intuitive steps with a high level of dependencies +- 1 - 2 High: A successful attack depends on conditions beyond the attacker's control. That is, a successful attack cannot be accomplished at will, but requires the attacker to invest in some measurable amount of effort in preparation or execution against the vulnerable component before a successful attack can be expected. ### Impact Scores from the following areas are added together to produce a score. The score for Impact is capped at 6. Each area is scored between 0 and 4. -* Integrity: Does this vulnerability cause non-public data to be accessible? If so, does the attacker have control over the data disclosed? (0-4) -* Disclosure: Can this exploit allow system data (or data handled by the system) to be compromised? If so, does the attacker have control over modification? (0-4) -* Code Execution: Does the vulnerability allow arbitrary code to be executed on an end users system, or the server that it runs on? (0-4) -* Availability: Is the availability of a service or application affected? Is it reduced availability or total loss of availability of a service / application? Availability includes networked services (e.g., databases) or resources such as consumption of network bandwidth, processor cycles, or disk space. (0-4) +- Integrity: Does this vulnerability cause non-public data to be accessible? If so, does the attacker have control over the data disclosed? (0-4) +- Disclosure: Can this exploit allow system data (or data handled by the system) to be compromised? If so, does the attacker have control over modification? (0-4) +- Code Execution: Does the vulnerability allow arbitrary code to be executed on an end users system, or the server that it runs on? (0-4) +- Availability: Is the availability of a service or application affected? Is it reduced availability or total loss of availability of a service / application? Availability includes networked services (e.g., databases) or resources such as consumption of network bandwidth, processor cycles, or disk space. (0-4) ### Affected Projects Scores from the following areas are added together to produce a score. The score for Affected Projects is capped at 4. -* Will it affect some or all projects using a component? (1-2) -* Is the usage of the component that would cause such a thing already considered bad practice? (0-1) -* How common/popular is the component (e.g. Core vs Distribution vs Schema Generator)? (0-2) -* Are a number of well-known FOSS projects using API Platform affected that requires coordinated releases? (0-1) +- Will it affect some or all projects using a component? (1-2) +- Is the usage of the component that would cause such a thing already considered bad practice? (0-1) +- How common/popular is the component (e.g. Core vs Distribution vs Schema Generator)? (0-2) +- Are a number of well-known FOSS projects using API Platform affected that requires coordinated releases? (0-1) ### Score Totals -* Attack Complexity: 1 - 5 -* Impact: 1 - 6 -* Affected Projects: 1 - 4 +- Attack Complexity: 1 - 5 +- Impact: 1 - 6 +- Affected Projects: 1 - 4 ### Severity levels -* Low: 1 - 5 -* Medium: 6 - 10 -* High: 11 - 12 -* Critical: 13 - 14 -* Exceptional: 15 +- Low: 1 - 5 +- Medium: 6 - 10 +- High: 11 - 12 +- Critical: 13 - 14 +- Exceptional: 15 ## Credits diff --git a/extra/troubleshooting.md b/extra/troubleshooting.md index 54d74be8727..f49e84b796a 100644 --- a/extra/troubleshooting.md +++ b/extra/troubleshooting.md @@ -39,7 +39,7 @@ In v1 of JMSSerializerBundle, the `serializer` alias is registered for the JMS S ```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`. diff --git a/laravel/filters.md b/laravel/filters.md index 260fb617d73..78582948d8b 100644 --- a/laravel/filters.md +++ b/laravel/filters.md @@ -7,7 +7,7 @@ API Platform is great for Rapid Application Development and provides lots of fun 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 +// app/Models/Book.php use ApiPlatform\Laravel\Eloquent\Filter\EqualsFilter; use ApiPlatform\Metadata\ApiResource; @@ -49,7 +49,7 @@ You can create your own filters by implementing the `ApiPlatform\Laravel\Eloquen You can add [validation rules](https://laravel.com/docs/validation) to parameters within the `constraints` attribute: ```php -// app/Models/Book.php +// app/Models/Book.php use ApiPlatform\Laravel\Eloquent\Filter\PartialSearchFilter; use ApiPlatform\Metadata\ApiResource; @@ -69,7 +69,7 @@ class Book extends Model 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 +// app/Models/Book.php use ApiPlatform\Laravel\Eloquent\Filter\PartialSearchFilter; use ApiPlatform\Laravel\Eloquent\Filter\OrderFilter; @@ -102,7 +102,7 @@ As shown above the following search filters are available: The `DateFilter` allows to filter dates with an operator (`eq`, `lt`, `gt`, `lte`, `gte`): ```php -// app/Models/Book.php +// app/Models/Book.php use ApiPlatform\Laravel\Eloquent\Filter\DateFilter; @@ -126,7 +126,7 @@ Our default strategy is to exclude null values, just remove the `filterContext` The `OrFilter` allows to filter using an `OR WHERE` clause: ```php -// app/Models/Book.php +// app/Models/Book.php use ApiPlatform\Laravel\Eloquent\Filter\DateFilter; @@ -160,7 +160,7 @@ Note: We strongly recommend using [Vulcain](https://vulcain.rocks) instead of th The property filter adds the possibility to select the properties to serialize (sparse fieldsets). ```php -// app/Models/Book.php +// app/Models/Book.php use ApiPlatform\Laravel\Eloquent\Filter\DateFilter; use ApiPlatform\Serializer\Filter\PropertyFilter; @@ -180,7 +180,7 @@ 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`. diff --git a/laravel/index.md b/laravel/index.md index 07d504c05bf..64d6f7aedcf 100644 --- a/laravel/index.md +++ b/laravel/index.md @@ -7,22 +7,22 @@ using Laravel! With API Platform, you can: -* [expose your Eloquent](#exposing-a-model) models in minutes as: - * a REST API implementing the industry-leading standards, formats and best practices: [JSON-LD](https://en.wikipedia.org/wiki/JSON-LD)/[RDF](https://en.wikipedia.org/wiki/Resource_Description_Framework), [JSON:API](https://jsonapi.org), [HAL](https://stateless.group/hal_specification.html), and many RFCs... - * a [GraphQL](#enabling-graphql) API - * or both at the same time, with the same code! -* automatically expose an [OpenAPI](https://www.openapis.org) specification (formerly Swagger), dynamically generated from your Eloquent models and always up to date -* automatically expose nice UIs and playgrounds to develop using your API ([Swagger UI](https://swagger.io/tools/swagger-ui/) and [GraphiQL](https://github.com/graphql/graphiql)) -* automatically paginate your collections -* add validation logic using Laravel [Form Request Validation](#write-operations-authorization-and-validation) -* add authorization logic using [gates and policies](#authorization) ([compatible with Sanctum, Passport, Socialite...](#authentication)) -* add [filtering logic](#adding-filters) +- [expose your Eloquent](#exposing-a-model) models in minutes as: + - a REST API implementing the industry-leading standards, formats and best practices: [JSON-LD](https://en.wikipedia.org/wiki/JSON-LD)/[RDF](https://en.wikipedia.org/wiki/Resource_Description_Framework), [JSON:API](https://jsonapi.org), [HAL](https://stateless.group/hal_specification.html), and many RFCs... + - a [GraphQL](#enabling-graphql) API + - or both at the same time, with the same code! +- automatically expose an [OpenAPI](https://www.openapis.org) specification (formerly Swagger), dynamically generated from your Eloquent models and always up to date +- automatically expose nice UIs and playgrounds to develop using your API ([Swagger UI](https://swagger.io/tools/swagger-ui/) and [GraphiQL](https://github.com/graphql/graphiql)) +- automatically paginate your collections +- add validation logic using Laravel [Form Request Validation](#write-operations-authorization-and-validation) +- add authorization logic using [gates and policies](#authorization) ([compatible with Sanctum, Passport, Socialite...](#authentication)) +- add [filtering logic](#adding-filters) -* 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 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!) -* 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 +- 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! @@ -160,8 +160,8 @@ the corresponding API request in the UI. Try it yourself by browsing to `http:// So, if you want to access the raw data, you have two alternatives: -* Add the correct `Accept` header (or don't set any `Accept` header at all if you don't care about security) - preferred when writing API clients -* Add the format you want as the extension of the resource - for debug purposes only +- 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. @@ -173,12 +173,13 @@ For instance, go to `http://127.0.0.1:8000/api/books.jsonld` to retrieve the lis 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: @@ -238,12 +239,12 @@ Register the state provider: ```php */ @@ -497,11 +498,11 @@ Then, update the `app/Models/Book.php` to hint Eloquent that it has an associate ```patch namespace App\Models; - + use ApiPlatform\Metadata\ApiResource; +use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; - + #[ApiResource] class Book extends Model { @@ -513,12 +514,12 @@ Reference this factory in the seeder (`database/seeder/DatabaseSeeder.php`): ```patch namespace Database\Seeders; - + +use App\Models\Book; use App\Models\User; // use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Database\Seeder; - + class DatabaseSeeder extends Seeder { /** @@ -527,12 +528,12 @@ Reference this factory in the seeder (`database/seeder/DatabaseSeeder.php`): public function run(): void { // User::factory(10)->create(); - + User::factory()->create([ 'name' => 'Test User', 'email' => 'test@example.com', ]); - + + Book::factory(100)->create(); } } @@ -560,11 +561,11 @@ This is configurable, to change to 10 items per page, change `app/Models/Book.ph ```patch namespace App\Models; - + use ApiPlatform\Metadata\ApiResource; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; - + -#[ApiResource] +#[ApiResource( + paginationItemsPerPage: 10, @@ -614,7 +615,6 @@ You can change the default configuration (for instance, which operations are ena 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: @@ -665,16 +665,15 @@ On top of that, some validation rules are automatically added based on the given API Platform comes with several filters dedicated to Laravel, [check them out](filters.md)! - ## Authentication API Platform hooks into the native [Laravel authentication mechanism](https://laravel.com/docs/authentication). It also natively supports: -* [Laravel Sanctum](https://laravel.com/docs/sanctum), an authentication system for SPAs (single page applications), mobile applications, and simple, token-based APIs -* [Laravel Passport](https://laravel.com/docs/passport), a full OAuth 2 server -* [Laravel Socialite](https://laravel.com/docs/socialite), OAuth providers including Facebook, X, LinkedIn, Google, GitHub, GitLab, Bitbucket, and Slack +- [Laravel Sanctum](https://laravel.com/docs/sanctum), an authentication system for SPAs (single page applications), mobile applications, and simple, token-based APIs +- [Laravel Passport](https://laravel.com/docs/passport), a full OAuth 2 server +- [Laravel Socialite](https://laravel.com/docs/socialite), OAuth providers including Facebook, X, LinkedIn, Google, GitHub, GitLab, Bitbucket, and Slack Follow the official instructions for the tool(s) you want to use. @@ -763,7 +762,7 @@ Then, add validation rules to the generated class (`app/Http/Requests/BookFormRe - return false; + return user()->isAdmin(); } - + /** * Get the validation rules that apply to the request. * @@ -844,7 +843,6 @@ API Platform also has an awesome [client generator](../create-client/index.md) a 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. diff --git a/laravel/security.md b/laravel/security.md index c4495ce9271..0d0b2751730 100644 --- a/laravel/security.md +++ b/laravel/security.md @@ -5,7 +5,7 @@ API platform is compatible with Laravel [authorization](https://laravel.com/docs/authorization) mechanism. Once a gate is defined, API Platform will automatically detect your policy. ```php -// app/Models/Book.php +// app/Models/Book.php use ApiPlatform\Metadata\Patch; @@ -18,7 +18,7 @@ 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: | Operation | Policy | -|----------------|------------------------------------------------------------| +| -------------- | ---------------------------------------------------------- | | GET collection | `viewAny` | | GET | `view` | | POST | `create` | @@ -27,6 +27,7 @@ API Platform will detect the operation and map it to a specific method in your p | 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: + ```php // app/Models/Book.php namespace App\Models; @@ -68,7 +69,7 @@ Gate::guessPolicyNamesUsing(function (string $modelClass): ?string { Usually, you will use [Sanctum](https://laravel.com/docs/sanctum) and add a middleware on secured routes: ```php -// app/Models/Book.php +// app/Models/Book.php use ApiPlatform\Metadata\Patch; diff --git a/laravel/validation.md b/laravel/validation.md index 6de1433c9d2..5909a0509aa 100644 --- a/laravel/validation.md +++ b/laravel/validation.md @@ -3,7 +3,7 @@ You can add [validation rules](https://laravel.com/docs/validation) within the `rules` option: ```php -// app/Models/Book.php +// app/Models/Book.php use ApiPlatform\Metadata\ApiResource; diff --git a/outline.yaml b/outline.yaml index 138710a3432..8b892d2ecd1 100644 --- a/outline.yaml +++ b/outline.yaml @@ -1,3 +1,4 @@ +--- chapters: - title: "API Platform for Symfony" path: symfony diff --git a/schema-generator/configuration.md b/schema-generator/configuration.md index b557e52102b..9007618fb21 100644 --- a/schema-generator/configuration.md +++ b/schema-generator/configuration.md @@ -39,13 +39,13 @@ 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 @@ -54,13 +54,13 @@ The cardinality of a property is automatically guessed. The `cardinality` option allows to override the guessed value. Supported cardinalities are: -* `(0..1)`: scalar, not required -* `(0..*)`: array, not required -* `(1..1)`: scalar, required -* `(1..*)`: array, required -* `(*..0)` -* `(*..1)` -* `(*..*)` +- `(0..1)`: scalar, not required +- `(0..*)`: array, not required +- `(1..1)`: scalar, required +- `(1..*)`: array, required +- `(*..0)` +- `(*..1)` +- `(*..*)` Cardinalities are enforced by the class generator, the Doctrine ORM generator and the Symfony validation generator. @@ -68,10 +68,10 @@ Example: ```yaml types: - Product: - properties: - sku: - cardinality: "(0..1)" + Product: + properties: + sku: + cardinality: '(0..1)' ``` ## Changing the Default Cardinality @@ -82,7 +82,7 @@ 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 @@ -94,26 +94,26 @@ For instance, if you want to change the join table name and add security for a s ```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,11 +124,11 @@ 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 @@ -140,8 +140,8 @@ Example: ```yaml types: - Person: - abstract: true + Person: + abstract: true ``` ## Define API Platform Operations @@ -150,11 +150,11 @@ 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 @@ -167,9 +167,9 @@ If no cardinality is found, it will be `true`. Example: ```yaml - Person: - properties: - name: { nullable: false } +Person: + properties: + name: { nullable: false } ``` The `#[Assert\NotNull]` constraint is automatically added. @@ -194,9 +194,9 @@ By default, this option is `false`. Example: ```yaml - Person: - properties: - email: { unique: true } +Person: + properties: + email: { unique: true } ``` Output: @@ -239,9 +239,9 @@ class Person A property can be marked read-only with the following configuration: ```yaml - Person: - properties: - email: { writable: false } +Person: + properties: + email: { writable: false } ``` In such case, no mutator method will be generated. @@ -251,9 +251,9 @@ In such case, no mutator method will be generated. A property can be marked write-only with the following configuration: ```yaml - Person: - properties: - email: { readable: false } +Person: + properties: + email: { readable: false } ``` In this case, no getter method will be generated. @@ -265,11 +265,11 @@ Force an `embeddable` class to be `embedded`. Example: ```yaml - QuantitativeValue: - embeddable: true - Product: - properties: - weight: { range: "QuantitativeValue", embedded: true } +QuantitativeValue: + embeddable: true +Product: + properties: + weight: { range: 'QuantitativeValue', embedded: true } ``` Output: @@ -344,7 +344,7 @@ This behavior can be disabled with the following setting: ```yaml id: - generate: false + generate: false ``` ## Generating UUIDs @@ -353,7 +353,7 @@ It's also possible to let the DBMS generate [UUIDs](https://en.wikipedia.org/wik ```yaml id: - generationStrategy: uuid + generationStrategy: uuid ``` ## User-submitted UUIDs @@ -362,8 +362,8 @@ 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 @@ -373,7 +373,7 @@ 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 @@ -384,7 +384,7 @@ This behavior can be disabled (to fall back to standard arrays) with the followi ```yaml doctrine: - useCollection: false + useCollection: false ``` ## Changing the Field Visibility @@ -395,7 +395,7 @@ The default visibility can be changed with the `fieldVisibility` option. Example: ```yaml -fieldVisibility: "protected" +fieldVisibility: 'protected' ``` ## Generating `Assert\Type` Attributes @@ -404,31 +404,31 @@ It's possible to automatically generate Symfony validator's `#[Assert\Type]` att ```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)), -thanks to the special `DoctrineOrmAssociationOverrideAttributeGenerator`. -* If a class has no child, an `#[Entity]` (or `#[Document]` for ODM) attribute is used. +- If a class has children and is referenced by a relation, + it will generate an inheritance mapping strategy with `#[InheritanceType]` (configurable, see below), `#[DiscriminatorColumn]` (`#[DiscriminatorField]` for ODM) and `#[DiscriminatorMap]`. + The discriminator map will be filled with all possible values. +- If a class has children but is not referenced by a relation, + it will generate a mapped superclass (`#[MappedSuperclass]`). + If this mapped superclass defines relations and is used by multiple children, + the generator will add `#[AssociationOverride]` attributes to them + (see the [related Doctrine documentation](https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/inheritance-mapping.html#association-override)), + thanks to the special `DoctrineOrmAssociationOverrideAttributeGenerator`. +- If a class has no child, an `#[Entity]` (or `#[Document]` for ODM) attribute is used. If this behaviour does not suit you, the inheritance attribute can be forced in the following way: ```yaml doctrine: - inheritanceType: SINGLE_TABLE # Default: JOINED - inheritanceAttributes: - CustomInheritanceAttribute: [] + inheritanceType: SINGLE_TABLE # Default: JOINED + inheritanceAttributes: + CustomInheritanceAttribute: [] ``` ## Interfaces and Doctrine Resolve Target Entity Listener @@ -444,14 +444,15 @@ To let the schema generator generate the mapping file usable with Symfony, add t ```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 @@ -460,8 +461,8 @@ 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 @@ -475,8 +476,8 @@ 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. @@ -486,11 +487,11 @@ For instance, to generate a data model from the [Video Game Ontology](http://pur ```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# # ... ``` @@ -503,16 +504,20 @@ 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 @@ -529,7 +534,7 @@ Add a `@author` PHPDoc annotation to class DocBlock. Example: ```yaml -author: "Kévin Dunglas " +author: 'Kévin Dunglas ' ``` ## PHP File Header @@ -540,14 +545,14 @@ Example: ```yaml header: | - /* - * This file is part of the Ecommerce package. - * - * (c) Kévin Dunglas - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ + /* + * This file is part of the Ecommerce package. + * + * (c) Kévin Dunglas + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ ``` ## Disabling Generators and Creating Custom Ones @@ -559,7 +564,7 @@ Example (enabling only the PHPDoc generator): ```yaml annotationGenerators: - - ApiPlatform\SchemaGenerator\AnnotationGenerator\PhpDocAnnotationGenerator + - ApiPlatform\SchemaGenerator\AnnotationGenerator\PhpDocAnnotationGenerator attributeGenerators: [] ``` @@ -580,245 +585,227 @@ 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 +debug: false # Use old API Platform attributes (API Platform < 2.7) apiPlatformOldAttributes: false # IDs configuration id: + # Automatically add an ID field to entities + generate: true - # 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 +useInterface: false # Emit a warning if a property is not derived from GoodRelations checkIsGoodRelations: false # A license or any text to use as header of generated files -header: null # Example: '// (c) Kévin Dunglas ' +header: null # Example: '// (c) Kévin Dunglas ' # 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 ' +author: false # Example: 'Kévin Dunglas ' # Visibility of entities fields -fieldVisibility: private # One of "private"; "protected"; "public" +fieldVisibility: private # One of "private"; "protected"; "public" # Set this flag to false to not generate getter, setter, adder and remover methods -accessorMethods: true +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 +allTypes: false # If a type is present in a vocabulary but not explicitly imported (types) or if the vocabulary is not totally imported (allTypes), it will be generated -resolveTypes: false +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#' - - # Is the class abstract? (null to guess) - abstract: null - - # Is the class embeddable? - embeddable: false + # Namespace of the vocabulary of this type (defaults to the global "vocabularyNamespace" entry) + vocabularyNamespace: null # Example: 'http://www.w3.org/ns/activitystreams#' - # Type namespaces - namespaces: - - # The namespace for the generated class (override any other defined namespace) - class: null + # Is the class abstract? (null to guess) + abstract: null - # The namespace for the generated interface (override any other defined namespace) - interface: null + # Is the class embeddable? + embeddable: false - # Attributes (merged with generated attributes) - attributes: [] + # Type namespaces + namespaces: + # The namespace for the generated class (override any other defined namespace) + class: null - # The parent class, set to false for a top level class - parent: false + # The namespace for the generated interface (override any other defined namespace) + interface: null - # If declaring a custom class, this will be the class from which properties type will be guessed - guessFrom: Thing + # Attributes (merged with generated attributes) + attributes: [] - # Operations for the class - operations: [] + # The parent class, set to false for a top level class + parent: false - # Import all existing properties - allProperties: false + # If declaring a custom class, this will be the class from which properties type will be guessed + guessFrom: Thing - # Properties of this type to use - properties: + # Operations for the class + operations: [] - # Prototype - id: + # Import all existing properties + allProperties: false - # 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: [] +generatorTemplates: [] ``` diff --git a/schema-generator/getting-started.md b/schema-generator/getting-started.md index 5f2743cb5d9..d3707e827b1 100644 --- a/schema-generator/getting-started.md +++ b/schema-generator/getting-started.md @@ -33,34 +33,34 @@ Then, write a simple YAML config file similar to the following. Here we will generate a data model for an address book with the following data: -* a [`Person`](https://schema.org/Person) which inherits from [`Thing`](https://schema.org/Thing); -* a [`PostalAddress`](https://schema.org/PostalAddress) (without its class hierarchy). +- a [`Person`](https://schema.org/Person) which inherits from [`Thing`](https://schema.org/Thing); +- a [`PostalAddress`](https://schema.org/PostalAddress) (without its class hierarchy). ```yaml # api/config/schema.yaml # The list of types and properties we want to use types: - # Parent class of Person - Thing: - properties: - name: ~ - Person: - # Enable the generation of the class hierarchy (not enabled by default) - parent: ~ - properties: - familyName: ~ - givenName: ~ - additionalName: ~ - address: ~ - PostalAddress: - properties: - # Force the type of the addressCountry property to text - addressCountry: { range: "Text" } - addressLocality: ~ - addressRegion: ~ - postOfficeBoxNumber: ~ - postalCode: ~ - streetAddress: ~ + # 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. @@ -74,8 +74,8 @@ 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 +90,7 @@ Write the following config file: ```yaml # api/config/schema.yaml openApi: - file: '../openapi.yaml' + file: '../openapi.yaml' ``` ## Usage diff --git a/schema-generator/index.md b/schema-generator/index.md index 7b89930668a..365674309a2 100644 --- a/schema-generator/index.md +++ b/schema-generator/index.md @@ -12,31 +12,31 @@ Alternatively, design your API with tools like [Stoplight](https://stoplight.io/ You get a fully featured PHP data model including: -* A set of PHP entities with properties, constants (enum values), getters, setters, adders and removers. The class -hierarchy provided by the vocabulary will be translated to a PHP class hierarchy with parents as `abstract` classes. -The generated code complies with [PSR](https://www.php-fig.org/) coding standards; -* Full, high-quality PHPDoc and type declarations for classes, properties, constants and methods extracted from the vocabulary; -* Doctrine ORM or MongoDB ODM attributes mapping including database columns / fields with type guessing, relations with cardinality guessing, -smart class inheritance (through the `#[MappedSuperclass]` or `#[InheritanceType]` attributes depending on if the resource is used in a relation); -* Data validation through [Symfony Validator](https://symfony.com/doc/current/book/validation.html) attributes including enum support (choices) and check for required properties; -* API Platform attributes; -* Interfaces and [Doctrine `ResolveTargetEntityListener`](https://www.doctrine-project.org/projects/doctrine-orm/en/current/cookbook/resolve-target-entity-listener.html) -support; -* Custom PHP namespace support; -* List of values provided the vocabulary with [PHP Enum](https://github.com/myclabs/php-enum) classes. +- A set of PHP entities with properties, constants (enum values), getters, setters, adders and removers. The class + hierarchy provided by the vocabulary will be translated to a PHP class hierarchy with parents as `abstract` classes. + The generated code complies with [PSR](https://www.php-fig.org/) coding standards; +- Full, high-quality PHPDoc and type declarations for classes, properties, constants and methods extracted from the vocabulary; +- Doctrine ORM or MongoDB ODM attributes mapping including database columns / fields with type guessing, relations with cardinality guessing, + smart class inheritance (through the `#[MappedSuperclass]` or `#[InheritanceType]` attributes depending on if the resource is used in a relation); +- Data validation through [Symfony Validator](https://symfony.com/doc/current/book/validation.html) attributes including enum support (choices) and check for required properties; +- API Platform attributes; +- Interfaces and [Doctrine `ResolveTargetEntityListener`](https://www.doctrine-project.org/projects/doctrine-orm/en/current/cookbook/resolve-target-entity-listener.html) + support; +- Custom PHP namespace support; +- List of values provided the vocabulary with [PHP Enum](https://github.com/myclabs/php-enum) classes. Bonus: -* The code generator is fully configurable and extendable. All features can be deactivated (e.g., the Doctrine mapping generator) -and a custom generator can be added; -* The code generator can load previously generated files and add new changes while keeping the user-added ones; -* The generated code can be used as is in a [Symfony](https://symfony.com) app (but it will work too in a raw PHP project -or any other framework including [Laravel](https://laravel.com) and [Zend Framework](https://framework.zend.com/)). +- 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). +[microdata]() and [RDF](https://en.wikipedia.org/wiki/Resource_Description_Framework). Extracting semantical data exposed in the Schema.org vocabulary is supported by a growing number of companies including Google (Search, Gmail), Yahoo!, Bing and Yandex. @@ -68,5 +68,5 @@ It opens the way to generic web API clients able to extract and process data fro ## Documentation -* [Getting Started](getting-started.md) -* [Configuration](configuration.md) +- [Getting Started](getting-started.md) +- [Configuration](configuration.md) diff --git a/symfony/debugging.md b/symfony/debugging.md index 10da1a54e7a..e128a4dfce5 100644 --- a/symfony/debugging.md +++ b/symfony/debugging.md @@ -19,12 +19,12 @@ First, [create a PHP debug remote server configuration](https://www.jetbrains.co 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! @@ -34,32 +34,32 @@ You can now use the debugger! 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 VSCode +## Using Xdebug With Visual Studio Code -If you are using VSCode, use the following `launch.json` to debug. +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" + } + } + ] } ``` diff --git a/symfony/index.md b/symfony/index.md index 5468d48408d..7ba2dcd3ee2 100644 --- a/symfony/index.md +++ b/symfony/index.md @@ -2,7 +2,7 @@ ![The welcome page](images/api-platform-3.0-welcome.png) -> *API Platform* is the most advanced API platform, in any framework or language. +> _API Platform_ is the most advanced API platform, in any framework or language. > > —Fabien Potencier (creator of Symfony) @@ -14,40 +14,39 @@ API Platform contains [a **PHP** library (Core)](../core/index.md) to create ful 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. +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/) to generate [Next.js](../create-client/ -) 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/), 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 +- 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/) to generate [Next.js](../create-client/) 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/), 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 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*. +model of our API** and handcraft it as _Plain Old PHP Objects_. API Platform uses these model classes to expose and document a web API having a bunch of built-in features: -* creating, retrieving, updating, and deleting (CRUD) resources -* data validation -* pagination -* filtering -* sorting -* hypermedia/[HATEOAS](https://en.wikipedia.org/wiki/HATEOAS) and content negotiation support ([JSON-LD](https://json-ld.org) and [Hydra](https://www.hydra-cg.com/), [JSON:API](https://jsonapi.org/), [HAL](https://tools.ietf.org/html/draft-kelly-json-hal-08)...) -* [GraphQL support](../core/graphql.md) -* Nice UI and machine-readable documentations ([Swagger UI/OpenAPI](https://swagger.io), [GraphiQL](https://github.com/graphql/graphiql)...) -* authentication ([Basic HTTP](https://en.wikipedia.org/wiki/Basic_access_authentication), cookies as well as [JWT](../core/jwt.md) and [OAuth](https://oauth.net) through extensions) -* [CORS headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS) -* security checks and headers (tested against [OWASP recommendations](https://www.owasp.org/index.php/REST_Security_Cheat_Sheet)) -* [invalidation-based HTTP caching](../core/performance.md) -* and basically everything needed to build modern APIs. +- creating, retrieving, updating, and deleting (CRUD) resources +- data validation +- pagination +- filtering +- sorting +- hypermedia/[HATEOAS](https://en.wikipedia.org/wiki/HATEOAS) and content negotiation support ([JSON-LD](https://json-ld.org) and [Hydra](https://www.hydra-cg.com/), [JSON:API](https://jsonapi.org/), [HAL](https://tools.ietf.org/html/draft-kelly-json-hal-08)...) +- [GraphQL support](../core/graphql.md) +- Nice UI and machine-readable documentations ([Swagger UI/OpenAPI](https://swagger.io), [GraphiQL](https://github.com/graphql/graphiql)...) +- authentication ([Basic HTTP](https://en.wikipedia.org/wiki/Basic_access_authentication), cookies as well as [JWT](../core/jwt.md) and [OAuth](https://oauth.net) through extensions) +- [CORS headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS) +- security checks and headers (tested against [OWASP recommendations](https://www.owasp.org/index.php/REST_Security_Cheat_Sheet)) +- [invalidation-based HTTP caching](../core/performance.md) +- and basically everything needed to build modern APIs. One more thing, before we start: as the API Platform distribution includes [the Symfony framework](https://symfony.com), it is compatible with most [Symfony bundles](https://symfony.com/bundles) @@ -82,30 +81,31 @@ docker compose build --no-cache Then, start Docker Compose in detached mode: ```console -docker compose up --wait +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: +> > ```console > SERVER_NAME=localhost:80 HTTP_PORT=8080 TRUSTED_HOSTS=localhost docker compose up --wait` > ``` This starts the following services: -| Name | Description | -|----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Name | Description | +| -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | php | The API powered by [FrankenPHP](https://frankenphp.dev) (a modern application server for PHP built on top of [Caddy web server](caddy.md) and with native support for [Mercure realtime](../core/mercure.md), [Vulcain relations preloading](https://vulcain.rocks), and [XDebug](debugging.md)), Composer, and sensitive configs | -| pwa | Next.js project compatible with Create Client and having Admin preinstalled | -| database | PostgreSQL database server | +| pwa | Next.js project compatible with Create Client and having Admin preinstalled | +| database | PostgreSQL database server | The following components are available: | URL | Path | Language | Description | -|----------------------------|--------------------|------------|-------------------------| +| -------------------------- | ------------------ | ---------- | ----------------------- | | `https://localhost/docs/` | `api/` | PHP | The API | | `https://localhost/` | `pwa/` | TypeScript | The Next.js application | | `https://localhost/admin/` | `pwa/pages/admin/` | TypeScript | The Admin | @@ -193,7 +193,7 @@ 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). +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. @@ -219,15 +219,15 @@ API Platform exposes a description of the API in the [OpenAPI](https://www.opena 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, +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 format you want as the extension of the resource - for debug purpose only +- 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. @@ -353,9 +353,9 @@ 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; + 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. + 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. @@ -371,7 +371,7 @@ Modify these files as described in these patches: use ApiPlatform\Metadata\ApiResource; use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\ORM\Mapping as ORM; - + /** A book. */ +#[ORM\Entity] #[ApiResource] @@ -380,31 +380,31 @@ Modify these files as described in these patches: /** The ID of this book. */ + #[ORM\Id, ORM\Column, ORM\GeneratedValue] private ?int $id = null; - + /** The ISBN of this book (or null if doesn't have one). */ + #[ORM\Column(nullable: true)] public ?string $isbn = null; - + /** The title of this book. */ + #[ORM\Column] public string $title = ''; - + /** The description of this book. */ + #[ORM\Column(type: 'text')] public string $description = ''; - + /** The author of this book. */ + #[ORM\Column] public string $author = ''; - + /** The publication date of this book. */ + #[ORM\Column] public ?\DateTimeImmutable $publicationDate = null; - + /** @var Review[] Available reviews for this book. */ + #[ORM\OneToMany(targetEntity: Review::class, mappedBy: 'book', cascade: ['persist', 'remove'])] public iterable $reviews; - + public function __construct() ``` @@ -412,10 +412,10 @@ Modify these files as described in these patches: ```diff namespace App\Entity; - + use ApiPlatform\Metadata\ApiResource; +use Doctrine\ORM\Mapping as ORM; - + /** A review of a book. */ +#[ORM\Entity] #[ApiResource] @@ -424,27 +424,27 @@ Modify these files as described in these patches: /** The ID of this review. */ + #[ORM\Id, ORM\Column, ORM\GeneratedValue] private ?int $id = null; - + /** The rating of this review (between 0 and 5). */ + #[ORM\Column(type: 'smallint')] public int $rating = 0; - + /** The body of the review. */ + #[ORM\Column(type: 'text')] public string $body = ''; - + /** The author of the review. */ + #[ORM\Column] public string $author = ''; - + /** The date of publication of this review.*/ + #[ORM\Column] public ?\DateTimeImmutable $publicationDate = null; - + /** The book this review is about. */ + #[ORM\ManyToOne(inversedBy: 'reviews')] public ?Book $book = null; - + public function getId(): ?int ``` @@ -521,11 +521,11 @@ Now, add a review for this book using the `POST` operation for the `Review` reso ```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" } ``` @@ -583,22 +583,22 @@ Modify the following files as described in these patches: use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Validator\Constraints as Assert; - #[ORM\Column(nullable: true)] + #[ORM\Column(nullable: true)] + #[Assert\Isbn] - public ?string $isbn = null; - + public ?string $isbn = null; + #[ORM\Column] + #[Assert\NotBlank] public string $title = ''; - + #[ORM\Column(type: 'text')] + #[Assert\NotBlank] public string $description = ''; - - #[ORM\Column] + + #[ORM\Column] + #[Assert\NotBlank] public string $author = ''; - + #[ORM\Column] + #[Assert\NotNull] public ?\DateTimeImmutable $publicationDate = null; @@ -611,26 +611,26 @@ Modify the following files as described in these patches: use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Validator\Constraints as Assert; - #[ORM\Column(type: 'smallint')] + #[ORM\Column(type: 'smallint')] + #[Assert\Range(min: 0, max: 5)] public int $rating = 0; - + #[ORM\Column(type: 'text')] + #[Assert\NotBlank] public string $body = ''; - + #[ORM\Column] + #[Assert\NotBlank] public string $author = ''; - - #[ORM\Column] + + #[ORM\Column] + #[Assert\NotNull] public ?\DateTimeImmutable $publicationDate = null; - - #[ORM\ManyToOne(inversedBy: 'reviews')] + + #[ORM\ManyToOne(inversedBy: 'reviews')] + #[Assert\NotNull] public ?Book $book = null; - + public function getId(): ?int ``` @@ -671,7 +671,7 @@ docker compose exec php sh -c ' composer require webonyx/graphql-php bin/console cache:clear ' -```` +``` 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: @@ -682,7 +682,7 @@ Try it out by creating a greeting: ```graphql mutation { - createGreeting(input: {name: "Test2"}) { + createGreeting(input: { name: "Test2" }) { greeting { id name @@ -751,8 +751,9 @@ 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: diff --git a/symfony/testing.md b/symfony/testing.md index c6acbe6eed6..4bf8e7db4c9 100644 --- a/symfony/testing.md +++ b/symfony/testing.md @@ -11,10 +11,10 @@ Let's learn how to use them! 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 @@ -211,7 +211,7 @@ class BooksTest extends ApiTestCase { // Create 100 books using our factory BookFactory::createMany(100); - + // The client implements Symfony HttpClient's `HttpClientInterface`, and the response `ResponseInterface` $response = static::createClient()->request('GET', '/books'); @@ -293,7 +293,7 @@ publicationDate: This value should not be null.', { // Only create the book we need with a given ISBN BookFactory::createOne(['isbn' => '9781344037075']); - + $client = static::createClient(); // findIriBy allows to retrieve the IRI of an item by searching for some of its properties. $iri = $this->findIriBy(Book::class, ['isbn' => '9781344037075']); @@ -305,7 +305,7 @@ publicationDate: This value should not be null.', ], 'headers' => [ 'Content-Type' => 'application/merge-patch+json', - ] + ] ]); $this->assertResponseIsSuccessful(); @@ -363,7 +363,7 @@ To do so, learn how to write unit tests with [PHPUnit](https://phpunit.de/) and 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]() 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. @@ -371,21 +371,21 @@ The API Platform Demo [contains a CD workflow](https://github.com/api-platform/d 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 +- [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 +- [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 +- [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. +- [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 (E2E) 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. +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, E2E testing should be done with a production-like setup. For your convenience, you may [run our Docker Compose setup +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#running-the-docker-compose-setup-for-production-locally). From d05e63805af750da99e5ff30b6b0a15f740e77b5 Mon Sep 17 00:00:00 2001 From: Jonny Eom Date: Fri, 11 Oct 2024 07:49:33 -0400 Subject: [PATCH 13/80] fix(doc): Remove duplicate swagger_ui_extra_configuration (#2032) --- core/configuration.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/core/configuration.md b/core/configuration.md index 922a799ae54..cb8f843e3c9 100644 --- a/core/configuration.md +++ b/core/configuration.md @@ -195,13 +195,6 @@ api_platform: # URL to the license used for the API. MUST be in the format of a URL. url: - swagger_ui_extra_configuration: - # Controls the default expansion setting for the operations and tags. It can be 'list' (expands only the tags), 'full' (expands the tags and operations) or 'none' (expands nothing). - docExpansion: list - # If set, enables filtering. The top bar will show an edit box that you can use to filter the tagged operations that are shown. - filter: false - # You can use any other configuration parameters too. - http_cache: # To make all responses public by default. public: ~ From 6c43cc11d79ab4c9bf47c19aab256052f368c7b4 Mon Sep 17 00:00:00 2001 From: DELON Romain <144770208+rdeloniut@users.noreply.github.com> Date: Fri, 11 Oct 2024 13:49:47 +0200 Subject: [PATCH 14/80] docs:Update filters.md - change wrong dependency injection (#2033) --- core/filters.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/filters.md b/core/filters.md index 18f35216761..801a86bacc5 100644 --- a/core/filters.md +++ b/core/filters.md @@ -895,7 +895,7 @@ namespace App\Entity; use ApiPlatform\Metadata\ApiFilter; use ApiPlatform\Metadata\ApiResource; -use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\ExistsFilter; +use ApiPlatform\Doctrine\Orm\Filter\ExistsFilter; #[ApiResource] #[ApiFilter(ExistsFilter::class, properties: ['transportFees'])] From 6fb431078c598e1401ed1d82162e4d6d7e65c86b Mon Sep 17 00:00:00 2001 From: Vincent Amstoutz Date: Fri, 11 Oct 2024 13:55:44 +0200 Subject: [PATCH 15/80] Add Laravel support to state processors documentation (#2019) Co-authored-by: Antoine Bluchet --- core/state-processors.md | 158 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 156 insertions(+), 2 deletions(-) diff --git a/core/state-processors.md b/core/state-processors.md index b3b9b5d7979..bdab3f7e65b 100644 --- a/core/state-processors.md +++ b/core/state-processors.md @@ -5,10 +5,13 @@ classes called **state processors**. State processors receive an instance of the the `#[ApiResource]` attribute). This instance contains data submitted by the client during [the deserialization process](serialization.md). -A state processor using [Doctrine ORM](https://www.doctrine-project.org/projects/orm.html) is included with the library and +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...) @@ -20,6 +23,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 @@ -75,8 +79,64 @@ use App\State\BlogPostProcessor; 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 +``` + +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: + +```php + + */ +final class BlogPostProcessor implements ProcessorInterface +{ + /** + * @return BlogPost|void + */ + public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed + { + // call your persistence layer to save $data + return $data; + } +} +``` + +The `process()` method must return the created or modified object, or nothing (that's why `void` is allowed) for `DELETE` operations. +The `process()` method can also take an object as input, in the `$data` parameter, that isn't of the same type that its output (the returned object). See [the DTO documentation entry](dto.md) for more details. + +We then configure our operation to use this processor: + +```php + + */ +final class UserProcessor implements ProcessorInterface +{ + public function __construct( + private ProcessorInterface $persistProcessor, + private ProcessorInterface $removeProcessor, + ) + { + } + + /** + * @return User|void + */ + public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed + { + if ($operation instanceof DeleteOperationInterface) { + return $this->removeProcessor->process($data, $operation, $uriVariables, $context); + } + + $result = $this->persistProcessor->process($data, $operation, $uriVariables, $context); + $this->sendWelcomeEmail($data); + + return $result; + } + + private function sendWelcomeEmail(User $user): void + { + // Your welcome email logic... + // Mail::to($user->getEmail())->send(new WelcomeMail($user)); + } +} +``` + +Don't forget to tag the service with the `PersistProcessor` and the `RemoveProcessor` state classes. + +```php +app->tag([UserProcessor::class], [PersistProcessor::class, RemoveProcessor::class,]); + } + + public function boot(): void + { + } +} +``` +If you're using Laravel MongoDB ODM instead of Eloquent ORM, make sure you're using the right services. + +Finally, configure that you want to use this processor on the User resource: + +```php + Date: Fri, 11 Oct 2024 13:56:09 +0200 Subject: [PATCH 16/80] Add Laravel support to state providers documentation (#2018) Co-authored-by: Antoine Bluchet --- core/state-providers.md | 226 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 215 insertions(+), 11 deletions(-) diff --git a/core/state-providers.md b/core/state-providers.md index af275568a95..6faa21f0c5e 100644 --- a/core/state-providers.md +++ b/core/state-providers.md @@ -1,11 +1,18 @@ # State Providers -To retrieve data exposed by the API, API Platform uses classes called **state providers**. A state provider using [Doctrine -ORM](https://www.doctrine-project.org/projects/orm.html) to retrieve data from a database, a state provider using +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, and a state provider using [Elasticsearch-PHP](https://www.elastic.co/guide/en/elasticsearch/client/php-api/current/index.html) -to retrieve data from an Elasticsearch cluster are included with the library. The first one is enabled by default. These -state providers natively support paged collections and filters. They can be used as-is and are perfectly suited to common uses. +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. + +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. Custom state providers can be used to do so. A project can include as many state providers as needed. The first able to @@ -13,12 +20,17 @@ 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 an entity class called `App\Entity\BlogPost`. -Note, that if your entity is not Doctrine-related, you need to flag the identifier property by using +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)). ## 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: @@ -114,7 +126,7 @@ final class BlogPostProvider implements ProviderInterface } ``` -We then need to configure this same provider on the BlogPost `GetCollection` operation, or for every operations 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 + */ +final class BlogPostProvider implements ProviderInterface +{ + private const DATA = [ + 'ab' => new BlogPost('ab'), + 'cd' => new BlogPost('cd'), + ]; + + public function provide(Operation $operation, array $uriVariables = [], array $context = []): BlogPost|null + { + return self::DATA[$uriVariables['id']] ?? null; + } +} +``` + +For the example, we store the list of our blog posts in an associative array (the `BlogPostProvider::DATA` constant). + +As this operation expects a `BlogPost`, the `provide` methods return the instance of the `BlogPost` corresponding to the ID passed in the URL. If the ID doesn't exist in the associative array, `provide()` returns `null`. API Platform will automatically generate a 404 response if the provider returns `null`. + +The `$uriVariables` parameter contains an array with the values of the URI variables. + +To use this provider we need to configure the provider on the operation: + +```php + + */ +final class BlogPostProvider implements ProviderInterface +{ + private const DATA = [ + 'ab' => new BlogPost('ab'), + 'cd' => new BlogPost('cd'), + ]; + + public function provide(Operation $operation, array $uriVariables = [], array $context = []): iterable|BlogPost|null + { + if ($operation instanceof CollectionOperationInterface) { + return self::DATA; + } + + return self::DATA[$uriVariables['id']] ?? null; + } +} +``` + +We then need to configure this same provider on the BlogPost `GetCollection` operation, or for every operation via the `ApiResource` attribute: + +```php +app->tag([BookRepresentationProvider::class], ProviderInterface::class); + } + + public function boot(): void + { + } +} +``` + +After that, you can inject the `ProviderInterface` +```php + + */ +final class BookRepresentationProvider implements ProviderInterface +{ + public function __construct( + private ProviderInterface $itemProvider, + ) + { + } + + public function provide(Operation $operation, array $uriVariables = [], array $context = []): AnotherRepresentation + { + $book = $this->itemProvider->provide($operation, $uriVariables, $context); + + return new AnotherRepresentation( + // Add DTO constructor params here. + // $book->getTitle(), + ); + } +} +``` + +And configure that you want to use this provider on the Book resource: + +```php + Date: Mon, 14 Oct 2024 13:56:58 +0200 Subject: [PATCH 17/80] fix: remove id-token permission --- .github/workflows/cd.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 7f64e18f0e9..6a4f0a6eb92 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -9,7 +9,6 @@ on: permissions: contents: read - id-token: read actions: read checks: write deployments: write From e4b6ed063bbde64ff7a2b24c859e5c4cc47038b7 Mon Sep 17 00:00:00 2001 From: Vincent Amstoutz Date: Mon, 14 Oct 2024 10:40:09 +0200 Subject: [PATCH 18/80] docs: adds Laravel-specific doc parts --- core/subresources.md | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/core/subresources.md b/core/subresources.md index 5c9e571337a..b06443cc802 100644 --- a/core/subresources.md +++ b/core/subresources.md @@ -5,8 +5,8 @@ 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 a working Doctrine layer for -subresources providing you add the correct configuration for URI Variables. +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 @@ -14,10 +14,13 @@ URI Variables are configured via the `uriVariables` node on an `ApiResource`. It 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 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”. + 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`. @@ -84,6 +87,7 @@ class Question ``` ```yaml +# The YAML syntax is only supported for Symfony # api/config/api_platform/resources.yaml resources: App\Entity\Answer: ~ @@ -92,6 +96,7 @@ resources: ```xml + @@ -228,6 +237,9 @@ resources: ### Company Employee's +> [!NOTE] +> In Symfony we use the term “entities”, while the following documentation is mostly for Laravel “models”. + Note that in this example, we declared an association using Doctrine only between Employee and Company using a ManyToOne. There is no inverse association hence the use of `toProperty` in the URI Variables definition. The following declares a few subresources: - `/companies/{companyId}/employees/{id}` - get an employee belonging to a company - `/companies/{companyId}/employees` - get the company employee's @@ -310,7 +322,7 @@ class Company } ``` -We did not define any Doctrine 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`. @@ -335,6 +347,9 @@ class Company { ## Security +> [!WARNING] +> This is not yet available with Laravel, you're welcome to contribute [on Github](github.com/api-platform/core) + In order to use Symfony's built-in security system on subresources the security option of the `Link` attribute can be used. To restrict the access to a subresource based on the parent object simply use the Symfony expression language as you would do normally, with the exception that the name defined in `toProperty` or `fromProperty` is used to access the object. From b1748bd34ccedc376333231d5ae4a09c195a004e Mon Sep 17 00:00:00 2001 From: Vincent Amstoutz Date: Wed, 23 Oct 2024 10:36:08 +0200 Subject: [PATCH 19/80] docs: move symfony validation docs to a specific doc - Create a `symfony/validation.md` file and move all Symfony-specific code from `core/validation.md` to it. - Update the `outline.yaml` accordingly. - Add links in `core/validation.md` to redirect users to the Laravel and Symfony validation documentation. --- core/validation.md | 620 +---------------------------------------- laravel/validation.md | 4 +- outline.yaml | 1 + symfony/validation.md | 622 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 628 insertions(+), 619 deletions(-) create mode 100644 symfony/validation.md diff --git a/core/validation.md b/core/validation.md index c3e69dda46d..42533bab891 100644 --- a/core/validation.md +++ b/core/validation.md @@ -1,622 +1,6 @@ # Validation API Platform takes care of validating the data sent to the API by the client (usually user data entered through forms). -By default, the framework relies on [the powerful Symfony Validator Component](https://symfony.com/doc/current/validation.html) -for this task, but you can replace it with your preferred validation library such as [the PHP filter extension](https://www.php.net/manual/en/intro.filter.php) if you want to. -

Validation screencast
Watch the Validation screencast

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

+ +## Validating Submitted Data + +Validating submitted data is as simple as adding [Symfony's built-in constraints](https://symfony.com/doc/current/reference/constraints.html) +or [custom constraints](https://symfony.com/doc/current/validation/custom_constraint.html) directly in classes marked with +the `#[ApiResource]` attribute: + +```php +context->buildViolation($constraint->message)->addViolation(); + } + } +} +``` + +If the data submitted by the client is invalid, the HTTP status code will be set to `422 Unprocessable Entity` and the response's +body will contain the list of violations serialized in a format compliant with the requested one. For instance, a validation +error will look like the following if the requested format is JSON-LD (the default): + +```json +{ + "@context": "/contexts/ConstraintViolationList", + "@type": "ConstraintViolationList", + "title": "An error occurred", + "description": "properties: The product must have the minimal properties required (\"description\", \"price\")", + "violations": [ + { + "propertyPath": "properties", + "message": "The product must have the minimal properties required (\"description\", \"price\")" + } + ] +} +``` + +Take a look at the [Errors Handling guide](errors.md) to learn how API Platform converts PHP exceptions like validation +errors to HTTP errors. + +## Using Validation Groups + +Without specific configuration, the default validation group is always used, but this behavior is customizable: the framework +is able to leverage Symfony's [validation groups](https://symfony.com/doc/current/validation/groups.html). + +You can configure the groups you want to use when the validation occurs directly through the `ApiResource` attribute: + +```php + ['a', 'b']])] +class Book +{ + #[Assert\NotBlank(groups: ['a'])] + public string $name; + + #[Assert\NotNull(groups: ['b'])] + public string $author; + + // ... +} +``` + +With the previous configuration, the validation groups `a` and `b` will be used when validation is performed. + +Like for [serialization groups](serialization.md#using-different-serialization-groups-per-operation), +you can specify validation groups globally or on a per-operation basis. + +Of course, you can use XML or YAML configuration format instead of attributes if you prefer. + +You may also pass in a [group sequence](https://symfony.com/doc/current/validation/sequence_provider.html) in place of +the array of group names. + +## Using Validation Groups on Operations + +You can have different validation for each [operation](operations.md) related to your resource. + +```php + ['Default', 'putValidation']])] +#[GetCollection] +#[Post(validationContext: ['groups' => ['Default', 'postValidation']])] +class Book +{ + #[Assert\Uuid] + private $id; + + #[Assert\NotBlank(groups: ['postValidation'])] + public $name; + + #[Assert\NotNull] + #[Assert\Length(min: 2, max: 50, groups: ['postValidation'])] + #[Assert\Length(min: 2, max: 70, groups: ['putValidation'])] + public $author; + + // ... +} +``` + +With this configuration, there are three validation groups: + +`Default` contains the constraints that belong to no other group. + +`postValidation` contains the constraints on the name and author (length from 2 to 50) fields only. + +`putValidation` contains the constraints on the author (length from 2 to 70) field only. + +## Dynamic Validation Groups + +If you need to dynamically determine which validation groups to use for an entity in different scenarios, just pass in a +[callable](https://www.php.net/manual/en/language.types.callable.php). The callback will receive the entity object as its first +argument, and should return an array of group names or a [group sequence](https://symfony.com/doc/current/validation/sequence_provider.html). + +In the following example, we use a static method to return the validation groups: + +```php + [Book::class, 'validationGroups']] +)] +class Book +{ + /** + * Return dynamic validation groups. + * + * @param self $book Contains the instance of Book to validate. + * + * @return string[] + */ + public static function validationGroups(self $book) + { + return ['a']; + } + + #[Assert\NotBlank(groups: ['a'])] + public $name; + + #[Assert\NotNull(groups: ['b'])] + public $author; + + // ... +} +``` + +Alternatively, you can use a service to retrieve the groups to use: + +```php +authorizationChecker = $authorizationChecker; + } + + public function __invoke($book): array + { + assert($book instanceof Book); + + return $this->authorizationChecker->isGranted('ROLE_ADMIN', $book) ? ['a', 'b'] : ['a']; + } +} +``` + +This class selects the groups to apply based on the role of the current user: if the current user has the `ROLE_ADMIN` role, groups `a` and `b` are returned. In other cases, just `a` is returned. + +This class is automatically registered as a service thanks to [the autowiring feature of the Symfony DependencyInjection component](https://symfony.com/doc/current/service_container/autowiring.html). + +Then, configure the entity class to use this service to retrieve validation groups: + +```php + AdminGroupsGenerator::class]) +class Book +{ + #[Assert\NotBlank(groups: ['a'])] + public $name; + + #[Assert\NotNull(groups: ['b'])] + public $author; + + // ... +} +``` + +## Sequential Validation Groups + +If you need to specify the order in which your validation groups must be tested against, you can use a [group sequence](https://symfony.com/doc/current/validation/sequence_provider.html). +First, you need to create your sequenced group. + +```php + MySequencedGroup::class])] +class Greeting +{ + #[ORM\Id, ORM\Column, ORM\GeneratedValue] + private ?int $id = null; + + /** + * @var A nice person + * + * I want this "second" validation to be executed after the "first" one even though I wrote them in this order. + * @One(groups={"second"}) + * @Two(groups={"first"}) + */ + #[ORM\Column] + public string $name = ''; + + public function getId(): int + { + return $this->id; + } +} +``` + +## Validating Delete Operations + +By default, validation rules that are specified on the API resource are not evaluated during DELETE operations. You need to trigger the validation in your code, if needed. + +Assume that you have the following entity that uses a custom delete validator: + +```php + ['deleteValidation']], processor: MyEntityRemoveProcessor::class) + ] +)] +#[AssertCanDelete(groups: ['deleteValidation'])] +class MyEntity +{ + #[ORM\Id, ORM\Column, ORM\GeneratedValue] + private ?int $id = null; + + #[ORM\Column] + public string $name = ''; +} +``` + +Create a processor, which receives the default processor, where you will trigger the validation: + +```php +validator->validate($data, ['groups' => ['deleteValidation']]); + $this->doctrineProcessor->process($data, $operation, $uriVariables, $context); + } +} +``` + +## Error Levels and Payload Serialization + +As stated in the [Symfony documentation](https://symfony.com/doc/current/validation/severity.html), you can use the payload field to define error levels. +You can retrieve the payload field by setting the `serialize_payload_fields` to an empty `array` in the API Platform config: + +```yaml +# api/config/packages/api_platform.yaml + +api_platform: + validator: + serialize_payload_fields: ~ +``` + +Then, the serializer will return all payload values in the error response. + +If you want to serialize only some payload fields, define them in the config like this: + +```yaml +# api/config/packages/api_platform.yaml + +api_platform: + validator: + serialize_payload_fields: [severity, anotherPayloadField] +``` + +In this example, only `severity` and `anotherPayloadField` will be serialized. + +## Validation on Collection Relations + +Use the [Valid](https://symfony.com/doc/current/reference/constraints/Valid.html) constraint. + +Note: this is related to the [collection relation denormalization](./serialization.md#collection-relation). +You may have an issue when trying to validate a relation representing a Doctrine's `ArrayCollection` (`toMany`). Fix the denormalization using the property getter. Return an `array` instead of an `ArrayCollection` with `$collectionRelation->getValues()`. Then, define your validation on the getter instead of the property. + +For example: + +```xml + + + +``` + +```php +cars = new ArrayCollection(); + } + + #[Assert\Valid] + public function getCars() + { + return $this->cars->getValues(); + } +} +``` + +## Open Vocabulary Generated from Validation Metadata + +API Platform automatically detects Symfony's built-in validators and generates schema.org IRI metadata accordingly. This allows for rich clients such as the Admin component to infer the field types for most basic use cases. + +The following validation constraints are covered: + +| Constraints | Vocabulary | +| ------------------------------------------------------------------------------------- | ---------------------------------- | +| [`Url`](https://symfony.com/doc/current/reference/constraints/Url.html) | `https://schema.org/url` | +| [`Email`](https://symfony.com/doc/current/reference/constraints/Email.html) | `https://schema.org/email` | +| [`Uuid`](https://symfony.com/doc/current/reference/constraints/Uuid.html) | `https://schema.org/identifier` | +| [`CardScheme`](https://symfony.com/doc/current/reference/constraints/CardScheme.html) | `https://schema.org/identifier` | +| [`Bic`](https://symfony.com/doc/current/reference/constraints/Bic.html) | `https://schema.org/identifier` | +| [`Iban`](https://symfony.com/doc/current/reference/constraints/Iban.html) | `https://schema.org/identifier` | +| [`Date`](https://symfony.com/doc/current/reference/constraints/Date.html) | `https://schema.org/Date` | +| [`DateTime`](https://symfony.com/doc/current/reference/constraints/DateTime.html) | `https://schema.org/DateTime` | +| [`Time`](https://symfony.com/doc/current/reference/constraints/Time.html) | `https://schema.org/Time` | +| [`Image`](https://symfony.com/doc/current/reference/constraints/Image.html) | `https://schema.org/image` | +| [`File`](https://symfony.com/doc/current/reference/constraints/File.html) | `https://schema.org/MediaObject` | +| [`Currency`](https://symfony.com/doc/current/reference/constraints/Currency.html) | `https://schema.org/priceCurrency` | +| [`Isbn`](https://symfony.com/doc/current/reference/constraints/Isbn.html) | `https://schema.org/isbn` | +| [`Issn`](https://symfony.com/doc/current/reference/constraints/Issn.html) | `https://schema.org/issn` | + +## Specification Property Restrictions + +API Platform generates specification property restrictions based on Symfony’s built-in validator. + +For example, from [`Regex`](https://symfony.com/doc/4.4/reference/constraints/Regex.html) constraint API +Platform builds [`pattern`](https://swagger.io/docs/specification/data-models/data-types/#pattern) restriction. + +For building custom property schema based on custom validation constraints you can create a custom class +for generating property scheme restriction. + +To create property schema, you have to implement the [`PropertySchemaRestrictionMetadataInterface`](https://github.com/api-platform/core/blob/main/src/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaRestrictionMetadataInterface.php). +This interface defines only 2 methods: + +- `create`: to create property schema +- `supports`: to check whether the property and constraint is supported + +Here is an implementation example: + +```php +// api/src/PropertySchemaRestriction/CustomPropertySchemaRestriction.php + +namespace App\PropertySchemaRestriction; + +use ApiPlatform\Metadata\ApiProperty; +use Symfony\Component\Validator\Constraint; +use App\Validator\CustomConstraint; + +final class CustomPropertySchemaRestriction implements PropertySchemaRestrictionMetadataInterface +{ + public function supports(Constraint $constraint, ApiProperty $propertyMetadata): bool + { + return $constraint instanceof CustomConstraint; + } + + public function create(Constraint $constraint, ApiProperty $propertyMetadata): array + { + // your logic to create property schema restriction based on constraint + return $restriction; + } +} +``` + +If you use a custom dependency injection configuration, you need to register the corresponding service and add the +`api_platform.metadata.property_schema_restriction` tag. The `priority` attribute can be used for service ordering. + +```yaml +# api/config/services.yaml + +services: + # ... + 'App\PropertySchemaRestriction\CustomPropertySchemaRestriction': + ~ + # Uncomment only if autoconfiguration is disabled + #tags: [ 'api_platform.metadata.property_schema_restriction' ] +``` + +## Collecting Denormalization Errors + +When submitting data you can collect denormalization errors using the [COLLECT_DENORMALIZATION_ERRORS option](https://symfony.com/doc/current/components/serializer.html#collecting-type-errors-while-denormalizing). + +It can be done directly in the `#[ApiResource]` attribute (or in the operations): + +```php + Date: Wed, 23 Oct 2024 16:10:20 +0200 Subject: [PATCH 20/80] Update minikube.md (#2043) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kévin Dunglas --- deployment/minikube.md | 39 ++++++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/deployment/minikube.md b/deployment/minikube.md index 9e669799c28..b571d9e83d6 100644 --- a/deployment/minikube.md +++ b/deployment/minikube.md @@ -1,8 +1,8 @@ -# Deploying to Minikube +# Deploying to minikube -## Install Minikube +## Install minikube -If you have no existing installation of Minikube on your computer, [follow the official tutorial](https://minikube.sigs.k8s.io/docs/start/). +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,40 +10,49 @@ 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. - -If you use Mac or Windows, [refer to the documentation](https://minikube.sigs.k8s.io/docs/handbook/registry/#docker-on-macos) to learn how to expose the Docker registry installed as an addon on the port 5000 of the host. +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. ## Building and Pushing Docker Images -First, build the images: +On GNU/Linux and macOS, run the following command following command to point your terminal’s docker-cli to the Docker Engine inside minikube: + +```console +eval $(minikube docker-env) +``` + +Now any `docker` command you run in this current terminal will run against the Docker Engine inside the minikube cluster. For detailed explanation and instructions for Windows [visit official minikube documentation](https://minikube.sigs.k8s.io/docs/handbook/pushing/#1-pushing-directly-to-the-in-cluster-docker-daemon-docker-env). + +Build the images in minikube: ```console -docker build -t localhost:5000/php api --target api_platform_php -docker build -t localhost:5000/caddy api --target api_platform_caddy -docker build -t localhost:5000/pwa pwa --target api_platform_pwa_prod +docker build -t localhost:5000/php api --target frankenphp_prod +docker build -t localhost:5000/pwa pwa --target prod ``` -Then push the images in the registry installed in Minikube: +Then push the images in the registry available in minikube: ```console docker push localhost:5000/php -docker push localhost:5000/caddy docker push localhost:5000/pwa ``` ## Deploying +Fetch Helm chart dependencies: + +```console +helm repo add postgresql https://charts.bitnami.com/bitnami/ +helm dependency build helm/api-platform +``` + Finally, deploy the project using the Helm chart: ```console -$ helm install my-project helm/api-platform \ +helm install my-project helm/api-platform \ --set php.image.repository=localhost:5000/php \ --set php.image.tag=latest \ - --set caddy.image.repository=localhost:5000/caddy \ - --set caddy.image.tag=latest \ --set pwa.image.repository=localhost:5000/pwa \ --set pwa.image.tag=latest ``` From f13bf217031ca99043f5b491570669c1745c46cf Mon Sep 17 00:00:00 2001 From: Saleen0x0f <95493765+Saleen0x0f@users.noreply.github.com> Date: Wed, 23 Oct 2024 17:10:57 +0300 Subject: [PATCH 21/80] fix: displaying images (#2040) Co-authored-by: root --- core/serialization.md | 2 +- deployment/docker-compose.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/serialization.md b/core/serialization.md index fcfd3150b5b..70594776175 100644 --- a/core/serialization.md +++ b/core/serialization.md @@ -8,7 +8,7 @@ API Platform embraces and extends the Symfony Serializer Component to transform The main serialization process has two stages: -![Serializer workflow](/docs/core/images/SerializerWorkflow.png) +![Serializer workflow](../core/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) diff --git a/deployment/docker-compose.md b/deployment/docker-compose.md index 4fee630a451..eea18991b77 100644 --- a/deployment/docker-compose.md +++ b/deployment/docker-compose.md @@ -46,7 +46,7 @@ your-domain-name.example.com. IN A 207.154.233.113 Example with the DigitalOcean Domains service ("Networking" > "Domains"): -![Configuring DNS on DigitalOcean](digitalocean-dns.png) +![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. From 2390b15d50875f9c2943efe7723d8c44bb37fcea Mon Sep 17 00:00:00 2001 From: Tobias Oitzinger <42447585+toitzi@users.noreply.github.com> Date: Fri, 25 Oct 2024 10:06:05 +0200 Subject: [PATCH 22/80] [Laravel] Add Caching documentation (#2048) --- laravel/index.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/laravel/index.md b/laravel/index.md index 64d6f7aedcf..a204a02177e 100644 --- a/laravel/index.md +++ b/laravel/index.md @@ -849,6 +849,15 @@ to make the app usable by people with disabilities. Checkout [the dedicated documentation](../create-client/index.md). +## Caching + +API Platform supports Caching Metadata out of the box. It uses the Laravel cache system to store that information. +Caching is automatically enabled in production environments (when `APP_DEBUG` is set to `false`). + +Calling `php artisan optimize` will cache the metadata and improve the performance of your API drastically. + +To clear the cache, use `php artisan optimize:clear`. + ## Hooking Your Own Business Logic Now that you learned the basics, be sure to read [the general design considerations](../core/design.md) and [how to extend API Platform](../core/extending.md) to understand how API Platform is designed, and how to hook your custom business logic! From 52b566cf12150a7f6554d7fb678b735f3df25443 Mon Sep 17 00:00:00 2001 From: Deuchnord Date: Mon, 28 Oct 2024 16:19:40 +0100 Subject: [PATCH 23/80] fix: Dockerfile for custom FrankenPHP builds (#2051) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kévin Dunglas --- core/performance.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/performance.md b/core/performance.md index acf278e9c7f..bb3711b7358 100644 --- a/core/performance.md +++ b/core/performance.md @@ -32,7 +32,7 @@ The integration using the cache handler is quite simple. You just have to update # Versions -FROM dunglas/frankenphp:1-php8.3 AS frankenphp_upstream -+FROM dunglas/frankenphp:latest-builder AS builder ++FROM dunglas/frankenphp:1-builder-php8.3 AS builder +COPY --from=caddy:builder /usr/bin/xcaddy /usr/bin/xcaddy + +RUN apt-get update && apt-get install --no-install-recommends -y \ From 48195df6ac661edf25217510856bbfb901f87634 Mon Sep 17 00:00:00 2001 From: Vincent Amstoutz Date: Mon, 28 Oct 2024 10:38:20 +0100 Subject: [PATCH 24/80] Create LICENSE file --- LICENSE | 397 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 397 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000000..c1cd95bc45a --- /dev/null +++ b/LICENSE @@ -0,0 +1,397 @@ +Licence Creative Commons Attribution 4.0 International (CC BY 4.0) + +Copyright (c) 2015-present [Kévin Dunglas] + +======================================================================= + +Creative Commons Corporation ("Creative Commons") is not a law firm and +does not provide legal services or legal advice. Distribution of +Creative Commons public licenses does not create a lawyer-client or +other relationship. Creative Commons makes its licenses and related +information available on an "as-is" basis. Creative Commons gives no +warranties regarding its licenses, any material licensed under their +terms and conditions, or any related information. Creative Commons +disclaims all liability for damages resulting from their use to the +fullest extent possible. + +Using Creative Commons Public Licenses + +Creative Commons public licenses provide a standard set of terms and +conditions that creators and other rights holders may use to share +original works of authorship and other material subject to copyright +and certain other rights specified in the public license below. The +following considerations are for informational purposes only, are not +exhaustive, and do not form part of our licenses. + + Considerations for licensors: Our public licenses are + intended for use by those authorized to give the public + permission to use material in ways otherwise restricted by + copyright and certain other rights. Our licenses are + irrevocable. Licensors should read and understand the terms + and conditions of the license they choose before applying it. + Licensors should also secure all rights necessary before + applying our licenses so that the public can reuse the + material as expected. Licensors should clearly mark any + material not subject to the license. This includes other CC- + licensed material, or material used under an exception or + limitation to copyright. More considerations for licensors: + wiki.creativecommons.org/Considerations_for_licensors + + Considerations for the public: By using one of our public + licenses, a licensor grants the public permission to use the + licensed material under specified terms and conditions. If + the licensor's permission is not necessary for any reason--for + example, because of any applicable exception or limitation to + copyright--then that use is not regulated by the license. Our + licenses grant only permissions under copyright and certain + other rights that a licensor has authority to grant. Use of + the licensed material may still be restricted for other + reasons, including because others have copyright or other + rights in the material. A licensor may make special requests, + such as asking that all changes be marked or described. + Although not required by our licenses, you are encouraged to + respect those requests where reasonable. More_considerations + for the public: + wiki.creativecommons.org/Considerations_for_licensees + +======================================================================= + +Creative Commons Attribution 4.0 International Public License + +By exercising the Licensed Rights (defined below), You accept and agree +to be bound by the terms and conditions of this Creative Commons +Attribution 4.0 International Public License ("Public License"). To the +extent this Public License may be interpreted as a contract, You are +granted the Licensed Rights in consideration of Your acceptance of +these terms and conditions, and the Licensor grants You such rights in +consideration of benefits the Licensor receives from making the +Licensed Material available under these terms and conditions. + + +Section 1 -- Definitions. + +a. Adapted Material means material subject to Copyright and Similar +Rights that is derived from or based upon the Licensed Material +and in which the Licensed Material is translated, altered, +arranged, transformed, or otherwise modified in a manner requiring +permission under the Copyright and Similar Rights held by the +Licensor. For purposes of this Public License, where the Licensed +Material is a musical work, performance, or sound recording, +Adapted Material is always produced where the Licensed Material is +synched in timed relation with a moving image. + +b. Adapter's License means the license You apply to Your Copyright +and Similar Rights in Your contributions to Adapted Material in +accordance with the terms and conditions of this Public License. + +c. Copyright and Similar Rights means copyright and/or similar rights +closely related to copyright including, without limitation, +performance, broadcast, sound recording, and Sui Generis Database +Rights, without regard to how the rights are labeled or +categorized. For purposes of this Public License, the rights +specified in Section 2(b)(1)-(2) are not Copyright and Similar +Rights. + +d. Effective Technological Measures means those measures that, in the +absence of proper authority, may not be circumvented under laws +fulfilling obligations under Article 11 of the WIPO Copyright +Treaty adopted on December 20, 1996, and/or similar international +agreements. + +e. Exceptions and Limitations means fair use, fair dealing, and/or +any other exception or limitation to Copyright and Similar Rights +that applies to Your use of the Licensed Material. + +f. Licensed Material means the artistic or literary work, database, +or other material to which the Licensor applied this Public +License. + +g. Licensed Rights means the rights granted to You subject to the +terms and conditions of this Public License, which are limited to +all Copyright and Similar Rights that apply to Your use of the +Licensed Material and that the Licensor has authority to license. + +h. Licensor means the individual(s) or entity(ies) granting rights +under this Public License. + +i. Share means to provide material to the public by any means or +process that requires permission under the Licensed Rights, such +as reproduction, public display, public performance, distribution, +dissemination, communication, or importation, and to make material +available to the public including in ways that members of the +public may access the material from a place and at a time +individually chosen by them. + +j. Sui Generis Database Rights means rights other than copyright +resulting from Directive 96/9/EC of the European Parliament and of +the Council of 11 March 1996 on the legal protection of databases, +as amended and/or succeeded, as well as other essentially +equivalent rights anywhere in the world. + +k. You means the individual or entity exercising the Licensed Rights +under this Public License. Your has a corresponding meaning. + + +Section 2 -- Scope. + +a. License grant. + + 1. Subject to the terms and conditions of this Public License, + the Licensor hereby grants You a worldwide, royalty-free, + non-sublicensable, non-exclusive, irrevocable license to + exercise the Licensed Rights in the Licensed Material to: + + a. reproduce and Share the Licensed Material, in whole or + in part; and + + b. produce, reproduce, and Share Adapted Material. + + 2. Exceptions and Limitations. For the avoidance of doubt, where + Exceptions and Limitations apply to Your use, this Public + License does not apply, and You do not need to comply with + its terms and conditions. + + 3. Term. The term of this Public License is specified in Section + 6(a). + + 4. Media and formats; technical modifications allowed. The + Licensor authorizes You to exercise the Licensed Rights in + all media and formats whether now known or hereafter created, + and to make technical modifications necessary to do so. The + Licensor waives and/or agrees not to assert any right or + authority to forbid You from making technical modifications + necessary to exercise the Licensed Rights, including + technical modifications necessary to circumvent Effective + Technological Measures. For purposes of this Public License, + simply making modifications authorized by this Section 2(a) + (4) never produces Adapted Material. + + 5. Downstream recipients. + + a. Offer from the Licensor -- Licensed Material. Every + recipient of the Licensed Material automatically + receives an offer from the Licensor to exercise the + Licensed Rights under the terms and conditions of this + Public License. + + b. No downstream restrictions. You may not offer or impose + any additional or different terms or conditions on, or + apply any Effective Technological Measures to, the + Licensed Material if doing so restricts exercise of the + Licensed Rights by any recipient of the Licensed + Material. + + 6. No endorsement. Nothing in this Public License constitutes or + may be construed as permission to assert or imply that You + are, or that Your use of the Licensed Material is, connected + with, or sponsored, endorsed, or granted official status by, + the Licensor or others designated to receive attribution as + provided in Section 3(a)(1)(A)(i). + +b. Other rights. + + 1. Moral rights, such as the right of integrity, are not + licensed under this Public License, nor are publicity, + privacy, and/or other similar personality rights; however, to + the extent possible, the Licensor waives and/or agrees not to + assert any such rights held by the Licensor to the limited + extent necessary to allow You to exercise the Licensed + Rights, but not otherwise. + + 2. Patent and trademark rights are not licensed under this + Public License. + + 3. To the extent possible, the Licensor waives any right to + collect royalties from You for the exercise of the Licensed + Rights, whether directly or through a collecting society + under any voluntary or waivable statutory or compulsory + licensing scheme. In all other cases the Licensor expressly + reserves any right to collect such royalties. + + +Section 3 -- License Conditions. + +Your exercise of the Licensed Rights is expressly made subject to the +following conditions. + +a. Attribution. + + 1. If You Share the Licensed Material (including in modified + form), You must: + + a. retain the following if it is supplied by the Licensor + with the Licensed Material: + + i. identification of the creator(s) of the Licensed + Material and any others designated to receive + attribution, in any reasonable manner requested by + the Licensor (including by pseudonym if + designated); + + ii. a copyright notice; + + iii. a notice that refers to this Public License; + + iv. a notice that refers to the disclaimer of + warranties; + + v. a URI or hyperlink to the Licensed Material to the + extent reasonably practicable; + + b. indicate if You modified the Licensed Material and + retain an indication of any previous modifications; and + + c. indicate the Licensed Material is licensed under this + Public License, and include the text of, or the URI or + hyperlink to, this Public License. + + 2. You may satisfy the conditions in Section 3(a)(1) in any + reasonable manner based on the medium, means, and context in + which You Share the Licensed Material. For example, it may be + reasonable to satisfy the conditions by providing a URI or + hyperlink to a resource that includes the required + information. + + 3. If requested by the Licensor, You must remove any of the + information required by Section 3(a)(1)(A) to the extent + reasonably practicable. + + 4. If You Share Adapted Material You produce, the Adapter's + License You apply must not prevent recipients of the Adapted + Material from complying with this Public License. + + +Section 4 -- Sui Generis Database Rights. + +Where the Licensed Rights include Sui Generis Database Rights that +apply to Your use of the Licensed Material: + +a. for the avoidance of doubt, Section 2(a)(1) grants You the right +to extract, reuse, reproduce, and Share all or a substantial +portion of the contents of the database; + +b. if You include all or a substantial portion of the database +contents in a database in which You have Sui Generis Database +Rights, then the database in which You have Sui Generis Database +Rights (but not its individual contents) is Adapted Material; and + +c. You must comply with the conditions in Section 3(a) if You Share +all or a substantial portion of the contents of the database. + +For the avoidance of doubt, this Section 4 supplements and does not +replace Your obligations under this Public License where the Licensed +Rights include other Copyright and Similar Rights. + + +Section 5 -- Disclaimer of Warranties and Limitation of Liability. + +a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE +EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS +AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF +ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, +IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, +WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, +ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT +KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT +ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. + +b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE +TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, +NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, +INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, +COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR +USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN +ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR +DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR +IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. + +c. The disclaimer of warranties and limitation of liability provided +above shall be interpreted in a manner that, to the extent +possible, most closely approximates an absolute disclaimer and +waiver of all liability. + + +Section 6 -- Term and Termination. + +a. This Public License applies for the term of the Copyright and +Similar Rights licensed here. However, if You fail to comply with +this Public License, then Your rights under this Public License +terminate automatically. + +b. Where Your right to use the Licensed Material has terminated under +Section 6(a), it reinstates: + + 1. automatically as of the date the violation is cured, provided + it is cured within 30 days of Your discovery of the + violation; or + + 2. upon express reinstatement by the Licensor. + + For the avoidance of doubt, this Section 6(b) does not affect any + right the Licensor may have to seek remedies for Your violations + of this Public License. + +c. For the avoidance of doubt, the Licensor may also offer the +Licensed Material under separate terms or conditions or stop +distributing the Licensed Material at any time; however, doing so +will not terminate this Public License. + +d. Sections 1, 5, 6, 7, and 8 survive termination of this Public +License. + + +Section 7 -- Other Terms and Conditions. + +a. The Licensor shall not be bound by any additional or different +terms or conditions communicated by You unless expressly agreed. + +b. Any arrangements, understandings, or agreements regarding the +Licensed Material not stated herein are separate from and +independent of the terms and conditions of this Public License. + + +Section 8 -- Interpretation. + +a. For the avoidance of doubt, this Public License does not, and +shall not be interpreted to, reduce, limit, restrict, or impose +conditions on any use of the Licensed Material that could lawfully +be made without permission under this Public License. + +b. To the extent possible, if any provision of this Public License is +deemed unenforceable, it shall be automatically reformed to the +minimum extent necessary to make it enforceable. If the provision +cannot be reformed, it shall be severed from this Public License +without affecting the enforceability of the remaining terms and +conditions. + +c. No term or condition of this Public License will be waived and no +failure to comply consented to unless expressly agreed to by the +Licensor. + +d. Nothing in this Public License constitutes or may be interpreted +as a limitation upon, or waiver of, any privileges and immunities +that apply to the Licensor or You, including from the legal +processes of any jurisdiction or authority. + + +======================================================================= + +Creative Commons is not a party to its public +licenses. Notwithstanding, Creative Commons may elect to apply one of +its public licenses to material it publishes and in those instances +will be considered the “Licensor.” The text of the Creative Commons +public licenses is dedicated to the public domain under the CC0 Public +Domain Dedication. Except for the limited purpose of indicating that +material is shared under a Creative Commons public license or as +otherwise permitted by the Creative Commons policies published at +creativecommons.org/policies, Creative Commons does not authorize the +use of the trademark "Creative Commons" or any other trademark or logo +of Creative Commons without its prior written consent including, +without limitation, in connection with any unauthorized modifications +to any of its public licenses or any other arrangements, +understandings, or agreements concerning use of licensed material. For +the avoidance of doubt, this paragraph does not form part of the +public licenses. + +Creative Commons may be contacted at creativecommons.org. From c1b6386bb3aa7f34c5d548d0a6b0c7197434b47a Mon Sep 17 00:00:00 2001 From: Mohamed Attia <92581654+Speelwolf@users.noreply.github.com> Date: Fri, 1 Nov 2024 09:34:04 +0100 Subject: [PATCH 25/80] Update extensions.md (#2052) --- core/extensions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/extensions.md b/core/extensions.md index 1cdbefc4c63..971f419e5be 100644 --- a/core/extensions.md +++ b/core/extensions.md @@ -6,7 +6,7 @@ Extensions are specific to Doctrine and Elasticsearch-PHP, and therefore, the Do 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/main/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 From bcfda353ce1ce98add5221062aa13b1cae3b7438 Mon Sep 17 00:00:00 2001 From: k-37 <60838818+k-37@users.noreply.github.com> Date: Sun, 3 Nov 2024 13:22:58 +0100 Subject: [PATCH 26/80] Update minikube.md (#2053) --- deployment/minikube.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/deployment/minikube.md b/deployment/minikube.md index b571d9e83d6..34d15483357 100644 --- a/deployment/minikube.md +++ b/deployment/minikube.md @@ -16,7 +16,7 @@ Finally, [install Helm](https://helm.sh/docs/intro/install/). We'll use it to de ## Building and Pushing Docker Images -On GNU/Linux and macOS, run the following command 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) @@ -43,14 +43,14 @@ docker push localhost:5000/pwa Fetch Helm chart dependencies: ```console -helm repo add postgresql https://charts.bitnami.com/bitnami/ +helm repo add bitnami https://charts.bitnami.com/bitnami/ helm dependency build helm/api-platform ``` Finally, deploy the project using the Helm chart: ```console -helm install my-project helm/api-platform \ +helm upgrade --install my-project helm/api-platform \ --set php.image.repository=localhost:5000/php \ --set php.image.tag=latest \ --set pwa.image.repository=localhost:5000/pwa \ From 2aad219434288a00b906158869646172c822c8be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Vernet?= Date: Tue, 5 Nov 2024 21:18:30 +0100 Subject: [PATCH 27/80] docs: add missing use --- laravel/index.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/laravel/index.md b/laravel/index.md index a204a02177e..219d569f20e 100644 --- a/laravel/index.md +++ b/laravel/index.md @@ -242,6 +242,8 @@ Register the state provider: namespace App\Providers; +use ApiPlatform\State\ProviderInterface; +use App\State\BookProvider; use Illuminate\Contracts\Foundation\Application; use Illuminate\Support\ServiceProvider; From 32a7cdc21ea06284701d20e16bb6e9762049bb5c Mon Sep 17 00:00:00 2001 From: Vincent Amstoutz Date: Fri, 8 Nov 2024 14:57:28 +0100 Subject: [PATCH 28/80] Create README file (#2050) --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 00000000000..f32cb8d650a --- /dev/null +++ b/README.md @@ -0,0 +1,14 @@ +

API Platform

+ +# API Platform Documentation + +[![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. + +This repository contains all the API Platform documentation resources. + +## Contributing + +Please check our [CONTRIBUTING file](/CONTRIBUTING.md) to contribute. + From 51bda9e52ca95a33b113199f8fb68420cb38f802 Mon Sep 17 00:00:00 2001 From: Alan Poulain Date: Fri, 8 Nov 2024 14:58:43 +0100 Subject: [PATCH 29/80] chore(file-upload): modernize to an existing resource part (#2059) --- core/file-upload.md | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/core/file-upload.md b/core/file-upload.md index 571680e1e04..703ba86fef0 100644 --- a/core/file-upload.md +++ b/core/file-upload.md @@ -349,9 +349,7 @@ use Symfony\Component\HttpFoundation\File\File; use Symfony\Component\Serializer\Annotation\Groups; use Vich\UploaderBundle\Mapping\Annotation as Vich; -/** - * @Vich\Uploadable - */ +#[Vich\Uploadable] #[ORM\Entity] #[ApiResource( normalizationContext: ['groups' => ['book:read']], @@ -373,9 +371,10 @@ class Book #[Groups(['book:read'])] public ?string $contentUrl = null; - /** - * @Vich\UploadableField(mapping="media_object", fileNameProperty="filePath") - */ + #[Vich\UploadableField( + mapping: 'media_object', + fileNameProperty: 'filePath', + )] #[Groups(['book:write'])] public ?File $file = null; @@ -404,7 +403,7 @@ final class MultipartDecoder implements DecoderInterface { public const FORMAT = 'multipart'; - public function __construct(private RequestStack $requestStack) + public function __construct(private readonly RequestStack $requestStack) { } @@ -459,8 +458,6 @@ final class UploadedFileDenormalizer implements DenormalizerInterface public function getSupportedTypes(?string $format): array { return [ - 'object' => null, - '*' => false, File::class => true, ]; } From df4f014466e06178b0bd239f2571f1995fe0aad2 Mon Sep 17 00:00:00 2001 From: g-cummings Date: Fri, 8 Nov 2024 14:03:17 +0000 Subject: [PATCH 30/80] fix(upgrade-guide): extra_properties should live under "defaults" --- core/upgrade-guide.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/core/upgrade-guide.md b/core/upgrade-guide.md index 70afac1ce3c..c895df78b0f 100644 --- a/core/upgrade-guide.md +++ b/core/upgrade-guide.md @@ -51,8 +51,9 @@ Standard PUT is now `true` by default, you can change its value using: ```yaml api_platform: - 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. From 8640a7f17d84020c0cf81387d0c4857de954a05b Mon Sep 17 00:00:00 2001 From: Vincent Amstoutz Date: Thu, 14 Nov 2024 12:14:30 +0100 Subject: [PATCH 31/80] docs(coc): fix broken link (#2060) --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2458ebb6e2e..05e7b3fa72a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,7 +17,7 @@ Before submitting your issue: ## Code of Conduct -By contributing to this project, you agree to abide by our [Code of Conduct](https://github.com/api-platform/docs#contributor-code-of-conduct). 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 From c2149d08e32eadd8f46745c3195f218295e39fff Mon Sep 17 00:00:00 2001 From: Vincent Amstoutz Date: Thu, 14 Nov 2024 16:04:36 +0100 Subject: [PATCH 32/80] refactor(doctrine): reference date filters constants from interface (#2057) --- core/filters.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/core/filters.md b/core/filters.md index 801a86bacc5..98615da0f31 100644 --- a/core/filters.md +++ b/core/filters.md @@ -664,10 +664,10 @@ 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\Orm\Filter\DateFilter::EXCLUDE_NULL` (`exclude_null`) | -| Consider items as oldest | `ApiPlatform\Doctrine\Orm\Filter\DateFilter::INCLUDE_NULL_BEFORE` (`include_null_before`) | -| Consider items as youngest | `ApiPlatform\Doctrine\Orm\Filter\DateFilter::INCLUDE_NULL_AFTER` (`include_null_after`) | -| Always include items | `ApiPlatform\Doctrine\Orm\Filter\DateFilter::INCLUDE_NULL_BEFORE_AND_AFTER` (`include_null_before_and_after`) | +| Exclude items | `ApiPlatform\Doctrine\Common\Filter\DateFilterInterface::EXCLUDE_NULL` (`exclude_null`) | +| Consider items as oldest | `ApiPlatform\Doctrine\Common\Filter\DateFilterInterface::INCLUDE_NULL_BEFORE` (`include_null_before`) | +| Consider items as youngest | `ApiPlatform\Doctrine\Common\Filter\DateFilterInterface::INCLUDE_NULL_AFTER` (`include_null_after`) | +| Always include items | `ApiPlatform\Doctrine\Common\Filter\DateFilterInterface::INCLUDE_NULL_BEFORE_AND_AFTER` (`include_null_before_and_after`) | For instance, exclude entries with a property value of `null` with the following service definition: @@ -678,12 +678,13 @@ For instance, exclude entries with a property value of `null` with the following // api/src/Entity/Offer.php namespace App\Entity; +use ApiPlatform\Doctrine\Common\Filter\DateFilterInterface; use ApiPlatform\Metadata\ApiFilter; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Doctrine\Orm\Filter\DateFilter; #[ApiResource] -#[ApiFilter(DateFilter::class, properties: ['dateProperty' => DateFilter::EXCLUDE_NULL])] +#[ApiFilter(DateFilter::class, properties: ['dateProperty' => DateFilterInterface::EXCLUDE_NULL])] class Offer { // ... @@ -1970,6 +1971,7 @@ For example, let's define three data filters (`DateFilter`, `SearchFilter` and ` // api/src/Entity/DummyCar.php namespace App\Entity; +use ApiPlatform\Doctrine\Common\Filter\DateFilterInterface; use ApiPlatform\Metadata\ApiFilter; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Doctrine\Orm\Filter\BooleanFilter; @@ -1981,7 +1983,7 @@ use Doctrine\ORM\Mapping as ORM; #[ApiResource] #[ApiFilter(BooleanFilter::class)] -#[ApiFilter(DateFilter::class, strategy: DateFilter::EXCLUDE_NULL)] +#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)] #[ApiFilter(SearchFilter::class, properties: ['colors.prop' => 'ipartial', 'name' => 'partial'])] #[ApiFilter(PropertyFilter::class, arguments: ['parameterName' => 'foobar'])] #[ApiFilter(GroupFilter::class, arguments: ['parameterName' => 'foobargroups'])] @@ -1998,10 +2000,10 @@ The `BooleanFilter` is applied to every `Boolean` property of the class. Indeed, #[ApiFilter(BooleanFilter::class)] ``` -The `DateFilter` given here will be applied to every `Date` property of the `DummyCar` class with the `DateFilter::EXCLUDE_NULL` strategy: +The `DateFilter` given here will be applied to every `Date` property of the `DummyCar` class with the `DateFilterInterface::EXCLUDE_NULL` strategy: ```php -#[ApiFilter(DateFilter::class, strategy: DateFilter::EXCLUDE_NULL)] +#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)] ``` The `SearchFilter` here adds properties. The result is the exact same as the example with attributes on properties: From 1d6a941dd28961213939f34d837bc059c18d43bf Mon Sep 17 00:00:00 2001 From: Vincent Amstoutz Date: Thu, 14 Nov 2024 16:25:15 +0100 Subject: [PATCH 33/80] fix: missing ContentNegotiationProvider(#2061) --- core/extending.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/core/extending.md b/core/extending.md index adfd4b39a80..d7c52e0ebd0 100644 --- a/core/extending.md +++ b/core/extending.md @@ -48,12 +48,13 @@ The schema below describes them: title: System providers and processors --- flowchart TB - C1(ReadProvider) --> C2(AccessCheckerProvider) - C2 --> C3(DeserializeProvider) - C3 --> C4(ParameterProvider) - C4 --> C5(ValidateProcessor) - C5 --> C6(WriteProcessor) - C6 --> C7(SerializeProcessor) + C1(ContentNegotiationProvider) --> C2(ReadProvider) + C2 --> C3(AccessCheckerProvider) + C3 --> C4(DeserializeProvider) + C4 --> C5(ParameterProvider) + C5 --> C6(ValidateProcessor) + C6 --> C7(WriteProcessor) + C7 --> C8(SerializeProcessor) ``` ### Symfony Access Checker Provider From 04c697f6704496de355d5d7fbbf32120a14e6492 Mon Sep 17 00:00:00 2001 From: Samuel NELA Date: Thu, 14 Nov 2024 20:04:08 +0100 Subject: [PATCH 34/80] fix: use stable version for doctrine projects (#2063) --- core/events.md | 2 +- core/extensions.md | 4 ++-- core/filters.md | 4 ++-- core/mongodb.md | 2 +- core/performance.md | 2 +- core/serialization.md | 4 ++-- symfony/index.md | 6 +++--- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/core/events.md b/core/events.md index 0efc1d5926b..4b17dfab79d 100644 --- a/core/events.md +++ b/core/events.md @@ -21,7 +21,7 @@ To do so, API Platform Core leverages [events triggered by the Symfony HTTP Kern 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/latest/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 diff --git a/core/extensions.md b/core/extensions.md index 971f419e5be..ac832b248fc 100644 --- a/core/extensions.md +++ b/core/extensions.md @@ -149,12 +149,12 @@ 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/latest/reference/aggregation-builder.html). +- `ApiPlatform\Doctrine\Odm\Extension\AggregationItemExtensionInterface` and `ApiPlatform\Doctrine\Odm\Extension\AggregationCollectionExtensionInterface` to add stages to the [aggregation builder](https://www.doctrine-project.org/projects/doctrine-mongodb-odm/en/current/reference/aggregation-builder.html). - `ApiPlatform\Doctrine\Odm\Extension\AggregationResultItemExtensionInterface` and `ApiPlatform\Doctrine\Odm\Extension\AggregationResultCollectionExtensionInterface` to return a result. The tags are `api_platform.doctrine_mongodb.odm.aggregation_extension.item` and `api_platform.doctrine_mongodb.odm.aggregation_extension.collection`. -The custom extensions receive the [aggregation builder](https://www.doctrine-project.org/projects/doctrine-mongodb-odm/en/latest/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 Elasticsearch Extension diff --git a/core/filters.md b/core/filters.md index 98615da0f31..2ccea89d8e3 100644 --- a/core/filters.md +++ b/core/filters.md @@ -1727,7 +1727,7 @@ class Offer ### Creating Custom Doctrine MongoDB ODM 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/latest/reference/aggregation-builder.html) +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. @@ -1775,7 +1775,7 @@ class AndOperatorFilterExtension implements RequestBodySearchCollectionExtension ### Using Doctrine ORM Filters -Doctrine ORM features [a filter system](https://www.doctrine-project.org/projects/doctrine-orm/en/latest/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 (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). diff --git a/core/mongodb.md b/core/mongodb.md index fd0460e33d6..e74a6ed71fd 100644 --- a/core/mongodb.md +++ b/core/mongodb.md @@ -8,7 +8,7 @@ can also shard the database easily for horizontal scalability and has a powerful 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/latest/reference/aggregation-builder.html) +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 diff --git a/core/performance.md b/core/performance.md index bb3711b7358..496f3481ef7 100644 --- a/core/performance.md +++ b/core/performance.md @@ -265,7 +265,7 @@ database driver. ### Eager Loading -By default, Doctrine comes with [lazy loading](https://www.doctrine-project.org/projects/doctrine-orm/en/latest/reference/working-with-objects.html#by-lazy-loading) - usually a killer time-saving feature but also a performance killer with large applications. +By default, Doctrine comes with [lazy loading](https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/working-with-objects.html#by-lazy-loading) - usually a killer time-saving feature but also a performance killer with large applications. Fortunately, Doctrine offers another approach to solve this problem: [eager loading](https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/working-with-objects.html#by-eager-loading). This can easily be enabled for a relation: `#[ORM\ManyToOne(fetch: "EAGER")]`. diff --git a/core/serialization.md b/core/serialization.md index 70594776175..6c52918388a 100644 --- a/core/serialization.md +++ b/core/serialization.md @@ -1076,7 +1076,7 @@ final class ApiNormalizer implements NormalizerInterface, DenormalizerInterface, ## Entity Identifier Case -API Platform is able to guess the entity 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/latest/reference/basic-mapping.html#identifiers)). +API Platform is able to guess the entity 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)). For ORM, it also supports [composite identifiers](https://www.doctrine-project.org/projects/doctrine-orm/en/current/tutorials/composite-primary-keys.html). If you are not using the Doctrine ORM or MongoDB ODM Provider, you must explicitly mark the identifier using the `identifier` attribute of @@ -1142,7 +1142,7 @@ must do the following: 1. create a setter for the identifier of the entity (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/latest/reference/basic-mapping.html#identifier-generation-strategies) +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 diff --git a/symfony/index.md b/symfony/index.md index 7ba2dcd3ee2..6ad993983bd 100644 --- a/symfony/index.md +++ b/symfony/index.md @@ -455,12 +455,12 @@ docker compose exec php \ bin/console make:entity --api-resource ``` -Doctrine's [attributes](https://www.doctrine-project.org/projects/doctrine-orm/en/latest/reference/attributes-reference.html) map these entities to tables in the database. -Mapping through [attributes](https://www.doctrine-project.org/projects/doctrine-orm/en/latest/reference/attributes-reference.html) is also supported, if you prefer those. +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 [attributes](https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/attributes-reference.html) is also supported, if you prefer those. 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/latest/reference/association-mapping.html) +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. From 129ea776df4af0b967ffc18e4e1aa2921cae90ec Mon Sep 17 00:00:00 2001 From: Vincent Amstoutz Date: Thu, 14 Nov 2024 20:05:14 +0100 Subject: [PATCH 35/80] refactor(doctrine): reference order filters constants from interface (#2062) --- core/filters.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/core/filters.md b/core/filters.md index 2ccea89d8e3..df736677736 100644 --- a/core/filters.md +++ b/core/filters.md @@ -1050,13 +1050,13 @@ App\Entity\Offer: When the property used for ordering can contain `null` values, you may want to specify how `null` values are treated in the comparison: -| Description | Strategy to set | -| ------------------------------------ | ---------------------------------------------------------------------------------------- | -| Use the default behavior of the DBMS | `null` | -| Consider items as smallest | `ApiPlatform\Doctrine\Orm\Filter\OrderFilter::NULLS_SMALLEST` (`nulls_smallest`) | -| Consider items as largest | `ApiPlatform\Doctrine\Orm\Filter\OrderFilter::NULLS_LARGEST` (`nulls_largest`) | -| Order items always first | `ApiPlatform\Doctrine\Orm\Filter\OrderFilter::NULLS_ALWAYS_FIRST` (`nulls_always_first`) | -| Order items always last | `ApiPlatform\Doctrine\Orm\Filter\OrderFilter::NULLS_ALWAYS_LAST` (`nulls_always_last`) | +| Description | Strategy to set | +| ------------------------------------ |---------------------------------------------------------------------------------------------------| +| Use the default behavior of the DBMS | `null` | +| Consider items as smallest | `ApiPlatform\Doctrine\Common\Filter\OrderFilterInterface::NULLS_SMALLEST` (`nulls_smallest`) | +| Consider items as largest | `ApiPlatform\Doctrine\Common\Filter\OrderFilterInterface::NULLS_LARGEST` (`nulls_largest`) | +| Order items always first | `ApiPlatform\Doctrine\Common\Filter\OrderFilterInterface::NULLS_ALWAYS_FIRST` (`nulls_always_first`) | +| Order items always last | `ApiPlatform\Doctrine\Common\Filter\OrderFilterInterface::NULLS_ALWAYS_LAST` (`nulls_always_last`) | For instance, treat entries with a property value of `null` as the smallest, with the following service definition: @@ -1067,12 +1067,13 @@ For instance, treat entries with a property value of `null` as the smallest, wit // api/src/Entity/Offer.php namespace App\Entity; +use ApiPlatform\Doctrine\Common\Filter\OrderFilterInterface; use ApiPlatform\Metadata\ApiFilter; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Doctrine\Orm\Filter\OrderFilter; #[ApiResource] -#[ApiFilter(OrderFilter::class, properties: ['validFrom' => ['nulls_comparison' => OrderFilter::NULLS_SMALLEST, 'default_direction' => 'DESC']])] +#[ApiFilter(OrderFilter::class, properties: ['validFrom' => ['nulls_comparison' => OrderFilterInterface::NULLS_SMALLEST, 'default_direction' => 'DESC']])] class Offer { // ... From 557ad639301328d787474e3d1547d119d63d945a Mon Sep 17 00:00:00 2001 From: Vincent Amstoutz Date: Wed, 20 Nov 2024 11:11:52 +0100 Subject: [PATCH 36/80] fix(ci): incorrect term "must-have" for NATURAL_LANGUAGE (#2067) --- extra/philosophy.md | 2 +- laravel/index.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/extra/philosophy.md b/extra/philosophy.md index df22dcc7ea6..07c71404681 100644 --- a/extra/philosophy.md +++ b/extra/philosophy.md @@ -4,7 +4,7 @@ In 25 years of PHP, the web changed dramatically and is now evolving faster than - 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**. +- [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. diff --git a/laravel/index.md b/laravel/index.md index 219d569f20e..bab1332e33c 100644 --- a/laravel/index.md +++ b/laravel/index.md @@ -445,7 +445,7 @@ There's a powerful mechanism inside API Platform to create routes using relation ## Paginating Data -A must-have feature for APIs is pagination. Without pagination, collection responses quickly become huge and slow, +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! From 0408e85cf31776eee2049c9ea2509b5f9ea9803b Mon Sep 17 00:00:00 2001 From: lobodol Date: Wed, 20 Nov 2024 15:15:17 +0100 Subject: [PATCH 37/80] docs: fix custom filter usage on several properties (#2066) --- core/filters.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/filters.md b/core/filters.md index df736677736..cab3eaecf92 100644 --- a/core/filters.md +++ b/core/filters.md @@ -1651,7 +1651,7 @@ use ApiPlatform\Core\Annotation\ApiResource; use App\Filter\CustomAndFilter; #[ApiResource] -#[ApiFilter(CustomAndFilter::class, properties={"name", "cost"})] +#[ApiFilter(CustomAndFilter::class, properties=["name", "cost"])] class Offer { // ... From db0be40311669f72c071a6080ea6000e8832720b Mon Sep 17 00:00:00 2001 From: Vincent Amstoutz Date: Thu, 21 Nov 2024 09:36:02 +0100 Subject: [PATCH 38/80] fix: license separators --- LICENSE | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/LICENSE b/LICENSE index c1cd95bc45a..a73c4686a34 100644 --- a/LICENSE +++ b/LICENSE @@ -2,7 +2,7 @@ Licence Creative Commons Attribution 4.0 International (CC BY 4.0) Copyright (c) 2015-present [Kévin Dunglas] -======================================================================= +--- Creative Commons Corporation ("Creative Commons") is not a law firm and does not provide legal services or legal advice. Distribution of @@ -54,7 +54,7 @@ exhaustive, and do not form part of our licenses. for the public: wiki.creativecommons.org/Considerations_for_licensees -======================================================================= +--- Creative Commons Attribution 4.0 International Public License @@ -375,7 +375,7 @@ that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority. -======================================================================= +--- Creative Commons is not a party to its public licenses. Notwithstanding, Creative Commons may elect to apply one of From c26fb86cd5ed0e623ccaee7883bcc4c0394fc018 Mon Sep 17 00:00:00 2001 From: Vincent Amstoutz Date: Thu, 21 Nov 2024 10:25:18 +0100 Subject: [PATCH 39/80] docs: include Laravel for config reference (#2044) --- core/configuration.md | 440 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 434 insertions(+), 6 deletions(-) diff --git a/core/configuration.md b/core/configuration.md index cb8f843e3c9..e448a0463e8 100644 --- a/core/configuration.md +++ b/core/configuration.md @@ -1,6 +1,8 @@ # Configuration -Here's the complete configuration of the Symfony bundle including default values: +## Symfony Configuration + +Here's the complete configuration of [API Platform for Symfony](../symfony/index.md) including default values: ```yaml # api/config/packages/api_platform.yaml @@ -133,22 +135,22 @@ api_platform: scopes: [] graphql: - # Enabled by default with installed webonyx/graphql-php. + # Enabled by default with installed api-platform/graphql. enabled: false # The default IDE (graphiql or graphql-playground) used when going to the GraphQL endpoint. False to disable. default_ide: 'graphiql' graphiql: - # Enabled by default with installed webonyx/graphql-php and Twig. + # Enabled by default with installed api-platform/graphql and Twig. enabled: false graphql_playground: - # Enabled by default with installed webonyx/graphql-php and Twig. + # Enabled by default with installed api-platform/graphql and Twig. enabled: false introspection: - # Enabled by default with installed webonyx/graphql-php. + # Enabled by default with installed api-platform/graphql. enabled: true # The nesting separator used in the filter names. @@ -278,7 +280,7 @@ api_platform: # ... ``` -## Global Resources Defaults +### Global Resources Defaults for Symfony If you need to globally configure all the resources instead of adding configuration in each one, it's possible to do so with the `defaults` key: @@ -376,3 +378,429 @@ api_platform: # ... ``` + +## Laravel Configuration + +Here's the complete configuration of [API Platform for Laravel](../laravel/index.md) including default values: + +```php + 'API title', + + // The description of the API. + 'description' => 'API description', + + // The version of the API. + 'version' => '0.0.0', + + // Set this to false if you want Webby to disappear. + 'show_webby' => true, + + // Specify a name converter to use. + 'name_converter' => null, + + // Specify an asset package name to use. + 'asset_package' => null, + + // Specify a path name generator to use. + 'path_segment_name_generator' => 'api_platform.path_segment_name_generator.underscore', + + 'validator' => [ + // Enable the serialization of payload fields when a validation error is thrown. + // If you want to serialize only some payload fields, define them like this: [ severity, anotherPayloadField ] + 'serialize_payload_fields' => [], + + // To enable or disable query parameters validation on collection GET requests + 'query_parameter_validation' => true, + ], + + 'eager_loading' => [ + // To enable or disable eager loading. + 'enabled' => true, + + // Fetch only partial data according to serialization groups. + // If enabled, Doctrine ORM entities will not work as expected if any of the other fields are used. + 'fetch_partial' => false, + + // Max number of joined relations before EagerLoading throws a RuntimeException. + 'max_joins' => 30, + + // Force join on every relation. + // If disabled, it will only join relations having the EAGER fetch mode. + 'force_eager' => true, + ], + + // Enable the Swagger documentation and export. + 'enable_swagger' => true, + + // Enable Swagger UI. + 'enable_swagger_ui' => true, + + // Enable ReDoc. + 'enable_re_doc' => true, + + // Enable the entrypoint. + 'enable_entrypoint' => true, + + // Enable the docs. + 'enable_docs' => true, + + // Enable the data collector and the WebProfilerBundle integration. + 'enable_profiler' => true, + + 'collection' => [ + // The name of the query parameter to filter nullable results (with the ExistsFilter). + 'exists_parameter_name' => 'exists', + + // The default order of results. + 'order' => 'ASC', + + // The name of the query parameter to order results (with the OrderFilter). + 'order_parameter_name' => 'order', + + 'pagination' => [ + // The default name of the parameter handling the page number. + 'page_parameter_name' => 'page', + + // The name of the query parameter to enable or disable pagination. + 'enabled_parameter_name' => 'pagination', + + // The name of the query parameter to set the number of items per page. + 'items_per_page_parameter_name' => 'itemsPerPage', + + // The name of the query parameter to enable or disable the partial pagination. + 'partial_parameter_name' => 'partial', + ], + ], + + 'mapping' => [ + // The list of paths with files or directories where the bundle will look for additional resource files. + 'paths' => [], + ], + + // The list of your resources class directories. Defaults to the directories of the mapping paths but might differ. + 'resource_class_directories' => [ + '%kernel.project_dir%/src/Entity', + ], + + 'doctrine' => [ + // To enable or disable Doctrine ORM support. + 'enabled' => true, + ], + + 'doctrine_mongodb_odm' => [ + // To enable or disable Doctrine MongoDB ODM support. + 'enabled' => false, + ], + + 'oauth' => [ + // To enable or disable OAuth. + 'enabled' => false, + + // The OAuth client ID. + 'clientId' => '', + + // The OAuth client secret. + 'clientSecret' => '', + + // The OAuth type. + 'type' => 'oauth2', + + // The OAuth flow grant type. + 'flow' => 'application', + + // The OAuth token URL. Make sure to check the specification tokenUrl is not needed for an implicit flow. + 'tokenUrl' => '', + + // The OAuth authentication URL. + 'authorizationUrl' => '', + + // The OAuth scopes. + 'scopes' => [], + ], + + 'graphql' => [ + // Enabled by default with installed api-platform/graphql. + 'enabled' => false, + + // The default IDE (graphiql or graphql-playground) used when going to the GraphQL endpoint. False to disable. + 'default_ide' => 'graphiql', + + 'graphiql' => [ + // Enabled by default with installed api-platform/graphql. + 'enabled' => false, + ], + + 'introspection' => [ + // Enabled by default with installed api-platform/graphql. + 'enabled' => true, + ], + + // The nesting separator used in the filter names. + 'nesting_separator' => '_', + + 'collection' => [ + 'pagination' => [ + 'enabled' => true, + ], + ], + ], + + 'swagger' => [ + // The active versions of OpenAPI to be exported or used in the swagger_ui. The first value is the default. + 'versions' => [2, 3], + + // The swagger API keys. + 'api_keys' => [], + + 'swagger_ui_extra_configuration' => [ + // Controls the default expansion setting for the operations and tags. It can be 'list' (expands only the tags), 'full' (expands the tags and operations) or 'none' (expands nothing). + 'docExpansion' => 'list', + + // If set, enables filtering. The top bar will show an edit box that you can use to filter the tagged operations that are shown. + 'filter' => false, + + // You can use any other configuration parameters too. + ], + ], + + 'openapi' => [ + // The contact information for the exposed API. + 'contact' => [ + // The identifying name of the contact person/organization. + 'name' => null, + + // The URL pointing to the contact information. MUST be in the format of a URL. + 'url' => null, + + // The email address of the contact person/organization. MUST be in the format of an email address. + 'email' => null, + ], + + // A URL to the Terms of Service for the API. MUST be in the format of a URL. + 'termsOfService' => null, + + // The license information for the exposed API. + 'license' => [ + // The license name used for the API. + 'name' => null, + + // URL to the license used for the API. MUST be in the format of a URL. + 'url' => null, + ], + ], + + 'http_cache' => [ + // To make all responses public by default. + 'public' => null, + + 'invalidation' => [ + // To enable the tags-based cache invalidation system. + 'enabled' => false, + + // URLs of the Varnish servers to purge using cache tags when a resource is updated. + 'varnish_urls' => [], + + // To pass options to the client charged with the request. + 'request_options' => [], + + // Use another service as the purger for example "api_platform.http_cache.purger.varnish.xkey" + 'purger' => 'api_platform.http_cache.purger.varnish.ban', + ], + ], + + 'mercure' => [ + // Enabled by default with installed symfony/mercure-bundle. + 'enabled' => false, + + // The URL sent in the Link HTTP header. If not set, will default to MercureBundle's default hub URL. + 'hub_url' => null, + ], + + 'messenger' => [ + // Enabled by default with installed symfony/messenger and not installed symfony/symfony. + 'enabled' => false, + ], + + 'elasticsearch' => [ + // To enable or disable Elasticsearch support. + 'enabled' => false, + + // The hosts to the Elasticsearch nodes. + 'hosts' => [], + + // The mapping between resource classes and indexes. + 'mapping' => [], + ], + + // The list of exceptions mapped to their HTTP status code. + 'exception_to_status' => [ + AuthenticationException::class => 401, + AuthorizationException::class => 403 + ], + + // The list of routes. + 'routes' => [ + // Global middleware applied to every API Platform routes + // 'middleware' => [] + ], + + // The list of resources. + 'resources' => [ + app_path('Models'), + ], + + // The list of enabled patch formats. The first one will be the default. + 'formats' => [ + 'jsonld' => ['mime_types' => ['application/ld+json']], + 'json' => ['mime_types' => ['application/json']], + 'html' => ['mime_types' => ['text/html']], + ], + + // The list of enabled patch formats. The first one will be the default. + 'patch_formats' => [ + 'json' => ['application/merge-patch+json'], + ], + + // The list of enabled docs formats. The first one will be the default. + 'docs_formats' => [ + 'jsonld' => ['application/ld+json'], + //'jsonapi' => ['application/vnd.api+json'], + 'jsonopenapi' => ['application/vnd.openapi+json'], + 'html' => ['text/html'], + ], + + // The list of enabled error formats. The first one will be the default. + 'error_formats' => [ + 'jsonproblem' => ['mime_types' => ['application/problem+json']], + ], + + // Global resources defaults, see in the next section. + 'defaults' => [ + 'pagination_enabled' => true, + 'pagination_partial' => false, + 'pagination_client_enabled' => false, + 'pagination_client_items_per_page' => false, + 'pagination_client_partial' => false, + 'pagination_items_per_page' => 30, + 'pagination_maximum_items_per_page' => 30, + 'route_prefix' => '/api', + 'middleware' => [], + ], +]; +``` + +### Global Resources Defaults for Laravel + +If you need to globally configure all the resources instead of adding configuration in each one, it's possible to do so with the `defaults` key: + +```php + [ + 'description' => null, + 'iri' => null, + 'short_name' => null, + 'item_operations' => null, + 'collection_operations' => null, + + 'graphql' => null, + + 'elasticsearch' => null, + + 'security' => null, + 'security_message' => null, + 'security_post_denormalize' => null, + 'security_post_denormalize_message' => null, + + 'cache_headers' => [ + // Automatically generate etags for API responses. + 'etag' => true, + + // Default value for the response max age. + 'max_age' => 3600, + + // Default value for the response shared (proxy) max age. + 'shared_max_age' => 3600, + + // Default values of the "Vary" HTTP header. + 'vary' => ['Accept'], + + 'invalidation' => [ + 'xkey' => [ + 'glue' => ' ', + ], + ], + ], + + 'normalization_context' => [ + // Default value to omit null values in conformance with the JSON Merge Patch RFC. + 'skip_null_values' => true, + ], + + 'denormalization_context' => null, + 'swagger_context' => null, + 'openapi_context' => null, + 'deprecation_reason' => null, + 'fetch_partial' => null, + 'force_eager' => null, + 'formats' => null, + 'filters' => null, + 'hydra_context' => null, + 'mercure' => null, + 'messenger' => null, + 'order' => null, + + // To enable or disable pagination for all resource collections. + 'pagination_enabled' => true, + + // To allow the client to enable or disable the pagination. + 'pagination_client_enabled' => false, + + // To allow the client to set the number of items per page. + 'pagination_client_items_per_page' => false, + + // To allow the client to enable or disable the partial pagination. + 'pagination_client_partial' => false, + + // The default number of items per page. + 'pagination_items_per_page' => 30, + + // The maximum number of items per page. + 'pagination_maximum_items_per_page' => null, + + // To allow partial pagination for all resource collections. + // This improves performances by skipping the `COUNT` query. + 'pagination_partial' => false, + + // To use cursor-based pagination. + 'pagination_via_cursor' => null, + + 'pagination_fetch_join_collection' => null, + + 'route_prefix' => null, + 'validation_groups' => null, + 'sunset' => null, + 'input' => null, + 'output' => null, + 'stateless' => null, + + // The URL generation strategy to use for IRIs + 'url_generation_strategy' => UrlGeneratorInterface::ABS_PATH, + + // To enable collecting denormalization errors + 'collectDenormalizationErrors' => false, + ], +]; +``` From de4ec8480b27997dd54c8ab715dc29d707d838ff Mon Sep 17 00:00:00 2001 From: Vincent Amstoutz Date: Thu, 21 Nov 2024 17:04:47 +0100 Subject: [PATCH 40/80] refactor: clean up docker cmds and improve code blocks (#2041) --- CONTRIBUTING.md | 4 +- admin/getting-started.md | 3 +- core/file-upload.md | 3 +- core/graphql.md | 2 +- core/json-schema.md | 6 +- core/jwt.md | 32 +++++++--- core/messenger.md | 3 +- core/mongodb.md | 7 +-- core/openapi.md | 15 ++--- core/testing.md | 3 +- create-client/nextjs.md | 95 +++++++++++++++++------------ deployment/docker-compose.md | 8 +-- deployment/heroku.md | 10 +-- deployment/kubernetes.md | 2 +- deployment/minikube.md | 4 +- deployment/traefik.md | 2 +- schema-generator/getting-started.md | 13 ++-- symfony/index.md | 25 ++++---- symfony/testing.md | 30 ++++----- 19 files changed, 137 insertions(+), 130 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 05e7b3fa72a..ad1e6404e08 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,11 +24,11 @@ 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: - ```bash + ```console git clone https://github.com/your-username/repository-name.git ``` 3. Create a new branch for your contribution: - ```bash + ```console git switch -c docs-your-branch-name ``` 4. Commit and push your changes diff --git a/admin/getting-started.md b/admin/getting-started.md index 2eda179a5b1..9378cb5f98f 100644 --- a/admin/getting-started.md +++ b/admin/getting-started.md @@ -58,8 +58,7 @@ nelmio_cors: Clear the cache to apply this change: ```console -docker compose exec php \ - bin/console cache:clear --env=prod +bin/console cache:clear --env=prod ``` Your new administration interface is ready! Type `npm start` to try it! diff --git a/core/file-upload.md b/core/file-upload.md index 703ba86fef0..dbb87a984f4 100644 --- a/core/file-upload.md +++ b/core/file-upload.md @@ -25,8 +25,7 @@ api_platform: Install the bundle with the help of Composer: ```console -docker compose exec php \ - composer require vich/uploader-bundle +composer require vich/uploader-bundle ``` This will create a new configuration file that you will need to slightly change diff --git a/core/graphql.md b/core/graphql.md index 7bcfb5ab6b2..a5370041f8d 100644 --- a/core/graphql.md +++ b/core/graphql.md @@ -13,7 +13,7 @@ Once enabled, you have nothing to do: your schema describing your API is automat To enable GraphQL and its IDE (GraphiQL and GraphQL Playground) in your API, simply require the `api-platform/graphql` package using Composer: ```console - composer require api-platform/graphql +composer require api-platform/graphql ``` You can now use GraphQL at the endpoint: `https://localhost:8443/graphql`. diff --git a/core/json-schema.md b/core/json-schema.md index 9a789ae893b..aa180980cfc 100644 --- a/core/json-schema.md +++ b/core/json-schema.md @@ -10,15 +10,13 @@ The generated schema can be used with libraries such as [react-json-schema-form] To export the schema corresponding to an API Resource, run the following command: ```console -docker compose exec php \ - bin/console api:json-schema:generate 'App\Entity\Book' +bin/console api:json-schema:generate 'App\Entity\Book' ``` To see all options available, try: ```console -docker compose exec php \ - bin/console help api:json-schema:generate +bin/console help api:json-schema:generate ``` ## Overriding the JSON Schema Specification diff --git a/core/jwt.md b/core/jwt.md index 30d933fc514..2ecf6a633cc 100644 --- a/core/jwt.md +++ b/core/jwt.md @@ -1,7 +1,10 @@ # JWT Authentication -> [JSON Web Token (JWT)](https://jwt.io/) is a JSON-based open standard ([RFC 7519](https://tools.ietf.org/html/rfc7519)) for creating access tokens that assert some number of claims. For example, a server could generate a token that has the claim "logged in as admin" and provide that to a client. The client could then use that token to prove that he/she is logged in as admin. -> The tokens are signed by the server's key, so the server is able to verify that the token is legitimate. The tokens are designed to be compact, URL-safe and usable especially in web browser single sign-on (SSO) context. +> [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) @@ -14,11 +17,17 @@ API Platform allows to easily add a JWT-based authentication to your API using [ We begin by installing the bundle: ```console -docker compose exec php \ - composer require lexik/jwt-authentication-bundle +composer require lexik/jwt-authentication-bundle ``` +Then we need to generate the public and private keys used for signing JWT tokens. -Then we need to generate the public and private keys used for signing JWT tokens. If you're using the [API Platform distribution](../symfony/index.md), you may run this from the project's root directory: +You can generate them by using this command: + +```console +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: ```console docker compose exec php sh -c ' @@ -30,13 +39,18 @@ 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. -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. +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). -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/master/api/docker/php/docker-entrypoint.sh). +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 diff --git a/core/messenger.md b/core/messenger.md index 35010337716..2bf42da15de 100644 --- a/core/messenger.md +++ b/core/messenger.md @@ -12,8 +12,7 @@ Many transports are supported to dispatch messages to async consumers, including To enable the support of Messenger, install the library: ```console -docker compose exec php \ - composer require messenger +composer require symfony/messenger ``` ## Dispatching a Resource through the Message Bus diff --git a/core/mongodb.md b/core/mongodb.md index e74a6ed71fd..a59a8421d27 100644 --- a/core/mongodb.md +++ b/core/mongodb.md @@ -18,7 +18,7 @@ the legacy [mongo](https://secure.php.net/manual/en/book.mongo.php) extension. If the `mongodb` PHP extension is not installed yet, [install it beforehand](https://secure.php.net/manual/en/mongodb.installation.pecl.php). -If you are using the [API Platform Distribution](../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 @@ -64,12 +64,11 @@ services: # ... ``` -Once the extension is installed, to enable the MongoDB support, require the [Doctrine MongoDB ODM bundle](https://github.com/doctrine/DoctrineMongoDBBundle) +In all cases, enable the MongoDB support by requiring the [Doctrine MongoDB ODM bundle](https://github.com/doctrine/DoctrineMongoDBBundle) package using Composer: ```console -docker compose exec php \ - composer require doctrine/mongodb-odm-bundle +composer require doctrine/mongodb-odm-bundle ``` Execute the contrib recipe to have it already configured. diff --git a/core/openapi.md b/core/openapi.md index 7b310b9ea1f..287c4576c06 100644 --- a/core/openapi.md +++ b/core/openapi.md @@ -20,36 +20,31 @@ You can also dump an OpenAPI specification for your API. OpenAPI, JSON format: ```console -docker compose exec php \ - bin/console api:openapi:export +bin/console api:openapi:export ``` OpenAPI, YAML format: ```console -docker compose exec php \ - bin/console api:openapi:export --yaml +bin/console api:openapi:export --yaml ``` Create a file containing the specification: ```console -docker compose exec php \ - bin/console api:openapi:export --output=swagger_docs.json +bin/console api:openapi:export --output=swagger_docs.json ``` If you want to use the old OpenAPI v2 (Swagger) JSON format, use: ```console -docker compose exec php \ - bin/console api:swagger:export +bin/console api:swagger:export ``` It is also possible to use OpenAPI v3.0.0 format: ```console -docker compose exec php \ - bin/console api:openapi:export --spec-version=3.0.0 +bin/console api:openapi:export --spec-version=3.0.0 ``` ## Overriding the OpenAPI Specification diff --git a/core/testing.md b/core/testing.md index cae08869149..c99e91fea0f 100644 --- a/core/testing.md +++ b/core/testing.md @@ -17,8 +17,7 @@ Reuse them to run, for instance, SQL queries or requests to external APIs direct Install the `symfony/http-client` and `symfony/browser-kit` packages to enabled the API Platform test client: ```console -docker compose exec php \ - composer require symfony/browser-kit symfony/http-client +composer require symfony/browser-kit symfony/http-client ``` To use the testing client, your test class must extend the `ApiTestCase` class: diff --git a/create-client/nextjs.md b/create-client/nextjs.md index 3d8e2daacd3..b82942ede2e 100644 --- a/create-client/nextjs.md +++ b/create-client/nextjs.md @@ -15,25 +15,34 @@ If you use API Platform, jump to the next section! Alternatively, create a Next.js application by executing: -```console -# using pnpm (recommended) -pnpm create next-app --typescript -# or using npm -npm init next-app --typescript -# or using yarn -yarn create next-app --typescript -``` +- 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: -```console -# using pnpm -pnpm install isomorphic-unfetch formik react-query -# or using npm -npm install isomorphic-unfetch formik react-query -# or using yarn -yarn add isomorphic-unfetch formik react-query -``` +- Pnpm (recommended) + ```console + pnpm install isomorphic-unfetch formik react-query + ``` +- Npm + ```console + npm install isomorphic-unfetch formik react-query + ``` +- Yarn + ```console + yarn add isomorphic-unfetch formik react-query + ``` The generated HTML will contain [Tailwind CSS](https://tailwindcss.com) classes. Optionally, [follow the Tailwind installation guide for Next.js projects](https://tailwindcss.com/docs/guides/nextjs) @@ -41,7 +50,7 @@ Optionally, [follow the Tailwind installation guide for Next.js projects](https: ## Generating Routes -If you use the API Platform symfony variant, 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 \ @@ -50,16 +59,20 @@ docker compose exec pwa \ Omit the resource flag to generate files for all resource types exposed by the API. -If you don't use the standalone installation, run the following command instead: - -```console -# using pnpm -pnpm create @api-platform/client https://demo.api-platform.com . --generator next --resource book -# or using npm -npm init @api-platform/client https://demo.api-platform.com . -- --generator next --resource book -# or using yarn -yarn create @api-platform/client https://demo.api-platform.com . --generator next --resource book -``` +Or if you don't use the standalone installation, run the following command instead: + +- Pnpm (recommended) + ```console + pnpm create @api-platform/client https://demo.api-platform.com . --generator next --resource book + ``` +- Npm + ```console + npm init @api-platform/client https://demo.api-platform.com . -- --generator next --resource book + ``` +- Yarn + ```console + yarn create @api-platform/client https://demo.api-platform.com . --generator next --resource book + ``` Replace the URL by the entrypoint of your Hydra-enabled API. You can also use an OpenAPI documentation with `-f openapi3`. @@ -85,22 +98,26 @@ export default App; ## Starting the Project -You can launch the server with - -```console -# using pnpm -pnpm dev -# or using npm -npm run dev -# or using yarn -yarn dev -``` - +You can launch the server with: + +- Pnpm (recommended) + ```console + pnpm dev + ``` +- Npm + ```console + npm run dev + ``` +- Yarn + ```console + yarn dev + ``` + Go to `http://localhost:3000/books/` to start using your app. ## Generating a production build locally with docker compose -If you want to generate a production build locally with docker compose, follow [these instructions](../deployment/docker-compose.md) +If you want to generate a production build locally with docker compose, follow [these instructions](../deployment/docker-compose.md). ## Screenshots diff --git a/deployment/docker-compose.md b/deployment/docker-compose.md index eea18991b77..2e31b9fde67 100644 --- a/deployment/docker-compose.md +++ b/deployment/docker-compose.md @@ -89,7 +89,7 @@ Go to `https://your-domain-name.example.com` and enjoy! Alternatively, if you don't want to expose an HTTPS server but only an HTTP one, run the following command: -```bash +```console SERVER_NAME=http://localhost \ MERCURE_PUBLIC_URL=http://localhost/.well-known/mercure \ TRUSTED_HOSTS='^localhost|php$' \ @@ -142,7 +142,7 @@ pwa: Begin by starting the php service container: -```bash +```console SERVER_NAME=http://localhost \ MERCURE_PUBLIC_URL=http://localhost/.well-known/mercure \ TRUSTED_HOSTS='^localhost|php$' \ @@ -162,13 +162,13 @@ NEXT_PUBLIC_ENTRYPOINT=http://php #### 4. Build the pwa service -```bash +```console docker compose -f compose.yaml -f compose.prod.yaml build pwa ``` #### 5. Finally, bring up the full project -```bash +```console SERVER_NAME=http://localhost \ MERCURE_PUBLIC_URL=http://localhost/.well-known/mercure \ TRUSTED_HOSTS='^localhost|php$' \ diff --git a/deployment/heroku.md b/deployment/heroku.md index a39f79fdb13..4536de6e583 100644 --- a/deployment/heroku.md +++ b/deployment/heroku.md @@ -97,31 +97,31 @@ Go to the `api/` directory, then 1. Initialize a Git repository: -```bash +```console git init ``` 2. Add all existing files: -```bash +```console git add --all ``` 3. Commit: -```bash +```console git commit -a -m "My first API Platform app running on Heroku!" ``` 4. Create the Heroku application: -```bash +```console heroku create ``` 5. And deploy for the first time: -```bash +```console git push heroku master ``` diff --git a/deployment/kubernetes.md b/deployment/kubernetes.md index 5601e84534a..10545ce9726 100644 --- a/deployment/kubernetes.md +++ b/deployment/kubernetes.md @@ -45,7 +45,7 @@ docker build -t gcr.io/test-api-platform/pwa:0.1.0 -t gcr.io/test-api-platform/p 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. -```bash +```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 ``` diff --git a/deployment/minikube.md b/deployment/minikube.md index 34d15483357..83eb3cac86b 100644 --- a/deployment/minikube.md +++ b/deployment/minikube.md @@ -71,7 +71,7 @@ First, install the [skaffold CLI](https://skaffold.dev/docs/install/#standalone- Then, run minikube: -```bash +```console minikube start ``` @@ -79,7 +79,7 @@ Add Skaffold configuration in the file `./helm/skaffold.yaml`. You can find a [c Finally, go to the helm folder, and run skaffold in dev mode: -```bash +```console cd ./helm skaffold dev ``` diff --git a/deployment/traefik.md b/deployment/traefik.md index 6053ba21415..dabdaa548ef 100644 --- a/deployment/traefik.md +++ b/deployment/traefik.md @@ -453,7 +453,7 @@ 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. -```bash +```console #!/bin/sh # /anywhere/api-platform/init-dc.sh diff --git a/schema-generator/getting-started.md b/schema-generator/getting-started.md index d3707e827b1..f8ff1453baf 100644 --- a/schema-generator/getting-started.md +++ b/schema-generator/getting-started.md @@ -2,15 +2,15 @@ ## Installation -If you use [the API Platform Symfony variant](../symfony/index.md), the Schema Generator is already installed as a development -dependency of your project and can be invoked through Docker: +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 -docker compose exec php \ - vendor/bin/schema +vendor/bin/schema ``` -The Schema Generator can also [be downloaded independently as a PHAR](https://github.com/api-platform/schema-generator/releases) or installed in an existing project using [Composer](https://getcomposer.org): +The Schema Generator can also [be downloaded independently as a PHAR](https://github.com/api-platform/schema-generator/releases) +or installed in an existing project using [Composer](https://getcomposer.org): ```console composer require --dev api-platform/schema-generator @@ -104,8 +104,7 @@ vendor/bin/schema generate api/src/ api/config/schema.yaml -vv Using [the API Platform Symfony variant](../symfony/index.md): ```console -docker compose exec php \ - vendor/bin/schema generate src/ config/schema.yaml -vv +vendor/bin/schema generate src/ config/schema.yaml -vv ``` The corresponding PHP classes will be automatically generated in the `src/` directory! diff --git a/symfony/index.md b/symfony/index.md index 6ad993983bd..b2e379bcdcb 100644 --- a/symfony/index.md +++ b/symfony/index.md @@ -147,6 +147,11 @@ That being said, keep in mind that API Platform is 100% independent of the persi best suit(s) your needs (including NoSQL databases or remote web services) by implementing the [right interfaces](../core/state-providers.md). API Platform even supports using several persistence systems together in the same project. +> [!TIP] +> The `php` container is where your API app stands. Prefixing a command by `docker compose exec php` allows executing the +> given command in this container. You may want [to create an alias](https://www.linfo.org/alias.html) to make your life easier. +> So, for example, you could run a command like this: `docker compose exec php `. + ### Using Symfony CLI Alternatively, the API Platform server component can also be installed directly on a local machine. @@ -451,8 +456,7 @@ Modify these files as described in these patches: **Tip**: you can also use Symfony [MakerBundle](https://symfonycasts.com/screencast/symfony-fundamentals/maker-command?cid=apip) thanks to the `--api-resource` option: ```console -docker compose exec php \ - bin/console make:entity --api-resource +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. @@ -467,15 +471,10 @@ Now, delete the file `api/src/Entity/Greeting.php`. This demo entity isn't usefu Finally, generate a new database migration using [Doctrine Migrations](https://symfony.com/doc/current/doctrine.html#migrations-creating-the-database-tables-schema) and apply it: ```console -docker compose exec php \ - bin/console doctrine:migrations:diff -docker compose exec php \ - bin/console doctrine:migrations:migrate +bin/console doctrine:migrations:diff +bin/console doctrine:migrations:migrate ``` -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. - **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: @@ -667,13 +666,11 @@ Isn't API Platform a REST **and** GraphQL framework? That's true! GraphQL suppor need to install the [graphql-php](https://webonyx.github.io/graphql-php/) library. Run the following command: ```console -docker compose exec php sh -c ' - composer require webonyx/graphql-php - bin/console cache:clear -' +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) +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) diff --git a/symfony/testing.md b/symfony/testing.md index 4bf8e7db4c9..7df71d83683 100644 --- a/symfony/testing.md +++ b/symfony/testing.md @@ -23,8 +23,7 @@ Before creating your functional tests, you will need a dataset to pre-populate y First, install [Foundry](https://github.com/zenstruck/foundry) and [Doctrine/DoctrineFixturesBundle](https://github.com/doctrine/DoctrineFixturesBundle): ```console -docker compose exec php \ - composer require --dev foundry orm-fixtures +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! @@ -32,10 +31,8 @@ Thanks to Symfony Flex, [DoctrineFixturesBundle](https://github.com/doctrine/Doc Then, create some factories for [the bookstore API you created in the tutorial](index.md): ```console -docker compose exec php \ - bin/console make:factory 'App\Entity\Book' -docker compose exec php \ - bin/console make:factory 'App\Entity\Review' +bin/console make:factory 'App\Entity\Book' +bin/console make:factory 'App\Entity\Review' ``` Improve the default values: @@ -79,10 +76,8 @@ use function Zenstruck\Foundry\lazy; Create some stories: ```console -docker compose exec php \ - bin/console make:story 'DefaultBooks' -docker compose exec php \ - bin/console make:story 'DefaultReviews' +bin/console make:story 'DefaultBooks' +bin/console make:story 'DefaultReviews' ``` ```php @@ -144,8 +139,7 @@ class AppFixtures extends Fixture You can now load your fixtures in the database with the following command: ```console -docker compose exec php \ - bin/console doctrine:fixtures:load +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). @@ -162,8 +156,7 @@ If you don't use the distribution, run `composer require --dev symfony/test-pack Install [DAMADoctrineTestBundle](https://github.com/dmaicher/doctrine-test-bundle) to reset the database automatically before each test: ```console -docker compose exec php \ - composer require --dev dama/doctrine-test-bundle +composer require --dev dama/doctrine-test-bundle ``` And activate it in the `phpunit.xml.dist` file: @@ -179,11 +172,11 @@ And activate it in the `phpunit.xml.dist` file: ``` -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 -docker compose exec php \ - composer require --dev justinrainbow/json-schema +composer require --dev justinrainbow/json-schema ``` Your API is now ready to be functionally tested. Create your test classes under the `tests/` directory. @@ -345,8 +338,7 @@ There is one caveat though: in some tests, it is necessary to perform multiple r All you have to do now is to run your tests: ```console -docker compose exec php \ - bin/phpunit +bin/phpunit ``` If everything is working properly, you should see `OK (5 tests, 17 assertions)`. From 327c6d5f7a6bf2c8357013b5c26192cea89d50d9 Mon Sep 17 00:00:00 2001 From: Vincent Amstoutz Date: Tue, 1 Oct 2024 10:19:56 +0200 Subject: [PATCH 41/80] refactor: split test docs and add laravel support --- core/getting-started.md | 2 +- core/json-schema.md | 6 +- core/testing.md | 215 +---------------- laravel/testing.md | 517 ++++++++++++++++++++++++++++++++++++++++ outline.yaml | 1 + symfony/testing.md | 230 +++++++++++++++++- 6 files changed, 750 insertions(+), 221 deletions(-) create mode 100644 laravel/testing.md diff --git a/core/getting-started.md b/core/getting-started.md index 8d60d799197..7b9071fb497 100644 --- a/core/getting-started.md +++ b/core/getting-started.md @@ -13,7 +13,7 @@ If you are starting a new project, the easiest way to get API Platform up is to It comes with the API Platform core library integrated with [the Symfony framework](https://symfony.com), [the schema generator](../schema-generator/), [Doctrine ORM](https://www.doctrine-project.org), -[NelmioCorsBundle](https://github.com/nelmio/NelmioCorsBundle) and [test assertions dedicated to APIs](testing.md). +[NelmioCorsBundle](https://github.com/nelmio/NelmioCorsBundle) and [test assertions dedicated to APIs](../symfony/testing-utilities.md). [MongoDB](mongodb.md) and [Elasticsearch](elasticsearch.md) can also be easily enabled. diff --git a/core/json-schema.md b/core/json-schema.md index aa180980cfc..afdb3022143 100644 --- a/core/json-schema.md +++ b/core/json-schema.md @@ -23,12 +23,12 @@ bin/console help api:json-schema:generate 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. -When [Testing the API](testing.md), JSON Schemas are useful to generate and automate unit testing. API Platform provides specific unit testing functionalities like [`assertMatchesResourceCollectionJsonSchema()`](testing.md#writing-functional-tests) or [`assertMatchesResourceItemJsonSchema()`](testing.md#writing-functional-tests) methods. +When [Testing the API](../symfony/testing-utilities.md), JSON Schemas are useful to generate and automate unit testing. API Platform provides specific unit testing functionalities like [`assertMatchesResourceCollectionJsonSchema()`](../symfony/testing-utilities.md#writing-functional-tests) or [`assertMatchesResourceItemJsonSchema()`](../symfony/testing-utilities.md#writing-functional-tests) methods. These methods generate a JSON Schema then do unit testing based on the generated schema automatically. Usually, the fact that API Platform uses a different schema version for unit testing is not a problem, but sometimes you may need to use the [`ApiProperty`](openapi.md#using-the-openapi-and-swagger-contexts) attribute to specify a [calculated field](serialization.md#calculated-field) type by overriding the OpenAPI Schema for the calculated field to be correctly documented. -When you will use [`assertMatchesResourceCollectionJsonSchema()`](testing.md#writing-functional-tests) or [`assertMatchesResourceItemJsonSchema()`](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 +When you will use [`assertMatchesResourceCollectionJsonSchema()`](../symfony/testing-utilities.md#writing-functional-tests) or [`assertMatchesResourceItemJsonSchema()`](../symfony/testing-utilities.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. @@ -82,4 +82,4 @@ To generate JSON Schemas programmatically, use the `api_platform.json_schema.sch ## Testing API Platform provides a PHPUnit assertion to test if a response is valid according to a given Schema: `assertMatchesJsonSchema()`. -Refer to [the testing documentation](testing.md) for more details. +Refer to [the testing documentation](../symfony/testing-utilities.md) for more details. diff --git a/core/testing.md b/core/testing.md index c99e91fea0f..480ae28fa24 100644 --- a/core/testing.md +++ b/core/testing.md @@ -1,210 +1,13 @@ -# Testing Utilities +# Testing the API -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 cookbook first](../symfony/testing.md). +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. -

Test and Assertions screencast
Watch the API Tests & Assertions screencast

+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). -## The Test HttpClient +## Testing Documentations -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 enabled the API Platform test client: - -```console -composer require symfony/browser-kit symfony/http-client -``` - -To use the testing client, your test class must extend the `ApiTestCase` class: - -```php -request('GET', '/books'); - // your assertions here... - } -} -``` - -Refer to [the Symfony HttpClient documentation](https://symfony.com/doc/current/components/http_client.html) to discover all the features of the client (custom headers, JSON encoding and decoding, HTTP Basic and Bearer authentication and cookies support, among other things). - -Note that you can create your own test case class extending the ApiTestCase. For example to set up a Json Web Token authentication: - -```php -getToken(); - - return static::createClient([], ['headers' => ['authorization' => 'Bearer '.$token]]); - } - - /** - * Use other credentials if needed. - */ - protected function getToken($body = []): string - { - if ($this->token) { - return $this->token; - } - - $response = static::createClient()->request('POST', '/login', ['json' => $body ?: [ - 'username' => 'admin@example.com', - 'password' => '$3cr3t', - ]]); - - $this->assertResponseIsSuccessful(); - $data = $response->toArray(); - $this->token = $data['token']; - - return $data['token']; - } -} -``` - -Use it by extending the `AbstractTest` class. For example this class tests the `/users` resource accessibility where only the admin can retrieve the collection: - -```php -createClientWithCredentials()->request('GET', '/users'); - $this->assertResponseIsSuccessful(); - } - - public function testLoginAsUser() - { - $token = $this->getToken([ - 'username' => 'user@example.com', - 'password' => '$3cr3t', - ]); - - $response = $this->createClientWithCredentials($token)->request('GET', '/users'); - $this->assertJsonContains(['description' => 'Access Denied.']); - $this->assertResponseStatusCodeSame(403); - } -} -``` - -## API Test Assertions - -In addition to [the built-in ones](https://phpunit.readthedocs.io/en/latest/assertions.html), API Platform provides convenient PHPUnit assertions dedicated to API testing: - -```php -request(...); - - // Asserts that the returned JSON is equal to the passed one - $this->assertJsonEquals(/* a JSON document as an array or as a string */); - - // Asserts that the returned JSON is a superset of the passed one - $this->assertJsonContains(/* a JSON document as an array or as a string */); - - // justinrainbow/json-schema must be installed to use the following assertions - - // Asserts that the returned JSON matches the passed JSON Schema - $this->assertMatchesJsonSchema(/* a JSON Schema as an array or as a string */); - - // Asserts that the returned JSON is validated by the JSON Schema generated for this resource by API Platform - - // For collections - $this->assertMatchesResourceCollectionJsonSchema(YourApiResource::class); - // And for items - $this->assertMatchesResourceItemJsonSchema(YourApiResource::class); - } -} -``` - -There is also a method to find the IRI matching a given resource and some criteria: - -```php -findIriBy(Book::class, ['isbn' => '9780451524935']); - static::createClient()->request('GET', $iri); - $this->assertResponseIsSuccessful(); - } -} -``` - -## HTTP Test Assertions - -All test assertions provided by Symfony (assertions for status codes, headers, cookies, XML documents...) can be used out of the box with the API Platform test client: - -```php -request('GET', '/books'); - - $this->assertResponseIsSuccessful(); - $this->assertResponseHeaderSame('content-type', 'application/ld+json; charset=utf-8'); - } -} -``` - -[Check out the dedicated Symfony documentation entry](https://symfony.com/doc/current/testing/functional_tests_assertions.html). +- 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/laravel/testing.md b/laravel/testing.md new file mode 100644 index 00000000000..b9039d53dea --- /dev/null +++ b/laravel/testing.md @@ -0,0 +1,517 @@ +# Testing the API with Laravel + +For an introduction to testing using API Platform, refer to the [Core Testing Documentation](../core/testing.md), or access the +[Symfony Testing Guide](../symfony/testing.md). + +Let's learn how to use tests with Laravel! + +In this article, you'll learn how to use: + +- **[Pest](https://pestphp.com/)**: A testing framework that enables you to write unit tests for your classes and create +API-oriented functional tests, thanks to its integrations with API Platform and [Laravel](https://laravel.com/docs/testing). +- **[PHPUnit](https://phpunit.de)**: A testing framework for writing unit tests for your classes and conducting API-oriented +functional tests, with support for API Platform and [Laravel](https://laravel.com/docs/testing). + +> [!TIP] +> Pest is built on top of PHPUnit and introduces additional features along with a syntax inspired by Ruby's RSpec and the +> Jest testing APIs. + +## Tests with Pest + +> [!TIP] +> Even if you are using Pest, you can also use PHPUnit's assertion API, which can be useful if you're already familiar +> with PHPUnit's assertion API or if you need to perform more complex assertions that aren't available in Pest's expectation API. +> For more information see the [Pest Assertion API](https://pestphp.com/docs/writing-tests#content-assertion-api) documentation. + +### Installing Pest + +By default, when using Laravel, Pest is pre-configured through the Composer plugin `pestphp/pest-plugin`. You can find this plugin listed in the `allow-plugins` section of your `composer.json` file. + +To check the Pest installation, run the following command: + +```console +php artisan test +``` + +If for some reason, Pest is not installed refer to the [Pest Installation Guide](https://pestphp.com/docs/installation). + +In that case, you can run Pest using: + +```console +./vendor/bin/pest +``` + +### Writing Functional Tests with Pest + +#### Generate the Factory + +Using Laravel, you can efficiently test databases by combining seeding with model factories. Model factories allow you +to generate large amounts of test data quickly, while seeding ensures your database is pre-populated with the necessary records. + +To create a factory for your model, you can use [Laravel Artisan](https://laravel.com/docs/artisan) command. +For example, to create a factory for a Book model, run: + +```console +php artisan make:factory BookFactory +``` + +For advanced customization and configuration, refer to the [Defining model Factories Laravel Guide](https://laravel.com/docs/eloquent-factories#defining-model-factories). + +Then, you can now use your factory in tests to quickly generate model instances. + +#### Writing Pest tests + +Here’s an example of tests, which use the Factory: + +```php +test(function () { + // Create 100 books using the factory + Book::factory()->count(100)->create(); + + // Send a GET request to the collection endpoint + $response = $this->getJson('/api/books'); + + // Assert that the response is successful (200 OK) + $response->assertStatus(200); + + // Check the Content-Type header + $response->assertHeader('Content-Type', 'application/json'); + + // Assert the returned JSON contains the expected structure using assertJsonContains from the trait + $this->assertJsonContains([ + '@context' => '/contexts/Book', + '@id' => '/books', + '@type' => 'Collection', + 'totalItems' => 100, + 'view' => [ + '@id' => '/books?page=1', + '@type' => 'PartialCollectionView', + 'first' => '/books?page=1', + 'last' => '/books?page=4', + 'next' => '/books?page=2', + ], + ], $response->json()); + + // Assert that 30 items are returned in the response + $this->assertCount(30, $response->json('data')); + }); + +it('creates a valid book') + ->test(function () { + // Send a POST request to create a book + $response = $this->postJson('/api/books', [ + 'isbn' => '0099740915', + 'title' => 'The Handmaid\'s Tale', + 'description' => 'Brilliantly conceived and executed, this powerful evocation of twenty-first century America...', + 'author' => 'Margaret Atwood', + 'publication_date' => '1985-07-31', + ]); + + // Assert that the book was created successfully (201) + $response->assertStatus(201); + + // Check the Content-Type header + $response->assertHeader('Content-Type', 'application/json'); + + // Assert the returned JSON contains the expected book information using assertJsonContains + $this->assertJsonContains([ + '@context' => '/contexts/Book', + '@type' => 'Book', + 'isbn' => '0099740915', + 'title' => 'The Handmaid\'s Tale', + 'author' => 'Margaret Atwood', + 'publication_date' => '1985-07-31', + 'reviews' => [], + ], $response->json()); + + // Assert that the URI of the created resource matches the expected format + $this->assertMatchesRegularExpression('~^/api/books/\d+$~', $response->json('@id')); + }); + +it('creates an invalid book and validates error handling') + ->test(function () { + // Send a POST request with invalid data + $response = $this->postJson('/api/books', [ + 'isbn' => 'invalid', + ]); + + // Assert that the response status is 422 Unprocessable Entity + $response->assertStatus(422); + + // Check the Content-Type header + $response->assertHeader('Content-Type', 'application/json'); + + // Assert the JSON response contains the validation errors using assertJsonContains + $this->assertJsonContains([ + '@context' => '/contexts/ConstraintViolationList', + '@type' => 'ConstraintViolationList', + 'title' => 'An error occurred', + 'description' => [ + 'isbn' => 'This value is neither a valid ISBN-10 nor a valid ISBN-13.', + 'title' => 'This value should not be blank.', + 'description' => 'This value should not be blank.', + 'author' => 'This value should not be blank.', + 'publication_date' => 'This value should not be null.', + ], + ], $response->json()); + }); + +it('updates a book') + ->test(function () { + // Create a book using the factory + $book = Book::factory()->create(['isbn' => '9781344037075']); + + // Get the IRI of the book using getIriFromResource from the trait + $iri = $this->getIriFromResource($book); + + // Send a PATCH request to update the book's title + $response = $this->patchJson($iri, [ + 'title' => 'Updated Title', + ]); + + // Assert that the response is successful (200 OK) + $response->assertStatus(200); + + // Assert the JSON response contains the updated book information using assertJsonContains + $this->assertJsonContains([ + '@id' => $iri, + 'isbn' => '9781344037075', + 'title' => 'Updated Title', + ], $response->json()); + }); + +it('deletes a book') + ->test(function () { + // Create a book using the factory + $book = Book::factory()->create(['isbn' => '9781344037075']); + + // Get the IRI of the book using getIriFromResource from the trait + $iri = $this->getIriFromResource($book); + + // Send a DELETE request to remove the book + $response = $this->deleteJson($iri); + + // Assert that the response status is 204 No Content + $response->assertStatus(204); + + // Assert that the book is no longer in the database + $this->assertDatabaseMissing('books', ['id' => $book->id]); + }); +``` + +In the example above, the [RefreshDatabase Trait](https://laravel.com/docs/database-testing#resetting-the-database-after-each-test) +is used to ensure that the database is automatically reset between test runs. This guarantees that each test starts with +a clean database state, avoiding conflicts from residual data and ensuring test isolation. + +This trait is especially useful when testing operations that modify the database, as it rolls back any changes made during the test. +As a result, your test environment remains reliable and consistent across multiple test executions. + +#### Run Pest tests + +If everything is working properly, you should see `Tests: 5 passed (15 assertions)`. +Your REST API is now properly tested! + +Check out the [API Test Assertions section](#api-test-assertions-with-laravel) to discover the full range of assertions +and other features provided by API Platform's test utilities. + +### Migrating from PHPUnit to Pest + +If you want to migrate from PHPUnit to Pest, refer to [Migrating from PHPUnit Guide](https://pestphp.com/docs/migrating-from-phpunit-guide) +and [Installation Guide](https://pestphp.com/docs/installation). + +## Tests with PHPUnit + +### Installing PHPUnit + +By default, with Laravel, PHPUnit is already a dependency in your project. You may see `phpunit/phpunit` in the `require-dev` +section of your `composer.json`. + +You can test the PHPUnit installation by running: +```console +./vendor/bin/phpunit --version +``` + +If for some reason, PHPUnit is not installed refer to the [PHPUnit Installation Guide](https://docs.phpunit.de/en/11.4/installation.html#installing-phpunit-with-composer). + +### Writing Functional Tests with PHPUnit + +For instructions on generating the factory, please refer to the [Generate The Factory section](#generate-the-factory). + +#### Writing PHPUnit tests + +Here’s an example of a test class, which use the Factory: + +```php +count(100)->create(); + + // Send a GET request to the collection endpoint + $response = $this->getJson('/api/books'); + + // Assert that the response is successful (200 OK) + $response->assertStatus(200); + + // Check the Content-Type header + $response->assertHeader('Content-Type', 'application/json'); + + // Assert the returned JSON contains the expected structure using assertJsonContains from the trait + $this->assertJsonContains([ + '@context' => '/contexts/Book', + '@id' => '/books', + '@type' => 'Collection', + 'totalItems' => 100, + 'view' => [ + '@id' => '/books?page=1', + '@type' => 'PartialCollectionView', + 'first' => '/books?page=1', + 'last' => '/books?page=4', + 'next' => '/books?page=2', + ], + ], $response->json()); + + // Assert that 30 items are returned in the response + $this->assertCount(30, $response->json('data')); + } + + /** + * Test to create a valid book. + */ + public function testCreateBook(): void + { + // Send a POST request to create a book + $response = $this->postJson('/api/books', [ + 'isbn' => '0099740915', + 'title' => 'The Handmaid\'s Tale', + 'description' => 'Brilliantly conceived and executed, this powerful evocation of twenty-first century America...', + 'author' => 'Margaret Atwood', + 'publication_date' => '1985-07-31', + ]); + + // Assert that the book was created successfully (201) + $response->assertStatus(201); + + // Check the Content-Type header + $response->assertHeader('Content-Type', 'application/json'); + + // Assert the returned JSON contains the expected book information using assertJsonContains + $this->assertJsonContains([ + '@context' => '/contexts/Book', + '@type' => 'Book', + 'isbn' => '0099740915', + 'title' => 'The Handmaid\'s Tale', + 'author' => 'Margaret Atwood', + 'publication_date' => '1985-07-31', + 'reviews' => [], + ], $response->json()); + + // Assert that the URI of the created resource matches the expected format + $this->assertMatchesRegularExpression('~^/api/books/\d+$~', $response->json('@id')); + } + + /** + * Test to create an invalid book and validate error handling. + */ + public function testCreateInvalidBook(): void + { + // Send a POST request with invalid data + $response = $this->postJson('/api/books', [ + 'isbn' => 'invalid', + ]); + + // Assert that the response status is 422 Unprocessable Entity + $response->assertStatus(422); + + // Check the Content-Type header + $response->assertHeader('Content-Type', 'application/json'); + + // Assert the JSON response contains the validation errors using assertJsonContains + $this->assertJsonContains([ + '@context' => '/contexts/ConstraintViolationList', + '@type' => 'ConstraintViolationList', + 'title' => 'An error occurred', + 'description' => [ + 'isbn' => 'This value is neither a valid ISBN-10 nor a valid ISBN-13.', + 'title' => 'This value should not be blank.', + 'description' => 'This value should not be blank.', + 'author' => 'This value should not be blank.', + 'publication_date' => 'This value should not be null.', + ], + ], $response->json()); + } + + /** + * Test to update a book. + */ + public function testUpdateBook(): void + { + // Create a book using the factory + $book = Book::factory()->create(['isbn' => '9781344037075']); + + // Get the IRI of the book using getIriFromResource from the trait + $iri = $this->getIriFromResource($book); + + // Send a PATCH request to update the book's title + $response = $this->patchJson($iri, [ + 'title' => 'Updated Title', + ]); + + // Assert that the response is successful (200 OK) + $response->assertStatus(200); + + // Assert the JSON response contains the updated book information using assertJsonContains + $this->assertJsonContains([ + '@id' => $iri, + 'isbn' => '9781344037075', + 'title' => 'Updated Title', + ], $response->json()); + } + + /** + * Test to delete a book. + */ + public function testDeleteBook(): void + { + // Create a book using the factory + $book = Book::factory()->create(['isbn' => '9781344037075']); + + // Get the IRI of the book using getIriFromResource from the trait + $iri = $this->getIriFromResource($book); + + // Send a DELETE request to remove the book + $response = $this->deleteJson($iri); + + // Assert that the response status is 204 No Content + $response->assertStatus(204); + + // Assert that the book is no longer in the database + $this->assertDatabaseMissing('books', ['id' => $book->id]); + } +} +``` + +In the example above, the [RefreshDatabase Trait](https://laravel.com/docs/database-testing#resetting-the-database-after-each-test) +is used to ensure that the database is automatically reset between test runs. This guarantees that each test starts with +a clean database state, avoiding conflicts from residual data and ensuring test isolation. + +This trait is especially useful when testing operations that modify the database, as it rolls back any changes made +during the test. As a result, your test environment remains reliable and consistent across multiple test executions. + +#### Run PHPUnit tests + +If everything is working properly, you should see `OK (5 tests, 15 assertions)`. +Your REST API is now properly tested! + +Check out the [API Test Assertions section](#api-test-assertions-with-laravel) to discover the full range of assertions +and other features provided by API Platform's test utilities. + +## Writing Unit Tests + +In addition to integration tests written using the helpers provided by Pest and PHPUnit, all the classes of your project +should be covered by [unit tests](https://en.wikipedia.org/wiki/Unit_testing). +To do so, learn how to write unit tests with [Pest](https://pestphp.com), [PHPUnit](https://phpunit.de/) and +[Laravel Creating Tests Guide](https://laravel.com/docs/11.x/testing#creating-tests). + +## Continuous Integration, Continuous Delivery and Continuous Deployment + +Running your test suite in your [CI/CD pipeline](https://en.wikipedia.org/wiki/Continuous_integration) is important to ensure good quality and delivery time. + +The API Platform 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]() 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. + +## Additional and Alternative Testing Tools + +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; +- [Playwright](https://playwright.dev) is recommended if you use have PWA/JavaScript-heavy app. + +## Testing Utilities for Laravel + +### API Test Assertions with Laravel + +In addition to [the built-in ones](https://phpunit.readthedocs.io/en/main/assertions.html), API Platform provides +convenient PHPUnit assertions dedicated to API testing: + +```php +get('/'); + + // Asserts that an array has a specified subset. + $this->assertArraySubset(/* An array or an iterable */); + + // Asserts that the returned JSON is a superset of the passed one + $this->assertJsonContains(/* a JSON document as an array or as a string */); + } +} +``` + +There is also a method to find the IRI matching a given resource: + +```php +getIriFromResource($book); + + $response = $this->get($iri); + + $response->assertStatus(200); + } +} +``` diff --git a/outline.yaml b/outline.yaml index 27bbac705d6..66588235f1d 100644 --- a/outline.yaml +++ b/outline.yaml @@ -12,6 +12,7 @@ chapters: path: laravel items: - index + - testing - filters - security - validation diff --git a/symfony/testing.md b/symfony/testing.md index 7df71d83683..4f9cb3b810b 100644 --- a/symfony/testing.md +++ b/symfony/testing.md @@ -1,11 +1,9 @@ -# Testing the API +# Testing the API with Symfony -Now that you have a functional API, you should write tests to ensure it has no bugs, and to prevent future regressions. -Some would argue that it's even better to [write tests first](https://martinfowler.com/bliki/TestDrivenDevelopment.html). +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). -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). - -Let's learn how to use them! +Let's learn how to use tests with Symfony!

Tests and Assertions screencast
Watch the Tests & Assertions screencast

@@ -344,7 +342,8 @@ bin/phpunit If everything is working properly, you should see `OK (5 tests, 17 assertions)`. Your REST API is now properly tested! -Check out the [testing documentation](../core/testing.md) 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 @@ -366,10 +365,8 @@ You may also be interested in these alternative testing tools (not included in t - [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; +- [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. @@ -381,3 +378,214 @@ works, you need [end-to-end testing](https://wiki.c2.com/?EndToEndPrinciple). To 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#running-the-docker-compose-setup-for-production-locally). + +## Testing Utilities for Symfony + +API Platform provides a set of useful utilities dedicated to API testing. +For an overview of how to test an API Platform app, be sure to read [the testing part first](#testing-the-api-with-symfony). + +

Test and Assertions screencast
Watch the API Tests & Assertions screencast

+ +### The Test HttpClient + +API Platform provides its own implementation of the [Symfony HttpClient](https://symfony.com/doc/current/components/http_client.html)'s interfaces, tailored to be used directly in [PHPUnit](https://phpunit.de/) test classes. + +While all the convenient features of Symfony HttpClient are available and usable directly, under the hood the API Platform implementation manipulates [the Symfony HttpKernel](https://symfony.com/doc/current/components/http_kernel.html) directly to simulate HTTP requests and responses. +This approach results in a huge performance boost compared to triggering real network requests. +It also allows access to the [Symfony HttpKernel](https://symfony.com/doc/current/components/http_kernel.html) and to all your services via the [Dependency Injection Container](https://symfony.com/doc/current/testing.html#accessing-the-container). +Reuse them to run, for instance, SQL queries or requests to external APIs directly from your tests. + +Install the `symfony/http-client` and `symfony/browser-kit` packages to enable the API Platform test client: + +```console +composer require symfony/browser-kit symfony/http-client +``` + +To use the testing client, your test class must extend the `ApiTestCase` class: + +```php +request('GET', '/books'); + // your assertions here... + } +} +``` + +Refer to [the Symfony HttpClient documentation](https://symfony.com/doc/current/components/http_client.html) to discover all the features of the client (custom headers, JSON encoding and decoding, HTTP Basic and Bearer authentication and cookies support, among other things). + +Note that you can create your own test case class extending the ApiTestCase. For example to set up a Json Web Token authentication: + +```php +getToken(); + + return static::createClient([], ['headers' => ['authorization' => 'Bearer '.$token]]); + } + + /** + * Use other credentials if needed. + */ + protected function getToken($body = []): string + { + if ($this->token) { + return $this->token; + } + + $response = static::createClient()->request('POST', '/login', ['json' => $body ?: [ + 'username' => 'admin@example.com', + 'password' => '$3cr3t', + ]]); + + $this->assertResponseIsSuccessful(); + $data = $response->toArray(); + $this->token = $data['token']; + + return $data['token']; + } +} +``` + +Use it by extending the `AbstractTest` class. For example this class tests the `/users` resource accessibility where only the admin can retrieve the collection: + +```php +createClientWithCredentials()->request('GET', '/users'); + $this->assertResponseIsSuccessful(); + } + + public function testLoginAsUser() + { + $token = $this->getToken([ + 'username' => 'user@example.com', + 'password' => '$3cr3t', + ]); + + $response = $this->createClientWithCredentials($token)->request('GET', '/users'); + $this->assertJsonContains(['description' => 'Access Denied.']); + $this->assertResponseStatusCodeSame(403); + } +} +``` + +### API Test Assertions with Symfony + +In addition to [the built-in ones](https://phpunit.readthedocs.io/en/11.4/assertions.html), API Platform provides convenient PHPUnit assertions dedicated to API testing: + +```php +request(...); + + // Asserts that the returned JSON is equal to the passed one + $this->assertJsonEquals(/* a JSON document as an array or as a string */); + + // Asserts that the returned JSON is a superset of the passed one + $this->assertJsonContains(/* a JSON document as an array or as a string */); + + // justinrainbow/json-schema must be installed to use the following assertions + + // Asserts that the returned JSON matches the passed JSON Schema + $this->assertMatchesJsonSchema(/* a JSON Schema as an array or as a string */); + + // Asserts that the returned JSON is validated by the JSON Schema generated for this resource by API Platform + + // For collections + $this->assertMatchesResourceCollectionJsonSchema(YourApiResource::class); + // And for items + $this->assertMatchesResourceItemJsonSchema(YourApiResource::class); + } +} +``` + +There is also a method to find the IRI matching a given resource and some criteria: + +```php +findIriBy(Book::class, ['isbn' => '9780451524935']); + static::createClient()->request('GET', $iri); + $this->assertResponseIsSuccessful(); + } +} +``` + +### HTTP Test Assertions + +All test assertions provided by Symfony (assertions for status codes, headers, cookies, XML documents...) can be used out of the box with the API Platform test client: + +```php +request('GET', '/books'); + + $this->assertResponseIsSuccessful(); + $this->assertResponseHeaderSame('content-type', 'application/ld+json; charset=utf-8'); + } +} +``` + +[Check out the dedicated Symfony documentation entry](https://symfony.com/doc/current/testing/functional_tests_assertions.html). From ce96acbf2cc2c7c5513a253911e5e599366251c9 Mon Sep 17 00:00:00 2001 From: Alan Poulain Date: Mon, 25 Nov 2024 15:27:03 +0100 Subject: [PATCH 42/80] fix(file-upload): always JSON encode field values (#2065) --- core/file-upload.md | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/core/file-upload.md b/core/file-upload.md index dbb87a984f4..f3f12ce17f6 100644 --- a/core/file-upload.md +++ b/core/file-upload.md @@ -330,6 +330,34 @@ The file and the resource fields will be posted to the resource endpoint. This example will use a custom `multipart/form-data` decoder to deserialize the resource instead of a custom controller. +> [!WARNING] +> Make sure to encode the fields in JSON before sending them. + +For instance, you could do something like this: +```js +async function uploadBook(file) { + const bookMetadata = { + title: "API Platform Best Practices", + genre: "Programming" + }; + + const formData = new FormData(); + for (const [name, value] of Object.entries(bookMetadata)) { + formData.append(name, JSON.stringify(value)); + } + formData.append('file', file); + + const response = await fetch('https://my-api.com/books', { + method: 'POST', + body: formData + }); + + const result = await response.json(); + + return result; +} +``` + ### Configuring the Existing Resource Receiving the Uploaded File The `Book` resource needs to be modified like this: @@ -416,9 +444,7 @@ final class MultipartDecoder implements DecoderInterface return array_map(static function (string $element) { // Multipart form values will be encoded in JSON. - $decoded = json_decode($element, true); - - return \is_array($decoded) ? $decoded : $element; + return json_decode($element, true, flags: \JSON_THROW_ON_ERROR); }, $request->request->all()) + $request->files->all(); } From 9ea9d63c8670e07e768a2c8caf01895650cc9a95 Mon Sep 17 00:00:00 2001 From: Vincent Amstoutz Date: Tue, 26 Nov 2024 18:34:16 +0100 Subject: [PATCH 43/80] refactor: split security doc and add laravel support (#2046) --- core/security.md | 355 +------------------------------------------- laravel/security.md | 29 +++- outline.yaml | 1 + symfony/security.md | 353 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 386 insertions(+), 352 deletions(-) create mode 100644 symfony/security.md diff --git a/core/security.md b/core/security.md index 5dadaeb716f..ed9eef1b7da 100644 --- a/core/security.md +++ b/core/security.md @@ -1,353 +1,12 @@ # Security -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. +API Platform provides advanced authentication and authorization features to secure your API. -

Security screencast
Watch the Security screencast

+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. -```php - - -Resource signature can be modified at the property level as well: - - - -```php - - -In this example: - -- The user must be logged in to interact with `Book` resources (configured at the resource level) -- Only users having [the role](https://symfony.com/doc/current/security.html#roles) `ROLE_ADMIN` can create a new resource (configured on the `post` operation) -- Only users having the `ROLE_ADMIN` or owning the current object can replace an existing book (configured on the `put` operation) -- Only users having the `ROLE_ADMIN` can view or modify the `adminOnlyProperty` property. Only users having the `ROLE_ADMIN` can create a new resource specifying `adminOnlyProperty` value. -- Only users that are granted the `UPDATE` attribute on the book (via a voter) can write to the field - -Available variables are: - -- `user`: the current logged in object, if any -- `object`: the current resource class during denormalization, the current resource during normalization, or collection of resources for collection operations -- `previous_object`: (`securityPostDenormalize` only) a clone of `object`, before modifications were made - this is `null` for create operations -- `request` (only at the resource level): the current request - -Access control checks in the `security` attribute are always executed before the [denormalization step](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](state-processors.md). - -## Executing Access Control Rules After Denormalization - -In some cases, it might be useful to execute a security after the denormalization step. -To do so, use the `securityPostDenormalize` attribute: - - - -```php - - -This time, the `object` variable contains data that have been extracted from the HTTP request body during the denormalization process. -However, the object is not persisted yet. - -Additionally, in some cases you need to perform security checks on the original data. For example here, only the actual owner should be allowed to edit their book. In these cases, you can use the `previous_object` variable which contains the object that was read from the state provider. - -The value in the `previous_object` variable is cloned from the original object. -Note that, by default, this clone is not a deep one (it doesn't clone relationships, relationships are references). -To make a deep clone, [implement `__clone` method](https://www.php.net/manual/en/language.oop5.cloning.php) in the concerned resource class. - -## Hooking Custom Permission Checks Using Voters - -The easiest and recommended way to hook custom access control logic is [to write Symfony Voter classes](https://symfony.com/doc/current/security/voters.html). Your custom voters will automatically be used in security expressions through the `is_granted()` function. - -In order to give the current `object` to your voter, use the expression `is_granted('READ', object)` - -For example: - - - -```php - - -Please note that if you use both `security: "..."` and then `"post" => ["securityPostDenormalize" => "..."]`, the `security` on top level is called first, and after `securityPostDenormalize`. This could lead to unwanted behaviour, so avoid using both of them simultaneously. -If you need to use `securityPostDenormalize`, consider adding `security` for the other operations instead of the global one. - -Create a _BookVoter_ with the `bin/console make:voter` command: - -```php -security = $security; - } - - protected function supports($attribute, $subject): bool - { - $supportsAttribute = in_array($attribute, ['BOOK_CREATE', 'BOOK_READ', 'BOOK_EDIT', 'BOOK_DELETE']); - $supportsSubject = $subject instanceof Book; - - return $supportsAttribute && $supportsSubject; - } - - /** - * @param string $attribute - * @param Book $subject - * @param TokenInterface $token - * @return bool - */ - protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool - { - /** ... check if the user is anonymous ... **/ - - switch ($attribute) { - case 'BOOK_CREATE': - if ( $this->security->isGranted(Role::ADMIN) ) { return true; } // only admins can create books - break; - case 'BOOK_READ': - /** ... other authorization rules ... **/ - } - - return false; - } -} -``` - -_Note 1: When using Voters on POST methods: The voter needs an `$attribute` and `$subject` as input parameter, so you have to use the `securityPostDenormalize` (i.e. `"post" = { "securityPostDenormalize" = "is_granted('BOOK_CREATE', object)" }` ) because the object does not exist before denormalization (it is not created, yet.)_ - -_Note 2: You can't use Voters on the collection GET method, use [Collection Filters](https://api-platform.com/docs/core/security/#filtering-collection-according-to-the-current-user-permissions) instead._ - -## Configuring the Access Control Error Message - -By default when API requests are denied, you will get the "Access Denied" message. -You can change it by configuring the `securityMessage` attribute or the `securityPostDenormalizeMessage` attribute. - -For example: - - - -```php - - -## Filtering Collection According to the Current User Permissions - -Filtering collections according to the role or permissions of the current user must be done directly at [the state provider](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](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](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](operations.md#enabling-and-disabling-operations) -section. - -## Changing Serialization Groups Depending of the Current User - -See [how to dynamically change](serialization.md#changing-the-serialization-context-dynamically) the current Serializer context according to the current logged in user. +- 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/laravel/security.md b/laravel/security.md index 0d0b2751730..cb8d59f1b93 100644 --- a/laravel/security.md +++ b/laravel/security.md @@ -1,8 +1,13 @@ -# Security +# Security with Laravel ## Policies -API platform is compatible with Laravel [authorization](https://laravel.com/docs/authorization) mechanism. Once a gate is defined, API Platform will automatically detect your policy. +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. + +Once a gate is defined, API Platform will automatically detect your policy. ```php // app/Models/Book.php @@ -15,7 +20,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 | | -------------- | ---------------------------------------------------------- | @@ -26,7 +32,8 @@ API Platform will detect the operation and map it to a specific method in your p | 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 @@ -78,3 +85,17 @@ class Book extends Model { } ``` + +Or you can define it globally in the configuration by adding the following code: + +```php + [ + // .... + 'middleware' => 'auth:sanctum', + ], +]; +``` diff --git a/outline.yaml b/outline.yaml index 66588235f1d..6a5d9429d63 100644 --- a/outline.yaml +++ b/outline.yaml @@ -5,6 +5,7 @@ chapters: items: - index - validation + - security - testing - debugging - caddy diff --git a/symfony/security.md b/symfony/security.md new file mode 100644 index 00000000000..a8848cdff9d --- /dev/null +++ b/symfony/security.md @@ -0,0 +1,353 @@ +# 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. + +

Security screencast
Watch the Security screencast

+ + + +```php + + +Resource signature can be modified at the property level as well: + + + +```php + + +In this example: + +- The user must be logged in to interact with `Book` resources (configured at the resource level) +- Only users having [the role](https://symfony.com/doc/current/security.html#roles) `ROLE_ADMIN` can create a new resource (configured on the `post` operation) +- Only users having the `ROLE_ADMIN` or owning the current object can replace an existing book (configured on the `put` operation) +- Only users having the `ROLE_ADMIN` can view or modify the `adminOnlyProperty` property. Only users having the `ROLE_ADMIN` can create a new resource specifying `adminOnlyProperty` value. +- Only users that are granted the `UPDATE` attribute on the book (via a voter) can write to the field + +Available variables are: + +- `user`: the current logged in object, if any +- `object`: the current resource class during denormalization, the current resource during normalization, or collection of resources for collection operations +- `previous_object`: (`securityPostDenormalize` only) a clone of `object`, before modifications were made - this is `null` for create operations +- `request` (only at the resource level): the current request + +Access control checks in the `security` attribute are always executed before the [denormalization step](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](state-processors.md). + +## Executing Access Control Rules After Denormalization + +In some cases, it might be useful to execute a security after the denormalization step. +To do so, use the `securityPostDenormalize` attribute: + + + +```php + + +This time, the `object` variable contains data that have been extracted from the HTTP request body during the denormalization process. +However, the object is not persisted yet. + +Additionally, in some cases you need to perform security checks on the original data. For example here, only the actual owner should be allowed to edit their book. In these cases, you can use the `previous_object` variable which contains the object that was read from the state provider. + +The value in the `previous_object` variable is cloned from the original object. +Note that, by default, this clone is not a deep one (it doesn't clone relationships, relationships are references). +To make a deep clone, [implement `__clone` method](https://www.php.net/manual/en/language.oop5.cloning.php) in the concerned resource class. + +## Hooking Custom Permission Checks Using Voters + +The easiest and recommended way to hook custom access control logic is [to write Symfony Voter classes](https://symfony.com/doc/current/security/voters.html). Your custom voters will automatically be used in security expressions through the `is_granted()` function. + +In order to give the current `object` to your voter, use the expression `is_granted('READ', object)` + +For example: + + + +```php + + +Please note that if you use both `security: "..."` and then `"post" => ["securityPostDenormalize" => "..."]`, the `security` on top level is called first, and after `securityPostDenormalize`. This could lead to unwanted behaviour, so avoid using both of them simultaneously. +If you need to use `securityPostDenormalize`, consider adding `security` for the other operations instead of the global one. + +Create a _BookVoter_ with the `bin/console make:voter` command: + +```php +security = $security; + } + + protected function supports($attribute, $subject): bool + { + $supportsAttribute = in_array($attribute, ['BOOK_CREATE', 'BOOK_READ', 'BOOK_EDIT', 'BOOK_DELETE']); + $supportsSubject = $subject instanceof Book; + + return $supportsAttribute && $supportsSubject; + } + + /** + * @param string $attribute + * @param Book $subject + * @param TokenInterface $token + * @return bool + */ + protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool + { + /** ... check if the user is anonymous ... **/ + + switch ($attribute) { + case 'BOOK_CREATE': + if ( $this->security->isGranted(Role::ADMIN) ) { return true; } // only admins can create books + break; + case 'BOOK_READ': + /** ... other authorization rules ... **/ + } + + return false; + } +} +``` + +_Note 1: When using Voters on POST methods: The voter needs an `$attribute` and `$subject` as input parameter, so you have to use the `securityPostDenormalize` (i.e. `"post" = { "securityPostDenormalize" = "is_granted('BOOK_CREATE', object)" }` ) because the object does not exist before denormalization (it is not created, yet.)_ + +_Note 2: You can't use Voters on the collection GET method, use [Collection Filters](https://api-platform.com/docs/core/security/#filtering-collection-according-to-the-current-user-permissions) instead._ + +## Configuring the Access Control Error Message + +By default when API requests are denied, you will get the "Access Denied" message. +You can change it by configuring the `securityMessage` attribute or the `securityPostDenormalizeMessage` attribute. + +For example: + + + +```php + + +## Filtering Collection According to the Current User Permissions + +Filtering collections according to the role or permissions of the current user must be done directly at [the state provider](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](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](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](operations.md#enabling-and-disabling-operations) +section. + +## Changing Serialization Groups Depending of the Current User + +See [how to dynamically change](serialization.md#changing-the-serialization-context-dynamically) the current Serializer context according to the current logged in user. From 81156864a71e4870e1f3a167ab2ccd7b092e512b Mon Sep 17 00:00:00 2001 From: Vincent Amstoutz Date: Tue, 26 Nov 2024 18:37:30 +0100 Subject: [PATCH 44/80] refactor(filters): dedicated files for specific filters and laravel support (#2038) --- core/doctrine-filters.md | 1353 +++++++++++++++++++++++++ core/elasticsearch-filters.md | 223 +++++ core/filters.md | 1735 +++------------------------------ outline.yaml | 2 + 4 files changed, 1698 insertions(+), 1615 deletions(-) create mode 100644 core/doctrine-filters.md create mode 100644 core/elasticsearch-filters.md diff --git a/core/doctrine-filters.md b/core/doctrine-filters.md new file mode 100644 index 00000000000..3e2b9d76461 --- /dev/null +++ b/core/doctrine-filters.md @@ -0,0 +1,1353 @@ +# Doctrine ORM and MongoDB ODM Filters + +For further documentation on filters (including for Eloquent and Elasticsearch), please see the [Filters documentation](filters.md). + +!> [!WARNING] +> Prefer using QueryParameter instead of ApiFilter for more flexibility, this is subject to change in the next major version. + +## 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. + +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: + offer.date_filter: + parent: 'api_platform.doctrine.orm.date_filter' + arguments: [{ dateProperty: ~ }] + tags: ['api_platform.filter'] +``` + +We're linking the filter `offer.date_filter` with the resource like this: + + + +```php + + + + + + + + + offer.date_filter + + + + + + +``` + + + +2. By using the `#[ApiFilter]` attribute. + +This attribute automatically declares the service, and you just have to use the filter class you want: + +```php + + +```php + 'exact', 'price' => 'exact', 'description' => 'partial'])] +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`. + +## Date Filter + +The date filter allows filtering a collection by date intervals. + +Syntax: `?property[]=value` + +The value can take any date format supported by the [`\DateTime` constructor](https://www.php.net/manual/en/datetime.construct.php). + +The `after` and `before` filters will filter including the value whereas `strictly_after` and `strictly_before` will filter excluding the value. + +Like other filters, the date filter must be explicitly enabled: + + + +```php + + +Given that the collection endpoint is `/offers`, you can filter offers by date with the following query: `/offers?createdAt[after]=2018-03-19`. + +It will return all offers where `createdAt` is superior or equal to `2018-03-19`. + +### Managing `null` Values + +The date filter is able to deal with date properties having `null` values. +Four behaviors are available at the property level of the filter: + +| Description | Strategy to set | +|--------------------------------------|---------------------------------------------------------------------------------------------------------------------------| +| Use the default behavior of the DBMS | `null` | +| Exclude items | `ApiPlatform\Doctrine\Common\Filter\DateFilterInterface::EXCLUDE_NULL` (`exclude_null`) | +| Consider items as oldest | `ApiPlatform\Doctrine\Common\Filter\DateFilterInterface::INCLUDE_NULL_BEFORE` (`include_null_before`) | +| Consider items as youngest | `ApiPlatform\Doctrine\Common\Filter\DateFilterInterface::INCLUDE_NULL_AFTER` (`include_null_after`) | +| Always include items | `ApiPlatform\Doctrine\Common\Filter\DateFilterInterface::INCLUDE_NULL_BEFORE_AND_AFTER` (`include_null_before_and_after`) | + +For instance, exclude entries with a property value of `null` with the following service definition: + + + +```php + 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 + +# config/api/Offer.yaml +App\Entity\Offer: + # ... + operations: + ApiPlatform\Metadata\GetCollection: + filters: ['offer.date_filter'] +``` + + + +## Boolean Filter + +The boolean filter allows you to search on boolean fields and values. + +Syntax: `?property=` + +Enable the filter: + + + +```php + + +Given that the collection endpoint is `/offers`, you can filter offers with the following query: `/offers?isAvailableGenericallyInMyCountry=true`. + +It will return all offers where `isAvailableGenericallyInMyCountry` equals `true`. + +## Numeric Filter + +The numeric filter allows you to search on numeric fields and values. + +Syntax: `?property=` + +Enable the filter: + + + +```php + + +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. + +Syntax: `?property[]=value` + +Enable the filter: + + + +```php + + +Given that the collection endpoint is `/offers`, you can filter the price with the following query: `/offers?price[between]=12.99..15.99`. + +It will return all offers with `price` between 12.99 and 15.99. + +You can filter offers by joining two values, for example: `/offers?price[gt]=12.99&price[lt]=19.99`. + +## Exists Filter + +The "exists" filter allows you to select items based on a nullable field value. +It will also check the emptiness of a collection association. + +Syntax: `?exists[property]=` + +Enable the filter: + + + +```php + + +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 + +A conflict will occur if `exists` is also the name of a property with the search filter enabled. +Luckily, the query parameter name to use is configurable: + +```yaml +# api/config/packages/api_platform.yaml +api_platform: + collection: + exists_parameter_name: 'not_null' # the URL query parameter to use is now "not_null" +``` + +## Order Filter (Sorting) + +The order filter allows sorting a collection against the given properties. + +Syntax: `?order[property]=` + +Enable the filter: + + + +```php + 'order'])] +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 + +# config/api/Offer.yaml +App\Entity\Offer: + # ... + operations: + ApiPlatform\Metadata\GetCollection: + filters: ['offer.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`. + +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: + + + +```php + 'ASC', 'name' => '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 + +# config/api/Offer.yaml +App\Entity\Offer: + # ... + operations: + ApiPlatform\Metadata\GetCollection: + filters: ['offer.order_filter'] +``` + + + +### Comparing with Null Values + +When the property used for ordering can contain `null` values, you may want to specify how `null` values are treated in +the comparison: + +| Description | Strategy to set | +|--------------------------------------|------------------------------------------------------------------------------------------------------| +| Use the default behavior of the DBMS | `null` | +| Consider items as smallest | `ApiPlatform\Doctrine\Common\Filter\OrderFilterInterface::NULLS_SMALLEST` (`nulls_smallest`) | +| Consider items as largest | `ApiPlatform\Doctrine\Common\Filter\OrderFilterInterface::NULLS_LARGEST` (`nulls_largest`) | +| Order items always first | `ApiPlatform\Doctrine\Common\Filter\OrderFilterInterface::NULLS_ALWAYS_FIRST` (`nulls_always_first`) | +| Order items always last | `ApiPlatform\Doctrine\Common\Filter\OrderFilterInterface::NULLS_ALWAYS_LAST` (`nulls_always_last`) | + +For instance, treat entries with a property value of `null` as the smallest, with the following service definition: + + + +```php + ['nulls_comparison' => OrderFilterInterface::NULLS_SMALLEST, 'default_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 + +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.: + + + +```php + 'exact'])] +class Offer +{ + // ... +} +``` + +```yaml +# config/services.yaml +services: + offer.order_filter: + parent: 'api_platform.doctrine.orm.order_filter' + arguments: [{ product.releaseDate: ~ }] + tags: ['api_platform.filter'] + # The following are mandatory only if a _defaults section is defined with inverted values. + # You may want to isolate filters in a dedicated file to avoid adding the following lines (by adding them in the defaults section) + autowire: false + autoconfigure: false + public: false + offer.search_filter: + parent: 'api_platform.doctrine.orm.search_filter' + arguments: [{ product.color: 'exact' }] + tags: ['api_platform.filter'] + # The following are mandatory only if a _defaults section is defined with inverted values. + # You may want to isolate filters in a dedicated file to avoid adding the following lines (by adding them in the defaults section) + autowire: false + autoconfigure: false + public: false + +# config/api/Offer.yaml +App\Entity\Offer: + # ... + operations: + ApiPlatform\Metadata\GetCollection: + filters: ['offer.order_filter', 'offer.search_filter'] +``` + + + +The above allows you to find offers by their respective product's color: `http://localhost:8000/api/offers?product.color=red`, +or order offers by the product's release date: `http://localhost:8000/api/offers?order[product.releaseDate]=desc` + +## Enabling a Filter for All Properties of a Resource + +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 +for all properties: + + + +```php + + +**Note: Filters on nested properties must still be enabled explicitly, in order to keep things sane.** + +Regardless of this option, filters can be applied on a property only if: + +- the property exists +- the value is supported (ex: `asc` or `desc` for the order filters). + +It means that the filter will be **silently** ignored if the property: + +- does not exist +- is not enabled +- has an invalid value + + +## Decorate a Doctrine filter using Symfony + +A filter that implements the `ApiPlatform\Doctrine\Common\Filter\PropertyAwareFilterInterface` interface can be decorated: + +```php +namespace App\Doctrine\Filter; + +use ApiPlatform\Doctrine\Common\Filter\PropertyAwareFilterInterface; +use ApiPlatform\Doctrine\Orm\Filter\FilterInterface; +use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; +use ApiPlatform\Metadata\Operation; +use Doctrine\ORM\QueryBuilder; +use Symfony\Component\DependencyInjection\Attribute\Autowire; + +final class SearchTextAndDateFilter implements FilterInterface +{ + public function __construct(#[Autowire('@api_platform.doctrine.orm.search_filter.instance')] readonly FilterInterface $searchFilter, #[Autowire('@api_platform.doctrine.orm.date_filter.instance')] readonly FilterInterface $dateFilter, protected ?array $properties = null, private array $dateFilterProperties = [], private array $searchFilterProperties = []) + { + } + + // This function is only used to hook in documentation generators (supported by Swagger and Hydra) + public function getDescription(string $resourceClass): array + { + if ($this->searchFilter instanceof PropertyAwareFilterInterface) { + $this->searchFilter->setProperties($this->searchFilterProperties); + } + if ($this->dateFilter instanceof PropertyAwareFilterInterface) { + $this->dateFilter->setProperties($this->dateFilterProperties); + } + + return array_merge($this->searchFilter->getDescription($resourceClass), $this->dateFilter->getDescription($resourceClass)); + } + + public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void + { + if ($this->searchFilter instanceof PropertyAwareFilterInterface) { + $this->searchFilter->setProperties($this->searchFilterProperties); + } + if ($this->dateFilter instanceof PropertyAwareFilterInterface) { + $this->dateFilter->setProperties($this->dateFilterProperties); + } + + $this->searchFilter->apply($queryBuilder, $queryNameGenerator, $resourceClass, $operation, ['filters' => $context['filters']['searchOnTextAndDate']] + $context); + $this->dateFilter->apply($queryBuilder, $queryNameGenerator, $resourceClass, $operation, ['filters' => $context['filters']['searchOnTextAndDate']] + $context); + } +} +``` + +This can be used with parameters using attributes: + +```php +namespace App\Entity; + +use ApiPlatform\Metadata\QueryParameter; + +#[GetCollection( + uriTemplate: 'search_filter_parameter{._format}', + parameters: [ + 'searchOnTextAndDate[:property]' => new QueryParameter(filter: 'app_filter_date_and_search'), + ] +)] +// Note that we link the parameter filter and this filter using the "alias" option: +#[ApiFilter(SearchTextAndDateFilter::class, alias: 'app_filter_date_and_search', properties: ['foo', 'createdAt'], arguments: ['dateFilterProperties' => ['createdAt' => 'exclude_null'], 'searchFilterProperties' => ['foo' => 'exact']])] +#[ORM\Entity] +class SearchFilterParameter +{ + /** + * @var int The id + */ + #[ORM\Column(type: 'integer')] + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'AUTO')] + private ?int $id = null; + #[ORM\Column(type: 'string')] + private string $foo = ''; + + #[ORM\Column(type: 'datetime_immutable', nullable: true)] + private ?\DateTimeImmutable $createdAt = null; + + public function getId(): ?int + { + return $this->id; + } + + public function getFoo(): string + { + return $this->foo; + } + + public function setFoo(string $foo): void + { + $this->foo = $foo; + } + + public function getCreatedAt(): ?\DateTimeImmutable + { + return $this->createdAt; + } + + public function setCreatedAt(\DateTimeImmutable $createdAt): void + { + $this->createdAt = $createdAt; + } +} +``` + +## Using Doctrine ORM Filters + +Doctrine ORM features [a filter system](https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/filters.html) that allows the developer to add SQL to the conditional clauses of queries, regardless of the place where the SQL is generated (e.g. from a DQL query, or by loading associated entities). +These are applied to collections and items and therefore are incredibly useful. + +The following information, specific to Doctrine filters in Symfony, is based upon [a great article posted on Michaël Perrin's blog](https://www.michaelperrin.fr/blog/2014/12/doctrine-filters). + +Suppose we have a `User` entity and an `Order` entity related to the `User` one. A user should only see his orders and no one else's. + +```php +getReflectionClass()->getAttributes(UserAware::class)[0] ?? null; + + $fieldName = $userAware?->getArguments()['userFieldName'] ?? null; + if ($fieldName === '' || is_null($fieldName)) { + return ''; + } + + try { + // Don't worry, getParameter automatically escapes parameters + $userId = $this->getParameter('id'); + } catch (\InvalidArgumentException $e) { + // No user ID has been defined + return ''; + } + + if (empty($fieldName) || empty($userId)) { + return ''; + } + + return sprintf('%s.%s = %s', $targetTableAlias, $fieldName, $userId); + } +} +``` + +Now, we must configure the Doctrine filter. + +```yaml +# api/config/packages/api_platform.yaml +doctrine: + orm: + filters: + user_filter: + class: App\Filter\UserFilter + enabled: true +``` + +Done: Doctrine will automatically filter all `UserAware`entities! + +## Creating Custom Doctrine ORM Filters + +Doctrine ORM filters have access to the context created from the HTTP request and to the `QueryBuilder` instance used to +retrieve data from the database. They are only applied to collections. If you want to deal with the DQL query generated +to retrieve items, [extensions](extensions.md) are the way to go. + +A Doctrine ORM filter is basically a class implementing the `ApiPlatform\Doctrine\Orm\Filter\FilterInterface`. +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 +` + +Enable the filter: + + + +```php + 'order'])] +class Tweet +{ + // ... +} +``` + +```yaml +# config/services.yaml +services: + tweet.order_filter: + parent: 'api_platform.doctrine.orm.order_filter' + arguments: + $properties: { id: ~, date: ~ } + $orderParameterName: 'order' + tags: ['api_platform.filter'] + # The following are mandatory only if a _defaults section is defined with inverted values. + # You may want to isolate filters in a dedicated file to avoid adding the following lines (by adding them in the defaults section) + autowire: false + autoconfigure: false + public: false + +# config/api/Tweet.yaml +App\Entity\Tweet: + # ... + filters: ['tweet.order_filter'] +``` + + + +Given that the collection endpoint is `/tweets`, you can filter tweets by ID and date in ascending or descending order: +`/tweets?order[id]=asc&order[date]=desc`. + +By default, whenever the query does not specify the direction explicitly (e.g: `/tweets?order[id]&order[date]`), filters +will not be applied unless you configure a default order direction to use: + +```php + 'asc', 'date' => 'desc'])] +class Tweet +{ + // ... +} +``` + +### Using a Custom Order Query Parameter Name + +A conflict will occur if `order` is also the name of a property with the term filter enabled. Luckily, the query +parameter name to use is configurable: + +```yaml +# api/config/packages/api_platform.yaml +api_platform: + collection: + order_parameter_name: '_order' # the URL query parameter to use is now "_order" +``` + +## Match Filter + +The match filter allows us to find resources that [match](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query.html) +the specified text on full-text fields. + +Syntax: `?property[]=value` + +Enable the filter: + +```php + $context['filters']['fullName'], + 'operator' => 'and', + ]; + + $requestBody['query']['constant_score']['filter']['bool']['must'][0]['match']['full_name'] = $andQuery; + + return $requestBody; + } +} +``` diff --git a/core/filters.md b/core/filters.md index cab3eaecf92..0bb76184480 100644 --- a/core/filters.md +++ b/core/filters.md @@ -1,7 +1,7 @@ # Filters API Platform provides a generic system to apply filters and sort criteria on collections. -Useful filters for Doctrine ORM, MongoDB ODM, and ElasticSearch are provided with the library. +Useful filters for Doctrine ORM, Eloquent ORM, MongoDB ODM and ElasticSearch are provided with the library. You can also create custom filters that fit your specific needs. You can also add filtering support to your custom [state providers](state-providers.md) by implementing interfaces provided @@ -14,6 +14,11 @@ It is also automatically documented as a `search` property for JSON-LD responses

Filtering and Searching screencast
Watch the Filtering & Searching screencast

+For the **specific filters documentation**, please refer to the following pages, depending on your needs: +- [Doctrine filters documentation](../core/doctrine-filters.md) +- [Elasticsearch filters documentation](../core/elasticsearch-filters.md) +- [Laravel filters documentation](../laravel/filters.md) + ## Parameters You can declare parameters on a Resource or an Operation through the `parameters` property. @@ -26,7 +31,7 @@ use ApiPlatform\Metadata\QueryParameter; // This parameter "page" works only on /books #[GetCollection(uriTemplate: '/books', parameters: ['page' => new QueryParameter])] -// This parameter is available on every operations, key is mandatory +// This parameter is available on every operation, key is mandatory #[QueryParameter(key: 'q', property: 'freetextQuery')] class Book {} ``` @@ -64,7 +69,7 @@ class Book { } ``` -If you don't have autoconfiguration enabled, declare the parameter as a tagged service: +If you use Symfony, but you don't have autoconfiguration enabled, declare the parameter as a tagged service: ```yaml services: @@ -73,8 +78,27 @@ services: - name: 'api_platform.parameter_provider' key: 'ApiPlatform\Tests\Fixtures\TestBundle\Parameter\CustomGroupParameterProvider' ``` +or if you are using Laravel tag your provider with: + +```php +app->tag([CustomGroupParameterProvider::class], ParameterProvider::class); + } +} +``` -### Call a filter +### Call a filter with Symfony A Parameter can also call a filter and works on filters that impact the data persistence layer (Doctrine ORM, ODM and Eloquent filters are supported). Let's assume, that we have an Order filter declared: @@ -108,6 +132,51 @@ class Offer { } ``` +### Call a filter with Laravel + +A Parameter can also call a filter and works on filters that impact the data persistence layer (Doctrine ORM, ODM and Eloquent filters are supported). Let's assume, that we have an Order filter declared: + +```php +app->singleton(OrderFilter::class, function ($app) { + return new OrderFilter(['id' => null, 'name' => null], 'order'); + }); + + $this->app->tag([OrderFilter::class], ApiFilter::class); + } +} +``` + +We can use this filter specifying we want a query parameter with the `:property` placeholder: + +```php +namespace App\ApiResource; + +use ApiPlatform\Metadata\QueryParameter; + +#[GetCollection( + uriTemplate: 'orders', + parameters: [ + 'order[:property]' => new QueryParameter(filter: 'offer.order_filter'), + ] +) +class Offer { + public string $id; + public string $name; +} +``` + ### Header parameters The `HeaderParameter` attribute allows to create a parameter that's using HTTP Headers instead of query parameters: @@ -163,7 +232,7 @@ class Book { ### Documentation -A parameter is quite close to its documentation and you can specify the JSON Schema and/or the OpenAPI documentation: +A parameter is quite close to its documentation, and you can specify the JSON Schema and/or the OpenAPI documentation: ```php namespace App\ApiResource; @@ -204,6 +273,8 @@ If you need you can use the `filterContext` to transfer information between a pa ### Parameter validation +If you use Laravel refers to the [Laravel Validation documentation](../laravel/validation.md). + Parameter validation is automatic based on the configuration for example: ```php @@ -228,10 +299,12 @@ use ApiPlatform\Metadata\QueryParameter; class ValidateParameter {} ``` -You can also use your own constraint by setting the `constraints` option on a Parameter. In that case we won't setup the automatic validation for you and it'll replace our defaults. +You can also use your own constraint by setting the `constraints` option on a Parameter. In that case we won't set up the automatic validation for you, and it'll replace our defaults. ### Parameter security +If you use Laravel refers to the [Laravel Security documentation](../laravel/security.md). + Parameters may have security checks: ```php @@ -250,1663 +323,95 @@ use ApiPlatform\Metadata\QueryParameter; class SecurityParameter {} ``` -### Decorate a Doctrine filter - -A filter that implements the `ApiPlatform\Doctrine\Common\Filter\PropertyAwareFilterInterface` interface can be decorated: - -```php -namespace App\Doctrine\Filter; - -use ApiPlatform\Doctrine\Common\Filter\PropertyAwareFilterInterface; -use ApiPlatform\Doctrine\Orm\Filter\FilterInterface; -use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; -use ApiPlatform\Metadata\Operation; -use Doctrine\ORM\QueryBuilder; -use Symfony\Component\DependencyInjection\Attribute\Autowire; - -final class SearchTextAndDateFilter implements FilterInterface -{ - public function __construct(#[Autowire('@api_platform.doctrine.orm.search_filter.instance')] readonly FilterInterface $searchFilter, #[Autowire('@api_platform.doctrine.orm.date_filter.instance')] readonly FilterInterface $dateFilter, protected ?array $properties = null, private array $dateFilterProperties = [], private array $searchFilterProperties = []) - { - } - - // This function is only used to hook in documentation generators (supported by Swagger and Hydra) - public function getDescription(string $resourceClass): array - { - if ($this->searchFilter instanceof PropertyAwareFilterInterface) { - $this->searchFilter->setProperties($this->searchFilterProperties); - } - if ($this->dateFilter instanceof PropertyAwareFilterInterface) { - $this->dateFilter->setProperties($this->dateFilterProperties); - } - - return array_merge($this->searchFilter->getDescription($resourceClass), $this->dateFilter->getDescription($resourceClass)); - } - - public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void - { - if ($this->searchFilter instanceof PropertyAwareFilterInterface) { - $this->searchFilter->setProperties($this->searchFilterProperties); - } - if ($this->dateFilter instanceof PropertyAwareFilterInterface) { - $this->dateFilter->setProperties($this->dateFilterProperties); - } - - $this->searchFilter->apply($queryBuilder, $queryNameGenerator, $resourceClass, $operation, ['filters' => $context['filters']['searchOnTextAndDate']] + $context); - $this->dateFilter->apply($queryBuilder, $queryNameGenerator, $resourceClass, $operation, ['filters' => $context['filters']['searchOnTextAndDate']] + $context); - } -} -``` - -This can be used with parameters using attributes: - -```php -namespace App\Entity; - -use ApiPlatform\Metadata\QueryParameter; - -#[GetCollection( - uriTemplate: 'search_filter_parameter{._format}', - parameters: [ - 'searchOnTextAndDate[:property]' => new QueryParameter(filter: 'app_filter_date_and_search'), - ] -)] -// Note that we link the parameter filter and this filter using the "alias" option: -#[ApiFilter(SearchTextAndDateFilter::class, alias: 'app_filter_date_and_search', properties: ['foo', 'createdAt'], arguments: ['dateFilterProperties' => ['createdAt' => 'exclude_null'], 'searchFilterProperties' => ['foo' => 'exact']])] -#[ORM\Entity] -class SearchFilterParameter -{ - /** - * @var int The id - */ - #[ORM\Column(type: 'integer')] - #[ORM\Id] - #[ORM\GeneratedValue(strategy: 'AUTO')] - private ?int $id = null; - #[ORM\Column(type: 'string')] - private string $foo = ''; - - #[ORM\Column(type: 'datetime_immutable', nullable: true)] - private ?\DateTimeImmutable $createdAt = null; - - public function getId(): ?int - { - return $this->id; - } - - public function getFoo(): string - { - return $this->foo; - } - - public function setFoo(string $foo): void - { - $this->foo = $foo; - } - - public function getCreatedAt(): ?\DateTimeImmutable - { - return $this->createdAt; - } - - public function setCreatedAt(\DateTimeImmutable $createdAt): void - { - $this->createdAt = $createdAt; - } -} -``` - -## Doctrine ORM and MongoDB ODM Filters - -!> [!WARNING] - -> Prefer using QueryParameter instead of ApiFilter for more flexibility, this is subject to change in the next major version. - -### Basic Knowledge - -Filters are services (see the section on [custom filters](#creating-custom-filters)), and they can be linked -to a Resource in two ways: - -1. Through the resource declaration, as the `filters` attribute. - -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: - offer.date_filter: - parent: 'api_platform.doctrine.orm.date_filter' - arguments: [{ dateProperty: ~ }] - tags: ['api_platform.filter'] -``` - -We're linking the filter `offer.date_filter` with the resource like this: - - - -```php - - - - - - - - - offer.date_filter - - - - - - -``` - - - -2. By using the `#[ApiFilter]` attribute. - -This attribute automatically declares the service, and you just have to use the filter class you want: - -```php -` -In the following example, we will see how to allow the filtering of a list of e-commerce offers: +You can add as many groups as you need. - +Enable the filter: ```php 'exact', 'price' => 'exact', 'description' => 'partial'])] -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 together: `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 +#[ApiFilter(GroupFilter::class, arguments: ['parameterName' => 'groups', 'overrideDefaultGroups' => false, 'whitelist' => ['allowed_group']])] +class Book { // ... } ``` -```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` +Three arguments are available to configure the filter: -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`. +- `parameterName` is the query parameter name (default `groups`) +- `overrideDefaultGroups` allows to override the default serialization groups (default `false`) +- `whitelist` groups whitelist to avoid uncontrolled data exposure (default `null` to allow all groups) -### Date Filter +Given that the collection endpoint is `/books`, you can filter by serialization groups with the following query: `/books?groups[]=read&groups[]=write`. -The date filter allows filtering a collection by date intervals. +### Property filter -Syntax: `?property[]=value` +**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 value can take any date format supported by the [`\DateTime` constructor](https://www.php.net/manual/en/datetime.construct.php). +The property filter adds the possibility to select the properties to serialize (sparse fieldsets). -The `after` and `before` filters will filter including the value whereas `strictly_after` and `strictly_before` will filter excluding the value. +Syntax: `?properties[]=&properties[][]=` -Like other filters, the date filter must be explicitly enabled: +You can add as many properties as you need. - +Enable the filter: ```php 'properties', 'overrideDefaultProperties' => false, 'whitelist' => ['allowed_property']])] +class Book { // ... } ``` -```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 - -# config/api/Offer.yaml -App\Entity\Offer: - # ... - operations: - ApiPlatform\Metadata\GetCollection: - filters: ['offer.date_filter'] -``` - - - -Given that the collection endpoint is `/offers`, you can filter offers by date with the following query: `/offers?createdAt[after]=2018-03-19`. - -It will return all offers where `createdAt` is superior or equal to `2018-03-19`. - -#### Managing `null` Values - -The date filter is able to deal with date properties having `null` values. -Four behaviors are available at the property level of the filter: - -| Description | Strategy to set | -| ------------------------------------ | ------------------------------------------------------------------------------------------------------------- | -| Use the default behavior of the DBMS | `null` | -| Exclude items | `ApiPlatform\Doctrine\Common\Filter\DateFilterInterface::EXCLUDE_NULL` (`exclude_null`) | -| Consider items as oldest | `ApiPlatform\Doctrine\Common\Filter\DateFilterInterface::INCLUDE_NULL_BEFORE` (`include_null_before`) | -| Consider items as youngest | `ApiPlatform\Doctrine\Common\Filter\DateFilterInterface::INCLUDE_NULL_AFTER` (`include_null_after`) | -| Always include items | `ApiPlatform\Doctrine\Common\Filter\DateFilterInterface::INCLUDE_NULL_BEFORE_AND_AFTER` (`include_null_before_and_after`) | +Three arguments are available to configure the filter: -For instance, exclude entries with a property value of `null` with the following service definition: +- `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`. -```php - DateFilterInterface::EXCLUDE_NULL])] -class Offer -{ - // ... -} -``` +API Platform provides a convenient way to create Doctrine ORM and MongoDB ODM filters. If you use [custom state providers](state-providers.md), +you can still create filters by implementing the previously mentioned interface, but - as API Platform isn't aware of your +persistence system's internals - you have to create the filtering logic by yourself. -```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 - -# config/api/Offer.yaml -App\Entity\Offer: - # ... - operations: - ApiPlatform\Metadata\GetCollection: - filters: ['offer.date_filter'] -``` - - - -### Boolean Filter - -The boolean filter allows you to search on boolean fields and values. - -Syntax: `?property=` - -Enable the filter: - - - -```php - - -Given that the collection endpoint is `/offers`, you can filter offers with the following query: `/offers?isAvailableGenericallyInMyCountry=true`. - -It will return all offers where `isAvailableGenericallyInMyCountry` equals `true`. - -### Numeric Filter - -The numeric filter allows you to search on numeric fields and values. - -Syntax: `?property=` - -Enable the filter: - - - -```php - - -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. - -Syntax: `?property[]=value` - -Enable the filter: - - - -```php - - -Given that the collection endpoint is `/offers`, you can filter the price with the following query: `/offers?price[between]=12.99..15.99`. - -It will return all offers with `price` between 12.99 and 15.99. - -You can filter offers by joining two values, for example: `/offers?price[gt]=12.99&price[lt]=19.99`. - -### Exists Filter - -The "exists" filter allows you to select items based on a nullable field value. -It will also check the emptiness of a collection association. - -Syntax: `?exists[property]=` - -Enable the filter: - - - -```php - - -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 - -A conflict will occur if `exists` is also the name of a property with the search filter enabled. -Luckily, the query parameter name to use is configurable: - -```yaml -# api/config/packages/api_platform.yaml -api_platform: - collection: - exists_parameter_name: 'not_null' # the URL query parameter to use is now "not_null" -``` - -### Order Filter (Sorting) - -The order filter allows sorting a collection against the given properties. - -Syntax: `?order[property]=` - -Enable the filter: - - - -```php - 'order'])] -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 - -# config/api/Offer.yaml -App\Entity\Offer: - # ... - operations: - ApiPlatform\Metadata\GetCollection: - filters: ['offer.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`. - -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: - - - -```php - 'ASC', 'name' => '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 - -# config/api/Offer.yaml -App\Entity\Offer: - # ... - operations: - ApiPlatform\Metadata\GetCollection: - filters: ['offer.order_filter'] -``` - - - -#### Comparing with Null Values - -When the property used for ordering can contain `null` values, you may want to specify how `null` values are treated in -the comparison: - -| Description | Strategy to set | -| ------------------------------------ |---------------------------------------------------------------------------------------------------| -| Use the default behavior of the DBMS | `null` | -| Consider items as smallest | `ApiPlatform\Doctrine\Common\Filter\OrderFilterInterface::NULLS_SMALLEST` (`nulls_smallest`) | -| Consider items as largest | `ApiPlatform\Doctrine\Common\Filter\OrderFilterInterface::NULLS_LARGEST` (`nulls_largest`) | -| Order items always first | `ApiPlatform\Doctrine\Common\Filter\OrderFilterInterface::NULLS_ALWAYS_FIRST` (`nulls_always_first`) | -| Order items always last | `ApiPlatform\Doctrine\Common\Filter\OrderFilterInterface::NULLS_ALWAYS_LAST` (`nulls_always_last`) | - -For instance, treat entries with a property value of `null` as the smallest, with the following service definition: - - - -```php - ['nulls_comparison' => OrderFilterInterface::NULLS_SMALLEST, 'default_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 - -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.: - - - -```php - 'exact'])] -class Offer -{ - // ... -} -``` - -```yaml -# config/services.yaml -services: - offer.order_filter: - parent: 'api_platform.doctrine.orm.order_filter' - arguments: [{ product.releaseDate: ~ }] - tags: ['api_platform.filter'] - # The following are mandatory only if a _defaults section is defined with inverted values. - # You may want to isolate filters in a dedicated file to avoid adding the following lines (by adding them in the defaults section) - autowire: false - autoconfigure: false - public: false - offer.search_filter: - parent: 'api_platform.doctrine.orm.search_filter' - arguments: [{ product.color: 'exact' }] - tags: ['api_platform.filter'] - # The following are mandatory only if a _defaults section is defined with inverted values. - # You may want to isolate filters in a dedicated file to avoid adding the following lines (by adding them in the defaults section) - autowire: false - autoconfigure: false - public: false - -# config/api/Offer.yaml -App\Entity\Offer: - # ... - operations: - ApiPlatform\Metadata\GetCollection: - filters: ['offer.order_filter', 'offer.search_filter'] -``` - - - -The above allows you to find offers by their respective product's color: `http://localhost:8000/api/offers?product.color=red`, -or order offers by the product's release date: `http://localhost:8000/api/offers?order[product.releaseDate]=desc` - -### Enabling a Filter for All Properties of a Resource - -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 -for all properties: - - - -```php - - -**Note: Filters on nested properties must still be enabled explicitly, in order to keep things sane.** - -Regardless of this option, filters can be applied on a property only if: - -- the property exists -- the value is supported (ex: `asc` or `desc` for the order filters). - -It means that the filter will be **silently** ignored if the property: - -- does not exist -- is not enabled -- has an invalid value - -## Elasticsearch Filters - -### Ordering Filter (Sorting) - -The order filter allows to [sort](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-sort.html) -a collection against the given properties. - -Syntax: `?order[property]=` - -Enable the filter: - - - -```php - 'order'])] -class Tweet -{ - // ... -} -``` - -```yaml -# config/services.yaml -services: - tweet.order_filter: - parent: 'api_platform.doctrine.orm.order_filter' - arguments: - $properties: { id: ~, date: ~ } - $orderParameterName: 'order' - tags: ['api_platform.filter'] - # The following are mandatory only if a _defaults section is defined with inverted values. - # You may want to isolate filters in a dedicated file to avoid adding the following lines (by adding them in the defaults section) - autowire: false - autoconfigure: false - public: false - -# config/api/Tweet.yaml -App\Entity\Tweet: - # ... - filters: ['tweet.order_filter'] -``` - - - -Given that the collection endpoint is `/tweets`, you can filter tweets by ID and date in ascending or descending order: -`/tweets?order[id]=asc&order[date]=desc`. - -By default, whenever the query does not specify the direction explicitly (e.g: `/tweets?order[id]&order[date]`), filters -will not be applied unless you configure a default order direction to use: - -```php - 'asc', 'date' => 'desc'])] -class Tweet -{ - // ... -} -``` - -#### Using a Custom Order Query Parameter Name (Elastic) - -A conflict will occur if `order` is also the name of a property with the term filter enabled. Luckily, the query -parameter name to use is configurable: - -```yaml -# api/config/packages/api_platform.yaml -api_platform: - collection: - order_parameter_name: '_order' # the URL query parameter to use is now "_order" -``` - -### Match Filter - -The match filter allows us to find resources that [match](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query.html) -the specified text on full-text fields. - -Syntax: `?property[]=value` - -Enable the filter: - -```php -` - -You can add as many groups as you need. - -Enable the filter: - -```php - 'groups', 'overrideDefaultGroups' => false, 'whitelist' => ['allowed_group']])] -class Book -{ - // ... -} -``` - -Three arguments are available to configure the filter: - -- `parameterName` is the query parameter name (default `groups`) -- `overrideDefaultGroups` allows to override the default serialization groups (default `false`) -- `whitelist` groups whitelist to avoid uncontrolled data exposure (default `null` to allow all groups) - -Given that the collection endpoint is `/books`, you can filter by serialization groups with the following query: `/books?groups[]=read&groups[]=write`. - -### 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. - -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 - 'properties', 'overrideDefaultProperties' => false, 'whitelist' => ['allowed_property']])] -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`. - -## Creating Custom Filters - -Custom filters can be written by implementing the `ApiPlatform\Api\FilterInterface` interface. - -API Platform provides a convenient way to create Doctrine ORM and MongoDB ODM filters. If you use [custom state providers](state-providers.md), -you can still create filters by implementing the previously mentioned interface, but - as API Platform isn't aware of your -persistence system's internals - you have to create the filtering logic by yourself. - -### Creating Custom Doctrine ORM Filters - -Doctrine ORM filters have access to the context created from the HTTP request and to the `QueryBuilder` instance used to -retrieve data from the database. They are only applied to collections. If you want to deal with the DQL query generated -to retrieve items, [extensions](extensions.md) are the way to go. - -A Doctrine ORM filter is basically a class implementing the `ApiPlatform\Doctrine\Orm\Filter\FilterInterface`. -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 - $context['filters']['fullName'], - 'operator' => 'and', - ]; - - $requestBody['query']['constant_score']['filter']['bool']['must'][0]['match']['full_name'] = $andQuery; - - return $requestBody; - } -} -``` - -### 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). -These are applied to collections and items and therefore are incredibly useful. - -The following information, specific to Doctrine filters in Symfony, is based upon [a great article posted on Michaël Perrin's blog](https://www.michaelperrin.fr/blog/2014/12/doctrine-filters). - -Suppose we have a `User` entity and an `Order` entity related to the `User` one. A user should only see his orders and no one else's. - -```php -getReflectionClass()->getAttributes(UserAware::class)[0] ?? null; - - $fieldName = $userAware?->getArguments()['userFieldName'] ?? null; - if ($fieldName === '' || is_null($fieldName)) { - return ''; - } - - try { - // Don't worry, getParameter automatically escapes parameters - $userId = $this->getParameter('id'); - } catch (\InvalidArgumentException $e) { - // No user ID has been defined - return ''; - } - - if (empty($fieldName) || empty($userId)) { - return ''; - } - - return sprintf('%s.%s = %s', $targetTableAlias, $fieldName, $userId); - } -} -``` - -Now, we must configure the Doctrine filter. - -```yaml -# api/config/packages/api_platform.yaml -doctrine: - orm: - filters: - user_filter: - class: App\Filter\UserFilter - enabled: true -``` +If you need more information about creating custom filters, refer to the following documentation: -Done: Doctrine will automatically filter all `UserAware`entities! +- [Creating Custom Doctrine ORM filters](../core/doctrine-filters.md#creating-custom-doctrine-orm-filters) +- [Creating Custom Doctrine Mongo ODM filters](../core/doctrine-filters.md#creating-custom-doctrine-mongodb-odm-filters) +- [Creating Custom Doctrine ORM filters](../core/elasticsearch-filters.md#creating-custom-elasticsearch-filters) ## ApiFilter Attribute diff --git a/outline.yaml b/outline.yaml index 6a5d9429d63..6456aeef13c 100644 --- a/outline.yaml +++ b/outline.yaml @@ -31,6 +31,8 @@ chapters: - state-providers - state-processors - filters + - elasticsearch-filters + - doctrine-filters - subresources - serialization - validation From 16881eee83d56b189b2b61ff36811e6eddd0fded Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Sat, 30 Nov 2024 10:31:05 +0100 Subject: [PATCH 45/80] Convert MongoDB tutorial to PHP Attributes (#2079) * Convert MongoDB tutorial to PHP Attributes api-platform/doctrine-odm is required but mapping.paths automatically contains ODM Document dir * Switch to mongodb-community-server --- core/getting-started.md | 2 - core/mongodb.md | 99 +++++++++++++---------------------------- 2 files changed, 31 insertions(+), 70 deletions(-) diff --git a/core/getting-started.md b/core/getting-started.md index 7b9071fb497..2f66b7d36ac 100644 --- a/core/getting-started.md +++ b/core/getting-started.md @@ -79,7 +79,6 @@ class Product // The class name will be used to name exposed resources /** * A name property - this description will be available in the API documentation too. - * */ #[ORM\Column] #[Assert\NotBlank] @@ -88,7 +87,6 @@ class Product // The class name will be used to name exposed resources // Notice the "cascade" option below, this is mandatory if you want Doctrine to automatically persist the related entity /** * @var Offer[]|ArrayCollection - * */ #[ORM\OneToMany(targetEntity: Offer::class, mappedBy: 'product', cascade: ['persist'])] public iterable $offers; diff --git a/core/mongodb.md b/core/mongodb.md index a59a8421d27..6d089e83819 100644 --- a/core/mongodb.md +++ b/core/mongodb.md @@ -49,12 +49,12 @@ services: # ... db-mongodb: # In production, you may want to use a managed database service - image: mongo + image: mongodb/mongodb-community-server:latest environment: - - MONGO_INITDB_DATABASE=api - - MONGO_INITDB_ROOT_USERNAME=api-platform + - MONGODB_INITDB_DATABASE=api + - MONGODB_INITDB_ROOT_USERNAME=api-platform # You should definitely change the password in production - - MONGO_INITDB_ROOT_PASSWORD=!ChangeMe! + - 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! @@ -64,11 +64,10 @@ services: # ... ``` -In all cases, enable the MongoDB support by requiring the [Doctrine MongoDB ODM bundle](https://github.com/doctrine/DoctrineMongoDBBundle) -package 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 +composer require doctrine/mongodb-odm-bundle api-platform/doctrine-odm ``` Execute the contrib recipe to have it already configured. @@ -81,20 +80,6 @@ MONGODB_URL=mongodb://api-platform:!ChangeMe!@db-mongodb MONGODB_DB=api ``` -Change the configuration of API Platform to add the right mapping path: - -```yaml -# api/config/packages/api_platform.yaml -api_platform: - # ... - - mapping: - paths: - ['%kernel.project_dir%/src/Entity', '%kernel.project_dir%/src/Document'] - - # ... -``` - ## Creating Documents Creating resources mapped to MongoDB documents is as simple as creating entities: @@ -107,30 +92,23 @@ namespace App\Document; use ApiPlatform\Metadata\ApiResource; use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; use Symfony\Component\Validator\Constraints as Assert; -/** - * @ODM\Document - */ +#[ODM\Document] #[ApiResource] class Product { - /** - * @ODM\Id(strategy="INCREMENT", type="int") - */ - private $id; - - /** - * @ODM\Field - */ + #[ODM\Id(strategy: 'INCREMENT')] + private ?int $id; + + #[ODM\Field] #[Assert\NotBlank] - public $name; + public string $name; - /** - * @ODM\ReferenceMany(targetDocument=Offer::class, mappedBy="product", cascade={"persist"}, storeAs="id") - */ - public $offers; + #[ODM\ReferenceMany(targetDocument: Offer::class, mappedBy: 'product', cascade: ['persist'], storeAs: 'id')] + public Collection $offers; public function __construct() { @@ -168,34 +146,23 @@ use ApiPlatform\Metadata\ApiResource; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; use Symfony\Component\Validator\Constraints as Assert; -/** - * @ODM\Document - */ +#[ODM\Document] #[ApiResource(types: ['https://schema.org/Offer'])] class Offer { - /** - * @ODM\Id(strategy="INCREMENT", type="int") - */ - private $id; - - /** - * @ODM\Field - */ - public $description; - - /** - * @ODM\Field(type="float") - * @Assert\NotBlank - * @Assert\Range(min=0, minMessage="The price must be superior to 0.") - * @Assert\Type(type="float") - */ - public $price; - - /** - * @ODM\ReferenceOne(targetDocument=Product::class, inversedBy="offers", storeAs="id") - */ - public $product; + #[ODM\Id(strategy: 'INCREMENT', type: 'int')] + private int $id; + + #[ODM\Field] + public string $description; + + #[ODM\Field(type: 'float')] + #[Assert\Range(min: 0, minMessage: 'The price must be superior to 0.')] + #[Assert\Type(type: 'float')] + public float $price; + + #[ODM\ReferenceOne(targetDocument: Product::class, inversedBy: 'offers', storeAs: 'id')] + public ?Product $product; public function getId(): ?int { @@ -234,9 +201,7 @@ use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\GetCollection; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; -/** - * @ODM\Document - */ +#[ODM\Document] #[ApiResource] #[GetCollection(extraProperties: ['doctrineMongodb' => ['execute_options' => ['allowDiskUse' => true]]])] class Offer @@ -256,9 +221,7 @@ namespace App\Document; use ApiPlatform\Metadata\ApiResource; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; -/** - * @ODM\Document - */ +#[ODM\Document] #[ApiResource(extraProperties: ['doctrineMongodb' => ['execute_options' => ['allowDiskUse' => true]]])] class Offer { From ebf8ba8685841f30c4f5eeabec01bd6379a75e00 Mon Sep 17 00:00:00 2001 From: Anton Bielykh Date: Fri, 6 Dec 2024 00:29:09 -0700 Subject: [PATCH 46/80] Fix Creating Custom Elasticsearch Filters documentation typo --- core/filters.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/filters.md b/core/filters.md index 0bb76184480..5c74d310236 100644 --- a/core/filters.md +++ b/core/filters.md @@ -411,7 +411,7 @@ If you need more information about creating custom filters, refer to the followi - [Creating Custom Doctrine ORM filters](../core/doctrine-filters.md#creating-custom-doctrine-orm-filters) - [Creating Custom Doctrine Mongo ODM filters](../core/doctrine-filters.md#creating-custom-doctrine-mongodb-odm-filters) -- [Creating Custom Doctrine ORM filters](../core/elasticsearch-filters.md#creating-custom-elasticsearch-filters) +- [Creating Custom Elasticsearch Filters](../core/elasticsearch-filters.md#creating-custom-elasticsearch-filters) ## ApiFilter Attribute From 446100680be55e215ae062d9ce96fc3310f462ad Mon Sep 17 00:00:00 2001 From: Vincent Amstoutz Date: Fri, 6 Dec 2024 09:39:22 +0100 Subject: [PATCH 47/80] fix: typo in alert warning message (#2086) --- 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 3e2b9d76461..7186492ff74 100644 --- a/core/doctrine-filters.md +++ b/core/doctrine-filters.md @@ -2,7 +2,7 @@ For further documentation on filters (including for Eloquent and Elasticsearch), please see the [Filters documentation](filters.md). -!> [!WARNING] +> [!WARNING] > Prefer using QueryParameter instead of ApiFilter for more flexibility, this is subject to change in the next major version. ## Basic Knowledge From 22d10d40a19f22b74ebb5f2114006604db210518 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Fri, 6 Dec 2024 16:36:24 +0100 Subject: [PATCH 48/80] Replace const with property in state provider examples MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "New expressions are not supported in this context\” --- core/state-providers.md | 43 ++++++++++++++++++----------------------- 1 file changed, 19 insertions(+), 24 deletions(-) diff --git a/core/state-providers.md b/core/state-providers.md index 6faa21f0c5e..7f16c32f8a3 100644 --- a/core/state-providers.md +++ b/core/state-providers.md @@ -58,19 +58,23 @@ use ApiPlatform\State\ProviderInterface; */ final class BlogPostProvider implements ProviderInterface { - private const DATA = [ - 'ab' => new BlogPost('ab'), - 'cd' => new BlogPost('cd'), - ]; + 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 self::DATA[$uriVariables['id']] ?? null; + return $this->data[$uriVariables['id']] ?? null; } } ``` -For the example, we store the list of our blog posts in an associative array (the `BlogPostProvider::DATA` constant). +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`. @@ -110,18 +114,15 @@ use ApiPlatform\Metadata\CollectionOperationInterface; */ final class BlogPostProvider implements ProviderInterface { - private const DATA = [ - 'ab' => new BlogPost('ab'), - 'cd' => new BlogPost('cd'), - ]; + private array $data; public function provide(Operation $operation, array $uriVariables = [], array $context = []): iterable|BlogPost|null { if ($operation instanceof CollectionOperationInterface) { - return self::DATA; + return $this->data; } - return self::DATA[$uriVariables['id']] ?? null; + return $this->data[$uriVariables['id']] ?? null; } } ``` @@ -169,19 +170,16 @@ use ApiPlatform\State\ProviderInterface; */ final class BlogPostProvider implements ProviderInterface { - private const DATA = [ - 'ab' => new BlogPost('ab'), - 'cd' => new BlogPost('cd'), - ]; + private array $data; public function provide(Operation $operation, array $uriVariables = [], array $context = []): BlogPost|null { - return self::DATA[$uriVariables['id']] ?? null; + return $this->data[$uriVariables['id']] ?? null; } } ``` -For the example, we store the list of our blog posts in an associative array (the `BlogPostProvider::DATA` constant). +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`. @@ -221,18 +219,15 @@ use ApiPlatform\Metadata\CollectionOperationInterface; */ final class BlogPostProvider implements ProviderInterface { - private const DATA = [ - 'ab' => new BlogPost('ab'), - 'cd' => new BlogPost('cd'), - ]; + private array $data; public function provide(Operation $operation, array $uriVariables = [], array $context = []): iterable|BlogPost|null { if ($operation instanceof CollectionOperationInterface) { - return self::DATA; + return $this->data; } - return self::DATA[$uriVariables['id']] ?? null; + return $this->data[$uriVariables['id']] ?? null; } } ``` From 041b4c5e8bbc3cf05b1d7c591e23582b266b9faa Mon Sep 17 00:00:00 2001 From: Vincent Amstoutz Date: Mon, 9 Dec 2024 09:15:47 +0100 Subject: [PATCH 49/80] docs(serialization): support for laravel and fix image (#2039) --- core/serialization.md | 162 ++++++++++++++++++++++++++---------------- 1 file changed, 100 insertions(+), 62 deletions(-) diff --git a/core/serialization.md b/core/serialization.md index 6c52918388a..2701a43f936 100644 --- a/core/serialization.md +++ b/core/serialization.md @@ -8,14 +8,17 @@ API Platform embraces and extends the Symfony Serializer Component to transform The main serialization process has two stages: -![Serializer workflow](../core/images/SerializerWorkflow.png) +![Serializer workflow](images/SerializerWorkflow.png) > As you can see in the picture above, an array is used as a man-in-the-middle. This way, Encoders will only deal with turning specific formats into arrays and vice versa. The same way, Normalizers will deal with turning specific objects into arrays and vice versa. > -- [The Symfony documentation](https://symfony.com/doc/current/components/serializer.html) -Unlike Symfony itself, 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 @@ -41,7 +44,7 @@ feature of the Symfony Serializer component. In addition to groups, you can use any option supported by the Symfony Serializer. For example, you can use [`enable_max_depth`](https://symfony.com/doc/current/components/serializer.html#handling-serialization-depth) to limit the serialization depth. -### Configuration +### 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. @@ -71,15 +74,16 @@ 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. ```php - + @@ -146,14 +153,15 @@ App\Entity\Book: - + + - + read write @@ -184,6 +192,12 @@ documentation generator. ## Using Serialization Groups per Operation +

Relations screencast
Watch the Relations screencast

+ +By default, the serializer provided with API Platform represents relations between objects using [dereferenceable IRIs](https://en.wikipedia.org/wiki/Internationalized_Resource_Identifier). +They allow you to retrieve details for related objects by issuing extra HTTP requests. However, for performance reasons, +it is sometimes preferable to avoid forcing the client to issue extra HTTP requests. + It is possible to specify normalization and denormalization contexts (as well as any other attribute) on a per-operation basis. API Platform will always use the most specific definition. For instance, if normalization groups are set both at the resource level and at the operation level, the configuration set at the operation level will be used and the resource @@ -195,8 +209,8 @@ In the following example we use different serialization groups for the `GET` and ```php - + @@ -272,13 +289,14 @@ App\Entity\Book: + - + get patch @@ -330,8 +348,8 @@ the `book` group, the author will be embedded. ```php -### Plain Identifiers +### 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: @@ -577,8 +603,8 @@ Instead of sending an IRI to set a relation, you may want to send a plain identi namespace App\Serializer; use ApiPlatform\Api\IriConverterInterface; -use App\Entity\Dummy; -use App\Entity\RelatedDummy; +use App\ApiResource\Dummy; +use App\ApiResource\RelatedDummy; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface; use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait; @@ -617,7 +643,7 @@ class PlainIdentifierDenormalizer implements DenormalizerInterface, Denormalizer } ``` -## Property Normalization Context +## 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. @@ -708,7 +734,7 @@ class Book } ``` -## Calculated Field +## 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. @@ -755,6 +781,7 @@ class Greeting ``` ```yaml +# The YAML syntax is only supported for Symfony # api/config/api_platform/resources/Greeting.yaml App\Entity\Greeting: operations: @@ -762,6 +789,7 @@ App\Entity\Greeting: normalizationContext: groups: 'greeting:collection:get' +# The YAML syntax is only supported for Symfony # api/config/serializer/Greeting.yaml App\Entity\Greeting: attributes: @@ -785,8 +813,8 @@ Let's imagine a resource where most fields can be managed by any user, but some ```php + - + ``` @@ -1140,7 +1177,7 @@ In some cases, you will want to set the identifier of a resource from the client 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 (e.g. `public function setId(string $id)`) or make it a `public` property , +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 @@ -1167,8 +1204,8 @@ attribute to the `#[ApiResource]` annotation: ```php getValues()`. Thanks to this, the relation is now a real array which is sequentially indexed. From 744130ca51aa1415fa1c5414dbbcfdd3b7cc3456 Mon Sep 17 00:00:00 2001 From: Vincent Amstoutz Date: Mon, 9 Dec 2024 14:35:02 +0100 Subject: [PATCH 50/80] docs(fosrestbundle): move from core to symfony and update (#2100) --- core/getting-started.md | 7 +++-- outline.yaml | 2 +- .../migrate-from-fosrestbundle.md | 28 ++++++++++--------- 3 files changed, 20 insertions(+), 17 deletions(-) rename {core => symfony}/migrate-from-fosrestbundle.md (82%) diff --git a/core/getting-started.md b/core/getting-started.md index 2f66b7d36ac..7ea79cd42d5 100644 --- a/core/getting-started.md +++ b/core/getting-started.md @@ -11,9 +11,9 @@ You can choose your preferred stack between Symfony, Laravel, or bootstrapping t 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/), +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-utilities.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. @@ -30,7 +30,8 @@ There are no mandatory configuration options although [many settings are availab ### Migrating from FOSRestBundle -If you plan to migrate from FOSRestBundle, you might want to read [this guide](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 diff --git a/outline.yaml b/outline.yaml index 6456aeef13c..e76d8dcba6f 100644 --- a/outline.yaml +++ b/outline.yaml @@ -9,6 +9,7 @@ chapters: - testing - debugging - caddy + - migrate-from-fosrestbundle - title: "API Platform for Laravel" path: laravel items: @@ -68,7 +69,6 @@ chapters: - nelmio-api-doc - bootstrap - configuration - - migrate-from-fosrestbundle - title: Schema Generator path: schema-generator items: diff --git a/core/migrate-from-fosrestbundle.md b/symfony/migrate-from-fosrestbundle.md similarity index 82% rename from core/migrate-from-fosrestbundle.md rename to symfony/migrate-from-fosrestbundle.md index d1ad2ac2ce5..ed58c337b36 100644 --- a/core/migrate-from-fosrestbundle.md +++ b/symfony/migrate-from-fosrestbundle.md @@ -1,9 +1,11 @@ -# Migrate From FOSRestBundle +# Migrate From FOSRestBundle with Symfony [FOSRestBundle](https://github.com/FriendsOfSymfony/FOSRestBundle) is a popular bundle to rapidly develop RESTful APIs with Symfony. This page provides a guide to help developers migrate from FOSRestBundle to API Platform. -[On 21 September 2021](https://x.com/lsmith/status/1440216817876627459), FOSRestBundle's creators recommended to use API Platform. +> [!IMPORTANT] +> Since [2021](https://x.com/lsmith/status/1440216817876627459), the creators of FOSRestBundle have recommended +> transitioning to **API Platform** as the preferred solution **for building modern APIs**. ## Features Comparison @@ -21,7 +23,7 @@ See [The view layer](https://github.com/FriendsOfSymfony/FOSRestBundle/blob/3.x/ Add the `ApiResource` attribute to your entities, and enable the operations you desire inside. By default, every operation is activated. -See [Operations](operations.md). +See [Operations](../core/operations.md). ### Make custom controllers @@ -31,11 +33,11 @@ Same as above. **In API Platform** -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](../core/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](../core/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](design.md). +See [General Design Considerations](../core/design.md). ### Routing system (with native documentation support) @@ -49,7 +51,7 @@ See [Full default annotations](https://github.com/FriendsOfSymfony/FOSRestBundle Use the `ApiResource` attribute to activate the HTTP methods you need for your entity. By default, all the methods are enabled. -See [Operations](operations.md). +See [Operations](../core/operations.md). ### Hook into the handling of the requests @@ -63,7 +65,7 @@ See [Listener support](https://github.com/FriendsOfSymfony/FOSRestBundle/blob/3. 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](extending.md) for more details. +See [Extending API Platform](../core/extending.md) for more details. ### Customize the formats of the requests and the responses @@ -81,7 +83,7 @@ Both the request and the response body's format can be customized. You can configure the formats of the API either globally or in specific resources or operations. API Platform provides native support for multiple formats including JSON, XML, CSV, YAML, etc. -See [Content negotiation](content-negotiation.md). +See [Content negotiation](../core/content-negotiation.md). ### Name conversion @@ -99,7 +101,7 @@ Both request and response bodies can be converted. API Platform uses [name converters](https://symfony.com/doc/current/components/serializer.html#component-serializer-converting-property-names-when-serializing-and-deserializing) included in the Serializer component of Symfony. You can create your own by implementing the `NameConverterInterface` provided by Symfony. -See [_Name Conversion_ in The Serialization Process](serialization.md#name-conversion). +See [_Name Conversion_ in The Serialization Process](../core/serialization.md#name-conversion-for-symfony). ### Handle errors @@ -113,7 +115,7 @@ See [ExceptionController support](https://github.com/FriendsOfSymfony/FOSRestBun Map the exceptions to HTTP statuses in the `api_platform.exception_to_status` parameter. -See [Errors Handling](errors.md). +See [Errors Handling](../core/errors.md). ### Security @@ -127,7 +129,7 @@ Use the `security` attribute in the `ApiResource` and `ApiProperty` attributes. Note you can also use the `security.yml` file if you only need to limit access to specific roles. -See [Security](security.md). +See [Security](../core/security.md). ### API versioning @@ -141,4 +143,4 @@ See [API versioning](https://github.com/FriendsOfSymfony/FOSRestBundle/blob/3.x/ 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](deprecations.md). +See [Deprecating Resources and Properties](../core/deprecations.md). From 3a15fdf4d0c5ab4966b71a5f733fa7e365ff8948 Mon Sep 17 00:00:00 2001 From: Vincent Amstoutz Date: Mon, 9 Dec 2024 14:38:26 +0100 Subject: [PATCH 51/80] docs(fosuser-bundle): move from core to symfony and update (#2099) --- core/jwt.md | 2 +- outline.yaml | 2 +- {core => symfony}/fosuser-bundle.md | 23 ++++++++++++++--------- 3 files changed, 16 insertions(+), 11 deletions(-) rename {core => symfony}/fosuser-bundle.md (61%) diff --git a/core/jwt.md b/core/jwt.md index 2ecf6a633cc..8f619ba3aeb 100644 --- a/core/jwt.md +++ b/core/jwt.md @@ -65,7 +65,7 @@ We're not done yet! Let's move on to configuring the Symfony SecurityBundle for 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](fosuser-bundle.md) (not recommended). +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). diff --git a/outline.yaml b/outline.yaml index e76d8dcba6f..0e36b3614b9 100644 --- a/outline.yaml +++ b/outline.yaml @@ -10,6 +10,7 @@ chapters: - debugging - caddy - migrate-from-fosrestbundle + - fosuser-bundle - title: "API Platform for Laravel" path: laravel items: @@ -65,7 +66,6 @@ chapters: - user - form-data - angularjs-integration - - fosuser-bundle - nelmio-api-doc - bootstrap - configuration diff --git a/core/fosuser-bundle.md b/symfony/fosuser-bundle.md similarity index 61% rename from core/fosuser-bundle.md rename to symfony/fosuser-bundle.md index dc5cbdb8c81..ce8378ad8b4 100644 --- a/core/fosuser-bundle.md +++ b/symfony/fosuser-bundle.md @@ -1,15 +1,20 @@ -# FOSUserBundle Integration +# FOSUserBundle Integration with Symfony + +> [!WARNING] +> The use of FOSUserBundle is no longer recommended for better flexibility and security. It is advised to switch to the +> [Doctrine entity user provider](https://symfony.com/doc/current/security/user_provider.html#entity-user-provider) (recommended) +> or consider [creating a custom user provider](https://symfony.com/doc/current/security/user_provider.html#creating-a-custom-user-provider). ## Installing the Bundle -The installation procedure of the FOSUserBundle is described [in the main Symfony docs](https://symfony.com/doc/master/bundles/FOSUserBundle/index.html) +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://symfony.com/doc/master/bundles/FOSUserBundle/index.html#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://symfony.com/doc/master/bundles/FOSUserBundle/index.html#step-4-configure-your-application-s-security-yml) - if you are planning to [use a JWT-based authentication using `LexikJWTAuthenticationBundle`](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: @@ -22,11 +27,11 @@ framework: ## 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/Resources/doc/index.rst#a-doctrine-orm-user-class). -There's also an example for a [Doctrine MongoDB ODM](https://github.com/FriendsOfSymfony/FOSUserBundle/blob/master/Resources/doc/index.rst#b-mongodb-user-class). +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`](serialization.md#normalization), while the properties -you can modify are handled with [`denormalizationContext`](serialization.md#denormalization). +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: From 2eedbd8860b1217eaf9fc9a5c4b54daf9d0da5d6 Mon Sep 17 00:00:00 2001 From: Vincent Amstoutz Date: Mon, 9 Dec 2024 14:42:18 +0100 Subject: [PATCH 52/80] docs(angularjs): remove legacy integration (#2102) --- core/angularjs-integration.md | 98 ----------------------------------- outline.yaml | 1 - 2 files changed, 99 deletions(-) delete mode 100644 core/angularjs-integration.md diff --git a/core/angularjs-integration.md b/core/angularjs-integration.md deleted file mode 100644 index a0147a9f2de..00000000000 --- a/core/angularjs-integration.md +++ /dev/null @@ -1,98 +0,0 @@ -# Angular Integration - -Warning: for a new project, you should consider using [the API Platform's Progressive Web App generator](../create-client/index.md) -(that supports React and Vue.js) instead of this Angular v1 integration. - -## Restangular - -API Platform works fine with [Angular v1](https://angularjs.org/). The popular [Restangular](https://github.com/mgonto/restangular) -REST client library for Angular can easily be configured to handle the API format. - -Here is a working Restangular config: - -```javascript -'use strict'; - -var app = angular.module('myAngularjsApp').config([ - 'RestangularProvider', - function (RestangularProvider) { - // The URL of the API endpoint - RestangularProvider.setBaseUrl('http://localhost:8000'); - - // JSON-LD @id support - RestangularProvider.setRestangularFields({ - id: '@id', - selfLink: '@id', - }); - RestangularProvider.setSelfLinkAbsoluteUrl(false); - - // Hydra collections support - RestangularProvider.addResponseInterceptor(function (data, operation) { - // Remove trailing slash to make Restangular working - function populateHref(data) { - if (data['@id']) { - data.href = data['@id'].substring(1); - } - } - - // Populate href property for the collection - populateHref(data); - - if ('getList' === operation) { - var collectionResponse = data['member']; - collectionResponse.metadata = {}; - - // Put metadata in a property of the collection - angular.forEach(data, function (value, key) { - if ('member' !== key) { - collectionResponse.metadata[key] = value; - } - }); - - // Populate href property for all elements of the collection - angular.forEach(collectionResponse, function (value) { - populateHref(value); - }); - - return collectionResponse; - } - - return data; - }); - }, -]); -``` - -## ng-admin - -If you want to use [ng-admin](https://github.com/marmelab/ng-admin), set the [Restangular](#restangular) config, -then create your entities like in the following example : - -```javascript -'use strict'; - -var nga = NgAdminConfigurationProvider; - -var admin = nga - .application('My First Admin') - .baseApiUrl('http://localhost:8000'); - -var article = nga.entity('articles'); -article.identifier(nga.field('@id')); -article.url(function (entityName, viewType, identifierValue) { - var url = '/' + entityName; - - if (viewType === 'ListView' || viewType === 'CreateView') { - return url; - } - - return identifierValue ? decodeURIComponent(identifierValue) : url; -}); - -article.listView().fields([nga.field('title'), nga.field('content')]); - -admin.addEntity(article); -nga.configure(admin); -``` - -You can look at what we have done on [api-platform/admin](https://github.com/api-platform/admin). diff --git a/outline.yaml b/outline.yaml index 0e36b3614b9..74068c2fe34 100644 --- a/outline.yaml +++ b/outline.yaml @@ -65,7 +65,6 @@ chapters: - jwt - user - form-data - - angularjs-integration - nelmio-api-doc - bootstrap - configuration From 4f05ac1f516e21dbb83b616fb61465d5605a8aaa Mon Sep 17 00:00:00 2001 From: Vincent Amstoutz Date: Mon, 9 Dec 2024 14:45:05 +0100 Subject: [PATCH 53/80] docs(nelmio): move from core to symfony and update (#2101) --- outline.yaml | 2 +- {core => symfony}/images/NelmioApiDocBundle.png | Bin {core => symfony}/nelmio-api-doc.md | 9 +++++---- 3 files changed, 6 insertions(+), 5 deletions(-) rename {core => symfony}/images/NelmioApiDocBundle.png (100%) rename {core => symfony}/nelmio-api-doc.md (82%) diff --git a/outline.yaml b/outline.yaml index 74068c2fe34..b27f181c88e 100644 --- a/outline.yaml +++ b/outline.yaml @@ -11,6 +11,7 @@ chapters: - caddy - migrate-from-fosrestbundle - fosuser-bundle + - nelmio-api-doc - title: "API Platform for Laravel" path: laravel items: @@ -65,7 +66,6 @@ chapters: - jwt - user - form-data - - nelmio-api-doc - bootstrap - configuration - title: Schema Generator diff --git a/core/images/NelmioApiDocBundle.png b/symfony/images/NelmioApiDocBundle.png similarity index 100% rename from core/images/NelmioApiDocBundle.png rename to symfony/images/NelmioApiDocBundle.png diff --git a/core/nelmio-api-doc.md b/symfony/nelmio-api-doc.md similarity index 82% rename from core/nelmio-api-doc.md rename to symfony/nelmio-api-doc.md index 1fb8944ad08..b9d86373982 100644 --- a/core/nelmio-api-doc.md +++ b/symfony/nelmio-api-doc.md @@ -1,12 +1,13 @@ -# NelmioApiDocBundle Integration +# NelmioApiDocBundle Integration with Symfony -NelmioApiDoc provides an alternative to [the native Swagger/Open API support](openapi.md) provided by API Platform. +> [!WARNING] +> For new projects, prefer using the built-in Swagger support and/or NelmioApiDoc 3. + +NelmioApiDoc provides an alternative to [the native Swagger/Open API support](../core/openapi.md) provided by API Platform. As NelmioApiDocBundle 3+ has built-in support for API Platform, this documentation is only relevant for people using NelmioApiDocBundle between version 2.9 and 3.0. -For new projects, prefer using the built-in Swagger support and/or NelmioApiDoc 3. - ![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. From 2a8a1af05934b33dc6dadb96a04fd36c99cf9a19 Mon Sep 17 00:00:00 2001 From: Vincent Amstoutz Date: Mon, 9 Dec 2024 14:45:43 +0100 Subject: [PATCH 54/80] docs(form-data): support for laravel and update (#2098) --- core/form-data.md | 158 ++++++++++++++++++++++++++++++---------------- 1 file changed, 105 insertions(+), 53 deletions(-) diff --git a/core/form-data.md b/core/form-data.md index d1e8e04fd1c..bbbd9f640ff 100644 --- a/core/form-data.md +++ b/core/form-data.md @@ -1,91 +1,143 @@ # Accept `application/x-www-form-urlencoded` Form Data -API Platform only supports raw documents as request input (encoded in JSON, XML, YAML...). This has many advantages including support of types and the ability to send back to the API documents originally retrieved through a `GET` request. -However, sometimes - for instance, to support legacy clients - it is necessary to accept inputs encoded in the traditional [`application/x-www-form-urlencoded`](https://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.1) format (HTML form content type). This can easily be done using [the powerful event system](events.md) of the framework. - -**⚠ Adding support for `application/x-www-form-urlencoded` makes your API vulnerable to [CSRF attacks](). Be sure to enable proper countermeasures [such as DunglasAngularCsrfBundle](https://github.com/dunglas/DunglasAngularCsrfBundle).** +API Platform only supports raw documents as request input (encoded in JSON, XML, YAML...). This has many advantages +including support of types and the ability to send back to the API documents originally retrieved through a `GET` request. +However, sometimes - for instance, to support legacy clients - it is necessary to accept inputs encoded in the traditional +[`application/x-www-form-urlencoded`](https://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.1) format +(HTML form content type). This can easily be done using the powerful [System providers and processors](extending.md#system-providers-and-processors) +of the framework. + +> [!WARNING] +> Adding support for `application/x-www-form-urlencoded` makes your API vulnerable to [CSRF (Cross-Site Request Forgery)](https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)) attacks. +> It's crucial to implement proper countermeasures to protect your application. +> +> If you're using Symfony, make sure you enable [Stateless CSRF protection](https://symfony.com/blog/new-in-symfony-7-2-stateless-csrf). +> +> If you're working with Laravel, refer to the [Laravel CSRF documentation](https://laravel.com/docs/csrf) to ensure +> adequate protection against such attacks. 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. -## Create your `DeserializeListener` Decorator +## Create your `FormRequestProcessorDecorator` processor 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 denormalizer = $denormalizer; - $this->serializerContextBuilder = $serializerContextBuilder; - $this->decorated = $decorated; - } - - public function onKernelRequest(RequestEvent $event): void { - $request = $event->getRequest(); - if ($request->isMethodCacheable(false) || $request->isMethod(Request::METHOD_DELETE)) { - return; + // If the content type is form data, we process it separately + if ('form' === $data->getContentType()) { + return $this->handleFormRequest($data); } - if ('form' === $request->getContentType()) { - $this->denormalizeFormRequest($request); - } else { - $this->decorated->onKernelRequest($event); - } + // Delegate the processing to the original processor for other cases + return $this->decorated->process($data, $operation, $uriVariables, $context); } - private function denormalizeFormRequest(Request $request): void + /** + * Handle form requests by deserializing the data into the correct entity + */ + private function handleFormRequest(Request $request) { - if (!$attributes = RequestAttributesExtractor::extractAttributes($request)) { - return; + $attributes = $request->attributes->get('_api_attributes'); + if (!$attributes) { + return null; } $context = $this->serializerContextBuilder->createFromRequest($request, false, $attributes); - $populated = $request->attributes->get('data'); - if (null !== $populated) { - $context['object_to_populate'] = $populated; - } + // Deserialize the form data into an entity $data = $request->request->all(); - $object = $this->denormalizer->denormalize($data, $attributes['resource_class'], null, $context); - $request->attributes->set('data', $object); + + return $this->denormalizer->denormalize($data, 'App\Entity\SomeEntity', null, $context); } } ``` -## Creating the Service Definition +Next, configure the `FormRequestProcessorDecorator` according to whether you're using Symfony or Laravel, as shown below: + +### Creating the Service Definition using Symfony ```yaml # api/config/services.yaml services: # ... - 'App\EventListener\DeserializeListener': - tags: - - { - name: 'kernel.event_listener', - event: 'kernel.request', - method: 'onKernelRequest', - priority: 2, - } - # Autoconfiguration must be disabled to set a custom priority - autoconfigure: false - decorates: 'api_platform.listener.request.deserialize' - arguments: - $decorated: '@App\EventListener\DeserializeListener.inner' + App\State\FormRequestProcessorDecorator: + decorates: api_platform.state.processor + arguments: + $decorated: '@App\State\FormRequestProcessorDecorator.inner' + $denormalizer: '@serializer' + $serializerContextBuilder: '@api_platform.serializer.context_builder' + tags: + - { name: 'api_platform.state.processor' } +``` + +### Registering a Decorated Processor using Laravel + +```php +app->bind(ProcessorInterface::class, function ($app) { + $decoratedProcessor = $app->make(ProcessorInterface::class); + + return new FormRequestProcessorDecorator( + $decoratedProcessor, + $app->make(DenormalizerInterface::class), + $app->make(SerializerContextBuilderInterface::class) + ); + }); + } +} +``` + +## Using your `FormRequestProcessorDecorator` processor + +Finally, you can use the processor in your API Resource like this: + +```php + Date: Mon, 9 Dec 2024 14:46:20 +0100 Subject: [PATCH 55/80] docs(elasticsearch): compatibility with laravel and improve (#2092) --- core/elasticsearch.md | 37 ++++++++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/core/elasticsearch.md b/core/elasticsearch.md index 7e5f931720b..cfd334f7b41 100644 --- a/core/elasticsearch.md +++ b/core/elasticsearch.md @@ -18,7 +18,9 @@ To enable the reading support for Elasticsearch, simply require the Elasticsearc composer require elasticsearch/elasticsearch:^7.11 ``` -Then, enable it inside the API Platform configuration: +Then, enable it inside the API Platform configuration, using one of the configurations below: + +### Enabling Reading Support using Symfony ```yaml # api/config/packages/api_platform.yaml @@ -38,12 +40,32 @@ api_platform: #... ``` +### Enabling Reading Support using Laravel + +```php + [ + 'paths' => [ + base_path('app/Models'), + ], + ], + 'elasticsearch' => [ + 'hosts' => [ + env('ELASTICSEARCH_HOST', 'http://localhost:9200'), + ], + ], +]; +``` + ## Creating Models API Platform follows the best practices of Elasticsearch: -- a single index per resource should be used because Elasticsearch is going to [drop support for index types and will allow only a single type per - index](https://www.elastic.co/guide/en/elasticsearch/reference/current/removal-of-types.html); +- 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. @@ -143,7 +165,7 @@ Here is an example of mappings for 2 resources, `User` and `Tweet`, and their mo ```php Date: Mon, 9 Dec 2024 14:48:46 +0100 Subject: [PATCH 56/80] docs(identifiers): compatibility with laravel (#2091) --- core/identifiers.md | 55 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 42 insertions(+), 13 deletions(-) diff --git a/core/identifiers.md b/core/identifiers.md index 2b47d5e4209..8e8e9a31f27 100644 --- a/core/identifiers.md +++ b/core/identifiers.md @@ -13,8 +13,8 @@ Let's say you have the following class, which is identified by a `UUID` type. In ```php - + - + ```
-Once registered as an `ApiResource`, having an existing person, it will be accessible through the following URL: `/people/110e8400-e29b-11d4-a716-446655440000`. -Note that the property identifying our resource is named `code`. +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` entity: +Let's create a `Provider` for the `Person` resource: ```php ```yaml # api/config/services.yaml +# The YAML syntax is only supported for Symfony services: App\Identifier\UuidUriVariableTransformer: @@ -153,6 +158,8 @@ services: ``` ```xml + + @@ -162,6 +169,28 @@ services: Your `PersonProvider` will now work as expected! +### Tag the Service using Laravel + +```php +app->tag([UuidUriVariableTransformer::class], UriVariableTransformerInterface::class); + } +} +``` + +Your `PersonProvider` will now work as expected! + ## Changing Identifier in a Doctrine Entity If your resource is also a Doctrine entity and you want to use another identifier other than the Doctrine one, you have to unmark it: From 7d0f6c7aef9c4b7fe83852cfc62e3d53af9e56f9 Mon Sep 17 00:00:00 2001 From: Vincent Amstoutz Date: Mon, 9 Dec 2024 14:49:43 +0100 Subject: [PATCH 57/80] docs(extending-jsonld-context): compatibility with laravel (#2090) --- core/extending-jsonld-context.md | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/core/extending-jsonld-context.md b/core/extending-jsonld-context.md index aa595bfdd98..1c341c08ec8 100644 --- a/core/extending-jsonld-context.md +++ b/core/extending-jsonld-context.md @@ -5,13 +5,14 @@

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. +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 + - + From 192944e00b409903a26fad65e5c84e5d82896c64 Mon Sep 17 00:00:00 2001 From: Vincent Amstoutz Date: Mon, 9 Dec 2024 14:54:01 +0100 Subject: [PATCH 58/80] docs(operation-path-naming): compatibility with laravel (#2088) --- core/operation-path-naming.md | 37 +++++++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/core/operation-path-naming.md b/core/operation-path-naming.md index 1433933ae3a..def62e3426b 100644 --- a/core/operation-path-naming.md +++ b/core/operation-path-naming.md @@ -8,11 +8,14 @@ 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` | The default resolver is `api_platform.metadata.path_segment_name_generator.underscore`. + +### Configuration using Symfony + To change it to the dash resolver, add the following lines to `api/config/packages/api_platform.yaml`: ```yaml @@ -21,6 +24,19 @@ api_platform: path_segment_name_generator: api_platform.metadata.path_segment_name_generator.dash ``` +### Configuration using Laravel + +To change it to the dash resolver, add the following lines to `config/api-platform.php`: + +```php + 'api_platform.metadata.path_segment_name_generator.dash' +]; +``` + ## Create a Custom Operation Path Resolver Let's assume we need URLs without separators (e.g. `api.tld/myresources`) @@ -31,7 +47,7 @@ Make sure the custom segment generator implements [`ApiPlatform\Metadata\Operati ```php App\Operation\SingularPathSegmentNameGenerator::class +]; +``` From 48149082d2a5396b4dd680be39a1562eed8b8e1f Mon Sep 17 00:00:00 2001 From: Vincent Amstoutz Date: Mon, 9 Dec 2024 14:54:41 +0100 Subject: [PATCH 59/80] docs(external-vocabularies): compatibility with laravel (#2087) --- core/external-vocabularies.md | 57 ++++++++++++++++++++++------------- 1 file changed, 36 insertions(+), 21 deletions(-) diff --git a/core/external-vocabularies.md b/core/external-vocabularies.md index 3abf7cf5449..593430fa891 100644 --- a/core/external-vocabularies.md +++ b/core/external-vocabularies.md @@ -7,8 +7,8 @@ API Platform provides attributes usable on PHP classes and properties for specif ```php Date: Mon, 9 Dec 2024 14:55:17 +0100 Subject: [PATCH 60/80] docs(errors): compatibility with laravel and improve (#2084) --- core/errors.md | 87 +++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 69 insertions(+), 18 deletions(-) diff --git a/core/errors.md b/core/errors.md index a65c33c4065..f0ca272931b 100644 --- a/core/errors.md +++ b/core/errors.md @@ -4,7 +4,7 @@ API Platform comes with a powerful error system. It handles expected (such as fa client or validation errors) as well as unexpected errors (PHP exceptions and errors). API Platform automatically sends the appropriate HTTP status code to the client: `400` for expected errors, `500` for unexpected ones. It also provides a description of the error in [the Hydra error format](https://www.hydra-cg.com/spec/latest/core/#description-of-http-status-codes-and-errors) -or in the format described in the [RFC 7807](https://tools.ietf.org/html/rfc7807), depending of the format selected during the [content negotiation](content-negotiation.md). +or in the format described in the [RFC 7807](https://tools.ietf.org/html/rfc7807), depending on the format selected during the [content negotiation](content-negotiation.md). ## Backward compatibility with < 3.1 @@ -25,16 +25,33 @@ This can also be configured on an `ApiResource` or in an `HttpOperation`, for ex ## Exception status code decision -There are many ways of configuring the exception status code we recommend reading the guides on how to use an [Error Provider](https://api-platform.com/docs/guides/error-provider/) or create an [Error Resource](https://api-platform.com/docs/guides/error-resource/). +There are many ways of configuring the exception status code we recommend reading the guides on how to use an +[Error Provider](https://api-platform.com/docs/guides/error-provider/) or create an [Error Resource](https://api-platform.com/docs/guides/error-resource/). -1. we look at `exception_to_status` and take one if there's a match +The decision works like this, if you are using API Platform with Symfony: + +1. We look at `exception_to_status` and take one if there's a match 2. If your exception is a `Symfony\Component\HttpKernel\Exception\HttpExceptionInterface` we get its status. 3. If the exception is a `ApiPlatform\Metadata\Exception\ProblemExceptionInterface` and there is a status we use it 4. Same for `ApiPlatform\Metadata\Exception\HttpExceptionInterface` -5. We have some defaults `Symfony\Component\HttpFoundation\Exception\RequestExceptionInterface` => 400 and `ApiPlatform\Validator\Exception\ValidationException` => 422 -6. the status defined on an `ErrorResource` +5. Use defaults for the following exceptions: + - `Symfony\Component\HttpFoundation\Exception\RequestExceptionInterface` => 400 + - `ApiPlatform\Symfony\Validator\Exception\ValidationException` => 422 +6. The status defined on an `ErrorResource` 7. 500 is the fallback +And like this, if you are using API Platform with Laravel: + +1. Check an `exception_to_status` array and use its value if a match is found. +2. If the exception implements `Illuminate\Contracts\Http\Exception\HttpResponseException`, retrieve its HTTP status. +3. If the exception implements `App\Contracts\Exceptions\ProblemExceptionInterface` and a status is defined, use it. +4. Similarly, check for `App\Contracts\Exceptions\HttpExceptionInterface`. +5. Use defaults for the following exceptions: + - `Illuminate\Http\Exceptions\HttpResponseException` => 400 + - `ApiPlatform\Symfony\Validator\Exception\ValidationException` => 422 +6. The status defined on an `ErrorResource` +7. Fallback to 500. + ## Exception to status The framework also allows you to configure the HTTP status code sent to the clients when custom exceptions are thrown @@ -45,7 +62,7 @@ configure API Platform to convert it to a `404 Not Found` error: ```php [ + // The 3 following handlers are registered by default, keep those lines to prevent unexpected side effects + Symfony\Component\Serializer\Exception\ExceptionInterface::class => 400, + ApiPlatform\Exception\InvalidArgumentException::class => Illuminate\Http\Response::HTTP_BAD_REQUEST, + ApiPlatform\ParameterValidator\Exception\ValidationExceptionInterface => 400, + + //Validation exception + ApiPlatform\Validator\Exception\ValidationException::class => Illuminate\Http\Response::HTTP_UNPROCESSABLE_ENTITY, + + //Custom mapping + App\Exception\ProductNotFoundException::class => 404 // Here is the handler for our custom exception + ], +]; +``` + +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` @@ -133,7 +176,9 @@ the error will be returned in this format as well: ### 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. +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. @@ -143,8 +188,8 @@ The `exceptionToStatus` configuration can be set on resources and operations: ```php = 500) { $error->setDetail('Something went wrong'); } @@ -213,6 +259,7 @@ final class ErrorProvider implements ProviderInterface ``` ```yaml +# The YAML syntax is only supported for Symfony api_platform.state.error_provider: class: 'App\State\ErrorProvider' tags: @@ -223,6 +270,7 @@ api_platform.state.error_provider: Note that our validation exception have their own error provider at: ```yaml +# The YAML syntax is only supported for Symfony api_platform.validator.state.error_provider: tags: - key: 'api_platform.validator.state.error_provider' @@ -271,4 +319,7 @@ class Error extends \Exception implements ProblemExceptionInterface } ``` -We recommend using the `\ApiPlatform\Metadata\Exception\ProblemExceptionInterface` and the `\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. +We recommend using the `\ApiPlatform\Metadata\Exception\ProblemExceptionInterface` and the +`\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. From 90de5abcc9c8b2a718c035c38435c639c782c38f Mon Sep 17 00:00:00 2001 From: Vincent Amstoutz Date: Mon, 9 Dec 2024 14:55:44 +0100 Subject: [PATCH 61/80] docs(push-relations): compatibility with laravel and improve (#2083) --- core/push-relations.md | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/core/push-relations.md b/core/push-relations.md index e3f52c6cd28..eb7826185ee 100644 --- a/core/push-relations.md +++ b/core/push-relations.md @@ -1,18 +1,21 @@ # 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 Date: Mon, 9 Dec 2024 15:01:37 +0100 Subject: [PATCH 62/80] docs(json-schema): compatibility with laravel and improve (#2081) --- core/json-schema.md | 60 +++++++++++++++++++++++++++++++++------------ 1 file changed, 44 insertions(+), 16 deletions(-) diff --git a/core/json-schema.md b/core/json-schema.md index afdb3022143..d751441d87d 100644 --- a/core/json-schema.md +++ b/core/json-schema.md @@ -1,16 +1,22 @@ # 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) + To export the schema corresponding to an API Resource, run the following command: ```console -bin/console api:json-schema:generate 'App\Entity\Book' +bin/console api:json-schema:generate 'App\ApiResource\Book' ``` To see all options available, try: @@ -21,33 +27,50 @@ 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 +> [API Test Assertions in Laravel documentation](../laravel/testing.md#api-test-assertions-with-laravel). -When [Testing the API](../symfony/testing-utilities.md), JSON Schemas are useful to generate and automate unit testing. API Platform provides specific unit testing functionalities like [`assertMatchesResourceCollectionJsonSchema()`](../symfony/testing-utilities.md#writing-functional-tests) or [`assertMatchesResourceItemJsonSchema()`](../symfony/testing-utilities.md#writing-functional-tests) methods. +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-utilities.md#writing-functional-tests) or [`assertMatchesResourceItemJsonSchema()`](../symfony/testing-utilities.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. -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 'integer', + 'example' => 1 + ] + )] private ?int $id = null; // [...] @@ -77,9 +100,14 @@ You can obtain more information about the available [JSON Schema Types and forma ## Generating a JSON Schema Programmatically -To generate JSON Schemas programmatically, use the `api_platform.json_schema.schema_factory` [service](https://symfony.com/doc/current/service_container.html#fetching-and-using-services). +To generate JSON Schemas programmatically, use the `api_platform.json_schema.schema_factory`. + +For further information, please consult the following documentations: + +- [Symfony: Fetching and Using Services](https://symfony.com/doc/current/service_container.html#fetching-and-using-services) +- [Laravel: Resolving Services](https://laravel.com/docs/container#resolving) ## Testing API Platform provides a PHPUnit assertion to test if a response is valid according to a given Schema: `assertMatchesJsonSchema()`. -Refer to [the testing documentation](../symfony/testing-utilities.md) for more details. +Refer to [the testing documentation](../core/testing.md) for more details. From 0bbc490e483b15c22d895833330b47dc07b2de85 Mon Sep 17 00:00:00 2001 From: Vincent Amstoutz Date: Mon, 9 Dec 2024 15:03:18 +0100 Subject: [PATCH 63/80] docs(openapi): compatibility with laravel and update (#2080) --- core/openapi.md | 292 +++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 264 insertions(+), 28 deletions(-) diff --git a/core/openapi.md b/core/openapi.md index 287c4576c06..1f4d1833b6f 100644 --- a/core/openapi.md +++ b/core/openapi.md @@ -15,6 +15,9 @@ API documentation in a user friendly way. ## Using the OpenAPI Command +> [!WARNING] +> These commands are not yet available with Laravel, you're welcome to contribute [on GitHub](https://github.com/api-platform/core) + You can also dump an OpenAPI specification for your API. OpenAPI, JSON format: @@ -49,6 +52,8 @@ bin/console api:openapi:export --spec-version=3.0.0 ## 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`. @@ -107,6 +112,78 @@ The impact on the swagger-ui is the following: ![Swagger UI](images/swagger-ui-modified.png) +### Overriding the OpenAPI Specification with Laravel + +Laravel allows to [decorate services](https://laravel.com/docs/container#extending-bindings), here we +need to decorate `api_platform.openapi.factory`. + +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. + +```php +app->extend(OpenApiFactoryInterface::class, function (OpenApiFactoryInterface $factory) { + return new OpenApiFactory($factory); + }); + } +} +``` + +```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; + } +} +``` + +The impact on the swagger-ui is the following: + +![Swagger UI](images/swagger-ui-modified.png) + ## Using the OpenAPI and Swagger Contexts Sometimes you may want to change the information included in your OpenAPI documentation. @@ -119,54 +196,68 @@ The following configuration will give you total control over your OpenAPI defini ```php 'integer', + 'example' => 1 + ] + )] + #[Assert\NotBlank] private ?int $id = null; /** * @param string $name A name property - this description will be available in the API documentation too. * */ - #[ORM\Column] #[Assert\NotBlank] #[ApiProperty( + description: 'A name property - this description will be available in the API documentation too.', openapiContext: [ 'type' => 'string', 'enum' => ['one', 'two'], 'example' => 'one' ] )] + #[Assert\NotBlank] public string $name; - #[ORM\Column(type: "datetime")] - #[Assert\DateTime] #[ApiProperty( + description: 'A timestamp property.', openapiContext: [ 'type' => 'string', - 'format' => 'date-time' + 'format' => 'date-time', ] )] - public $timestamp; + #[Assert\DateTime] + public string $timestamp; - // ... + // Optionnel : Ajout d'un constructeur pour faciliter l'initialisation + public function __construct(string $name, string $timestamp) + { + $this->name = $name; + $this->timestamp = $timestamp; + } } + ``` ```yaml # api/config/api_platform/properties.yaml +# The YAML syntax is only supported for Symfony properties: - App\Entity\Product: + App\ApiResource\Product: name: openapiContext: type: string @@ -181,12 +272,12 @@ properties: ```xml - + - + type @@ -200,7 +291,7 @@ properties: - + string @@ -267,8 +358,8 @@ Using the `openapi` boolean option disables this operation from the OpenAPI docu ```php + + + 'This scope value requests access to the email and email_verified Claims.', + 'address' => 'This scope value requests access to the address Claim.', + 'phone' => 'This scope value requests access to the phone_number and phone_number_verified Claims.', + ] + ], +]; +``` + +> [!NOTE] +> If you're using an OpenID Connect server (such as Keycloak or Auth0), the `openid` scope **must** be set according +> to the [OpenID Connect specification](https://openid.net/specs/openid-connect-core-1_0.html). ## Info Object -The [info object](https://swagger.io/specification/#info-object) provides metadata about the API like licensing information or a contact. You can specify this information using API Platform's configuration: +The [info object](https://swagger.io/specification/#info-object) provides metadata about the API like licensing +information or a contact. You can specify this information using API Platform's configuration below: + +### Info Object Configuration using Symfony ```yaml api_platform: @@ -669,3 +879,29 @@ api_platform: # URL to the license used for the API. MUST be in the format of a URL. url: ``` + +### Info Object Configuration using Laravel + +```php + 'API title', // The title of the API. + 'description' => 'API description', // The description of the API. + 'version' => '0.0.0', // The version of the API. + //... + 'openapi' => [ + 'contact' => [ // The contact information for the exposed API. + 'name' => '', // The identifying name of the contact person/organization. + 'url' => '', // The URL pointing to the contact information. MUST be in the format of a URL. + 'email' => '', // The email address of the contact person/organization. MUST be in the format of an email address. + ], + 'termsOfService' => '', // A URL to the Terms of Service for the API. MUST be in the format of a URL. + 'license' => [ // The license information for the exposed API. + 'name' => '', // The license name used for the API. + 'url' => '', // URL to the license used for the API. MUST be in the format of a URL. + ] + ], +]; +``` From 83c8397e8dfdd2cef2a5a13d71d6655c47b458e2 Mon Sep 17 00:00:00 2001 From: Vincent Amstoutz Date: Mon, 9 Dec 2024 15:03:53 +0100 Subject: [PATCH 64/80] docs(url-generation-strategy): compatibility with laravel (#2089) --- core/url-generation-strategy.md | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/core/url-generation-strategy.md b/core/url-generation-strategy.md index 0e2ba891a97..e918663680c 100644 --- a/core/url-generation-strategy.md +++ b/core/url-generation-strategy.md @@ -22,7 +22,11 @@ For instance, in JSON-LD, you will get a collection like this: You may want to use absolute URLs (for instance if resources are used in another API) or network paths instead. -It can be configured globally: +## Configure URL Generation Globally + +It can be configured globally using one of the configurations below: + +### Configure URL Generation Globally using Symfony ```yaml # api/config/packages/api_platform.yaml @@ -30,6 +34,20 @@ api_platform: defaults: url_generation_strategy: !php/const ApiPlatform\Api\UrlGeneratorInterface::ABS_URL ``` +### Configure URL Generation Globally using Laravel + +```php + [ + 'url_generation_strategy' => ApiPlatform\Api\UrlGeneratorInterface::ABS_URL + ], +]; +``` + +## Configure URL Generation for a Specific Resource It can also be configured only for a specific resource: @@ -37,7 +55,9 @@ It can also be configured only for a specific resource: ```php + - + ``` From 3a6ffae7ea61aee768079e0f4e032967d5ed7e16 Mon Sep 17 00:00:00 2001 From: Vincent Amstoutz Date: Mon, 9 Dec 2024 15:05:50 +0100 Subject: [PATCH 65/80] docs(extensions): explain better current support (#2074) --- core/extensions.md | 7 +++++-- core/graphql.md | 12 +++++++----- core/subresources.md | 2 +- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/core/extensions.md b/core/extensions.md index ac832b248fc..9d06acc282d 100644 --- a/core/extensions.md +++ b/core/extensions.md @@ -1,4 +1,7 @@ -# Extensions +# 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) API Platform provides a system to extend queries on items and collections. @@ -127,7 +130,7 @@ Note that your extensions should have a positive priority if defined. Internal e | `api_platform.doctrine.orm.query_extension.order` | -32 | ApiPlatform\Doctrine\Orm\Extension\OrderExtension | | `api_platform.doctrine.orm.query_extension.pagination` | -64 | ApiPlatform\Doctrine\Orm\Extension\PaginationExtension | -#### Blocking Anonymous Users +#### 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. diff --git a/core/graphql.md b/core/graphql.md index a5370041f8d..007b7026653 100644 --- a/core/graphql.md +++ b/core/graphql.md @@ -19,7 +19,8 @@ composer require api-platform/graphql You can now use GraphQL at the endpoint: `https://localhost:8443/graphql`. > [!NOTE] -> If you used [the Symfony Variant thanks to Symfony Flex](../symfony/index.md#using-symfony-flex-and-composer-advanced-users) or the Laravel variant, URLs will be prefixed with `/api` by default. For example, the GraphQL endpoint will be: `https://localhost:8443/api/graphql`. +> If you used [the Symfony Variant thanks to Symfony Flex](../symfony/index.md#installing-the-framework) +> or the Laravel variant, URLs will be prefixed with `/api` by default. For example, the GraphQL endpoint will be: `https://localhost:8443/api/graphql`. ## Changing Location of the GraphQL Endpoint @@ -138,7 +139,7 @@ api_platform: ### Disable GraphQL Playground with Laravel > [!WARNING] -> This is not yet available with Laravel, you're welcome to contribute [on Github](github.com/api-platform/core) +> 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 @@ -1100,7 +1101,7 @@ resources: ### Syntax for Filters with a List of Key / Value Arguments -Some filters like the [exists filter](filters.md#exists-filter) or the [order filter](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: @@ -1387,7 +1388,8 @@ resources:
-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#client-side) 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: @@ -2812,7 +2814,7 @@ 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](github.com/api-platform/core) +> 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. diff --git a/core/subresources.md b/core/subresources.md index b06443cc802..cbdcd382b04 100644 --- a/core/subresources.md +++ b/core/subresources.md @@ -348,7 +348,7 @@ class Company { ## Security > [!WARNING] -> This is not yet available with Laravel, you're welcome to contribute [on Github](github.com/api-platform/core) +> 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. From 41d48ddd6b7d42d2f0a5caaae696384ba7d8685e Mon Sep 17 00:00:00 2001 From: Vincent Amstoutz Date: Mon, 9 Dec 2024 15:07:53 +0100 Subject: [PATCH 66/80] docs(dto): improve dto documentation and support laravel (#2076) --- core/dto.md | 38 ++++++++++++++++++-------------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/core/dto.md b/core/dto.md index 8eeeeb94281..f7296f9eac9 100644 --- a/core/dto.md +++ b/core/dto.md @@ -1,5 +1,7 @@ # Using Data Transfer Objects (DTOs) +

Custom Resources screencast
Watch the 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. 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. @@ -10,8 +12,7 @@ Using an input, the request body will be denormalized to the input instead of yo ```php ```php + Date: Mon, 9 Dec 2024 15:09:46 +0100 Subject: [PATCH 67/80] docs(perfs): compatibility with laravel and reorganize (#2073) --- core/performance.md | 96 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 87 insertions(+), 9 deletions(-) diff --git a/core/performance.md b/core/performance.md index 496f3481ef7..d4012a26b81 100644 --- a/core/performance.md +++ b/core/performance.md @@ -71,7 +71,9 @@ 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. -Setup the HTTP cache invalidation in your API Platform project +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: @@ -83,6 +85,24 @@ api_platform: purger: api_platform.http_cache.purger.souin ``` +##### Cache Invalidation Configuration using Laravel + +```php + [ + 'invalidation' => [ + // We assume that your API can reach your caddy instance by the hostname http://caddy. + // The endpoint /souin-api/souin is the default path to the invalidation API. + 'urls' => ['http://caddy/souin-api/souin'], + 'purger' => 'api_platform.http_cache.purger.souin', + ] + ], +]; +``` + Don't forget to set your `Cache-Control` directive to enable caching on your API resource class. This can be achieved using the `cacheHeaders` property: @@ -107,6 +127,8 @@ And voilà, you have a fully working HTTP cache with an invalidation API. Integration with Varnish and Doctrine ORM is shipped with the core library. +##### Varnish cache invalidation system using Symfony + Add the following configuration to enable the cache invalidation system: ```yaml @@ -122,12 +144,39 @@ api_platform: vary: ['Content-Type', 'Authorization', 'Origin'] ``` +##### Varnish cache invalidation system using Laravel + +Add the following configuration to enable the cache invalidation system: + +```php + [ + 'invalidation' => [ + 'enabled' => true, + 'varnish_urls' => ['%env(VARNISH_URL)%'], + ] + ], + 'defaults' => [ + 'cache_headers' => [ + 'max_age' => 0, + 'shared_max_age' => 3600, + 'vary' => ['Content-Type', 'Authorization', 'Origin'], + ] + ], +]; +``` + ## Configuration Support for reverse proxies other than Varnish or Caddy with the HTTP cache module can be added by implementing the `ApiPlatform\HttpCache\PurgerInterface`. Three purgers are available, the built-in caddy HTTP cache purger (`api_platform.http_cache.purger.souin`), the HTTP tags (`api_platform.http_cache.purger.varnish.ban`), the surrogate key implementation (`api_platform.http_cache.purger.varnish.xkey`). You can specify the implementation using the `purger` configuration node, -for example, to use the `xkey` implementation: +for example, to use the `Xkey` implementation see the Symfony or Laravel configuration below: + +### Exemple of Varnish Xkey implementation using Symfony ```yaml api_platform: @@ -147,6 +196,37 @@ api_platform: glue: ', ' ``` +### Exemple of Varnish Xkey implementation using Laravel + +```php + [ + 'invalidation' => [ + 'enabled' => true, + 'varnish_urls' => ['%env(VARNISH_URL)%'], + 'purger' => 'api_platform.http_cache.purger.varnish.xkey', + ], + 'public' => true, + ], + 'defaults' => [ + 'cache_headers' => [ + 'max_age' => 0, + 'shared_max_age' => 3600, + 'vary' => ['Content-Type', 'Authorization', 'Origin'], + 'invalidation' => [ + 'xkey' => [ + 'glue' => ', ', + ] + ], + ] + ], +]; +``` + + In addition to the cache invalidation mechanism, you may want to [use HTTP/2 Server Push to pre-emptively send relations to the client](push-relations.md). @@ -257,20 +337,18 @@ This feature is enabled by default in the production environment of the API Plat ### 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. +driver, you may want to carefully index it by using a [function-based index,](https://use-the-index-luke.com/sql/where-clause/functions/case-insensitive-search) or it will impact performance +with a huge collection. [Here are some examples to index LIKE filters](https://use-the-index-luke.com/sql/where-clause/searching-for-ranges/like-performance-tuning) depending on your database driver. ### Eager Loading -By default, Doctrine comes with [lazy loading](https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/working-with-objects.html#by-lazy-loading) - usually a killer time-saving feature but also a performance killer with large applications. +By default, Doctrine comes with [lazy loading](https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/working-with-objects.html#by-lazy-loading) +- usually a killer time-saving feature but also a performance killer with large applications. Fortunately, Doctrine offers another approach to solve this problem: [eager loading](https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/working-with-objects.html#by-eager-loading). This can easily be enabled for a relation: `#[ORM\ManyToOne(fetch: "EAGER")]`. -By default in API Platform, we chose to force eager loading for all relations, with or without the Doctrine +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: From 27913424a8b22ed08cf7ea8f35d2f9d0cafed846 Mon Sep 17 00:00:00 2001 From: Vincent Amstoutz Date: Mon, 9 Dec 2024 15:14:41 +0100 Subject: [PATCH 68/80] docs(default-order): compatibility with laravel (#2072) --- core/default-order.md | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/core/default-order.md b/core/default-order.md index 0489add6811..3633359eb01 100644 --- a/core/default-order.md +++ b/core/default-order.md @@ -9,8 +9,8 @@ customize this order, you must add an `order` attribute on your ApiResource anno ```php Date: Mon, 9 Dec 2024 15:15:22 +0100 Subject: [PATCH 69/80] docs(deprecations): compatibility with laravel (#2071) --- core/deprecations.md | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/core/deprecations.md b/core/deprecations.md index df648023268..746e3a2d8b4 100644 --- a/core/deprecations.md +++ b/core/deprecations.md @@ -21,8 +21,8 @@ To deprecate a resource class, use the `deprecationReason` attribute: ```php Date: Mon, 9 Dec 2024 15:18:28 +0100 Subject: [PATCH 70/80] docs(pagination): support for laravel and reorganize (#2070) --- core/pagination.md | 245 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 203 insertions(+), 42 deletions(-) diff --git a/core/pagination.md b/core/pagination.md index 5afdefa7ee1..6c9018ed965 100644 --- a/core/pagination.md +++ b/core/pagination.md @@ -43,6 +43,8 @@ of total items in the collection. The name of the page parameter can be changed with the following configuration: +## Changing page parameter name with Symfony + ```yaml # api/config/packages/api_platform.yaml api_platform: @@ -51,6 +53,21 @@ api_platform: page_parameter_name: _page ``` +## Changing page parameter name with Laravel + +```php + [ + 'pagination' => [ + 'page_parameter_name' => '_page', + ] + ], +]; +``` + ## Disabling the Pagination Paginating collections is generally accepted as a good practice. It allows browsing large collections without too much @@ -59,6 +76,8 @@ However, for small collections, it can be convenient to fully disable the pagina ### Disabling the Pagination Globally +#### Disabling the Pagination Globally with Symfony + The pagination can be disabled for all resources using this configuration: ```yaml @@ -68,6 +87,21 @@ api_platform: pagination_enabled: false ``` +#### Disabling the Pagination Globally with Laravel + +The pagination can be disabled for all resources using this configuration: + +```php + [ + 'pagination_enabled' => false, + ], +]; +``` + ### Disabling the Pagination For a Specific Resource It can also be disabled for a specific resource: @@ -76,8 +110,8 @@ It can also be disabled for a specific resource: ```php + - + @@ -156,14 +193,17 @@ resources: #### Disabling the Pagination Client-side Globally -You can configure API Platform to let the client enable or disable the pagination. To activate this feature globally, -use the following configuration: +You can configure API Platform to let the client enable or disable the pagination. + +##### Disabling the Pagination Client-side Globally with Symfony + +To configure this feature globally, use the following configuration: ```yaml # api/config/packages/api_platform.yaml api_platform: defaults: - pagination_client_enabled: true + pagination_client_enabled: false collection: pagination: enabled_parameter_name: pagination # optional @@ -177,14 +217,42 @@ The pagination can now be enabled or disabled by adding a query parameter named 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 + +To configure this feature globally, use the following configuration: + +```php + [ + '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`: + +- `GET /books?pagination=false`: disabled +- `GET /books?pagination=true`: enabled + +Any value accepted by the [`FILTER_VALIDATE_BOOLEAN`](https://www.php.net/manual/en/filter.filters.validate.php) filter can be +used as the value. + #### Disabling the Pagination Client-side For a Specific Resource The client ability to disable the pagination can also be set in the resource configuration: ```php [ + 'pagination_items_per_page' => 30, + ], +]; +``` + ### Changing the Number of Items per Page For a Specific Resource ```php [ + 'pagination_client_items_per_page' => true, + ], + 'collection' => [ + 'pagination' => [ + 'items_per_page_parameter_name' => 'itemsPerPage', + ], + ], +]; +``` + +The number of items per page can now be changed adding a query parameter named `itemsPerPage`: `GET /books?itemsPerPage=20`. + #### Changing the Number of Items per Page Client-side For a Specific Resource Changing the number of items per page can be enabled (or disabled) for a specific resource: ```php [ + 'pagination_maximum_items_per_page' => 50, + ], +]; +``` + ### Changing Maximum Items Per Page For a Specific Resource ```php [ + 'pagination_partial' => true, // Disabled by default + ], +]; +``` + ### Partial Pagination For a Specific Resource ```php [ + 'pagination_client_partial' => true, // Disabled by default + ], + 'collection' => [ + 'pagination' => [ + 'partial_parameter_name' => 'partial' // Default value + ], + ], +]; +``` + +The partial pagination retrieval can now be changed by toggling a query parameter named `partial`: `GET /books?partial=true`. + #### Partial Pagination Client-side For a Specific Resource ```php Date: Mon, 9 Dec 2024 15:28:18 +0100 Subject: [PATCH 71/80] docs(mercure): compatibility with laravel and improve (#2082) --- core/mercure.md | 52 +++++++++++++++++++++++++++++-------------------- 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/core/mercure.md b/core/mercure.md index fb64a2f1182..11bea20a9c1 100644 --- a/core/mercure.md +++ b/core/mercure.md @@ -16,9 +16,13 @@ Then, the Mercure hub dispatches the updates to all connected clients using [Ser 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 have installed API Platform using another method (such as `composer require api`), you need to install [a Mercure hub](https://mercure.rocks/docs/getting-started) and the Symfony MercureBundle. +If you installed API Platform using another method (e.g., `composer require api`), you will need to set up the following: -[Learn how to install and configure MercureBundle manually on the Symfony website](https://symfony.com/doc/current/mercure.html) +1. A [Mercure hub](https://mercure.rocks/docs/getting-started). + +2. One of the following, depending on your framework: + - For Symfony users: the [MercureBundle](https://symfony.com/doc/current/mercure.html). + - For Laravel users: the [Laravel Mercure Broadcaster](https://github.com/mvanduijker/laravel-mercure-broadcaster). ## Pushing the API Updates @@ -26,8 +30,8 @@ Use the `mercure` attribute to hint API Platform that it must dispatch the updat ```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}`. > -> 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) @@ -119,12 +129,12 @@ Below is an example using the `topics` option: ```php Date: Mon, 9 Dec 2024 15:39:38 +0100 Subject: [PATCH 72/80] docs(entity): move from core to symfony (#2097) --- core/index.md | 2 +- outline.yaml | 2 +- {core => symfony}/user.md | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) rename {core => symfony}/user.md (98%) diff --git a/core/index.md b/core/index.md index 23aee4631b3..4930af7f370 100644 --- a/core/index.md +++ b/core/index.md @@ -34,7 +34,7 @@ Here is the fully featured REST API you'll get in minutes: - Advanced [serialization](serialization.md) thanks to the Symfony Serializer Component (groups support, relation embedding, max depth...) - Automatic route registration - Automatic entry point generation giving access to all resources -- [User](user.md) support +- [User Management using Symfony](../symfony/user.md) - [JWT](jwt.md) and [OAuth](https://oauth.net/) support - Files and `\DateTime` and serialization and deserialization diff --git a/outline.yaml b/outline.yaml index b27f181c88e..68ce339f4a5 100644 --- a/outline.yaml +++ b/outline.yaml @@ -12,6 +12,7 @@ chapters: - migrate-from-fosrestbundle - fosuser-bundle - nelmio-api-doc + - user - title: "API Platform for Laravel" path: laravel items: @@ -64,7 +65,6 @@ chapters: - events - file-upload - jwt - - user - form-data - bootstrap - configuration diff --git a/core/user.md b/symfony/user.md similarity index 98% rename from core/user.md rename to symfony/user.md index dba55a90a52..51b5f94368b 100644 --- a/core/user.md +++ b/symfony/user.md @@ -1,4 +1,4 @@ -# User Entity +# 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. @@ -219,7 +219,7 @@ 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](state-processors.md) for auto-hashing plain passwords. +Happily you can use the API Platform [state processors](../core/state-processors.md) for auto-hashing plain passwords. First create a new state processor: From b232756342e087979e77e8ac9fe733ed35e74e27 Mon Sep 17 00:00:00 2001 From: Vincent Amstoutz Date: Mon, 9 Dec 2024 15:46:02 +0100 Subject: [PATCH 73/80] docs(jwt): support for laravel and reorganize (#2095) --- core/jwt.md | 304 +----------------------------------------------- laravel/jwt.md | 120 +++++++++++++++++++ outline.yaml | 2 + symfony/jwt.md | 310 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 434 insertions(+), 302 deletions(-) create mode 100644 laravel/jwt.md create mode 100644 symfony/jwt.md diff --git a/core/jwt.md b/core/jwt.md index 8f619ba3aeb..99f01c81008 100644 --- a/core/jwt.md +++ b/core/jwt.md @@ -8,305 +8,5 @@ > > ―[Wikipedia](https://en.wikipedia.org/wiki/JSON_Web_Token) -API Platform allows to easily add a JWT-based authentication to your API using [LexikJWTAuthenticationBundle](https://github.com/lexik/LexikJWTAuthenticationBundle). - -

JWT screencast
Watch the LexikJWTAuthenticationBundle screencast

- -## Installing LexikJWTAuthenticationBundle - -We begin by installing the bundle: - -```console -composer require lexik/jwt-authentication-bundle -``` -Then we need to generate the public and private keys used for signing JWT tokens. - -You can generate them by using this command: - -```console -php bin/console lexik:jwt:generate-keypair -``` - -Or if you're using the [API Platform distribution with Symfony](../symfony/index.md), you may run this from the project's root directory: - -```console -docker compose exec php sh -c ' - set -e - apt-get install openssl - php bin/console lexik:jwt:generate-keypair - setfacl -R -m u:www-data:rX -m u:"$(whoami)":rwX config/jwt - setfacl -dR -m u:www-data:rX -m u:"$(whoami)":rwX config/jwt -' -``` - -Note that the `setfacl` command relies on the `acl` package. This is installed by default 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. - -If you want the keys to be auto generated in `dev` environment, see an example in the -[docker-entrypoint script of api-platform/demo](https://github.com/api-platform/demo/blob/a03ce4fb1f0e072c126e8104e42a938bb840bffc/api/docker/php/docker-entrypoint.sh#L16-L17). - -Since these keys are created by the `root` user from a container, your host user will not be able to read them during -the `docker compose build caddy` process. Add the `config/jwt/` folder to the `api/.dockerignore` file so that they are -skipped from the result image. - -The keys should not be checked in to the repository (i.e. it's in `api/.gitignore`). However, note that a JWT token could -only pass signature validation against the same pair of keys it was signed with. This is especially relevant in a production -environment, where you don't want to accidentally invalidate all your clients' tokens at every deployment. - -For more information, refer to [the bundle's documentation](https://github.com/lexik/LexikJWTAuthenticationBundle/blob/2.x/Resources/doc/index.rst) -or read a [general introduction to JWT here](https://jwt.io/introduction/). - -We're not done yet! Let's move on to configuring the Symfony SecurityBundle for JWT authentication. - -## Configuring the Symfony SecurityBundle - -It is necessary to configure a user provider. You can either use the [Doctrine entity user provider](https://symfony.com/doc/current/security/user_provider.html#entity-user-provider) -provided by Symfony (recommended), [create a custom user provider](https://symfony.com/doc/current/security/user_provider.html#creating-a-custom-user-provider) -or use [API Platform's FOSUserBundle integration](../symfony/fosuser-bundle.md) (not recommended). - -If you choose to use the Doctrine entity user provider, start by [creating your `User` class](https://symfony.com/doc/current/security.html#a-create-your-user-class). - -Then update the security configuration: - -```yaml -# api/config/packages/security.yaml -security: - # https://symfony.com/doc/current/security.html#c-hashing-passwords - password_hashers: - App\Entity\User: 'auto' - - # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers - providers: - # used to reload user from session & other features (e.g. switch_user) - users: - entity: - class: App\Entity\User - property: email - # mongodb: - # class: App\Document\User - # property: email - - firewalls: - dev: - pattern: ^/_(profiler|wdt) - security: false - main: - stateless: true - provider: users - json_login: - check_path: auth # The name in routes.yaml is enough for mapping - username_path: email - password_path: password - success_handler: lexik_jwt_authentication.handler.authentication_success - failure_handler: lexik_jwt_authentication.handler.authentication_failure - jwt: ~ - - access_control: - - { path: ^/$, roles: PUBLIC_ACCESS } # Allows accessing the Swagger UI - - { path: ^/docs, roles: PUBLIC_ACCESS } # Allows accessing the Swagger UI docs - - { path: ^/auth, roles: PUBLIC_ACCESS } - - { path: ^/, roles: IS_AUTHENTICATED_FULLY } -``` - -You must also declare the route used for `/auth`: - -```yaml -# api/config/routes.yaml -auth: - path: /auth - methods: ['POST'] -``` - -If you want to avoid loading the `User` entity from database each time a JWT token needs to be authenticated, you may consider using -the [database-less user provider](https://github.com/lexik/LexikJWTAuthenticationBundle/blob/2.x/Resources/doc/8-jwt-user-provider.rst) provided by LexikJWTAuthenticationBundle. However, it means you will have to fetch the `User` entity from the database yourself as needed (probably through the Doctrine EntityManager). - -Refer to the section on [Security](security.md) to learn how to control access to API resources and operations. You may -also want to [configure Swagger UI for JWT authentication](#documenting-the-authentication-mechanism-with-swaggeropen-api). - -### Adding Authentication to an API Which Uses a Path Prefix - -If your API uses a [path prefix](https://symfony.com/doc/current/routing/external_resources.html#route-groups-and-prefixes), the security configuration would look something like this instead: - -```yaml -# api/config/packages/security.yaml -security: - # https://symfony.com/doc/current/security.html#c-hashing-passwords - password_hashers: - App\Entity\User: 'auto' - # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers - providers: - # used to reload user from session & other features (e.g. switch_user) - users: - entity: - class: App\Entity\User - property: email - - firewalls: - dev: - pattern: ^/_(profiler|wdt) - security: false - api: - pattern: ^/api/ - stateless: true - provider: users - jwt: ~ - main: - json_login: - check_path: auth # The name in routes.yaml is enough for mapping - username_path: email - password_path: password - success_handler: lexik_jwt_authentication.handler.authentication_success - failure_handler: lexik_jwt_authentication.handler.authentication_failure - - access_control: - - { path: ^/$, roles: PUBLIC_ACCESS } # Allows accessing the Swagger UI - - { path: ^/docs, roles: PUBLIC_ACCESS } # Allows accessing API documentations and Swagger UI docs - - { path: ^/auth, roles: PUBLIC_ACCESS } - - { path: ^/, roles: IS_AUTHENTICATED_FULLY } -``` - -### Be sure to have lexik_jwt_authentication configured on your user_identity_field - -```yaml -# api/config/packages/lexik_jwt_authentication.yaml -lexik_jwt_authentication: - secret_key: '%env(resolve:JWT_SECRET_KEY)%' - public_key: '%env(resolve:JWT_PUBLIC_KEY)%' - pass_phrase: '%env(JWT_PASSPHRASE)%' -``` - -## Documenting the Authentication Mechanism with Swagger/Open API - -Want to test the routes of your JWT-authentication-protected API? - -### Configuring API Platform - -```yaml -# api/config/packages/api_platform.yaml -api_platform: - swagger: - api_keys: - JWT: - name: Authorization - type: header -``` - -The "Authorize" button will automatically appear in Swagger UI. - -![Screenshot of API Platform with Authorize button](images/JWTAuthorizeButton.png) - -### Adding a New API Key - -All you have to do is configure the API key in the `value` field. -By default, [only the authorization header mode is enabled](https://github.com/lexik/LexikJWTAuthenticationBundle/blob/2.x/Resources/doc/index.rst#2-use-the-token) in LexikJWTAuthenticationBundle. -You must set the [JWT token](https://github.com/lexik/LexikJWTAuthenticationBundle/blob/2.x/Resources/doc/index.rst#1-obtain-the-token) as below and click on the "Authorize" button. - -`Bearer MY_NEW_TOKEN` - -![Screenshot of API Platform with the configuration API Key](images/JWTConfigureApiKey.png) - -### Adding endpoint to SwaggerUI to retrieve a JWT token - -LexikJWTAuthenticationBundle has an integration with API Platform to automatically -add an OpenAPI endpoint to conveniently retrieve the token in Swagger UI. - -If you need to modify the default configuration, you can do it in the dedicated configuration file: - -```yaml -# config/packages/lexik_jwt_authentication.yaml -lexik_jwt_authentication: - # ... - api_platform: - check_path: /auth - username_path: email - password_path: password -``` - -You will see something like this in Swagger UI: - -![API Endpoint to retrieve JWT Token from SwaggerUI](images/jwt-token-swagger-ui.png) - -## Testing - -To test your authentication with `ApiTestCase`, you can write a method as below: - -```php -setEmail('test@example.com'); - $user->setPassword( - $container->get('security.user_password_hasher')->hashPassword($user, '$3CR3T') - ); - - $manager = $container->get('doctrine')->getManager(); - $manager->persist($user); - $manager->flush(); - - // retrieve a token - $response = $client->request('POST', '/auth', [ - 'headers' => ['Content-Type' => 'application/json'], - 'json' => [ - 'email' => 'test@example.com', - 'password' => '$3CR3T', - ], - ]); - - $json = $response->toArray(); - $this->assertResponseIsSuccessful(); - $this->assertArrayHasKey('token', $json); - - // test not authorized - $client->request('GET', '/greetings'); - $this->assertResponseStatusCodeSame(401); - - // test authorized - $client->request('GET', '/greetings', ['auth_bearer' => $json['token']]); - $this->assertResponseIsSuccessful(); - } -} -``` - -Refer to [Testing the API](../symfony/testing.md) for more information about testing API Platform. - -### Improving Tests Suite Speed - -Since now we have a `JWT` authentication, functional tests require us to log in each time we want to test an API endpoint. This is where [Password Hashers](https://symfony.com/doc/current/security/passwords.html) come into play. - -Hashers are used for 2 reasons: - -1. To generate a hash for a raw password (`$container->get('security.user_password_hasher')->hashPassword($user, '$3CR3T')`) -2. To verify a password during authentication - -While hashing and verifying 1 password is quite a fast operation, doing it hundreds or even thousands of times in a tests suite becomes a bottleneck, because reliable hashing algorithms are slow by their nature. - -To significantly improve the test suite speed, we can use more simple password hasher specifically for the `test` environment. - -```yaml -# override in api/config/packages/test/security.yaml for test env -security: - password_hashers: - App\Entity\User: - algorithm: md5 - encode_as_base64: false - iterations: 0 -``` +- 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). diff --git a/laravel/jwt.md b/laravel/jwt.md new file mode 100644 index 00000000000..568be4fefd5 --- /dev/null +++ b/laravel/jwt.md @@ -0,0 +1,120 @@ +# JWT Authentication with Laravel + +> [!NOTE] +> While solutions like `tymondesigns/jwt-auth` (Laravel) or `LexikJWTAuthenticationBundle` (Symfony) are popular, +> **we recommend adopting open standards such as [OpenID Connect (OIDC)](https://openid.net/connect/)** for robust, scalable, +> and interoperable authentication. + +For comprehensive details on authentication, refer to our [Laravel Authentication documentation](../laravel/index.md#authentication). + +## Setup Instructions + +1. **Install** + Follow the official installation guide of [Laravel Passport](https://laravel.com/docs/passport#installation) to implement + OpenID Connect (OIDC) standards in your Laravel application. + Alternatively, if you prefer an ad-hoc solution, you can use [tymondesigns/jwt-auth](https://github.com/tymondesigns/jwt-auth) + to set up JWT authentication in your Laravel project. + +2. **Configure Authentication** + Refer to the [Authentication section](../laravel/index.md#authentication) of our documentation to properly configure + and secure your API with JWT tokens. + +> [!TIP] +> Use [Laravel middlewares with API Platform](../laravel/index.md#middlewares) such as `auth:api` to +> restrict access to certain endpoints, ensuring only authenticated users can access them. + +By following these steps, you can set up a secure and scalable JWT-based authentication system in your Laravel application. + +## Testing + +To verify your authentication setup using `ApiTestCase`, you can write a test method tailored to your preferred testing +framework. Here's how you can approach it for both **Pest** and **PHPUnit**: + +> [!NOTE] +> Ensure your routes (/api/auth) and authentication mechanisms are configured to match your application's implementation. + +### Test with Pest + +```php +create([ + 'email' => 'test@example.com', + 'password' => bcrypt('$3CR3T'), // Hash the password + ]); + + // Retrieve a token + $response = $this->postJson('/api/auth', [ + 'email' => 'test@example.com', + 'password' => '$3CR3T', + ]); + + $response->assertStatus(200) + ->assertJsonStructure(['token']); + + $token = $response->json('token'); + + // Test not authorized + $this->getJson('/api/greetings') + ->assertStatus(401); + + // Test authorized + $this->withHeader('Authorization', "Bearer $token") + ->getJson('/api/greetings') + ->assertStatus(200); +}); +``` + +### Test with PHPUnit + +```php +create([ + 'email' => 'test@example.com', + 'password' => bcrypt('$3CR3T'), // Hash the password + ]); + + // Retrieve a token + $response = $this->postJson('/api/auth', [ + 'email' => 'test@example.com', + 'password' => '$3CR3T', + ]); + + $response->assertStatus(200) + ->assertJsonStructure(['token']); + + $token = $response->json('token'); + + // Test not authorized + $this->getJson('/api/greetings') + ->assertStatus(401); + + // Test authorized + $this->withHeader('Authorization', "Bearer $token") + ->getJson('/api/greetings') + ->assertStatus(200); + } +} +``` + + diff --git a/outline.yaml b/outline.yaml index 68ce339f4a5..849b020984b 100644 --- a/outline.yaml +++ b/outline.yaml @@ -13,6 +13,7 @@ chapters: - fosuser-bundle - nelmio-api-doc - user + - jwt - title: "API Platform for Laravel" path: laravel items: @@ -21,6 +22,7 @@ chapters: - filters - security - validation + - jwt - title: Core path: core items: diff --git a/symfony/jwt.md b/symfony/jwt.md new file mode 100644 index 00000000000..460da9b417a --- /dev/null +++ b/symfony/jwt.md @@ -0,0 +1,310 @@ +# JWT Authentication with Symfony + +> [!NOTE] +> While solutions like `LexikJWTAuthenticationBundle` (Symfony) or `tymondesigns/jwt-auth` (Laravel) are popular, +> **we recommend adopting open standards such as [OpenID Connect (OIDC)](https://openid.net/connect/)** for robust, scalable, +> and interoperable authentication. + +

JWT screencast
Watch the LexikJWTAuthenticationBundle screencast

+ +## Installing LexikJWTAuthenticationBundle + +> [!NOTE] +> API Platform makes it easy to add JWT-based authentication to your API using [LexikJWTAuthenticationBundle](https://github.com/lexik/LexikJWTAuthenticationBundle). + +We begin by installing the bundle: + +```console +composer require lexik/jwt-authentication-bundle +``` +Then we need to generate the public and private keys used for signing JWT tokens. + +You can generate them by using this command: + +```console +php bin/console lexik:jwt:generate-keypair +``` + +Or if you're using the [API Platform distribution with Symfony](../symfony/index.md), you may run this from the project's root directory: + +```console +docker compose exec php sh -c ' + set -e + apt-get install openssl + php bin/console lexik:jwt:generate-keypair + setfacl -R -m u:www-data:rX -m u:"$(whoami)":rwX config/jwt + setfacl -dR -m u:www-data:rX -m u:"$(whoami)":rwX config/jwt +' +``` + +Note that the `setfacl` command relies on the `acl` package. This is installed by default 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. + +If you want the keys to be auto generated in `dev` environment, see an example in the +[docker-entrypoint script of api-platform/demo](https://github.com/api-platform/demo/blob/a03ce4fb1f0e072c126e8104e42a938bb840bffc/api/docker/php/docker-entrypoint.sh#L16-L17). + +Since these keys are created by the `root` user from a container, your host user will not be able to read them during +the `docker compose build caddy` process. Add the `config/jwt/` folder to the `api/.dockerignore` file so that they are +skipped from the result image. + +The keys should not be checked in to the repository (i.e. it's in `api/.gitignore`). However, note that a JWT token could +only pass signature validation against the same pair of keys it was signed with. This is especially relevant in a production +environment, where you don't want to accidentally invalidate all your clients' tokens at every deployment. + +For more information, refer to [the bundle's documentation](https://github.com/lexik/LexikJWTAuthenticationBundle/blob/2.x/Resources/doc/index.rst) +or read a [general introduction to JWT here](https://jwt.io/introduction/). + +We're not done yet! Let's move on to configuring the Symfony SecurityBundle for JWT authentication. + +## Configuring the Symfony SecurityBundle + +It is necessary to configure a user provider. You can either use the [Doctrine entity user provider](https://symfony.com/doc/current/security/user_provider.html#entity-user-provider) +provided by Symfony (recommended), [create a custom user provider](https://symfony.com/doc/current/security/user_provider.html#creating-a-custom-user-provider) +or use [API Platform's FOSUserBundle integration](../symfony/fosuser-bundle.md) (**not recommended**). + +If you choose to use the Doctrine entity user provider, start by [creating your `User` class](https://symfony.com/doc/current/security.html#a-create-your-user-class). + +Then update the security configuration: + +```yaml +# api/config/packages/security.yaml +security: + # https://symfony.com/doc/current/security.html#c-hashing-passwords + password_hashers: + App\Entity\User: 'auto' + + # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers + providers: + # used to reload user from session & other features (e.g. switch_user) + users: + entity: + class: App\Entity\User + property: email + # mongodb: + # class: App\Document\User + # property: email + + firewalls: + dev: + pattern: ^/_(profiler|wdt) + security: false + main: + stateless: true + provider: users + json_login: + check_path: auth # The name in routes.yaml is enough for mapping + username_path: email + password_path: password + success_handler: lexik_jwt_authentication.handler.authentication_success + failure_handler: lexik_jwt_authentication.handler.authentication_failure + jwt: ~ + + access_control: + - { path: ^/$, roles: PUBLIC_ACCESS } # Allows accessing the Swagger UI + - { path: ^/docs, roles: PUBLIC_ACCESS } # Allows accessing the Swagger UI docs + - { path: ^/auth, roles: PUBLIC_ACCESS } + - { path: ^/, roles: IS_AUTHENTICATED_FULLY } +``` + +You must also declare the route used for `/auth`: + +```yaml +# api/config/routes.yaml +auth: + path: /auth + methods: ['POST'] +``` + +If you want to avoid loading the `User` entity from database each time a JWT token needs to be authenticated, you may consider using +the [database-less user provider](https://github.com/lexik/LexikJWTAuthenticationBundle/blob/2.x/Resources/doc/8-jwt-user-provider.rst) provided by LexikJWTAuthenticationBundle. However, it means you will have to fetch the `User` entity from the database yourself as needed (probably through the Doctrine EntityManager). + +Refer to the section on [Security](security.md) to learn how to control access to API resources and operations. You may +also want to [configure Swagger UI for JWT authentication](#documenting-the-authentication-mechanism-with-swaggeropen-api). + +### Adding Authentication to an API Which Uses a Path Prefix + +If your API uses a [path prefix](https://symfony.com/doc/current/routing/external_resources.html#route-groups-and-prefixes), the security configuration would look something like this instead: + +```yaml +# api/config/packages/security.yaml +security: + # https://symfony.com/doc/current/security.html#c-hashing-passwords + password_hashers: + App\Entity\User: 'auto' + # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers + providers: + # used to reload user from session & other features (e.g. switch_user) + users: + entity: + class: App\Entity\User + property: email + + firewalls: + dev: + pattern: ^/_(profiler|wdt) + security: false + api: + pattern: ^/api/ + stateless: true + provider: users + jwt: ~ + main: + json_login: + check_path: auth # The name in routes.yaml is enough for mapping + username_path: email + password_path: password + success_handler: lexik_jwt_authentication.handler.authentication_success + failure_handler: lexik_jwt_authentication.handler.authentication_failure + + access_control: + - { path: ^/$, roles: PUBLIC_ACCESS } # Allows accessing the Swagger UI + - { path: ^/docs, roles: PUBLIC_ACCESS } # Allows accessing API documentations and Swagger UI docs + - { path: ^/auth, roles: PUBLIC_ACCESS } + - { path: ^/, roles: IS_AUTHENTICATED_FULLY } +``` + +### Be sure to have lexik_jwt_authentication configured on your user_identity_field + +```yaml +# api/config/packages/lexik_jwt_authentication.yaml +lexik_jwt_authentication: + secret_key: '%env(resolve:JWT_SECRET_KEY)%' + public_key: '%env(resolve:JWT_PUBLIC_KEY)%' + pass_phrase: '%env(JWT_PASSPHRASE)%' +``` + +## Documenting the Authentication Mechanism with Swagger/Open API + +Want to test the routes of your JWT-authentication-protected API? + +### Configuring API Platform + +```yaml +# api/config/packages/api_platform.yaml +api_platform: + swagger: + api_keys: + JWT: + name: Authorization + type: header +``` + +The "Authorize" button will automatically appear in Swagger UI. + +![Screenshot of API Platform with Authorize button](../core/images/JWTAuthorizeButton.png) + +### Adding a New API Key + +All you have to do is configure the API key in the `value` field. +By default, [only the authorization header mode is enabled](https://github.com/lexik/LexikJWTAuthenticationBundle/blob/2.x/Resources/doc/index.rst#2-use-the-token) in LexikJWTAuthenticationBundle. +You must set the [JWT token](https://github.com/lexik/LexikJWTAuthenticationBundle/blob/2.x/Resources/doc/index.rst#1-obtain-the-token) as below and click on the "Authorize" button. + +`Bearer MY_NEW_TOKEN` + +![Screenshot of API Platform with the configuration API Key](../core/images/JWTConfigureApiKey.png) + +### Adding endpoint to SwaggerUI to retrieve a JWT token + +LexikJWTAuthenticationBundle has an integration with API Platform to automatically +add an OpenAPI endpoint to conveniently retrieve the token in Swagger UI. + +If you need to modify the default configuration, you can do it in the dedicated configuration file: + +```yaml +# config/packages/lexik_jwt_authentication.yaml +lexik_jwt_authentication: + # ... + api_platform: + check_path: /auth + username_path: email + password_path: password +``` + +You will see something like this in Swagger UI: + +![API Endpoint to retrieve JWT Token from SwaggerUI](../core/images/jwt-token-swagger-ui.png) + +## Testing + +To test your authentication with `ApiTestCase`, you can write a method as below: + +```php +setEmail('test@example.com'); + $user->setPassword( + $container->get('security.user_password_hasher')->hashPassword($user, '$3CR3T') + ); + + $manager = $container->get('doctrine')->getManager(); + $manager->persist($user); + $manager->flush(); + + // retrieve a token + $response = $client->request('POST', '/auth', [ + 'headers' => ['Content-Type' => 'application/json'], + 'json' => [ + 'email' => 'test@example.com', + 'password' => '$3CR3T', + ], + ]); + + $json = $response->toArray(); + $this->assertResponseIsSuccessful(); + $this->assertArrayHasKey('token', $json); + + // test not authorized + $client->request('GET', '/greetings'); + $this->assertResponseStatusCodeSame(401); + + // test authorized + $client->request('GET', '/greetings', ['auth_bearer' => $json['token']]); + $this->assertResponseIsSuccessful(); + } +} +``` + +Refer to [Testing the API](../symfony/testing.md) for more information about testing API Platform. + +### Improving Tests Suite Speed + +Since now we have a `JWT` authentication, functional tests require us to log in each time we want to test an API endpoint. This is where [Password Hashers](https://symfony.com/doc/current/security/passwords.html) come into play. + +Hashers are used for 2 reasons: + +1. To generate a hash for a raw password (`$container->get('security.user_password_hasher')->hashPassword($user, '$3CR3T')`) +2. To verify a password during authentication + +While hashing and verifying 1 password is quite a fast operation, doing it hundreds or even thousands of times in a tests suite becomes a bottleneck, because reliable hashing algorithms are slow by their nature. + +To significantly improve the test suite speed, we can use more simple password hasher specifically for the `test` environment. + +```yaml +# override in api/config/packages/test/security.yaml for test env +security: + password_hashers: + App\Entity\User: + algorithm: md5 + encode_as_base64: false + iterations: 0 +``` From 78d3384d42b078dbcb1f7e55d1b1662d938a4c8c Mon Sep 17 00:00:00 2001 From: Vincent Amstoutz Date: Mon, 9 Dec 2024 15:49:40 +0100 Subject: [PATCH 74/80] docs(file-upload): move from core to symfony and update (#2094) --- admin/file-upload.md | 2 +- core/graphql.md | 9 +++++---- outline.yaml | 2 +- {core => symfony}/file-upload.md | 21 ++++++++++----------- 4 files changed, 17 insertions(+), 17 deletions(-) rename {core => symfony}/file-upload.md (95%) diff --git a/admin/file-upload.md b/admin/file-upload.md index 6cc50907f57..55af6df595b 100644 --- a/admin/file-upload.md +++ b/admin/file-upload.md @@ -1,6 +1,6 @@ # Handling File Upload -If you need to handle the file upload in the server part, please follow [the related documentation](../core/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. diff --git a/core/graphql.md b/core/graphql.md index 007b7026653..b815599a0a6 100644 --- a/core/graphql.md +++ b/core/graphql.md @@ -19,8 +19,8 @@ composer require api-platform/graphql You can now use GraphQL at the endpoint: `https://localhost:8443/graphql`. > [!NOTE] -> If you used [the Symfony Variant thanks to Symfony Flex](../symfony/index.md#installing-the-framework) -> or the Laravel variant, URLs will be prefixed with `/api` by default. For example, the GraphQL endpoint will be: `https://localhost:8443/api/graphql`. +> If you used [the Symfony Variant thanks to Symfony Flex](../symfony/index.md#installing-the-framework) or the Laravel +> variant, URLs will be prefixed with `/api` by default. For example, the GraphQL endpoint will be: `https://localhost:8443/api/graphql`. ## Changing Location of the GraphQL Endpoint @@ -2830,9 +2830,10 @@ Since the command prints the schema to the output if you don't use the `-o` opti bin/console api:graphql:export > path/in/host/schema.graphql ``` -## Handling File Upload +## Handling File Upload with Symfony -Please follow the [file upload documentation](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). diff --git a/outline.yaml b/outline.yaml index 849b020984b..42d4cd8c70f 100644 --- a/outline.yaml +++ b/outline.yaml @@ -14,6 +14,7 @@ chapters: - nelmio-api-doc - user - jwt + - file-upload - title: "API Platform for Laravel" path: laravel items: @@ -65,7 +66,6 @@ chapters: - elasticsearch - controllers - events - - file-upload - jwt - form-data - bootstrap diff --git a/core/file-upload.md b/symfony/file-upload.md similarity index 95% rename from core/file-upload.md rename to symfony/file-upload.md index f3f12ce17f6..dc3b25458b4 100644 --- a/core/file-upload.md +++ b/symfony/file-upload.md @@ -1,15 +1,14 @@ -# Handling File Upload - -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) +# Handling File Upload with Symfony + +As common a problem as it may seem, handling file upload requires a custom implementation in your app. This page will +guide you in handling file upload in your API, with the help of[VichUploaderBundle](https://github.com/dustin10/VichUploaderBundle). +It is recommended you [read the documentation of VichUploaderBundle](https://github.com/dustin10/VichUploaderBundle/blob/master/docs/index.md) before proceeding. It will help you get a grasp on how the bundle works, and why we use it. -**Note**: Uploading files won't work in `PUT` or `PATCH` requests, you must use `POST` method to upload files. -See [the related issue on Symfony](https://github.com/symfony/symfony/issues/9226) and [the related bug in PHP](https://bugs.php.net/bug.php?id=55815) talking about this behavior. +> [!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: @@ -137,7 +136,7 @@ Note: From V3.3 onwards, `'multipart/form-data'` must either be including in the 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](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 Date: Mon, 9 Dec 2024 15:53:27 +0100 Subject: [PATCH 75/80] docs(controllers): move from core to symfony and update (#2093) --- core/extending.md | 15 ++++++++------- outline.yaml | 2 +- {core => symfony}/controllers.md | 18 ++++++++++-------- symfony/migrate-from-fosrestbundle.md | 2 +- 4 files changed, 20 insertions(+), 17 deletions(-) rename {core => symfony}/controllers.md (95%) diff --git a/core/extending.md b/core/extending.md index d7c52e0ebd0..74486008435 100644 --- a/core/extending.md +++ b/core/extending.md @@ -7,10 +7,11 @@ Those extensions points are taken into account both by the REST and [GraphQL](gr The following tables summarizes which extension point to use depending on what you want to do: | Extension Point | Usage | -| ---------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +|------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | [State Providers](state-providers.md) | adapters for custom persistence layers, virtual fields, custom hydration | | [Denormalizers](serialization.md) | post-process objects created from the payload sent in the HTTP request body | -| [Voters](security.md#hooking-custom-permission-checks-using-voters) | custom authorization logic | +| [Symfony Voters](../symfony/security.md#hooking-custom-permission-checks-using-voters) | custom authorization logic | +| [Laravel Policies](../laravel/security.md#policies) | custom authorization logic | | [Validation constraints](validation.md) | custom validation logic | | [State Processors](state-processors) | custom business logic and computations to trigger before or after persistence (ex: mail, call to an external API...) | | [Normalizers](serialization.md#decorating-a-serializer-and-adding-extra-data) | customize the resource sent to the client (add fields in JSON documents, encode codes, dates...) | @@ -22,16 +23,16 @@ The following tables summarizes which extension point to use depending on what y ## Doctrine Specific Extension Points -| Extension Point | Usage | -| ---------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | -| [Extensions](extensions.md) | Access to the query builder to change the DQL query | -| [Filters](filters.md#doctrine-orm-and-mongodb-odm-filters) | Add filters documentations (OpenAPI, GraphQL, Hydra) and automatically apply them to the DQL query | +| Extension Point | Usage | +|--------------------------------|----------------------------------------------------------------------------------------------------| +| [Extensions](extensions.md) | Access to the query builder to change the DQL query | +| [Filters](doctrine-filters.md) | Add filters documentations (OpenAPI, GraphQL, Hydra) and automatically apply them to the DQL query | ## Leveraging the Built-in Infrastructure Using Composition While most API Platform classes are marked as `final`, built-in services are straightforward to reuse and customize [using composition](https://en.wikipedia.org/wiki/Composition_over_inheritance). -For instance, if you want to send a mail after a resource has been persisted, but still want to benefit from the native Doctrine ORM [state processor](state-processors.md), use [the decorator design pattern](https://en.wikipedia.org/wiki/Decorator_pattern#PHP) to wrap the native state processor in your own class sending the mail, as demonstrated in [this example](state-processors.md#decorating-the-built-in-state-processors). +For instance, if you want to send a mail after a resource has been persisted, but still want to benefit from the native Doctrine ORM [state processor](state-processors.md), use [the decorator design pattern](https://en.wikipedia.org/wiki/Decorator_pattern#PHP) to wrap the native state processor in your own class sending the mail, as demonstrated in [this example](../core/state-processors.md#creating-a-custom-state-processor). To replace existing API Platform services with your decorators, [check out how to decorate services](https://symfony.com/doc/current/service_container/service_decoration.html). diff --git a/outline.yaml b/outline.yaml index 42d4cd8c70f..946601e7f79 100644 --- a/outline.yaml +++ b/outline.yaml @@ -15,6 +15,7 @@ chapters: - user - jwt - file-upload + - controllers - title: "API Platform for Laravel" path: laravel items: @@ -64,7 +65,6 @@ chapters: - identifiers - mongodb - elasticsearch - - controllers - events - jwt - form-data diff --git a/core/controllers.md b/symfony/controllers.md similarity index 95% rename from core/controllers.md rename to symfony/controllers.md index 283b4bf78ca..caf6613fe7d 100644 --- a/core/controllers.md +++ b/symfony/controllers.md @@ -1,11 +1,13 @@ -# Creating Custom Operations and Controllers +# Creating Custom Operations and Symfony Controllers -Note: using custom 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](design.md). +> [!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`](http://api.symfony.com/4.1/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.html) +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: @@ -39,7 +41,7 @@ If your resource has any identifier, this operation will look like `/books/{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](openapi.md) part of the documentation to do so. +See the [OpenAPI](../core/openapi.md) part of the documentation to do so. First, let's create your custom operation: @@ -90,7 +92,7 @@ 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](events.md#built-in-event-listeners) of API Platform. It will be +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. @@ -165,7 +167,7 @@ Complex use cases may lead you to create multiple custom operations. In such a case, you will probably create the same amount of custom controllers while you may not need to perform custom logic inside. -To avoid that, API Platform provides the `ApiPlatform\Action\PlaceholderAction` which behaves the same when using the [built-in operations](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: @@ -370,7 +372,7 @@ resources:
-This way, it will skip the `ReadListener`. You can do the same for some other built-in listeners. See [Built-in Event Listeners](events.md#built-in-event-listeners) +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. diff --git a/symfony/migrate-from-fosrestbundle.md b/symfony/migrate-from-fosrestbundle.md index ed58c337b36..ba24de384fa 100644 --- a/symfony/migrate-from-fosrestbundle.md +++ b/symfony/migrate-from-fosrestbundle.md @@ -33,7 +33,7 @@ Same as above. **In API Platform** -Even though this is not recommended, API Platform allows you to [create custom controllers](../core/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](../core/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. From 53a00ca5f944c2e078df1c63c14c18d5a97ceea8 Mon Sep 17 00:00:00 2001 From: Vincent Amstoutz Date: Mon, 9 Dec 2024 15:56:10 +0100 Subject: [PATCH 76/80] refactor: move symfony messenger from core to symfony (#2075) --- core/design.md | 2 +- core/dto.md | 2 +- core/extending.md | 2 +- outline.yaml | 2 +- {core => symfony}/messenger.md | 8 ++++---- symfony/migrate-from-fosrestbundle.md | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) rename {core => symfony}/messenger.md (93%) diff --git a/core/design.md b/core/design.md index 8253465ecf2..199376426fc 100644 --- a/core/design.md +++ b/core/design.md @@ -40,7 +40,7 @@ Again, it's up to the developers to use, or to not use these built-in state prov 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](messenger.md) and the [DTO support](dto.md). +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: diff --git a/core/dto.md b/core/dto.md index f7296f9eac9..d9040ddfbf1 100644 --- a/core/dto.md +++ b/core/dto.md @@ -74,7 +74,7 @@ In some cases, using an input DTO is a way to avoid serialization groups. ## Use Symfony Messenger With an Input DTO -Let's use a message that will be processed by [Symfony Messenger](https://symfony.com/components/Messenger). API Platform has an [integration with messenger](./messenger.md), to use a DTO as input you need to specify the `input` attribute: +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: ```php Date: Mon, 9 Dec 2024 16:27:07 +0100 Subject: [PATCH 77/80] docs: fix links and update outline (#2103) --- core/dto.md | 2 +- core/extending.md | 4 ++-- core/serialization.md | 3 ++- outline.yaml | 10 +++++----- symfony/index.md | 4 ++-- symfony/jwt.md | 2 +- symfony/security.md | 12 ++++++------ symfony/testing.md | 2 +- symfony/validation.md | 8 ++++---- 9 files changed, 24 insertions(+), 23 deletions(-) diff --git a/core/dto.md b/core/dto.md index d9040ddfbf1..b3c353b7870 100644 --- a/core/dto.md +++ b/core/dto.md @@ -1,6 +1,6 @@ # Using Data Transfer Objects (DTOs) -

Custom Resources screencast
Watch the Custom Resources screencast

+

Custom Resources screencast
Watch the 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. diff --git a/core/extending.md b/core/extending.md index 9a1332c1c4d..9bf25614d43 100644 --- a/core/extending.md +++ b/core/extending.md @@ -14,10 +14,10 @@ The following tables summarizes which extension point to use depending on what y | [Laravel Policies](../laravel/security.md#policies) | custom authorization logic | | [Validation constraints](validation.md) | custom validation logic | | [State Processors](state-processors) | custom business logic and computations to trigger before or after persistence (ex: mail, call to an external API...) | -| [Normalizers](serialization.md#decorating-a-serializer-and-adding-extra-data) | customize the resource sent to the client (add fields in JSON documents, encode codes, dates...) | +| [Normalizers](serialization.md#changing-the-serialization-context-dynamically) | customize the resource sent to the client (add fields in JSON documents, encode codes, dates...) | | [Filters](filters.md) | create filters for collections and automatically document them (OpenAPI, GraphQL, Hydra) | | [Serializer Context Builders](serialization.md#changing-the-serialization-context-dynamically) | change the Serialization context (e.g. groups) dynamically | -| [Messenger Handlers](../symfony/messenger.md) | create 100% custom, RPC, async, service-oriented endpoints (should be used in place of custom controllers because the messenger integration is compatible with both REST and GraphQL, while custom controllers only work with REST) | +| [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) | diff --git a/core/serialization.md b/core/serialization.md index 2701a43f936..400c0ce0d11 100644 --- a/core/serialization.md +++ b/core/serialization.md @@ -192,7 +192,7 @@ documentation generator. ## Using Serialization Groups per Operation -

Relations screencast
Watch the Relations screencast

+

Relations screencast
Watch the Relations screencast

By default, the serializer provided with API Platform represents relations between objects using [dereferenceable IRIs](https://en.wikipedia.org/wiki/Internationalized_Resource_Identifier). They allow you to retrieve details for related objects by issuing extra HTTP requests. However, for performance reasons, @@ -239,6 +239,7 @@ App\ApiResource\Book: normalizationContext: groups: ['get'] operations: + ApiPlatform\Metadata\Get: ~ ApiPlatform\Metadata\Get: ~ ApiPlatform\Metadata\Patch: normalizationContext: diff --git a/outline.yaml b/outline.yaml index d06d9052727..2ab6b4879ea 100644 --- a/outline.yaml +++ b/outline.yaml @@ -9,14 +9,14 @@ chapters: - testing - debugging - caddy - - migrate-from-fosrestbundle - - fosuser-bundle - - nelmio-api-doc - - user - jwt + - messenger + - user - file-upload - controllers - - messenger + - nelmio-api-doc + - migrate-from-fosrestbundle + - fosuser-bundle - title: "API Platform for Laravel" path: laravel items: diff --git a/symfony/index.md b/symfony/index.md index b2e379bcdcb..0073627a427 100644 --- a/symfony/index.md +++ b/symfony/index.md @@ -19,8 +19,8 @@ API Platform is shipped with **[Docker](../deployment/docker-compose.md)** and * 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/) to generate [Next.js](../create-client/) 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/), built on top of React Admin, dynamically created by parsing the API documentation +- [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 diff --git a/symfony/jwt.md b/symfony/jwt.md index 460da9b417a..fbf241a28f2 100644 --- a/symfony/jwt.md +++ b/symfony/jwt.md @@ -5,7 +5,7 @@ > **we recommend adopting open standards such as [OpenID Connect (OIDC)](https://openid.net/connect/)** for robust, scalable, > and interoperable authentication. -

JWT screencast
Watch the LexikJWTAuthenticationBundle screencast

+

JWT screencast
Watch the LexikJWTAuthenticationBundle screencast

## Installing LexikJWTAuthenticationBundle diff --git a/symfony/security.md b/symfony/security.md index a8848cdff9d..e2e97988025 100644 --- a/symfony/security.md +++ b/symfony/security.md @@ -110,8 +110,8 @@ Available variables are: - `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](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](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 @@ -337,17 +337,17 @@ resources: ## 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](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](extensions.md). +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](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](operations.md#enabling-and-disabling-operations) +To completely disable some operations from your application, refer to the [disabling operations](../core/operations.md#enabling-and-disabling-operations) section. ## Changing Serialization Groups Depending of the Current User -See [how to dynamically change](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 4f9cb3b810b..2b658bf4edc 100644 --- a/symfony/testing.md +++ b/symfony/testing.md @@ -377,7 +377,7 @@ If you would like to verify that your stack (including services such as the DBMS 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#running-the-docker-compose-setup-for-production-locally). +for production locally](../deployment/docker-compose.md#deploying-with-docker-compose). ## Testing Utilities for Symfony diff --git a/symfony/validation.md b/symfony/validation.md index ec7eaa6dfd7..49cbc1ff99c 100644 --- a/symfony/validation.md +++ b/symfony/validation.md @@ -104,7 +104,7 @@ error will look like the following if the requested format is JSON-LD (the defau } ``` -Take a look at the [Errors Handling guide](errors.md) to learn how API Platform converts PHP exceptions like validation +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 @@ -136,7 +136,7 @@ class Book With the previous configuration, the validation groups `a` and `b` will be used when validation is performed. -Like for [serialization groups](serialization.md#using-different-serialization-groups-per-operation), +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. @@ -146,7 +146,7 @@ the array of group names. ## Using Validation Groups on Operations -You can have different validation for each [operation](operations.md) related to your resource. +You can have different validation for each [operation](../core/operations.md) related to your resource. ```php getValues()`. Then, define your validation on the getter instead of the property. For example: From d9f20997e1d7b5d12b96bda8bc120172b042af65 Mon Sep 17 00:00:00 2001 From: Samuel NELA Date: Tue, 10 Dec 2024 08:46:53 +0100 Subject: [PATCH 78/80] docs: use attribute for group sequence example (#2078) --- symfony/validation.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/symfony/validation.md b/symfony/validation.md index 49cbc1ff99c..a72e656d818 100644 --- a/symfony/validation.md +++ b/symfony/validation.md @@ -347,9 +347,9 @@ class Greeting * @var A nice person * * I want this "second" validation to be executed after the "first" one even though I wrote them in this order. - * @One(groups={"second"}) - * @Two(groups={"first"}) */ + #[One(groups: ['second'])] + #[Two(groups: ['first'])] #[ORM\Column] public string $name = ''; From 5c1f3e5fe5ae21b1f3dc344dad450405dc4c3827 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Tue, 10 Dec 2024 08:48:10 +0100 Subject: [PATCH 79/80] docs: use newly registered YAML media type (#2054) --- core/content-negotiation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/content-negotiation.md b/core/content-negotiation.md index 7eadcd6b3e6..e52c44b86ca 100644 --- a/core/content-negotiation.md +++ b/core/content-negotiation.md @@ -24,7 +24,7 @@ Format | Format name | [GraphQL](graphql.md) | n/a | n/a | yes [JSON:API](https://jsonapi.org/) | `jsonapi` | `application/vnd.api+json` | yes [HAL](https://stateless.group/hal_specification.html) | `jsonhal` | `application/hal+json` | yes -[YAML](https://yaml.org/) | `yaml` | `application/x-yaml` | no +[YAML](https://yaml.org/) | `yaml` | `application/yaml` | no [CSV](https://tools.ietf.org/html/rfc4180) | `csv` | `text/csv` | no [HTML](https://whatwg.org/) (API docs) | `html` | `text/html` | no [XML](https://www.w3.org/XML/) | `xml` | `application/xml`, `text/xml` | no From be9521d8f3cb3ce5ad78c960736b8640d97d651a Mon Sep 17 00:00:00 2001 From: Vincent Amstoutz Date: Tue, 10 Dec 2024 09:09:13 +0100 Subject: [PATCH 80/80] docs: improve configuration title (#2104) --- core/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/configuration.md b/core/configuration.md index e448a0463e8..86a0c5ec6b4 100644 --- a/core/configuration.md +++ b/core/configuration.md @@ -1,4 +1,4 @@ -# Configuration +# Configuration Reference ## Symfony Configuration