diff --git a/.github/linters/.textlintrc b/.github/linters/.textlintrc
new file mode 100644
index 00000000000..b2ce9c62619
--- /dev/null
+++ b/.github/linters/.textlintrc
@@ -0,0 +1,9 @@
+{
+ "rules": {
+ "terminology": {
+ "exclude": [
+ "Node(?:js)?"
+ ]
+ }
+ }
+}
diff --git a/admin/authentication-support.md b/admin/authentication-support.md
index 4ccad2e326a..85848936b4b 100644
--- a/admin/authentication-support.md
+++ b/admin/authentication-support.md
@@ -4,18 +4,22 @@ API Platform Admin delegates the authentication support to React Admin.
Refer to [the chapter dedicated to authentication in the React Admin documentation](https://marmelab.com/react-admin/Authentication.html)
for more information.
-In short, you have to tweak data provider and api documentation parser, like this:
+In short, you have to tweak the data provider and the API documentation parser like this:
-```javascript
-// admin/src/App.js
+```typescript
+// pwa/pages/admin/index.tsx
-import React from "react";
+import Head from "next/head";
import { Redirect, Route } from "react-router-dom";
-import { HydraAdmin, hydraDataProvider as baseHydraDataProvider, fetchHydra as baseFetchHydra, useIntrospection } from "@api-platform/admin";
-import parseHydraDocumentation from "@api-platform/api-doc-parser/lib/hydra/parseHydraDocumentation";
-import authProvider from "./authProvider";
+import {
+ fetchHydra as baseFetchHydra,
+ hydraDataProvider as baseHydraDataProvider,
+ useIntrospection,
+} from "@api-platform/admin";
+import { parseHydraDocumentation } from "@api-platform/api-doc-parser";
+import authProvider from "utils/authProvider";
+import { ENTRYPOINT } from "config/entrypoint";
-const entrypoint = process.env.REACT_APP_API_ENTRYPOINT;
const getHeaders = () => localStorage.getItem("token") ? {
Authorization: `Bearer ${localStorage.getItem("token")}`,
} : {};
@@ -33,35 +37,53 @@ const RedirectToLogin = () => {
}
return ;
};
-const apiDocumentationParser = async (entrypoint) => {
+const apiDocumentationParser = async () => {
try {
- const { api } = await parseHydraDocumentation(entrypoint, { headers: getHeaders });
- return { api };
+ return await parseHydraDocumentation(ENTRYPOINT, { headers: getHeaders });
} catch (result) {
- if (result.status === 401) {
- // Prevent infinite loop if the token is expired
- localStorage.removeItem("token");
-
- return {
- api: result.api,
- customRoutes: [
-
- ],
- };
+ const { api, response, status } = result;
+ if (status !== 401 || !response) {
+ throw result;
}
- throw result;
+ // Prevent infinite loop if the token is expired
+ localStorage.removeItem("token");
+
+ return {
+ api,
+ response,
+ status,
+ customRoutes: [
+
+ ],
+ };
}
};
-const dataProvider = baseHydraDataProvider(entrypoint, fetchHydra, apiDocumentationParser);
+const dataProvider = baseHydraDataProvider({
+ entrypoint: ENTRYPOINT,
+ httpClient: fetchHydra,
+ apiDocumentationParser,
+});
+
+const AdminLoader = () => {
+ if (typeof window !== "undefined") {
+ const { HydraAdmin } = require("@api-platform/admin");
+ return ;
+ }
+
+ return <>>;
+};
+
+const Admin = () => (
+ <>
+
+ API Platform Admin
+
-export default () => (
-
+
+ >
);
+export default Admin;
```
-For the implementation of the auth provider, you can find a working example in the [API Platform's demo application](https://github.com/api-platform/demo/blob/master/admin/src/authProvider.js).
+For the implementation of the auth provider, you can find a working example in the [API Platform's demo application](https://github.com/api-platform/demo/blob/main/pwa/utils/authProvider.tsx).
diff --git a/admin/components.md b/admin/components.md
index aa50937eabf..b714bc12e92 100644
--- a/admin/components.md
+++ b/admin/components.md
@@ -4,11 +4,11 @@
### AdminGuesser
-`` renders automatically an [ component](https://marmelab.com/react-admin/Admin.html) for resources exposed by a web API documented with any format supported by `@api-platform/api-doc-parser` (for Hydra documented APIs,
-use the [ component](components.md#hydraadmin) instead).
-It also creates a [schema analyzer](components.md#schemaanalyzer) context, where the `schemaAnalyzer` service (for getting information about the provided API documentation) is stored.
+`` renders automatically an [Admin component](https://marmelab.com/react-admin/Admin.html) for resources exposed by a web API documented with any format supported by `@api-platform/api-doc-parser` (for Hydra documented APIs,
+use the [HydraAdmin component](components.md#hydraadmin) instead).
+It also creates a [schema analyzer](components.md#schema-analyzer) context, where the `schemaAnalyzer` service (for getting information about the provided API documentation) is stored.
-`` renders all exposed resources by default, but you can choose what resource you want to render by passing [ components](components.md#resourceguesser) as children.
+`` renders all exposed resources by default, but you can choose what resource you want to render by passing [ResourceGuesser components](components.md#resourceguesser) as children.
Deprecated resources are hidden by default, but you can add them back using an explicit `` component.
```javascript
@@ -21,12 +21,12 @@ const App = () => (
dataProvider={dataProvider}
authProvider={authProvider}>
-
+
)
@@ -45,7 +45,8 @@ export default App;
### ResourceGuesser
-Based on React Admin [ component](https://marmelab.com/react-admin/Resource.html), `ResourceGuesser` provides default props [](components.md#createguesser), [](components.md#listguesser), [](components.md#editguesser) and [](components.md#showguesser).
+Based on React Admin [Resource component](https://marmelab.com/react-admin/Resource.html), `` provides default props [CreateGuesser](components.md#createguesser), [ListGuesser](components.md#listguesser), [EditGuesser](components.md#editguesser) and [ShowGuesser](components.md#showguesser).
+
Otherwise, you can pass it your own CRUD components using `create`, `list`, `edit`, `show` props.
```javascript
@@ -77,18 +78,18 @@ export default App;
|------|--------|-------|----------|--------------------------|
| name | string | - | yes | endpoint of the resource |
-You can also use props accepted by React Admin [ component](https://marmelab.com/react-admin/Resource.html). For example, the props `list`, `show`, `create` or `edit`.
+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`.
## Page Components
### ListGuesser
-Based on React Admin [](https://marmelab.com/react-admin/List.html), ListGuesser displays a list of resources in a [](https://marmelab.com/react-admin/List.html#the-datagrid-component), according to children passed to it (usually [](components.md#fieldguesser) or any [field component](https://marmelab.com/react-admin/Fields.html#basic-fields)
+Based on React Admin [List](https://marmelab.com/react-admin/List.html), `` displays a list of resources in a [Datagrid](https://marmelab.com/react-admin/List.html#the-datagrid-component), according to children passed to it (usually [FieldGuesser](components.md#fieldguesser) or any [field component](https://marmelab.com/react-admin/Fields.html#basic-fields)
available in React Admin).
Use `hasShow` and `hasEdit` props if you want to display `show` and `edit` buttons (both set to `true` by default).
-By default, `` comes with [](components.md#pagination).
+By default, `` comes with [Pagination](components.md#pagination).
```javascript
// BooksList.js
@@ -114,12 +115,12 @@ export const BooksList = props => (
| resource | string | - | yes | endpoint of the resource |
| filters | element | - | no | filters that can be applied to the list |
-You can also use props accepted by React Admin [](https://marmelab.com/react-admin/List.html).
+You can also use props accepted by React Admin [List](https://marmelab.com/react-admin/List.html).
### CreateGuesser
-Displays a creation page for a single item. Uses React Admin [](https://marmelab.com/react-admin/CreateEdit.html) and [](https://marmelab.com/react-admin/CreateEdit.html#the-simpleform-component) components.
-For simple inputs, you can pass as children API Platform Admin [](components.md#inputguesser), or any React Admin [Input components](https://marmelab.com/react-admin/Inputs.html#input-components) for more complex inputs.
+Displays a creation page for a single item. Uses React Admin [Create](https://marmelab.com/react-admin/CreateEdit.html) and [SimpleForm](https://marmelab.com/react-admin/CreateEdit.html#the-simpleform-component) components.
+For simple inputs, you can pass as children API Platform Admin [InputGuesser](components.md#inputguesser), or any React Admin [Input components](https://marmelab.com/react-admin/Inputs.html#input-components) for more complex inputs.
```javascript
// BooksCreate.js
@@ -143,12 +144,12 @@ export const BooksCreate = props => (
| children | node or function | - | no | - |
| resource | string | - | yes | endpoint of the resource |
-You can also use props accepted by React Admin [](https://marmelab.com/react-admin/CreateEdit.html).
+You can also use props accepted by React Admin [Create](https://marmelab.com/react-admin/CreateEdit.html).
### EditGuesser
-Displays an edition page for a single item. Uses React Admin [](https://marmelab.com/react-admin/CreateEdit.html) and [](https://marmelab.com/react-admin/CreateEdit.html#the-simpleform-component) components.
-For simple inputs, you can use API Platform Admin [](components.md#inputguesser), or any React Admin [Input components](https://marmelab.com/react-admin/Inputs.html#input-components) for more complex inputs.
+Displays an edition page for a single item. Uses React Admin [Edit](https://marmelab.com/react-admin/CreateEdit.html) and [SimpleForm](https://marmelab.com/react-admin/CreateEdit.html#the-simpleform-component) components.
+For simple inputs, you can use API Platform Admin [InputGuesser](components.md#inputguesser), or any React Admin [Input components](https://marmelab.com/react-admin/Inputs.html#input-components) for more complex inputs.
```javascript
// BooksEdit.js
@@ -172,11 +173,11 @@ export const BooksEdit = props => (
| children | node or function | - | no | - |
| resource | string | - | yes | endpoint of the resource |
-You can also use props accepted by React Admin [](https://marmelab.com/react-admin/CreateEdit.html).
+You can also use props accepted by React Admin [Edit](https://marmelab.com/react-admin/CreateEdit.html).
### ShowGuesser
-Displays a detailed page for one item. Based on React Admin [ component](https://marmelab.com/react-admin/Show.html). You can pass [](components.md#fieldguesser) as children for simple fields, or use any of React Admin [basic fields](https://marmelab.com/react-admin/Fields.html#basic-fields) for more complex fields.
+Displays a detailed page for one item. Based on React Admin [Show component](https://marmelab.com/react-admin/Show.html). You can pass [FieldGuesser](components.md#fieldguesser) as children for simple fields, or use any of React Admin [basic fields](https://marmelab.com/react-admin/Fields.html#basic-fields) for more complex fields.
```javascript
// BooksShow.js
@@ -200,13 +201,14 @@ export const BooksShow = props => (
| children | node or function | - | no | - |
| resource | string | - | yes | endpoint of the resource |
-You can also use props accepted by React Admin [ component](https://marmelab.com/react-admin/Show.html).
+You can also use props accepted by React Admin [Show component](https://marmelab.com/react-admin/Show.html).
## Hydra
### HydraAdmin
-Creates a complete Admin, as [``](components.md#adminguesser), but configured specially for [Hydra](https://www.hydra-cg.com/). If you want to use other formats (see supported formats: `@api-platform/api-doc-parser`) use [](components.md#adminguesser) instead.
+Creates a complete Admin, as [AdminGuesser](components.md#adminguesser), but configured specially for [Hydra](https://www.hydra-cg.com/).
+If you want to use other formats (see supported formats: `@api-platform/api-doc-parser`) use [AdminGuesser](components.md#adminguesser) instead.
```javascript
// App.js
@@ -228,13 +230,20 @@ export default App;
#### HydraAdmin Props
-| Name | Type | Value | required | Description |
-|------------|--------|-------|----------|-----------------------|
-| entrypoint | string | - | yes | entrypoint of the API |
+| Name | Type | Value | required | Description |
+|------------|----------------|-------|----------|------------------------------|
+| entrypoint | string | - | yes | entrypoint of the API |
+| mercure | boolean|object | * | yes | 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
### Data Provider
-Based on React Admin `create`, `delete`, `getList`, `getManyReference`, `getOne`, `update` methods, the `dataProvider` is used by API Platform Admin to communicate with the API. In addition, the specific `introspect` method parses your API documentation.
+Based on React Admin `create`, `delete`, `getList`, `getManyReference`, `getOne`, `update` methods, the `dataProvider` is used by API Platform Admin to communicate with the API.
+In addition, the specific `introspect` method parses your API documentation.
Note that the `dataProvider` can be overridden to fit your API needs.
### Schema Analyzer
@@ -245,13 +254,15 @@ Analyses your resources and retrieves their types according to the [Schema.org](
### Pagination
-Set by default in the [ component](components.md#listguesser), the `Pagination` component uses React Admin [ component](https://marmelab.com/react-admin/List.html#pagination).
-By default, it renders 30 items per page and displays a navigation UI. If you want to change the number of items per page or disable the pagination, see the [Pagination documentation](../core/pagination.md).
+Set by default in the [ListGuesser component](components.md#listguesser), the `Pagination` component uses React Admin [Pagination component](https://marmelab.com/react-admin/List.html#pagination).
+By default, it renders 30 items per page and displays a navigation UI.
+If you want to change the number of items per page or disable the pagination, see the [Pagination documentation](../core/pagination.md).
It is also capable to handle partial pagination.
### FieldGuesser
-Renders fields according to their types, using the [schema analyzer](components.md#schemaanalyzer). Based on React Admin [field components](https://marmelab.com/react-admin/Fields.html).
+Renders fields according to their types, using the [schema analyzer](components.md#schemaanalyzer).
+Based on React Admin [field components](https://marmelab.com/react-admin/Fields.html).
```javascript
// BooksShow.js
diff --git a/admin/customizing.md b/admin/customizing.md
index f1c5ebe658b..7a3f68643dc 100644
--- a/admin/customizing.md
+++ b/admin/customizing.md
@@ -6,7 +6,7 @@ To do so, you can use the React components provided by API Platform Admin itself
## Customizing the Admin's Main Page and the Resource List
-By default, API Platform Admin automatically builds a tailored [`` component](https://marmelab.com/react-admin/Resource.html) (and all its appropriate children) for each resource type exposed by a web API.
+By default, API Platform Admin automatically builds a tailored [Resource component](https://marmelab.com/react-admin/Resource.html) (and all its appropriate children) for each resource type exposed by a web API.
Under the hood it uses the `@api-platform/api-doc-parser` library to parse the API documentation. The API documentation can use Hydra, OpenAPI and any other format supported by the library.
Resources are listed in the order they appear in the machine-readable documentation.
@@ -14,7 +14,6 @@ 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 React from "react";
import { HydraAdmin, ResourceGuesser } from "@api-platform/admin";
export default () => (
@@ -28,14 +27,13 @@ export default () => (
);
```
-Instead of using the `` component provided by API Platform Admin, you can also pass custom React Admin's [`` components](https://marmelab.com/react-admin/Resource.html), or any other React components that are supported by React Admin's [``](https://marmelab.com/react-admin/Admin.html).
+Instead of using the `` component provided by API Platform Admin, you can also pass custom React Admin's [Resource components](https://marmelab.com/react-admin/Resource.html), or any other React components that are supported by React Admin's [Admin](https://marmelab.com/react-admin/Admin.html).
## Customizing the List View
The list view can be customized following the same pattern:
```javascript
-import React from "react";
import {
HydraAdmin,
ResourceGuesser,
@@ -70,7 +68,6 @@ In addition to the `` component, [all React Admin Fields component
For the show view:
```javascript
-import React from "react";
import {
HydraAdmin,
ResourceGuesser,
@@ -107,7 +104,6 @@ In addition to the `` component, [all React Admin Fields component
Again, the same logic applies to forms. Here is how to customize the create form:
```javascript
-import React from "react";
import {
HydraAdmin,
ResourceGuesser,
@@ -146,7 +142,6 @@ For instance, using an autocomplete input is straightforward, [check out the ded
Finally, you can customize the edit form the same way:
```javascript
-import React from "react";
import {
HydraAdmin,
ResourceGuesser,
@@ -183,6 +178,6 @@ For instance, using an autocomplete input is straightforward, [checkout the dedi
## Going Further
API Platform is built on top of [React Admin](https://marmelab.com/react-admin/).
-You can use all the features provided by the underlying library with API Platform Admin, including support for [file upload](https://marmelab.com/react-admin/DataProviders.html#decorating-your-data-provider-example-of-file-upload), [authentication](https://marmelab.com/react-admin/Authentication.html), [authorization](https://marmelab.com/react-admin/Authorization.html) and deeper customization.
+You can use all the features provided by the underlying library with API Platform Admin, including support for [authentication](https://marmelab.com/react-admin/Authentication.html), [authorization](https://marmelab.com/react-admin/Authorization.html) and deeper customization.
To learn more about these capabilities, refer to [the React Admin documentation](https://marmelab.com/react-admin/).
diff --git a/admin/getting-started.md b/admin/getting-started.md
index ffe3f51fe12..621df7200d3 100644
--- a/admin/getting-started.md
+++ b/admin/getting-started.md
@@ -30,7 +30,6 @@ 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 React from "react";
import { HydraAdmin } from "@api-platform/admin";
// Replace with your own API entrypoint
diff --git a/admin/handling-relations.md b/admin/handling-relations.md
index 16be8ba9b7d..53ac42bcfda 100644
--- a/admin/handling-relations.md
+++ b/admin/handling-relations.md
@@ -16,23 +16,23 @@ you can keep the embedded data by setting the `useEmbedded` parameter of the Hyd
```javascript
// admin/src/App.js
-import React from "react";
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(
+const dataProvider = hydraDataProvider({
entrypoint,
- fetchHydra,
- parseHydraDocumentation,
- true // useEmbedded parameter
-);
+ httpClient: fetchHydra,
+ apiDocumentationParser: parseHydraDocumentation,
+ mercure: true,
+ useEmbedded: true,
+});
export default () => (
);
```
@@ -79,7 +79,6 @@ For instance, if your API returns:
If you want to display the author first name in the list, you need to write the following code:
```javascript
-import React from "react";
import {
HydraAdmin,
FieldGuesser,
@@ -112,7 +111,6 @@ export default () => (
If the `useEmbedded` parameter is set to `true` (will be the default behavior in 3.0), you need to use the dot notation to display a field:
```javascript
-import React from "react";
import {
HydraAdmin,
FieldGuesser,
@@ -155,23 +153,15 @@ namespace App\Entity;
use ApiPlatform\Metadata\ApiResource;
use Doctrine\ORM\Mapping as ORM;
-/**
- * @ORM\Entity
- */
+#[ORM\Entity]
#[ApiResource]
class Review
{
- /**
- * @ORM\Column(type="integer")
- * @ORM\GeneratedValue
- * @ORM\Id
- */
- public $id;
-
- /**
- * @ORM\ManyToOne(targetEntity=Book::class, inversedBy="reviews")
- */
- public $book;
+ #[ORM\Id, ORM\Column, ORM\GeneratedValue]
+ public ?int $id = null;
+
+ #[ORM\ManyToOne]
+ public Book $book;
}
```
@@ -184,30 +174,21 @@ use ApiPlatform\Core\Annotation\ApiFilter;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\SearchFilter;
use Doctrine\Common\Collections\ArrayCollection;
+use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
-/**
- * @ORM\Entity
- */
+#[ORM\Entity]
#[ApiResource]
class Book
{
- /**
- * @ORM\Column(type="integer")
- * @ORM\GeneratedValue
- * @ORM\Id
- */
- public $id;
-
- /**
- * @ORM\Column
- * @ApiFilter(SearchFilter::class, strategy="ipartial")
- */
- public $title;
-
- /**
- * @ORM\OneToMany(targetEntity=Review::class, mappedBy="book")
- */
+ #[ORM\Id, ORM\Column, ORM\GeneratedValue]
+ public ?int $id = null;
+
+ #[ORM\Column]
+ #[ApiFilter(SearchFilter::class, strategy: 'ipartial')]
+ public string $title;
+
+ #[ORM\OneToMany(targetEntity: Review::class, mappedBy: 'book')]
public $reviews;
public function __construct()
@@ -222,7 +203,6 @@ Notice the "partial search" [filter](../core/filters.md) on the `title` property
Now, let's configure API Platform Admin to enable autocompletion for the relation selector:
```javascript
-import React from "react";
import {
HydraAdmin,
ResourceGuesser,
@@ -284,7 +264,6 @@ If the book is embedded into a review and if the `useEmbedded` parameter is set
you need to change the `ReferenceInput` for the edit component:
```javascript
-import React from "react";
import {
HydraAdmin,
ResourceGuesser,
diff --git a/admin/index.md b/admin/index.md
index 50ea934f54d..a6dffa4ba06 100644
--- a/admin/index.md
+++ b/admin/index.md
@@ -8,7 +8,7 @@ for any API supporting [the Hydra Core Vocabulary](http://www.hydra-cg.com/) or
API Platform Admin is the perfect companion of APIs created
using [the API Platform framework](https://api-platform.com), but also supports APIs written with any other programming language or framework as long as they expose a standard Hydra API documentation.
-API Platform Admin is a 100% standalone Single-Page-Application with no coupling to the server part,
+API Platform Admin is a 100% standalone Single-Page-Application written in TypeScript with no coupling to the server part,
according to the API-first paradigm.
API Platform Admin parses the API documentation then uses the awesome [React Admin](https://marmelab.com/react-admin/)
@@ -28,4 +28,5 @@ You can **customize everything** by using provided React Admin and [Material UI]
* 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
* Nicely displays server-side errors (e.g. advanced validation)
+* Supports real-time updates with [Mercure](https://mercure.rocks)
* **100% customizable**
diff --git a/admin/performance.md b/admin/performance.md
index 92dd8b7d544..a94e38edf70 100644
--- a/admin/performance.md
+++ b/admin/performance.md
@@ -24,23 +24,15 @@ use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Metadata\ApiResource;
use Doctrine\ORM\Mapping as ORM;
-/**
- * @ORM\Entity
- */
+#[ORM\Entity]
#[ApiResource]
class Author
{
- /**
- * @ORM\Column(type="integer")
- * @ORM\GeneratedValue
- * @ORM\Id
- */
+ #[ORM\Id, ORM\Column, ORM\GeneratedValue]
#[ApiFilter(SearchFilter::class, strategy: "exact")]
- public $id;
+ public ?int $id = null;
- /**
- * @ORM\Column
- */
- public $name;
+ #[ORM\Column]
+ public string $name;
}
```
diff --git a/admin/real-time-mercure.md b/admin/real-time-mercure.md
new file mode 100644
index 00000000000..30dcb3b9aea
--- /dev/null
+++ b/admin/real-time-mercure.md
@@ -0,0 +1,43 @@
+# Real-time Updates With Mercure
+
+API Platform Admin support real-time updates by using the [Mercure protocol](https://mercure.rocks).
+
+Updates are received by using the `useMercureSubscription` hook in the `ListGuesser`, `ShowGuesser` and `EditGuesser` components.
+
+To enable Mercure server-side, see the [related documentation](../core/mercure.md).
+
+Once enabled, API Platform Admin will automatically detect that Mercure is enabled and will discover the Mercure hub URL by itself.
+
+## Advanced Configuration
+
+If you want to customize the default Mercure configuration, you can either do it with a prop in the `` component:
+
+```javascript
+import { HydraAdmin } from "@api-platform/admin";
+
+export default () => (
+
+);
+```
+
+Or in the data provider factory:
+
+```javascript
+import { hydraDataProvider, fetchHydra } from "@api-platform/admin";
+import { parseHydraDocumentation } from "@api-platform/api-doc-parser";
+
+const dataProvider = baseHydraDataProvider({
+ entrypoint,
+ httpClient: fetchHydra,
+ apiDocumentationParser: parseHydraDocumentation,
+ mercure: { hub: "https://mercure.rocks/hub" },
+});
+```
+
+The `mercure` object can take the following properties:
+- `hub`: the URL to your Mercure hub (default value: null ; when null it will be discovered by using API responses)
+- `jwt`: a subscriber JWT to access your Mercure hub (default value: null)
+- `topicUrl`: the topic URL of your resources (default value: entrypoint)
diff --git a/core/controllers.md b/core/controllers.md
index 89abcd93810..9d86109edf4 100644
--- a/core/controllers.md
+++ b/core/controllers.md
@@ -71,6 +71,7 @@ The entity is retrieved in the `__invoke` method thanks to a dedicated argument
When using `GET`, the `__invoke()` method parameter will receive the identifier and should be called the same as the resource identifier.
So for the path `/user/{uuid}/bookmarks`, you must use `__invoke(string $uuid)`.
+**Warning: the `__invoke()` method parameter [MUST be called `$data`](https://symfony.com/doc/current/components/http_kernel.html#4-getting-the-controller-arguments)**, otherwise, it will not be filled correctly!
Services (`$bookPublishingHandler` here) are automatically injected thanks to the autowiring feature. You can type-hint any service
you need and it will be autowired too.
diff --git a/core/data-persisters.md b/core/data-persisters.md
index a94ae76ec4d..dee1d679f7d 100644
--- a/core/data-persisters.md
+++ b/core/data-persisters.md
@@ -148,7 +148,8 @@ Even with service autowiring and autoconfiguration enabled, you must still confi
services:
# ...
App\DataPersister\UserDataPersister:
- decorates: 'api_platform.doctrine.orm.data_persister'
+ bind:
+ $decorated: '@api_platform.doctrine.orm.data_persister'
# Uncomment only if autoconfiguration is disabled
#arguments: ['@App\DataPersister\UserDataPersister.inner']
#tags: [ 'api_platform.data_persister' ]
diff --git a/core/events.md b/core/events.md
index 3fbcb24da33..ae1a444d01d 100644
--- a/core/events.md
+++ b/core/events.md
@@ -61,7 +61,7 @@ Attribute | Type | Default | Description
Registering your own event listeners to add extra logic is convenient.
-The [`ApiPlatform\Core\EventListener\EventPriorities`](https://github.com/api-platform/core/blob/main/src/Core/EventListener/EventPriorities.php) class comes with a convenient set of class constants corresponding to commonly used priorities:
+The [`ApiPlatform\Core\EventListener\EventPriorities`](https://github.com/api-platform/core/blob/2.6/src/EventListener/EventPriorities.php) class comes with a convenient set of class constants corresponding to commonly used priorities:
Constant | Event | Priority |
-------------------|-------------------|----------|
@@ -92,12 +92,14 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\ViewEvent;
use Symfony\Component\HttpKernel\KernelEvents;
+use Symfony\Component\Mime\Email;
+use Symfony\Component\Mailer\MailerInterface;
final class BookMailSubscriber implements EventSubscriberInterface
{
private $mailer;
- public function __construct(\Swift_Mailer $mailer)
+ public function __construct(MailerInterface $mailer)
{
$this->mailer = $mailer;
}
@@ -118,10 +120,11 @@ final class BookMailSubscriber implements EventSubscriberInterface
return;
}
- $message = (new \Swift_Message('A new book has been added'))
- ->setFrom('system@example.com')
- ->setTo('contact@les-tilleuls.coop')
- ->setBody(sprintf('The book #%d has been added.', $book->getId()));
+ $message = (new Email())
+ ->from('system@example.com')
+ ->to('contact@les-tilleuls.coop')
+ ->subject('A new book has been added')
+ ->text(sprintf('The book #%d has been added.', $book->getId()));
$this->mailer->send($message);
}
diff --git a/core/extensions.md b/core/extensions.md
index 5d195dda842..4915574f0d9 100644
--- a/core/extensions.md
+++ b/core/extensions.md
@@ -44,11 +44,8 @@ use ApiPlatform\Metadata\ApiResource;
#[ApiResource]
class Offer
{
- /**
- * @var User
- * @ORM\ManyToOne(targetEntity="User")
- */
- public $user;
+ #[ORM\ManyToOne]
+ public User $user;
//...
}
diff --git a/core/file-upload.md b/core/file-upload.md
index cdf0cddfe1b..387e5e6c45d 100644
--- a/core/file-upload.md
+++ b/core/file-upload.md
@@ -42,6 +42,9 @@ resource (in our case: `Book`).
This example will use a custom controller to receive the file.
The second example will use a custom `multipart/form-data` decoder to deserialize the resource instead.
+**Note**: Uploading files won't work in `PUT` or `PATCH` requests, you must use `POST` method to upload files.
+See [the related issue on Symfony](https://github.com/symfony/symfony/issues/9226) and [the related bug in PHP](https://bugs.php.net/bug.php?id=55815) talking about this behavior.
+
### Configuring the Resource Receiving the Uploaded File
The `MediaObject` resource is implemented like this:
@@ -64,9 +67,9 @@ use Symfony\Component\Validator\Constraints as Assert;
use Vich\UploaderBundle\Mapping\Annotation as Vich;
/**
- * @ORM\Entity
* @Vich\Uploadable
*/
+#[ORM\Entity]
#[ApiResource(
normalizationContext: ['groups' => ['media_object:read']],
types: ['http://schema.org/MediaObject']
@@ -97,11 +100,7 @@ use Vich\UploaderBundle\Mapping\Annotation as Vich;
)]
class MediaObject
{
- /**
- * @ORM\Column(type="integer")
- * @ORM\GeneratedValue
- * @ORM\Id
- */
+ #[ORM\Id, ORM\Column, ORM\GeneratedValue]
private ?int $id = null;
#[ApiProperty(types: ['http://schema.org/contentUrl'])]
@@ -114,9 +113,7 @@ class MediaObject
#[Assert\NotNull(groups: ['media_object_create'])]
public ?File $file = null;
- /**
- * @ORM\Column(nullable=true)
- */
+ #[ORM\Column(nullable: true)]
public ?string $filePath = null;
public function getId(): ?int
@@ -257,18 +254,14 @@ use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\HttpFoundation\File\File;
use Vich\UploaderBundle\Mapping\Annotation as Vich;
-/**
- * @ORM\Entity
- */
+#[ORM\Entity]
#[ApiResource(types: ['http://schema.org/Book'])]
class Book
{
// ...
- /**
- * @ORM\ManyToOne(targetEntity=MediaObject::class)
- * @ORM\JoinColumn(nullable=true)
- */
+ #[ORM\ManyToOne(targetEntity: MediaObject::class)]
+ #[ORM\JoinColumn(nullable: true)]
#[ApiProperty(types: ['http://schema.org/image'])]
public ?MediaObject $image = null;
@@ -362,9 +355,9 @@ use Symfony\Component\Serializer\Annotation\Groups;
use Vich\UploaderBundle\Mapping\Annotation as Vich;
/**
- * @ORM\Entity
* @Vich\Uploadable
*/
+#[ORM\Entity]
#[ApiResource(
normalizationContext: ['groups' => ['book:read']],
denormalizationContext: ['groups' => ['book:write']],
@@ -386,9 +379,7 @@ 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 22569d7d1ea..0a7868e06b0 100644
--- a/core/filters.md
+++ b/core/filters.md
@@ -1491,6 +1491,7 @@ doctrine:
filters:
user_filter:
class: App\Filter\UserFilter
+ enabled: true
```
Done: Doctrine will automatically filter all `UserAware`entities!
@@ -1517,12 +1518,10 @@ use App\Entity\DummyCarColor;
#[ApiResource]
class DummyCar
{
- #[ORM\Id]
- #[ORM\GeneratedValue]
- #[ORM\Column(type: 'integer')]
+ #[ORM\Id, ORM\Column, ORM\GeneratedValue]
private ?int $id = null;
- #[ORM\Column(type: 'string')]
+ #[ORM\Column]
#[ApiFilter(SearchFilter::class, strategy: 'partial')]
public ?string $name = null;
diff --git a/core/fosuser-bundle.md b/core/fosuser-bundle.md
index 3ec7cf53639..ba54f1694e3 100644
--- a/core/fosuser-bundle.md
+++ b/core/fosuser-bundle.md
@@ -62,37 +62,29 @@ use FOS\UserBundle\Model\User as BaseUser;
use FOS\UserBundle\Model\UserInterface;
use Symfony\Component\Serializer\Annotation\Groups;
-/**
- * @ORM\Entity
- * @ORM\Table(name="fos_user")
- */
- #[ApiResource(
+#[ORM\Entity]
+#[ORM\Table(name: 'fos_user')]
+#[ApiResource(
normalizationContext: ['groups' => ['user', 'user:read']],
denormalizationContext: ['groups' => ['user', 'user:write']],
- )]
+)]
class User extends BaseUser
{
- /**
- * @ORM\Id
- * @ORM\Column(type="integer")
- * @ORM\GeneratedValue(strategy="AUTO")
- */
- protected $id;
+ #[ORM\Id, ORM\Column, ORM\GeneratedValue]
+ protected ?int $id = null;
#[Groups("user")]
- protected $email;
+ protected string $email;
- /**
- * @ORM\Column(type="string", length=255, nullable=true)
- */
+ #[ORM\Column(nullable: true)]
#[Groups("user")]
- protected $fullname;
+ protected string $fullname;
#[Groups("user:write")]
- protected $plainPassword;
+ protected string $plainPassword;
#[Groups("user")]
- protected $username;
+ protected string $username;
public function setFullname(?string $fullname): void
{
diff --git a/core/getting-started.md b/core/getting-started.md
index 3e85f3ff7bf..7fb4c6c1cf1 100644
--- a/core/getting-started.md
+++ b/core/getting-started.md
@@ -46,24 +46,18 @@ use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\Validator\Constraints as Assert;
-/**
- * @ORM\Entity
- */
+#[ORM\Entity]
#[ApiResource]
class Product // The class name will be used to name exposed resources
{
- /**
- * @ORM\Id
- * @ORM\GeneratedValue(strategy="AUTO")
- * @ORM\Column(type="integer")
- */
+ #[ORM\Id, ORM\Column, ORM\GeneratedValue]
private ?int $id = null;
/**
* A name property - this description will be available in the API documentation too.
*
- * @ORM\Column
*/
+ #[ORM\Column]
#[Assert\NotBlank]
public string $name = '';
@@ -71,8 +65,8 @@ class Product // The class name will be used to name exposed resources
/**
* @var Offer[]|ArrayCollection
*
- * @ORM\OneToMany(targetEntity="Offer", mappedBy="product", cascade={"persist"})
*/
+ #[ORM\OneToMany(targetEntity: Offer::class, mappedBy: 'product', cascade: ['persist'])]
public iterable $offers;
public function __construct()
@@ -115,32 +109,22 @@ use Symfony\Component\Validator\Constraints as Assert;
/**
* An offer from my shop - this description will be automatically extracted from the PHPDoc to document the API.
*
- * @ORM\Entity
*/
+#[ORM\Entity]
#[ApiResource(types: ['http://schema.org/Offer'])]
class Offer
{
- /**
- * @ORM\Column(type="integer")
- * @ORM\Id
- * @ORM\GeneratedValue(strategy="AUTO")
- */
+ #[ORM\Id, ORM\Column, ORM\GeneratedValue]
private ?int $id = null;
- /**
- * @ORM\Column(type="text")
- */
+ #[ORM\Column(type: 'text')]
public string $description = '';
- /**
- * @ORM\Column(type="float")
- */
+ #[ORM\Column]
#[Assert\Range(minMessage: 'The price must be superior to 0.', min: 0)]
public float $price = -1.0;
- /**
- * @ORM\ManyToOne(targetEntity="Product", inversedBy="offers")
- */
+ #[ORM\ManyToOne(targetEntity: Product::class, inversedBy: 'offers')]
public ?Product $product = null;
public function getId(): ?int
diff --git a/core/graphql.md b/core/graphql.md
index f8623c31526..8f038d96cfb 100644
--- a/core/graphql.md
+++ b/core/graphql.md
@@ -380,7 +380,7 @@ For each resource, three mutations are available: one for creating it (`create`)
When updating or deleting a resource, you need to pass the **IRI** of the resource as argument. See [Global Object Identifier](#global-object-identifier) for more information.
-### Client Mutation Id
+### Client Mutation ID
Following the [Relay Input Object Mutations Specification](https://github.com/facebook/relay/blob/v7.1.0/website/spec/Mutations.md#relay-input-object-mutations-specification),
you can pass a `clientMutationId` as argument and can ask its value as a field.
@@ -583,7 +583,7 @@ You can also pass `clientSubscriptionId` as argument and can ask its value as a
In the payload of the subscription, the given fields of the resource will be the fields you subscribe to: if any of these fields is updated, you will be pushed their updated values.
-The `mercureUrl` field is the Mercure URL you need to use to [subscribe to the updates](https://mercure.rocks/docs/getting-started#subscribing) on the client side.
+The `mercureUrl` field is the Mercure URL you need to use to [subscribe to the updates](https://mercure.rocks/docs/getting-started#subscribing) on the client-side.
### Receiving an Update
@@ -1642,9 +1642,7 @@ class Book
public $title;
- /**
- * @ORM\OneToMany(targetEntity="Book")
- */
+ #[ORM\OneToMany(targetEntity: Book::class)]
public $relatedBooks;
// ...
@@ -1959,9 +1957,9 @@ use Symfony\Component\Validator\Constraints as Assert;
use Vich\UploaderBundle\Mapping\Annotation as Vich;
/**
- * @ORM\Entity
* @Vich\Uploadable
*/
+#[ORM\Entity]
#[ApiResource(
normalizationContext: ['groups' => ['media_object_read']],
types: ['http://schema.org/MediaObject']
@@ -1979,36 +1977,21 @@ use Vich\UploaderBundle\Mapping\Annotation as Vich;
)]
class MediaObject
{
- /**
- * @var int|null
- *
- * @ORM\Column(type="integer")
- * @ORM\GeneratedValue
- * @ORM\Id
- */
- protected $id;
+ #[ORM\Id, ORM\Column, ORM\GeneratedValue]
+ protected ?int $id = null;
- /**
- * @var string|null
- */
- #[Groups(['media_object_read'])]
#[ApiProperty(types: ['http://schema.org/contentUrl'])]
- public $contentUrl;
+ #[Groups(['media_object_read'])]
+ public ?string $contentUrl = null;
/**
- * @var File|null
- *
- * @Assert\NotNull(groups={"media_object_create"})
* @Vich\UploadableField(mapping="media_object", fileNameProperty="filePath")
*/
- public $file;
+ #[Assert\NotNull(groups: ['media_object_create'])]
+ public ?File $file = null;
- /**
- * @var string|null
- *
- * @ORM\Column(nullable=true)
- */
- public $filePath;
+ #[ORM\Column(nullable: true)]
+ public ?string $filePath = null;
public function getId(): ?int
{
diff --git a/core/identifiers.md b/core/identifiers.md
index 3bbec3c82fa..cf34b7d8045 100644
--- a/core/identifiers.md
+++ b/core/identifiers.md
@@ -155,26 +155,18 @@ use ApiPlatform\Metadata\ApiResource;
use App\Uuid;
use Doctrine\ORM\Mapping as ORM;
-/**
- * @ORM\Entity
- */
+#[ORM\Entity]
#[ApiResource]
final class Person
{
- /**
- * @var int
- *
- * @ORM\Id()
- * @ORM\GeneratedValue()
- * @ORM\Column(type="integer")
- */
+ #[ORM\Id, ORM\Column, ORM\GeneratedValue]
#[ApiProperty(identifier: false)]
- private $id;
+ private ?int $id = null;
/**
* @var Uuid
- * @ORM\Column(type="uuid", unique=true)
*/
+ #[ORM\Column(type: 'uuid', unique: true)]
#[ApiProperty(identifier: true)]
public $code;
diff --git a/core/json-schema.md b/core/json-schema.md
index 367d606da00..a0896bcea09 100644
--- a/core/json-schema.md
+++ b/core/json-schema.md
@@ -49,9 +49,7 @@ use Doctrine\ORM\Mapping as ORM;
#[ApiResource]
class Greeting
{
- #[ORM\Id]
- #[ORM\GeneratedValue]
- #[ORM\Column(type: "integer")]
+ #[ORM\Id, ORM\Column, ORM\GeneratedValue]
private ?int $id = null;
// [...]
diff --git a/core/jwt.md b/core/jwt.md
index 7e1eff6deaf..8fa95c8e75b 100644
--- a/core/jwt.md
+++ b/core/jwt.md
@@ -342,3 +342,26 @@ class AuthenticationTest extends ApiTestCase
```
Refer to [Testing the API](../distribution/testing.md) for more information about testing API Platform.
+
+### Improving Tests Suite Speed
+
+Since now we have a `JWT` authentication, functional tests require us to log in each time we want to test an API endpoint. This is where [Password Hashers](https://symfony.com/doc/current/security/passwords.html) come into play.
+
+Hashers are used for 2 reasons:
+
+1. To generate a hash for a raw password (`self::$container->get('security.user_password_hasher')->hashPassword($user, '$3CR3T')`)
+2. To verify a password during authentication
+
+While hashing and verifying 1 password is quite a fast operation, doing it hundreds or even thousands of times in a tests suite becomes a bottleneck, because reliable hashing algorithms are slow by their nature.
+
+To significantly improve the test suite speed, we can use more simple password hasher specifically for the `test` environment.
+
+```yaml
+# override in api/config/packages/test/security.yaml for test env
+security:
+ password_hashers:
+ App\Entity\User:
+ algorithm: md5
+ encode_as_base64: false
+ iterations: 0
+```
diff --git a/core/mercure.md b/core/mercure.md
index 975b2db1f24..9724bb29b80 100644
--- a/core/mercure.md
+++ b/core/mercure.md
@@ -16,26 +16,9 @@ Then, the Mercure hub dispatches the updates to all connected clients using [Ser
Mercure support is already installed, configured and enabled in [the API Platform distribution](../distribution/index.md).
If you use the distribution, you have nothing more to do, and you can skip to the next section.
-If you have installed API Platform using another method (such as `composer require api`), you need to install a Mercure hub, and the [Symfony MercureBundle](https://symfony.com/doc/current/mercure.html):
+If you have installed API Platform using another method (such as `composer require api`), you need to install [a Mercure hub](https://mercure.rocks/docs/getting-started) and the Symfony MercureBundle.
-First, [download and run a Mercure hub](https://mercure.rocks/docs/hub/install).
-Then, install the Symfony bundle:
-
-```console
-composer require symfony/mercure-bundle
-```
-
-Finally, 3 environment variables [must be set](https://symfony.com/doc/current/configuration.html#configuration-based-on-environment-variables):
-
-* `MERCURE_URL`: the URL that must be used by API Platform to publish updates to your Mercure hub (can be an internal or a public URL)
-* `MERCURE_PUBLIC_URL`: the **public** URL of the Mercure hub that clients will use to subscribe to updates
-* `MERCURE_JWT_SECRET`: a valid Mercure [JSON Web Token (JWT)](https://jwt.io/) allowing API Platform to publish updates to the hub
-
-The JWT **must** contain a `mercure.publish` property containing an array of topic selectors.
-This array can be empty to allow publishing anonymous updates only. It can also be `["*"]` to allow publishing on every topics.
-[Example publisher JWT](https://jwt.io/#debugger-io?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdfX0.obDjwCgqtPuIvwBlTxUEmibbBf0zypKCNzNKP7Op2UM) (demo key: `!ChangeMe!`).
-
-[Learn more about Mercure authorization.](https://mercure.rocks/spec#authorization)
+[Learn how to install and configure MercureBundle manually on the Symfony website](https://symfony.com/doc/current/mercure.html)
## Pushing the API Updates
@@ -71,7 +54,7 @@ Clients generated using [the API Platform Client Generator](../client-generator/
## Dispatching Private Updates (Authorized Mode)
-Mercure allows to dispatch [private updates, that will be received only by authorized clients](https://mercure.rocks/spec#authorization).
+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.
Then, use options to mark the published updates as privates:
@@ -114,7 +97,7 @@ In addition to `private`, the following options are available:
* `topics`: the list of topics of this update, if not the resource IRI is used
* `data`: the content of this update, if not set the content will be the serialization of the resource using the default format
-* `id`: the SSE id of this event, if not set the ID will be generated by the mercure Hub
+* `id`: the SSE id of this event, if not set the ID will be generated by the Mercure Hub
* `type`: the SSE type of this event, if not set this field is omitted
* `retry`: the `retry` field of the SSE, if not set this field is omitted
* `normalization_context`: the specific normalization context to use for the update.
diff --git a/core/migrate-from-fosrestbundle.md b/core/migrate-from-fosrestbundle.md
index 6b0c05462e3..eb432cda322 100644
--- a/core/migrate-from-fosrestbundle.md
+++ b/core/migrate-from-fosrestbundle.md
@@ -21,7 +21,7 @@ See [The view layer](https://github.com/FriendsOfSymfony/FOSRestBundle/blob/3.x/
Add the `ApiResource` attribute to your entities, and enable operations you desire inside. By default, every operations are activated.
-See [Operations](../operations.md).
+See [Operations](operations.md).
### Make custom controllers
@@ -31,11 +31,11 @@ Same as above.
**In API Platform**
-Even though this is not recommended, API Platform allows you to [create custom controllers](../controllers.md) and declare them in your entity's `ApiResource` attribute.
+Even though this is not recommended, API Platform allows you to [create custom controllers](controllers.md) and declare them in your entity's `ApiResource` attribute.
-You can use them as you migrate from FOSRestBundle, but you should consider [switching to Symfony Messenger](../messenger.md) as it will give you more benefits, such as compatibility with both REST and GraphQL, and better performances of your API on big tasks.
+You can use them as you migrate from FOSRestBundle, but you should consider [switching to Symfony Messenger](messenger.md) as it will give you more benefits, such as compatibility with both REST and GraphQL, and better performances of your API on big tasks.
-See [General Design Considerations](../design.md).
+See [General Design Considerations](design.md).
### Routing system (with native documentation support)
@@ -50,7 +50,7 @@ See [Full default annotations](https://github.com/FriendsOfSymfony/FOSRestBundle
Use the `ApiResource` attribute to activate the HTTP methods you need for your entity. By default, all the methods are enabled.
-See [Operations](../operations.md).
+See [Operations](operations.md).
### Hook into the requests handling
@@ -64,7 +64,7 @@ See [Listener support](https://github.com/FriendsOfSymfony/FOSRestBundle/blob/3.
API Platform provides a lot of ways to customize the behavior of your API, depending on what you exactly want to do.
-See [Extending API Platform](../extending.md) for more details.
+See [Extending API Platform](extending.md) for more details.
### Customize the formats of the requests and the responses
@@ -82,7 +82,7 @@ Both the request and the response body's format can be customized.
You can configure the formats of the API either globally or in specific resources or operations. API Platform provides native support for multiple formats including JSON, XML, CSV, YAML, etc.
-See [Content negociation](../content-negotiation.md).
+See [Content negociation](content-negotiation.md).
### Name conversion
@@ -100,7 +100,7 @@ Both request and response bodies can be converted.
API Platform uses [name converters](https://symfony.com/doc/current/components/serializer.html#component-serializer-converting-property-names-when-serializing-and-deserializing) included in the Serializer component of Symfony. You can create your own by implementing the `NameConverterInterface` provided by Symfony.
-See [_Name Conversion_ in The Serialization Process](../serialization.md#name-conversion).
+See [_Name Conversion_ in The Serialization Process](serialization.md#name-conversion).
### Handle errors
@@ -114,7 +114,7 @@ See [ExceptionController support](https://github.com/FriendsOfSymfony/FOSRestBun
Map the exceptions to HTTP statuses in the `api_platform.exception_to_status` parameter.
-See [Errors Handling](../errors.md).
+See [Errors Handling](errors.md).
### Security
@@ -128,7 +128,7 @@ Use the `security` attribute in the `ApiResource` and `ApiProperty` attributes.
Note you can also use the `security.yml` file if you only need to limit access to specific roles.
-See [Security](../security.md).
+See [Security](security.md).
### API versioning
@@ -142,4 +142,4 @@ See [API versioning](https://github.com/FriendsOfSymfony/FOSRestBundle/blob/3.x/
API Platform has no native support to API versioning, but instead provides an approach consisting of deprecating resources when needed. It allows a smoother upgrade for clients, as they need to change their code only when it is necessary.
-See [Deprecating Resources and Properties](../deprecations.md).
+See [Deprecating Resources and Properties](deprecations.md).
diff --git a/core/openapi.md b/core/openapi.md
index c6dea81f6df..45a12a67335 100644
--- a/core/openapi.md
+++ b/core/openapi.md
@@ -121,24 +121,18 @@ use ApiPlatform\Metadata\ApiProperty;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
-/**
- * @ORM\Entity
- */
+#[ORM\Entity]
#[ApiResource]
class Product // The class name will be used to name exposed resources
{
- /**
- * @ORM\Column(type="integer")
- * @ORM\Id
- * @ORM\GeneratedValue(strategy="AUTO")
- */
- private $id;
+ #[ORM\Id, ORM\Column, ORM\GeneratedValue]
+ private ?int $id = null;
/**
* @param string $name A name property - this description will be available in the API documentation too.
*
- * @ORM\Column
*/
+ #[ORM\Column]
#[Assert\NotBlank]
#[ApiProperty(
openapiContext: [
@@ -147,11 +141,9 @@ class Product // The class name will be used to name exposed resources
'example' => 'one'
]
)]
- public $name;
+ public string $name;
- /**
- * @ORM\Column
- */
+ #[ORM\Column(type: "datetime")]
#[Assert\DateTime]
#[ApiProperty(
openapiContext: [
diff --git a/core/operations.md b/core/operations.md
index 0fc77df9698..efdffd25f5a 100644
--- a/core/operations.md
+++ b/core/operations.md
@@ -449,31 +449,19 @@ namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
-/**
- * @ORM\Entity
- */
+#[ORM\Entity]
class Place
{
- /**
- * @ORM\Id
- * @ORM\GeneratedValue
- * @ORM\Column(type="integer")
- */
+ #[ORM\Id, ORM\Column, ORM\GeneratedValue]
private ?int $id = null;
- /**
- * @ORM\Column
- */
+ #[ORM\Column]
private string $name = '';
- /**
- * @ORM\Column(type="float")
- */
+ #[ORM\Column(type: 'float')]
private float $latitude = 0;
- /**
- * @ORM\Column(type="float")
- */
+ #[ORM\Column(type: 'float')]
private float $longitude = 0;
// ...
@@ -512,9 +500,6 @@ use ApiPlatform\Metadata\ApiResource;
use App\Controller\GetWeather;
use Doctrine\ORM\Mapping as ORM;
-/**
- * @ORM\Entity
- */
#[ApiResource]
#[Get]
#[Put]
@@ -522,6 +507,7 @@ use Doctrine\ORM\Mapping as ORM;
#[Get(name: 'weather', uriTemplate: '/places/{id}/weather', controller: GetWeather::class)]
#[GetCollection]
#[Post]
+#[ORM\Entity]
class Place
{
// ...
diff --git a/core/pagination.md b/core/pagination.md
index bed84c37797..aeb318bd7a0 100644
--- a/core/pagination.md
+++ b/core/pagination.md
@@ -2,7 +2,7 @@

Watch the Pagination screencast
-API Platform Core has native support for paged collections. Pagination is enabled by default for all collections. Each collections
+API Platform Core has native support for paged collections. Pagination is enabled by default for all collections. Each collection
contains 30 items per page.
The activation of the pagination and the number of elements per page can be configured from:
@@ -342,7 +342,7 @@ To know more about cursor-based pagination take a look at [this blog post on med
## Controlling The Behavior of The Doctrine ORM Paginator
-The [PaginationExtension](https://github.com/api-platform/core/blob/main/src/Bridge/Doctrine/Orm/Extension/PaginationExtension.php) of API Platform performs some checks on the `QueryBuilder` to guess, in most common cases, the correct values to use when configuring the Doctrine ORM Paginator:
+The [PaginationExtension](https://github.com/api-platform/core/blob/2.6/src/Bridge/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.
diff --git a/core/performance.md b/core/performance.md
index c52b9525dd6..aeeca7c4292 100644
--- a/core/performance.md
+++ b/core/performance.md
@@ -164,7 +164,7 @@ database driver.
By default Doctrine comes with [lazy loading](https://www.doctrine-project.org/projects/doctrine-orm/en/latest/reference/working-with-objects.html#by-lazy-loading) - usually a killer time-saving feature but also a performance killer with large applications.
Fortunately, Doctrine offers another approach to solve this problem: [eager loading](https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/working-with-objects.html#by-eager-loading).
-This can easily be enabled for a relation: `@ORM\ManyToOne(fetch="EAGER")`.
+This can easily be enabled for a relation: `#[ORM\ManyToOne(fetch: "EAGER")]`.
By default in API Platform, we made the choice to force eager loading for all relations, with or without the Doctrine
`fetch` attribute. Thanks to the eager loading [extension](extensions.md). The `EagerLoadingExtension` will join every
@@ -235,9 +235,7 @@ namespace App\Entity;
use ApiPlatform\Metadata\ApiResource;
use Doctrine\ORM\Mapping as ORM;
-/**
- * @ORM\Entity
- */
+#[ORM\Entity]
#[ApiResource]
class Address
{
@@ -253,25 +251,18 @@ namespace App\Entity;
use ApiPlatform\Metadata\ApiResource;
use Doctrine\ORM\Mapping as ORM;
-/**
- * @ORM\Entity
- */
+#[ORM\Entity]
#[ApiResource(forceEager: false)]
class User
{
- /**
- * @var Address
- *
- * @ORM\ManyToOne(targetEntity="Address", fetch="EAGER")
- */
- public $address;
+ #[ORM\ManyToOne(fetch: 'EAGER')]
+ public Address $address;
/**
* @var Group[]
- *
- * @ORM\ManyToMany(targetEntity="Group", inversedBy="users")
- * @ORM\JoinTable(name="users_groups")
*/
+ #[ORM\ManyToMany(targetEntity: 'Group', inversedBy: 'users')]
+ #[ORM\JoinTable(name: 'users_groups')]
public $groups;
// ...
@@ -289,20 +280,17 @@ use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\GetCollection;
use Doctrine\ORM\Mapping as ORM;
-/**
- * @ORM\Entity
- */
#[ApiResource(forceEager: false)]
#[Get(forceEager: true)]
#[Post]
#[GetCollection(forceEager: true)]
+#[ORM\Entity]
class Group
{
/**
* @var User[]
- *
- * @ManyToMany(targetEntity="User", mappedBy="groups")
*/
+ #[ORM\ManyToMany(targetEntity: 'User', mappedBy: 'groups')]
public $users;
// ...
diff --git a/core/push-relations.md b/core/push-relations.md
index 3d6f57fafcd..1955b6558e7 100644
--- a/core/push-relations.md
+++ b/core/push-relations.md
@@ -20,11 +20,8 @@ use ApiPlatform\Metadata\ApiResource;
#[ApiResource]
class Book
{
- /**
- * @var Author
- */
#[ApiProperty(push: true)]
- public $author;
+ public Author $author;
// ...
}
diff --git a/core/security.md b/core/security.md
index 9229a6e37b0..e1612308039 100644
--- a/core/security.md
+++ b/core/security.md
@@ -23,39 +23,24 @@ use Symfony\Component\Validator\Constraints as Assert;
/**
* Secured resource.
- *
- * @ORM\Entity
*/
#[ApiResource(security: "is_granted('ROLE_USER')")]
#[Get]
#[Put(security: "is_granted('ROLE_ADMIN') or object.owner == user")]
#[GetCollection]
#[Post(security: "is_granted('ROLE_ADMIN')")]
+#[ORM\Entity]
class Book
{
- /**
- * @var int
- *
- * @ORM\Column(type="integer")
- * @ORM\Id
- * @ORM\GeneratedValue(strategy="AUTO")
- */
- private $id;
+ #[ORM\Id, ORM\Column, ORM\GeneratedValue]
+ private ?int $id = null;
- /**
- * @var string The title
- *
- * @ORM\Column
- */
+ #[ORM\Column]
#[Assert\NotBlank]
- public $title;
+ public string $title;
- /**
- * @var User The owner
- *
- * @ORM\ManyToOne(targetEntity=User::class)
- */
- public $owner;
+ #[ORM\ManyToOne]
+ public User $owner;
// ...
}
@@ -120,7 +105,7 @@ In this example:
Available variables are:
* `user`: the current logged in object, if any
-* `object`: the current resource, or collection of resources for collection operations (note: this is `null` for update/create operations)
+* `object`: the current resource class during denormalization, the current resource during normalization, or collection of resources for collection operations
* `previous_object`: (`securityPostDenormalize` only) a clone of `object`, before modifications were made - this is `null` for create operations
* `request` (only at the resource level): the current request
diff --git a/core/serialization.md b/core/serialization.md
index 5798e77e9e0..7614358fab8 100644
--- a/core/serialization.md
+++ b/core/serialization.md
@@ -431,14 +431,11 @@ use Symfony\Component\Serializer\Annotation\Groups;
class Person
{
#[Groups('person')]
- public $name;
+ public string $name;
- /**
- * @var Person
- */
#[Groups('person')]
#[ApiProperty(readableLink: false, writableLink: false)]
- public $parent; // This property is now serialized/deserialized as an IRI.
+ public Person $parent; // This property is now serialized/deserialized as an IRI.
// ...
}
@@ -535,15 +532,11 @@ use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Context;
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
-/**
- * @ORM\Entity
- */
+#[ORM\Entity]
#[ApiResource]
class Book
{
- /**
- * @ORM\Column(type="date")
- */
+ #[ORM\Column]
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
public ?\DateTimeInterface $publicationDate = null;
}
@@ -572,15 +565,11 @@ use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Context;
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
-/**
- * @ORM\Entity
- */
+#[ORM\Entity]
#[ApiResource]
class Book
{
- /**
- * @ORM\Column(type="date")
- */
+ #[ORM\Column]
#[Context(normalizationContext: [DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
public ?\DateTimeInterface $publicationDate = null;
}
@@ -599,15 +588,11 @@ use Symfony\Component\Serializer\Annotation\Context;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
-/**
- * @ORM\Entity
- */
+#[ORM\Entity]
#[ApiResource]
class Book
{
- /**
- * @ORM\Column(type="date")
- */
+ #[ORM\Column]
#[Groups(["extended"])]
#[Context([DateTimeNormalizer::FORMAT_KEY => \DateTime::RFC3339])]
#[Context(
@@ -634,34 +619,21 @@ use ApiPlatform\Metadata\GetCollection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
-/**
- * @ORM\Entity
- */
#[ApiResource]
#[GetCollection(normalizationContext: ['groups' => 'greeting:collection:get'])]
class Greeting
{
- /**
- * @var int The entity Id
- *
- * @ORM\Id
- * @ORM\GeneratedValue
- * @ORM\Column(type="integer")
- */
- #[Groups('greeting:collection:get')]
- private $id;
+ #[ORM\Id, ORM\Column, ORM\GeneratedValue]
+ #[Groups("greeting:collection:get")]
+ private ?int $id = null;
private $a = 1;
private $b = 2;
- /**
- * @var string A nice person
- *
- * @ORM\Column
- */
- #[Groups('greeting:collection:get')]
- public $name = '';
+ #[ORM\Column]
+ #[Groups("greeting:collection:get")]
+ public string $name = '';
public function getId(): int
{
@@ -691,7 +663,7 @@ App\Entity\Greeting:
groups: 'greeting:collection:get'
name:
groups: 'greeting:collection:get'
- getSum:
+ sum:
groups: 'greeting:collection:get'
```
@@ -723,19 +695,15 @@ class Book
/**
* This field can be managed only by an admin
- *
- * @var bool
*/
#[Groups(['book:output', 'admin:input'}])]
- public $active = false;
+ public bool $active = false;
/**
* This field can be managed by any user
- *
- * @var string
*/
#[Groups(['book:output', 'book:input'])]
- public $name;
+ public string $name;
// ...
}
@@ -1025,17 +993,13 @@ class Book
/**
* This field can be managed only by an admin
- *
- * @var bool
*/
- public $active = false;
+ public bool $active = false;
/**
* This field can be managed by any user
- *
- * @var string
*/
- public $name;
+ public string $name;
// ...
}
@@ -1134,27 +1098,17 @@ use ApiPlatform\Metadata\ApiResource;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
-/**
- * @ORM\Entity
- */
+#[ORM\Entity]
#[ApiResource]
final class Brand
{
- /**
- * @ORM\Id
- * @ORM\Column(type="integer")
- * @ORM\GeneratedValue(strategy="AUTO")
- */
- private $id;
+ #[ORM\Id, ORM\Column, ORM\GeneratedValue]
+ private ?int $id = null;
- /**
- * @ORM\ManyToMany(targetEntity="App\Entity\Car", inversedBy="brands")
- * @ORM\JoinTable(
- * name="CarToBrand",
- * joinColumns={@ORM\JoinColumn(name="brand_id", referencedColumnName="id", nullable=false)},
- * inverseJoinColumns={@ORM\JoinColumn(name="car_id", referencedColumnName="id", nullable=false)}
- * )
- */
+ #[ORM\ManyToMany(targetEntity: Car::class, inversedBy: 'brands')]
+ #[ORM\JoinTable(name: 'CarToBrand')]
+ #[ORM\JoinColumn(name: 'brand_id', referencedColumnName: 'id', nullable: false)]
+ #[ORM\InverseJoinColumn(name: 'car_id', referencedColumnName: 'id', nullable: false)]
private $cars;
public function __construct()
diff --git a/core/subresources.md b/core/subresources.md
index a84d59011b1..8b136aa87d6 100644
--- a/core/subresources.md
+++ b/core/subresources.md
@@ -19,28 +19,18 @@ namespace App\Entity;
use ApiPlatform\Metadata\ApiResource;
use Doctrine\ORM\Mapping as ORM;
-/**
- * @ORM\Entity
- */
+#[ORM\Entity]
#[ApiResource]
class Answer
{
- /**
- * @ORM\Column(type="integer")
- * @ORM\Id
- * @ORM\GeneratedValue(strategy="AUTO")
- */
- private $id;
-
- /**
- * @ORM\Column
- */
- public $content;
-
- /**
- * @ORM\OneToOne(targetEntity="Question", mappedBy="answer")
- */
- public $question;
+ #[ORM\Id, ORM\Column, ORM\GeneratedValue]
+ private ?int $id = null;
+
+ #[ORM\Column(type: 'text')]
+ public string $content;
+
+ #[ORM\OneToOne]
+ public Question $question;
public function getId(): ?int
{
@@ -57,30 +47,20 @@ use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Core\Annotation\ApiSubresource;
use Doctrine\ORM\Mapping as ORM;
-/**
- * @ORM\Entity
- */
+#[ORM\Entity]
#[ApiResource]
class Question
{
- /**
- * @ORM\Column(type="integer")
- * @ORM\Id
- * @ORM\GeneratedValue(strategy="AUTO")
- */
- private $id;
-
- /**
- * @ORM\Column
- */
- public $content;
-
- /**
- * @ORM\OneToOne(targetEntity="Answer", inversedBy="question")
- * @ORM\JoinColumn(referencedColumnName="id", unique=true)
- */
+ #[ORM\Id, ORM\Column, ORM\GeneratedValue]
+ private ?int $id = null;
+
+ #[ORM\Column(type: 'text')]
+ public string $content;
+
+ #[ORM\OneToOne]
+ #[ORM\JoinColumn(referencedColumnName: 'id', unique: true)]
#[ApiSubresource]
- public $answer;
+ public Answer $answer;
public function getId(): ?int
{
@@ -245,7 +225,7 @@ class Answer
### Limiting Depth
You can control depth of subresources with the parameter `maxDepth`. For example, if the `Answer` entity also has a subresource
-such as `comments`and you don't want the route `api/questions/{id}/answers/{id}/comments` to be generated. You can do this by adding the parameter maxDepth in the ApiSubresource annotation or YAML/XML file configuration.
+such as `comments` and you don't want the route `api/questions/{id}/answers/{id}/comments` to be generated. You can do this by adding the parameter maxDepth in the ApiSubresource annotation or YAML/XML file configuration.
```php
createClientWithCredentials($token)->request('GET', '/users');
$this->assertJsonContains(['hydra:description' => 'Access Denied.']);
- $this->assertResponseStatusCodeSame('403');
+ $this->assertResponseStatusCodeSame(403);
}
}
```
@@ -163,7 +163,7 @@ class MyTest extends ApiTestCase
}
```
-There is a also a method to find the IRI matching a given resource and some criterias:
+There is also a method to find the IRI matching a given resource and some criterias:
```php
['a', 'b']])]
class Book
{
- /**
- * @Assert\NotBlank(groups={"a"})
- */
- public $name;
+ #[Assert\NotBlank(groups: ['a'])]
+ public string $name;
- /**
- * @Assert\NotNull(groups={"b"})
- */
- public $author;
+ #[Assert\NotNull(groups: ['b'])]
+ public string $author;
// ...
}
@@ -189,29 +175,15 @@ use Symfony\Component\Validator\Constraints as Assert;
#[Post(validationContext: ['groups' => ['Default', 'postValidation']])]
class Book
{
- /**
- * @Assert\Uuid
- */
+ #[Assert\Uuid]
private $id;
- /**
- * @Assert\NotBlank(groups={"postValidation"})
- */
+ #[Assert\NotBlank(groups: ['postValidation'])]
public $name;
- /**
- * @Assert\NotNull
- * @Assert\Length(
- * min = 2,
- * max = 50,
- * groups={"postValidation"}
- * )
- * @Assert\Length(
- * min = 2,
- * max = 70,
- * groups={"putValidation"}
- * )
- */
+ #[Assert\NotNull]
+ #[Assert\Length(min: 2, max: 50, groups: ['postValidation'])]
+ #[Assert\Length(min: 2, max: 70, groups: ['putValidation'])]
public $author;
// ...
@@ -258,14 +230,10 @@ class Book
return ['a'];
}
- /**
- * @Assert\NotBlank(groups={"a"})
- */
+ #[Assert\NotBlank(groups: ['a'])]
public $name;
- /**
- * @Assert\NotNull(groups={"b"})
- */
+ #[Assert\NotNull(groups: ['b'])]
public $author;
// ...
@@ -323,14 +291,10 @@ use Symfony\Component\Validator\Constraints as Assert;
#[ApiResource(validationContext: ['groups' => [AdminGroupsGenerator::class]])
class Book
{
- /**
- * @Assert\NotBlank(groups={"a"})
- */
+ #[Assert\NotBlank(groups: ['a'])]
public $name;
- /**
- * @Assert\NotNull(groups={"b"})
- */
+ #[Assert\NotNull(groups: ['b'])]
public $author;
// ...
@@ -380,32 +344,23 @@ use App\Validator\Two; // classic custom constraint
use App\Validator\MySequencedGroup; // the sequence group to use
use Doctrine\ORM\Mapping as ORM;
-/**
- * @ORM\Entity
- */
+#[ORM\Entity]
#[ApiResource]
#[Post(validationContext: ['groups' => MySequencedGroup::class])]
class Greeting
{
- /**
- * @var int The entity Id
- *
- * @ORM\Id
- * @ORM\GeneratedValue
- * @ORM\Column(type="integer")
- */
- private $id;
+ #[ORM\Id, ORM\Column, ORM\GeneratedValue]
+ private ?int $id = null;
/**
- * @var string A nice person
- *
- * @ORM\Column
+ * @var A nice person
*
* I want this "second" validation to be executed after the "first" one even though I wrote them in this order.
* @One(groups={"second"})
* @Two(groups={"first"})
*/
- public $name = '';
+ #[ORM\Column]
+ public string $name = '';
public function getId(): int
{
@@ -473,9 +428,7 @@ final class Brand
$this->cars = new ArrayCollection();
}
- /**
- * @Assert\Valid
- */
+ #[Assert\Valid]
public function getCars()
{
return $this->cars->getValues();
diff --git a/deployment/index.md b/deployment/index.md
index 2227b8e4714..cac63909614 100644
--- a/deployment/index.md
+++ b/deployment/index.md
@@ -1,6 +1,6 @@
# Deploying API Platform Applications
-API Platform apps are super easy to deploy in production thanks to the [Docker Compose definetion](docker-compose.md) and to the [Kubernetes chart](kubernetes.md) we provide.
+API Platform apps are super easy to deploy in production thanks to the [Docker Compose definition](docker-compose.md) and to the [Kubernetes chart](kubernetes.md) we provide.
We strongly recommend using Kubernetes or Docker Compose to deploy your apps.
@@ -12,7 +12,7 @@ while the Progressive Web Application is a standard Next.js project:

Watch the Animated Deployment with Ansistrano screencast
* [Deploying the Symfony application](https://symfony.com/doc/current/deployment.html)
-* [Deployin the Next.js application](https://nextjs.org/docs/deployment)
+* [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):
diff --git a/deployment/kubernetes.md b/deployment/kubernetes.md
index fd560e687c3..34ad2b79aef 100644
--- a/deployment/kubernetes.md
+++ b/deployment/kubernetes.md
@@ -115,7 +115,7 @@ If you prefer to use a managed DBMS like [Heroku Postgres](https://www.heroku.co
--set postgresql.url=pgsql://username:password@host/database?serverVersion=13
Finally, build the `pwa` (client and admin) JavaScript apps and [deploy them on a static
-website hosting service](https://create-react-app.dev/docs/deployment/).
+site hosting service](https://create-react-app.dev/docs/deployment/).
## Access the container
@@ -165,7 +165,7 @@ You have to use the *.image.pullPolicy=Always see the last 3 parameters.
## GitHub Actions Example for deployment
-You can find a [complete deploy command for GKE](https://github.com/api-platform/demo/blob/main/.github/workflows/deploy.yml) on the [demo project](https://github.com/api-platform/demo/):
+You can find a [complete deploy command for GKE](https://github.com/api-platform/demo/blob/main/.github/workflows/cd.yml) on the [demo project](https://github.com/api-platform/demo/):
## Symfony Messenger
diff --git a/distribution/debugging.md b/distribution/debugging.md
index 59e7be4e6fd..13d9d44b658 100644
--- a/distribution/debugging.md
+++ b/distribution/debugging.md
@@ -17,7 +17,7 @@ it's recommended to add a custom stage to the end of the `api/Dockerfile`.
# api/Dockerfile
FROM api_platform_php as api_platform_php_dev
-ARG XDEBUG_VERSION=3.0.2
+ARG XDEBUG_VERSION=3.1.3
RUN set -eux; \
apk add --no-cache --virtual .build-deps $PHPIZE_DEPS; \
pecl install xdebug-$XDEBUG_VERSION; \
@@ -95,6 +95,6 @@ $ docker-compose exec php \
php --version
PHP …
- with Xdebug v3.0.2, Copyright (c) 2002-2021, by Derick Rethans
+ with Xdebug v3.1.3, Copyright (c) 2002-2021, by Derick Rethans
…
```
diff --git a/distribution/index.md b/distribution/index.md
index f5884afe1c9..05eba17d68a 100644
--- a/distribution/index.md
+++ b/distribution/index.md
@@ -371,67 +371,58 @@ Modify these files as described in these patches:
+/**
+ * A book.
+ *
-+ * @ORM\Entity
+ */
+ #[ORM\Entity]
#[ApiResource]
class Book
{
- /** The id of this book. */
+ /**
+ * The id of this book.
-+ *
-+ * @ORM\Id
-+ * @ORM\GeneratedValue
-+ * @ORM\Column(type="integer")
+ */
+ #[ORM\Id, ORM\Column, ORM\GeneratedValue]
private ?int $id = null;
- /** The ISBN of this book (or null if doesn't have one). */
+ /**
+ * The ISBN of this book (or null if doesn't have one).
-+ *
-+ * @ORM\Column(nullable=true)
+ */
+ #[ORM\Column(nullable: true)]
public ?string $isbn = null;
- /** The title of this book. */
+ /**
+ * The title of this book.
-+ *
-+ * @ORM\Column
+ */
+ #[ORM\Column]
public string $title = '';
- /** The description of this book. */
+ /**
+ * The description of this book.
-+ *
-+ * @ORM\Column(type="text")
+ */
+ #[ORM\Column(type="text")]
public string $description = '';
- /** The author of this book. */
+ /**
+ * The author of this book.
-+ *
-+ * @ORM\Column
+ */
+ #[ORM\Column]
public string $author = '';
- /** The publication date of this book. */
+ /**
+ * The publication date of this book.
-+ *
-+ * @ORM\Column(type="datetime_immutable")
+ */
+ #[ORM\Column]
public ?\DateTimeInterface $publicationDate = null;
- /** @var Review[] Available reviews for this book. */
+ /**
+ * @var Review[] Available reviews for this book.
-+ *
-+ * @ORM\OneToMany(targetEntity="Review", mappedBy="book", cascade={"persist", "remove"})
+ */
+ #[ORM\OneToMany(mappedBy: 'book', targetEntity: 'Review', cascade: ['persist', 'remove'])]
public iterable $reviews;
public function __construct()
@@ -448,60 +439,51 @@ Modify these files as described in these patches:
-/** A review of a book. */
+/**
+ * A review of a book.
-+ *
-+ * @ORM\Entity
+ */
+ #[ORM\Entity]
#[ApiResource]
class Review
{
- /** The id of this review. */
+ /**
+ * The id of this review.
-+ *
-+ * @ORM\Id
-+ * @ORM\GeneratedValue
-+ * @ORM\Column(type="integer")
+ */
+ #[ORM\Id, ORM\Column, ORM\GeneratedValue]
private ?int $id = null;
- /** The rating of this review (between 0 and 5). */
+ /**
+ * The rating of this review (between 0 and 5).
-+ *
-+ * @ORM\Column(type="smallint")
+ */
+ #[ORM\Column(type: "smallint")]
public int $rating = 0;
- /** The body of the review. */
+ /**
+ * The body of the review.
-+ *
-+ * @ORM\Column(type="text")
+ */
+ #[ORM\Column(type: "text")]
public string $body = '';
- /** The author of the review. */
+ /**
+ * The author of the review.
-+ *
-+ * @ORM\Column
+ */
+ #[ORM\Column]
public string $author = '';
- /** The date of publication of this review.*/
+ /**
+ * The date of publication of this review.
-+ *
-+ * @ORM\Column(type="datetime_immutable")
+ */
+ #[ORM\Column]
public ?\DateTimeInterface $publicationDate = null;
- /** The book this review is about. */
+ /**
+ * The book this review is about.
-+ *
-+ * @ORM\ManyToOne(targetEntity="Book", inversedBy="reviews")
+ */
+ #[ORM\ManyToOne(targetEntity: "Book", inversedBy: "reviews")]
public ?Book $book = null;
public function getId(): ?int
@@ -639,29 +621,24 @@ Modify the following files as described in these patches:
use Doctrine\Common\Collections\ArrayCollection;
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;
-
- * @ORM\Column
- */
+ public ?string $isbn = null;
+
+ #[ORM\Column]
+ #[Assert\NotBlank]
public string $title = '';
- * @ORM\Column(type="text")
- */
+ #[ORM\Column]
+ #[Assert\NotBlank]
public string $description = '';
- * @ORM\Column
- */
+ #[ORM\Column]
+ #[Assert\NotBlank]
public string $author = '';
- * @ORM\Column(type="datetime_immutable")
- */
+ #[ORM\Column]
+ #[Assert\NotNull]
public ?\DateTimeInterface $publicationDate = null;
```
@@ -672,29 +649,24 @@ Modify the following files as described in these patches:
use ApiPlatform\Metadata\ApiResource;
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")
- */
+ #[ORM\Column(type: 'text')]
+ #[Assert\NotBlank]
public string $body = '';
- * @ORM\Column
- */
+ #[ORM\Column]
+ #[Assert\NotBlank]
public string $author = '';
- * @ORM\Column(type="datetime_immutable")
- */
+ #[ORM\Column]
+ #[Assert\NotNull]
public ?\DateTimeInterface $publicationDate = null;
- * @ORM\ManyToOne(targetEntity="Book", inversedBy="reviews")
- */
+ #[ORM\ManyToOne(inverdedBy: 'reviews')]
+ #[Assert\NotNull]
public ?Book $book = null;
diff --git a/outline.yaml b/outline.yaml
index 81fb5eff228..e1f333a984f 100644
--- a/outline.yaml
+++ b/outline.yaml
@@ -67,6 +67,7 @@ chapters:
- handling-relations
- schema.org
- validation
+ - real-time-mercure
- authentication-support
- file-upload
- performance
diff --git a/schema-generator/configuration.md b/schema-generator/configuration.md
index 1fdcf942249..fbd8e13c9e8 100644
--- a/schema-generator/configuration.md
+++ b/schema-generator/configuration.md
@@ -175,11 +175,9 @@ The `#[Assert\NotNull]` constraint is automatically added.
/**
* The name of the item.
*/
-#[ORM\Column]
-#[Assert\NotNull]
-private string $name;
-
-...
+ #[ORM\Column]
+ #[Assert\NotNull]
+ private string $name;
```
## Forcing a Unique Property
@@ -206,6 +204,7 @@ namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
+use Doctrine\ORM\Mapping as ORM;
/**
* A person (alive, dead, undead, or fictional).
@@ -213,7 +212,7 @@ use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
* @see https://schema.org/Person
*/
#[ORM\Entity]
-#[ApiResource(iri: 'https://schema.org/Person')]
+#[ApiResource(types: ['https://schema.org/Person'])]
#[UniqueEntity('email')]
class Person
{
@@ -285,14 +284,17 @@ namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
+use Doctrine\ORM\Mapping as ORM;
+use ApiPlatform\Core\Annotation\ApiProperty;
+use ApiPlatform\Core\Annotation\ApiResource;
/**
* A person (alive, dead, undead, or fictional).
*
- * @see https://schema.org/Person
+ * @see https://schema.org/Person Documentation on Schema.org
*/
#[ORM\Entity]
-#[ApiResource(iri: 'https://schema.org/Person')]
+#[ApiResource(types: ['https://schema.org/Person'])]
class Person
{
/**
@@ -300,9 +302,10 @@ class Person
*
* @see https://schema.org/name
*/
- #[ORM\Column(nullable: true)]
- #[ApiProperty(iri: 'https://schema.org/name')]
+ #[ORM\Column(nullable: true)
+ #[Assert\Type(type: 'string')]
#[Groups(['public'])]
+ #[ApiProperty(types: ['https://schema.org/name'])]
private string $name;
// ...
@@ -343,22 +346,25 @@ namespace App\Entity;
use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\ApiResource;
use Doctrine\ORM\Mapping as ORM;
+use ApiPlatform\Core\Annotation\ApiProperty;
+use ApiPlatform\Core\Annotation\ApiResource;
/**
* Any offered product or service.
*
- * @see https://schema.org/Product
+ * @see https://schema.org/Product Documentation on Schema.org
*/
#[ORM\Entity]
-#[ApiResource(iri: 'https://schema.org/Product')]
+#[ApiResource(types: ['https://schema.org/Product'])]
+#[UniqueEntity('gtin13s')]
class Product
{
/**
* The weight of the product or person.
*
- * @see https://schema.org/weight
+ * @see http://schema.org/weight
*/
- #[ORM\Embedded(class: 'App\Entity\QuantitativeValue', columnPrefix: 'weight_')]
+ #[ORM\Embedded(class: QuantitativeValue::class, columnPrefix: 'weight_')]
#[ApiProperty(iri: 'https://schema.org/weight')]
private ?QuantitativeValue $weight = null;