diff --git a/.github/linters/.textlintrc b/.github/linters/.textlintrc new file mode 100644 index 00000000000..9a594e1278c --- /dev/null +++ b/.github/linters/.textlintrc @@ -0,0 +1,10 @@ +{ + "rules": { + "terminology": { + "exclude": [ + "Node(?:js)?", + "web[- ]?site(s)?" + ] + } + } +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 116c42780d8..5c32c320355 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,7 @@ jobs: fetch-depth: 0 - name: Lint - uses: github/super-linter@v3 + uses: github/super-linter@v4 env: VALIDATE_ALL_CODEBASE: false VALIDATE_EDITORCONFIG: false diff --git a/admin/authentication-support.md b/admin/authentication-support.md index ca71266e236..85848936b4b 100644 --- a/admin/authentication-support.md +++ b/admin/authentication-support.md @@ -4,15 +4,19 @@ API Platform Admin delegates the authentication support to React Admin. Refer to [the chapter dedicated to authentication in the React Admin documentation](https://marmelab.com/react-admin/Authentication.html) for more information. -In short, you have to tweak the data provider and the api documentation parser like this: +In short, you have to tweak the data provider and the API documentation parser like this: ```typescript // pwa/pages/admin/index.tsx import Head from "next/head"; import { Redirect, Route } from "react-router-dom"; -import { hydraDataProvider as baseHydraDataProvider, fetchHydra as baseFetchHydra, useIntrospection } from "@api-platform/admin"; -import parseHydraDocumentation from "@api-platform/api-doc-parser/lib/hydra/parseHydraDocumentation"; +import { + fetchHydra as baseFetchHydra, + hydraDataProvider as baseHydraDataProvider, + useIntrospection, +} from "@api-platform/admin"; +import { parseHydraDocumentation } from "@api-platform/api-doc-parser"; import authProvider from "utils/authProvider"; import { ENTRYPOINT } from "config/entrypoint"; @@ -35,10 +39,10 @@ const RedirectToLogin = () => { }; const apiDocumentationParser = async () => { try { - const { api } = await parseHydraDocumentation(ENTRYPOINT, { headers: getHeaders }); - return { api }; + return await parseHydraDocumentation(ENTRYPOINT, { headers: getHeaders }); } catch (result) { - if (result.status !== 401) { + const { api, response, status } = result; + if (status !== 401 || !response) { throw result; } @@ -46,7 +50,9 @@ const apiDocumentationParser = async () => { localStorage.removeItem("token"); return { - api: result.api, + api, + response, + status, customRoutes: [ ], diff --git a/admin/components.md b/admin/components.md index 95849ab0be1..b714bc12e92 100644 --- a/admin/components.md +++ b/admin/components.md @@ -21,12 +21,12 @@ const App = () => ( dataProvider={dataProvider} authProvider={authProvider}> - + ) diff --git a/admin/handling-relations.md b/admin/handling-relations.md index d569965005d..85e311ae94c 100644 --- a/admin/handling-relations.md +++ b/admin/handling-relations.md @@ -62,11 +62,11 @@ For instance, if your API returns: "hydra:member": [ { "@id": "/books/07b90597-542e-480b-a6bf-5db223c761aa", - "@type": "http://schema.org/Book", + "@type": "https://schema.org/Book", "title": "War and Peace", "author": { "@id": "/authors/d7a133c1-689f-4083-8cfc-afa6d867f37d", - "@type": "http://schema.org/Author", + "@type": "https://schema.org/Author", "firstName": "Leo", "lastName": "Tolstoi" } @@ -158,9 +158,7 @@ use Doctrine\ORM\Mapping as ORM; #[ApiResource] class Review { - #[ORM\Id] - #[ORM\GeneratedValue] - #[ORM\Column(type: 'integer')] + #[ORM\Id, ORM\Column, ORM\GeneratedValue] public ?int $id = null; #[ORM\ManyToOne] @@ -185,17 +183,15 @@ use Doctrine\ORM\Mapping as ORM; #[ApiResource] class Book { - #[ORM\Id] - #[ORM\GeneratedValue] - #[ORM\Column(type: 'integer')] + #[ORM\Id, ORM\Column, ORM\GeneratedValue] public ?int $id = null; - #[ORM\Column] + #[ORM\Column] #[ApiFilter(SearchFilter::class, strategy: 'ipartial')] public string $title; - #[ORM\OneToMany(targetEntity: Review::class)] - public Collection $reviews; + #[ORM\OneToMany(targetEntity: Review::class, mappedBy: 'book')] + public $reviews; public function __construct() { diff --git a/admin/index.md b/admin/index.md index 309d3427de5..1c14c69ebe5 100644 --- a/admin/index.md +++ b/admin/index.md @@ -8,7 +8,7 @@ for any API supporting [the Hydra Core Vocabulary](http://www.hydra-cg.com/) or API Platform Admin is the perfect companion of APIs created using [the API Platform framework](https://api-platform.com), but also supports APIs written with any other programming language or framework as long as they expose a standard Hydra API documentation. -API Platform Admin is a 100% standalone Single-Page-Application with no coupling to the server part, +API Platform Admin is a 100% standalone Single-Page-Application written in TypeScript with no coupling to the server part, according to the API-first paradigm. API Platform Admin parses the API documentation then uses the awesome [React Admin](https://marmelab.com/react-admin/) @@ -21,7 +21,7 @@ You can **customize everything** by using provided React Admin and [Material UI] * Automatically generates an admin interface for all the resources of the API thanks to hypermedia features of Hydra * 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 `http://schema.org/email`) +* 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 diff --git a/admin/performance.md b/admin/performance.md index 220e67714f9..118590d52d8 100644 --- a/admin/performance.md +++ b/admin/performance.md @@ -29,9 +29,7 @@ use Doctrine\ORM\Mapping as ORM; #[ApiResource] class Author { - #[ORM\Id] - #[ORM\GeneratedValue] - #[ORM\Column(type: 'integer')] + #[ORM\Id, ORM\Column, ORM\GeneratedValue] #[ApiFilter(SearchFilter::class, strategy: "exact")] public ?int $id = null; diff --git a/admin/schema.org.md b/admin/schema.org.md index f2c16792df6..fd2e4151f93 100644 --- a/admin/schema.org.md +++ b/admin/schema.org.md @@ -12,12 +12,12 @@ The following examples will use [API Platform Core](../core/) to create such API 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 `http://schema.org/name` type: +To configure which property should be shown to represent your entity, map the property containing the name of the object with the `https://schema.org/name` type: ```php // api/src/Entity/Person.php -#[ApiProperty(iri: "http://schema.org/name")] +#[ApiProperty(iri: "https://schema.org/name")] private $name; ``` @@ -27,8 +27,8 @@ Besides, it is also possible to use the documentation to customize some fields a The following Schema.org types are currently supported by API Platform Admin: -* `http://schema.org/email`: the field will be rendered using the `` React Admin component -* `http://schema.org/url`: the field will be rendered using the `` React Admin component -* `http://schema.org/identifier`: the field will be formatted properly in inputs +* `https://schema.org/email`: the field will be rendered using the `` React Admin component +* `https://schema.org/url`: the field will be rendered using the `` React Admin component +* `https://schema.org/identifier`: the field will be formatted properly in inputs Note: if you already use validation on your properties, the semantics are already configured correctly (see [the correspondence table](../core/validation.md#open-vocabulary-generated-from-validation-metadata))! diff --git a/core/configuration.md b/core/configuration.md index bfdc87c1bfd..825a1a21ca2 100644 --- a/core/configuration.md +++ b/core/configuration.md @@ -160,11 +160,11 @@ api_platform: # The OAuth flow grant type. flow: 'application' - # The OAuth token URL. - tokenUrl: '/oauth/v2/token' + # The OAuth token URL. Make sure to check the specification tokenUrl is not needed for an implicit flow. + tokenUrl: '' # The OAuth authentication URL. - authorizationUrl: '/oauth/v2/auth' + authorizationUrl: '' # The OAuth scopes. scopes: [] diff --git a/core/content-negotiation.md b/core/content-negotiation.md index 0c74118b166..8f6bc08fb85 100644 --- a/core/content-negotiation.md +++ b/core/content-negotiation.md @@ -122,7 +122,7 @@ this configuration might disable the `json` or the `html` on this resource for e You can specify different accepted formats at operation level too, it's especially convenient for to configure formats available for the `PATCH` method: -[codeSelector] + ```php ``` -[/codeSelector] + ## Supporting Custom Formats diff --git a/core/controllers.md b/core/controllers.md index 6d54f2d515a..93a73cd6b6f 100644 --- a/core/controllers.md +++ b/core/controllers.md @@ -68,7 +68,7 @@ This action will be automatically registered as a service (the service name is t API Platform automatically retrieves the appropriate PHP entity using the data provider then deserializes user data in it, and for `POST`, `PUT` and `PATCH` requests updates the entity with data provided by the user. -**Warning: the `__invoke()` method parameter [MUST be called `$data`](https://symfony.com/doc/current/components/http_kernel.html#getting-the-controller-arguments)**, otherwise, it will not be filled correctly! +**Warning: the `__invoke()` method parameter [MUST be called `$data`](https://symfony.com/doc/current/components/http_kernel.html#4-getting-the-controller-arguments)**, otherwise, it will not be filled correctly! Services (`$bookPublishingHandler` here) are automatically injected thanks to the autowiring feature. You can type-hint any service you need and it will be autowired too. @@ -82,7 +82,7 @@ the client. The routing has not been configured yet because we will add it at the resource configuration level: -[codeSelector] + ```php ``` -[/codeSelector] + It is mandatory to set the `method`, `path` and `controller` attributes. They allow API Platform to configure the routing path and the associated controller respectively. @@ -147,7 +147,7 @@ the associated controller respectively. You may want different serialization groups for your custom operations. Just configure the proper `normalization_context` and/or `denormalization_context` in your operation: -[codeSelector] + ```php ``` -[/codeSelector] + ## Retrieving the Entity If you want to bypass the automatic retrieval of the entity in your custom operation, you can set `"read"=false` in the operation attribute: -[codeSelector] + ```php ``` -[/codeSelector] + This way, it will skip the `ReadListener`. You can do the same for some other built-in listeners. See [Built-in Event Listeners](events.md#built-in-event-listeners) for more information. @@ -301,7 +301,7 @@ for `book_post_discontinuation` when neither `method` nor `route_name` attribute First, let's create your resource configuration: -[codeSelector] + ```php ``` -[/codeSelector] + API Platform will automatically map this `post_publication` operation to the route `book_post_publication`. Let's create a custom action and its related route using annotations: diff --git a/core/data-persisters.md b/core/data-persisters.md index 07b06acb5be..0cb61ae9011 100644 --- a/core/data-persisters.md +++ b/core/data-persisters.md @@ -22,7 +22,7 @@ persist data for a given resource will be used. ## Creating a Custom Data Persister -To create a data persister, you have to implement the [`ContextAwareDataPersisterInterface`](https://github.com/api-platform/core/blob/main/src/DataPersister/ContextAwareDataPersisterInterface.php). +To create a data persister, you have to implement the [`ContextAwareDataPersisterInterface`](https://github.com/api-platform/core/blob/2.6/src/DataPersister/ContextAwareDataPersisterInterface.php). This interface defines only 3 methods: * `persist`: to create or update the given data @@ -71,7 +71,7 @@ services: #tags: [ 'api_platform.data_persister' ] ``` -Note that if you don't need any `$context` in your data persister's methods, you can implement the [`DataPersisterInterface`](https://github.com/api-platform/core/blob/main/src/DataPersister/DataPersisterInterface.php) instead. +Note that if you don't need any `$context` in your data persister's methods, you can implement the [`DataPersisterInterface`](https://github.com/api-platform/core/blob/2.6/src/DataPersister/DataPersisterInterface.php) instead. ## Decorating the Built-In Data Persisters @@ -142,7 +142,8 @@ Even with service autowiring and autoconfiguration enabled, you must still confi services: # ... App\DataPersister\UserDataPersister: - decorates: 'api_platform.doctrine.orm.data_persister' + bind: + $decorated: '@api_platform.doctrine.orm.data_persister' # Uncomment only if autoconfiguration is disabled #arguments: ['@App\DataPersister\UserDataPersister.inner'] #tags: [ 'api_platform.data_persister' ] diff --git a/core/data-providers.md b/core/data-providers.md index 0d0e8023832..5377c0bc5d6 100644 --- a/core/data-providers.md +++ b/core/data-providers.md @@ -13,13 +13,15 @@ retrieve data for a given resource will be used. For a given resource, you can implement two kinds of interface: -* the [`CollectionDataProviderInterface`](https://github.com/api-platform/core/blob/main/src/Core/DataProvider/CollectionDataProviderInterface.php) +* the [`CollectionDataProviderInterface`](https://github.com/api-platform/core/blob/2.6/src/DataProvider/CollectionDataProviderInterface.php) is used when fetching a collection. -* the [`ItemDataProviderInterface`](https://github.com/api-platform/core/blob/main/src/Core/DataProvider/ItemDataProviderInterface.php) +* the [`ItemDataProviderInterface`](https://github.com/api-platform/core/blob/2.6/src/DataProvider/ItemDataProviderInterface.php) + is used when fetching items. +* the [`SubresourceDataProviderInterface`](https://github.com/api-platform/core/blob/2.6/src/DataProvider/SubresourceDataProviderInterface.php) is used when fetching items. Both implementations can also implement a third, optional, interface called -['RestrictedDataProviderInterface'](https://github.com/api-platform/core/blob/main/src/Core/DataProvider/RestrictedDataProviderInterface.php) +['RestrictedDataProviderInterface'](https://github.com/api-platform/core/blob/2.6/src/DataProvider/RestrictedDataProviderInterface.php) if you want to limit their effects to a single resource or operation. In the following examples we will create custom data providers for an entity class called `App\Entity\BlogPost`. @@ -27,9 +29,9 @@ Note, that if your entity is not Doctrine-related, you need to flag the identifi ## Custom Collection Data Provider -First, your `BlogPostCollectionDataProvider` has to implement the [`CollectionDataProviderInterface`](https://github.com/api-platform/core/blob/main/src/Core/DataProvider/CollectionDataProviderInterface.php): +First, your `BlogPostCollectionDataProvider` has to implement the [`CollectionDataProviderInterface`](https://github.com/api-platform/core/blob/2.6/src/DataProvider/CollectionDataProviderInterface.php): -The `getCollection` method must return an `array`, a `Traversable` or a [`ApiPlatform\Core\DataProvider\PaginatorInterface`](https://github.com/api-platform/core/blob/main/src/Core/DataProvider/PaginatorInterface.php) instance. +The `getCollection` method must return an `array`, a `Traversable` or a [`ApiPlatform\Core\DataProvider\PaginatorInterface`](https://github.com/api-platform/core/blob/2.6/src/DataProvider/PaginatorInterface.php) instance. If no data is available, you should return an empty array. ```php @@ -79,7 +81,7 @@ You can find a full working example in the [API Platform's demo application](htt ## Custom Item Data Provider -The process is similar for item data providers. Create a `BlogPostItemDataProvider` implementing the [`ItemDataProviderInterface`](https://github.com/api-platform/core/blob/main/src/Core/DataProvider/ItemDataProviderInterface.php) +The process is similar for item data providers. Create a `BlogPostItemDataProvider` implementing the [`ItemDataProviderInterface`](https://github.com/api-platform/core/blob/2.6/src/DataProvider/ItemDataProviderInterface.php) interface: The `getItem` method can return `null` if no result has been found. @@ -126,6 +128,49 @@ services: You can find a full working example in the [API Platform's demo application](https://github.com/api-platform/demo/blob/main/api/src/DataProvider/TopBookItemDataProvider.php). +## Custom Subresources Data Provider + +You can add custom logic or update subresources data provider with the SubresourceDataProviderInterface . + +```php +subresourceDataProvider->getSubresource($resourceClass, $identifiers, $context, $operationName); + // write your own logic + + return blogPosts; + } +} +``` + +Declare the service in your services configuration: + +```yaml +# api/config/services.yaml +services: + # ... + 'App\DataProvider\BlogPostSubresourceDataProvider': + arguments: + - '@api_platform.doctrine.orm.default.subresource_data_provider' +``` + ## Injecting the Serializer in an `ItemDataProvider` In some cases, you may need to inject the `Serializer` in your `DataProvider`. There are no issues with the @@ -170,7 +215,7 @@ API Platform provides a few extensions that you can reuse in your custom DataPro Note that there are a few kinds of extensions which are detailed in [their own chapter of the documentation](extensions.md). Because extensions are tagged services, you can use the [injection of tagged services](https://symfony.com/blog/new-in-symfony-3-4-simpler-injection-of-tagged-services): -[codeSelector] + ```yaml services: @@ -187,7 +232,7 @@ services: ``` -[/codeSelector] + Your data provider will now have access to the core extensions, here is an example on how to use them: diff --git a/core/default-order.md b/core/default-order.md index b47a8c1cb8b..a5df6ad574a 100644 --- a/core/default-order.md +++ b/core/default-order.md @@ -5,7 +5,7 @@ API Platform Core provides an easy way to override the default order of items in By default, items in the collection are ordered in ascending (ASC) order by their resource identifier(s). If you want to customize this order, you must add an `order` attribute on your ApiResource annotation: -[codeSelector] + ```php This `order` attribute is used as an array: the key defines the order field, the values defines the direction. If you only specify the key, `ASC` direction will be used as default. For example, to order by `foo` & `bar`: -[codeSelector] + ```php It's also possible to configure the default order on an association property: -[codeSelector] + ```php Another possibility is to apply the default order for a specific collection operation, which will override the global default order configuration. -[codeSelector] + ```php #[ApiResource( @@ -161,4 +161,4 @@ App\Entity\Book: name: ASC ``` -[/codeSelector] + diff --git a/core/deprecations.md b/core/deprecations.md index da03d33e32d..92f7fe802cb 100644 --- a/core/deprecations.md +++ b/core/deprecations.md @@ -67,7 +67,7 @@ class Parchment It's also possible to deprecate a single property: -[codeSelector] + ```php * With JSON-lD / Hydra, [an `owl:deprecated` annotation property](https://www.w3.org/TR/owl2-syntax/#Annotation_Properties) will be added to the appropriate data structure * With Swagger / OpenAPI, [a `deprecated` property](https://swagger.io/docs/specification/2-0/paths-and-operations/) will be added diff --git a/core/dto.md b/core/dto.md index 739e1df9a58..504449365e8 100644 --- a/core/dto.md +++ b/core/dto.md @@ -9,7 +9,7 @@ However, it's sometimes useful to use a specific class to represent the input or For a given resource class, you may want to have a different representation of this class as input (write) or output (read). To do so, a resource can take an input and/or an output class: -[codeSelector] + ```php ``` -[/codeSelector] + The `input` attribute is used during [the deserialization process](serialization.md), when transforming the user-provided data to a resource instance. Similarly, the `output` attribute is used during [the serialization process](serialization.md). This class represents how the `Book` resource will be represented in the `Response`. @@ -357,7 +357,7 @@ will be skipped. If `output` is `false`, the serialization process will be skipp `input` and `output` attributes can be set on a per operation basis: -[codeSelector] + ```php ``` -[/codeSelector] + ## Input/Output Metadata diff --git a/core/events.md b/core/events.md index 0b512b7785a..3a9612b3309 100644 --- a/core/events.md +++ b/core/events.md @@ -59,7 +59,7 @@ Attribute | Type | Default | Description Registering your own event listeners to add extra logic is convenient. -The [`ApiPlatform\Core\EventListener\EventPriorities`](https://github.com/api-platform/core/blob/main/src/Core/EventListener/EventPriorities.php) class comes with a convenient set of class constants corresponding to commonly used priorities: +The [`ApiPlatform\Core\EventListener\EventPriorities`](https://github.com/api-platform/core/blob/2.6/src/EventListener/EventPriorities.php) class comes with a convenient set of class constants corresponding to commonly used priorities: Constant | Event | Priority | -------------------|-------------------|----------| diff --git a/core/extending-jsonld-context.md b/core/extending-jsonld-context.md index 30679f643d6..74fb29fccaa 100644 --- a/core/extending-jsonld-context.md +++ b/core/extending-jsonld-context.md @@ -2,7 +2,7 @@ ## JSON-LD -

JSON-LD screencast
Watch the JSON-LD screencast
+

JSON-LD screencast
Watch the JSON-LD screencast

API Platform Core provides the possibility to extend the JSON-LD context of properties. This allows you to describe JSON-LD-typed values, inverse properties using the `@reverse` keyword and you can even overwrite the `@id` property this way. Everything you define @@ -17,13 +17,13 @@ namespace App\Entity; use ApiPlatform\Core\Annotation\ApiProperty; use ApiPlatform\Core\Annotation\ApiResource; -#[ApiResource(iri: "http://schema.org/Book")] +#[ApiResource(iri: "https://schema.org/Book")] class Book { // ... #[ApiProperty( - iri: "http://schema.org/name", + iri: "https://schema.org/name", attributes: [ "jsonld_context" => [ "@id" => "http://yourcustomid.com", @@ -70,7 +70,7 @@ Note that you do not have to provide the `@id` attribute. If you do not provide It's also possible to replace the Hydra context used by the documentation generator: -[codeSelector] + ```php ``` -[/codeSelector] + diff --git a/core/extensions.md b/core/extensions.md index 058dfc755c7..ca86e0b7bc8 100644 --- a/core/extensions.md +++ b/core/extensions.md @@ -46,11 +46,8 @@ use ApiPlatform\Core\Annotation\ApiResource; #[ApiResource] class Offer { - /** - * @var User - * @ORM\ManyToOne(targetEntity="User") - */ - public $user; + #[ORM\ManyToOne] + public User $user; //... } diff --git a/core/external-vocabularies.md b/core/external-vocabularies.md index 7b8ba2dbe04..98079a1d0c2 100644 --- a/core/external-vocabularies.md +++ b/core/external-vocabularies.md @@ -14,12 +14,12 @@ namespace App\Entity; use ApiPlatform\Core\Annotation\ApiProperty; use ApiPlatform\Core\Annotation\ApiResource; -#[ApiResource(iri: "http://schema.org/Book")] +#[ApiResource(iri: "https://schema.org/Book")] class Book { // ... - #[ApiProperty(iri: "http://schema.org/name")] + #[ApiProperty(iri: "https://schema.org/name")] public $name; // ... @@ -58,17 +58,17 @@ Built-in mapping is: Constraints | Schema.org type | ---------------------------------------------------- |-----------------------------------| -`Symfony\Component\Validator\Constraints\Url` | `http://schema.org/url` | -`Symfony\Component\Validator\Constraints\Email` | `http://schema.org/email` | -`Symfony\Component\Validator\Constraints\Uuid` | `http://schema.org/identifier` | -`Symfony\Component\Validator\Constraints\CardScheme` | `http://schema.org/identifier` | -`Symfony\Component\Validator\Constraints\Bic` | `http://schema.org/identifier` | -`Symfony\Component\Validator\Constraints\Iban` | `http://schema.org/identifier` | -`Symfony\Component\Validator\Constraints\Date` | `http://schema.org/Date` | -`Symfony\Component\Validator\Constraints\DateTime` | `http://schema.org/DateTime` | -`Symfony\Component\Validator\Constraints\Time` | `http://schema.org/Time` | -`Symfony\Component\Validator\Constraints\Image` | `http://schema.org/image` | -`Symfony\Component\Validator\Constraints\File` | `http://schema.org/MediaObject` | -`Symfony\Component\Validator\Constraints\Currency` | `http://schema.org/priceCurrency` | -`Symfony\Component\Validator\Constraints\Isbn` | `http://schema.org/isbn` | -`Symfony\Component\Validator\Constraints\Issn` | `http://schema.org/issn` | +`Symfony\Component\Validator\Constraints\Url` | `https://schema.org/url` | +`Symfony\Component\Validator\Constraints\Email` | `https://schema.org/email` | +`Symfony\Component\Validator\Constraints\Uuid` | `https://schema.org/identifier` | +`Symfony\Component\Validator\Constraints\CardScheme` | `https://schema.org/identifier` | +`Symfony\Component\Validator\Constraints\Bic` | `https://schema.org/identifier` | +`Symfony\Component\Validator\Constraints\Iban` | `https://schema.org/identifier` | +`Symfony\Component\Validator\Constraints\Date` | `https://schema.org/Date` | +`Symfony\Component\Validator\Constraints\DateTime` | `https://schema.org/DateTime` | +`Symfony\Component\Validator\Constraints\Time` | `https://schema.org/Time` | +`Symfony\Component\Validator\Constraints\Image` | `https://schema.org/image` | +`Symfony\Component\Validator\Constraints\File` | `https://schema.org/MediaObject` | +`Symfony\Component\Validator\Constraints\Currency` | `https://schema.org/priceCurrency` | +`Symfony\Component\Validator\Constraints\Isbn` | `https://schema.org/isbn` | +`Symfony\Component\Validator\Constraints\Issn` | `https://schema.org/issn` | diff --git a/core/file-upload.md b/core/file-upload.md index 736ac5077d7..02260e20c69 100644 --- a/core/file-upload.md +++ b/core/file-upload.md @@ -42,6 +42,9 @@ resource (in our case: `Book`). This example will use a custom controller to receive the file. The second example will use a custom `multipart/form-data` decoder to deserialize the resource instead. +**Note**: Uploading files won't work in `PUT` or `PATCH` requests, you must use `POST` method to upload files. +See [the related issue on Symfony](https://github.com/symfony/symfony/issues/9226) and [the related bug in PHP](https://bugs.php.net/bug.php?id=55815) talking about this behavior. + ### Configuring the Resource Receiving the Uploaded File The `MediaObject` resource is implemented like this: @@ -61,11 +64,11 @@ use Symfony\Component\Validator\Constraints as Assert; use Vich\UploaderBundle\Mapping\Annotation as Vich; /** - * @ORM\Entity * @Vich\Uploadable */ +#[ORM\Entity] #[ApiResource( - iri: 'http://schema.org/MediaObject', + iri: 'https://schema.org/MediaObject', normalizationContext: ['groups' => ['media_object:read']], itemOperations: ['get'], collectionOperations: [ @@ -96,14 +99,10 @@ use Vich\UploaderBundle\Mapping\Annotation as Vich; )] class MediaObject { - /** - * @ORM\Column(type="integer") - * @ORM\GeneratedValue - * @ORM\Id - */ + #[ORM\Id, ORM\Column, ORM\GeneratedValue] private ?int $id = null; - #[ApiProperty(iri: 'http://schema.org/contentUrl')] + #[ApiProperty(iri: 'https://schema.org/contentUrl')] #[Groups(['media_object:read'])] public ?string $contentUrl = null; @@ -113,9 +112,7 @@ class MediaObject #[Assert\NotNull(groups: ['media_object_create'])] public ?File $file = null; - /** - * @ORM\Column(nullable=true) - */ + #[ORM\Column(nullable: true)] public ?string $filePath = null; public function getId(): ?int @@ -218,7 +215,7 @@ your data, you will get a response looking like this: ```json { - "@type": "http://schema.org/MediaObject", + "@type": "https://schema.org/MediaObject", "@id": "/media_objects/", "contentUrl": "" } @@ -255,19 +252,15 @@ use Doctrine\ORM\Mapping as ORM; use Symfony\Component\HttpFoundation\File\File; use Vich\UploaderBundle\Mapping\Annotation as Vich; -/** - * @ORM\Entity - */ -#[ApiResource(iri: 'http://schema.org/Book')] +#[ORM\Entity] +#[ApiResource(iri: 'https://schema.org/Book')] class Book { // ... - /** - * @ORM\ManyToOne(targetEntity=MediaObject::class) - * @ORM\JoinColumn(nullable=true) - */ - #[ApiProperty(iri: 'http://schema.org/image')] + #[ORM\ManyToOne(targetEntity: MediaObject::class)] + #[ORM\JoinColumn(nullable: true)] + #[ApiProperty(iri: 'https://schema.org/image')] public ?MediaObject $image = null; // ... @@ -358,11 +351,11 @@ use Symfony\Component\Serializer\Annotation\Groups; use Vich\UploaderBundle\Mapping\Annotation as Vich; /** - * @ORM\Entity * @Vich\Uploadable */ +#[ORM\Entity] #[ApiResource( - iri: 'http://schema.org/Book', + iri: 'https://schema.org/Book', normalizationContext: ['groups' => ['book:read']], denormalizationContext: ['groups' => ['book:write']], collectionOperations: [ @@ -378,7 +371,7 @@ class Book { // ... - #[ApiProperty(iri: 'http://schema.org/contentUrl')] + #[ApiProperty(iri: 'https://schema.org/contentUrl')] #[Groups(['book:read'])] public ?string $contentUrl = null; @@ -388,9 +381,7 @@ class Book #[Groups(['book:write'])] public ?File $file = null; - /** - * @ORM\Column(nullable=true) - */ + #[ORM\Column(nullable: true)] public ?string $filePath = null; // ... diff --git a/core/filters.md b/core/filters.md index 55c97098ec7..3f6d1397b4a 100644 --- a/core/filters.md +++ b/core/filters.md @@ -12,7 +12,7 @@ By default, all filters are disabled. They must be enabled explicitly. When a filter is enabled, it automatically appears in the [OpenAPI](swagger.md) and [GraphQL](graphql.md) documentations. It is also automatically documented as a `hydra:search` property for JSON-LD responses. -

Filtering and Searching screencast
Watch the Filtering & Searching screencast
+

Filtering and Searching screencast
Watch the Filtering & Searching screencast

## Doctrine ORM and MongoDB ODM Filters @@ -42,7 +42,7 @@ to a Resource in two ways: We're linking the filter `offer.date_filter` with the resource like this: - [codeSelector] + ```php ``` - [/codeSelector] + 2. By using the `#[ApiFilter]` attribute. @@ -145,7 +145,7 @@ Syntax: `?property[]=foo&property[]=bar` In the following example, we will see how to allow the filtering of a list of e-commerce offers: -[codeSelector] + ```php `http://localhost:8000/api/offers?price=10` will return all offers with a price being exactly `10`. `http://localhost:8000/api/offers?description=shirt` will return all offers with a description containing the word "shirt". @@ -195,7 +195,7 @@ Filters can be combined together: `http://localhost:8000/api/offers?price=10&des It is possible to filter on relations too, if `Offer` has a `Product` relation: -[codeSelector] + ```php With this service definition, it is possible to find all offers belonging to the product identified by a given IRI. Try the following: `http://localhost:8000/api/offers?product=/api/products/12`. @@ -256,7 +256,7 @@ The `after` and `before` filters will filter including the value whereas `strict Like others filters, the date filter must be explicitly enabled: -[codeSelector] + ```php Given that the collection endpoint is `/offers`, you can filter offers by date with the following query: `/offers?createdAt[after]=2018-03-19`. @@ -317,7 +317,7 @@ Always include items | `ApiPlatform\Core\Bridge\Doctrine\Orm\Fil For instance, exclude entries with a property value of `null` with the following service definition: -[codeSelector] + ```php ### Boolean Filter @@ -368,7 +368,7 @@ Syntax: `?property=` Enable the filter: -[codeSelector] + ```php Given that the collection endpoint is `/offers`, you can filter offers with the following query: `/offers?isAvailableGenericallyInMyCountry=true`. @@ -423,7 +423,7 @@ Syntax: `?property=` Enable the filter: -[codeSelector] + ```php Given that the collection endpoint is `/offers`, you can filter offers with the following query: `/offers?sold=1`. @@ -478,7 +478,7 @@ Syntax: `?property[]=value` Enable the filter: -[codeSelector] + ```php Given that the collection endpoint is `/offers`, you can filter the price with the following query: `/offers?price[between]=12.99..15.99`. @@ -538,7 +538,7 @@ Previous syntax (deprecated): `?property[exists]=` Enable the filter: -[codeSelector] + ```php Given that the collection endpoint is `/offers`, you can filter offers on nullable field with the following query: `/offers?exists[transportFees]=true`. @@ -605,7 +605,7 @@ Syntax: `?order[property]=` Enable the filter: -[codeSelector] + ```php Given that the collection endpoint is `/offers`, you can filter offers by name in ascending order and then by ID in descending order with the following query: `/offers?order[name]=desc&order[id]=asc`. @@ -656,7 +656,7 @@ order with the following query: `/offers?order[name]=desc&order[id]=asc`. By default, whenever the query does not specify the direction explicitly (e.g.: `/offers?order[name]&order[id]`), filters will not be applied unless you configure a default order direction to use: -[codeSelector] + ```php #### Comparing with Null Values @@ -712,7 +712,7 @@ Consider items as largest | `ApiPlatform\Core\Bridge\Doctrine\Orm\Fil For instance, treat entries with a property value of `null` as the smallest, with the following service definition: -[codeSelector] + ```php #### Using a Custom Order Query Parameter Name @@ -772,7 +772,7 @@ api_platform: Sometimes, you need to be able to perform filtering based on some linked resources (on the other side of a relation). All built-in filters support nested properties using the dot (`.`) syntax, e.g.: -[codeSelector] + ```php The above allows you to find offers by their respective product's color: `http://localhost:8000/api/offers?product.color=red`, or order offers by the product's release date: `http://localhost:8000/api/offers?order[product.releaseDate]=desc` @@ -835,7 +835,7 @@ As we have seen in previous examples, properties where filters can be applied mu care about security and performance (e.g. an API with restricted access), it is also possible to enable built-in filters for all properties: -[codeSelector] + ```php **Note: Filters on nested properties must still be enabled explicitly, in order to keep things sane.** @@ -902,7 +902,7 @@ Syntax: `?order[property]=` Enable the filter: -[codeSelector] + ```php Given that the collection endpoint is `/tweets`, you can filter tweets by id and date in ascending or descending order: `/tweets?order[id]=asc&order[date]=desc`. @@ -1215,10 +1215,12 @@ final class RegexpFilter extends AbstractContextAwareFilter 'property' => $property, 'type' => Type::BUILTIN_TYPE_STRING, 'required' => false, - 'swagger' => [ - 'description' => 'Filter using a regex. This will appear in the Swagger documentation!', - 'name' => 'Custom name to use in the Swagger documentation', - 'type' => 'Will appear below the name in the Swagger documentation', + 'description' => 'Filter using a regex. This will appear in the OpenApi documentation!', + 'openapi' => [ + 'example' => 'Custom example that will be in the documentation and be the default value of the sandbox', + 'allowReserved' => false,// if true, query parameters will be not percent-encoded + 'allowEmptyValue' => true, + 'explode' => false, // to be true, the type must be Type::BUILTIN_TYPE_ARRAY, ?product=blue,green will be ?product=blue&product=green ], ]; } @@ -1530,12 +1532,10 @@ use App\Entity\DummyCarColor; #[ApiResource] class DummyCar { - #[ORM\Id] - #[ORM\GeneratedValue] - #[ORM\Column(type: 'integer')] + #[ORM\Id, ORM\Column, ORM\GeneratedValue] private ?int $id = null; - #[ORM\Column(type: 'string')] + #[ORM\Column] #[ApiFilter(SearchFilter::class, strategy: 'partial')] public ?string $name = null; diff --git a/core/fosuser-bundle.md b/core/fosuser-bundle.md index dca43bf913f..a518b84adb8 100644 --- a/core/fosuser-bundle.md +++ b/core/fosuser-bundle.md @@ -63,37 +63,29 @@ use FOS\UserBundle\Model\User as BaseUser; use FOS\UserBundle\Model\UserInterface; use Symfony\Component\Serializer\Annotation\Groups; -/** - * @ORM\Entity - * @ORM\Table(name="fos_user") - */ +#[ORM\Entity] +#[ORM\Table(name: 'fos_user')] #[ApiResource( normalizationContext: ["groups" => ["user", "user:read"]], denormalizationContext: ["groups" => ["user", "user:write"]] )] class User extends BaseUser { - /** - * @ORM\Id - * @ORM\Column(type="integer") - * @ORM\GeneratedValue(strategy="AUTO") - */ - protected $id; + #[ORM\Id, ORM\Column, ORM\GeneratedValue] + protected ?int $id = null; #[Groups("user")] - protected $email; + protected string $email; - /** - * @ORM\Column(type="string", length=255, nullable=true) - */ + #[ORM\Column(nullable: true)] #[Groups("user")] - protected $fullname; + protected string $fullname; #[Groups("user:write")] - protected $plainPassword; + protected string $plainPassword; #[Groups("user")] - protected $username; + protected string $username; public function setFullname(?string $fullname): void { diff --git a/core/getting-started.md b/core/getting-started.md index a92863c4fc7..dc516e8afd1 100644 --- a/core/getting-started.md +++ b/core/getting-started.md @@ -47,24 +47,18 @@ use Doctrine\ORM\Mapping as ORM; use Doctrine\Common\Collections\ArrayCollection; use Symfony\Component\Validator\Constraints as Assert; -/** - * @ORM\Entity - */ +#[ORM\Entity] #[ApiResource] class Product // The class name will be used to name exposed resources { - /** - * @ORM\Id - * @ORM\GeneratedValue(strategy="AUTO") - * @ORM\Column(type="integer") - */ + #[ORM\Id, ORM\Column, ORM\GeneratedValue] private ?int $id = null; /** * A name property - this description will be available in the API documentation too. * - * @ORM\Column */ + #[ORM\Column] #[Assert\NotBlank] public string $name = ''; @@ -72,8 +66,8 @@ class Product // The class name will be used to name exposed resources /** * @var Offer[]|ArrayCollection * - * @ORM\OneToMany(targetEntity="Offer", mappedBy="product", cascade={"persist"}) */ + #[ORM\OneToMany(targetEntity: Offer::class, mappedBy: 'product', cascade: ['persist'])] public iterable $offers; public function __construct() @@ -117,32 +111,22 @@ use Symfony\Component\Validator\Constraints as Assert; /** * An offer from my shop - this description will be automatically extracted from the PHPDoc to document the API. * - * @ORM\Entity */ -#[ApiResource(iri: 'http://schema.org/Offer')] +#[ORM\Entity] +#[ApiResource(iri: 'https://schema.org/Offer')] class Offer { - /** - * @ORM\Column(type="integer") - * @ORM\Id - * @ORM\GeneratedValue(strategy="AUTO") - */ + #[ORM\Id, ORM\Column, ORM\GeneratedValue] private ?int $id = null; - /** - * @ORM\Column(type="text") - */ + #[ORM\Column(type: 'text')] public string $description = ''; - /** - * @ORM\Column(type="float") - */ + #[ORM\Column] #[Assert\Range(minMessage: 'The price must be superior to 0.', min: 0)] public float $price = -1.0; - /** - * @ORM\ManyToOne(targetEntity="Product", inversedBy="offers") - */ + #[ORM\ManyToOne(targetEntity: Product::class, inversedBy: 'offers')] public ?Product $product = null; public function getId(): ?int @@ -182,7 +166,7 @@ It is also possible to override the naming convention using [operation path nami As an alternative to annotations, you can map entity classes using YAML or XML: -[codeSelector] + ```yaml # api/config/api_platform/resources.yaml @@ -191,7 +175,7 @@ resources: App\Entity\Offer: shortName: 'Offer' # optional description: 'An offer from my shop' # optional - iri: 'http://schema.org/Offer' # optional + iri: 'https://schema.org/Offer' # optional attributes: # optional pagination_items_per_page: 25 # optional ``` @@ -209,12 +193,12 @@ resources: class="App\Entity\Offer" shortName="Offer" description="An offer from my shop" - iri="http://schema.org/Offer" + iri="https://schema.org/Offer" /> ``` -[/codeSelector] + If you prefer to use YAML or XML files instead of annotations, you must configure API Platform to load the appropriate files: diff --git a/core/graphql.md b/core/graphql.md index 86522437301..1d93c98402b 100644 --- a/core/graphql.md +++ b/core/graphql.md @@ -382,7 +382,7 @@ For each resource, three mutations are available: one for creating it (`create`) When updating or deleting a resource, you need to pass the **IRI** of the resource as argument. See [Global Object Identifier](#global-object-identifier) for more information. -### Client Mutation Id +### Client Mutation ID Following the [Relay Input Object Mutations Specification](https://github.com/facebook/relay/blob/v7.1.0/website/spec/Mutations.md#relay-input-object-mutations-specification), you can pass a `clientMutationId` as argument and can ask its value as a field. @@ -503,7 +503,7 @@ Your custom mutations will be available like this: } mutation { - withCustomArgsMutationBook(input: {sendMail: true, clientMutationId: "myId}) { + withCustomArgsMutationBook(input: {sendMail: true, clientMutationId: "myId"}) { book { title } @@ -584,7 +584,7 @@ You can also pass `clientSubscriptionId` as argument and can ask its value as a In the payload of the subscription, the given fields of the resource will be the fields you subscribe to: if any of these fields is updated, you will be pushed their updated values. -The `mercureUrl` field is the Mercure URL you need to use to [subscribe to the updates](https://mercure.rocks/docs/getting-started#subscribing) on the client side. +The `mercureUrl` field is the Mercure URL you need to use to [subscribe to the updates](https://mercure.rocks/docs/getting-started#subscribing) on the client-side. ### Receiving an Update @@ -1169,7 +1169,7 @@ use Symfony\Component\Serializer\Annotation\Groups; 'normalization_context' => ['groups' => ['collection_query']], 'denormalization_context' => ['groups' => ['mutation']] ] - } + ] )] class Book { @@ -1249,7 +1249,7 @@ final class ErrorHandler implements ErrorHandlerInterface Then register the service: -[codeSelector] + ```yaml # api/config/services.yaml @@ -1291,7 +1291,7 @@ return function(ContainerConfigurator $configurator) { }; ``` -[/codeSelector] + ### Formatting Exceptions and Errors @@ -1451,9 +1451,7 @@ class Book public $name; - /** - * @ORM\OneToMany(targetEntity="Book") - */ + #[ORM\OneToMany(targetEntity: Book::class)] public $relatedBooks; // ... @@ -1646,7 +1644,7 @@ final class TypeConverter implements TypeConverterInterface /** * {@inheritdoc} */ - public function convertType(Type $type, bool $input, ?string $queryName, ?string $mutationName, string $resourceClass, string $rootResource, ?string $property, int $depth) + public function convertType(Type $type, bool $input, ?string $queryName, ?string $mutationName, ?string $subscriptionName, string $resourceClass, string $rootResource, ?string $property, int $depth) { if ('publicationDate' === $property && Book::class === $resourceClass @@ -1654,7 +1652,7 @@ final class TypeConverter implements TypeConverterInterface return 'DateTime'; } - return $this->defaultTypeConverter->convertType($type, $input, $queryName, $mutationName, $resourceClass, $rootResource, $property, $depth); + return $this->defaultTypeConverter->convertType($type, $input, $queryName, $mutationName, $subscriptionName, $resourceClass, $rootResource, $property, $depth); } /** @@ -1771,11 +1769,11 @@ use Symfony\Component\Validator\Constraints as Assert; use Vich\UploaderBundle\Mapping\Annotation as Vich; /** - * @ORM\Entity * @Vich\Uploadable */ +#[ORM\Entity] #[ApiResource( - iri: 'http://schema.org/MediaObject', + iri: 'https://schema.org/MediaObject', normalizationContext: [ 'groups' => ['media_object_read'] ], @@ -1791,37 +1789,21 @@ use Vich\UploaderBundle\Mapping\Annotation as Vich; )] class MediaObject { - /** - * @var int|null - * - * @ORM\Column(type="integer") - * @ORM\GeneratedValue - * @ORM\Id - */ - protected $id; + #[ORM\Id, ORM\Column, ORM\GeneratedValue] + protected ?int $id = null; - /** - * @var string|null - * - * @Groups({"media_object_read"}) - */ - #[ApiProperty(iri: 'http://schema.org/contentUrl')] - public $contentUrl; + #[ApiProperty(iri: 'https://schema.org/contentUrl')] + #[Groups(['media_object_read'])] + public ?string $contentUrl = null; /** - * @var File|null - * - * @Assert\NotNull(groups={"media_object_create"}) * @Vich\UploadableField(mapping="media_object", fileNameProperty="filePath") */ - public $file; + #[Assert\NotNull(groups: ['media_object_create'])] + public ?File $file = null; - /** - * @var string|null - * - * @ORM\Column(nullable=true) - */ - public $filePath; + #[ORM\Column(nullable: true)] + public ?string $filePath = null; public function getId(): ?int { diff --git a/core/identifiers.md b/core/identifiers.md index d697293cb43..27b8f7b52af 100644 --- a/core/identifiers.md +++ b/core/identifiers.md @@ -9,7 +9,7 @@ To help with your development experience, we introduced an identifier normalizat Let's say you have the following class, which is identified by a `UUID` type. In this example, `UUID` is not a simple string but an object with many attributes. -[codeSelector] + ```php ``` -[/codeSelector] + Once registered as an `ApiResource`, having an existing person, it will be accessible through the following URL: `/people/110e8400-e29b-11d4-a716-446655440000`. Note that the property identifying our resource is named `code`. @@ -123,7 +123,7 @@ final class UuidNormalizer implements DenormalizerInterface Tag this service as an `api_platform.identifier.denormalizer`: -[codeSelector] + ```yaml services: @@ -138,7 +138,7 @@ services: ``` -[/codeSelector] + Your `PersonDataProvider` will now work as expected! @@ -155,26 +155,18 @@ use ApiPlatform\Core\Annotation\ApiProperty; use App\Uuid; use Doctrine\ORM\Mapping as ORM; -/** - * @ORM\Entity - */ +#[ORM\Entity] #[ApiResource] final class Person { - /** - * @var int - * - * @ORM\Id() - * @ORM\GeneratedValue() - * @ORM\Column(type="integer") - */ + #[ORM\Id, ORM\Column, ORM\GeneratedValue] #[ApiProperty(identifier: false)] - private $id; + private ?int $id = null; /** * @var Uuid - * @ORM\Column(type="uuid", unique=true) */ + #[ORM\Column(type: 'uuid', unique: true)] #[ApiProperty(identifier: true)] public $code; diff --git a/core/json-schema.md b/core/json-schema.md index 13bb81888d7..9519709f971 100644 --- a/core/json-schema.md +++ b/core/json-schema.md @@ -49,9 +49,7 @@ use Doctrine\ORM\Mapping as ORM; #[ApiResource] class Greeting { - #[ORM\Id] - #[ORM\GeneratedValue] - #[ORM\Column(type: "integer")] + #[ORM\Id, ORM\Column, ORM\GeneratedValue] private ?int $id = null; // [...] diff --git a/core/jwt.md b/core/jwt.md index d3d6d954d7b..2ebeb678f3a 100644 --- a/core/jwt.md +++ b/core/jwt.md @@ -42,7 +42,7 @@ The keys should not be checked in to the repository (i.e. it's in `api/.gitignor only pass signature validation against the same pair of keys it was signed with. This is especially relevant in a production environment, where you don't want to accidentally invalidate all your clients' tokens at every deployment. -For more information, refer to [the bundle's documentation](https://github.com/lexik/LexikJWTAuthenticationBundle/blob/master/Resources/doc/index.md) +For more information, refer to [the bundle's documentation](https://github.com/lexik/LexikJWTAuthenticationBundle/blob/2.x/Resources/doc/index.rst) or read a [general introduction to JWT here](https://jwt.io/introduction/). We're not done yet! Let's move on to configuring the Symfony SecurityBundle for JWT authentication. @@ -105,7 +105,7 @@ authentication_token: ``` If you want to avoid loading the `User` entity from database each time a JWT token needs to be authenticated, you may consider using -the [database-less user provider](https://github.com/lexik/LexikJWTAuthenticationBundle/blob/master/Resources/doc/8-jwt-user-provider.md) provided by LexikJWTAuthenticationBundle. However, it means you will have to fetch the `User` entity from the database yourself as needed (probably through the Doctrine EntityManager). +the [database-less user provider](https://github.com/lexik/LexikJWTAuthenticationBundle/blob/2.x/Resources/doc/8-jwt-user-provider.rst) provided by LexikJWTAuthenticationBundle. However, it means you will have to fetch the `User` entity from the database yourself as needed (probably through the Doctrine EntityManager). Refer to the section on [Security](security.md) to learn how to control access to API resources and operations. You may also want to [configure Swagger UI for JWT authentication](#documenting-the-authentication-mechanism-with-swaggeropen-api). @@ -120,7 +120,7 @@ security: # https://symfony.com/doc/current/security.html#c-hashing-passwords password_hashers: App\Entity\User: 'auto' - + # https://symfony.com/doc/current/security/authenticator_manager.html enable_authenticator_manager: true # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers @@ -154,6 +154,18 @@ security: - { path: ^/, roles: IS_AUTHENTICATED_FULLY } ``` +### Be sure to have lexik_jwt_authentication configured on your user_identity_field + +```yaml +# api/config/packages/lexik_jwt_authentication.yaml +lexik_jwt_authentication: + secret_key: '%env(resolve:JWT_SECRET_KEY)%' + public_key: '%env(resolve:JWT_PUBLIC_KEY)%' + pass_phrase: '%env(JWT_PASSPHRASE)%' + + user_identity_field: email # Or the field you have setted using make:user +``` + ## Documenting the Authentication Mechanism with Swagger/Open API Want to test the routes of your JWT-authentication-protected API? @@ -165,7 +177,7 @@ Want to test the routes of your JWT-authentication-protected API? api_platform: swagger: api_keys: - apiKey: + JWT: name: Authorization type: header ``` @@ -177,8 +189,8 @@ The "Authorize" button will automatically appear in Swagger UI. ### Adding a New API Key All you have to do is configure the API key in the `value` field. -By default, [only the authorization header mode is enabled](https://github.com/lexik/LexikJWTAuthenticationBundle/blob/master/Resources/doc/index.md#2-use-the-token) in LexikJWTAuthenticationBundle. -You must set the [JWT token](https://github.com/lexik/LexikJWTAuthenticationBundle/blob/master/Resources/doc/index.md#1-obtain-the-token) as below and click on the "Authorize" button. +By default, [only the authorization header mode is enabled](https://github.com/lexik/LexikJWTAuthenticationBundle/blob/2.x/Resources/doc/index.rst#2-use-the-token) in LexikJWTAuthenticationBundle. +You must set the [JWT token](https://github.com/lexik/LexikJWTAuthenticationBundle/blob/2.x/Resources/doc/index.rst#1-obtain-the-token) as below and click on the "Authorize" button. `Bearer MY_NEW_TOKEN` @@ -238,6 +250,13 @@ final class JwtDecorator implements OpenApiFactoryInterface ], ]); + $schemas = $openApi->getComponents()->getSecuritySchemes() ?? []; + $schemas['JWT'] = new \ArrayObject([ + 'type' => 'http', + 'scheme' => 'bearer', + 'bearerFormat' => 'JWT', + ]); + $pathItem = new Model\PathItem( ref: 'JWT Token', post: new Model\Operation( @@ -266,6 +285,7 @@ final class JwtDecorator implements OpenApiFactoryInterface ], ]), ), + security: [], ), ); $openApi->getPaths()->addPath('/authentication_token', $pathItem); @@ -280,11 +300,11 @@ And register this service in `config/services.yaml`: ```yaml # api/config/services.yaml services: - # ... + # ... App\OpenApi\JwtDecorator: decorates: 'api_platform.openapi.factory' - arguments: ['@.inner'] + arguments: ['@.inner'] ``` ## Testing @@ -308,14 +328,15 @@ class AuthenticationTest extends ApiTestCase public function testLogin(): void { $client = self::createClient(); + $container = self::getContainer(); $user = new User(); $user->setEmail('test@example.com'); $user->setPassword( - self::$container->get('security.user_password_hasher')->hashPassword($user, '$3CR3T') + $container->get('security.user_password_hasher')->hashPassword($user, '$3CR3T') ); - $manager = self::$container->get('doctrine')->getManager(); + $manager = $container->get('doctrine')->getManager(); $manager->persist($user); $manager->flush(); @@ -344,3 +365,26 @@ class AuthenticationTest extends ApiTestCase ``` Refer to [Testing the API](../distribution/testing.md) for more information about testing API Platform. + +### Improving Tests Suite Speed + +Since now we have a `JWT` authentication, functional tests require us to log in each time we want to test an API endpoint. This is where [Password Hashers](https://symfony.com/doc/current/security/passwords.html) come into play. + +Hashers are used for 2 reasons: + +1. To generate a hash for a raw password (`$container->get('security.user_password_hasher')->hashPassword($user, '$3CR3T')`) +2. To verify a password during authentication + +While hashing and verifying 1 password is quite a fast operation, doing it hundreds or even thousands of times in a tests suite becomes a bottleneck, because reliable hashing algorithms are slow by their nature. + +To significantly improve the test suite speed, we can use more simple password hasher specifically for the `test` environment. + +```yaml +# override in api/config/packages/test/security.yaml for test env +security: + password_hashers: + App\Entity\User: + algorithm: md5 + encode_as_base64: false + iterations: 0 +``` diff --git a/core/mercure.md b/core/mercure.md index 3cda83f2927..1238a64c5eb 100644 --- a/core/mercure.md +++ b/core/mercure.md @@ -4,7 +4,7 @@ API Platform can automatically push the modified version of the resources expose > *Mercure* is a protocol allowing to push data updates to web browsers and other HTTP clients in a convenient, fast, reliable and battery-efficient way. It is especially useful to publish real-time updates of resources served through web APIs, to reactive web and mobile apps. > -> — +> —[https://mercure.rocks](https://mercure.rocks) API Platform detects changes made to your Doctrine entities, and sends the updated resources to the Mercure hub. Then, the Mercure hub dispatches the updates to all connected clients using [Server-sent Events (SSE)](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events). diff --git a/core/messenger.md b/core/messenger.md index 6c9e475ff5d..36460835882 100644 --- a/core/messenger.md +++ b/core/messenger.md @@ -20,7 +20,7 @@ docker-compose exec php \ Set the `messenger` attribute to `true`, and API Platform will automatically dispatch the API Resource instance as a message using the message bus provided by the Messenger Component. The following example allows you to create a new `Person` in an asynchronous manner: -[codeSelector] + ```php Because the `messenger` attribute is `true`, when a `POST` is handled by API Platform, the corresponding instance of the `Person` will be dispatched. diff --git a/core/mongodb.md b/core/mongodb.md index 56c11d8da90..f03115499ad 100644 --- a/core/mongodb.md +++ b/core/mongodb.md @@ -170,7 +170,7 @@ use Symfony\Component\Validator\Constraints as Assert; /** * @ODM\Document */ -#[ApiResource(iri: "http://schema.org/Offer")] +#[ApiResource(iri: "https://schema.org/Offer")] class Offer { /** @@ -203,7 +203,7 @@ class Offer } ``` -When defining references, always use the id for storing them instead of the native [DBRef](https://docs.mongodb.com/manual/reference/database-references/#dbrefs). +When defining references, always use the ID for storing them instead of the native [DBRef](https://docs.mongodb.com/manual/reference/database-references/#dbrefs). It allows API Platform to manage [filtering on nested properties](filters.md#apifilter-annotation) by using [lookups](https://docs.mongodb.com/manual/reference/operator/aggregation/lookup/). ## Filtering diff --git a/core/openapi.md b/core/openapi.md index 005c3e054a3..75d98c58d29 100644 --- a/core/openapi.md +++ b/core/openapi.md @@ -109,7 +109,7 @@ The impact on the swagger-ui is the following: Sometimes you may want to change the information included in your OpenAPI documentation. The following configuration will give you total control over your OpenAPI definitions: -[codeSelector] + ```php "string", "format" => "date-time"] @@ -209,7 +201,7 @@ resources: ``` -[/codeSelector] + This will produce the following Swagger documentation: @@ -301,7 +293,7 @@ class User You also have full control over both built-in and custom operations documentation. -[codeSelector] + ```php ``` -[/codeSelector] + ![Impact on Swagger UI](../distribution/images/swagger-ui-2.png) @@ -444,7 +436,9 @@ Change `/docs` to the URI you wish Swagger to be accessible on. ## Using a custom Asset Package in Swagger UI -Sometimes you may want to use a different [Asset Package](https://symfony.com/doc/current/reference/configuration/framework.html#packages) for the Swagger UI. In this way you'll have more fine-grained control over the asset url generations. This is useful i.e. if you want to use different base path, base url or asset versioning strategy. +Sometimes you may want to use a different [Asset Package](https://symfony.com/doc/current/reference/configuration/framework.html#packages) for the Swagger UI. +In this way you'll have more fine-grained control over the asset URL generations. +This is useful i.e. if you want to use different base path, base URL or asset versioning strategy. Specify a custom asset package name: @@ -482,7 +476,7 @@ As described [in the Symfony documentation](https://symfony.com/doc/current/temp ``` -You may want to copy the [one shipped with API Platform](https://github.com/api-platform/core/blob/main/src/Bridge/Symfony/Bundle/Resources/views/SwaggerUi/index.html.twig) and customize it. +You may want to copy the [one shipped with API Platform](https://github.com/api-platform/core/blob/2.6/src/Bridge/Symfony/Bundle/Resources/views/SwaggerUi/index.html.twig) and customize it. ## Compatibility Layer with Amazon API Gateway @@ -490,7 +484,7 @@ You may want to copy the [one shipped with API Platform](https://github.com/api- API Platform provides a way to be compatible with Amazon API Gateway. To enable API Gateway compatibility on your OpenAPI docs, add `api_gateway=true` as query parameter: `http://www.example.com/docs.json?api_gateway=true`. -The flag `--api-gateway` is also available through the command line. +The flag `--api-gateway` is also available through the command-line. ## OAuth diff --git a/core/operations.md b/core/operations.md index aa66243e0ca..13d9f79a986 100644 --- a/core/operations.md +++ b/core/operations.md @@ -68,7 +68,7 @@ empty list of operations is provided, all operations are disabled. If the operation's name matches a supported HTTP methods (`GET`, `POST`, `PUT`, `PATCH` or `DELETE`), the corresponding `method` property will be automatically added. -[codeSelector] + ```php ``` -[/codeSelector] + The previous example can also be written with an explicit method definition: -[codeSelector] + ```php ``` -[/codeSelector] + API Platform Core is smart enough to automatically register the applicable Symfony route referencing a built-in CRUD action just by specifying the method name as key, or by checking the explicitly configured HTTP method. If you do not want to allow access to the resource item (i.e. you don't want a `GET` item operation), instead of omitting it altogether, you should instead declare a `GET` item operation which returns HTTP 404 (Not Found), so that the resource item can still be identified by an IRI. For example: -[codeSelector] + ```php ``` -[/codeSelector] + ## Configuring Operations @@ -255,7 +255,7 @@ The URL, the method and the default status code (among other options) can be con In the next example, both `GET` and `POST` operations are registered with custom URLs. Those will override the URLs generated by default. In addition to that, we require the `id` parameter in the URL of the `GET` operation to be an integer, and we configure the status code generated after successful `POST` request to be `301`: -[codeSelector] + ```php ``` -[/codeSelector] + In all these examples, the `method` attribute is omitted because it matches the operation name. +When specifying sub options, you must always use snake case as demonstrated below with the `denormalization_context` option on the `put` operation: + + + +```php + [ + 'denormalization_context' => [ + 'groups' => ['item:put'], + 'swagger_definition_name' => 'put', + ], + ], + 'delete', + ], + ], + denormalizationContext: [ + 'groups' => ['item:post'], + 'swagger_definition_name' => 'post', + ], +)] +class Book +{ + //... +} +``` + +```yaml +# api/config/api_platform/resources.yaml +App\Entity\Book: + itemOperations: + get: ~ + put: + denormalization_context: + groups: ['item:put'] + swagger_definition_name: 'put', + delete: ~ + denormalizationContext: + groups: ['item:post'] + swagger_definition_name: 'post' +``` + +```xml + + + + + + + + + + + item:put + + put + + + + + + + item:post + + post + + + +``` + + + ## Prefixing All Routes of All Operations Sometimes it's also useful to put a whole resource into its own "namespace" regarding the URI. Let's say you want to put everything that's related to a `Book` into the `library` so that URIs become `library/book/{id}`. In that case you don't need to override all the operations to set the path but configure the `route_prefix` attribute for the whole entity instead: -[codeSelector] + ```php ``` -[/codeSelector] + Alternatively, the more verbose attribute syntax can be used: `#[ApiResource(attributes: ["route_prefix" => "/library"])]`. @@ -415,31 +498,19 @@ namespace App\Entity; use Doctrine\ORM\Mapping as ORM; -/** - * @ORM\Entity - */ +#[ORM\Entity] class Place { - /** - * @ORM\Id - * @ORM\GeneratedValue - * @ORM\Column(type="integer") - */ + #[ORM\Id, ORM\Column, ORM\GeneratedValue] private ?int $id = null; - /** - * @ORM\Column - */ + #[ORM\Column] private string $name = ''; - /** - * @ORM\Column(type="float") - */ + #[ORM\Column(type: 'float')] private float $latitude = 0; - /** - * @ORM\Column(type="float") - */ + #[ORM\Column(type: 'float')] private float $longitude = 0; // ... @@ -475,9 +546,7 @@ use ApiPlatform\Core\Annotation\ApiResource; use App\Controller\GetWeather; use Doctrine\ORM\Mapping as ORM; -/** - * @ORM\Entity - */ +#[ORM\Entity] #[ApiResource( collectionOperations: [ 'get', diff --git a/core/pagination.md b/core/pagination.md index d7a6b705801..4dba8e2a4bf 100644 --- a/core/pagination.md +++ b/core/pagination.md @@ -20,7 +20,7 @@ is returned. It's a valid JSON(-LD) document containing items of the requested p "hydra:member": [ { "@id": "/books/1", - "@type": "http://schema.org/Book", + "@type": "https://schema.org/Book", "name": "My awesome book" }, { @@ -337,7 +337,7 @@ To know more about cursor-based pagination take a look at [this blog post on med ## Controlling The Behavior of The Doctrine ORM Paginator -The [PaginationExtension](https://github.com/api-platform/core/blob/main/src/Bridge/Doctrine/Orm/Extension/PaginationExtension.php) of API Platform performs some checks on the `QueryBuilder` to guess, in most common cases, the correct values to use when configuring the Doctrine ORM Paginator: +The [PaginationExtension](https://github.com/api-platform/core/blob/2.6/src/Bridge/Doctrine/Orm/Extension/PaginationExtension.php) of API Platform performs some checks on the `QueryBuilder` to guess, in most common cases, the correct values to use when configuring the Doctrine ORM Paginator: * `$fetchJoinCollection` argument: Whether there is a join to a collection-valued association. When set to `true`, the Doctrine ORM Paginator will perform an additional query, in order to get the correct number of results. diff --git a/core/performance.md b/core/performance.md index e52a0139ae3..5d74d685fe9 100644 --- a/core/performance.md +++ b/core/performance.md @@ -163,7 +163,7 @@ database driver. By default Doctrine comes with [lazy loading](https://www.doctrine-project.org/projects/doctrine-orm/en/latest/reference/working-with-objects.html#by-lazy-loading) - usually a killer time-saving feature but also a performance killer with large applications. Fortunately, Doctrine offers another approach to solve this problem: [eager loading](https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/working-with-objects.html#by-eager-loading). -This can easily be enabled for a relation: `@ORM\ManyToOne(fetch="EAGER")`. +This can easily be enabled for a relation: `#[ORM\ManyToOne(fetch: "EAGER")]`. By default in API Platform, we made the choice to force eager loading for all relations, with or without the Doctrine `fetch` attribute. Thanks to the eager loading [extension](extensions.md). The `EagerLoadingExtension` will join every @@ -231,9 +231,7 @@ namespace App\Entity; use ApiPlatform\Core\Annotation\ApiResource; use Doctrine\ORM\Mapping as ORM; -/** - * @ORM\Entity - */ +#[ORM\Entity] #[ApiResource] class Address { @@ -250,25 +248,18 @@ namespace App\Entity; use ApiPlatform\Core\Annotation\ApiResource; use Doctrine\ORM\Mapping as ORM; -/** - * @ORM\Entity - */ +#[ORM\Entity] #[ApiResource(forceEager: false)] class User { - /** - * @var Address - * - * @ORM\ManyToOne(targetEntity="Address", fetch="EAGER") - */ - public $address; + #[ORM\ManyToOne(fetch: 'EAGER')] + public Address $address; /** * @var Group[] - * - * @ORM\ManyToMany(targetEntity="Group", inversedBy="users") - * @ORM\JoinTable(name="users_groups") */ + #[ORM\ManyToMany(targetEntity: 'Group', inversedBy: 'users')] + #[ORM\JoinTable(name: 'users_groups')] public $groups; // ... @@ -284,9 +275,7 @@ namespace App\Entity; use ApiPlatform\Core\Annotation\ApiResource; use Doctrine\ORM\Mapping as ORM; -/** - * @ORM\Entity - */ +#[ORM\Entity] #[ApiResource( forceEager: false, itemOperations: [ @@ -302,9 +291,8 @@ class Group { /** * @var User[] - * - * @ManyToMany(targetEntity="User", mappedBy="groups") */ + #[ORM\ManyToMany(targetEntity: 'User', mappedBy: 'groups')] public $users; // ... diff --git a/core/push-relations.md b/core/push-relations.md index 0a702312d6f..9ed529713cc 100644 --- a/core/push-relations.md +++ b/core/push-relations.md @@ -7,7 +7,7 @@ API Platform leverages this capability by pushing relations of a resource to clients. **Note:** We strongly recommend using [Vulcain](https://vulcain.rocks) instead of this feature. -Vulcain is faster, cleaner, more flexible, and is supported out of the box in the API Platform distribution. +Vulcain is faster, cleaner, more flexible, and is supported out of the box in [the API Platform distribution](../distribution/index.md). ```php Security screencast
Watch the Security screencast
+

Security screencast
Watch the Security screencast

-[codeSelector] + ```php "is_granted('ROLE_USER')"], collectionOperations: [ @@ -34,29 +34,15 @@ use Symfony\Component\Validator\Constraints as Assert; )] class Book { - /** - * @var int - * - * @ORM\Column(type="integer") - * @ORM\Id - * @ORM\GeneratedValue(strategy="AUTO") - */ - private $id; + #[ORM\Id, ORM\Column, ORM\GeneratedValue] + private ?int $id = null; - /** - * @var string The title - * - * @ORM\Column - */ + #[ORM\Column] #[Assert\NotBlank] - public $title; + public string $title; - /** - * @var User The owner - * - * @ORM\ManyToOne(targetEntity=User::class) - */ - public $owner; + #[ORM\ManyToOne] + public User $owner; // ... } @@ -77,11 +63,11 @@ App\Entity\Book: security: 'is_granted("ROLE_ADMIN") or object.owner == user' ``` -[/codeSelector] + Resource signature can be modified at the property level as well: -[codeSelector] + ```php class Book @@ -105,7 +91,7 @@ App\Entity\Book: security: 'is_granted("ROLE_ADMIN")' ``` -[/codeSelector] + In this example: @@ -117,8 +103,7 @@ In this example: Available variables are: * `user`: the current logged in object, if any -* `object`: the current resource, or collection of resources for collection operations -* `request` (only at the resource level): the current request +* `object`: the current resource class during denormalization, the current resource during normalization, or collection of resources for collection operations Access control checks in the `security` attribute are always executed before the [denormalization step](serialization.md). It means than for `PUT` or `PATCH` requests, `object` doesn't contain the value submitted by the user, but values currently stored in [the persistence layer](data-persisters.md). @@ -128,7 +113,7 @@ It means than for `PUT` or `PATCH` requests, `object` doesn't contain the value In some cases, it might be useful to execute a security after the denormalization step. To do so, use the `security_post_denormalize` attribute: -[codeSelector] + ```php This time, the `object` variable contains data that have been extracted from the HTTP request body during the denormalization process. However, the object is not persisted yet. @@ -179,7 +164,7 @@ In order to give the current `object` to your voter, use the expression `is_gran For example: -[codeSelector] + ```php Please note that if you use both `attributes={"security"="..` and then `"post" = { "security_post_denormalize" = "...`, the `security` on top level is called first, and after `security_post_denormalize`. This could lead to unwanted behaviour, so avoid using both of them simultaneously. If you need to use `security_post_denormalize`, consider adding `security` for the other operations instead of the global one. @@ -295,7 +280,7 @@ You can change it by configuring the `security_message` attribute or the `securi For example: -[codeSelector] + ```php ## Filtering Collection According to the Current User Permissions diff --git a/core/serialization.md b/core/serialization.md index e7c4f1a5f37..9333fa0a9e3 100644 --- a/core/serialization.md +++ b/core/serialization.md @@ -74,7 +74,7 @@ It is simple to specify what groups to use in the API system: 1. Add the normalization context and denormalization context attributes to the resource, and specify which groups to use. Here you see that we add `read` and `write`, respectively. You can use any group names you wish. 2. Apply the groups to properties in the object. -[codeSelector] + ```php Alternatively, you can use the more verbose syntax: @@ -158,7 +158,7 @@ level ignored. In the following example we use different serialization groups for the `GET` and `PUT` operations: -[codeSelector] + ```php The `name` and `author` properties will be included in the document generated during a `GET` operation because the configuration defined at the resource level is inherited. However the document generated when a `PUT` request will be received will only @@ -247,7 +247,7 @@ response through the use of serialization groups. By using the following seriali a JSON representation of the author is embedded in the book response. As soon as any of the author's attributes is in the `book` group, the author will be embedded. -[codeSelector] + ```php -[codeSelector] + ```php The generated JSON using previous settings is below: @@ -347,7 +347,7 @@ Instead of embedding relations in the main HTTP response, you may want [to "push It is also possible to embed a relation in `PUT`, `PATCH` and `POST` requests. To enable that feature, set the serialization groups the same way as normalization. For example: -[codeSelector] + ```php The following rules apply when denormalizing embedded relations: @@ -386,7 +386,7 @@ You can specify as many embedded relation levels as you want. It is a common problem to have entities that reference other entities of the same type: -[codeSelector] + ```php The problem here is that the **$parent** property become automatically an embedded object. Besides, the property won't be shown on the OpenAPI view. To force the **$parent** property to be used as an IRI, add an **#[ApiProperty(readableLink: false, writableLink: false)]** annotation: -[codeSelector] + ```php ## Property Normalization Context @@ -524,15 +521,11 @@ use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Serializer\Annotation\Context; use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer; -/** - * @ORM\Entity - */ +#[ORM\Entity] #[ApiResource] class Book { - /** - * @ORM\Column(type="date") - */ + #[ORM\Column] #[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])] public ?\DateTimeInterface $publicationDate = null; } @@ -544,7 +537,7 @@ In the above example, you will receive the book's data like this: { "@context": "/contexts/Book", "@id": "/books/3", - "@type": "http://schema.org/Book", + "@type": "https://schema.org/Book", "publicationDate": "1989-06-16" } ``` @@ -564,15 +557,11 @@ use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Serializer\Annotation\Context; use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer; -/** - * @ORM\Entity - */ +#[ORM\Entity] #[ApiResource] class Book { - /** - * @ORM\Column(type="date") - */ + #[ORM\Column] #[Context(normalizationContext: [DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])] public ?\DateTimeInterface $publicationDate = null; } @@ -594,15 +583,11 @@ use Symfony\Component\Serializer\Annotation\Context; use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer; -/** - * @ORM\Entity - */ +#[ORM\Entity] #[ApiResource] class Book { - /** - * @ORM\Column(type="date") - */ + #[ORM\Column] #[Groups(["extended"])] #[Context([DateTimeNormalizer::FORMAT_KEY => \DateTime::RFC3339])] #[Context( @@ -617,7 +602,7 @@ class Book Sometimes you need to expose calculated fields. This can be done by leveraging the groups. This time not on a property, but on a method. -[codeSelector] + ```php ['normalization_context' => ['groups' => 'greeting:collection:get']], @@ -640,27 +623,17 @@ use Symfony\Component\Serializer\Annotation\Groups; )] class Greeting { - /** - * @var int The entity Id - * - * @ORM\Id - * @ORM\GeneratedValue - * @ORM\Column(type="integer") - */ + #[ORM\Id, ORM\Column, ORM\GeneratedValue] #[Groups("greeting:collection:get")] - private $id; + private ?int $id = null; private $a = 1; private $b = 2; - /** - * @var string A nice person - * - * @ORM\Column - */ + #[ORM\Column] #[Groups("greeting:collection:get")] - public $name = ''; + public string $name = ''; public function getId(): int { @@ -690,11 +663,11 @@ App\Entity\Greeting: groups: 'greeting:collection:get' name: groups: 'greeting:collection:get' - getSum: + sum: groups: 'greeting:collection:get' ``` -[/codeSelector] + ## Changing the Serialization Context Dynamically @@ -702,7 +675,7 @@ App\Entity\Greeting: Let's imagine a resource where most fields can be managed by any user, but some can be managed only by admin users: -[codeSelector] + ```php All entry points are the same for all users, so we should find a way to detect if the authenticated user is an admin, and if so dynamically add the `admin:input` value to deserialization groups in the `$context` array. @@ -947,7 +916,7 @@ Note: this normalizer will work only for JSON-LD format, if you want to process ```php decorated->supportsDenormalization($data, $type, $format); } - public function denormalize($data, $class, $format = null, array $context = []) + public function denormalize($data, string $type, string $format = null, array $context = []) { - return $this->decorated->denormalize($data, $class, $format, $context); + return $this->decorated->denormalize($data, $type, $format, $context); } public function setSerializer(SerializerInterface $serializer) @@ -1022,17 +991,13 @@ class Book /** * This field can be managed only by an admin - * - * @var bool */ - public $active = false; + public bool $active = false; /** * This field can be managed by any user - * - * @var string */ - public $name; + public string $name; // ... } @@ -1075,7 +1040,7 @@ an IRI. A client that uses JSON-LD must send a second HTTP request to retrieve i You can configure API Platform to embed the JSON-LD context in the root document by adding the `jsonld_embed_context` attribute to the `#[ApiResource]` annotation: -[codeSelector] + ```php The JSON output will now include the embedded context: @@ -1109,8 +1074,8 @@ The JSON output will now include the embedded context: "@context": { "@vocab": "http://localhost:8000/apidoc#", "hydra": "http://www.w3.org/ns/hydra/core#", - "name": "http://schema.org/name", - "author": "http://schema.org/author" + "name": "https://schema.org/name", + "author": "https://schema.org/author" }, "@id": "/books/62", "@type": "Book", @@ -1133,27 +1098,17 @@ use ApiPlatform\Core\Annotation\ApiResource; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\ORM\Mapping as ORM; -/** - * @ORM\Entity - */ +#[ORM\Entity] #[ApiResource] final class Brand { - /** - * @ORM\Id - * @ORM\Column(type="integer") - * @ORM\GeneratedValue(strategy="AUTO") - */ - private $id; + #[ORM\Id, ORM\Column, ORM\GeneratedValue] + private ?int $id = null; - /** - * @ORM\ManyToMany(targetEntity="App\Entity\Car", inversedBy="brands") - * @ORM\JoinTable( - * name="CarToBrand", - * joinColumns={@ORM\JoinColumn(name="brand_id", referencedColumnName="id", nullable=false)}, - * inverseJoinColumns={@ORM\JoinColumn(name="car_id", referencedColumnName="id", nullable=false)} - * ) - */ + #[ORM\ManyToMany(targetEntity: Car::class, inversedBy: 'brands')] + #[ORM\JoinTable(name: 'CarToBrand')] + #[ORM\JoinColumn(name: 'brand_id', referencedColumnName: 'id', nullable: false)] + #[ORM\InverseJoinColumn(name: 'car_id', referencedColumnName: 'id', nullable: false)] private $cars; public function __construct() diff --git a/core/subresources.md b/core/subresources.md index 6743fa3e7f0..4834d86380e 100644 --- a/core/subresources.md +++ b/core/subresources.md @@ -9,7 +9,7 @@ The starting point of a subresource must be a relation on an existing resource. For example, let's create two entities (Question, Answer) and set up a subresource so that `/question/42/answer` gives us the answer to the question 42: -[codeSelector] + ```php Note that all we had to do is to set up `#[ApiSubresource]` on the `Question::answer` relation. Because the `answer` is a to-one relation, we know that this subresource is an item. Therefore the response will look like this: @@ -129,7 +109,7 @@ Note: only for `GET` operations are supported at the moment You may want custom groups on subresources, you can set `normalization_context` or `denormalization_context` on that operation. To do so, add a `subresourceOperations` node. For example: -[codeSelector] + ```php ``` -[/codeSelector] + In the previous examples, the `method` attribute is mandatory, because the operation name doesn't match a supported HTTP method. @@ -198,7 +178,7 @@ may use `bin/console debug:router`. ## Using Custom Paths -You can control the path of subresources with the `path` option of the `subresourceOperations` parameter: +You can control the path of subresources with the `path` option of the `subresourceOperations` parameter. ```php [ + 'answer_get_subresource' => [ 'method' => 'GET', 'path' => '/questions/{id}/all-answers', ], diff --git a/core/url-generation-strategy.md b/core/url-generation-strategy.md index ed164738194..fedb9780a97 100644 --- a/core/url-generation-strategy.md +++ b/core/url-generation-strategy.md @@ -12,7 +12,7 @@ For instance, in JSON-LD, you will get a collection like this: "hydra:member": [ { "@id": "/books/1", - "@type": "http://schema.org/Book", + "@type": "https://schema.org/Book", "name": "My awesome book" } ], @@ -33,7 +33,7 @@ api_platform: It can also be configured only for a specific resource: -[codeSelector] + ```php ``` -[/codeSelector] + For the above configuration, the collection will be like this: @@ -83,7 +83,7 @@ For the above configuration, the collection will be like this: "hydra:member": [ { "@id": "http://example.com/books/1", - "@type": "http://schema.org/Book", + "@type": "https://schema.org/Book", "name": "My awesome book" } ], diff --git a/core/validation.md b/core/validation.md index a7046eba3d8..73c9301b5ee 100644 --- a/core/validation.md +++ b/core/validation.md @@ -26,34 +26,24 @@ use Symfony\Component\Validator\Constraints as Assert; // Symfony's built-in con /** * A product. * - * @ORM\Entity */ +#[ORM\Entity] #[ApiResource] class Product { - /** - * @var int The id of this product. - * - * @ORM\Id - * @ORM\GeneratedValue - * @ORM\Column(type="integer") - */ - private $id; + #[ORM\Id, ORM\Column, ORM\GeneratedValue] + private ?int $id = null; - /** - * @var string The name of the product - * - * @Assert\NotBlank - * @ORM\Column - */ - public $name; + #[ORM\Column] + #[Assert\NotBlank] + public string $name; /** * @var string[] Describe the product * * @MinimalProperties - * @ORM\Column(type="json") */ + #[ORM\Column(type: 'json')] public $properties; // Getters and setters... @@ -141,15 +131,11 @@ use Symfony\Component\Validator\Constraints as Assert; #[ApiResource(attributes: ['validation_groups' => ['a', 'b']])] class Book { - /** - * @Assert\NotBlank(groups={"a"}) - */ - public $name; + #[Assert\NotBlank(groups: ['a'])] + public string $name; - /** - * @Assert\NotNull(groups={"b"}) - */ - public $author; + #[Assert\NotNull(groups: ['b'])] + public string $author; // ... } @@ -189,29 +175,15 @@ use Symfony\Component\Validator\Constraints as Assert; )] class Book { - /** - * @Assert\Uuid - */ + #[Assert\Uuid] private $id; - /** - * @Assert\NotBlank(groups={"postValidation"}) - */ + #[Assert\NotBlank(groups: ['postValidation'])] public $name; - /** - * @Assert\NotNull - * @Assert\Length( - * min = 2, - * max = 50, - * groups={"postValidation"} - * ) - * @Assert\Length( - * min = 2, - * max = 70, - * groups={"putValidation"} - * ) - */ + #[Assert\NotNull] + #[Assert\Length(min: 2, max: 50, groups: ['postValidation'])] + #[Assert\Length(min: 2, max: 70, groups: ['putValidation'])] public $author; // ... @@ -258,14 +230,10 @@ class Book return ['a']; } - /** - * @Assert\NotBlank(groups={"a"}) - */ + #[Assert\NotBlank(groups: ['a'])] public $name; - /** - * @Assert\NotNull(groups={"b"}) - */ + #[Assert\NotNull(groups: ['b'])] public $author; // ... @@ -324,14 +292,10 @@ use Symfony\Component\Validator\Constraints as Assert; #[ApiResource(attributes: ['validation_groups' => AdminGroupsGenerator::class]) class Book { - /** - * @Assert\NotBlank(groups={"a"}) - */ + #[Assert\NotBlank(groups: ['a'])] public $name; - /** - * @Assert\NotNull(groups={"b"}) - */ + #[Assert\NotNull(groups: ['b'])] public $author; // ... @@ -383,9 +347,7 @@ use App\Validator\Two; // classic custom constraint use App\Validator\MySequencedGroup; // the sequence group to use use Doctrine\ORM\Mapping as ORM; -/** - * @ORM\Entity - */ +#[ORM\Entity] #[ApiResource( collectionOperations: [ 'post' => [ @@ -395,25 +357,18 @@ use Doctrine\ORM\Mapping as ORM; )] class Greeting { - /** - * @var int The entity Id - * - * @ORM\Id - * @ORM\GeneratedValue - * @ORM\Column(type="integer") - */ - private $id; + #[ORM\Id, ORM\Column, ORM\GeneratedValue] + private ?int $id = null; /** - * @var string A nice person - * - * @ORM\Column + * @var A nice person * * I want this "second" validation to be executed after the "first" one even though I wrote them in this order. * @One(groups={"second"}) * @Two(groups={"first"}) */ - public $name = ''; + #[ORM\Column] + public string $name = ''; public function getId(): int { @@ -422,6 +377,89 @@ class Greeting } ``` +## Validating Delete Operations + +By default, validation rules that are specified on the API resource are not evaluated during DELETE operations. You need to trigger the validation in your code, if needed. + +Assume that you have the following entity that uses a custom delete validator: + +```php + [ + 'validation_groups' => ['deleteValidation'] + ] + ] +)] +#[AssertCanDelete(groups: ['deleteValidation'])] +class MyEntity +{ + #[ORM\Id, ORM\Column, ORM\GeneratedValue] + private ?int $id = null; + + #[ORM\Column] + public string $name = ''; +} +``` + +Create a data persister, which decorates the default data persister, where you will trigger the validation: + +```php +decoratedDoctrineDataPersister->persist($data); + } + + public function remove($data): void { + $this->validator->validate( + $data, + ['groups' => ['deleteValidation']] + ); + $this->decoratedDoctrineDataPersister->remove($data); + } + + public function supports($data): bool { + return $data instanceof MyEntity; + } +} +``` + +Register the new data persister in `api/config/services.yaml`: + +```yaml +# api/config/services.yaml +services: + _defaults: + bind: + $decoratedDoctrineDataPersister: '@api_platform.doctrine.orm.data_persister' +``` + ## Error Levels and Payload Serialization As stated in the [Symfony documentation](https://symfony.com/doc/current/validation/severity.html), you can use the payload field to define error levels. @@ -481,9 +519,7 @@ final class Brand $this->cars = new ArrayCollection(); } - /** - * @Assert\Valid - */ + #[Assert\Valid] public function getCars() { return $this->cars->getValues(); @@ -499,20 +535,20 @@ The following validation constraints are covered: Constraints | Vocabulary | --------------------------------------------------------------------------------------|-----------------------------------| -[`Url`](https://symfony.com/doc/current/reference/constraints/Url.html) | `http://schema.org/url` | -[`Email`](https://symfony.com/doc/current/reference/constraints/Email.html) | `http://schema.org/email` | -[`Uuid`](https://symfony.com/doc/current/reference/constraints/Uuid.html) | `http://schema.org/identifier` | -[`CardScheme`](https://symfony.com/doc/current/reference/constraints/CardScheme.html) | `http://schema.org/identifier` | -[`Bic`](https://symfony.com/doc/current/reference/constraints/Bic.html) | `http://schema.org/identifier` | -[`Iban`](https://symfony.com/doc/current/reference/constraints/Iban.html) | `http://schema.org/identifier` | -[`Date`](https://symfony.com/doc/current/reference/constraints/Date.html) | `http://schema.org/Date` | -[`DateTime`](https://symfony.com/doc/current/reference/constraints/DateTime.html) | `http://schema.org/DateTime` | -[`Time`](https://symfony.com/doc/current/reference/constraints/Time.html) | `http://schema.org/Time` | -[`Image`](https://symfony.com/doc/current/reference/constraints/Image.html) | `http://schema.org/image` | -[`File`](https://symfony.com/doc/current/reference/constraints/File.html) | `http://schema.org/MediaObject` | -[`Currency`](https://symfony.com/doc/current/reference/constraints/Currency.html) | `http://schema.org/priceCurrency` | -[`Isbn`](https://symfony.com/doc/current/reference/constraints/Isbn.html) | `http://schema.org/isbn` | -[`Issn`](https://symfony.com/doc/current/reference/constraints/Issn.html) | `http://schema.org/issn` | +[`Url`](https://symfony.com/doc/current/reference/constraints/Url.html) | `https://schema.org/url` | +[`Email`](https://symfony.com/doc/current/reference/constraints/Email.html) | `https://schema.org/email` | +[`Uuid`](https://symfony.com/doc/current/reference/constraints/Uuid.html) | `https://schema.org/identifier` | +[`CardScheme`](https://symfony.com/doc/current/reference/constraints/CardScheme.html) | `https://schema.org/identifier` | +[`Bic`](https://symfony.com/doc/current/reference/constraints/Bic.html) | `https://schema.org/identifier` | +[`Iban`](https://symfony.com/doc/current/reference/constraints/Iban.html) | `https://schema.org/identifier` | +[`Date`](https://symfony.com/doc/current/reference/constraints/Date.html) | `https://schema.org/Date` | +[`DateTime`](https://symfony.com/doc/current/reference/constraints/DateTime.html) | `https://schema.org/DateTime` | +[`Time`](https://symfony.com/doc/current/reference/constraints/Time.html) | `https://schema.org/Time` | +[`Image`](https://symfony.com/doc/current/reference/constraints/Image.html) | `https://schema.org/image` | +[`File`](https://symfony.com/doc/current/reference/constraints/File.html) | `https://schema.org/MediaObject` | +[`Currency`](https://symfony.com/doc/current/reference/constraints/Currency.html) | `https://schema.org/priceCurrency` | +[`Isbn`](https://symfony.com/doc/current/reference/constraints/Isbn.html) | `https://schema.org/isbn` | +[`Issn`](https://symfony.com/doc/current/reference/constraints/Issn.html) | `https://schema.org/issn` | ## Specification property restrictions diff --git a/deployment/kubernetes.md b/deployment/kubernetes.md index fd560e687c3..31fca52dd2a 100644 --- a/deployment/kubernetes.md +++ b/deployment/kubernetes.md @@ -29,29 +29,37 @@ Change the name "test-api-platform" to your Google project ID (not the project n [Quickstart Google Cloud](https://cloud.google.com/sdk/docs/quickstart?hl=de) If you do not have gcloud yet, install it with these command. - curl https://sdk.cloud.google.com | bash +```console +curl https://sdk.cloud.google.com | bash +``` #### 1. Build the PHP and Caddy Docker images and tag them Versioning: The 0.1.0 is the version. This value should be the same as the attribute `appVersion` in `Chart.yaml`. Infos for [Google Container pulling and pushing](https://cloud.google.com/container-registry/docs/pushing-and-pulling) - docker build -t gcr.io/test-api-platform/php:0.1.0 -t gcr.io/test-api-platform/php:latest api --target api_platform_php - docker build -t gcr.io/test-api-platform/caddy:0.1.0 -t gcr.io/test-api-platform/caddy:latest api --target api_platform_caddy - docker build -t gcr.io/test-api-platform/pwa:0.1.0 -t gcr.io/test-api-platform/pwa:latest pwa --target api_platform_pwa_prod +```console +docker build -t gcr.io/test-api-platform/php:0.1.0 -t gcr.io/test-api-platform/php:latest api --target api_platform_php +docker build -t gcr.io/test-api-platform/caddy:0.1.0 -t gcr.io/test-api-platform/caddy:latest api --target api_platform_caddy +docker build -t gcr.io/test-api-platform/pwa:0.1.0 -t gcr.io/test-api-platform/pwa:latest pwa --target api_platform_pwa_prod +``` #### 2. Push your images to your Docker registry - gcloud auth configure-docker - docker push gcr.io/test-api-platform/php - docker push gcr.io/test-api-platform/caddy - docker push gcr.io/test-api-platform/pwa +```console +gcloud auth configure-docker +docker push gcr.io/test-api-platform/php +docker push gcr.io/test-api-platform/caddy +docker push gcr.io/test-api-platform/pwa +``` Optional push the version images: - docker push gcr.io/test-api-platform/php:0.1.0 - docker push gcr.io/test-api-platform/caddy:0.1.0 - docker push gcr.io/test-api-platform/pwa:0.1.0 +```console +docker push gcr.io/test-api-platform/php:0.1.0 +docker push gcr.io/test-api-platform/caddy:0.1.0 +docker push gcr.io/test-api-platform/pwa:0.1.0 +``` The result should look similar to these images. @@ -62,35 +70,43 @@ The result should look similar to these images. ### 1. Check the Helm version - helm version +```console +helm version +``` If you are using version 2.x follow this [guide to migrate Helm to v3](https://helm.sh/docs/topics/v2_v3_migration/#helm) ### 2. Firstly you need to update helm dependencies by running - helm dependency update ./helm/api-platform +```console +helm dependency update ./helm/api-platform +``` This will create a folder helm/api-platform/charts/ and add all dependencies there. Actual this is [bitnami/postgresql](https://bitnami.com/stack/postgresql/helm), a file postgresql-[VERSION].tgz is created. ### 3. Optional: If you made changes to the Helm chart, check if its format is correct - helm lint ./helm/api-platform +```console +helm lint ./helm/api-platform +``` ### 4. Deploy your API to the container - helm upgrade main ./helm/api-platform --namespace=default --create-namespace --wait \ - --install \ - --set "php.image.repository=gcr.io/test-api-platform/php" \ - --set php.image.tag=latest \ - --set "caddy.image.repository=gcr.io/test-api-platform/caddy" \ - --set caddy.image.tag=latest \ - --set "pwa.image.repository=gcr.io/test-api-platform/pwa" \ - --set pwa.image.tag=latest \ - --set php.appSecret='!ChangeMe!' \ - --set postgresql.postgresqlPassword='!ChangeMe!' \ - --set postgresql.persistence.enabled=true \ - --set "corsAllowOrigin=^https?:\/\/[a-z]*\.mywebsite.com$" +```console +helm upgrade main ./helm/api-platform --namespace=default --create-namespace --wait \ + --install \ + --set "php.image.repository=gcr.io/test-api-platform/php" \ + --set php.image.tag=latest \ + --set "caddy.image.repository=gcr.io/test-api-platform/caddy" \ + --set caddy.image.tag=latest \ + --set "pwa.image.repository=gcr.io/test-api-platform/pwa" \ + --set pwa.image.tag=latest \ + --set php.appSecret='!ChangeMe!' \ + --set postgresql.postgresqlPassword='!ChangeMe!' \ + --set postgresql.persistence.enabled=true \ + --set "corsAllowOrigin=^https?:\/\/[a-z]*\.mywebsite.com$" +``` The `"` are necessary for Windows. Use ^ on Windows instead of \ to split commands into multiple lines. You can add the parameter `--dry-run` to check upfront if anything is correct. @@ -109,21 +125,25 @@ get access on your local machine to the deploy. See image below. If you prefer to use a managed DBMS like [Heroku Postgres](https://www.heroku.com/postgres) or [Google Cloud SQL](https://cloud.google.com/sql/docs/postgres/) (recommended): - helm upgrade api-platform ./helm/api-platform \ - # ... - --set postgresql.enabled=false \ - --set postgresql.url=pgsql://username:password@host/database?serverVersion=13 +```console +helm upgrade api-platform ./helm/api-platform \ + # ... + --set postgresql.enabled=false \ + --set postgresql.url=pgsql://username:password@host/database?serverVersion=13 +``` Finally, build the `pwa` (client and admin) JavaScript apps and [deploy them on a static -website hosting service](https://create-react-app.dev/docs/deployment/). +site hosting service](https://create-react-app.dev/docs/deployment/). ## Access the container You can access the php container of the pod with the following command. In this example the symfony console is called. - CADDY_PHP_POD=$(kubectl --namespace=default get pods -l app.kubernetes.io/name=api-platform -o jsonpath="{.items[0].metadata.name}") - kubectl --namespace=default exec -it $CADDY_PHP_POD -c api-platform-php -- bin/console +```console +CADDY_PHP_POD=$(kubectl --namespace=default get pods -l app.kubernetes.io/name=api-platform -o jsonpath="{.items[0].metadata.name}") +kubectl --namespace=default exec -it $CADDY_PHP_POD -c api-platform-php -- bin/console +``` ## Caution for system architecture @@ -132,7 +152,7 @@ there is probably a problem with the system architecture. `standard_init_linux.go:211: exec user process caused "exec format error` Build the images with the same system architecture as the cluster runs. Example: Building with Mac M1 with arm64 leads to problems. Most cluster will run with x86_64. -Solution: +Solution: [https://blog.jaimyn.dev/how-to-build-multi-architecture-docker-images-on-an-m1-mac](https://blog.jaimyn.dev/how-to-build-multi-architecture-docker-images-on-an-m1-mac) ## Updates @@ -148,7 +168,10 @@ You can upgrade with the same command from the installation and pass all paramet Infos about [best practices for tagging images for kubernetes](https://kubernetes.io/docs/concepts/containers/images/) You have to use the *.image.pullPolicy=Always see the last 3 parameters. - helm upgrade api-platform ./helm/api-platform --namespace=default \ +```console +PHP_POD=$(kubectl --namespace=bar get pods -l app=php -o jsonpath="{.items[0].metadata.name}") +kubectl --namespace=bar exec -it $PHP_POD -- bin/console doctrine:schema:create +helm upgrade api-platform ./helm/api-platform --namespace=default \ --set "php.image.repository=gcr.io/test-api-platform/php" \ --set php.image.tag=latest \ --set "caddy.image.repository=gcr.io/test-api-platform/caddy" \ @@ -162,10 +185,11 @@ You have to use the *.image.pullPolicy=Always see the last 3 parameters. --set php.image.pullPolicy=Always \ --set caddy.image.pullPolicy=Always \ --set pwa.image.pullPolicy=Always +``` ## GitHub Actions Example for deployment -You can find a [complete deploy command for GKE](https://github.com/api-platform/demo/blob/main/.github/workflows/deploy.yml) on the [demo project](https://github.com/api-platform/demo/): +You can find a [complete deploy command for GKE](https://github.com/api-platform/demo/blob/main/.github/workflows/cd.yml) on the [demo project](https://github.com/api-platform/demo/): ## Symfony Messenger @@ -175,24 +199,30 @@ Start by creating a new template for the queue-worker-deployment. The `deploymen Add the following lines under `containers` to overwrite the command. - command: - {{ range .Values.queue_worker.command }} - - {{ . | quote }} - {{ end }} - args: - {{ range .Values.queue_worker.commandArgs }} - - {{ . | quote }} - {{ end }} +```yaml +command: +{{ range .Values.queue_worker.command }} + - {{ . | quote }} +{{ end }} +args: +{{ range .Values.queue_worker.commandArgs }} + - {{ . | quote }} +{{ end }} +``` Here is an example on how to use it from your `values.yaml`: - command: ['bin/console'] - commandArgs: ['messenger:consume', 'async', '--memory-limit=100M'] +```yaml +command: ['bin/console'] +commandArgs: ['messenger:consume', 'async', '--memory-limit=100M'] +``` The `readinessProbe` and the `livenessProble` can not use the default `docker-healthcheck` but should test if the command is running. - readinessProbe: - exec: - command: ["/bin/sh", "-c", "/bin/ps -ef | grep messenger:consume | grep -v grep"] - initialDelaySeconds: 120 - periodSeconds: 3 +```yaml +readinessProbe: + exec: + command: ["/bin/sh", "-c", "/bin/ps -ef | grep messenger:consume | grep -v grep"] + initialDelaySeconds: 120 + periodSeconds: 3 +``` diff --git a/deployment/traefik.md b/deployment/traefik.md index 18d5df85f12..f7f625fe3a4 100644 --- a/deployment/traefik.md +++ b/deployment/traefik.md @@ -2,7 +2,7 @@ > An open-source reverse proxy and load balancer for HTTP and TCP-based applications that is easy, dynamic, automatic, fast, full-featured, production proven, provides metrics and integrates with every major cluster technology. > -> — +> —[https://traefik.io](https://traefik.io) ## Basic Implementation diff --git a/distribution/caddy.md b/distribution/caddy.md new file mode 100644 index 00000000000..b6fa7a22f69 --- /dev/null +++ b/distribution/caddy.md @@ -0,0 +1,35 @@ +# Configuring the Caddy Web Server + +[The API Platform distribution](index.md) is shipped with [the Caddy web server](https://caddyserver.com). +The build contains the [Mercure](../core/mercure.md) and the [Vulcain](https://vulcain.rocks) Caddy modules. + +Caddy is positioned in front of the web API and of the Progressive Web App. +It routes requests to either service depending on the value of the `Accept` HTTP header or the extension +of the requested file. + +Using the same domain to serve the API and the PWA [improves performance by preventing unnecessary CORS preflight requests +and encourages embracing the REST principles](https://dunglas.fr/2022/01/preventing-cors-preflight-requests-using-content-negotiation/). + +By default, requests having an `Accept` request header containing the `text/html` media type are routed to the Next.js application, +except for some paths known to be resources served by the API (e.g. the Swagger UI documentation, static files provided by bundles...). +Other requests are routed to the API. + +Sometimes, you may want to let the PHP application generate HTML responses. +For instance, when you create your own Symfony controllers serving HTML pages, +or when using bundles such as EasyAdmin or SonataAdmin. + +To do so, you have to tweak the rules used to route the requests. +Open `api-platform/api/docker/caddy/Caddyfile` and modify the expression. +You can use [any CEL (Common Expression Language) expression](https://caddyserver.com/docs/caddyfile/matchers#expression) supported by Caddy. + +For instance, if you want to route all requests to a path starting with `/admin` to the API, modify the existing expression like this: + +```patch +# Matches requests for HTML documents, for static files and for Next.js files, +# except for known API paths and paths with extensions handled by API Platform +@pwa expression `( + {header.Accept}.matches("\\btext/html\\b") +- && !{path}.matches("(?i)(?:^/docs|^/graphql|^/bundles/|^/_profiler|^/_wdt|\\.(?:json|html$|csv$|ya?ml$|xml$))") ++ && !{path}.matches("(?i)(?:^/admin|^/docs|^/graphql|^/bundles/|^/_profiler|^/_wdt|\\.(?:json|html$|csv$|ya?ml$|xml$))") + )` +``` diff --git a/distribution/debugging.md b/distribution/debugging.md index aa5529803a1..ee887d30932 100644 --- a/distribution/debugging.md +++ b/distribution/debugging.md @@ -17,7 +17,7 @@ it's recommended to add a custom stage to the end of the `api/Dockerfile`. # api/Dockerfile FROM api_platform_php as api_platform_php_dev -ARG XDEBUG_VERSION=3.0.2 +ARG XDEBUG_VERSION=3.1.3 RUN set -eux; \ apk add --no-cache --virtual .build-deps $PHPIZE_DEPS; \ pecl install xdebug-$XDEBUG_VERSION; \ @@ -94,6 +94,6 @@ $ docker-compose exec php \ php --version PHP … - with Xdebug v3.0.2, Copyright (c) 2002-2021, by Derick Rethans + with Xdebug v3.1.3, Copyright (c) 2002-2021, by Derick Rethans … ``` diff --git a/distribution/index.md b/distribution/index.md index 32c2ee4be61..d81eab290e1 100644 --- a/distribution/index.md +++ b/distribution/index.md @@ -10,7 +10,7 @@ ## Introduction -API Platform contains [a **PHP** library (Core)](../core/index.md) to create fully featured hypermedia (or [GraphQL](../core/graphql.md)) web APIs supporting industry-leading standards: [JSON-LD](https://json-ld.org/) with [Hydra](https://www.hydra-cg.com/), [OpenAPI](../core/swagger.md)... +API Platform contains [a **PHP** library (Core)](../core/index.md) to create fully featured hypermedia (or [GraphQL](../core/graphql.md)) web APIs supporting industry-leading standards: [JSON-LD](https://json-ld.org) with [Hydra](https://www.hydra-cg.com), [OpenAPI](../core/swagger.md)... API Platform also provides ambitious **JavaScript** tools to create web and mobile applications based on the most popular frontend technologies in a snap. These tools parse the documentation of the API (or of any other API supporting Hydra or OpenAPI). @@ -43,7 +43,7 @@ API Platform uses these model classes to expose and document a web API having a * hypermedia/[HATEOAS](https://en.wikipedia.org/wiki/HATEOAS) and content negotiation support ([JSON-LD](https://json-ld.org) and [Hydra](https://www.hydra-cg.com/), [JSON:API](https://jsonapi.org/), [HAL](https://tools.ietf.org/html/draft-kelly-json-hal-08)...) * [GraphQL support](../core/graphql.md) * Nice UI and machine-readable documentations ([Swagger UI/OpenAPI](https://swagger.io), [GraphiQL](https://github.com/graphql/graphiql)...) -* authentication ([Basic HTTP](https://en.wikipedia.org/wiki/Basic_access_authentication), cookies as well as [JWT](https://jwt.io/) and [OAuth](https://oauth.net/) through extensions) +* authentication ([Basic HTTP](https://en.wikipedia.org/wiki/Basic_access_authentication), cookies as well as [JWT](../core/jwt.md) and [OAuth](https://oauth.net) through extensions) * [CORS headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS) * security checks and headers (tested against [OWASP recommendations](https://www.owasp.org/index.php/REST_Security_Cheat_Sheet)) * [invalidation-based HTTP caching](../core/performance.md) @@ -91,7 +91,7 @@ This starts the following services: | Name | Description | |----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| caddy | [Caddy web server](https://caddyserver.com) with the [Mercure](../core/mercure.md) (real-time and async) and [Vulcain](https://vulcain.rocks) (relations preloading) modules | +| caddy | [Caddy web server](caddy.md) with the [Mercure](../core/mercure.md) (real-time and async) and [Vulcain](https://vulcain.rocks) (relations preloading) modules | | php | The API with PHP 8, Composer and sensitive configs | | pwa | Next.js webapp with API Platform Admin and Client Generator preinstalled | | database | PostgreSQL database server | @@ -141,7 +141,7 @@ That being said, keep in mind that API Platform is 100% independent of the persi best suit(s) your needs (including NoSQL databases or remote web services) by implementing the [right interfaces](../core/data-providers.md). API Platform even supports using several persistence systems together in the same project. -### Using Symfony and Composer +### Using Symfony CLI Alternatively, the API Platform server component can also be installed directly on a local machine. **This method is recommended only for users who want full control over the directory structure and the installed @@ -153,12 +153,12 @@ The rest of this tutorial assumes that you have installed API Platform using the next section if it's your case. API Platform has an official Symfony Flex recipe. It means that you can easily install it from any Symfony -application using [Composer](https://getcomposer.org/): +application using [the Symfony binary](https://symfony.com/download): Create a new Symfony project: ```console -composer create-project symfony/skeleton bookshop-api +symfony new bookshop-api ``` Enter the project directory: @@ -170,20 +170,20 @@ cd bookshop-api Install the API Platform's server component in this skeleton: ```console -composer require api +symfony composer require api ``` Then, create the database and its schema: ```console -bin/console doctrine:database:create -bin/console doctrine:schema:create +symfony console doctrine:database:create +symfony console doctrine:schema:create ``` And start the built-in PHP server: ```console -php -S 127.0.0.1:8000 -t public +symfony serve ``` All JavaScript components are also [available as standalone libraries](https://github.com/api-platform?language=javascript) @@ -202,7 +202,7 @@ You'll need to add a security exception in your browser to accept the self-signe for this container when installing the framework. Later you will probably replace this welcome screen by the homepage of your Next.js application. If you don't plan to create -a Progressive Web App, you can remove the `pwa/` directory and the related lines in `docker-compose*.yaml` (don't do it +a Progressive Web App, you can remove the `pwa/` directory as well as the related lines in `docker-compose*.yml` and in `api/docker/caddy/Caddyfile` (don't do it now, we'll use this container later in this tutorial). Click on the "API" button, or go to `https://localhost/docs/`: @@ -269,7 +269,7 @@ class Book public string $author = ''; /** The publication date of this book. */ - public ?\DateTimeInterface $publicationDate = null; + public ?\DateTimeImmutable $publicationDate = null; /** @var Review[] Available reviews for this book. */ public iterable $reviews; @@ -311,7 +311,7 @@ class Review public string $author = ''; /** The date of publication of this review.*/ - public ?\DateTimeInterface $publicationDate = null; + public ?\DateTimeImmutable $publicationDate = null; /** The book this review is about. */ public ?Book $book = null; @@ -339,8 +339,8 @@ The framework also use these metadata to serialize and deserialize data from JSO For the sake of simplicity, in this example we used public properties (except for the id, see below). API Platform (as well as Symfony and Doctrine) also supports accessor methods (getters/setters), use them if you want to. -We used a private property and a getter for the id to enforce the fact that it is read only (we will let the DBMS generating it). API Platform also has first-grade support for UUIDs. [You should -probably use them instead of auto-incremented ids](https://www.clever-cloud.com/blog/engineering/2015/05/20/why-auto-increment-is-a-terrible-idea/). +We used a private property and a getter for the ID to enforce the fact that it is read only (we will let the DBMS generating it). API Platform also has first-grade support for UUIDs. [You should +probably use them instead of auto-incremented IDs](https://www.clever-cloud.com/blog/engineering/2015/05/20/why-auto-increment-is-a-terrible-idea/). Because API Platform provides all the infrastructure for us, our API is almost ready! @@ -369,71 +369,37 @@ Modify these files as described in these patches: use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\ORM\Mapping as ORM; --/** A book. */ -+/** -+ * A book. -+ * -+ * @ORM\Entity -+ */ + /** A book. */ ++#[ORM\Entity] #[ApiResource] class Book { -- /** The id of this book. */ -+ /** -+ * The id of this book. -+ * -+ * @ORM\Id -+ * @ORM\GeneratedValue -+ * @ORM\Column(type="integer") -+ */ + /** The id of this book. */ ++ #[ORM\Id, ORM\Column, ORM\GeneratedValue] private ?int $id = null; -- /** The ISBN of this book (or null if doesn't have one). */ -+ /** -+ * The ISBN of this book (or null if doesn't have one). -+ * -+ * @ORM\Column(nullable=true) -+ */ + /** The ISBN of this book (or null if doesn't have one). */ ++ #[ORM\Column(nullable: true)] public ?string $isbn = null; -- /** The title of this book. */ -+ /** -+ * The title of this book. -+ * -+ * @ORM\Column -+ */ + /** The title of this book. */ ++ #[ORM\Column] public string $title = ''; -- /** The description of this book. */ -+ /** -+ * The description of this book. -+ * -+ * @ORM\Column(type="text") -+ */ + /** The description of this book. */ ++ #[ORM\Column(type: 'text')] public string $description = ''; -- /** The author of this book. */ -+ /** -+ * The author of this book. -+ * -+ * @ORM\Column -+ */ + /** The author of this book. */ ++ #[ORM\Column] public string $author = ''; -- /** The publication date of this book. */ -+ /** -+ * The publication date of this book. -+ * -+ * @ORM\Column(type="datetime_immutable") -+ */ - public ?\DateTimeInterface $publicationDate = null; + /** The publication date of this book. */ ++ #[ORM\Column] + public ?\DateTimeImmutable $publicationDate = null; -- /** @var Review[] Available reviews for this book. */ -+ /** -+ * @var Review[] Available reviews for this book. -+ * -+ * @ORM\OneToMany(targetEntity="Review", mappedBy="book", cascade={"persist", "remove"}) -+ */ + /** @var Review[] Available reviews for this book. */ ++ #[ORM\OneToMany(targetEntity: Review::class, mappedBy: 'book', cascade: ['persist', 'remove'])] public iterable $reviews; public function __construct() @@ -447,63 +413,33 @@ Modify these files as described in these patches: use ApiPlatform\Core\Annotation\ApiResource; +use Doctrine\ORM\Mapping as ORM; --/** A review of a book. */ -+/** -+ * A review of a book. -+ * -+ * @ORM\Entity -+ */ + /** A review of a book. */ ++#[ORM\Entity] #[ApiResource] class Review { -- /** The id of this review. */ -+ /** -+ * The id of this review. -+ * -+ * @ORM\Id -+ * @ORM\GeneratedValue -+ * @ORM\Column(type="integer") -+ */ + /** The id of this review. */ ++ #[ORM\Id, ORM\Column, ORM\GeneratedValue] private ?int $id = null; -- /** The rating of this review (between 0 and 5). */ -+ /** -+ * The rating of this review (between 0 and 5). -+ * -+ * @ORM\Column(type="smallint") -+ */ + /** The rating of this review (between 0 and 5). */ ++ #[ORM\Column(type: 'smallint')] public int $rating = 0; -- /** The body of the review. */ -+ /** -+ * The body of the review. -+ * -+ * @ORM\Column(type="text") -+ */ + /** The body of the review. */ ++ #[ORM\Column(type: 'text')] public string $body = ''; -- /** The author of the review. */ -+ /** -+ * The author of the review. -+ * -+ * @ORM\Column -+ */ + /** The author of the review. */ ++ #[ORM\Column] public string $author = ''; -- /** The date of publication of this review.*/ -+ /** -+ * The date of publication of this review. -+ * -+ * @ORM\Column(type="datetime_immutable") -+ */ - public ?\DateTimeInterface $publicationDate = null; + /** The date of publication of this review.*/ ++ #[ORM\Column] + public ?\DateTimeImmutable $publicationDate = null; -- /** The book this review is about. */ -+ /** -+ * The book this review is about. -+ * -+ * @ORM\ManyToOne(targetEntity="Book", inversedBy="reviews") -+ */ + /** The book this review is about. */ ++ #[ORM\ManyToOne(inversedBy: 'reviews')] public ?Book $book = null; public function getId(): ?int @@ -640,31 +576,26 @@ Modify the following files as described in these patches: use Doctrine\Common\Collections\ArrayCollection; use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Validator\Constraints as Assert; - - * @ORM\Column(nullable=true) - */ + + #[ORM\Column(nullable: true)] + #[Assert\Isbn] - public ?string $isbn = null; - - * @ORM\Column - */ + public ?string $isbn = null; + + #[ORM\Column] + #[Assert\NotBlank] public string $title = ''; - * @ORM\Column(type="text") - */ + #[ORM\Column(type: 'text')] + #[Assert\NotBlank] public string $description = ''; - * @ORM\Column - */ + #[ORM\Column] + #[Assert\NotBlank] public string $author = ''; - * @ORM\Column(type="datetime_immutable") - */ + #[ORM\Column] + #[Assert\NotNull] - public ?\DateTimeInterface $publicationDate = null; + public ?\DateTimeImmutable $publicationDate = null; ``` `api/src/Entity/Review.php` @@ -673,29 +604,24 @@ Modify the following files as described in these patches: use ApiPlatform\Core\Annotation\ApiResource; use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Validator\Constraints as Assert; - - * @ORM\Column(type="smallint") - */ + + #[ORM\Column(type: 'smallint')] + #[Assert\Range(min: 0, max: 5)] public int $rating = 0; - * @ORM\Column(type="text") - */ + #[ORM\Column(type: 'text')] + #[Assert\NotBlank] public string $body = ''; - * @ORM\Column - */ + #[ORM\Column] + #[Assert\NotBlank] public string $author = ''; - * @ORM\Column(type="datetime_immutable") - */ + #[ORM\Column] + #[Assert\NotNull] - public ?\DateTimeInterface $publicationDate = null; + public ?\DateTimeImmutable $publicationDate = null; - * @ORM\ManyToOne(targetEntity="Book", inversedBy="reviews") - */ + #[ORM\ManyToOne(inversedBy: 'reviews')] + #[Assert\NotNull] public ?Book $book = null; diff --git a/distribution/testing.md b/distribution/testing.md index 89db4e2ebaf..df745cf77a1 100644 --- a/distribution/testing.md +++ b/distribution/testing.md @@ -207,7 +207,7 @@ publicationDate: This value should not be null.', $this->assertResponseStatusCodeSame(204); $this->assertNull( // Through the container, you can access all your services from the tests, including the ORM, the mailer, remote API clients... - static::$container->get('doctrine')->getRepository(Book::class)->findOneBy(['isbn' => '9781344037075']) + static::getContainer()->get('doctrine')->getRepository(Book::class)->findOneBy(['isbn' => '9781344037075']) ); } diff --git a/extra/contribution-guides.md b/extra/contribution-guides.md index 951fe0e510b..ed84b118074 100644 --- a/extra/contribution-guides.md +++ b/extra/contribution-guides.md @@ -7,4 +7,4 @@ **To report a security issue, please refer to [the dedicated document](security.md).** -

JWT screencast
Watch the Contributing back to Symfony screencast (free-

+

JWT screencast
Watch the Contributing back to Symfony screencast (free)

diff --git a/extra/philosophy.md b/extra/philosophy.md index d32176e0337..8a762c8c3c9 100644 --- a/extra/philosophy.md +++ b/extra/philosophy.md @@ -6,7 +6,7 @@ In 25 years of PHP, the web changed dramatically and is now evolving faster than [full-JavaScript Progressive Web Apps](https://en.wikipedia.org/wiki/Progressive_web_application) **are becoming the standard**. * [Internet users spend more time on their mobile devices than on desktops](https://www.broadbandsearch.net/blog/mobile-desktop-internet-usage-statistics): having a mobile-first website is mandatory and **native mobile apps are a must-have**. * [The semantic web](https://en.wikipedia.org/wiki/Semantic_Web) and **especially [Linked Data](https://en.wikipedia.org/wiki/Linked_data) - is a reality**: with the [Schema.org](http://schema.org/) initiative and new open web standards such as [JSON-LD](http://json-ld.org/), + is a reality**: with the [Schema.org](https://schema.org/) initiative and new open web standards such as [JSON-LD](http://json-ld.org/), search engines (among a bunch of other services and software) consume structured and machine-readable data at web scale. Not exposing such data decrease interoperability and search engine ranking/efficiency (think rich snippets). * HTTP/2 and HTTP/3 [dramatically improve the performance of web applications](https://vulcain.rocks) thanks to multiplexing, Server Push and their other new capabilities. @@ -29,7 +29,7 @@ API Platform makes modern development easy and fun again: be understood by any compliant client such as your apps but also search engines (JSON-LD with Schema.org vocabulary). This API is the central and unique entry point to access and modify data. It also encapsulates the whole business logic. * [Then **create as many clients as you want using frontend technologies you love**](../client-generator/index.md): a JavaScript - webapp built in React or in Vue querying the API but also a native iOS or Android app, or even a desktop application. Clients + webapp built with React or with Vue querying the API but also a native iOS or Android app, or even a desktop application. Clients only display data and forms. See also [the general design](../core/design.md) of the framework. diff --git a/outline.yaml b/outline.yaml index e1f333a984f..8579f4ddcef 100644 --- a/outline.yaml +++ b/outline.yaml @@ -5,6 +5,7 @@ chapters: - index - testing - debugging + - caddy - title: The API Component path: core items: diff --git a/schema-generator/configuration.md b/schema-generator/configuration.md index 1a32ae56135..abb103e24c5 100644 --- a/schema-generator/configuration.md +++ b/schema-generator/configuration.md @@ -155,9 +155,9 @@ The `@Assert\NotNull` constraint is automatically added. /** * The name of the item. * - * @ORM\Column - * @Assert\NotNull */ + #[ORM\Column] + #[Assert\NotNull] private string $name; ``` @@ -182,24 +182,25 @@ Output: ... use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; +use Doctrine\ORM\Mapping as ORM; /** * A person (alive, dead, undead, or fictional). * - * @see http://schema.org/Person Documentation on Schema.org + * @see https://schema.org/Person Documentation on Schema.org * - * @ORM\Entity - * @UniqueEntity("email") - * @Iri("http://schema.org/Person") + * @Iri("https://schema.org/Person") */ +#[ORM\Entity] +#[UniqueEntity('email')] class Person { /** * Email address. * - * @ORM\Column - * @Assert\Email */ + #[ORM\Column] + #[Assert\Email] private string $email; ``` @@ -256,15 +257,18 @@ Output: ... use Symfony\Component\Serializer\Annotation\Groups; +use Doctrine\ORM\Mapping as ORM; +use ApiPlatform\Core\Annotation\ApiProperty; +use ApiPlatform\Core\Annotation\ApiResource; /** * A person (alive, dead, undead, or fictional). * - * @see http://schema.org/Person Documentation on Schema.org + * @see https://schema.org/Person Documentation on Schema.org * - * @ORM\Entity - * @Iri("http://schema.org/Person") */ +#[ORM\Entity] +#[ApiResource(iri: "https://schema.org/Person")] class Person { /** @@ -272,11 +276,11 @@ class Person * * @see https://schema.org/name * - * @ORM\Column(nullable=true) - * @Assert\Type(type="string") - * @Iri("https://schema.org/name") - * @Groups({"public"}) */ + #[ORM\Column(nullable: true) + #[Assert\Type(type: 'string')] + #[Groups(['public'])] + #[ApiProperty(iri: 'https://schema.org/name')] private string $name; ``` @@ -310,26 +314,28 @@ Output: ... use Doctrine\ORM\Mapping as ORM; +use ApiPlatform\Core\Annotation\ApiProperty; +use ApiPlatform\Core\Annotation\ApiResource; /** * Any offered product or service. * - * @see http://schema.org/Product Documentation on Schema.org + * @see https://schema.org/Product Documentation on Schema.org * - * @ORM\Entity - * @ApiResource(iri="http://schema.org/Product") - * @UniqueEntity("gtin13s") */ +#[ORM\Entity] +#[ApiResource(iri: 'https://schema.org/Product')] +#[UniqueEntity('gtin13s')] class Product { /** * the weight of the product or person * - * @see http://schema.org/weight + * @see https://schema.org/weight * - * @ORM\Embedded(class="App\Entity\QuantitativeValue", columnPrefix="weight_") - * @ApiProperty(iri="http://schema.org/weight") */ + #[ORM\Embedded(class: QuantitativeValue::class, columnPrefix: 'weight_')] + #[ApiProperty(iri: 'https://schema.org/weight')] private ?QuantitativeValue $weight = null; ``` @@ -475,13 +481,20 @@ mappings. If you set the option `useInterface` to true, the generator will generate an interface corresponding to each generated entity and will use them in relation mappings. -To let PHP Schema generate the XML mapping file usable with Symfony, add the following to your config file: +To let PHP Schema generate the mapping file usable with Symfony, add the following to your config file: ```yaml doctrine: resolveTargetEntityConfigPath: path/to/doctrine.xml ``` +The default mapping file format is XML, but you can change it to YAML with the following option: +```yaml +doctrine: + resolveTargetEntityConfigPath: path/to/doctrine.yaml + resolveTargetEntityConfigType: YAML # Supports XML & YAML +``` + ## Custom Schemas The generator can use your own schema definitions. They must be written in RDFa and follow the format of the [Schema.org's @@ -554,7 +567,7 @@ config: format: null # Example: rdfxml # Namespace of the vocabulary to import - vocabularyNamespace: 'http://schema.org/' # Example: 'http://www.w3.org/ns/activitystreams#' + vocabularyNamespace: 'https://schema.org/' # Example: 'http://www.w3.org/ns/activitystreams#' # OWL relation files containing cardinality information in the GoodRelations format relations: # Example: 'https://purl.org/goodrelations/v1.owl' diff --git a/schema-generator/getting-started.md b/schema-generator/getting-started.md index bfa7ae30125..d4f67550fe0 100644 --- a/schema-generator/getting-started.md +++ b/schema-generator/getting-started.md @@ -26,8 +26,8 @@ Then, write a simple YAML config file similar to the following. Here we will generate a data model for an address book with the following data: -* a [`Person`](http://schema.org/Person) which inherits from [`Thing`](http://schema.org/Thing) -* a [`PostalAddress`](http://schema.org/PostalAddress) which inherits from [`ContactPoint`](http://schema.org/ContactPoint), which itself inherits from [`StructuredValue`](http://schema.org/StructuredValue), etc. +* a [`Person`](https://schema.org/Person) which inherits from [`Thing`](https://schema.org/Thing) +* a [`PostalAddress`](https://schema.org/PostalAddress) which inherits from [`ContactPoint`](https://schema.org/ContactPoint), which itself inherits from [`StructuredValue`](https://schema.org/StructuredValue), etc. ```yaml # api/config/schema.yaml