@@ -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
42107require './vendor/autoload.php';
43108
109+ use App\BookProcessor;
110+ use App\BookProvider;
44111use ApiPlatform\Hydra\Serializer\CollectionFiltersNormalizer;
45112use ApiPlatform\Hydra\Serializer\CollectionNormalizer as HydraCollectionNormalizer;
46- use ApiPlatform\Hydra\Serializer\ConstraintViolationListNormalizer as HydraConstraintViolationListNormalizer;
47- use ApiPlatform\Hydra\Serializer\EntrypointNormalizer as HydraEntrypointNormalizer;
48113use ApiPlatform\Hydra\Serializer\PartialCollectionViewNormalizer;
114+ use ApiPlatform\JsonLd\Action\ContextAction;
49115use ApiPlatform\JsonLd\ContextBuilder as JsonLdContextBuilder;
50116use ApiPlatform\JsonLd\Serializer\ItemNormalizer as JsonLdItemNormalizer;
51117use ApiPlatform\JsonLd\Serializer\ObjectNormalizer as JsonLdObjectNormalizer;
52- use ApiPlatform\Metadata\ApiResource;
53- use ApiPlatform\Metadata\CollectionOperationInterface;
54118use ApiPlatform\Metadata\HttpOperation;
55119use ApiPlatform\Metadata\IdentifiersExtractor;
56- use ApiPlatform\Metadata\Operation;
57120use ApiPlatform\Metadata\Operation\UnderscorePathSegmentNameGenerator;
58121use ApiPlatform\Metadata\Property\Factory\AttributePropertyMetadataFactory;
59122use ApiPlatform\Metadata\Property\Factory\PropertyInfoPropertyMetadataFactory;
@@ -117,7 +180,6 @@ use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
117180use Symfony\Component\Routing\Generator\UrlGenerator;
118181use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
119182use Symfony\Component\Routing\Matcher\UrlMatcher;
120- use Symfony\Component\Routing\Matcher\UrlMatcherInterface;
121183use Symfony\Component\Routing\RequestContext;
122184use Symfony\Component\Routing\Route;
123185use 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
679704php -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