From 83fd0ba2b7fd9ce400a0e6de1550f53a2a5ec46f Mon Sep 17 00:00:00 2001 From: Vincent Amstoutz Date: Wed, 9 Oct 2024 14:09:52 +0200 Subject: [PATCH 1/5] build: bump super-linter from v4 to v7.1.0 --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0e457f53c4d..91eab5a8fde 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,12 +23,12 @@ jobs: fetch-depth: 0 - name: Lint - uses: github/super-linter/slim@v4 + uses: super-linter/super-linter@v7.1.0 env: VALIDATE_ALL_CODEBASE: false VALIDATE_EDITORCONFIG: false VALIDATE_JSCPD: false - DEFAULT_BRANCH: "4.0" + DEFAULT_BRANCH: "origin/4.0" GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - uses: actions/cache@v4 From 6899139698174c863116c16cd7c9fee46f6cfedf Mon Sep 17 00:00:00 2001 From: Vincent Amstoutz Date: Wed, 9 Oct 2024 14:36:16 +0200 Subject: [PATCH 2/5] fix(lint): add permissions to CD, lint & bump GA images --- .github/workflows/cd.yml | 46 ++++++++++++++++++++++------------------ .github/workflows/ci.yml | 6 ++++-- 2 files changed, 29 insertions(+), 23 deletions(-) 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 91eab5a8fde..3adc04d5f86 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,7 +25,7 @@ jobs: fetch-depth: 0 - name: Lint - uses: super-linter/super-linter@v7.1.0 + uses: super-linter/super-linter@v7 env: VALIDATE_ALL_CODEBASE: false VALIDATE_EDITORCONFIG: false From 53cefc881f64310dd746872ff1474a0ba5b1809e Mon Sep 17 00:00:00 2001 From: Vincent Amstoutz Date: Wed, 9 Oct 2024 16:12:26 +0200 Subject: [PATCH 3/5] feat(lint): enable full codebase validation in CI Changed the VALIDATE_ALL_CODEBASE environment variable from false to true (default value) in the GitHub Actions CI workflow. This ensures that the linter checks the entire codebase rather than only changes, potentially catching more issues. --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3adc04d5f86..8e1b77f2511 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,7 +27,6 @@ jobs: - name: Lint uses: super-linter/super-linter@v7 env: - VALIDATE_ALL_CODEBASE: false VALIDATE_EDITORCONFIG: false VALIDATE_JSCPD: false DEFAULT_BRANCH: "origin/4.0" From 493303a793a7ba69e8f0576e9fcaa4735451b8c8 Mon Sep 17 00:00:00 2001 From: Vincent Amstoutz Date: Wed, 9 Oct 2024 16:14:16 +0200 Subject: [PATCH 4/5] fix(ci): lint all project files according to the new config --- .github/ISSUE_TEMPLATE/1_Support_question.md | 7 +- .../ISSUE_TEMPLATE/2_Documentation_issue.md | 1 - .github/workflows/ci.yml | 2 +- .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 +- 94 files changed, 2638 insertions(+), 2609 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/ci.yml b/.github/workflows/ci.yml index 8e1b77f2511..cd5f53ca3e5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,7 @@ jobs: fetch-depth: 0 - name: Lint - uses: super-linter/super-linter@v7 + uses: super-linter/super-linter/slim@v7 env: VALIDATE_EDITORCONFIG: false VALIDATE_JSCPD: false 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 bbe00679493969f2683be0da861bb885e3e987af Mon Sep 17 00:00:00 2001 From: Vincent Amstoutz Date: Wed, 9 Oct 2024 21:52:57 +0200 Subject: [PATCH 5/5] chore(linter): disable MARKDOWN_PRETTIER to resolve conflicts Disabled MARKDOWN_PRETTIER to avoid conflicts with markdownlint. --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cd5f53ca3e5..f845ae2b77e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,6 +29,7 @@ jobs: env: VALIDATE_EDITORCONFIG: false VALIDATE_JSCPD: false + VALIDATE_MARKDOWN_PRETTIER: false DEFAULT_BRANCH: "origin/4.0" GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}