Skip to content

Commit 231a2c6

Browse files
committed
Fix review issues in bootstrap documentation
- Fix double provider invocation: split ContentNegotiationProvider (standalone) and ReadProvider (with CallableProvider) for separate listeners - Move Book/BookProvider/BookProcessor to separate src/Book.php file with proper PSR-4 autoload instructions - Add JSON-LD context route (/contexts/{shortName}) for @context URLs - Remove unused normalizers (entrypoint without route, constraint violations without validation) - Remove unused $debug variable and symfony/validator from composer require - Add composer.json autoload configuration in Running section
1 parent 0dfdf14 commit 231a2c6

1 file changed

Lines changed: 112 additions & 87 deletions

File tree

core/bootstrap.md

Lines changed: 112 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -27,33 +27,96 @@ composer require \
2727
symfony/serializer \
2828
symfony/property-info \
2929
symfony/property-access \
30-
symfony/validator \
3130
phpdocumentor/reflection-docblock \
3231
willdurand/negotiation
3332
```
3433

3534
## Full Bootstrap Example
3635

37-
Create a `bootstrap.php` file at the root of your project:
36+
Create the following file structure:
37+
38+
```text
39+
├── bootstrap.php
40+
├── composer.json
41+
└── src/
42+
└── Book.php
43+
```
44+
45+
Create `src/Book.php`:
46+
47+
```php
48+
<?php
49+
50+
namespace App;
51+
52+
use ApiPlatform\Metadata\ApiResource;
53+
use ApiPlatform\Metadata\CollectionOperationInterface;
54+
use ApiPlatform\Metadata\Operation;
55+
use ApiPlatform\State\ProcessorInterface;
56+
use ApiPlatform\State\ProviderInterface;
57+
58+
#[ApiResource(provider: BookProvider::class, processor: BookProcessor::class)]
59+
class Book
60+
{
61+
public int $id;
62+
public string $title = '';
63+
}
64+
65+
class BookProvider implements ProviderInterface
66+
{
67+
public function provide(
68+
Operation $operation,
69+
array $uriVariables = [],
70+
array $context = [],
71+
): object|array|null {
72+
if ($operation instanceof CollectionOperationInterface) {
73+
$book = new Book();
74+
$book->id = 1;
75+
$book->title = 'API Platform';
76+
77+
return [$book];
78+
}
79+
80+
$book = new Book();
81+
$book->id = $uriVariables['id'];
82+
$book->title = 'API Platform';
83+
84+
return $book;
85+
}
86+
}
87+
88+
class BookProcessor implements ProcessorInterface
89+
{
90+
public function process(
91+
mixed $data,
92+
Operation $operation,
93+
array $uriVariables = [],
94+
array $context = [],
95+
): mixed {
96+
// Persist your data here
97+
return $data;
98+
}
99+
}
100+
```
101+
102+
Then create `bootstrap.php`:
38103

39104
```php
40105
<?php
41106

42107
require './vendor/autoload.php';
43108

109+
use App\BookProcessor;
110+
use App\BookProvider;
44111
use ApiPlatform\Hydra\Serializer\CollectionFiltersNormalizer;
45112
use ApiPlatform\Hydra\Serializer\CollectionNormalizer as HydraCollectionNormalizer;
46-
use ApiPlatform\Hydra\Serializer\ConstraintViolationListNormalizer as HydraConstraintViolationListNormalizer;
47-
use ApiPlatform\Hydra\Serializer\EntrypointNormalizer as HydraEntrypointNormalizer;
48113
use ApiPlatform\Hydra\Serializer\PartialCollectionViewNormalizer;
114+
use ApiPlatform\JsonLd\Action\ContextAction;
49115
use ApiPlatform\JsonLd\ContextBuilder as JsonLdContextBuilder;
50116
use ApiPlatform\JsonLd\Serializer\ItemNormalizer as JsonLdItemNormalizer;
51117
use ApiPlatform\JsonLd\Serializer\ObjectNormalizer as JsonLdObjectNormalizer;
52-
use ApiPlatform\Metadata\ApiResource;
53-
use ApiPlatform\Metadata\CollectionOperationInterface;
54118
use ApiPlatform\Metadata\HttpOperation;
55119
use ApiPlatform\Metadata\IdentifiersExtractor;
56-
use ApiPlatform\Metadata\Operation;
57120
use ApiPlatform\Metadata\Operation\UnderscorePathSegmentNameGenerator;
58121
use ApiPlatform\Metadata\Property\Factory\AttributePropertyMetadataFactory;
59122
use ApiPlatform\Metadata\Property\Factory\PropertyInfoPropertyMetadataFactory;
@@ -117,7 +180,6 @@ use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
117180
use Symfony\Component\Routing\Generator\UrlGenerator;
118181
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
119182
use Symfony\Component\Routing\Matcher\UrlMatcher;
120-
use Symfony\Component\Routing\Matcher\UrlMatcherInterface;
121183
use Symfony\Component\Routing\RequestContext;
122184
use Symfony\Component\Routing\Route;
123185
use Symfony\Component\Routing\RouteCollection;
@@ -136,7 +198,6 @@ use Symfony\Component\Serializer\Serializer;
136198
// 1. Configuration
137199
// ──────────────────────────────────────────────
138200

139-
$debug = true;
140201
$defaultContext = [];
141202
$formats = ['jsonld' => ['application/ld+json']];
142203
$patchFormats = ['json' => ['application/merge-patch+json']];
@@ -243,56 +304,7 @@ $filterLocator = new class implements ContainerInterface {
243304
};
244305

245306
// ──────────────────────────────────────────────
246-
// 6. User-defined Resource, Provider and Processor
247-
// ──────────────────────────────────────────────
248-
249-
// Place this class in src/Book.php (matching the path given to AttributesResourceNameCollectionFactory)
250-
251-
#[ApiResource(provider: BookProvider::class, processor: BookProcessor::class)]
252-
class Book
253-
{
254-
public int $id;
255-
public string $title = '';
256-
}
257-
258-
class BookProvider implements ProviderInterface
259-
{
260-
public function provide(
261-
Operation $operation,
262-
array $uriVariables = [],
263-
array $context = [],
264-
): object|array|null {
265-
if ($operation instanceof CollectionOperationInterface) {
266-
$book = new Book();
267-
$book->id = 1;
268-
$book->title = 'API Platform';
269-
270-
return [$book];
271-
}
272-
273-
$book = new Book();
274-
$book->id = $uriVariables['id'];
275-
$book->title = 'API Platform';
276-
277-
return $book;
278-
}
279-
}
280-
281-
class BookProcessor implements ProcessorInterface
282-
{
283-
public function process(
284-
mixed $data,
285-
Operation $operation,
286-
array $uriVariables = [],
287-
array $context = [],
288-
): mixed {
289-
// Persist your data here
290-
return $data;
291-
}
292-
}
293-
294-
// ──────────────────────────────────────────────
295-
// 7. State Provider and Processor Locators
307+
// 6. State Provider and Processor Locators
296308
// ──────────────────────────────────────────────
297309

298310
$providerLocator = new class implements ContainerInterface {
@@ -309,7 +321,7 @@ $providerLocator = new class implements ContainerInterface {
309321
return isset($this->providers[$id]);
310322
}
311323
};
312-
$providerLocator->providers[BookProvider::class] = new BookProvider();
324+
$providerLocator->providers[\App\BookProvider::class] = new BookProvider();
313325

314326
$processorLocator = new class implements ContainerInterface {
315327
/** @var array<string, ProcessorInterface> */
@@ -325,13 +337,13 @@ $processorLocator = new class implements ContainerInterface {
325337
return isset($this->processors[$id]);
326338
}
327339
};
328-
$processorLocator->processors[BookProcessor::class] = new BookProcessor();
340+
$processorLocator->processors[\App\BookProcessor::class] = new BookProcessor();
329341

330342
$callableProvider = new CallableProvider($providerLocator);
331343
$callableProcessor = new CallableProcessor($processorLocator);
332344

333345
// ──────────────────────────────────────────────
334-
// 8. Route Building
346+
// 7. Route Building
335347
// ──────────────────────────────────────────────
336348

337349
$propertyAccessor = PropertyAccess::createPropertyAccessor();
@@ -410,7 +422,7 @@ $routes->add('api_genid', new Route(
410422
));
411423

412424
// ──────────────────────────────────────────────
413-
// 9. Router and URL Generator
425+
// 8. Router and URL Generator
414426
// ──────────────────────────────────────────────
415427

416428
$requestContext = new RequestContext();
@@ -422,7 +434,7 @@ class Router implements RouterInterface
422434
{
423435
public function __construct(
424436
private RouteCollection $routes,
425-
private UrlMatcherInterface $matcher,
437+
private UrlMatcher $matcher,
426438
private UrlGeneratorInterface $generator,
427439
private RequestContext $context,
428440
) {
@@ -477,7 +489,7 @@ $router = new Router($routes, $matcher, $generator, $requestContext);
477489
$apiUrlGenerator = new ApiUrlGenerator($generator);
478490

479491
// ──────────────────────────────────────────────
480-
// 10. IRI Converter
492+
// 9. IRI Converter
481493
// ──────────────────────────────────────────────
482494

483495
$uriVariablesConverter = new UriVariablesConverter(
@@ -497,7 +509,7 @@ $iriConverter = new IriConverter(
497509
);
498510

499511
// ──────────────────────────────────────────────
500-
// 11. Serializer
512+
// 10. Serializer
501513
// ──────────────────────────────────────────────
502514

503515
$serializerContextBuilder = new SerializerContextBuilder($resourceMetadataFactory);
@@ -514,6 +526,18 @@ $jsonLdContextBuilder = new JsonLdContextBuilder(
514526
$nameConverter,
515527
);
516528

529+
// Add the JSON-LD context route (required for @context URLs in responses)
530+
$contextAction = new ContextAction(
531+
$jsonLdContextBuilder,
532+
$resourceNameCollectionFactory,
533+
$resourceMetadataFactory,
534+
);
535+
$routes->add('api_jsonld_context', new Route(
536+
'/contexts/{shortName}.{_format}',
537+
['_controller' => $contextAction, '_format' => 'jsonld', '_api_respond' => true],
538+
['shortName' => '.+'],
539+
));
540+
517541
// JSON-LD normalizers
518542
$jsonLdItemNormalizer = new JsonLdItemNormalizer(
519543
$resourceMetadataFactory,
@@ -558,17 +582,6 @@ $hydraCollectionFiltersNormalizer = new CollectionFiltersNormalizer(
558582
$filterLocator,
559583
);
560584

561-
$hydraEntrypointNormalizer = new HydraEntrypointNormalizer(
562-
$resourceMetadataFactory,
563-
$iriConverter,
564-
$apiUrlGenerator,
565-
);
566-
567-
$hydraConstraintViolationNormalizer = new HydraConstraintViolationListNormalizer(
568-
[],
569-
$nameConverter,
570-
);
571-
572585
// Core normalizer
573586
$itemNormalizer = new ItemNormalizer(
574587
$propertyNameCollectionFactory,
@@ -592,8 +605,6 @@ $dateTimeNormalizer = new DateTimeNormalizer($defaultContext);
592605
// Register normalizers with priorities (same as Symfony bundle)
593606
$list = new \SplPriorityQueue();
594607
$list->insert($unwrappingDenormalizer, 1000);
595-
$list->insert($hydraConstraintViolationNormalizer, -780);
596-
$list->insert($hydraEntrypointNormalizer, -800);
597608
$list->insert($hydraCollectionFiltersNormalizer, -800);
598609
$list->insert($jsonLdItemNormalizer, -890);
599610
$list->insert($jsonLdObjectNormalizer, -995);
@@ -606,18 +617,20 @@ $encoders = [new JsonEncoder(), new ApiJsonLdEncoder('jsonld', new JsonEncoder()
606617
$serializer = new Serializer(iterator_to_array($list), $encoders);
607618

608619
// ──────────────────────────────────────────────
609-
// 12. State Providers and Processors
620+
// 11. State Providers and Processors
610621
// ──────────────────────────────────────────────
611622

612-
// Provider chain: reads data from the user's provider
613-
$readProvider = new ReadProvider($callableProvider, $serializerContextBuilder, $logger);
623+
// Content negotiation provider (standalone: only negotiates format, does not read data)
614624
$contentNegotiationProvider = new ContentNegotiationProvider(
615-
$readProvider,
625+
null,
616626
new Negotiator(),
617627
$formats,
618628
$errorFormats,
619629
);
620630

631+
// Read provider: fetches data from the user's provider
632+
$readProvider = new ReadProvider($callableProvider, $serializerContextBuilder, $logger);
633+
621634
// Processor chain: writes data, serializes, and creates the HTTP response
622635
$respondProcessor = new RespondProcessor(
623636
$iriConverter,
@@ -633,12 +646,12 @@ $serializeProcessor = new SerializeProcessor(
633646
);
634647

635648
// ──────────────────────────────────────────────
636-
// 13. Event Listeners and HttpKernel
649+
// 12. Event Listeners and HttpKernel
637650
// ──────────────────────────────────────────────
638651

639652
$formatListener = new AddFormatListener($contentNegotiationProvider, $resourceMetadataFactory);
640653
$readListener = new ReadListener(
641-
$contentNegotiationProvider,
654+
$readProvider,
642655
$resourceMetadataFactory,
643656
$uriVariablesConverter,
644657
);
@@ -673,7 +686,19 @@ $kernel->terminate($request, $response);
673686

674687
## Running
675688

676-
Start the PHP built-in server:
689+
Make sure your `composer.json` includes the PSR-4 autoload for the `src/` directory:
690+
691+
```json
692+
{
693+
"autoload": {
694+
"psr-4": {
695+
"App\\": "src/"
696+
}
697+
}
698+
}
699+
```
700+
701+
Then run `composer dump-autoload` and start the PHP built-in server:
677702

678703
```console
679704
php -S localhost:8000 bootstrap.php
@@ -692,7 +717,7 @@ This bootstrap provides a minimal JSON-LD/Hydra API. To add more features, you c
692717

693718
- **Deserialization**: add `DeserializeProvider` and `DeserializeListener` for POST/PUT/PATCH
694719
support
695-
- **Validation**: add `ValidateProvider` and `ValidateListener` with Symfony Validator
720+
- **Validation**: add `ValidateProvider` and `ValidateListener` with `symfony/validator`
696721
- **HAL/JSON:API**: register the corresponding normalizers with additional `api-platform/hal` or
697722
`api-platform/jsonapi` packages
698723
- **OpenAPI**: add `OpenApiFactory` and `OpenApiNormalizer` for automatic API documentation

0 commit comments

Comments
 (0)