Skip to content

Commit 3dfe3ee

Browse files
committed
CollectionNormalizer
1 parent 5478b02 commit 3dfe3ee

4 files changed

Lines changed: 165 additions & 1 deletion

File tree

src/Hydra/Serializer/CollectionNormalizer.php

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use ApiPlatform\JsonLd\Serializer\HydraPrefixTrait;
1818
use ApiPlatform\JsonLd\Serializer\JsonLdContextTrait;
1919
use ApiPlatform\Metadata\IriConverterInterface;
20+
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
2021
use ApiPlatform\Metadata\ResourceClassResolverInterface;
2122
use ApiPlatform\Metadata\UrlGeneratorInterface;
2223
use ApiPlatform\Serializer\AbstractCollectionNormalizer;
@@ -33,6 +34,7 @@ final class CollectionNormalizer extends AbstractCollectionNormalizer
3334
{
3435
use HydraPrefixTrait;
3536
use JsonLdContextTrait;
37+
use HydraOperationsTrait;
3638

3739
public const FORMAT = 'jsonld';
3840
public const IRI_ONLY = 'iri_only';
@@ -42,7 +44,7 @@ final class CollectionNormalizer extends AbstractCollectionNormalizer
4244
self::PRESERVE_COLLECTION_KEYS => false,
4345
];
4446

45-
public function __construct(private readonly ContextBuilderInterface $contextBuilder, ResourceClassResolverInterface $resourceClassResolver, private readonly IriConverterInterface $iriConverter, array $defaultContext = [])
47+
public function __construct(private readonly ContextBuilderInterface $contextBuilder, ResourceClassResolverInterface $resourceClassResolver, private readonly IriConverterInterface $iriConverter, private ?ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null, array $defaultContext = [])
4648
{
4749
$this->defaultContext = array_merge($this->defaultContext, $defaultContext);
4850

@@ -70,6 +72,24 @@ protected function getPaginationData(iterable $object, array $context = []): arr
7072
$data[$hydraPrefix.'totalItems'] = \count($object);
7173
}
7274

75+
if (null !== $this->resourceMetadataCollectionFactory && ($context['hydra_operations'] ?? $this->defaultContext['hydra_operations'] ?? false)) {
76+
$allHydraOperations = [];
77+
foreach ($this->resourceMetadataCollectionFactory->create($resourceClass) as $resourceMetadata) {
78+
$hydraOperations = $this->getHydraOperations(
79+
true,
80+
$resourceMetadata,
81+
$hydraPrefix
82+
);
83+
if (!empty($hydraOperations)) {
84+
$allHydraOperations = array_merge($allHydraOperations, $hydraOperations);
85+
}
86+
}
87+
88+
if (!empty($allHydraOperations)) {
89+
$data[$hydraPrefix.'supportedOperation'] = $allHydraOperations;
90+
}
91+
}
92+
7393
return $data;
7494
}
7595

src/Hydra/Tests/Serializer/CollectionNormalizerTest.php

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,14 @@
1717
use ApiPlatform\Hydra\Tests\Fixtures\Foo;
1818
use ApiPlatform\JsonLd\ContextBuilder;
1919
use ApiPlatform\JsonLd\ContextBuilderInterface;
20+
use ApiPlatform\Metadata\ApiResource;
21+
use ApiPlatform\Metadata\GetCollection;
2022
use ApiPlatform\Metadata\IriConverterInterface;
23+
use ApiPlatform\Metadata\Operations;
24+
use ApiPlatform\Metadata\Post;
25+
use ApiPlatform\Metadata\Patch;
26+
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
27+
use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
2128
use ApiPlatform\Metadata\ResourceClassResolverInterface;
2229
use ApiPlatform\Metadata\UrlGeneratorInterface;
2330
use ApiPlatform\Serializer\AbstractItemNormalizer;
@@ -445,4 +452,139 @@ public function testNormalizeResourceCollectionWithoutPrefix(): void
445452
'totalItems' => 2,
446453
], $actual);
447454
}
455+
456+
public function testNormalizeCollectionWithHydraOperations(): void
457+
{
458+
$fooOne = new Foo();
459+
$fooOne->id = 1;
460+
$fooOne->bar = 'baz';
461+
462+
$data = [$fooOne];
463+
464+
$normalizedFooOne = [
465+
'@id' => '/foos/1',
466+
'@type' => 'Foo',
467+
'bar' => 'baz',
468+
];
469+
470+
$contextBuilderProphecy = $this->prophesize(ContextBuilderInterface::class);
471+
$contextBuilderProphecy->getResourceContextUri(Foo::class)->willReturn('/contexts/Foo');
472+
473+
$resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class);
474+
$resourceClassResolverProphecy->getResourceClass($data, Foo::class)->willReturn(Foo::class);
475+
476+
$iriConverterProphecy = $this->prophesize(IriConverterInterface::class);
477+
$iriConverterProphecy->getIriFromResource(Foo::class, UrlGeneratorInterface::ABS_PATH, Argument::any(), Argument::any())->willReturn('/foos');
478+
479+
$resourceMetadataCollectionFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class);
480+
$resourceMetadataCollectionFactoryProphecy->create(Foo::class)->willReturn(new ResourceMetadataCollection('Foo', [
481+
(new ApiResource())
482+
->withShortName('Foo')
483+
->withOperations(new Operations(['get' => (new GetCollection())->withShortName('Foo'), 'post' => (new Post())->withShortName('Foo')])),
484+
]));
485+
486+
$delegateNormalizerProphecy = $this->prophesize(NormalizerInterface::class);
487+
$delegateNormalizerProphecy->normalize($fooOne, CollectionNormalizer::FORMAT, Argument::allOf(
488+
Argument::withEntry('resource_class', Foo::class),
489+
Argument::withEntry('api_sub_level', true)
490+
))->willReturn($normalizedFooOne);
491+
492+
$normalizer = new CollectionNormalizer(
493+
$contextBuilderProphecy->reveal(),
494+
$resourceClassResolverProphecy->reveal(),
495+
$iriConverterProphecy->reveal(),
496+
$resourceMetadataCollectionFactoryProphecy->reveal(),
497+
['hydra_prefix' => false, 'hydra_operations' => true]
498+
);
499+
$normalizer->setNormalizer($delegateNormalizerProphecy->reveal());
500+
501+
$actual = $normalizer->normalize($data, CollectionNormalizer::FORMAT, [
502+
'operation_name' => 'get',
503+
'resource_class' => Foo::class,
504+
]);
505+
506+
$this->assertArrayHasKey('supportedOperation', $actual);
507+
$this->assertIsArray($actual['supportedOperation']);
508+
$this->assertNotEmpty($actual['supportedOperation']);
509+
510+
$methods = array_map(fn($op) => $op['method'] ?? null, $actual['supportedOperation']);
511+
$this->assertContains('GET', $methods);
512+
$this->assertContains('POST', $methods);
513+
514+
$this->assertArrayHasKey('@context', $actual);
515+
$this->assertArrayHasKey('@id', $actual);
516+
$this->assertArrayHasKey('@type', $actual);
517+
$this->assertArrayHasKey('member', $actual);
518+
$this->assertArrayHasKey('totalItems', $actual);
519+
}
520+
521+
public function testNormalizeCollectionWithMultipleApiResourcesAndHydraOperations(): void
522+
{
523+
$fooOne = new Foo();
524+
$fooOne->id = 1;
525+
$fooOne->bar = 'baz';
526+
527+
$data = [$fooOne];
528+
529+
$normalizedFooOne = [
530+
'@id' => '/foos/1',
531+
'@type' => 'Foo',
532+
'bar' => 'baz',
533+
];
534+
535+
$contextBuilderProphecy = $this->prophesize(ContextBuilderInterface::class);
536+
$contextBuilderProphecy->getResourceContextUri(Foo::class)->willReturn('/contexts/Foo');
537+
538+
$resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class);
539+
$resourceClassResolverProphecy->getResourceClass($data, Foo::class)->willReturn(Foo::class);
540+
541+
$iriConverterProphecy = $this->prophesize(IriConverterInterface::class);
542+
$iriConverterProphecy->getIriFromResource(Foo::class, UrlGeneratorInterface::ABS_PATH, Argument::any(), Argument::any())->willReturn('/foos');
543+
544+
$resourceMetadataCollectionFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class);
545+
$resourceMetadataCollectionFactoryProphecy->create(Foo::class)->willReturn(new ResourceMetadataCollection('Foo', [
546+
(new ApiResource())
547+
->withShortName('Foo')
548+
->withOperations(new Operations(['get' => (new GetCollection())->withShortName('Foo')])),
549+
(new ApiResource())
550+
->withShortName('Foo')
551+
->withOperations(new Operations(['post' => (new Post())->withShortName('Foo')])),
552+
]));
553+
554+
$delegateNormalizerProphecy = $this->prophesize(NormalizerInterface::class);
555+
$delegateNormalizerProphecy->normalize($fooOne, CollectionNormalizer::FORMAT, Argument::allOf(
556+
Argument::withEntry('resource_class', Foo::class),
557+
Argument::withEntry('api_sub_level', true)
558+
))->willReturn($normalizedFooOne);
559+
560+
$normalizer = new CollectionNormalizer(
561+
$contextBuilderProphecy->reveal(),
562+
$resourceClassResolverProphecy->reveal(),
563+
$iriConverterProphecy->reveal(),
564+
$resourceMetadataCollectionFactoryProphecy->reveal(),
565+
['hydra_prefix' => false, 'hydra_operations' => true]
566+
);
567+
$normalizer->setNormalizer($delegateNormalizerProphecy->reveal());
568+
569+
$actual = $normalizer->normalize($data, CollectionNormalizer::FORMAT, [
570+
'operation_name' => 'get',
571+
'resource_class' => Foo::class,
572+
]);
573+
574+
$this->assertArrayHasKey('supportedOperation', $actual);
575+
$this->assertIsArray($actual['supportedOperation']);
576+
577+
$this->assertCount(2, $actual['supportedOperation']);
578+
579+
$methods = array_map(fn($op) => $op['method'] ?? null, $actual['supportedOperation']);
580+
$this->assertContains('GET', $methods);
581+
$this->assertContains('POST', $methods);
582+
583+
$this->assertArrayHasKey('@context', $actual);
584+
$this->assertArrayHasKey('@id', $actual);
585+
$this->assertArrayHasKey('@type', $actual);
586+
$this->assertEquals('Collection', $actual['@type']);
587+
$this->assertArrayHasKey('member', $actual);
588+
$this->assertArrayHasKey('totalItems', $actual);
589+
}
448590
}

src/Laravel/ApiPlatformProvider.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -919,6 +919,7 @@ public function register(): void
919919
$app->make(ContextBuilderInterface::class),
920920
$app->make(ResourceClassResolverInterface::class),
921921
$app->make(IriConverterInterface::class),
922+
$app->make(ResourceMetadataCollectionFactoryInterface::class),
922923
$defaultContext
923924
),
924925
$app->make(ResourceMetadataCollectionFactoryInterface::class),

src/Symfony/Bundle/Resources/config/hydra.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
service('api_platform.jsonld.context_builder'),
6969
service('api_platform.resource_class_resolver'),
7070
service('api_platform.iri_converter'),
71+
service('api_platform.metadata.resource.metadata_collection_factory'),
7172
'%api_platform.serializer.default_context%',
7273
])
7374
->tag('serializer.normalizer', ['priority' => -985]);

0 commit comments

Comments
 (0)