Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions core/content-negotiation.md
Original file line number Diff line number Diff line change
Expand Up @@ -420,3 +420,37 @@ API Platform automatically adds two HTTP headers to responses for resources:

> See [LDP §4.2 / Primer notes on Accept-Post](https://www.w3.org/TR/ldp/#Accept-Post) and typical
> exposure via OPTIONS.

## Content-Type Charset

> [!NOTE] Behavioral change in API Platform 4.4 (core
> [#8226](https://github.com/api-platform/core/pull/8226)).

Since 4.4, the `; charset=utf-8` parameter is only appended to the `Content-Type` response header
for media types whose IANA registration actually defines a `charset` parameter: `text/*` types (for
example `text/html`, `text/csv`, `text/xml`) and `application/xml`. JSON-based media types —
`application/json`, `application/ld+json`, `application/hal+json`, `application/vnd.api+json`,
`application/merge-patch+json`, `application/problem+json`, and so on — do not define a `charset`
parameter (per RFC 8259, and the `+json` structured syntax suffix of RFC 6839) and are always UTF-8,
so the parameter is no longer added to them.

Before 4.4, `; charset=utf-8` was appended unconditionally to every response, including JSON-based
formats. For example, for a JSON-LD response:

```diff
- Content-Type: application/ld+json; charset=utf-8
+ Content-Type: application/ld+json
```

An XML response is unaffected, as `application/xml` still gets the parameter:

```http
Content-Type: application/xml; charset=utf-8
```

> [!WARNING] If a client or a test asserts on the exact `Content-Type` header value for a JSON-based
> format, it will break after upgrading to 4.4: the `charset=utf-8` suffix is gone. Update those
> assertions accordingly.

The rule is implemented in `formatContentType()` in
[`HttpResponseHeadersTrait`](https://github.com/api-platform/core/blob/4.4/src/State/Util/HttpResponseHeadersTrait.php).
282 changes: 249 additions & 33 deletions core/doctrine-filters.md

Large diffs are not rendered by default.

87 changes: 87 additions & 0 deletions core/extending-jsonld-context.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,62 @@ The generated context will now have your custom attributes set:
Note that you do not have to provide the `@id` attribute. If you do not provide an `@id` attribute,
the value from `iri` will be used.

### Extending the Context of a Whole Resource

The `jsonldContext` option is also available on `#[ApiResource]` itself. Its main use is declaring
namespace prefixes once for the whole resource, instead of repeating a full IRI on every property
that needs one:

```php
<?php
// api/src/ApiResource/Book.php with Symfony or app/ApiResource/Book.php with Laravel
namespace App\ApiResource;

use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\ApiResource;

#[ApiResource(
types: ['https://schema.org/Book'],
jsonldContext: ['dct' => 'http://purl.org/dc/terms/'],
)]
class Book
{
// ...

#[ApiProperty(types: ['https://schema.org/name'], iris: ['dct:title'])]
public $name;

// ...
}
```

The `dct` prefix is merged into the top-level `@context`, so properties can then reference it
through a compact IRI such as `dct:title`:

`GET /contexts/Book`

```json
{
"@context": {
"@vocab": "http://example.com/apidoc#",
"hydra": "http://www.w3.org/ns/hydra/core#",
"dct": "http://purl.org/dc/terms/",
"name": "dct:title"
}
}
```

The resource-level context is merged into `@context` before the per-property entries are added, and
each property's entry is then written to `@context[<propertyName>]`, overwriting anything already
present at that key. In practice, the two never collide because the resource-level context is meant
for prefix declarations, while a property's own entry is keyed by the property name; a collision
only happens if a property is literally named after one of your prefixes, in which case the property
wins.

If an operation (for example a `Get` or a `Patch`) declares its own `jsonldContext`, that value is
used as-is for that operation instead of the resource's: the two are not merged together, the
operation's `jsonldContext` simply takes precedence.

## Hydra

<p align="center" class="symfonycasts"><a href="https://symfonycasts.com/screencast/api-platform/hydra?cid=apip"><img src="../symfony/images/symfonycasts-player.png" alt="Hydra screencast"><br>Watch the Hydra screencast</a></p>
Expand Down Expand Up @@ -122,3 +178,34 @@ resources:
```

</code-selector>

### The `hydra:memberAssertion` Property

For every resource exposing a collection operation, the generated Hydra API documentation
(`GET /docs.jsonld`) automatically adds a `hydra:memberAssertion` entry to the entrypoint's
`hydra:supportedProperty` for that collection. It asserts, as an `rdf:type` statement, that every
member returned by the collection is an instance of the resource:

```json
{
"@id": "#Entrypoint/books",
"@type": "hydra:Link",
"domain": "#Entrypoint",
"owl:maxCardinality": 1,
"range": "hydra:Collection",
"hydra:memberAssertion": {
"hydra:property": { "@id": "rdf:type" },
"hydra:object": { "@id": "#Book" }
},
"hydra:supportedOperation": ["..."]
}
```

This assertion is generated automatically for every collection and isn't configurable through
`jsonldContext` or `hydraContext`.

> [!NOTE] Before API Platform 4.4, the same assertion was expressed as an `owl:equivalentClass`
> restriction (an `owl:onProperty: hydra:member` / `owl:allValuesFrom: #Book` pair nested inside the
> `range` array). `owl:equivalentClass` no longer appears anywhere in the generated Hydra
> documentation: if you parse `range` and expect that structure, read `hydra:memberAssertion`
> instead.
64 changes: 54 additions & 10 deletions core/filters.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,19 @@ a new instance:
- **`PartialSearchFilter`**: For partial string matching (SQL `LIKE %...%`). Supports dot notation
for nested properties.
- Usage: `new QueryParameter(filter: PartialSearchFilter::class)`
- **`StartSearchFilter`**: For prefix matching (`LIKE value%`). Supports dot notation for nested
properties. See the [Doctrine Filters documentation](doctrine-filters.md#start-search-filter) for
case-sensitivity defaults.
- Usage: `new QueryParameter(filter: StartSearchFilter::class)`
- **`EndSearchFilter`**: For suffix matching (`LIKE %value`). Supports dot notation for nested
properties. See the [Doctrine Filters documentation](doctrine-filters.md#end-search-filter) for
case-sensitivity defaults.
- Usage: `new QueryParameter(filter: EndSearchFilter::class)`
- **`WordStartSearchFilter`** (Doctrine ORM/ODM only, no Laravel/Eloquent equivalent): Matches
fields containing a word that starts with the value. Supports dot notation for nested properties.
See the [Doctrine Filters documentation](doctrine-filters.md#word-start-search-filter) for
details.
- Usage: `new QueryParameter(filter: WordStartSearchFilter::class)`
- **`IriFilter`**: For filtering by IRIs (e.g., relations). Supports dot notation for nested
associations.
- Usage: `new QueryParameter(filter: IriFilter::class)`
Expand All @@ -66,8 +79,8 @@ a new instance:
- **`OrFilter`**: A decorator that forces a filter to combine criteria with `OR` instead of `AND`.
- Usage:
`new QueryParameter(filter: new OrFilter(new ExactFilter()), properties: ['name', 'ean'])`
- **`ChainFilter`** (Doctrine ORM/ODM only): Composes several filters on a single parameter key; each
wrapped filter self-selects by the shape of the value. See the
- **`ChainFilter`** (Doctrine ORM/ODM only): Composes several filters on a single parameter key;
each wrapped filter self-selects by the shape of the value. See the
[Doctrine Filters documentation](doctrine-filters.md#chain-filter) for details.
- Usage:
`new QueryParameter(filter: new ChainFilter([new ExactFilter(), new DateFilter()]), property: 'birthdate')`
Expand Down Expand Up @@ -131,8 +144,9 @@ Instead of repeating the same parameter configuration on every resource, you can
default parameters that are automatically applied to all resources. This is done via the `defaults`
key in your API Platform configuration.

Add a `parameters` map under `defaults` in your API Platform configuration. Each entry maps a
fully-qualified parameter class name to its options.
Add a `parameters` map under `defaults` in your API Platform configuration. In Symfony, entries can
either use the fully-qualified parameter class name as their key, or use a custom name and specify
the class explicitly with the `class` option.

```yaml
# Symfony: api/config/packages/api_platform.yaml
Expand All @@ -150,6 +164,30 @@ api_platform:
description: "API version"
```

To define multiple global parameters using the same parameter class, use named entries with an
explicit `class`:

```yaml
# Symfony: api/config/packages/api_platform.yaml
api_platform:
defaults:
parameters:
api_token:
class: ApiPlatform\Metadata\HeaderParameter
key: "API-Token"
required: true
description: "API authentication token"

request_id:
class: ApiPlatform\Metadata\HeaderParameter
key: "Request-ID"
required: false
description: "A unique request identifier"
```

The name of a named entry is only a configuration identifier. The `key` option defines the parameter
name exposed at runtime.

```php
<?php
// Laravel: config/api-platform.php
Expand Down Expand Up @@ -262,10 +300,10 @@ This configuration allows clients to filter events by date ranges using queries
- `/events?endDate[lt]=2023-12-31` — events ending before December 31st 2023
- `/events?startDate[gte]=2023-01-01&endDate[lte]=2023-12-31` — events within a date range

> [!NOTE] This is a plain comparison, distinct from
> [`DateFilter`](doctrine-filters.md#date-filter): it does not provide per-property `null`
> management, tolerant handling of invalid or empty values, or the `before`/`after` versus
> `strictly_before`/`strictly_after` vocabulary. Use `DateFilter` when you need those behaviors.
> [!NOTE] This is a plain comparison, distinct from [`DateFilter`](doctrine-filters.md#date-filter):
> it does not provide per-property `null` management, tolerant handling of invalid or empty values,
> or the `before`/`after` versus `strictly_before`/`strictly_after` vocabulary. Use `DateFilter`
> when you need those behaviors.

### Filtering a Single Property

Expand Down Expand Up @@ -508,6 +546,8 @@ use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\HeaderParameter;
use ApiPlatform\Metadata\QueryParameter;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\TypeInfo\Type\BuiltinType;
use Symfony\Component\TypeInfo\TypeIdentifier;

#[ApiResource(operations: [
new GetCollection(
Expand All @@ -520,7 +560,8 @@ use Symfony\Component\Validator\Constraints as Assert;
'X-Request-ID' => new HeaderParameter(
description: 'A unique request identifier.',
required: true,
constraints: [new Assert\Uuid()]
constraints: [new Assert\Uuid()],
nativeType: new BuiltinType(typeIdentifier: TypeIdentifier::STRING)
)
]
)
Expand All @@ -531,6 +572,9 @@ class User {}
> [!NOTE] When `castToNativeType` is enabled, API Platform infers type validation from the JSON
> Schema.

If `constraints` are used then a valid type needs to be passed using `nativeType` named argument.
Otherwise, the values will be passed as an array to each constraints.

The `ApiPlatform\Validator\Util\ParameterValidationConstraints` trait can be used to automatically
infer validation constraints from the JSON Schema and OpenAPI definitions of a parameter.

Expand Down Expand Up @@ -949,7 +993,7 @@ class User {}
## Parameter Security

You can secure individual parameters using Symfony expression language. When a security expression
evaluates to `false`, the parameter will be ignored and treated as if it wasn't provided.
evaluates to `false`, a context appropriate `AccessDeniedException` will be thrown.

```php
<?php
Expand Down
2 changes: 2 additions & 0 deletions core/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
You can choose your preferred stack between Symfony, Laravel, or bootstrapping the API Platform core
library manually.

API Platform requires PHP 8.2 or higher. The Symfony variant requires Symfony `^7.4` or `^8.0`.

> [!CAUTION] If you are migrating from an older version of API Platform, make sure you read the
> [Upgrade Guide](upgrade-guide.md).

Expand Down
56 changes: 56 additions & 0 deletions core/mercure.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ Mercure hub. Then, the Mercure hub dispatches the updates to all connected clien

![Mercure subscriptions](images/mercure-subscriptions.png)

## Which Protocol Version Does Your Hub Speak?

Mercure has a 1.0 protocol version that changes how subscribers pick which updates to receive:
`match=` and `match_urlpattern=` query parameters replace `topic=`. API Platform talks to 0.x hubs
by default; switching a hub to 1.0 is done by configuring `protocol_version: '1.0'` on the
[MercureBundle](https://symfony.com/doc/current/mercure.html) side, not on this page. The examples
below cover both; check with whoever operates your hub if you're not sure which version it runs.

## Installing Mercure Support

Mercure support is already installed, configured and enabled in
Expand Down Expand Up @@ -67,6 +75,54 @@ automatically subscribe to Mercure updates when available:

[Learn how to use the discovery capabilities of Mercure in your own clients](https://mercure.rocks/docs/ecosystem/awesome).

## Subscribing to Updates

API Platform publishes each update on the resource's absolute IRI — the same value as the `@id` of
the JSON-LD document the API serves for that resource. This means the topic to subscribe to is
nothing you need to build: it's the `@id` you already have from fetching the resource.

A minimal browser client fetches the resource, discovers the hub from the `Link: rel="mercure"`
header, and subscribes using the resource's `@id` as the topic:

```javascript
const response = await fetch("/books/1", { headers: { Accept: "application/ld+json" } });
const book = await response.json();

// Discover the hub through the Link header API Platform adds to the response
const linkHeader = response.headers.get("Link");
const [, hubUrlString] = linkHeader.match(/<([^>]+)>;\s*rel="mercure"/);
const hubUrl = new URL(hubUrlString);

hubUrl.searchParams.append("topic", book["@id"]); // Mercure 0.x

const eventSource = new EventSource(hubUrl);
eventSource.onmessage = (event) => {
console.log(JSON.parse(event.data));
};
```

If your hub speaks the Mercure 1.0 protocol, subscribe with `match` instead of `topic`:

```javascript
hubUrl.searchParams.append("match", book["@id"]); // Mercure 1.0
```

To subscribe to every book instead of a single one, 1.0 hubs also accept `match_urlpattern`, with a
[URL Pattern](https://mercure.rocks/docs/1.0/concepts/topics-and-matchers) matching the resources'
IRIs:

```javascript
hubUrl.searchParams.append("match_urlpattern", "https://api.example.com/books/:id"); // Mercure 1.0
```

`match_urlpattern` has no 0.x equivalent: under 0.x, subscribing to a family of resources at once
means either subscribing to each IRI individually, or using the alternate-topic technique described
below.

See the [Mercure protocol reference](https://mercure.rocks/docs/1.0/reference/protocol) (1.0) or
[the spec's subscribers section](https://mercure.rocks/spec#subscribers) (0.x) for the full
`topic`/`match`/`match_urlpattern` semantics.

## Dispatching Private Updates (Authorized Mode)

Mercure allows dispatching
Expand Down
Loading
Loading