Skip to content
Open
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: 33 additions & 1 deletion src/JsonLd/ContextBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
final class ContextBuilder implements AnonymousContextBuilderInterface
final class ContextBuilder implements AnonymousContextBuilderInterface, OperationContextBuilderInterface
{
use ClassInfoTrait;
use HydraPrefixTrait;
Expand Down Expand Up @@ -164,6 +164,38 @@ public function getAnonymousResourceContext(object $object, array $context = [],
return $jsonLdContext;
}

/**
* {@inheritdoc}
*/
public function getResourceContextUriFromOperation(HttpOperation $operation, ?int $referenceType = null): string
{
if (null === $referenceType) {
$referenceType = $operation->getUrlGenerationStrategy();
}

return $this->urlGenerator->generate('api_jsonld_context', ['shortName' => $operation->getShortName()], $referenceType ?? UrlGeneratorInterface::ABS_PATH);
}

/**
* {@inheritdoc}
*/
public function getResourceContextFromOperation(HttpOperation $operation, string $resourceClass, int $referenceType = UrlGeneratorInterface::ABS_PATH): array
{
if (null === $shortName = $operation->getShortName()) {
return [];
}

$context = $operation->getNormalizationContext();
if ($context['iri_only'] ?? false) {
$context = $this->getBaseContext($referenceType);
$context[$this->getHydraPrefix($context).'member']['@type'] = '@id';

return $context;
}

return $this->getResourceContextWithShortname($resourceClass, $referenceType, $shortName, $operation);
}

private function getResourceContextWithShortname(string $resourceClass, int $referenceType, string $shortName, ?HttpOperation $operation = null): array
{
$context = $this->getBaseContext($referenceType);
Expand Down
39 changes: 39 additions & 0 deletions src/JsonLd/OperationContextBuilderInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace ApiPlatform\JsonLd;

use ApiPlatform\Metadata\HttpOperation;
use ApiPlatform\Metadata\UrlGeneratorInterface;

/**
* JSON-LD context builder that is aware of the current operation.
*
* This interface extends ContextBuilderInterface with operation-aware methods
* to correctly resolve context URIs when a resource class has multiple
* ApiResource attributes with different shortNames.
*
* @author Antoine Bluchet <soyuka@gmail.com>
*/
interface OperationContextBuilderInterface extends ContextBuilderInterface
{
/**
* Gets the URI of the resource context for a specific operation.
*/
public function getResourceContextUriFromOperation(HttpOperation $operation, ?int $referenceType = null): string;

/**
* Gets the resource context for a specific operation.
*/
public function getResourceContextFromOperation(HttpOperation $operation, string $resourceClass, int $referenceType = UrlGeneratorInterface::ABS_PATH): array;
}
3 changes: 3 additions & 0 deletions src/JsonLd/Serializer/ItemNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,9 @@ public function normalize(mixed $data, ?string $format = null, array $context =
$metadata = [];
if ($isResourceClass = $this->resourceClassResolver->isResourceClass($resourceClass) && (null === $previousResourceClass || $this->resourceClassResolver->isResourceClass($previousResourceClass))) {
$resourceClass = $this->resourceClassResolver->getResourceClass($data, $previousResourceClass);
if (isset($context['operation']) && $context['operation'] instanceof HttpOperation && $context['operation']->getClass() !== $resourceClass) {
$context['operation'] = $this->resourceMetadataCollectionFactory->create($resourceClass)->getOperation(null, false, true);
}
$context = $this->initContext($resourceClass, $context);
$metadata = $this->addJsonLdContext($this->contextBuilder, $resourceClass, $context);
} elseif ($this->contextBuilder instanceof AnonymousContextBuilderInterface) {
Expand Down
13 changes: 11 additions & 2 deletions src/JsonLd/Serializer/JsonLdContextTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
use ApiPlatform\JsonLd\AnonymousContextBuilderInterface;
use ApiPlatform\JsonLd\ContextBuilder;
use ApiPlatform\JsonLd\ContextBuilderInterface;
use ApiPlatform\JsonLd\OperationContextBuilderInterface;
use ApiPlatform\Metadata\HttpOperation;

/**
* Creates and manipulates the Serializer context.
Expand All @@ -37,13 +39,20 @@ private function addJsonLdContext(ContextBuilderInterface $contextBuilder, strin

$context['jsonld_has_context'] = true;

$operation = $context['operation'] ?? null;
$useOperationAware = $operation instanceof HttpOperation && $contextBuilder instanceof OperationContextBuilderInterface;

if (isset($context['jsonld_embed_context'])) {
$data['@context'] = $contextBuilder->getResourceContext($resourceClass);
$data['@context'] = $useOperationAware
? $contextBuilder->getResourceContextFromOperation($operation, $resourceClass)
: $contextBuilder->getResourceContext($resourceClass);

return $data;
}

$data['@context'] = $contextBuilder->getResourceContextUri($resourceClass);
$data['@context'] = $useOperationAware
? $contextBuilder->getResourceContextUriFromOperation($operation)
: $contextBuilder->getResourceContextUri($resourceClass);

return $data;
}
Expand Down
45 changes: 45 additions & 0 deletions tests/Fixtures/TestBundle/Entity/MultiResourceEntity.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace ApiPlatform\Tests\Fixtures\TestBundle\Entity;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
#[ApiResource(
shortName: 'AdminMultiResource',
operations: [
new Get(uriTemplate: '/admin/multi_resources/{id}'),
new GetCollection(uriTemplate: '/admin/multi_resources'),
],
)]
#[ApiResource(
shortName: 'MultiResource',
operations: [
new Get(uriTemplate: '/multi_resources/{id}'),
new GetCollection(uriTemplate: '/multi_resources'),
],
)]
class MultiResourceEntity
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
public ?int $id = null;

#[ORM\Column(length: 255)]
public string $title = '';
}
71 changes: 35 additions & 36 deletions tests/Functional/JsonLdTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,14 @@
use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\ItemUriTemplateWithCollection\RecipeCollection;
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue6465\Bar;
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue6465\Foo;
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\MultiResourceEntity;
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Recipe as EntityRecipe;
use ApiPlatform\Tests\RecreateSchemaTrait;
use ApiPlatform\Tests\SetupClassResourcesTrait;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Tools\SchemaTool;

class JsonLdTest extends ApiTestCase
{
use RecreateSchemaTrait;
use SetupClassResourcesTrait;

protected static ?bool $alwaysBootKernel = false;
Expand All @@ -55,6 +56,7 @@ public static function getResources(): array
ImageModuleResource::class,
Recipe::class,
RecipeCollection::class,
MultiResourceEntity::class,
];
}

Expand Down Expand Up @@ -226,28 +228,41 @@ public function testItemUriTemplateWithStateOption(): void
]);
}

/**
* Tests that @context uses the correct shortName when an entity has multiple ApiResource attributes.
*/
public function testMultiResourceContextUsesCorrectShortName(): void
{
if ($this->isMongoDB()) {
$this->markTestSkipped();
}

// Test the second declared ApiResource (shortName: 'MultiResource')
$response = self::createClient()->request('GET', '/multi_resources');
$this->assertResponseIsSuccessful();
$this->assertJsonContains([
'@context' => '/contexts/MultiResource',
]);

// Test the first declared ApiResource (shortName: 'AdminMultiResource')
$response = self::createClient()->request('GET', '/admin/multi_resources');
$this->assertResponseIsSuccessful();
$this->assertJsonContains([
'@context' => '/contexts/AdminMultiResource',
]);
}

protected function setUp(): void
{
self::bootKernel();

$container = static::getContainer();
$registry = $container->get('doctrine');
$manager = $registry->getManager();
if (!$manager instanceof EntityManagerInterface) {
return;
}

$classes = [];
foreach ([Foo::class, Bar::class, EntityRecipe::class] as $entityClass) {
$classes[] = $manager->getClassMetadata($entityClass);
if ($this->isMongoDB()) {
$this->markTestSkipped('This test uses Doctrine ORM entities without MongoDB equivalents.');
}

try {
$schemaTool = new SchemaTool($manager);
@$schemaTool->createSchema($classes);
} catch (\Exception $e) {
}
$this->recreateSchema([Foo::class, Bar::class, EntityRecipe::class, MultiResourceEntity::class]);

$manager = $this->getManager();
$foo = new Foo();
$foo->title = 'Foo';
$manager->persist($foo);
Expand All @@ -260,25 +275,9 @@ protected function setUp(): void
$bar2 = new Bar();
$bar2->title = 'Bar two';
$manager->persist($bar2);
$multi = new MultiResourceEntity();
$multi->title = 'Multi Resource';
$manager->persist($multi);
$manager->flush();
}

protected function tearDown(): void
{
$container = static::getContainer();
$registry = $container->get('doctrine');
$manager = $registry->getManager();
if (!$manager instanceof EntityManagerInterface) {
return;
}

$classes = [];
foreach ([Foo::class, Bar::class] as $entityClass) {
$classes[] = $manager->getClassMetadata($entityClass);
}

$schemaTool = new SchemaTool($manager);
@$schemaTool->dropSchema($classes);
parent::tearDown();
}
}
Loading