Skip to content

Commit d4ac684

Browse files
Add Laravel support to state providers documentation (#2018)
Co-authored-by: Antoine Bluchet <soyuka@users.noreply.github.com>
1 parent 6fb4310 commit d4ac684

File tree

1 file changed

+215
-11
lines changed

1 file changed

+215
-11
lines changed

core/state-providers.md

Lines changed: 215 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,36 @@
11
# State Providers
22

3-
To retrieve data exposed by the API, API Platform uses classes called **state providers**. A state provider using [Doctrine
4-
ORM](https://www.doctrine-project.org/projects/orm.html) to retrieve data from a database, a state provider using
3+
To retrieve data exposed by the API, API Platform uses classes called **state providers**.
4+
5+
With the Symfony variant, a state provider using [Doctrine
6+
ORM](https://www.doctrine-project.org/projects/orm.html) is ready to retrieve data from a database and a state provider using
57
[Doctrine MongoDB ODM](https://www.doctrine-project.org/projects/mongodb-odm.html) to retrieve data from a document
6-
database, and a state provider using [Elasticsearch-PHP](https://www.elastic.co/guide/en/elasticsearch/client/php-api/current/index.html)
7-
to retrieve data from an Elasticsearch cluster are included with the library. The first one is enabled by default. These
8-
state providers natively support paged collections and filters. They can be used as-is and are perfectly suited to common uses.
8+
database.
9+
10+
With the Laravel variant, a state provider using [Eloquent ORM](https://laravel.com/docs/eloquent) to retrieve data from a relational database and a state provider.
11+
12+
The ORM providers are enabled by default, based on your framework variant (Eloquent or Doctrine will be set up).
13+
14+
15+
These state providers natively support paged collections and filters. They can be used as-is and are perfectly suited to common uses.
916

1017
However, you sometimes want to retrieve data from other sources such as another persistence layer or a webservice.
1118
Custom state providers can be used to do so. A project can include as many state providers as needed. The first able to
1219
retrieve data for a given resource will be used.
1320

1421
To do so you need to implement the `ApiPlatform\State\ProviderInterface`.
1522

16-
In the following examples we will create custom state providers for an entity class called `App\Entity\BlogPost`.
17-
Note, that if your entity is not Doctrine-related, you need to flag the identifier property by using
23+
In the following examples we will create custom state providers for Symfony entities and Laravel models:
24+
- For Symfony we will create an entity class called `App\Entity\BlogPost`.
25+
- For Laravel, we will create a model class called `App\Models\BlogPost`.
26+
27+
Note, that if your entity is not Doctrine-related or Eloquent-related, you need to flag the identifier property by using
1828
`#[ApiProperty(identifier: true)` for things to work properly (see also [Entity Identifier Case](serialization.md#entity-identifier-case)).
1929

2030
## Creating a Custom State Provider
2131

32+
### Custom State Provider with Symfony
33+
2234
If the [Symfony MakerBundle](https://symfony.com/doc/current/bundles/SymfonyMakerBundle) is installed in your project,
2335
you can use the following command to generate a custom state provider easily:
2436

@@ -114,7 +126,7 @@ final class BlogPostProvider implements ProviderInterface
114126
}
115127
```
116128

117-
We then need to configure this same provider on the BlogPost `GetCollection` operation, or for every operations via the `ApiResource` attribute:
129+
We then need to configure this same provider on the BlogPost `GetCollection` operation, or for every operation via the `ApiResource` attribute:
118130

119131
```php
120132
<?php
@@ -129,11 +141,124 @@ use App\State\BlogPostProvider;
129141
class BlogPost {}
130142
```
131143

144+
#### Custom State Provider with Laravel
145+
146+
Using [Laravel Artisan Console](https://laravel.com/docs/artisan), you can generate a custom state provider easily with the following command:
147+
148+
```console
149+
php artisan make:state-provider
150+
```
151+
152+
Let's start with a State Provider for the URI: `/blog_posts/{id}`.
153+
154+
First, your `BlogPostProvider` has to implement the
155+
[`ProviderInterface`](https://github.com/api-platform/core/blob/main/src/State/ProviderInterface.php):
156+
157+
```php
158+
<?php
159+
// api/src/State/BlogPostProvider.php
160+
161+
namespace App\State;
162+
163+
use App\Models\BlogPost;
164+
use ApiPlatform\Metadata\Operation;
165+
use ApiPlatform\State\ProviderInterface;
166+
167+
/**
168+
* @implements ProviderInterface<BlogPost|null>
169+
*/
170+
final class BlogPostProvider implements ProviderInterface
171+
{
172+
private const DATA = [
173+
'ab' => new BlogPost('ab'),
174+
'cd' => new BlogPost('cd'),
175+
];
176+
177+
public function provide(Operation $operation, array $uriVariables = [], array $context = []): BlogPost|null
178+
{
179+
return self::DATA[$uriVariables['id']] ?? null;
180+
}
181+
}
182+
```
183+
184+
For the example, we store the list of our blog posts in an associative array (the `BlogPostProvider::DATA` constant).
185+
186+
As this operation expects a `BlogPost`, the `provide` methods return the instance of the `BlogPost` corresponding to the ID passed in the URL. If the ID doesn't exist in the associative array, `provide()` returns `null`. API Platform will automatically generate a 404 response if the provider returns `null`.
187+
188+
The `$uriVariables` parameter contains an array with the values of the URI variables.
189+
190+
To use this provider we need to configure the provider on the operation:
191+
192+
```php
193+
<?php
194+
// api/src/Models/BlogPost.php
195+
196+
namespace App\Models;
197+
198+
use ApiPlatform\Metadata\Get;
199+
use App\State\BlogPostProvider;
200+
201+
#[Get(provider: BlogPostProvider::class)]
202+
class BlogPost {}
203+
```
204+
205+
Now let's say that we also want to handle the `/blog_posts` URI which returns a collection. We can change the Provider into
206+
supporting a wider range of operations. Then we can provide a collection of blog posts when the operation is a `CollectionOperationInterface`:
207+
208+
```php
209+
<?php
210+
// api/src/State/BlogPostProvider.php
211+
212+
namespace App\State;
213+
214+
use App\Models\BlogPost;
215+
use ApiPlatform\Metadata\Operation;
216+
use ApiPlatform\State\ProviderInterface;
217+
use ApiPlatform\Metadata\CollectionOperationInterface;
218+
219+
/**
220+
* @implements ProviderInterface<BlogPost[]|BlogPost|null>
221+
*/
222+
final class BlogPostProvider implements ProviderInterface
223+
{
224+
private const DATA = [
225+
'ab' => new BlogPost('ab'),
226+
'cd' => new BlogPost('cd'),
227+
];
228+
229+
public function provide(Operation $operation, array $uriVariables = [], array $context = []): iterable|BlogPost|null
230+
{
231+
if ($operation instanceof CollectionOperationInterface) {
232+
return self::DATA;
233+
}
234+
235+
return self::DATA[$uriVariables['id']] ?? null;
236+
}
237+
}
238+
```
239+
240+
We then need to configure this same provider on the BlogPost `GetCollection` operation, or for every operation via the `ApiResource` attribute:
241+
242+
```php
243+
<?php
244+
// api/src/Models/BlogPost.php
245+
246+
namespace App\Models;
247+
248+
use ApiPlatform\Metadata\ApiResource;
249+
use App\State\BlogPostProvider;
250+
251+
#[ApiResource(provider: BlogPostProvider::class)]
252+
class BlogPost {}
253+
```
254+
132255
## Hooking into the Built-In State Provider
133256

134257
If you want to execute custom business logic before or after retrieving data, this can be achieved by [decorating](https://symfony.com/doc/current/service_container/service_decoration.html) the built-in state providers or using [composition](https://en.wikipedia.org/wiki/Object_composition).
135258

136-
The next example uses a [DTO](https://api-platform.com/docs/core/dto/#using-data-transfer-objects-dtos) to change the presentation for data originally retrieved by the default state provider.
259+
The next examples (one for Symfony and one for Laravel) uses a [DTO](https://api-platform.com/docs/core/dto/#using-data-transfer-objects-dtos) to change the presentation for data originally retrieved by the default state provider.
260+
261+
### Symfony State Provider mechanism
137262

138263
```php
139264
<?php
@@ -142,7 +267,7 @@ The next example uses a [DTO](https://api-platform.com/docs/core/dto/#using-data
142267
namespace App\State;
143268

144269
use App\Dto\AnotherRepresentation;
145-
use App\Model\Book;
270+
use App\Entity\Book;
146271
use ApiPlatform\Metadata\Operation;
147272
use ApiPlatform\State\ProviderInterface;
148273
use Symfony\Component\DependencyInjection\Attribute\Autowire;
@@ -187,7 +312,86 @@ use App\State\BookRepresentationProvider;
187312
class Book {}
188313
```
189314

190-
## Registering Services Without Autowiring
315+
#### Laravel State Provider mechanism
316+
317+
First, don't forget to tag the service with the `ProviderInterface`
318+
319+
```php
320+
<?php
321+
322+
namespace App\Providers;
323+
324+
use ApiPlatform\State\ProviderInterface;
325+
use App\State\BookStateProvider;
326+
use Illuminate\Support\ServiceProvider;
327+
328+
class AppServiceProvider extends ServiceProvider
329+
{
330+
public function register(): void
331+
{
332+
$this->app->tag([BookRepresentationProvider::class], ProviderInterface::class);
333+
}
334+
335+
public function boot(): void
336+
{
337+
}
338+
}
339+
```
340+
341+
After that, you can inject the `ProviderInterface`
342+
```php
343+
<?php
344+
// api/src/State/BlogPostProvider.php
345+
346+
namespace App\State;
347+
348+
use App\Dto\AnotherRepresentation;
349+
use App\Models\Book;
350+
use ApiPlatform\Metadata\Operation;
351+
use ApiPlatform\State\ProviderInterface;
352+
use Symfony\Component\DependencyInjection\Attribute\Autowire;
353+
354+
/**
355+
* @implements ProviderInterface<AnotherRepresentation>
356+
*/
357+
final class BookRepresentationProvider implements ProviderInterface
358+
{
359+
public function __construct(
360+
private ProviderInterface $itemProvider,
361+
)
362+
{
363+
}
364+
365+
public function provide(Operation $operation, array $uriVariables = [], array $context = []): AnotherRepresentation
366+
{
367+
$book = $this->itemProvider->provide($operation, $uriVariables, $context);
368+
369+
return new AnotherRepresentation(
370+
// Add DTO constructor params here.
371+
// $book->getTitle(),
372+
);
373+
}
374+
}
375+
```
376+
377+
And configure that you want to use this provider on the Book resource:
378+
379+
```php
380+
<?php
381+
// api/app/Models/Book.php
382+
383+
namespace App\Models;
384+
385+
use ApiPlatform\Metadata\Get;
386+
use App\Dto\AnotherRepresentation;
387+
use App\State\BookRepresentationProvider;
388+
389+
#[Get(output: AnotherRepresentation::class, provider: BookRepresentationProvider::class)]
390+
class Book {}
391+
```
392+
393+
394+
## Registering Services Without Autowiring (only for the Symfony variant)
191395

192396
The services in the previous examples are automatically registered because
193397
[autowiring](https://symfony.com/doc/current/service_container/autowiring.html)

0 commit comments

Comments
 (0)