diff --git a/admin/advanced-customization.md b/admin/advanced-customization.md new file mode 100644 index 00000000000..f714af21e9e --- /dev/null +++ b/admin/advanced-customization.md @@ -0,0 +1,462 @@ +# Customizing the Admin + +In the previous sections, we have seen how to customize the generated Admin by [updating the schema](./schema.md), and by [customizing the guesser components](./customizing.md). + +But we can go much further in customizing the generated pages by leveraging React Admin components and props. + +In the following sections, we will for instance learn how to: + +- Change the default theme and layout +- Display the number of related records instead of listing them +- Navigate from the list using simple row click +- Make mutations undoable +- Improve the layout of a form +- Switch to a tabbed layout +- Create a custom field component +- Add icons to the menu + +Let's dive in! + +## Changing the Default Theme and Layout + +API Platform comes with its own [layout](https://marmelab.com/react-admin/Admin.html#layout) and [themes](https://marmelab.com/react-admin/Admin.html#theme) by default. + +![Admin with default API Platform theme and layout](./images/api-platform-admin-theme.png) + +However you may not find them to your liking, or you may want to remove the API Platform logo from the top bar. + +To change the top bar logo, you will need to opt out of API Platform's default Layout component, and provide your own. + +You can for instance use the default [Layout](https://marmelab.com/react-admin/Layout.html) provided by `react-admin`. + +```diff +import { HydraAdmin, ResourceGuesser } from "@api-platform/admin"; ++import { Layout } from 'react-admin'; + +export const App = () => ( +- ++ + + + +); +``` + +To customize the light and dark themes, you will need to use the [`theme`](https://marmelab.com/react-admin/Admin.html#theme) and [`darkTheme`](https://marmelab.com/react-admin/Admin.html#darktheme) props of the `` component. + +Here too, we can use the default themes provided by `react-admin`. + +```diff +import { HydraAdmin, ResourceGuesser } from "@api-platform/admin"; +-import { Layout } from 'react-admin'; ++import { Layout, defaultDarkTheme, defaultLightTheme } from 'react-admin'; + +export const App = () => ( +- ++ + + + +); +``` + +Here is the result: + +![Admin with default react-admin theme and layout](./images/react-admin-theme.png) + +## Displaying the Number of Related Records + +When dealing with related records, the default behavior of the guessers is to display the list of related records. + +However if there are many related records, it can be more suitable to display the number of related records instead. + +Reusing our example with `books` and `reviews`, here is how you can display the number of reviews for each book in the book list: + +```diff +import { ListGuesser, FieldGuesser } from '@api-platform/admin'; +import { NumberField } from 'react-admin'; + +const BookList = () => ( + + + + + +- ++ + +); +``` + +![Admin with number of related records](./images/admin-reference-record-count.png) + +**Tip:** It is recommended to also set a custom `label` to the column, as the label is otherwise humanized from the `source` prop, which is no longer suitable with a source like `reviews.length`. + +## Hiding the Show And Edit Buttons in the List View + +By default, the list guesser displays a `Show` and `Edit` button for each row. + +However the UX can often be improved by setting a default action when clicking on a row, and removing the `Show` and `Edit` buttons. + +To hide these buttons, we will need to replace the `` by a [``](https://marmelab.com/react-admin/List.html) component, provided by `react-admin`. + +Then, to get the same layout as before, we will choose to render the list items using a [``](https://marmelab.com/react-admin/Datagrid.html) component. + +`` will automatically set the row click action to redirect to the show view if there is one, or to the edit view otherwise. + +```diff +-import { ListGuesser, FieldGuesser } from '@api-platform/admin'; ++import { FieldGuesser } from '@api-platform/admin'; +-import { NumberField } from 'react-admin'; ++import { List, Datagrid, NumberField } from 'react-admin'; + +const BookList = () => ( +- ++ ++ + + + + + ++ ++ +- +); +``` + +The UI is now more concise: + +![Admin with hidden show and edit buttons](./images/admin-datagrid.png) + +If you want, you can use the [`rowClick`](https://marmelab.com/react-admin/Datagrid.html#rowclick) prop to customize the row click action, for instance to redirect to the book edit view instead: + +```diff +const BookList = () => ( + +- ++ + + + + + + + +); +``` + +**Tip:** Check out the [`` documentation](https://marmelab.com/react-admin/Datagrid.html) for more customization options. + +## Enabling Undoable Mutations + +React Admin offers the possibility to make mutations (e.g. updating or deleting a record) undoable. + +When this feature is enabled, a notification will be displayed at the bottom of the page, allowing the user to undo the mutation for a certain amount of time. + +If the user clicks on the UNDO button, the record will be restored to its previous state. Otherwise, the change is persisted to the API. + +Let's, for instance, add the possibility to undo an update to a book. To do that, we will leverage the [`mutationMode`](https://marmelab.com/react-admin/Edit.html#mutationmode) prop provided by React Admin, and set its value to `"undoable"`. + +This is possible because the `` component is a wrapper around the [``](https://marmelab.com/react-admin/Edit.html) component provided by React Admin, and it will forward the `mutationMode` prop to it. + +```diff +import { EditGuesser, InputGuesser } from "@api-platform/admin"; + +export const BookEdit = () => ( +- ++ + + + + + + + +); +``` + +That's enough to display an undoable notification when updating a book: + +![Admin with undoable mutations](./images/admin-undoable-mutation.png) + +**Tip:** The default `mutationMode` set by `` is `"pessimistic"`, however the default `mutationMode` set by React Admin's `` component is `"undoable"`. + +## Warning the User When There Are Unsaved Changes + +Another feature offered by React Admin is the possibility to warn the user when there are unsaved changes in a form. + +When the user tries to navigate away from a form with unsaved changes, a confirmation dialog will be displayed, asking the user if they want to leave the page. This prevents the risk of losing unsaved data. + +To enable this feature, all we need to do is to leverage the [`warnWhenUnsavedChanges`](https://marmelab.com/react-admin/SimpleForm.html#warnwhenunsavedchanges) prop provided by React Admin. + +This is possible because the `` component is also a wrapper around the [``](https://marmelab.com/react-admin/SimpleForm.html) component provided by React Admin, and it will forward the `warnWhenUnsavedChanges` prop to it. + +```diff +import { EditGuesser, InputGuesser } from "@api-platform/admin"; + +export const BookEdit = () => ( +- ++ + + + + + + + +); +``` + +Now, if the user tries to navigate away from the form with unsaved changes, they will be warned: + +![Admin with unsaved changes warning](./images/admin-warnWhenUnsavedChanges.png) + +## Customizing the Form Layout + +As we saw earlier, `` actually renders two (nested) React Admin components: [``](https://marmelab.com/react-admin/Edit.html) and [``](https://marmelab.com/react-admin/SimpleForm.html). +You can pass additional props to `` which will be forwarded to `` or `` accordingly. + +However there are cases where this won't be enough. For instance, if we want to customize the form layout, we will need to specifically target the form component to pass styling props (such as `sx`), or to replace the component altogether (e.g. to use a [``](https://marmelab.com/react-admin/TabbedForm.html) instead). + +So, for our example, let's first replace the `` by an `` and a ``. + +```diff +-import { EditGuesser, InputGuesser } from "@api-platform/admin"; ++import { InputGuesser } from "@api-platform/admin"; ++import { Edit, SimpleForm } from "react-admin"; + +export const BookEdit = () => ( +- ++ ++ + + + + + + ++ ++ +- +); +``` + +**Tip:** This will also enable [undoable mutation mode](./advanced-customization.md#enabling-undoable-mutations). Indeed, the default `mutationMode` set by `` is `"pessimistic"`, however the default `mutationMode` set by React Admin's `` component is `"undoable"`. You can set the `mutationMode` prop back to `"pessimistic"` if you want to keep the same behavior as before. + +By default, `` organizes the inputs in a very simple layout, simply stacking them vertically. +Under the hood, it uses Material UI's [``](https://mui.com/material-ui/react-stack/) component. +This means we can use with `` any prop that `` accepts, and customize the style of the component using [the `sx` prop](https://marmelab.com/react-admin/SX.html). + +For instance, let's limit the width of the inputs to 500px: + +```diff +export const BookEdit = () => ( + +- ++ + + + + + + + + +); +``` + +We can also use `` directly in the `` to customize the layout further: + +```tsx +import { InputGuesser } from '@api-platform/admin'; +import { Edit, SimpleForm } from 'react-admin'; +import { Stack } from '@mui/material'; + +export const BookEdit = () => ( + + + + + + + + + + + +); +``` + +With these simple changes we already get a more appealing form layout: + +![Admin with customized form layout](./images/admin-form-layout.png) + +**Tip:** Feel free to look at the [``](https://marmelab.com/react-admin/Edit.html) and [``](https://marmelab.com/react-admin/SimpleForm.html) documentation pages to learn more about the customization options they offer. + +**Tip:** `` is not the only form layout provided by React Admin. You can also use another layout such as [``](https://marmelab.com/react-admin/TabbedForm.html), [``](https://marmelab.com/react-admin/LongForm.html), +[``](https://marmelab.com/react-admin/AccordionForm.html), [``](https://marmelab.com/react-admin/WizardForm.html) or even [create your own](https://marmelab.com/react-admin/Form.html). + +## Rendering Related Records in a Dedicated Tab + +Speaking of tabbed layout, a common pattern is to display related records in a dedicated tab of the show view of the main record. + +For instance, let's leverage the [``](https://marmelab.com/react-admin/TabbedShowLayout.html) component provided by React Admin to display the reviews of a book in a dedicated tab. + +We will also leverage `` to fetch the related reviews of a book, and `` to display them in a list. + +```tsx +import { Show, TabbedShowLayout, TextField, DateField, ReferenceArrayField, SimpleList } from 'react-admin'; + +const BookShow = () => ( + + + + + + + + + + + + + review.author + .split(' ') + .map((name: string) => name[0]) + .join('') + } + /> + + + + +); +``` + +Here is the result: + +![Admin with tabbed show layout](./images/admin-tabbed-show-layout.png) + +**Tip:** Feel free to look at the [``](https://marmelab.com/react-admin/TabbedShowLayout.html), [``](https://marmelab.com/react-admin/ReferenceArrayField.html) and [``](https://marmelab.com/react-admin/SimpleList.html) documentation pages to learn more about the customization options they offer. + +## Creating A Custom Field Component + +React Admin already provides numerous off-the-shelf [field](https://marmelab.com/react-admin/Fields.html) and [input](https://marmelab.com/react-admin/Inputs.html) components. + +However, you may still need to create your own custom field component to display a specific type of data, or to add a specific behavior. + +Fortunately, React Admin makes it easy to create custom [field](https://marmelab.com/react-admin/Fields.html#writing-your-own-field-component) or [input](https://marmelab.com/react-admin/Inputs.html#writing-your-own-input-component) components, thanks to the many building blocks it provides. + +Let's take a look at a concrete example. Let's say we want to create a custom field component to display a rating as a series of stars. + +We will leverage Material UI's [``](https://mui.com/material-ui/react-rating/) component for the rendering. + +Since the component is fairly simple, we won't create a dedicated React component, but will instead leverage [``](https://marmelab.com/react-admin/WithRecord.html), a React Admin component allowing to build a custom field on-the-fly. + +```tsx +import { ShowGuesser } from '@api-platform/admin'; +import { FieldGuesser, WithRecord, Labeled } from 'react-admin'; +import { Rating } from '@mui/material'; + +const ReviewShow = () => ( + + + + + + ( + + )} + /> + + + +); +``` + +Here is the result: + +![Admin with custom field component](./images/admin-custom-field.png) + +**Tip:** For a more complex field component, the preferred approach would probably be to create a dedicated React component. You can then leverage the [`useRecordContext`](https://marmelab.com/react-admin/useRecordContext.html) hook to achieve the same result. + +**Tip:** Check out the [Writing Your Own Field Component](https://marmelab.com/react-admin/Fields.html#writing-your-own-field-component) documentation to learn more about creating custom field components. + +Now let's create a custom input component, allowing not only to display a rating as a series of stars, but also to edit it. + +Again, we will leverage Material UI's [``](https://mui.com/material-ui/react-rating/) component for the rendering. But this time, we will leverage the [`useInput`](https://marmelab.com/react-admin/useInput.html) hook provided by React Admin, which allows to easily create a custom input component. + +```tsx +import { useInput } from 'react-admin'; +import { Rating } from '@mui/material'; + +const RatingInput = (props: InputProps) => { + const { field } = useInput(props); + return ( + { + field.onChange(value); + }} + /> + ); +}; +``` + +As you see, the `RatingInput` component is really short. It simply needs to call `field.onChange` whenever the rating changes. + +Now let's use this custom input component in the `ReviewEdit` component: + +```tsx +import { Edit, SimpleForm, InputGuesser, Labeled } from 'react-admin'; +import { RatingInput } from './RatingInput'; + +const ReviewEdit = () => ( + + + + + + + + + + + +); +``` + +Here is the result: + +![Admin with custom input component](./images/admin-custom-input.png) + +## React Admin Components + +As you saw from the previous sections and examples, while API Platform Admin aims at providing a complete and ready-to-use admin interface with as little code as possible, it always provides the flexibility to fully customize every aspect of the generated admin, while keeping a pleasant developer experience. + +This is made possible thanks to the numerous **React Admin components**. They are battle-tested, backend agnostic, fully customizable solutions to common Admin requirements. + +Here are some examples, from the simplest to the most complete solutions: + +- [PrevNextButton](https://marmelab.com/react-admin/PrevNextButton.html) +- [MenuLive](https://marmelab.com/react-admin/MenuLive.html) +- [FilterList](https://marmelab.com/react-admin/FilterList.html) +- [RevisionsButton](https://marmelab.com/react-admin/RevisionsButton.html) +- [WizardForm](https://marmelab.com/react-admin/WizardForm.html) +- [Calendar](https://marmelab.com/react-admin/Calendar.html) +- [SmartRichTextInput](https://marmelab.com/react-admin/SmartRichTextInput.html) +- [SolarLayout](https://marmelab.com/react-admin/SolarLayout.html) +- And many more... + +React Admin already includes 230+ hooks and components. And it always allows you to make your own, thanks to the building blocks it provides. Feel free to read through its [All Features](https://marmelab.com/react-admin/Features.html) documentation page to discover them all. diff --git a/admin/authentication-support.md b/admin/authentication-support.md index af5cc650271..024af85a507 100644 --- a/admin/authentication-support.md +++ b/admin/authentication-support.md @@ -1,17 +1,24 @@ # Authentication Support 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. + +Refer to the [Auth Provider Setup](https://marmelab.com/react-admin/Authentication.html) documentation for more information. + +**Tip:** Once you have set up the authentication, you can also configure React Admin to perform client-side Authorization checks. Refer to the [Authorization](https://marmelab.com/react-admin/Permissions.html) documentation for more information. ## HydraAdmin -The authentication layer for [HydraAdmin component](https://api-platform.com/docs/admin/components/#hydra) -consists of a few parts, which need to be integrated together. +Enabling authentication support for [`` component](./components.md#hydra) consists of a few parts, which need to be integrated together. + +In the following steps, we will see how to: + +- Make authenticated requests to the API (i.e. include the `Authorization` header) +- Redirect users to the login page if they are not authenticated +- Clear expired tokens when encountering unauthorized `401` response -### Authentication +### Make Authenticated Requests -Add the Bearer token from `localStorage` to request headers. +First, we need to implement a `getHeaders` function, that will add the Bearer token from `localStorage` (if there is one) to the `Authorization` header. ```typescript const getHeaders = () => @@ -20,9 +27,13 @@ const getHeaders = () => : {}; ``` -Extend the Hydra fetch function with custom headers for authentication. +Then, extend the Hydra `fetch` function to use the `getHeaders` function to add the `Authorization` header to the requests. ```typescript +import { + fetchHydra as baseFetchHydra, +} from "@api-platform/admin"; + const fetchHydra = (url, options = {}) => baseFetchHydra(url, { ...options, @@ -31,11 +42,14 @@ const fetchHydra = (url, options = {}) => ``` -### Login Redirection +### Redirect To Login Page -Redirect users to a `/login` path, if no token is available in the `localStorage`. +Then, we'll create a `` component, that will redirect users to the `/login` route if no token is available in the `localStorage`, and call the dataProvider's `introspect` function otherwise. + +```tsx +import { Navigate } from "react-router-dom"; +import { useIntrospection } from "@api-platform/admin"; -```typescript const RedirectToLogin = () => { const introspect = useIntrospection(); @@ -47,13 +61,16 @@ const RedirectToLogin = () => { }; ``` -### API Documentation Parsing +### Clear Expired Tokens -Extend the `parseHydraDocumentaion` function from the [API Doc Parser library](https://github.com/api-platform/api-doc-parser) -to handle the documentation parsing. Customize it to clear -expired tokens when encountering unauthorized `401` response. +Now, we will extend the `parseHydraDocumentaion` function (imported from the [@api-platform/api-doc-parser](https://github.com/api-platform/api-doc-parser) library). + +We will customize it to clear expired tokens when encountering unauthorized `401` response. ```typescript +import { parseHydraDocumentation } from "@api-platform/api-doc-parser"; +import { ENTRYPOINT } from "config/entrypoint"; + const apiDocumentationParser = (setRedirectToLogin) => async () => { try { setRedirectToLogin(false); @@ -72,11 +89,16 @@ const apiDocumentationParser = (setRedirectToLogin) => async () => { }; ``` -### Data Provider +### Extend The Data Provider -Initialize the hydra data provider with custom headers and the documentation parser. +Now, we can initialize the Hydra data provider with the custom `fetchHydra` (with custom headers) and `apiDocumentationParser` functions created earlier. ```typescript +import { + hydraDataProvider as baseHydraDataProvider, +} from "@api-platform/admin"; +import { ENTRYPOINT } from "config/entrypoint"; + const dataProvider = (setRedirectToLogin) => baseHydraDataProvider({ entrypoint: ENTRYPOINT, @@ -85,12 +107,12 @@ const dataProvider = (setRedirectToLogin) => }); ``` -### Export Admin Component +### Update The Admin Component -Export the Hydra admin component, and track the users' authentication status. +Lastly, we can stitch everything together in the `Admin` component. -```typescript -// components/admin/Admin.tsx +```tsx +// src/Admin.tsx import Head from "next/head"; import { useState } from "react"; @@ -106,14 +128,14 @@ import { parseHydraDocumentation } from "@api-platform/api-doc-parser"; import authProvider from "utils/authProvider"; import { ENTRYPOINT } from "config/entrypoint"; -// Auth, Parser, Provider calls +// Functions and components created in the previous steps: const getHeaders = () => {...}; const fetchHydra = (url, options = {}) => {...}; const RedirectToLogin = () => {...}; const apiDocumentationParser = (setRedirectToLogin) => async () => {...}; const dataProvider = (setRedirectToLogin) => {...}; -const Admin = () => { +export const Admin = () => { const [redirectToLogin, setRedirectToLogin] = useState(false); return ( @@ -142,29 +164,32 @@ const Admin = () => { ); }; -export default Admin; ``` -### Additional Notes +### Example Implementation For the implementation of the admin component, you can find a working example in the [API Platform's demo application](https://github.com/api-platform/demo/blob/4.0/pwa/components/admin/Admin.tsx). ## OpenApiAdmin -This section explains how to set up and customize the [OpenApiAdmin component](https://api-platform.com/docs/admin/components/#openapi) authentication layer. -It covers: -* Creating a custom HTTP Client -* Data and rest data provider configuration -* Implementation of an auth provider +This section explains how to set up and customize the [`` component](./components.md/#openapi) to enable authentication. -### Data Provider & HTTP Client +In the following steps, we will see how to: -Create a custom HTTP client to add authentication tokens to request headers. -Configure the `openApiDataProvider`, and -inject the custom HTTP client into the [Simple REST Data Provider for React-Admin](https://github.com/Serind/ra-data-simple-rest). +- Make authenticated requests to the API (i.e. include the `Authorization` header) +- Implement an authProvider to redirect users to the login page if they are not authenticated, and clear expired tokens when encountering unauthorized `401` response + +### Making Authenticated Requests + +First, we need to create a custom `httpClient` to add authentication tokens (via the the `Authorization` HTTP header) to requests. + +We will then configure `openApiDataProvider` to use [`ra-data-simple-rest`](https://github.com/marmelab/react-admin/blob/master/packages/ra-data-simple-rest/README.md), a simple REST dataProvider for React Admin, and make it use the `httpClient` we created earlier. -**File:** `src/components/jsonDataProvider.tsx` ```typescript +// src/dataProvider.ts + +const getAccessToken = () => localStorage.getItem("token"); + const httpClient = async (url: string, options: fetchUtils.Options = {}) => { options.headers = new Headers({ ...options.headers, @@ -177,32 +202,31 @@ const httpClient = async (url: string, options: fetchUtils.Options = {}) => { return await fetchUtils.fetchJson(url, options); }; -const jsonDataProvider = openApiDataProvider({ +const dataProvider = openApiDataProvider({ dataProvider: simpleRestProvider(API_ENTRYPOINT_PATH, httpClient), entrypoint: API_ENTRYPOINT_PATH, docEntrypoint: API_DOCS_PATH, }); ``` -> [!NOTE] -> The `simpleRestProvider` provider expect the API to include a `Content-Range` header in the response. -> You can find more about the header syntax in the [Mozilla’s MDN documentation: Content-Range](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Range). -> -> The `getAccessToken` function retrieves the JWT token stored in the browser. +**Note:** The `simpleRestProvider` provider expect the API to include a `Content-Range` header in the response. You can find more about the header syntax in the [Mozilla’s MDN documentation: Content-Range](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Range). + +**Note:** The `getAccessToken` function retrieves the JWT token stored in the browser's localStorage. Replace it with your own logic in case you don't store the token that way. -### Authentication and Authorization +### Creating The AuthProvider -Create and export an `authProvider` object that handles authentication and authorization logic. +Now let's create and export an `authProvider` object that handles authentication and authorization logic. -**File:** `src/components/authProvider.tsx` ```typescript +// src/authProvider.ts + interface JwtPayload { - exp?: number; - iat?: number; - roles: string[]; + sub: string; username: string; } +const getAccessToken = () => localStorage.getItem("token"); + const authProvider = { login: async ({username, password}: { username: string; password: string }) => { const request = new Request(API_AUTH_PATH, { @@ -242,7 +266,7 @@ const authProvider = { const decoded = jwtDecode(token); return Promise.resolve({ - id: "", + id: decoded.sub, fullName: decoded.username, avatar: "", }); @@ -253,20 +277,23 @@ const authProvider = { export default authProvider; ``` -### Export OpenApiAdmin Component +### Updating The Admin Component -**File:** `src/App.tsx` -```typescript -import {OpenApiAdmin} from '@api-platform/admin'; -import authProvider from "./components/authProvider"; -import jsonDataProvider from "./components/jsonDataProvider"; -import {API_DOCS_PATH, API_ENTRYPOINT_PATH} from "./config/api"; +Finally, we can update the `Admin` component to use the `authProvider` and `dataProvider` we created earlier. + +```tsx +// src/Admin.tsx + +import { OpenApiAdmin } from '@api-platform/admin'; +import authProvider from "./authProvider"; +import dataProvider from "./dataProvider"; +import { API_DOCS_PATH, API_ENTRYPOINT_PATH } from "./config/api"; export default () => ( ); diff --git a/admin/components.md b/admin/components.md index ec3d634465e..5aca722b8cf 100644 --- a/admin/components.md +++ b/admin/components.md @@ -1,55 +1,137 @@ -# Components +# Components Reference -## Resource Components +## HydraAdmin -### AdminGuesser +Creates a complete Admin, using [``](./components.md#adminguesser), but configured specially for [Hydra](https://www.hydra-cg.com/). -`` 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, -for OpenAPI documented APIs, use the [OpenApiAdmin component](components.md#openapiadmin) 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. +**Tip:** For OpenAPI documented APIs, use the [`` component](./components.md#openapiadmin) instead. -`` renders all exposed resources by default, but you can choose what resource you want to render by passing [ResourceGuesser components](components.md#resourceguesser) as children. -Deprecated resources are hidden by default, but you can add them back using an explicit `` component. +**Tip:** If you want to use other formats (see supported formats: `@api-platform/api-doc-parser`) use [``](./components.md#adminguesser) instead. -```javascript -// App.js -import { AdminGuesser, ResourceGuesser } from '@api-platform/admin'; +```tsx +// App.tsx +import { HydraAdmin, ResourceGuesser } from '@api-platform/admin'; const App = () => ( - - - - + + + {/* ... */} + ); export default App; ``` -#### Props +### HydraAdmin Props + +| Name | Type | Value | required | Description | +| ------------ | ------------------- | ------------ | -------- | ---------------------------- | +| entrypoint | string | - | yes | entrypoint of the API | +| mercure | object|boolean | \* | no | configuration to use Mercure | +| dataProvider | object | dataProvider | no | hydra data provider to use | + +\* `false` to explicitly disable, `true` to enable with default parameters or an object with the following properties: + +- `hub`: the URL to your Mercure hub +- `jwt`: a subscriber JWT to access your Mercure hub +- `topicUrl`: the topic URL of your resources + +### Hydra Data Provider + +An implementation for the React Admin [dataProvider methods](https://marmelab.com/react-admin/DataProviderWriting.html): `create`, `delete`, `getList`, `getManyReference`, `getOne` and `update`. + +The `dataProvider` is used by API Platform Admin to communicate with the API. + +In addition, the specific `introspect` method parses your API documentation. + +Note that the `dataProvider` can be overridden to fit your API needs. + +### Hydra Schema Analyzer + +Analyses your resources and retrieves their types according to the [Schema.org](https://schema.org) vocabulary. + +## OpenApiAdmin -| 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 | -| includeDeprecated | boolean | true or false | no | displays or not deprecated resources | +Creates a complete Admin, as [``](./components.md#adminguesser), but configured specially for [OpenAPI](https://www.openapis.org/). + +**Tip:** If you want to use other formats (see supported formats: `@api-platform/api-doc-parser`) use [``](./components.md#adminguesser) instead. + +```tsx +// App.tsx +import { OpenApiAdmin, ResourceGuesser } from '@api-platform/admin'; -### ResourceGuesser +const App = () => ( + + + {/* ... */} + +); -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). +export default App; +``` -Otherwise, you can pass it your own CRUD components using `create`, `list`, `edit`, `show` props. +### OpenApiAdmin Props -```javascript -// App.js +| Name | Type | Value | required | Description | +| ------------- | ------------------- | ----- | -------- | ---------------------------- | +| docEntrypoint | string | - | yes | doc entrypoint of the API | +| entrypoint | string | - | yes | entrypoint of the API | +| dataProvider | dataProvider | - | no | data provider to use | +| mercure | object|boolean | \* | no | configuration to use Mercure | + +\* `false` to explicitly disable, `true` to enable with default parameters or an object with the following properties: + +- `hub`: the URL to your Mercure hub +- `jwt`: a subscriber JWT to access your Mercure hub +- `topicUrl`: the topic URL of your resources + +### Open API Data Provider + +An implementation for the React Admin [dataProvider methods](https://marmelab.com/react-admin/DataProviderWriting.html): `create`, `delete`, `getList`, `getManyReference`, `getOne` and `update`. + +The `dataProvider` is used by API Platform Admin to communicate with the API. + +In addition, the specific `introspect` method parses your API documentation. + +Note that the `dataProvider` can be overridden to fit your API needs. + +### Open API Schema Analyzer + +Analyses your resources and retrieves their types according to the [Schema.org](https://schema.org) vocabulary. + +## AdminGuesser + +`` automatically renders an [`` component](https://marmelab.com/react-admin/Admin.html) for resources exposed by a web API documented with any format supported by `@api-platform/api-doc-parser`. + +```tsx +// App.tsx +import { AdminGuesser } from '@api-platform/admin'; +import dataProvider from './dataProvider'; +import schemaAnalyzer from './schemaAnalyzer'; + +const App = () => ( + +); + +export default App; +``` + +Use it if your API is neither documented with Hydra nor OpenAPI, but in a format supported by `@api-platform/api-doc-parser`. + +**Tip:** For Hydra documented APIs, use the [`` component](./components.md#hydraadmin) instead. + +**Tip:** For OpenAPI documented APIs, use the [`` component](./components.md#openapiadmin) instead. + +`` renders all exposed resources by default, but you can choose what resource you want to render by passing [`` components](./components.md#resourceguesser) as children. + +```tsx +// App.tsx import { AdminGuesser, ResourceGuesser } from '@api-platform/admin'; +import dataProvider from './dataProvider'; +import schemaAnalyzer from './schemaAnalyzer'; const App = () => ( @@ -57,266 +139,310 @@ const App = () => ( name="books" list={BooksList} show={BooksShow} - create={BooksCreate} edit={BooksEdit} + create={BooksCreate} /> - + ); export default App; ``` -#### ResourceGuesser Props +**Tip:** Deprecated resources are hidden by default, but you can add them back using an explicit `` component. -| Name | Type | Value | required | Description | -| ---- | ------ | ----- | -------- | ------------------------ | -| name | string | - | yes | endpoint of the resource | +### AdminGuesser Props -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`. +| Name | Type | Value | required | Description | +| ----------------- | --------------- | -------------- | -------- | -------------------------------------------------------------------------------- | +| dataProvider | object | dataProvider | yes | the dataProvider to use to communicate with your API | +| schemaAnalyzer | object | schemaAnalyzer | yes | retrieves resource type according to [Schema.org](https://schema.org) vocabulary | +| authProvider | object | authProvider | no | the authProvider to use to manage authentication | +| admin | React component | - | no | React component to use to render the Admin | +| includeDeprecated | boolean | true or false | no | displays or not deprecated resources | -## Page Components +`` also accepts all props accepted by React Admin's [`` component](https://marmelab.com/react-admin/Admin.html), such as [`theme`](https://marmelab.com/react-admin/Admin.html#theme), [`darkTheme`](https://marmelab.com/react-admin/Admin.html#darktheme), [`layout`](https://marmelab.com/react-admin/Admin.html#layout) and many others. -### ListGuesser +## ResourceGuesser -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). +Based on React Admin [`` component](https://marmelab.com/react-admin/Resource.html), `` provides the default component to render for each view: [``](./components.md#createguesser), [``](./components.md#listguesser), [``](./components.md#editguesser) and [``](./components.md#showguesser). -Use `hasShow` and `hasEdit` props if you want to display `show` and `edit` buttons (both set to `true` by default). +You can also pass your own component to use for any view, using the `create`, `list`, `edit` or `show` props. -By default, `` comes with [Pagination](components.md#pagination). - -```javascript -// BooksList.js -import { FieldGuesser, ListGuesser } from '@api-platform/admin'; -import { ReferenceField, TextField } from 'react-admin'; +```tsx +// App.tsx +import { AdminGuesser, ResourceGuesser } from '@api-platform/admin'; -export const BooksList = (props) => ( - - - - - - - +const App = () => ( + + {/* Uses the default guesser components for each CRUD view */} + + {/* Overrides only the list view */} + + ); + +export default App; ``` -#### ListGuesser Props +### ResourceGuesser Props -| Name | Type | Value | required | Description | -| ------- | ------- | ----- | -------- | --------------------------------------- | -| filters | element | - | no | filters that can be applied to the list | +| Name | Type | Value | required | Description | +| ------ | ------------------- | ----- | -------- | ------------------------------------------- | +| name | string | - | yes | endpoint of the resource | +| list | React ComponentType | - | no | the component to render for the list view | +| create | React ComponentType | - | no | the component to render for the create view | +| edit | React ComponentType | - | no | the component to render for the edit view | +| show | React ComponentType | - | no | the component to render for the show view | -You can also use props accepted by React Admin [List](https://marmelab.com/react-admin/List.html). +`` also accepts all props accepted by React Admin's [`` component](https://marmelab.com/react-admin/Resource.html), such as [`recordRepresentation`](https://marmelab.com/react-admin/Resource.html#recordrepresentation), [`icon`](https://marmelab.com/react-admin/Resource.html#icon) or [`options`](https://marmelab.com/react-admin/Resource.html#options). -### CreateGuesser +## ListGuesser -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. +Based on React Admin [``](https://marmelab.com/react-admin/List.html), `` displays a list of records in a [``](https://marmelab.com/react-admin/Datagrid.html). -```javascript -// BooksCreate.js -import { CreateGuesser, InputGuesser } from '@api-platform/admin'; +If no children are passed, it will display fields guessed from the schema. -export const BooksCreate = (props) => ( - - - - - - - +```tsx +// BooksList.tsx +import { ListGuesser } from '@api-platform/admin'; + +export const BooksList = () => ( + /* Will display fields guessed from the schema */ + ); ``` -#### CreateGuesser Props +It also accepts a list of fields as children. They can be either [``](./components.md#fieldguesser) elements, or any [field component](https://marmelab.com/react-admin/Fields.html) +available in React Admin, such as [``](https://marmelab.com/react-admin/TextField.html), [``](https://marmelab.com/react-admin/DateField.html) or [``](https://marmelab.com/react-admin/ReferenceField.html) for instance. -You can use props accepted by React Admin [Create](https://marmelab.com/react-admin/CreateEdit.html). +```tsx +// BooksList.tsx +import { FieldGuesser, ListGuesser } from '@api-platform/admin'; +import { DateField, NumberField } from 'react-admin'; + +export const BooksList = () => ( + + {/* FieldGuesser comes from API Platform Admin */} + + + + + {/* DateField and NumberField come from React Admin */} + + + +); +``` -### EditGuesser +### ListGuesser Props -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. +`` accepts all props accepted by both React Admin [`` component](https://marmelab.com/react-admin/List.html) and [`` component](https://marmelab.com/react-admin/Datagrid.html). -```javascript -// BooksEdit.js -import { EditGuesser, InputGuesser } from '@api-platform/admin'; +For instance you can pass props such as [`filters`](https://marmelab.com/react-admin/List.html#filters-filter-inputs), [`sort`](https://marmelab.com/react-admin/List.html#sort) or [`pagination`](https://marmelab.com/react-admin/List.html#pagination). -export const BooksEdit = (props) => ( - - - - - - - -); -``` +## CreateGuesser -#### EditGuesser Props +Displays a creation page for a single item. Uses React Admin [``](https://marmelab.com/react-admin/Create.html) and [``](https://marmelab.com/react-admin/SimpleForm.html) components. -You can use props accepted by React Admin [Edit](https://marmelab.com/react-admin/CreateEdit.html). +If no children are passed, it will display inputs guessed from the schema. -### ShowGuesser +```tsx +// BooksCreate.tsx +import { CreateGuesser } from '@api-platform/admin'; -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. +export const BooksCreate = () => ( + /* Will display inputs guessed from the schema */ + +); +``` -```javascript -// BooksShow.js -import { FieldGuesser, ShowGuesser } from '@api-platform/admin'; +It also accepts a list of inputs as children, which can be either [``](./components.md#inputguesser) elements, or any [input component](https://marmelab.com/react-admin/Inputs.html) available in React Admin, +such as [``](https://marmelab.com/react-admin/TextInput.html), [``](https://marmelab.com/react-admin/DateInput.html) or [``](https://marmelab.com/react-admin/ReferenceInput.html) for instance. -export const BooksShow = (props) => ( - - - - - - - +```tsx +// BooksCreate.tsx +import { CreateGuesser, InputGuesser } from '@api-platform/admin'; +import { DateInput, TextInput, required } from 'react-admin'; + +export const BooksCreate = () => ( + + {/* InputGuesser comes from API Platform Admin */} + + + + + {/* DateInput and TextInput come from React Admin */} + + + ); ``` -#### ShowGuesser Props +### CreateGuesser Props -You can use props accepted by React Admin [Show component](https://marmelab.com/react-admin/Show.html). +`` accepts all props accepted by both React Admin [`` component](https://marmelab.com/react-admin/Create.html) and [`` component](https://marmelab.com/react-admin/SimpleForm.html). -## Hydra +For instance you can pass props such as [`redirect`](https://marmelab.com/react-admin/Create.html#redirect), [`defaultValues`](https://marmelab.com/react-admin/SimpleForm.html#defaultvalues) or [`warnWhenUnsavedChanges`](https://marmelab.com/react-admin/SimpleForm.html#warnwhenunsavedchanges). -### HydraAdmin +## EditGuesser -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. +Displays an edition page for a single item. Uses React Admin [``](https://marmelab.com/react-admin/Edit.html) and [``](https://marmelab.com/react-admin/SimpleForm.html) components. -```javascript -// App.js -import { HydraAdmin, ResourceGuesser } from '@api-platform/admin'; +If no children are passed, it will display inputs guessed from the schema. -const App = () => ( - - - {/* ... */} - -); +```tsx +// BooksEdit.tsx +import { EditGuesser } from '@api-platform/admin'; -export default App; +export const BooksEdit = () => ( + /* Will display inputs guessed from the schema */ + +); ``` -#### HydraAdmin Props +It also accepts a list of inputs as children, which can be either [``](./components.md#inputguesser) elements, or any [input component](https://marmelab.com/react-admin/Inputs.html) available in React Admin, +such as [``](https://marmelab.com/react-admin/TextInput.html), [``](https://marmelab.com/react-admin/DateInput.html) or [``](https://marmelab.com/react-admin/ReferenceInput.html) for instance. -| Name | Type | Value | required | Description | -| ------------ | ------------------- | ------------ | -------- | ---------------------------- | -| entrypoint | string | - | yes | entrypoint of the API | -| mercure | object|boolean | \* | no | configuration to use Mercure | -| dataProvider | object | dataProvider | no | hydra data provider to use | - -\* `false` to explicitly disable, `true` to enable with default parameters or an object with the following properties: +```tsx +// BooksEdit.tsx +import { EditGuesser, InputGuesser } from '@api-platform/admin'; +import { DateInput, TextInput, required } from 'react-admin'; -- `hub`: the URL to your Mercure hub -- `jwt`: a subscriber JWT to access your Mercure hub -- `topicUrl`: the topic URL of your resources +export const BooksEdit = () => ( + + {/* InputGuesser comes from API Platform Admin */} + + + -### Hydra Data Provider + {/* DateInput and TextInput come from React Admin */} + + + +); +``` -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. +### EditGuesser Props -### Hydra Schema Analyzer +`` accepts all props accepted by both React Admin [`` component](https://marmelab.com/react-admin/Edit.html) and [`` component](https://marmelab.com/react-admin/SimpleForm.html). -Analyses your resources and retrieves their types according to the [Schema.org](https://schema.org) vocabulary. +For instance you can pass props such as [`redirect`](https://marmelab.com/react-admin/Edit.html#redirect), [`mutationMode`](https://marmelab.com/react-admin/Edit.html#mutationmode), [`defaultValues`](https://marmelab.com/react-admin/SimpleForm.html#defaultvalues) or [`warnWhenUnsavedChanges`](https://marmelab.com/react-admin/SimpleForm.html#warnwhenunsavedchanges). -## OpenAPI +## ShowGuesser -### OpenApiAdmin +Displays a detailed page for one item. Based on React Admin [``](https://marmelab.com/react-admin/Show.html) ans [``](https://marmelab.com/react-admin/SimpleShowLayout.html) components. -Creates a complete Admin, as [AdminGuesser](components.md#adminguesser), but configured specially for [OpenAPI](https://www.openapis.org/). -If you want to use other formats (see supported formats: `@api-platform/api-doc-parser`) use [AdminGuesser](components.md#adminguesser) instead. +If you pass no children, it will display fields guessed from the schema. -```javascript -// App.js -import { OpenApiAdmin, ResourceGuesser } from '@api-platform/admin'; +```tsx +// BooksShow.tsx +import { ShowGuesser } from '@api-platform/admin'; -const App = () => ( - - - {/* ... */} - +export const BooksShow = () => ( + /* Will display fields guessed from the schema */ + ); - -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 | +It also accepts a list of fields as children, which can be either [``](./components.md#fieldguesser) elements, or any [field component](https://marmelab.com/react-admin/Fields.html) available in React Admin, +such as [``](https://marmelab.com/react-admin/TextField.html), [``](https://marmelab.com/react-admin/DateField.html) or [``](https://marmelab.com/react-admin/ReferenceField.html) for instance. -\* `false` to explicitly disable, `true` to enable with default parameters or an object with the following properties: +```tsx +// BooksShow.tsx +import { FieldGuesser, ShowGuesser } from '@api-platform/admin'; +import { DateField, NumberField } from 'react-admin'; -- `hub`: the URL to your Mercure hub -- `jwt`: a subscriber JWT to access your Mercure hub -- `topicUrl`: the topic URL of your resources +export const BooksShow = () => ( + + {/* FieldGuesser comes from API Platform Admin */} + + + -### Open API Data Provider + {/* DateField and NumberField come from React Admin */} + + + +); +``` -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. +### ShowGuesser Props -### Open API Schema Analyzer +`` accepts all props accepted by both React Admin [`` component](https://marmelab.com/react-admin/Show.html) and [`` component](https://marmelab.com/react-admin/SimpleShowLayout.html). -Analyses your resources and retrieves their types according to the [Schema.org](https://schema.org) vocabulary. -## Other Components +## FieldGuesser -### FieldGuesser +Renders a field according to its type, using the [schema analyzer](./components.md#hydra-schema-analyzer). -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). +Based on React Admin [field components](https://marmelab.com/react-admin/Fields.html), such as [``](https://marmelab.com/react-admin/TextField.html), [``](https://marmelab.com/react-admin/DateField.html) or [``](https://marmelab.com/react-admin/ReferenceField.html). -```javascript -// BooksShow.js +```tsx +// BooksShow.tsx import { FieldGuesser, ShowGuesser } from '@api-platform/admin'; -export const BooksShow = (props) => ( - - +export const BooksShow = () => ( + + {/* Renders a TextField */} + {/* Renders a NumberField */} - + {/* Renders a DateField */} ); ``` -#### FieldGuesser 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). +`` also accepts any [common field prop](https://marmelab.com/react-admin/Fields.html#common-field-props) supported by React Admin, such as [`label`](https://marmelab.com/react-admin/Fields.html#label) for instance. + +## InputGuesser -### InputGuesser +Renders an input according to its type, using the [schema analyzer](./components.md#hydra-schema-analyzer). -Uses React Admin [input components](https://marmelab.com/react-admin/Inputs.html) to generate inputs according to your API documentation (e.g. number HTML input for numbers, checkbox for booleans, selectbox for relationships...). +Uses React Admin [input components](https://marmelab.com/react-admin/Inputs.html), such as [``](https://marmelab.com/react-admin/TextInput.html), [``](https://marmelab.com/react-admin/DateInput.html) or [``](https://marmelab.com/react-admin/ReferenceInput.html). -#### InputGuesser Props +```tsx +// BooksCreate.tsx +import { CreateGuesser, InputGuesser } from '@api-platform/admin'; + +export const BooksCreate = () => ( + + {/* Renders a TextInput */} + + {/* Renders a NumberInput */} + + {/* Renders a DateInput */} + + +); +``` + +### InputGuesser Props | Name | Type | Value | required | Description | | ------ | ------ | ----- | -------- | ------------------------------------ | | source | string | - | yes | name of the property of the resource | + +`` also accepts any [common input prop](https://marmelab.com/react-admin/Inputs.html#common-input-props) supported by React Admin, such as [`defaultValue`](https://marmelab.com/react-admin/Inputs.html#defaultvalue), [`readOnly`](https://marmelab.com/react-admin/Inputs.html#readonly), [`helperText`](https://marmelab.com/react-admin/Inputs.html#helpertext) or [`label`](https://marmelab.com/react-admin/Inputs.html#label). + +You can also pass props that are specific to a certain input component. For example, if you know an `` will render a `` and you would like that input to be multiline, you can set the [`multiline`](https://marmelab.com/react-admin/TextInput.html#multiline) prop. + +```tsx + +``` + diff --git a/admin/customizing.md b/admin/customizing.md index eac82936902..83001b3540e 100644 --- a/admin/customizing.md +++ b/admin/customizing.md @@ -1,75 +1,192 @@ -# Customizing the Admin +# Customizing the Guessers -Customizing API Platform Admin is easy and idiomatic. The tool gives you the ability to customize everything, from the list of resource types that must be administrable to every single input or button. +Using `` or `` directly is a great way to quickly get started with API Platform Admin. They will introspect your API schema (using `@api-platform/api-doc-parser`) and automatically generate CRUD pages for all the resources it exposes. They will even [configure filtering, sorting, and real-time updates with Mercure](./schema.md) if your API supports it. -To do so, you can use the React components provided by API Platform Admin itself, [React Admin](https://marmelab.com/react-admin/), [Material UI](https://material-ui.com/), [community libraries](https://github.com/brillout/awesome-react-components), or [write your own](https://reactjs.org/tutorial/tutorial.html). +For some this may be enough, but you will often find yourself wanting to customize the generated pages further. For instance, you may want to: -## Customizing the Admin's Main Page and the Resource List +- Hide or reorder resources in the menu +- Hide or reorder columns in the list view +- Hide or reorder fields in the show, create and edit views +- Customize the generated list, e.g. add a default sort order +- Customize the generated create and edit views, e.g. to add a warning when there are unsaved changes +- Customize the generated inputs, e.g. set a custom label or make a text input multiline -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. +Such changes can't be achieved by modifying the Schema, they require customizing the React components generated by API Platform Admin. -However, it's also possible to display only specific resources, and to order them, while still benefiting from all discovery features provided by API Platform Admin. -To cherry-pick the resources to make available through the admin, pass a list of `` components as children of the root component: +Fortunately, API Platform Admin has you covered! -```javascript -import { HydraAdmin, ResourceGuesser } from '@api-platform/admin'; +## From `` To `` -export default () => ( - - - +If you are using `` or `` directly, there is a simple way to start customizing the generated pages. - {/* While deprecated resources are hidden by default, using an explicit ResourceGuesser component allows to add them back. */} - - +Simply open your browser's developer tools and look at the console. You will see messages like this: + +```txt +If you want to override at least one resource, paste this content in the component of your app: + + + + +``` + +This message tells you which resources are exposed by your API and how to customize the generated pages for each of them. + +Let's say we'd like to hide the `greetings` resource from the menu. We can do this by replacing the `` component (`` in our case) children with a list of ``: + +```diff +-import { HydraAdmin } from "@api-platform/admin"; ++import { HydraAdmin, ResourceGuesser } from "@api-platform/admin"; + +const App = () => ( +- ++ ++ ++ ++ ); ``` -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). +Now the `greetings` resource will no longer be displayed in the menu. -## Customizing the List View +![Customized Admin menu](./images/admin-menu.png) -The list view can be customized following the same pattern: +`` also accepts all props react-admin's [``](https://marmelab.com/react-admin/Resource.html) component accepts. This means that, for instance, you can use the `list` prop to use your own list component, but keep using the create, edit and show components introspected by ``: -```javascript -import { - HydraAdmin, - ResourceGuesser, - ListGuesser, - FieldGuesser, -} from '@api-platform/admin'; +```diff +import { HydraAdmin, ResourceGuesser } from "@api-platform/admin"; ++import { BookList } from "./BookList"; -const ReviewsList = (props) => ( - - - +const App = () => ( + +- ++ + + +); +``` - {/* While deprecated fields are hidden by default, using an explicit FieldGuesser component allows to add them back. */} - - +Likewise, you can use the `icon` prop to customize the icon displayed in the menu: + +```diff ++import AutoStoriesIcon from '@mui/icons-material/AutoStories'; ++import ReviewsIcon from '@mui/icons-material/Reviews'; +import { HydraAdmin, ResourceGuesser } from "@api-platform/admin"; + +const App = () => ( + +- ++ +- ++ + ); +``` -export default () => ( - - - {/* ... */} - +Here is the result: + +![Admin menu with custom icons](./images/admin-menu-icons.png) + +## Customizing the `` + +By default, `` will render a `` component as the list view for a resource. + +This component will automatically introspect the API schema and generate a list view with all the fields of the resource. + +![Admin default generated list view](./images/admin-default-list.png) + +This is already usable, but may not provide the best user experience yet. + +To start customizing the list view, you can look at the DevTools console. You will see messages like this: + +```txt +If you want to override at least one field, create a BookList component with this content: + +import { ListGuesser, FieldGuesser } from "@api-platform/admin"; + +export const BookList = () => ( + + + + + + + + +); + +Then, update your main admin component: + +import { HydraAdmin, ResourceGuesser } from "@api-platform/admin"; +import { BookList } from './BookList'; + +const App = () => ( + + + {/* ... */} + +); +``` + +If you follow these instructions, you will end up with the same view as before, but now you can start customizing it. + +For instance, we'll hide the 'Description' column as it takes too much space (we'll reserve that to the show view). And we will also add a default sort order to show the most recent books first. + +Here's how to achieve this: + +```diff +export const BookList = () => ( +- ++ + + +- + + + + ); ``` -In this example, only the fields `author`, `book` and `letter` (that is hidden by default because it is deprecated) will be displayed. The defined order will be respected. +And here is the result: + +![Admin with customized list guesser](./images/admin-custom-list-guesser.png) + +That's already better isn't it? 🙂 + +## Customizing the `` + +Removing or reordering `` components is not the only thing we can do. We can also customize them. -In addition to the `` component, [all React Admin Fields components](https://marmelab.com/react-admin/Fields.html) can be passed as children of ``. +Indeed, `` will forward additional props to the underlying React Admin [Field component](https://marmelab.com/react-admin/Fields.html). -## Customizing the Show View +This means we can use any [common field prop](https://marmelab.com/react-admin/Fields.html#common-field-props) on them. -For the show view: +For instance, let's add a `label` prop to customize the label of the ISBN column to be all uppercase: -```javascript +```diff +export const BookList = () => ( + +- ++ + + + + + +); +``` + +And here is the result: + +![Admin with customized list guesser and field guesser](./images/admin-custom-list-field-guesser.png) + +## Customizing the `` + +Following the same principles as the `` (including looking at the DevTools console) we can customize the show view. + +In the following example, the show view for the `books` resource was customized to make the label of the `isbn` field uppercase: + +```tsx import { HydraAdmin, ResourceGuesser, @@ -77,109 +194,150 @@ import { FieldGuesser, } from '@api-platform/admin'; -const ReviewsShow = (props) => ( - +const BookShow = () => ( + + + + - - - - {/* While deprecated fields are hidden by default, using an explicit FieldGuesser component allows to add them back. */} - - - + ); export default () => ( - - {/* ... */} + + ); ``` -In addition to the `` component, [all React Admin Fields components](https://marmelab.com/react-admin/Fields.html) can be passed as children of ``. +Here is the result: -## Customizing the Create Form +![Admin with customized show guesser](./images/admin-custom-show-guesser.png) -Again, the same logic applies to forms. Here is how to customize the create form: +## From `` To React Admin Fields -```javascript -import { - HydraAdmin, - ResourceGuesser, - CreateGuesser, - InputGuesser, -} from '@api-platform/admin'; +As mentioned in the [Customizing the ``](./customizing.md#customizing-the-fieldguesser) section, we can use any [common field prop](https://marmelab.com/react-admin/Fields.html#common-field-props) from React Admin to customize the `` elements. -const ReviewsCreate = (props) => ( - - - - +However in some cases you may want to go further and use a React Admin [field components](https://marmelab.com/react-admin/Fields.html), such as [``](https://marmelab.com/react-admin/TextField.html), [``](https://marmelab.com/react-admin/DateField.html) or [``](https://marmelab.com/react-admin/ReferenceField.html) directly, to access more advanced features. - {/* While deprecated fields are hidden by default, using an explicit InputGuesser component allows to add them back. */} - +For instance, you can replace a `` with a [``](https://marmelab.com/react-admin/DateField.html) to control more precisely how the publication date is displayed, leveraging the [`showTime`](https://marmelab.com/react-admin/DateField.html#showtime) prop: - - - -); +```diff +import { ShowGuesser, FieldGuesser } from '@api-platform/admin'; ++import { DateField } from 'react-admin'; -export default () => ( - - - {/* ... */} - +const ReviewShow = () => ( + +- ++ + ); ``` -In addition to the `` component, [all React Admin Input components](https://marmelab.com/react-admin/Inputs.html) can be passed as children of ``. +## Customizing the `` and `` -For instance, using an autocomplete input is straightforward, [check out the dedicated documentation entry](handling-relations.md#using-an-autocomplete-input-for-relations)! +Customizing the `` and `` is very similar to customizing the ``. -## Customizing the Edit Form +We can start by looking at the DevTools console to get the initial code of the components. -Finally, you can customize the edit form the same way: +```txt +If you want to override at least one input, create a ReviewEdit component with this content: -```javascript -import { - HydraAdmin, - ResourceGuesser, - EditGuesser, - InputGuesser, -} from '@api-platform/admin'; +import { EditGuesser, InputGuesser } from "@api-platform/admin"; -const ReviewsEdit = (props) => ( - - - - +export const ReviewEdit = () => ( + + + + + + + +); - {/* While deprecated fields are hidden by default, using an explicit InputGuesser component allows to add them back. */} - +Then, update your main admin component: - - - +import { HydraAdmin, ResourceGuesser } from "@api-platform/admin"; +import { ReviewEdit } from './ReviewEdit'; + +const App = () => ( + + + {/* ... */} + +); +``` + +Let's customize this `ReviewEdit` component to: + +- reorder the inputs +- make the `body` input multiline +- mark the `publicationDate` input as read-only + + +```diff +export const ReviewEdit = () => ( + +- +- +- +- +- ++ ++ ++ ++ ++ + ); +``` -export default () => ( - - - {/* ... */} - +Here is the result: + +![Admin with customized edit guesser](./images/admin-custom-edit-guesser.png) + +**Tip:** Here, we leveraged the `multiline` and `readOnly` props of the `` component. But you can use any [common input prop](https://marmelab.com/react-admin/Inputs.html#common-input-props) supported by React Admin [Inputs](https://marmelab.com/react-admin/Inputs.html) on them. + +## From `` To React Admin Inputs + +As mentioned in the previous section, we can use any [common input prop](https://marmelab.com/react-admin/Inputs.html#common-input-props) from React Admin to customize the `` elements. + +However in some cases you may want to go further and use a React Admin [input components](https://marmelab.com/react-admin/Inputs.html), such as [``](https://marmelab.com/react-admin/TextInput.html), [``](https://marmelab.com/react-admin/DateInput.html) or [``](https://marmelab.com/react-admin/ReferenceInput.html) directly, to access more advanced features. + +A good example is to use an [Autocomplete Input to edit a relation](./handling-relations.md#using-an-autocomplete-input-for-relations). + +This leverages both [``](https://marmelab.com/react-admin/ReferenceInput.html) and [``](https://marmelab.com/react-admin/AutocompleteInput.html) to offer a better user experience when editing the relation: + +```diff +import { EditGuesser, InputGuesser } from '@api-platform/admin'; ++import { ReferenceInput, AutocompleteInput } from 'react-admin'; + +const ReviewsEdit = () => ( + + +- ++ ++ ({ title: searchText })} ++ optionText="title" ++ /> ++ + ); ``` -In addition to the `` component, [all React Admin Input components](https://marmelab.com/react-admin/Inputs.html) can be passed as children of ``. +![Admin With AutocompleteInput](./images/AutocompleteInput.png) + +> [!WARNING] +> When replacing `` with a React Admin input component, the validation rules are not automatically applied. You will need to manually add them back. Fortunately, this is very easy to do. Read the [Validation With React Admin Inputs](./validation.md#validation-with-react-admin-inputs) section to learn more. -For instance, using an autocomplete input is straightforward, [checkout the dedicated documentation entry](handling-relations.md#using-an-autocomplete-input-for-relations)! +## Next Step -## Going Further +The above examples are limited to customizing the various API Platform Admin Guessers, but this is just the tip of the iceberg. -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 [authentication](https://marmelab.com/react-admin/Authentication.html), [authorization](https://marmelab.com/react-admin/Authorization.html) and deeper customization. +By leveraging React Admin components and props, you can go much further in customizing the generated pages. -To learn more about these capabilities, refer to [the React Admin documentation](https://marmelab.com/react-admin/documentation.html). +Head to the next section, [Customizing the Admin](./advanced-customization.md), for step-by-step examples. diff --git a/admin/file-upload.md b/admin/file-upload.md index 55af6df595b..4de9702a3a2 100644 --- a/admin/file-upload.md +++ b/admin/file-upload.md @@ -4,9 +4,9 @@ If you need to handle the file upload in the server part, please follow [the rel This documentation assumes you have a `/media_objects` endpoint accepting `multipart/form-data`-encoded data. -To manage the upload in the admin part, you need to customize [the create form](customizing.md#customizing-the-create-form) or [the edit form](customizing.md#customizing-the-edit-form). +To manage the upload in the admin part, you need to [customize the guessed create or edit form](./customizing.md#from-inputguesser-to-react-admin-inputs). -Add a [FileInput](https://marmelab.com/react-admin/Inputs.html#fileinput) as a child of the guesser. For example, for the create form: +Add a [``](https://marmelab.com/react-admin/FileInput.html) as a child of the guesser. For example, for the create form: ```js import { @@ -16,15 +16,15 @@ import { } from '@api-platform/admin'; import { FileField, FileInput } from 'react-admin'; -const MediaObjectsCreate = (props) => ( - +const MediaObjectsCreate = () => ( + ); -export default () => ( +export const App = () => ( {/* ... */} @@ -35,4 +35,5 @@ export default () => ( And that's it! The guessers are able to detect that you have used a `FileInput` and are passing this information to the data provider, through a `hasFileField` field in the `extraInformation` object, itself in the data. If you are using the Hydra data provider, it uses a `multipart/form-data` request instead of a JSON-LD one. -In the case of the `EditGuesser`, the HTTP method used also becomes a `POST` instead of a `PUT`, to prevent a [PHP bug](https://bugs.php.net/bug.php?id=55815). + +**Note:** In the case of the `EditGuesser`, the HTTP method used becomes a `POST` instead of a `PUT`, to prevent a [PHP bug](https://bugs.php.net/bug.php?id=55815). diff --git a/admin/getting-started.md b/admin/getting-started.md index 9378cb5f98f..cbd2c535fe7 100644 --- a/admin/getting-started.md +++ b/admin/getting-started.md @@ -1,37 +1,99 @@ # Getting Started -## Installation +## API Platform Symfony variant -If you use the [API Platform Symfony variant](../symfony/), API Platform Admin is already installed, you can skip this installation guide. +If you use the [API Platform Symfony variant](../symfony/), good news, API Platform Admin is already installed! 🎉 -Otherwise, follow this guide. +You can access it by visiting `/admin` on your API Platform application. -If you don't have an existing React Application, create one using [Create React App](https://create-react-app.dev/): +When running locally, you can also click on the "Admin" button of the welcome page at [https://localhost](https://localhost). -```console -npm init react-app my-admin +![API Platform welcome page](./images/api-platform-welcome-page.png) + +Here is what it looks like with a simple API exposing a `Greetings` resource: + +![Basic admin with the Greetings resource](./images/basic-admin-greetings.png) + +## Manual Installation + +If you did not use the Symfony variant of API Platform and need to install API Platform Admin manually, follow this guide. + +First, let's scaffold a React Admin Application by using the [Create React Admin](https://marmelab.com/react-admin/CreateReactAdmin.html) tool: + +```bash +npx create-react-admin@latest my-admin cd my-admin ``` Then, install the `@api-platform/admin` library: -```console +```bash npm install @api-platform/admin ``` -## Creating the Admin +Now you can use either: -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: +- [``](./getting-started.md#using-hydraadmin) to connect your app to an API exposing a Hydra documentation +- [``](./getting-started.md#using-openapiadmin) to connect your app to an API exposing an OpenAPI documentation -```javascript -import { HydraAdmin } from '@api-platform/admin'; +## Using `HydraAdmin` + +You can use the [``](./components.md#hydraadmin) component exported by `@api-platform/admin` to connect your app to an API exposing a Hydra documentation. + +If you used Create React Admin, you can replace the content of `src/App.tsx` by: + +```tsx +import { HydraAdmin } from "@api-platform/admin"; // Replace with your own API entrypoint // For instance if https://example.com/api/books is the path to the collection of book resources, then the entrypoint is https://example.com/api -export default () => ; +export const App = () => ; ``` +**Tip:** if you don't want to hardcode the API URL, you can [use an environment variable](https://vite.dev/guide/env-and-mode). + +Your new administration interface is ready! `HydraAdmin` will automatically fetch the Hydra documentation of your API and generate CRUD pages for all the resources it exposes. + +Type `npm run dev` to try it! + +![Basic admin with the Greetings resource](./images/basic-admin-greetings.png) + +**Tip:** There are more props you can pass to the `HydraAdmin` component to customize the dataProvider or the connection to Mercure. Check the [API documentation](./components.md#hydraadmin) for more information. + +**Tip:** You may also need to configure your API to set the correct CORS headers. Refer to the [Configuring CORS](./getting-started.md#configuring-cors) section below to learn more. + +## Using `OpenApiAdmin` + +You can use the [``](./components.md#openapiadmin) component exported by `@api-platform/admin` to connect your app to an API exposing an OpenAPI documentation. + +If you used Create React Admin, you can replace the content of `src/App.tsx` by: + +```tsx +import { OpenApiAdmin } from "@api-platform/admin"; + +// Replace with your own API entrypoint +export const App = () => ( + +); +``` + +**Tip:** If you don't want to hardcode the API URL, you can use an environment variable (see [Vite.js](https://vite.dev/guide/env-and-mode) or [Next.js](https://nextjs.org/docs/pages/building-your-application/configuring/environment-variables) docs). + +Your new administration interface is ready! `OpenApiAdmin` will automatically fetch the Hydra documentation of your API and generate CRUD pages for all the resources it exposes. + +Type `npm run dev` to try it! + +![Basic admin with the Greetings resource](./images/basic-admin-greetings.png) + +**Tip:** There are more props you can pass to the `OpenApiAdmin` component to customize the dataProvider or the connection to Mercure. Check the [API documentation](./components.md#openapiadmin) for more information. + +**Tip:** You may also need to configure your API to set the correct CORS headers. Refer to the [Configuring CORS](./getting-started.md#configuring-cors) section below to learn more. + +## Configuring CORS + Be sure to make your API send proper [CORS HTTP headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) to allow the admin's domain to access it. @@ -61,6 +123,6 @@ Clear the cache to apply this change: bin/console cache:clear --env=prod ``` -Your new administration interface is ready! Type `npm start` to try it! +## Next Step -Note: if you don't want to hardcode the API URL, you can [use an environment variable](https://create-react-app.dev/docs/adding-custom-environment-variables). +Learn how to add more features to your generated Admin by [Customizing the Schema](./schema.md). \ No newline at end of file diff --git a/admin/handling-relations.md b/admin/handling-relations.md index c98641f6a8f..7eebda4f133 100644 --- a/admin/handling-relations.md +++ b/admin/handling-relations.md @@ -1,48 +1,54 @@ # Handling Relations -API Platform Admin handles `to-one` and `to-many` relations automatically. +API Platform Admin handles `one-to-one`, `many-to-one` and `one-to-many` relations automatically. -Thanks to [the Schema.org support](schema.org.md), you can easily display the name of a related resource instead of its IRI. +However, in some cases, dependeing on whether or not you chose to embed the relation in the serialized data, you may need to customize the way the relation is displayed and/or can be edited. -## Embedded Relations +## Working With Embedded Relations -If a relation is an array of [embeddeds or an embedded](../core/serialization.md#embedding-relations) resource, the admin will keep them by default. +You can configure your API to [embed the related data](../core/serialization.md#embedding-relations) in the serialized response. -The embedded data will be displayed as text field and editable as text input: the admin cannot determine the fields present in it. -To display the fields you want, see [this section](handling-relations.md#display-a-field-of-an-embedded-relation). - -You can also ask the admin to automatically replace the embedded resources' data by their IRI, -by setting the `useEmbedded` parameter of the Hydra data provider to `false`. -Embedded data is inserted to a local cache: it will not be necessary to make more requests if you reference some fields of the embedded resource later on. +```js +// Without Embedded Book Data +{ + "@id": "/reviews/15", + id: 15, + rating: 5, + body: "A must-read for any software developer. Martin's insights are invaluable.", + author: "Alice Smith", + book: "/books/7" +} -```javascript -// admin/src/App.js +// With Embedded Book Data +{ + "@id": "/reviews/15", + id: 15, + rating: 5, + body: "A must-read for any software developer. Martin's insights are invaluable.", + author: "Alice Smith", + book: { + "@id": "/books/7", + id: 7, + title: "Clean Code", + author: "Robert C. Martin", + } +} +``` -import { HydraAdmin, fetchHydra, hydraDataProvider } from '@api-platform/admin'; -import { parseHydraDocumentation } from '@api-platform/api-doc-parser'; +If you do so, by default the admin will render the full object as text field and text input, which is not very user-friendly. -const entrypoint = process.env.REACT_APP_API_ENTRYPOINT; +![Embedded Relation With Full Object](images/embedded-relation-full-object.png) -const dataProvider = hydraDataProvider({ - entrypoint, - httpClient: fetchHydra, - apiDocumentationParser: parseHydraDocumentation, - mercure: true, - useEmbedded: false, -}); +There are two ways you can handle this situation: -export default () => ( - -); -``` +1. Change the Field and Input components to [display the fields you want](./handling-relations.md#displaying-a-field-of-an-embedded-relation) +2. Ask the admin to [return the embedded resources' IRI instead of the full record](./handling-relations.md#return-the-embedded-resources-iri-instead-of-the-full-record), by leveraging the `useEmbedded` parameter -## Display a Field of an Embedded Relation +### Displaying a Field of an Embedded Relation -If you have an [embedded relation](../core/serialization.md#embedding-relations) and need to display a nested field, the code you need to write depends of the value of `useEmbedded` of the Hydra data provider. +React Admin fields allow to use the dot notation (e.g. `book.title`) to target a field from an embedded relation. -If `true` (default behavior), you need to use the dot notation to display a field: - -```javascript +```jsx import { HydraAdmin, FieldGuesser, @@ -51,54 +57,130 @@ import { } from '@api-platform/admin'; import { TextField } from 'react-admin'; -const BooksList = (props) => ( - - +const ReviewList = () => ( + + + + {/* Use react-admin components directly when you want complex fields. */} - + ); -export default () => ( - - +export const App = () => ( + + ); ``` -If `useEmbedded` is explicitly set to `false`, make sure you write the code as if the relation needs to be fetched as a reference. +![Embedded Relation With Dot Notation](images/embedded-relation-dot-notation.png) -In this case, you _cannot_ use the dot separator to do so. +Allowing to edit the relation, on the other hand, is a little trickier, as it requires transforming the record to replace the nested object by its IRI. -Note that you cannot edit the embedded data directly with this behavior. +Fortunately, this can be done by leveraging the `transform` prop of the `` component. -For instance, if your API returns: +We can edit the relation by leveraging either [``](https://marmelab.com/react-admin/ReferenceInput.html) for a `to-one` relation or [``](https://marmelab.com/react-admin/ReferenceArrayInput.html) for a `to-many` relation. -```json -{ - "@context": "/contexts/Book", - "@id": "/books", - "@type": "Collection", - "member": [ - { - "@id": "/books/07b90597-542e-480b-a6bf-5db223c761aa", - "@type": "https://schema.org/Book", - "title": "War and Peace", - "author": { - "@id": "/authors/d7a133c1-689f-4083-8cfc-afa6d867f37d", - "@type": "https://schema.org/Author", - "firstName": "Leo", - "lastName": "Tolstoi" - } - } - ], - "totalItems": 1 +```jsx +import { + HydraAdmin, + InputGuesser, + EditGuesser, + ResourceGuesser, +} from '@api-platform/admin'; +import { ReferenceInput, AutocompleteInput } from 'react-admin'; + +const reviewEditTransform = (values) => ({ + ...values, + book: values.book['@id'], +}); + +const ReviewEdit = () => ( + + + + + + ({ title: searchText })} + /> + + +); + +export const App = () => ( + + + +); +``` + +This offers a nice and convenient way to edit the relation. + +![Embedded Relation With ReferenceInput](images/embedded-relation-ReferenceInput.png) + +**Tip:** We also had to customize ``'s child [``](https://marmelab.com/react-admin/AutocompleteInput.html) component to override its `label` and `filterToQuery` props. You can learn more about why that's necessary in the [Using an AutoComplete Input for Relations](./handling-relations.md#using-an-autocomplete-input-for-relations) section. + +### Return the Embedded Resources' IRI Instead of the Full Record + +You can also ask the admin to return the embedded resources' IRI instead of the full record, by setting the `useEmbedded` parameter of the Hydra data provider to `false`. + +```jsx +// admin/src/App.jsx + +import { HydraAdmin, dataProvider } from '@api-platform/admin'; + +const entrypoint = process.env.ENTRYPOINT; + +export const App = () => ( + +); +``` + +This tells the dataProvider to return only the IRI in the record, discarding the embedded data. + +```js +// With useEmbedded=true (default) +const record = { + "@id": "/reviews/15", + id: 15, + rating: 5, + body: "A must-read for any software developer. Martin's insights are invaluable.", + author: "Alice Smith", + book: { + "@id": "/books/7", + id: 7, + title: "Clean Code", + author: "Robert C. Martin", + } +} + +// With useEmbedded=false +const record = { + "@id": "/reviews/15", + id: 15, + rating: 5, + body: "A must-read for any software developer. Martin's insights are invaluable.", + author: "Alice Smith", + book: "/books/7" } ``` -If you want to display the author first name in the list, you need to write the following code: +This way, the related record's IRI is returned and can be displayed. -```javascript +![Embedded Relation With useEmbedded To False](images/embedded-relation-useEmbedded-false.png) + +We can improve the UI further by leveraging React Admin's [``](https://marmelab.com/react-admin/ReferenceField.html) component: + +```jsx import { HydraAdmin, FieldGuesser, @@ -107,31 +189,72 @@ import { } from '@api-platform/admin'; import { ReferenceField, TextField } from 'react-admin'; -const BooksList = (props) => ( - - - {/* Use react-admin components directly when you want complex fields. */} - - +const ReviewList = () => ( + + + + + + ); -export default () => ( - - - +export const App = () => ( + + + +); +``` + +This allows to display the title of the related book instead of its IRI. + +![Embedded Relation With ReferenceField](images/embedded-relation-ReferenceField.png) + +Lastly, this also allows to easily edit the relation by leveraging either [``](https://marmelab.com/react-admin/ReferenceInput.html) for a `to-one` relation or [``](https://marmelab.com/react-admin/ReferenceArrayInput.html) for a `to-many` relation. + +```jsx +import { + HydraAdmin, + InputGuesser, + EditGuesser, + ResourceGuesser, +} from '@api-platform/admin'; +import { ReferenceInput, AutocompleteInput } from 'react-admin'; + +const ReviewEdit = () => ( + + + + + + ({ title: searchText })} + /> + + +); + +export const App = () => ( + + ); ``` +This offers a nice and convenient way to edit the relation. + +![Embedded Relation With ReferenceInput](images/embedded-relation-ReferenceInput.png) + +**Tip:** We also had to customize ``'s child [``](https://marmelab.com/react-admin/AutocompleteInput.html) component to override its `filterToQuery` props. You can learn more about why that's necessary in the [Using an AutoComplete Input for Relations](./handling-relations.md#using-an-autocomplete-input-for-relations) section. + ## Using an Autocomplete Input for Relations -Let's go one step further thanks to the [customization capabilities](customizing.md) of API Platform Admin by adding autocompletion support to form inputs for relations. +By default, `` will render a [``](https://marmelab.com/react-admin/SelectInput.html) when it detects a relation. + +We can improve the UX further by rendering an [``](https://marmelab.com/react-admin/AutocompleteInput.html) instead. + +`` allows to search for a related record by typing its name in an input field. This is much more convenient when there are many records to choose from. Let's consider an API exposing `Review` and `Book` resources linked by a `many-to-one` relation (through the `book` property). @@ -192,9 +315,9 @@ class Book Notice the "partial search" [filter](../core/filters.md) on the `title` property of the `Book` resource class. -Now, let's configure API Platform Admin to enable autocompletion for the relation selector: +Now, let's configure API Platform Admin to enable autocompletion for the book selector. We will leverage the [``](https://marmelab.com/react-admin/ReferenceInput.html) and [``](https://marmelab.com/react-admin/AutocompleteInput.html) components from React Admin: -```javascript +```jsx import { HydraAdmin, ResourceGuesser, @@ -204,32 +327,14 @@ import { } from '@api-platform/admin'; import { ReferenceInput, AutocompleteInput } from 'react-admin'; -const ReviewsCreate = (props) => ( - - - - ({ title: searchText })} - optionText="title" - label="Books" - /> - - - - - - -); - -const ReviewsEdit = (props) => ( - +const ReviewsEdit = () => ( + ({ title: searchText })} optionText="title" - label="Books" /> @@ -239,67 +344,30 @@ const ReviewsEdit = (props) => ( ); -export default () => ( - - +export const App = () => ( + + ); ``` -If the book is embedded into a review and if the `useEmbedded` parameter is `true` (default behavior), -you need to change the `ReferenceInput` for the edit component: +The important things to note are: -```javascript -import { - HydraAdmin, - ResourceGuesser, - CreateGuesser, - EditGuesser, - InputGuesser, -} from '@api-platform/admin'; -import { ReferenceInput, AutocompleteInput } from 'react-admin'; +- the `filterToQuery` prop, which allows to search for books by title (leveraging the "partial search" filter mentioned above) +- the `optionText` prop, which tells the `` component to render books using their `title` property -const ReviewsCreate = (props) => ( - - - - ({ title: searchText })} - optionText="title" - label="Books" - /> - +You can now search for books by title in the book selector of the review form. - - - - -); +![Admin With AutocompleteInput](./images/AutocompleteInput.png) -const ReviewsEdit = (props) => ( - - +## Displaying Related Record Name Instead of Their IRI - - ({ title: searchText })} - format={(v) => v['@id'] || v} - optionText="title" - label="Books" - /> - +Thanks to the [Schema.org](./schema.md) support, you can easily display the name of a related resource instead of its IRI. - - - - -); +Follow the [Displaying Related Resource's Name Instead of its IRI](./schema.md#displaying-related-resources-name-instead-of-its-iri) section of the Schema.org documentation to implement this feature. -export default () => ( - - - -); -``` +## Going Further + +React Admin can handle many types of relations, even `many-to-many`. You can learn more about them in the [Fields For Relationships](https://marmelab.com/react-admin/FieldsForRelationships.html) documentation. -The autocomplete field should now work properly! +You can also read the [Handling Relationships in React Admin](https://marmelab.com/blog/2025/02/06/handling-relationships-in-react-admin.html) post from the React Admin blog for concrete examples and source code. diff --git a/admin/images/AutocompleteInput.png b/admin/images/AutocompleteInput.png new file mode 100644 index 00000000000..fff66e529e8 Binary files /dev/null and b/admin/images/AutocompleteInput.png differ diff --git a/admin/images/admin-custom-edit-guesser.png b/admin/images/admin-custom-edit-guesser.png new file mode 100644 index 00000000000..78e12a6cab7 Binary files /dev/null and b/admin/images/admin-custom-edit-guesser.png differ diff --git a/admin/images/admin-custom-field.png b/admin/images/admin-custom-field.png new file mode 100644 index 00000000000..fe5921e185d Binary files /dev/null and b/admin/images/admin-custom-field.png differ diff --git a/admin/images/admin-custom-input.png b/admin/images/admin-custom-input.png new file mode 100644 index 00000000000..552713c88a3 Binary files /dev/null and b/admin/images/admin-custom-input.png differ diff --git a/admin/images/admin-custom-list-field-guesser.png b/admin/images/admin-custom-list-field-guesser.png new file mode 100644 index 00000000000..b50bed61040 Binary files /dev/null and b/admin/images/admin-custom-list-field-guesser.png differ diff --git a/admin/images/admin-custom-list-guesser.png b/admin/images/admin-custom-list-guesser.png new file mode 100644 index 00000000000..755330a34d6 Binary files /dev/null and b/admin/images/admin-custom-list-guesser.png differ diff --git a/admin/images/admin-custom-show-guesser.png b/admin/images/admin-custom-show-guesser.png new file mode 100644 index 00000000000..26412203ace Binary files /dev/null and b/admin/images/admin-custom-show-guesser.png differ diff --git a/admin/images/admin-datagrid.png b/admin/images/admin-datagrid.png new file mode 100644 index 00000000000..0e4b4527d43 Binary files /dev/null and b/admin/images/admin-datagrid.png differ diff --git a/admin/images/admin-default-list.png b/admin/images/admin-default-list.png new file mode 100644 index 00000000000..8d0bb4fa83e Binary files /dev/null and b/admin/images/admin-default-list.png differ diff --git a/admin/images/admin-demo.mp4 b/admin/images/admin-demo.mp4 new file mode 100644 index 00000000000..9681502ec7e Binary files /dev/null and b/admin/images/admin-demo.mp4 differ diff --git a/admin/images/admin-demo.webm b/admin/images/admin-demo.webm new file mode 100644 index 00000000000..c213fe67b87 Binary files /dev/null and b/admin/images/admin-demo.webm differ diff --git a/admin/images/admin-filter.png b/admin/images/admin-filter.png new file mode 100644 index 00000000000..705c039453c Binary files /dev/null and b/admin/images/admin-filter.png differ diff --git a/admin/images/admin-form-layout.png b/admin/images/admin-form-layout.png new file mode 100644 index 00000000000..872e3a0894b Binary files /dev/null and b/admin/images/admin-form-layout.png differ diff --git a/admin/images/admin-menu-icons.png b/admin/images/admin-menu-icons.png new file mode 100644 index 00000000000..a4ee014e502 Binary files /dev/null and b/admin/images/admin-menu-icons.png differ diff --git a/admin/images/admin-menu.png b/admin/images/admin-menu.png new file mode 100644 index 00000000000..e0cdcb49804 Binary files /dev/null and b/admin/images/admin-menu.png differ diff --git a/admin/images/admin-reference-record-count.png b/admin/images/admin-reference-record-count.png new file mode 100644 index 00000000000..005fed2125b Binary files /dev/null and b/admin/images/admin-reference-record-count.png differ diff --git a/admin/images/admin-sort.png b/admin/images/admin-sort.png new file mode 100644 index 00000000000..1b651647df5 Binary files /dev/null and b/admin/images/admin-sort.png differ diff --git a/admin/images/admin-tabbed-show-layout.png b/admin/images/admin-tabbed-show-layout.png new file mode 100644 index 00000000000..ad8e4334b6a Binary files /dev/null and b/admin/images/admin-tabbed-show-layout.png differ diff --git a/admin/images/admin-undoable-mutation.png b/admin/images/admin-undoable-mutation.png new file mode 100644 index 00000000000..2505b86c496 Binary files /dev/null and b/admin/images/admin-undoable-mutation.png differ diff --git a/admin/images/admin-warnWhenUnsavedChanges.png b/admin/images/admin-warnWhenUnsavedChanges.png new file mode 100644 index 00000000000..924c91aeed9 Binary files /dev/null and b/admin/images/admin-warnWhenUnsavedChanges.png differ diff --git a/admin/images/api-platform-admin-theme.png b/admin/images/api-platform-admin-theme.png new file mode 100644 index 00000000000..08a94780173 Binary files /dev/null and b/admin/images/api-platform-admin-theme.png differ diff --git a/admin/images/api-platform-welcome-page.png b/admin/images/api-platform-welcome-page.png new file mode 100644 index 00000000000..11fa3b88a18 Binary files /dev/null and b/admin/images/api-platform-welcome-page.png differ diff --git a/admin/images/basic-admin-greetings.png b/admin/images/basic-admin-greetings.png new file mode 100644 index 00000000000..3c115f4fc2a Binary files /dev/null and b/admin/images/basic-admin-greetings.png differ diff --git a/admin/images/embedded-relation-ReferenceField.png b/admin/images/embedded-relation-ReferenceField.png new file mode 100644 index 00000000000..a687ade02ec Binary files /dev/null and b/admin/images/embedded-relation-ReferenceField.png differ diff --git a/admin/images/embedded-relation-ReferenceInput.png b/admin/images/embedded-relation-ReferenceInput.png new file mode 100644 index 00000000000..ffeea852f0c Binary files /dev/null and b/admin/images/embedded-relation-ReferenceInput.png differ diff --git a/admin/images/embedded-relation-dot-notation.png b/admin/images/embedded-relation-dot-notation.png new file mode 100644 index 00000000000..aef056a2fd4 Binary files /dev/null and b/admin/images/embedded-relation-dot-notation.png differ diff --git a/admin/images/embedded-relation-full-object.png b/admin/images/embedded-relation-full-object.png new file mode 100644 index 00000000000..0eecdb36a31 Binary files /dev/null and b/admin/images/embedded-relation-full-object.png differ diff --git a/admin/images/embedded-relation-useEmbedded-false.png b/admin/images/embedded-relation-useEmbedded-false.png new file mode 100644 index 00000000000..8405a95c2f9 Binary files /dev/null and b/admin/images/embedded-relation-useEmbedded-false.png differ diff --git a/admin/images/react-admin-theme.png b/admin/images/react-admin-theme.png new file mode 100644 index 00000000000..5e6191e8dd2 Binary files /dev/null and b/admin/images/react-admin-theme.png differ diff --git a/admin/images/related-record-with-iri.png b/admin/images/related-record-with-iri.png new file mode 100644 index 00000000000..eebdf49687e Binary files /dev/null and b/admin/images/related-record-with-iri.png differ diff --git a/admin/images/related-record-with-name.png b/admin/images/related-record-with-name.png new file mode 100644 index 00000000000..b8a2417868f Binary files /dev/null and b/admin/images/related-record-with-name.png differ diff --git a/admin/index.md b/admin/index.md index 0349b705bcf..6330913770d 100644 --- a/admin/index.md +++ b/admin/index.md @@ -1,36 +1,59 @@ # The API Platform Admin -![Screencast](images/admin-demo.gif) + -API Platform Admin is a tool to automatically create a beautiful (Material Design) and fully-featured administration interface -for any API supporting [the Hydra Core Vocabulary](https://www.hydra-cg.com/), exposing an [OpenAPI documentation](https://www.openapis.org/) -or other API specification formats supported by [`@api-platform/api-doc-parser`](https://github.com/api-platform/api-doc-parser). +API Platform **Admin** is a tool to automatically create a beautiful (Material Design) and fully-featured administration interface +for any API implementing specification formats supported by [`@api-platform/api-doc-parser`](https://github.com/api-platform/api-doc-parser). -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 or an OpenAPI documentation. +In particular, that includes: -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. +- APIs using [the Hydra Core Vocabulary](https://www.hydra-cg.com/) +- APIs exposing an [OpenAPI documentation](https://www.openapis.org/) -API Platform Admin parses the API documentation then uses the awesome [React Admin](https://marmelab.com/react-admin/) -library to expose a nice, responsive, management interface (Create-Retrieve-Update-Delete) for all documented resource types. +Of course, API Platform Admin is the perfect companion of APIs created +using [the API Platform framework](https://api-platform.com). But it also supports APIs written with any other programming language or framework as long as they expose a standard Hydra or OpenAPI documentation. -You can **customize everything** by using provided React Admin and [MUI](https://mui.com/) components, or by writing your custom [React](https://reactjs.org/) components. +## Based On React Admin + +API Platform Admin is a Single Page Application (SPA), based on [React Admin](https://marmelab.com/react-admin/), a powerful frontend framework for building B2B applications on top of REST/GraphQL APIs, written in TypeScript and React. + +Thanks to its built-in **guessers**, API Platform Admin parses the API documentation then uses React Admin to expose a nice, responsive management interface (Create-Retrieve-Update-Delete, i.e. CRUD) for all documented resource types. + +Afterwards, you can **customize everything** by using the numerous components provided by [React Admin](https://marmelab.com/react-admin/documentation.html) and [MUI](https://mui.com/), or even writing your own [React](https://reactjs.org/) components. + +

React Admin Screencast
Watch the React Admin screencast

## 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 +Simply by reading your API documentation, API Platform Admin provides the following features: + +- Generate 'list', 'create', 'show', and 'edit' views for all resources +- Automatically detect the type for inputs and fields +- Client-side [validation](./validation.md) on required inputs +- Pagination +- Filtering and ordering +- Easily view and edit [related records](./handling-relations.md) +- Display the related resource’s name instead of its IRI ([using the Schema.org vocabulary](./schema.md#displaying-related-resources-name-instead-of-its-iri)) - Nicely displays server-side errors (e.g. advanced validation) -- 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** +- Real-time updates with [Mercure](https://mercure.rocks) + +By [leveraging React Admin components](./advanced-customization.md), you can further customize the generated interface and get access to many more features: + +- Powerful Datagrid components +- Search and filtering +- Advanced form validation +- Undoable mutations +- Authentication +- Access Control +- Internationalization +- [And many more](https://marmelab.com/react-admin/Features.html) + +## Next Step + +Get your Admin up and running by following the [Getting Started guide](./getting-started.md). \ No newline at end of file diff --git a/admin/openapi.md b/admin/openapi.md deleted file mode 100644 index cfba6580cfd..00000000000 --- a/admin/openapi.md +++ /dev/null @@ -1,46 +0,0 @@ -# OpenAPI - -API Platform Admin has native support for API exposing an [OpenAPI documentation](https://www.openapis.org/). - -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'; - -export default () => ( - -); -``` - -> [!NOTE] -> -> The OpenAPI documentation needs to follow some assumptions to be understood correctly by the underlying `api-doc-parser`. -> See the [dedicated part in the `api-doc-parser` library README](https://github.com/api-platform/api-doc-parser#openapi-support). - -## Data Provider - -By default, the component will use a basic data provider, without pagination support. - -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'; - -export default () => ( - -); -``` - -## Mercure Support - -Mercure support can be enabled manually by giving the `mercure` prop to the `OpenApiAdmin` component. - -See also [the dedicated section](real-time-mercure.md). diff --git a/admin/performance.md b/admin/performance.md index d769f17ec8c..b856ab16ae4 100644 --- a/admin/performance.md +++ b/admin/performance.md @@ -1,4 +1,4 @@ -# Performance +# Performance Tips To make the admin faster and greener, you can make some changes to your API. @@ -9,9 +9,9 @@ the admin will fetch the relations one by one. In this case, it can be improved by doing only one request for all the related resources instead. -To do so, you need to make sure the [search filter](../core/filters.md#search-filter) is enabled for the identifier of the related resource. +To do so, you need to make sure the [search filter](../core/doctrine-filters.md#search-filter) is enabled for the identifier of the related resource. -For instance, if you have a book resource having a relation to author resources and you display the author names on your book list, +For instance, if you have a `book` resource having a relation to `author` resources and you display the author names on your book list, you can make sure the authors are retrieved in one go by writing: ```php @@ -36,3 +36,16 @@ class Author public string $name; } ``` + +Instead of issuing a separate request for each author, the admin will now fetch all the authors in one go, with a request similar to the following: + +```txt +https://localhost/authors? + page=1 + &itemsPerPage=5 + &id[0]=/authors/7 + &id[1]=/authors/8 + &id[2]=/authors/9 + &id[3]=/authors/10 + &id[4]=/authors/11 +``` diff --git a/admin/schema.md b/admin/schema.md new file mode 100644 index 00000000000..18618db1bbe --- /dev/null +++ b/admin/schema.md @@ -0,0 +1,147 @@ +# Customizing the Schema + +Both [`HydraAdmin`](./components.md#hydraadmin) and [`OpenApiAdmin`](./components.md#openapiadmin) leverage introspection of the API schema to discover its capabilities, like **filtering** and **sorting**. + +They also detect wether the API has real-time capabilities using [Mercure](./real-time-mercure.md), and automatically enable it if it does. + +Lastly, API Platform Admin has native support for the popular [Schema.org](./schema.md#about-schemaorg) vocabulary, which enables it to automatically use the field type matching your data, or display a related resource's name instead of its IRI. + +## Adding Filtering Capabilities + +You can add the [`ApiFilter` attribute](../core/filters.md#apifilter-attribute) to an API Platform resource to configure a filter on a property. + +For instance, here is how configure filtering on the `id`, `title` and `author` properties of a `Book` resource: + +```php + 'exact', + 'title' => 'ipartial', + 'author' => 'ipartial' +])] +class Book +{ + // ... +} +``` + +If you are using the guessers, the Admin will automatically update the Book list view to include a filter on the selected properties. + +![Filtering on the title property](./images/admin-filter.png) + +**Tip:** Learn more about the [`ApiFilter` attribute](../core/filters.md#apifilter-attribute) in the core documentation. + +## Adding Sorting Capabilities + +You can also use the [`ApiFilter` attribute](../core/filters.md#apifilter-attribute) on an API Plaform resource to configure sorting. + +For instance, here is how to configure sorting on the `id`, `isbn`, `title`, `author` and `publicationDate` properties of a `Book` resource: + +```php + 'ASC', + 'isbn' => 'ASC', + 'title' => 'ASC', + 'author' => 'ASC', + 'publicationDate' => 'DESC' +])] +class Book +{ + // ... +} +``` + +If you are using the guessers, the Admin will automatically update the Book list view to make the selected columns sortable. + +![Sorting by the title property](./images/admin-sort.png) + +**Tip:** Learn more about the [`ApiFilter` attribute](../core/filters.md#apifilter-attribute) in the core documentation. + +## Enabling Real-Time Updates + +You can use the `mercure` attribute to hint API Platform that it must dispatch the updates regarding the given resources to the Mercure hub: + +```php + Schema.org is a collaborative, community activity with a mission to create, maintain, and promote schemas for structured data on the Internet, on web pages, in email messages, and beyond. + +To leverage this capability, your API must use the JSON-LD format and the appropriate Schema.org types. +The following examples will use [API Platform Core](../core/) to create such API, but keep in mind that this feature will work with any JSON-LD API using the Schema.org vocabulary, regardless of the used web framework or programming language. + +## Displaying Related Resource's Name Instead of its IRI + +By default, IRIs of related objects are displayed in lists and forms. +However, it is often more user-friendly to display a string representation of the resource (such as its name) instead of its ID. + +To configure which property should be shown to represent your entity, map the property containing the name of the object with the `https://schema.org/name` type: + +```php +// api/src/Entity/Person.php +... + +#[ApiProperty(iris: ["https://schema.org/name"])] +private $name; + +... +``` + +| With IRI | With Resource Name | +| ---------------------------------------------------------------- | ---------------------------------------------------------------------------- | +| ![Related Record With IRI](./images/related-record-with-iri.png) | ![Related Record With Resource Name](./images/related-record-with-name.png) | + +## Emails, URLs and Identifiers + +Besides, it is also possible to use the documentation to customize some fields automatically while configuring the semantics of your data. + +The following Schema.org types are currently supported by API Platform Admin: + +- `https://schema.org/email`: the field will be rendered using the [``](https://marmelab.com/react-admin/EmailField.html) React Admin component +- `https://schema.org/url`: the field will be rendered using the [``](https://marmelab.com/react-admin/UrlField.html) React Admin component +- `https://schema.org/identifier`: the field will be formatted properly in inputs + +Note: if you already use validation on your properties, the semantics are already configured correctly (see [the correspondence table](../core/validation.md#open-vocabulary-generated-from-validation-metadata))! + +## Next Step + +Learn how to tweak the generated Admin by [Customizing the Guessers](./customizing.md). diff --git a/admin/schema.org.md b/admin/schema.org.md deleted file mode 100644 index f8ecc7b7c75..00000000000 --- a/admin/schema.org.md +++ /dev/null @@ -1,37 +0,0 @@ -# Using the Schema.org Vocabulary - -API Platform Admin has native support for the popular [Schema.org](https://schema.org) vocabulary. - -> Schema.org is a collaborative, community activity with a mission to create, maintain, and promote schemas for structured data on the Internet, on web pages, in email messages, and beyond. - -To leverage this capability, your API must use the JSON-LD format and the appropriate Schema.org types. -The following examples will use [API Platform Core](../core/) to create such API, but keep in mind that this feature will work with any JSON-LD API using the Schema.org vocabulary, regardless of the used web framework or programming language. - -## Displaying Related Resource's Name Instead of its IRI - -By default, IRIs of related objects are displayed in lists and forms. -However, it is often more user-friendly to display a string representation of the resource (such as its name) instead of its ID. - -To configure which property should be shown to represent your entity, map the property containing the name of the object with the `https://schema.org/name` type: - -```php -// api/src/Entity/Person.php -... - -#[ApiProperty(iris: ["https://schema.org/name"])] -private $name; - -... -``` - -## Emails, URLs and Identifiers - -Besides, it is also possible to use the documentation to customize some fields automatically while configuring the semantics of your data. - -The following Schema.org types are currently supported by API Platform Admin: - -- `https://schema.org/email`: the field will be rendered using the `` 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/admin/validation.md b/admin/validation.md index efc67c707de..777ecdd318d 100644 --- a/admin/validation.md +++ b/admin/validation.md @@ -5,7 +5,7 @@ API Platform Admin manages automatically two types of validation: client-side va ## Client-side Validation If the API documentation indicates that a field is mandatory, -API Platform Admin will automatically add a [required client-side validation](https://marmelab.com/react-admin/CreateEdit.html#per-input-validation-built-in-field-validators). +API Platform Admin will automatically add a [required client-side validation](https://marmelab.com/react-admin/Validation.html#per-input-validation-built-in-field-validators). For instance, with API Platform as backend, if you write the following: @@ -25,7 +25,7 @@ class Book } ``` -If you create a new book and touch the "Title" field without typing, you will see: +If you create a new book and try to submit without filling the "Title" field, you will see: ![Required title field](images/required-field.png) @@ -34,8 +34,7 @@ If you create a new book and touch the "Title" field without typing, you will se When the form is submitted and if submission errors are received, API Platform Admin will automatically show the errors for the corresponding fields. -To do so, it uses the [submission validation](https://marmelab.com/react-admin/CreateEdit.html#submission-validation) feature of React Admin, -and the mapping between the response and the fields is done by the [schema analyzer](components.md#schemaanalyzer) with its method `getSubmissionErrors`. +To do so, it uses the [Server-Side Validation](https://marmelab.com/react-admin/Validation.html#server-side-validation) feature of React Admin, and the mapping between the response and the fields is done by the [schema analyzer](components.md#hydra-schema-analyzer) with its method `getSubmissionErrors`. API Platform is supported by default, but if you use another backend, you will need to override the `getSubmissionErrors` method. @@ -60,3 +59,39 @@ class Book If you submit the form with an invalid ISBN, you will see: ![Submission error field](images/submission-error-field.png) + +## Validation With React Admin Inputs + +If you replace an `` with a React Admin [input component](https://marmelab.com/react-admin/Inputs.html), such as [``](https://marmelab.com/react-admin/TextInput.html), [``](https://marmelab.com/react-admin/DateInput.html) or [``](https://marmelab.com/react-admin/ReferenceInput.html), you will need to **manually add the validation rules back**. + +Fortunately, this is very easy to do, thanks to the [`validate`](https://marmelab.com/react-admin/Inputs.html#validate) prop of the input components. + +For instance, here is how to replace the input for the required `title` field: + +```diff +import { EditGuesser, InputGuesser } from '@api-platform/admin'; ++import { TextInput, required } from 'react-admin'; + +export const BookEdit = () => ( + +- ++ + +); +``` + +React Admin already comes with several [built-in validators](https://marmelab.com/react-admin/Validation.html#per-input-validation-built-in-field-validators), such as: + +* `required(message)` if the field is mandatory, +* `minValue(min, message)` to specify a minimum value for integers, +* `maxValue(max, message)` to specify a maximum value for integers, +* `minLength(min, message)` to specify a minimum length for strings, +* `maxLength(max, message)` to specify a maximum length for strings, +* `number(message)` to check that the input is a valid number, +* `email(message)` to check that the input is a valid email address, +* `regex(pattern, message)` to validate that the input matches a regular expression, +* `choices(list, message)` to validate that the input is within a given list + +React Admin also supports [Global Validation](https://marmelab.com/react-admin/Validation.html#global-validation) (at the form level). + +Check out the [Form Validation](https://marmelab.com/react-admin/Validation.html) documentation to learn more. diff --git a/core/bootstrap.md b/core/bootstrap.md index 21bab4c747d..68c6906ba8c 100644 --- a/core/bootstrap.md +++ b/core/bootstrap.md @@ -23,12 +23,12 @@ use ApiPlatform\Action\EntrypointAction; use ApiPlatform\Action\ExceptionAction; use ApiPlatform\Action\NotExposedAction; use ApiPlatform\Action\PlaceholderAction; -use ApiPlatform\Api\IdentifiersExtractor; -use ApiPlatform\Api\ResourceClassResolver; -use ApiPlatform\Api\UriVariablesConverter; -use ApiPlatform\Api\UriVariableTransformer\DateTimeUriVariableTransformer; -use ApiPlatform\Api\UriVariableTransformer\IntegerUriVariableTransformer; -use ApiPlatform\Api\UrlGeneratorInterface as ApiUrlGeneratorInterface; +use ApiPlatform\Metadata\IdentifiersExtractor; +use ApiPlatform\Metadata\ResourceClassResolver; +use ApiPlatform\Metadata\UriVariablesConverter; +use ApiPlatform\Metadata\UriVariableTransformer\DateTimeUriVariableTransformer; +use ApiPlatform\Metadata\UriVariableTransformer\IntegerUriVariableTransformer; +use ApiPlatform\Metadata\UrlGeneratorInterface as ApiUrlGeneratorInterface; use ApiPlatform\Symfony\Validator\EventListener\ValidationExceptionListener; use ApiPlatform\Documentation\DocumentationInterface; use ApiPlatform\Symfony\EventListener\AddFormatListener; diff --git a/core/configuration.md b/core/configuration.md index 7306e18e5b6..0d55bd5c506 100644 --- a/core/configuration.md +++ b/core/configuration.md @@ -369,9 +369,12 @@ api_platform: input: ~ output: ~ stateless: ~ + schemes: ~ + options: ~ + host: ~ # The URL generation strategy to use for IRIs - url_generation_strategy: !php/const ApiPlatform\Api\UrlGeneratorInterface::ABS_PATH + url_generation_strategy: !php/const ApiPlatform\Metadata\UrlGeneratorInterface::ABS_PATH # To enable collecting denormalization errors collectDenormalizationErrors: false @@ -708,7 +711,7 @@ If you need to globally configure all the resources instead of adding configurat [ diff --git a/core/content-negotiation.md b/core/content-negotiation.md index 7194b60c855..d7f1edb5ae9 100644 --- a/core/content-negotiation.md +++ b/core/content-negotiation.md @@ -356,7 +356,7 @@ merge new encoders and normalizers in API Platform. ## JSON:API sparse fieldset and sort parameters > [!WARNING] -> The SortFilter is for Eloquent, the Doctrine equivalent is the OrderFilter. +> The SortFilter is for Eloquent, the Doctrine equivalent is the OrderFilter. > The config/api-platform.php is Laravel specific. When working with JSON:API you may want to declare the `SparseFieldset` and the @@ -371,11 +371,11 @@ use ApiPlatform\JsonApi\Filter\SparseFieldset; use ApiPlatform\Laravel\Eloquent\Filter\JsonApi\SortFilter; return [ - // ... - 'parameters' => [ - new QueryParameter(key: 'fields', filter: SparseFieldset::class), - new QueryParameter(key: 'sort', filter: SortFilter::class), - ], + // ... + 'parameters' => [ + new QueryParameter(key: 'fields', filter: SparseFieldset::class), + new QueryParameter(key: 'sort', filter: SortFilter::class), + ], ]; ``` diff --git a/core/doctrine-filters.md b/core/doctrine-filters.md index 7186492ff74..a9e6e3778e6 100644 --- a/core/doctrine-filters.md +++ b/core/doctrine-filters.md @@ -1268,7 +1268,7 @@ use ApiPlatform\Core\Annotation\ApiResource; use App\Filter\CustomAndFilter; #[ApiResource] -#[ApiFilter(CustomAndFilter::class, properties=["name", "cost"])] +#[ApiFilter(CustomAndFilter::class, properties: ['name', 'cost'])] class Offer { // ... diff --git a/core/elasticsearch.md b/core/elasticsearch.md index 370085e987e..53b9db80951 100644 --- a/core/elasticsearch.md +++ b/core/elasticsearch.md @@ -8,7 +8,7 @@ application search, security analytics, metrics, logging, etc. API Platform comes natively with the **reading** support for Elasticsearch. It uses internally the official PHP client for Elasticsearch: [Elasticsearch-PHP](https://www.elastic.co/guide/en/elasticsearch/client/php-api/current/index.html). -Be careful, API Platform only supports Elasticsearch >= 7.11.0 < 8.0 and Elasticsearch >= 8.4 < 9.0. Support for +Be careful, API Platform only supports Elasticsearch >= 7.11.0 < 8.0 and Elasticsearch >= 8.4 < 9.0. Support for Elasticsearch 8 was introduced in API Platform 3.2. ## Enabling Reading Support diff --git a/core/errors.md b/core/errors.md index f0ca272931b..2bc78da84b7 100644 --- a/core/errors.md +++ b/core/errors.md @@ -323,3 +323,68 @@ We recommend using the `\ApiPlatform\Metadata\Exception\ProblemExceptionInterfac `\ApiPlatform\Metadata\Exception\HttpExceptionInterface`. For security reasons we add: `normalizationContext: ['ignored_attributes' => ['trace', 'file', 'line', 'code', 'message', 'traceAsString']]` because you usually don't want these. You can override this context value if you want. + +## Document your exceptions + +Since 3.4, you also have the possibility to link your specific domain exceptions to your ApiResources so that they appear +directly in your OpenAPI definition ! + +Let's say that you have a `Greetings` resource, and that one of its providers can throw the following exception for the +`ApiPlatform\Metadata\GetCollection` Operation: + +```php +use ApiPlatform\Metadata\ErrorResource; +use ApiPlatform\Metadata\Exception\ProblemExceptionInterface; + +#[ErrorResource] +class MyDomainException extends \Exception implements ProblemExceptionInterface +{ + public function getType(): string + { + return '/errors/418'; + } + + public function getTitle(): ?string + { + return 'Teapot error'; + } + + public function getStatus(): ?int + { + return 418; + } + + public function getDetail(): ?string + { + return $this->getMessage(); + } + + public function getInstance(): ?string + { + return null; + } + + public string $myCustomField = 'I usually prefer coffee.'; +} +``` + +As long as your Exception implements `ApiPlatform\Metadata\Exception\ProblemExceptionInterface` and has the `ErrorResource` +attribute, you can then map it to your Operation this way: + +```php +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\GetCollection; +use App\Exception\MyDomainException; + +#[ApiResource(operations: [ + new GetCollection(errors: [MyDomainException::class]) + ], +)] +class Greeting +{ +} +``` + +This will automatically document your potential domain exception as a Response in the OpenAPI definition, and show it in the UI : + +![Swagger UI](images/open-api-documented-error.png) diff --git a/core/extensions.md b/core/extensions.md index 9d06acc282d..258d37cbac7 100644 --- a/core/extensions.md +++ b/core/extensions.md @@ -77,12 +77,12 @@ final readonly class CurrentUserExtension implements QueryCollectionExtensionInt { } - public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, Operation $operation = null, array $context = []): void + public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void { $this->addWhere($queryBuilder, $resourceClass); } - public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, Operation $operation = null, array $context = []): void + public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, ?Operation $operation = null, array $context = []): void { $this->addWhere($queryBuilder, $resourceClass); } diff --git a/core/filters.md b/core/filters.md index cdd61c7b6fb..62f1505c1b7 100644 --- a/core/filters.md +++ b/core/filters.md @@ -1,470 +1,761 @@ -# Filters +# Parameters and Filters -API Platform provides a generic system to apply filters and sort criteria on collections. -Useful filters for Doctrine ORM, Eloquent ORM, MongoDB ODM and ElasticSearch are provided with the library. +API Platform provides a generic and powerful system to apply filters, sort criteria, and handle other request parameters. This system is primarily managed through **Parameter attributes** (`#[QueryParameter]` and `#[HeaderParameter]`), which allow for detailed and explicit configuration of how an API consumer can interact with a resource. -You can also create custom filters that fit your specific needs. -You can also add filtering support to your custom [state providers](state-providers.md) by implementing interfaces provided -by the library. +These parameters can be linked to **Filters**, which are classes that contain the logic for applying criteria to your persistence backend (like Doctrine ORM or MongoDB ODM). -By default, all filters are disabled. They must be enabled explicitly. - -When a filter is enabled, it automatically appears in the [OpenAPI](openapi.md) and [GraphQL](graphql.md) documentations. -It is also automatically documented as a `search` property for JSON-LD responses. +You can declare parameters on a resource class to apply them to all operations, or on a specific operation for more granular control. When parameters are enabled, they automatically appear in the Hydra, [OpenAPI](openapi.md) and [GraphQL](graphql.md) documentations.

Filtering and Searching screencast
Watch the Filtering & Searching screencast

-For the **specific filters documentation**, please refer to the following pages, depending on your needs: -- [Doctrine filters documentation](../core/doctrine-filters.md) -- [Elasticsearch filters documentation](../core/elasticsearch-filters.md) -- [Laravel filters documentation](../laravel/filters.md) - -## Parameters +For documentation on the specific filter implementations available for your persistence layer, please refer to the following pages: -You can declare parameters on a Resource or an Operation through the `parameters` property. +* [Doctrine Filters](../core/doctrine-filters.md) +* [Elasticsearch Filters](../core/elasticsearch-filters.md) -```php -namespace App\ApiResource; +## Declaring Parameters -use ApiPlatform\Metadata\GetCollection; -use ApiPlatform\Metadata\QueryParameter; - -// This parameter "page" works only on /books -#[GetCollection(uriTemplate: '/books', parameters: ['page' => new QueryParameter])] -// This parameter is available on every operation, key is mandatory -#[QueryParameter(key: 'q', property: 'freetextQuery')] -class Book {} -``` +The recommended way to define parameters is by using Parameter attributes directly on a resource class or on an operation. API Platform provides two main types of Parameter attributes based on their location (matching the OpenAPI `in` configuration): -Note that `property` is used to document the Hydra view. You can also specify an [OpenAPI Parameter](https://api-platform.com/docs/references/OpenApi/Model/Parameter/) if needed. -A Parameter can be linked to a filter, there are two types of filters: +* `ApiPlatform\Metadata\QueryParameter`: For URL query parameters (e.g., `?name=value`). +* `ApiPlatform\Metadata\HeaderParameter`: For HTTP headers (e.g., `Custom-Header: value`). -- metadata filters, most common are serializer filters (PropertyFilter and GroupFilter) that alter the normalization context -- query filters that alter the results of your database queries (Doctrine, Eloquent, Elasticsearch etc.) +You can declare a parameter on the resource class to make it available for all its operations: -### Alter the Operation via a parameter +```php +withNormalizationContext(['groups' => $request->query->all('groups')]); - } +#[ApiResource] +#[QueryParameter(key: 'author')] +class Book +{ + // ... } ``` -Then plug this provider on your parameter: +Or you can declare it on a specific operation for more targeted use cases: ```php -namespace App\ApiResource; + new HeaderParameter(provider: GroupsParameterProvider::class)])] -class Book { - public string $id; +#[ApiResource( + operations: [ + new GetCollection( + parameters: [ + 'name' => new QueryParameter(description: 'Filter our friends by name'), + 'Request-ID' => new HeaderParameter(description: 'A unique request identifier') // keys are case insensitive + ] + ) + ] +)] +class Friend +{ + // ... } ``` -If you use Symfony, but you don't have autoconfiguration enabled, declare the parameter as a tagged service: +### Filtering a Single Property -```yaml -services: - ApiPlatform\Tests\Fixtures\TestBundle\Parameter\CustomGroupParameterProvider: - tags: - - name: 'api_platform.parameter_provider' - key: 'ApiPlatform\Tests\Fixtures\TestBundle\Parameter\CustomGroupParameterProvider' -``` - -With Laravel the services are automatically tagged. +Most of the time, a parameter maps directly to a property on your resource. For example, a `?name=Frodo` query parameter would filter for resources where the `name` property is "Frodo". This behavior is often handled by built-in or custom filters that you link to the parameter. -### Declare a filter with Laravel +For Hydra, you can map a query parameter to `hydra:freetextQuery` to indicate a general-purpose search query. -Filters are classes implementing our `ApiPlatform\Laravel\Eloquent\Filter\FilterInterface`, use [our code](https://github.com/api-platform/core/tree/main/src/Laravel/Eloquent/Filter) as examples. Filters are automatically registered and tagged by our `ServiceProvider`. - -### Declare a filter with Symfony +```php + new QueryParameter(property: 'hydra:freetextQuery', required: true) + ] + ) +])] +class Issue {} +``` -```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'] +This will generate the following Hydra `IriTemplateMapping`: +```json +{ + "@context": "http://www.w3.org/ns/hydra/context.jsonld", + "@type": "IriTemplate", + "template": "http://api.example.com/issues{?q}", + "variableRepresentation": "BasicRepresentation", + "mapping": [ + { + "@type": "IriTemplateMapping", + "variable": "q", + "property": "hydra:freetextQuery", + "required": true + } + ] +} ``` -We can use this filter specifying we want a query parameter with the `:property` placeholder: +### Filtering Multiple Properties with `:property` -```php -namespace App\ApiResource; +Sometimes you need a generic filter that can operate on multiple properties. You can achieve this by using the `:property` placeholder in the parameter's `key`. +```php + new QueryParameter(filter: 'offer.order_filter'), - ] -) -class Offer { - public string $id; - public string $name; +#[ApiResource(operations: [ + new GetCollection( + parameters: [ + 'search[:property]' => new QueryParameter( + filter: 'api_platform.doctrine.orm.search_filter.instance' + ) + ] + ) +])] +class Book +{ + // ... } ``` -### Header parameters +This configuration creates a dynamic parameter. API clients can now filter on any of the properties configured in the `SearchFilter` (in this case, `title` and `description`) by using a URL like `/books?search[title]=Ring` or `/books?search[description]=journey`. -The `HeaderParameter` attribute allows to create a parameter that's using HTTP Headers instead of query parameters: +When using the `:property` placeholder, API Platform automatically populates the parameter's `extraProperties` with a `_properties` array containing all the available properties for the filter. Your filter can access this information: ```php -namespace App\ApiResource; - -use ApiPlatform\Metadata\HeaderParameter; -use App\Filter\MyApiKeyFilter; - -#[HeaderParameter(key: 'API_KEY', filter: MyApiKeyFilter::class)] -class Book { +public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void +{ + $parameter = $context['parameter'] ?? null; + $properties = $parameter?->getExtraProperties()['_properties'] ?? []; + + // $properties contains: ['title' => 'title', 'description' => 'description'] + // This allows your filter to know which properties are available for filtering } ``` -When you declare a parameter on top of a class, you need to specify it's key. +Note that we're using `api_platform.doctrine.orm.search_filter.instance` (exists also for ODM). Indeed this is a special instance of the search filter where `properties` can be changed during runtime. This is considered as "legacy filter" below, in API Platform 4.0 we'll recommend to create a custom filter or to use the `PartialSearchFilter`. -### The :property placeholder +### Restricting Properties with `:property` Placeholders -When used on a Parameter, the `:property` placeholder allows to map automatically a parameter to the readable properties of your resource. +There are two different approaches to property restriction depending on your filter design: -```php -namespace App\ApiResource; +#### 1. Legacy Filters (SearchFilter, etc.) - Not Recommended -use App\Filter\SearchFilter; -use ApiPlatform\Metadata\QueryParameter; +> [!WARNING] +> Filters that extend `AbstractFilter` with pre-configured properties are considered legacy. They don't support property restriction via parameters and may be deprecated in future versions. Consider using per-parameter filters instead for better flexibility and performance. -#[QueryParameter(key: ':property', filter: SearchFilter::class)] -class Book { - public string $id; - public string $title; - public Author $author; -} +For existing filters that extend `AbstractFilter` and have pre-configured properties, the parameter's `properties` does **not** restrict the filter's behavior. These filters use their own internal property configuration: + +```php + new QueryParameter( + properties: ['title', 'author'], // Only affects _properties, doesn't restrict filter + filter: new SearchFilter(properties: ['title' => 'partial', 'description' => 'partial']) +) + +// To restrict legacy filters, configure them with only the desired properties: +'search[:property]' => new QueryParameter( + filter: new SearchFilter(properties: ['title' => 'partial', 'author' => 'exact']) +) ``` -This will declare a query parameter for each property (ID, title and author) calling the SearchFilter. +#### 2. Per-Parameter Filters (Recommended) + +> [!NOTE] +> Per-parameter filters are the modern approach. They provide better performance (only process requested properties), cleaner code, and full support for parameter-based property restriction. -This is especially useful for sort filters where you'd like to use `?sort[name]=asc`: +Modern filters that work on a per-parameter basis can be effectively restricted using the parameter's `properties`: ```php -namespace App\ApiResource; +getValue(); + + // Get the property for this specific parameter + $property = $parameter->getProperty(); + $alias = $queryBuilder->getRootAliases()[0]; + $field = $alias.'.'.$property; + + $parameterName = $queryNameGenerator->generateParameterName($property); + + $queryBuilder + ->andWhere($queryBuilder->expr()->like('LOWER('.$field.')', ':'.$parameterName)) + ->setParameter($parameterName, '%'.strtolower($value).'%'); + } +} +``` -#[QueryParameter(key: 'sort[:property]', filter: OrderFilter::class)] +```php + new QueryParameter( + properties: ['title', 'author'], // Only these properties get parameters created + filter: new PartialSearchFilter() + ) + ] + ) +])] class Book { - public string $id; - public string $title; - public Author $author; + // ... } ``` -### Documentation +**How it works:** +1. API Platform creates individual parameters: `search[title]` and `search[author]` only +2. URLs like `/books?search[description]=foo` are ignored (no parameter exists) +3. Each parameter calls the filter with its specific property via `$parameter->getProperty()` +4. The filter processes only that one property -A parameter is quite close to its documentation, and you can specify the JSON Schema and/or the OpenAPI documentation: +This approach is recommended for new filters as it's more flexible and allows true property restriction via the parameter configuration. -```php -namespace App\ApiResource; +Note that invalid values are usually ignored by our filters, use [validation](#parameter-validation) to trigger errors for wrong parameter values. -use ApiPlatform\Metadata\QueryParameter; -use ApiPlatform\OpenApi\Model\Parameter; +## OpenAPI and JSON Schema -#[QueryParameter( - key: 'q', - required: true, - schema: ['type' => 'string'], - openApi: new Parameter(in: 'query', name: 'q', allowEmptyValue: true) -)] -class Book { - public string $id; - public string $title; - public Author $author; -} -``` +You have full control over how your parameters are documented in OpenAPI. -### Filter aliasing +### Customizing the OpenAPI Parameter -Filter aliasing is done by declaring a parameter key with a different property: +You can pass a fully configured `ApiPlatform\OpenApi\Model\Parameter` object to the `openApi` property of your parameter attribute. This gives you total control over the generated documentation. ```php -#[GetCollection( - parameters: [ - 'fooAlias' => new QueryParameter(filter: 'app_search_filter_via_parameter', property: 'foo'), - ] -)] -class Book { - public string $id; - public string $foo; -} + new QueryParameter( + schema: ['enum' => ['a', 'b'], 'uniqueItems' => true], + castToArray: true, + openApi: new OpenApiParameter(name: 'enum', in: 'query', style: 'deepObject') + ) + ] + ) +])] +class User {} ``` -If you need you can use the `filterContext` to transfer information between a parameter and its filter. +### Using JSON Schema and Type Casting -### Parameter validation +The `schema` property allows you to define validation rules using JSON Schema keywords. This is useful for simple validation like ranges, patterns, or enumerations. -If you use Laravel refers to the [Laravel Validation documentation](../laravel/validation.md). - -Parameter validation is automatic based on the configuration for example: +When you define a `schema`, API Platform can often infer the native PHP type of the parameter. For instance, `['type' => 'boolean']` implies a boolean. If you want to ensure the incoming string value (e.g., "true", "0") is cast to its actual native type before validation and filtering, set `castToNativeType` to `true`. ```php new QueryParameter(schema: ['enum' => ['a', 'b'], 'uniqueItems' => true]), - 'num' => new QueryParameter(schema: ['minimum' => 1, 'maximum' => 3]), - 'exclusiveNum' => new QueryParameter(schema: ['exclusiveMinimum' => 1, 'exclusiveMaximum' => 3]), - 'blank' => new QueryParameter(openApi: new OpenApiParameter(name: 'blank', in: 'query', allowEmptyValue: false)), - 'length' => new QueryParameter(schema: ['maxLength' => 1, 'minLength' => 3]), - 'array' => new QueryParameter(schema: ['minItems' => 2, 'maxItems' => 3]), - 'multipleOf' => new QueryParameter(schema: ['multipleOf' => 2]), - 'pattern' => new QueryParameter(schema: ['pattern' => '/\d/']), - 'required' => new QueryParameter(required: true), - ], -)] -class ValidateParameter {} +#[ApiResource(operations: [ + new GetCollection( + uriTemplate: '/settings', + parameters: [ + 'isEnabled' => new QueryParameter( + schema: ['type' => 'boolean'], + castToNativeType: true + ) + ] + ) +])] +class Setting {} ``` -You can also use your own constraint by setting the `constraints` option on a Parameter. In that case we won't set up the automatic validation for you, and it'll replace our defaults. - -### Parameter security +If you need a custom validation function use the `castFn` property of the `Parameter` class. -If you use Laravel refers to the [Laravel Security documentation](../laravel/security.md). +## Parameter Validation -Parameters may have security checks: +You can enforce validation rules on your parameters using the `required` property or by attaching Symfony Validator constraints. ```php new QueryParameter(security: 'is_granted("ROLE_ADMIN")'), - 'auth' => new HeaderParameter(security: '"secretKey" == auth[0]'), - ], -)] -class SecurityParameter {} +use Symfony\Component\Validator\Constraints as Assert; + +#[ApiResource(operations: [ + new GetCollection( + uriTemplate: '/users/validate', + parameters: [ + 'country' => new QueryParameter( + description: 'Filter by country code.', + constraints: [new Assert\Country()] + ), + 'X-Request-ID' => new HeaderParameter( + description: 'A unique request identifier.', + required: true, + constraints: [new Assert\Uuid()] + ) + ] + ) +])] +class User {} ``` -## Serializer Filters +Note that when `castToNativeType` is enabled, API Platform infers type validation from the JSON Schema. + +The `ApiPlatform\Validator\Util\ParameterValidationConstraints` trait can be used to automatically infer validation constraints from the JSON Schema and OpenAPI definitions of a parameter. + +Here is the list of validation constraints that are automatically inferred from the JSON Schema and OpenAPI definitions of a parameter. -### Group Filter +### From OpenAPI Definition -The group filter allows you to filter by serialization groups. +* **`allowEmptyValue`**: If set to `false`, a `Symfony\Component\Validator\Constraints\NotBlank` constraint is added. -Syntax: `?groups[]=` +### From JSON Schema (`schema` property) -You can add as many groups as you need. +* **`minimum`** / **`maximum`**: + * If both are set, a `Symfony\Component\Validator\Constraints\Range` constraint is added. + * If only `minimum` is set, a `Symfony\Component\Validator\Constraints\GreaterThanOrEqual` constraint is added. + * If only `maximum` is set, a `Symfony\Component\Validator\Constraints\LessThanOrEqual` constraint is added. +* **`exclusiveMinimum`** / **`exclusiveMaximum`**: + * If `exclusiveMinimum` is used, it becomes a `Symfony\Component\Validator\Constraints\GreaterThan` constraint. + * If `exclusiveMaximum` is used, it becomes a `Symfony\Component\Validator\Constraints\LessThan` constraint. +* **`pattern`**: Becomes a `Symfony\Component\Validator\Constraints\Regex` constraint. +* **`minLength`** / **`maxLength`**: Becomes a `Symfony\Component\Validator\Constraints\Length` constraint. +* **`multipleOf`**: Becomes a `Symfony\Component\Validator\Constraints\DivisibleBy` constraint. +* **`enum`**: Becomes a `Symfony\Component\Validator\Constraints\Choice` constraint with the specified values. +* **`minItems`** / **`maxItems`**: Becomes a `Symfony\Component\Validator\Constraints\Count` constraint (for arrays). +* **`uniqueItems`**: If `true`, becomes a `Symfony\Component\Validator\Constraints\Unique` constraint (for arrays). +* **`type`**: + * If set to `'array'`, a `Symfony\Component\Validator\Constraints\Type('array')` constraint is added. + * If `castToNativeType` is also `true`, the schema `type` will add a `Symfony\Component\Validator\Constraints\Type` constraint for `'boolean'`, `'integer'`, and `'number'` (as `float`). -Enable the filter: +### From the Parameter's `required` Property + +* **`required`**: If set to `true`, a `Symfony\Component\Validator\Constraints\NotNull` constraint is added. + +### Strict Parameter Validation + +By default, API Platform allows clients to send extra query parameters that are not defined in the operation's `parameters`. To enforce a stricter contract, you can set `strictQueryParameterValidation` to `true` on an operation. If an unsupported parameter is sent, API Platform will return a 400 Bad Request error. ```php 'groups', 'overrideDefaultGroups' => false, 'whitelist' => ['allowed_group']])] -class Book -{ - // ... -} +#[ApiResource(operations: [ + new Get( + uriTemplate: 'strict_query_parameters', + strictQueryParameterValidation: true, + parameters: [ + 'foo' => new QueryParameter(), + ] + ) +])] +class StrictParameters {} ``` -Three arguments are available to configure the filter: +With this configuration, a request to `/strict_query_parameters?bar=test` will fail with a 400 error because `bar` is not a supported parameter. -- `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) +## Parameter Providers -Given that the collection endpoint is `/books`, you can filter by serialization groups with the following query: `/books?groups[]=read&groups[]=write`. +Parameter Providers are powerful services that can inspect, transform, or provide values for parameters. They can even modify the current `Operation` metadata on the fly. A provider is a class that implements `ApiPlatform\State\ParameterProviderInterface`. -### Property filter +### `IriConverterParameterProvider` -**Note:** We strongly recommend using [Vulcain](https://vulcain.rocks) instead of this filter. -Vulcain is faster, allows a better hit rate, and is supported out of the box in the API Platform distribution. +This built-in provider takes an IRI string (e.g., `/users/1`) and converts it into the corresponding Doctrine entity object. It supports both single IRIs and arrays of IRIs. -The property filter adds the possibility to select the properties to serialize (sparse fieldsets). +```php + new QueryParameter(provider: IriConverterParameterProvider::class), + 'related' => new QueryParameter( + provider: IriConverterParameterProvider::class, + extraProperties: ['fetch_data' => true] // Forces fetching the entity data + ), + ], + provider: [self::class, 'provideDummyFromParameter'], + ) +])] +class WithParameter +{ + public static function provideDummyFromParameter(Operation $operation, array $uriVariables = [], array $context = []): object|array + { + // The value has been transformed from an IRI to an entity by the provider. + $dummy = $operation->getParameters()->get('dummy')->getValue(); + + // If multiple IRIs were provided as an array, this will be an array of entities + $related = $operation->getParameters()->get('related')->getValue(); + + return $dummy; + } +} +``` -Syntax: `?properties[]=&properties[][]=` +#### Configuration Options -You can add as many properties as you need. +The `IriConverterParameterProvider` supports the following options in `extraProperties`: -Enable the filter: +- **`fetch_data`**: Boolean (default: `false`) - When `true`, forces the IRI converter to fetch the actual entity data instead of just creating a reference. + +### `ReadLinkParameterProvider` + +This provider fetches a linked resource from a given identifier. This is useful when you need to load a related entity to use later, for example in your own state provider. +When you have an API resource with a custom `uriTemplate` that includes parameters, the `ReadLinkParameterProvider` can automatically resolve the linked resource using the operation's URI template. This is particularly useful for nested resources or when you need to load a parent resource based on URI variables. ```php 'properties', 'overrideDefaultProperties' => false, 'whitelist' => ['allowed_property']])] -class Book +#[Get( + uriTemplate: 'with_parameters/{id}{._format}', + uriVariables: [ + 'id' => new Link(schema: ['type' => 'string', 'format' => 'uuid'], property: 'id'), + ], + parameters: [ + 'dummy' => new QueryParameter( + provider: ReadLinkParameterProvider::class, + extraProperties: [ + 'resource_class' => Dummy::class, + 'uri_template' => '/dummies/{id}' // Optional: specify the template for the linked resource + ] + ) + ], + provider: [self::class, 'provideDummyFromParameter'], +)] +class WithParameter { - // ... + public static function provideDummyFromParameter(Operation $operation, array $uriVariables = [], array $context = []): object|array + { + // The dummy parameter has been resolved to the actual Dummy entity + // based on the parameter value and the specified uri_template + return $operation->getParameters()->get('dummy')->getValue(); + } } ``` -Three arguments are available to configure the filter: +The provider will: +- Take the parameter value (e.g., a UUID or identifier) +- Use the `resource_class` to determine which resource to load +- Optionally use the `uri_template` from `extraProperties` to construct the proper operation for loading the resource +- Return the loaded entity, making it available in your state provider -- `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) +#### ReadLinkParameterProvider Configuration Options -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`. +You can control the behavior of `ReadLinkParameterProvider` with these `extraProperties`: -## Creating Custom Filters +- **`resource_class`**: The class of the resource to load +- **`uri_template`**: Optional URI template for the linked resource operation +- **`uri_variable`**: Name of the URI variable to use when building URI variables array +- **`throw_not_found`**: Boolean (default: `true`) - Whether to throw `NotFoundHttpException` when resource is not found -Custom filters can be written by implementing the `ApiPlatform\Api\FilterInterface` interface. +```php +'dummy' => new QueryParameter( + provider: ReadLinkParameterProvider::class, + extraProperties: [ + 'resource_class' => Dummy::class, + 'throw_not_found' => false, // Won't throw NotFoundHttpException if resource is missing + 'uri_variable' => 'customId' // Use 'customId' as the URI variable name + ] +) +``` -API Platform provides a convenient way to create Doctrine ORM and MongoDB ODM filters. If you use [custom state providers](state-providers.md), -you can still create filters by implementing the previously mentioned interface, but - as API Platform isn't aware of your -persistence system's internals - you have to create the filtering logic by yourself. +### Array Support -If you need more information about creating custom filters, refer to the following documentation: +Both `IriConverterParameterProvider` and `ReadLinkParameterProvider` support processing arrays of values. When you pass an array of identifiers or IRIs, they will return an array of resolved entities: -- [Creating Custom Doctrine ORM filters](../core/doctrine-filters.md#creating-custom-doctrine-orm-filters) -- [Creating Custom Doctrine Mongo ODM filters](../core/doctrine-filters.md#creating-custom-doctrine-mongodb-odm-filters) -- [Creating Custom Elasticsearch Filters](../core/elasticsearch-filters.md#creating-custom-elasticsearch-filters) +```php +// For IRI converter: ?related[]=/dummies/1&related[]=/dummies/2 +// For ReadLink provider: ?dummies[]=uuid1&dummies[]=uuid2 +'items' => new QueryParameter( + provider: ReadLinkParameterProvider::class, + extraProperties: ['resource_class' => Dummy::class] +) +``` -## ApiFilter Attribute +### Creating a Custom Parameter Provider -The attribute can be used on a `property` or on a `class`. +You can create your own providers to implement any custom logic. A provider must implement `ParameterProviderInterface`. The `provide` method can modify the parameter's value or even return a modified `Operation` to alter the request handling flow. -If the attribute is given over a property, the filter will be configured on the property. For example, let's add a search filter on `name` and on the `prop` property of the `colors` relation: +For instance, a provider could add serialization groups to the normalization context based on a query parameter: ```php 'ipartial'])] - public Collection $colors; - - public function __construct() + public function provide(Parameter $parameter, array $parameters = [], array $context = []): ?Operation { - $this->colors = new ArrayCollection(); + $operation = $context['operation'] ?? null; + if (!$operation) { + return null; + } + + $value = $parameter->getValue(); + if ('extended' === $value) { + $context = $operation->getNormalizationContext(); + $context[AbstractNormalizer::GROUPS][] = 'extended_read'; + return $operation->withNormalizationContext($context); + } + + return $operation; } - - // ... } ``` -On the first property, `name`, it's straightforward. The first attribute argument is the filter class, the second specifies options, here, the strategy: - -```php -#[ApiFilter(SearchFilter::class, strategy: 'partial')] -``` +### Changing how to parse Query / Header Parameters -In the second attribute, we specify `properties` to which the filter should apply. It's necessary here because we don't want to filter `colors` but the `prop` property of the `colors` association. -Note that for each given property we specify the strategy: +We use our own algorithm to parse a request's query, if you want to do the parsing of `QUERY_STRING` yourself, set `_api_query_parameters` in the Request attributes (`$request->attributes->set('_api_query_parameters', [])`) yourself. +By default we use Symfony's `$request->headers->all()`, you can also set `_api_header_parameters` if you want to parse them yourself. -```php -#[ApiFilter(SearchFilter::class, properties: ['colors.prop' => 'ipartial'])] -``` +## Creating Custom Filters -The `ApiFilter` attribute can be set on the class as well. If you don't specify any properties, it'll act on every property of the class. +For data-provider-specific filtering (e.g., Doctrine ORM), the recommended way to create a filter is to implement the corresponding `FilterInterface`. -For example, let's define three data filters (`DateFilter`, `SearchFilter` and `BooleanFilter`) and two serialization filters (`PropertyFilter` and `GroupFilter`) on our `DummyCar` class: +For Doctrine ORM, your filter should implement `ApiPlatform\Doctrine\Orm\Filter\FilterInterface`: ```php 'ipartial', 'name' => 'partial'])] -#[ApiFilter(PropertyFilter::class, arguments: ['parameterName' => 'foobar'])] -#[ApiFilter(GroupFilter::class, arguments: ['parameterName' => 'foobargroups'])] -class DummyCar +final class RegexpFilter implements FilterInterface { - // ... -} + public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void + { + $parameter = $context['parameter'] ?? null; + $value = $parameter?->getValue(); + + // The parameter may not be present. + // It's recommended to add validation (e.g., `required: true`) on the Parameter attribute + // if the filter logic depends on the value. + if ($value instanceof ParameterNotFound) { + return; + } + + $alias = $queryBuilder->getRootAliases()[0]; + $parameterName = $queryNameGenerator->generateParameterName('regexp_name'); + + // Access the parameter's property or use the parameter key as fallback + $property = $parameter->getProperty() ?? $parameter->getKey() ?? 'name'; + + // You can also access filter context if the parameter provides it + $filterContext = $parameter->getFilterContext() ?? null; + + $queryBuilder + ->andWhere(sprintf('REGEXP(%s.%s, :%s) = 1', $alias, $property, $parameterName)) + ->setParameter($parameterName, $value); + } + // For BC, this function is not useful anymore when documentation occurs on the Parameter + public function getDescription(): array { + return []; + } +} ``` -The `BooleanFilter` is applied to every `Boolean` property of the class. Indeed, in each core filter, we check the Doctrine type. It's written only by using the filter class: +You can then instantiate this filter directly in your `QueryParameter`: ```php -#[ApiFilter(BooleanFilter::class)] + new QueryParameter(filter: new RegexpFilter()) + ] + ) +])] +class User {} ``` -The `DateFilter` given here will be applied to every `Date` property of the `DummyCar` class with the `DateFilterInterface::EXCLUDE_NULL` strategy: +### Advanced Use Case: Composing Filters + +You can create complex filters by composing existing ones. This is useful when you want to apply multiple filtering logics based on a single parameter. ```php -#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)] +getValue(); + if ($value instanceof ParameterNotFound) { + return; + } + + // Create a new context for the sub-filters, passing the value. + $subContext = ['filters' => ['searchOnTextAndDate' => $value]] + $context; + + $this->searchFilter->apply($queryBuilder, $queryNameGenerator, $resourceClass, $operation, $subContext); + $this->dateFilter->apply($queryBuilder, $queryNameGenerator, $resourceClass, $operation, $subContext); + } +} ``` -The `SearchFilter` here adds properties. The result is the exact same as the example with attributes on properties: +To use this composite filter, register it as a service and reference it by its ID: +```yaml +# config/services.yaml +services: + 'app.filter_date_and_search': + class: App\Filter\SearchTextAndDateFilter + autowire: true ```php -#[ApiFilter(SearchFilter::class, properties: ['colors.prop' => 'ipartial', 'name' => 'partial'])] + new QueryParameter(filter: 'app.filter_date_and_search') + ] + ) +])] +class LogEntry {} ``` -Note that you can specify the `properties` argument on every filter. +## Parameter Attribute Reference + +| Property | Description | +|---|---| +| `key` | The name of the parameter (e.g., `name`, `order`). | +| `filter` | The filter service or instance that processes the parameter's value. | +| `provider` | A service that transforms the parameter's value before it's used. | +| `description` | A description for the API documentation. | +| `property` | The resource property this parameter is mapped to. | +| `required` | Whether the parameter is required. | +| `constraints` | Symfony Validator constraints to apply to the value. | +| `schema` | A JSON Schema for validation and documentation. | +| `castToArray` | Casts the parameter value to an array. Useful for query parameters like `foo[]=1&foo[]=2`. Defaults to `true`. | +| `castToNativeType` | Casts the parameter value to its native PHP type based on the `schema`. | +| `openApi` | Customize OpenAPI documentation or hide the parameter (`false`). | +| `hydra` | Hide the parameter from Hydra documentation (`false`). | +| `security` | A [Symfony expression](https://symfony.com/doc/current/security/expressions.html) to control access to the parameter. | -The next filters are not related to how the data is fetched but rather to how the serialization is done on those. We can give an `arguments` option ([see here for the available arguments](#serializer-filters)): +## Parameter Security + +You can secure individual parameters using Symfony expression language. When a security expression evaluates to `false`, the parameter will be ignored and treated as if it wasn't provided. ```php -#[ApiFilter(PropertyFilter::class, arguments: ['parameterName' => 'foobar'])] -#[ApiFilter(GroupFilter::class, arguments: ['parameterName' => 'foobargroups'])] + new QueryParameter( + security: 'is_granted("ROLE_ADMIN")' + ), + 'auth' => new HeaderParameter( + security: '"secured" == auth', + description: 'Only accessible when auth header equals "secured"' + ), + 'secret' => new QueryParameter( + security: '"secured" == secret', + description: 'Only accessible when secret parameter equals "secured"' + ) + ] + ) +])] +class SecureResource +{ + // ... +} ``` + +In the security expressions, you have access to: +- Parameter values by their key name (e.g., `auth`, `secret`) +- Standard security functions like `is_granted()` +- The current user via `user` +- Request object via `request` + diff --git a/core/form-data.md b/core/form-data.md index bbbd9f640ff..df616effe5f 100644 --- a/core/form-data.md +++ b/core/form-data.md @@ -31,14 +31,14 @@ namespace App\State; use ApiPlatform\State\ProcessorInterface; use Symfony\Component\HttpFoundation\Request; use ApiPlatform\Metadata\Operation; -use ApiPlatform\Serializer\SerializerContextBuilderInterface; +use ApiPlatform\State\SerializerContextBuilderInterface; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; final class FormRequestProcessorDecorator implements ProcessorInterface { public function __construct( private readonly ProcessorInterface $decorated, - private readonlyDenormalizerInterface $denormalizer, + private readonly DenormalizerInterface $denormalizer, private readonly SerializerContextBuilderInterface $serializerContextBuilder ) {} @@ -101,9 +101,9 @@ namespace App\Providers; use Illuminate\Support\ServiceProvider; use App\State\FormRequestProcessorDecorator; -use ApiPlatform\Core\State\ProcessorInterface; +use ApiPlatform\State\ProcessorInterface; +use ApiPlatform\State\SerializerContextBuilderInterface; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; -use ApiPlatform\Core\Serializer\SerializerContextBuilderInterface; class AppServiceProvider extends ServiceProvider { diff --git a/core/graphql.md b/core/graphql.md index b815599a0a6..ef36ea901d9 100644 --- a/core/graphql.md +++ b/core/graphql.md @@ -16,11 +16,11 @@ To enable GraphQL and its IDE (GraphiQL and GraphQL Playground) in your API, sim composer require api-platform/graphql ``` -You can now use GraphQL at the endpoint: `https://localhost:8443/graphql`. +You can now use GraphQL at the endpoint: `https://localhost/graphql`. > [!NOTE] > If you used [the Symfony Variant thanks to Symfony Flex](../symfony/index.md#installing-the-framework) or the Laravel -> variant, URLs will be prefixed with `/api` by default. For example, the GraphQL endpoint will be: `https://localhost:8443/api/graphql`. +> variant, the default GraphQL endpoint will be available at a relative URL like `/graphql`. For example: `https://localhost/graphql`. ## Changing Location of the GraphQL Endpoint @@ -33,12 +33,12 @@ Using the Symfony variant we can do this modification by adding the following co ```yaml # api/config/routes.yaml api_graphql_entrypoint: - path: /api/graphql + path: /graphql controller: api_platform.graphql.action.entrypoint # ... ``` -Change `/api/graphql` to the URI you wish the GraphQL endpoint to be accessible on. +Change `/graphql` to the URI you wish the GraphQL endpoint to be accessible on. ### Laravel Routes @@ -49,11 +49,11 @@ Using the Laravel variant we can do this modification by adding the following co use Illuminate\Support\Facades\Route; use ApiPlatform\GraphQL\Action\EntrypointAction; -Route::post('/api/graphql', EntrypointAction::class) +Route::post('/graphql', EntrypointAction::class) ->name('api_graphql_entrypoint'); ``` -Change `/api/graphql` to the URI you wish the GraphQL endpoint to be accessible on. +Change `/graphql` to the URI you wish the GraphQL endpoint to be accessible on. ## GraphiQL @@ -2750,8 +2750,8 @@ The decorator could be like this: ['execute_options' => ['allowDiskUse' => true]]])] +#[GetCollection(extraProperties: ['doctrine_mongodb' => ['execute_options' => ['allowDiskUse' => true]]])] class Offer { // ... @@ -222,7 +222,7 @@ use ApiPlatform\Metadata\ApiResource; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; #[ODM\Document] -#[ApiResource(extraProperties: ['doctrineMongodb' => ['execute_options' => ['allowDiskUse' => true]]])] +#[ApiResource(extraProperties: ['doctrine_mongodb' => ['execute_options' => ['allowDiskUse' => true]]])] class Offer { // ... diff --git a/core/openapi.md b/core/openapi.md index 2159ef2375d..80685b63675 100644 --- a/core/openapi.md +++ b/core/openapi.md @@ -497,7 +497,27 @@ use App\Controller\RandomRabbit; ] ] ]) - ) + ), + responses: [ + 201 => new Model\Response( + content: new \ArrayObject([ + 'application/json' => [ + 'schema' => [ + 'type' => 'object', + 'properties' => [ + 'status' => ['type' => 'string'], + 'description' => ['type' => 'string'] + ] + ], + 'example' => [ + 'status' => 'success', + 'description' => 'Rabbit picture created.', + ] + ] + ] + ) + ) + ], ) )] class Rabbit @@ -755,21 +775,21 @@ You may want to copy the [one shipped with API Platform](https://github.com/api- ### Overriding the UI Template using Laravel -As described [in the Laravel documentation](https://laravel.com/docs/blade#extending-a-layout), it's possible to override the Blade template that loads Swagger UI and renders the documentation: +As described [in the Laravel documentation](https://laravel.com/docs/packages#overriding-package-views), it's possible to override the Blade template that loads Swagger UI and renders the documentation: ```html -{# resources/views/swagger-ui.blade.php #} +{{-- resources/views/vendor/api-platform/swagger-ui.blade.php --}} @if(isset($title)) - {{ $title }} + {{ $title }} @endif My custom template - {# ... #} + {{-- ... --}} ``` diff --git a/core/operations.md b/core/operations.md index 8e637c2d473..fbc0fecbaa8 100644 --- a/core/operations.md +++ b/core/operations.md @@ -479,167 +479,3 @@ resources: API Platform will find the operation matching this `itemUriTemplate` and use it to generate the IRI. If this option is not set, the first `Get` operation is used to generate the IRI. - -## Expose a Model Without Any Routes - -Sometimes, you may want to expose a model, but want it to be used through subrequests only, and never through item or collection operations. -Because the OpenAPI standard requires at least one route to be exposed to make your models consumable, let's see how you can manage this kind -of issue. - -Let's say you have the following entities in your project: - -```php -decorated = $decorated; - } - - public function __invoke(array $context = []): OpenApi - { - $openApi = $this->decorated->__invoke($context); - - $paths = $openApi->getPaths()->getPaths(); - - $filteredPaths = new Model\Paths(); - foreach ($paths as $path => $pathItem) { - // If a prefix is configured on API Platform's routes, it must appear here. - if ($path === '/weathers/{id}') { - continue; - } - $filteredPaths->addPath($path, $pathItem); - } - - return $openApi->withPaths($filteredPaths); - } -} -``` - -That's it: your route is gone! diff --git a/core/performance.md b/core/performance.md index 1d464561f80..26e9ccaff71 100644 --- a/core/performance.md +++ b/core/performance.md @@ -77,9 +77,9 @@ Set up HTTP cache invalidation in your API Platform project using the Symfony or api_platform: http_cache: invalidation: - # We assume that your API can reach your caddy instance by the hostname http://caddy. + # We assume that your API can reach your caddy/frankenphp instance by the hostname http://php. # The endpoint /souin-api/souin is the default path to the invalidation API. - urls: ['http://caddy/souin-api/souin'] + urls: ['http://php:2019/souin-api/souin'] purger: api_platform.http_cache.purger.souin ``` diff --git a/core/serialization.md b/core/serialization.md index 4c0fd4a37fa..1cb8c480813 100644 --- a/core/serialization.md +++ b/core/serialization.md @@ -37,7 +37,7 @@ JSON-LD, or JavaScript Object Notation for Linked Data, is a method of encoding

Serialization Groups screencast
Watch the Serialization Groups screencast

-API Platform allows you to specify the `$context` variable used by the Symfony Serializer. This variable is an associative array that has a handy `groups` key allowing you to choose which attributes of the resource are exposed during the normalization (read) and denormalization (write) processes. +API Platform allows you to specify the `$context` variable used by the Symfony Serializer. This variable is an associative array that has a handy `groups` key allowing you to choose which properties of the resource are exposed during the normalization (read) and denormalization (write) processes. It relies on the [serialization (and deserialization) groups](https://symfony.com/doc/current/components/serializer.html#attributes-groups) feature of the Symfony Serializer component. @@ -648,7 +648,7 @@ Instead of sending an IRI to set a relation, you may want to send a plain identi namespace App\Serializer; -use ApiPlatform\Api\IriConverterInterface; +use ApiPlatform\Metadata\IriConverterInterface; use App\ApiResource\Dummy; use App\ApiResource\RelatedDummy; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; @@ -932,10 +932,10 @@ services: // api/src/Serializer/BookContextBuilder.php namespace App\Serializer; -use ApiPlatform\Serializer\SerializerContextBuilderInterface; +use App\ApiResource\Book; use Symfony\Component\HttpFoundation\Request; +use ApiPlatform\State\SerializerContextBuilderInterface; use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; -use App\ApiResource\Book; final class BookContextBuilder implements SerializerContextBuilderInterface { diff --git a/core/state-processors.md b/core/state-processors.md index abca3da7609..28137a95fd1 100644 --- a/core/state-processors.md +++ b/core/state-processors.md @@ -264,6 +264,41 @@ final class UserProcessor implements ProcessorInterface } ``` +Next, we bind the [PersistProcessor](https://github.com/api-platform/core/blob/main/src/Laravel/Eloquent/State/PersistProcessor.php) and [RemoveProcessor](https://github.com/api-platform/core/blob/main/src/Laravel/Eloquent/State/RemoveProcessor.php) in our Service Provider: + +```php +app->tag(UserProcessor::class, ProcessorInterface::class); + + $this->app->singleton(UserProcessor::class, function (Application $app) { + return new UserProcessor( + $app->make(PersistProcessor::class), + $app->make(RemoveProcessor::class), + ); + }); + } + + // ... +} +``` + Finally, configure that you want to use this processor on the User resource: ```php diff --git a/core/state-providers.md b/core/state-providers.md index 9912913c013..49f6b603d0b 100644 --- a/core/state-providers.md +++ b/core/state-providers.md @@ -345,7 +345,37 @@ final class BookRepresentationProvider implements ProviderInterface } ``` -And configure that you want to use this provider on the Book resource: +And we bind the [ItemProvider](https://github.com/api-platform/core/blob/main/src/Laravel/Eloquent/State/ItemProvider.php) in our Service Provider + +```php +app->singleton(BookRepresentationProvider::class, function (Application $app) { + return new BookRepresentationProvider( + $app->make(ItemProvider::class), + ); + }); + } + + //... +} +``` + +Finally, configure that you want to use this provider on the Book resource: ```php [ - 'url_generation_strategy' => ApiPlatform\Api\UrlGeneratorInterface::ABS_URL + 'url_generation_strategy' => ApiPlatform\Metadata\UrlGeneratorInterface::ABS_URL ], ]; ``` @@ -60,7 +60,7 @@ It can also be configured only for a specific resource: namespace App\ApiResource; use ApiPlatform\Metadata\ApiResource; -use ApiPlatform\Api\UrlGeneratorInterface; +use ApiPlatform\Metadata\UrlGeneratorInterface; #[ApiResource(urlGenerationStrategy: UrlGeneratorInterface::ABS_URL)] class Book diff --git a/deployment/kubernetes.md b/deployment/kubernetes.md index 10545ce9726..2c5cffcc730 100644 --- a/deployment/kubernetes.md +++ b/deployment/kubernetes.md @@ -186,7 +186,7 @@ helm upgrade api-platform ./helm/api-platform --namespace=default \ ## GitHub Actions Example for deployment -You can find a [complete deploy command for GKE](https://github.com/api-platform/demo/blob/main/.github/workflows/cd.yml) on the [demo project](https://github.com/api-platform/demo/): +You can find a [complete deploy command for GKE](https://github.com/api-platform/demo/blob/4.1/.github/workflows/cd.yml) on the [demo project](https://github.com/api-platform/demo/): ## Symfony Messenger diff --git a/extra/releases.md b/extra/releases.md index 50655968081..37f68ddaadd 100644 --- a/extra/releases.md +++ b/extra/releases.md @@ -10,8 +10,8 @@ For example: - version 3.2 has been released on 12 October 2023; - version 3.3 has been released on 9 April 2024 (we were a little late, it should have been published in March); - versions 3.4 has been released on 18 September 2024; -- versions 4.0 has been released on 27 September 2024; -- versions 4.1 has been released on 28 February 2025; +- versions 4.0 has been released on 27 September 2024; +- versions 4.1 has been released on 28 February 2025; ## Maintenance diff --git a/laravel/filters.md b/laravel/filters.md index 78582948d8b..a066e1d8e00 100644 --- a/laravel/filters.md +++ b/laravel/filters.md @@ -44,6 +44,24 @@ final class EqualsFilter implements FilterInterface You can create your own filters by implementing the `ApiPlatform\Laravel\Eloquent\Filter\FilterInterface`. API Platform provides several eloquent filters for a RAD approach. +### Parameter for Specific Operations + +To defines a parameter for only a `GetCollection` operation, you can do the following: + +```php +// app/Models/Book.php + +use ApiPlatform\Laravel\Eloquent\Filter\EqualsFilter; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\QueryParameter; + +#[ApiResource] +#[GetCollection(parameters: ['name' => new QueryParameter(key: 'name', filter: EqualsFilter::class)])] +class Book extends Model +{ +} +``` + ### Parameter Validation You can add [validation rules](https://laravel.com/docs/validation) to parameters within the `constraints` attribute: @@ -86,6 +104,26 @@ class Book extends Model The documentation will output a query parameter per property that applies the `PartialSearchFilter` and also gives the ability to sort by name and ID using: `/books?name=search&order[id]=asc&order[name]=desc`. +### Filtering on Specific Properties Only + +To enable partial search filtering and sorting on specific properties like `name` and `description`: + +```php +// app/Models/Book.php + +use ApiPlatform\Laravel\Eloquent\Filter\PartialSearchFilter; +use ApiPlatform\Laravel\Eloquent\Filter\OrderFilter; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\QueryParameter; + +#[ApiResource] +#[QueryParameter(key: 'sort[:property]', filter: OrderFilter::class, properties: ['name', 'description'])] +#[QueryParameter(key: ':property', filter: PartialSearchFilter::class, properties: ['name', 'description'])] +class Book extends Model +{ +} +``` + ## Filters ### Text @@ -101,6 +139,38 @@ As shown above the following search filters are available: The `DateFilter` allows to filter dates with an operator (`eq`, `lt`, `gt`, `lte`, `gte`): +- `eq` - equals (exact match) +- `gt` - greater than (strictly after) +- `gte` - greater than or equal (after or on) +- `lt` - less than (strictly before) +- `lte` - less than or equal (before or on) +- `after` - alias for `gte` +- `before` - alias for `lte` +- `strictly_after` - alias for `gt` +- `strictly_before` - alias for `lt` + +Usage Examples + +With the `DateFilter` applied, you can now filter dates between 2024-01-01 and 2024-01-31 using these API calls: + +**Option 1: Using gte and lte operators** + +```http +GET /api/your_entities?createdAt[gte]=2024-01-01&createdAt[lte]=2024-01-31 +``` + +**Option 2: Using after and before operators** + +```http +GET /api/your_entities?createdAt[after]=2024-01-01&createdAt[before]=2024-01-31 +``` + +**Option 3: Using strictly_after and strictly_before** + +```http +GET /api/your_entities?createdAt[strictly_after]=2023-12-31&createdAt[strictly_before]=2024-02-01 +``` + ```php // app/Models/Book.php diff --git a/laravel/index.md b/laravel/index.md index ea10b72648e..24cf4c0bd8c 100644 --- a/laravel/index.md +++ b/laravel/index.md @@ -417,6 +417,59 @@ Content-Type: application/merge-patch+json There's a powerful mechanism inside API Platform to create routes using relation (e.g.: `/api/authors/2/books`), read more about [subresources here](../core/subresources.md). +If you need to embed data, you can use [serialization groups](/core/serialization.md). Note that when you apply groups on Eloquent models they don't have properties therefore you need to specify groups using `#[ApiProperty(property: 'title')]`. Here's an example to embed the `author`: + +```php +namespace App\Models; + +use ApiPlatform\Metadata\ApiResource; +use Illuminate\Database\Eloquent\Model; + +#[ApiResource(normalizationContext: ['groups' => ['book:read']])] +#[ApiProperty(serialize: new Groups(['book:read']), property: 'title')] +#[ApiProperty(serialize: new Groups(['book:read']), property: 'description')] +#[ApiProperty(serialize: new Groups(['book:read']), property: 'author')] +class Book extends Model +{ + public function author(): BelongsTo + { + return $this->belongsTo(Author::class); + } +} +``` + +If you need a group on every properties use the `Group` attribute on the class (note that we use the same group as specified on the Book's normalizationContext): + +```php +namespace App\Models; + +use ApiPlatform\Metadata\ApiResource; +use Illuminate\Database\Eloquent\Model; + +#[ApiResource)] +#[Groups(['book:read'])] +class Author extends Model +{ +} +``` + +You'll see: + +```json +{ + "@context": "/api/contexts/Book", + "@id": "/api/books/1", + "@type": "Book", + "name": "Miss Nikki Senger V", + "isbn": "9784291624633", + "publicationDate": "1971-09-04", + "author": { + "@id": "/api/author/1", + "name": "Homer" + } +} +``` + ## Paginating Data A must have feature for APIs is pagination. Without pagination, collection responses quickly become huge and slow, @@ -784,7 +837,7 @@ You can create your own `Error` resource following [this guide](https://api-plat Read the detailed documentation about [Laravel data validation in API Platform](validation.md). -## Authorization +### Authorization To protect an operation and ensure that only authorized users can access it, start by creating a Laravel [policy](https://laravel.com/docs/authorization#creating-policies): @@ -796,10 +849,6 @@ Laravel will automatically detect your new policy and use it when manipulating a Read the detailed documentation about using [Laravel gates and policies with API Platform](security.md). - - ## Using the JavaScript Tools ### The Admin diff --git a/outline.yaml b/outline.yaml index 0e50c8e99ae..929ef2e1f6b 100644 --- a/outline.yaml +++ b/outline.yaml @@ -82,15 +82,15 @@ chapters: items: - index - getting-started + - schema + - customizing + - advanced-customization - handling-relations - - openapi - - schema.org - validation - real-time-mercure - authentication-support - file-upload - performance - - customizing - components - title: Create Client path: create-client diff --git a/symfony/file-upload.md b/symfony/file-upload.md index 5321853242b..100cbdd00a7 100644 --- a/symfony/file-upload.md +++ b/symfony/file-upload.md @@ -189,6 +189,85 @@ class MediaObjectNormalizer implements NormalizerInterface ``` +### Handling the Multipart Deserialization + +By default, Symfony is not able to decode `multipart/form-data`-encoded data. +We need to create our own decoder to do it: + +```php +requestStack->getCurrentRequest(); + + if (!$request) { + return null; + } + + return array_map(static function (string $element) { + // Multipart form values will be encoded in JSON. + return json_decode($element, true, flags: \JSON_THROW_ON_ERROR); + }, $request->request->all()) + $request->files->all(); + } + + public function supportsDecoding(string $format): bool + { + return self::FORMAT === $format; + } +} +``` + +If you're not using `autowiring` and `autoconfiguring`, don't forget to register the service and tag it as `serializer.encoder`. + +We also need to make sure the field containing the uploaded file is not denormalized: + +```php + true, + ]; + } +} +``` + +If you're not using `autowiring` and `autoconfiguring`, don't forget to register the service and tag it as `serializer.normalizer`. + ### Making a Request to the `/media_objects` Endpoint Your `/media_objects` endpoint is now ready to receive a `POST` request with a @@ -409,84 +488,3 @@ class Book // ... } ``` - -### Handling the Multipart Deserialization - -By default, Symfony is not able to decode `multipart/form-data`-encoded data. -We need to create our own decoder to do it: - -```php -requestStack->getCurrentRequest(); - - if (!$request) { - return null; - } - - return array_map(static function (string $element) { - // Multipart form values will be encoded in JSON. - return json_decode($element, true, flags: \JSON_THROW_ON_ERROR); - }, $request->request->all()) + $request->files->all(); - } - - public function supportsDecoding(string $format): bool - { - return self::FORMAT === $format; - } -} -``` - -If you're not using `autowiring` and `autoconfiguring`, don't forget to register the service and tag it as `serializer.encoder`. - -We also need to make sure the field containing the uploaded file is not denormalized: - -```php - true, - ]; - } -} -``` - -If you're not using `autowiring` and `autoconfiguring`, don't forget to register the service and tag it as `serializer.normalizer`. - -For resolving the file URL, you can use a custom normalizer, like shown in [the previous example](#resolving-the-file-url). diff --git a/symfony/index.md b/symfony/index.md index 04c69bfaad2..fbf3ca9f404 100644 --- a/symfony/index.md +++ b/symfony/index.md @@ -91,7 +91,7 @@ docker compose up --wait > 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` +> SERVER_NAME=localhost:80 HTTP_PORT=8080 TRUSTED_HOSTS=localhost docker compose up --wait > ``` This starts the following services: @@ -126,7 +126,7 @@ with its awesome [Symfony](https://confluence.jetbrains.com/display/PhpStorm/Get and [Php Inspections](https://plugins.jetbrains.com/plugin/7622-php-inspections-ea-extended-) plugins. Give them a try, you'll get auto-completion for almost everything and awesome quality analysis. -[PHP IntelliSense for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=zobo.php-intellisense) also works well, and is free and open source. +[PHP Intelephense for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=bmewburn.vscode-intelephense-client) also works well, and is free and open source. The API Platform distribution comes with a dummy entity for test purposes: `api/src/Entity/Greeting.php`. We will remove it later. @@ -239,6 +239,21 @@ For instance, go to `https://localhost/greetings.jsonld` to retrieve the list of Of course, you can also use your favorite HTTP client to query the API. We are fond of [Hoppscotch](https://hoppscotch.com), a free and open source API client with good support of API Platform. +## Keep Your Project in Sync with the API Platform Template + +You have started a project with the API Platform template and you would like to benefit from the latest enhancements introduced since you created your project (i.e. [FrankenPHP](https://frankenphp.dev/)). Just use this Git based tool +[The _template-sync_ project](https://github.com/coopTilleuls/template-sync) got you covered. + +Run the following command to import the changes since your last update: + +```console +curl -sSL https://raw.githubusercontent.com/coopTilleuls/template-sync/main/template-sync.sh | sh -s -- https://github.com/api-platform/api-platform +``` + +Resolve potential conflicts, run `git cherry-pick --continue` and you are done! + +For more details, refer to the [coopTilleuls/template-sync documentation](https://github.com/coopTilleuls/template-sync/blob/main/README.md) + ## Bringing your Own Model Your API Platform project is now 100% functional. Let's expose our own data model. @@ -453,10 +468,10 @@ Modify these files as described in these patches: public function getId(): ?int ``` -**Tip**: you can also use Symfony [MakerBundle](https://symfonycasts.com/screencast/symfony-fundamentals/maker-command?cid=apip) thanks to the `--api-resource` option: +**Tip**: You can use Symfony [MakerBundle](https://symfonycasts.com/screencast/symfony-fundamentals/maker-command?cid=apip) to generate a Doctrine entity that is also a resource thanks to the `--api-resource` option: ```console -bin/console make:entity --api-resource +docker compose exec php bin/console make:entity --api-resource ``` Doctrine's [attributes](https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/attributes-reference.html) map these entities to tables in the database. @@ -471,8 +486,8 @@ Now, delete the file `api/src/Entity/Greeting.php`. This demo entity isn't usefu Finally, generate a new database migration using [Doctrine Migrations](https://symfony.com/doc/current/doctrine.html#migrations-creating-the-database-tables-schema) and apply it: ```console -bin/console doctrine:migrations:diff -bin/console doctrine:migrations:migrate +docker compose exec php bin/console doctrine:migrations:diff +docker compose exec php bin/console doctrine:migrations:migrate ``` **We now have a working API with read and write capabilities!** @@ -666,7 +681,7 @@ Isn't API Platform a REST **and** GraphQL framework? That's true! GraphQL suppor need to install the [graphql-php](https://webonyx.github.io/graphql-php/) library. Run the following command: ```console -composer require api-platform/graphql +docker compose exec php composer require api-platform/graphql ``` You now have a GraphQL API! Open `https://localhost/graphql` (or `https://localhost/api/graphql` if you used Symfony Flex @@ -675,26 +690,32 @@ UI that is shipped with API Platform: ![GraphQL endpoint](images/api-platform-2.6-graphql.png) -Try it out by creating a greeting: +Try it out by creating a book: ```graphql mutation { - createGreeting(input: { name: "Test2" }) { - greeting { + createBook(input: { + isbn: "9781782164104", + title: "Persistence in PHP with the Doctrine ORM", + description: "This book is designed for PHP developers and architects who want to modernize their skills through better understanding of Persistence and ORM.", + author: "Kévin Dunglas", + publicationDate: "2013-12-01" + }) { + book { id - name + title } } } ``` -And by reading out the greeting: +And by reading out the book: ```graphql { - greeting(id: "/greetings/1") { + book(id: "/books/2") { id - name + title _id } } @@ -794,7 +815,7 @@ Keep in mind that you can use your favorite client-side technology: API Platform requests is OK (even COBOL can do that). To go further, the API Platform team maintains a demo application showing more advanced use cases like leveraging serialization -groups, user management, or JWT and OAuth authentication. [Checkout the demo code source on GitHub](https://github.com/api-platform/demo) +groups, user management, or JWT and OAuth authentication. [Check out the demo code source on GitHub](https://github.com/api-platform/demo) and [browse it online](https://demo.api-platform.com). ## Screencasts diff --git a/symfony/jwt.md b/symfony/jwt.md index 4330a5bf5f6..5f25ae75192 100644 --- a/symfony/jwt.md +++ b/symfony/jwt.md @@ -95,7 +95,7 @@ security: stateless: true provider: users json_login: - check_path: auth # The name in routes.yaml is enough for mapping + 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 @@ -154,7 +154,7 @@ security: jwt: ~ main: json_login: - check_path: auth # The name in routes.yaml is enough for mapping + 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 diff --git a/symfony/user.md b/symfony/user.md index 51b5f94368b..484b8cf621b 100644 --- a/symfony/user.md +++ b/symfony/user.md @@ -12,8 +12,6 @@ You can follow the [official Symfony Documentation](https://symfony.com/doc/curr namespace App\Entity; -use ApiPlatform\Metadata\ApiFilter; -use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Delete; use ApiPlatform\Metadata\Get; @@ -144,10 +142,13 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface /** * @see UserInterface + * + * Required until Symfony 8.0, where eraseCredentials() will be removed from the interface. + * No-op since plainPassword is cleared manually in the password processor. */ public function eraseCredentials(): void { - $this->plainPassword = null; + // Intentionally left blank } } ``` @@ -231,6 +232,7 @@ namespace App\State; use ApiPlatform\Metadata\Operation; use ApiPlatform\State\ProcessorInterface; +use App\Entity\User; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; /** @@ -259,7 +261,9 @@ final readonly class UserPasswordHasher implements ProcessorInterface $data->getPlainPassword() ); $data->setPassword($hashedPassword); - $data->eraseCredentials(); + + // To avoid leaving sensitive data like the plain password in memory or logs, we manually clear it after hashing. + $data->setPlainPassword(null); return $this->processor->process($data, $operation, $uriVariables, $context); }