Skip to content

Commit

Permalink
Merge pull request #2175 from usu/feature/content-type-entity-path
Browse files Browse the repository at this point in the history
inject entityPath into ContentType
  • Loading branch information
carlobeltrame authored Nov 9, 2021
2 parents 89978e8 + 93fedc5 commit a441368
Show file tree
Hide file tree
Showing 13 changed files with 224 additions and 18 deletions.
3 changes: 3 additions & 0 deletions api/config/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ services:
App\Serializer\Normalizer\CircularReferenceDetectingHalItemNormalizer:
decorates: 'api_platform.hal.normalizer.item'

App\Serializer\Normalizer\ContentTypeNormalizer:
decorates: 'api_platform.hal.normalizer.item'

App\Serializer\Normalizer\UriTemplateNormalizer:
decorates: 'api_platform.hal.normalizer.entrypoint'

Expand Down
2 changes: 1 addition & 1 deletion api/src/Entity/ContentNode.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
denormalizationContext: ['groups' => ['write']],
normalizationContext: ['groups' => ['read']],
)]
#[ApiFilter(SearchFilter::class, properties: ['parent'])]
#[ApiFilter(SearchFilter::class, properties: ['parent', 'contentType'])]
abstract class ContentNode extends BaseEntity implements BelongsToCampInterface {
/**
* @ORM\OneToOne(targetEntity="AbstractContentNodeOwner", mappedBy="rootContentNode", cascade={"persist"})
Expand Down
15 changes: 15 additions & 0 deletions api/src/Entity/ContentType.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,19 @@ class ContentType extends BaseEntity {
*/
#[ApiProperty(writable: false)]
public ?array $jsonConfig = [];

/**
* API endpoint link for creating new entities of type entityClass.
*/
#[Groups(['read'])]
#[ApiProperty(
example: '/content_node/column_layouts?contentType=%2Fcontent_types%2F1a2b3c4d',
openapiContext: [
'type' => 'array',
'format' => 'iri-reference',
]
)]
public function getContentNodes(): array {
return []; // empty here; actual content is filled/decorated in ContentTypeNormalizer
}
}
2 changes: 1 addition & 1 deletion api/src/Entity/Day.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ public function getDayNumber(): int {
* @return ScheduleEntry[]
*/
#[ApiProperty(example: '/schedule_entries?period=%2Fperiods%2F1a2b3c4d&start%5Bstrictly_before%5D=2022-01-03T00%3A00%3A00%2B00%3A00&end%5Bafter%5D=2022-01-02T00%3A00%3A00%2B00%3A00')]
#[RelatedCollectionLink('scheduleEntry', ['period' => 'period', 'start[strictly_before]' => 'end', 'end[after]' => 'start'])]
#[RelatedCollectionLink(ScheduleEntry::class, ['period' => 'period', 'start[strictly_before]' => 'end', 'end[after]' => 'start'])]
#[Groups(['read'])]
public function getScheduleEntries(): array {
return array_filter(
Expand Down
16 changes: 15 additions & 1 deletion api/src/Metadata/Resource/Factory/UriTemplateFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,25 @@ public function __construct(
* @return array contains the templated URI (or null when not successful), as well as a boolean flag which
* indicates whether any template parameters are present in the URI
*/
public function create(string $shortName): array {
public function createFromShortname(string $shortName): array {
$resourceClass = $this->resourceNameMapping[lcfirst($shortName)] ?? null;

if (!$resourceClass) {
return [null, false];
}

return $this->createFromResourceClass($resourceClass);
}

/**
* Create an URI template based on the allowed parameters for the specified entity.
*
* @throws ResourceClassNotFoundException
*
* @return array contains the templated URI (or null when not successful), as well as a boolean flag which
* indicates whether any template parameters are present in the URI
*/
public function createFromResourceClass(string $resourceClass): array {
$resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);

$baseUri = $this->iriConverter->getIriFromResourceClass($resourceClass);
Expand Down
52 changes: 52 additions & 0 deletions api/src/Serializer/Normalizer/ContentTypeNormalizer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

namespace App\Serializer\Normalizer;

use ApiPlatform\Core\Api\IriConverterInterface;
use App\Entity\ContentType;
use App\Metadata\Resource\Factory\UriTemplateFactory;
use Rize\UriTemplate;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\SerializerAwareInterface;
use Symfony\Component\Serializer\SerializerInterface;

/**
* Adds API link to contentNodes for ContentType based on the defined 'entityClass'.
*/
class ContentTypeNormalizer implements NormalizerInterface, SerializerAwareInterface {
public function __construct(
private NormalizerInterface $decorated,
private UriTemplate $uriTemplate,
private UriTemplateFactory $uriTemplateFactory,
private IriConverterInterface $iriConverter,
) {
}

public function supportsNormalization($data, $format = null) {
return $this->decorated->supportsNormalization($data, $format);
}

public function normalize($object, $format = null, array $context = []) {
$data = $this->decorated->normalize($object, $format, $context);

if ($object instanceof ContentType && isset($object->entityClass)) {
// get uri for the respective ContentNode entity and add ContentType as query parameter
[$uriTemplate, $templated] = $this->uriTemplateFactory->createFromResourceClass($object->entityClass);
$uri = $this->uriTemplate->expand($uriTemplate, ['contentType' => $this->iriConverter->getIriFromItem($object)]);

// add uri as HAL link
$data['_links']['contentNodes']['href'] = $uri;

// unset the property itself (property definition was only needed to ensure proper API documentation)
unset($data['contentNodes']);
}

return $data;
}

public function setSerializer(SerializerInterface $serializer) {
if ($this->decorated instanceof SerializerAwareInterface) {
$this->decorated->setSerializer($serializer);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ public function getRelatedCollectionHref($object, $rel, array $context = []): st
if ($annotation = $this->getRelatedCollectionLinkAnnotation($resourceClass, $rel)) {
// If there is an explicit annotation, there is no need to inspect the Doctrine metadata
$params = $this->extractUriParams($object, $annotation->getParams());
[$uriTemplate] = $this->uriTemplateFactory->create($annotation->getRelatedEntity());
[$uriTemplate] = $this->uriTemplateFactory->createFromResourceClass($annotation->getRelatedEntity());

return $this->uriTemplate->expand($uriTemplate, $params);
}
Expand Down
2 changes: 1 addition & 1 deletion api/src/Serializer/Normalizer/UriTemplateNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public function normalize($object, $format = null, array $context = []) {
continue;
}

[$uriTemplate, $templated] = $this->uriTemplateFactory->create($rel);
[$uriTemplate, $templated] = $this->uriTemplateFactory->createFromShortname($rel);
if (!$uriTemplate) {
continue;
}
Expand Down
10 changes: 10 additions & 0 deletions api/tests/Api/ContentTypes/ReadContentTypeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ public function testGetSingleContentTypeIsAllowedForAnonymousUser() {
'id' => $contentType->getId(),
'name' => $contentType->name,
'active' => $contentType->active,
'_links' => [
'contentNodes' => [
'href' => '/content_node/single_texts?contentType='.urlencode($this->getIriFor($contentType)),
],
],
]);
}

Expand All @@ -30,6 +35,11 @@ public function testGetSingleContentTypeIsAllowedForLoggedInUser() {
'id' => $contentType->getId(),
'name' => $contentType->name,
'active' => $contentType->active,
'_links' => [
'contentNodes' => [
'href' => '/content_node/single_texts?contentType='.urlencode($this->getIriFor($contentType)),
],
],
]);
}
}
12 changes: 6 additions & 6 deletions api/tests/Metadata/Resource/Factory/UriTemplateFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public function testCantCreateUriTemplateForNonexistentResource() {
$this->createFactory();

// when
[$uri, $templated] = $this->uriTemplateFactory->create($resource);
[$uri, $templated] = $this->uriTemplateFactory->createFromShortname($resource);

// then
self::assertThat($uri, self::equalTo(null));
Expand All @@ -68,7 +68,7 @@ public function testCreatesNonTemplatedUri() {
$this->createFactory();

// when
[$uri, $templated] = $this->uriTemplateFactory->create($resource);
[$uri, $templated] = $this->uriTemplateFactory->createFromShortname($resource);

// then
self::assertThat($uri, self::equalTo('/dummys'));
Expand All @@ -81,7 +81,7 @@ public function testCreatesTemplatedUriWithIdPathParameter() {
$this->createFactory();

// when
[$uri, $templated] = $this->uriTemplateFactory->create($resource);
[$uri, $templated] = $this->uriTemplateFactory->createFromShortname($resource);

// then
self::assertThat($uri, self::equalTo('/dummys{/id}'));
Expand All @@ -100,7 +100,7 @@ public function testCreatesTemplatedUriWithFilterQueryParameter() {
$this->createFactory();

// when
[$uri, $templated] = $this->uriTemplateFactory->create($resource);
[$uri, $templated] = $this->uriTemplateFactory->createFromShortname($resource);

// then
self::assertThat($uri, self::equalTo('/dummys{/id}{?some_filter}'));
Expand All @@ -114,7 +114,7 @@ public function testCreatesTemplatedUriWithPaginationQueryParameter() {
$this->createFactory();

// when
[$uri, $templated] = $this->uriTemplateFactory->create($resource);
[$uri, $templated] = $this->uriTemplateFactory->createFromShortname($resource);

// then
self::assertThat($uri, self::equalTo('/dummys{/id}{?page}'));
Expand All @@ -132,7 +132,7 @@ public function testCreatesTemplatedUriWithAdvancedPaginationQueryParameters() {
$this->createFactory();

// when
[$uri, $templated] = $this->uriTemplateFactory->create($resource);
[$uri, $templated] = $this->uriTemplateFactory->createFromShortname($resource);

// then
self::assertThat($uri, self::equalTo('/dummys{/id}{?page,itemsPerPage,pagination}'));
Expand Down
112 changes: 112 additions & 0 deletions api/tests/Serializer/Normalizer/ContentTypeNormalizerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<?php

namespace App\Tests\Serializer\Normalizer;

use ApiPlatform\Core\Api\IriConverterInterface;
use ApiPlatform\Core\Bridge\Symfony\Routing\RouteNameResolverInterface;
use App\Entity\ContentType;
use App\Metadata\Resource\Factory\UriTemplateFactory;
use App\Serializer\Normalizer\ContentTypeNormalizer;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Rize\UriTemplate;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\SerializerInterface;

/**
* @internal
*/
class ContentTypeNormalizerTest extends TestCase {
private ContentTypeNormalizer $normalizer;

private MockObject|NormalizerInterface $decoratedMock;
private MockObject|RouteNameResolverInterface $routeNameResolver;
private MockObject|RouterInterface $routerMock;
private MockObject|IriConverterInterface $iriConverter;
private MockObject|UriTemplate $uriTemplate;
private MockObject|UriTemplateFactory $uriTemplateFactory;

protected function setUp(): void {
$this->decoratedMock = $this->createMock(ContextAwareNormalizerInterface::class);

$this->iriConverter = $this->createMock(IriConverterInterface::class);
$this->uriTemplate = $this->createMock(UriTemplate::class);
$this->uriTemplateFactory = $this->createMock(UriTemplateFactory::class);

$this->normalizer = new ContentTypeNormalizer(
$this->decoratedMock,
$this->uriTemplate,
$this->uriTemplateFactory,
$this->iriConverter,
);
$this->normalizer->setSerializer($this->createMock(SerializerInterface::class));
}

public function testDelegatesSupportCheckToDecorated() {
$this->decoratedMock
->expects($this->exactly(2))
->method('supportsNormalization')
->willReturnOnConsecutiveCalls(true, false)
;

$this->assertTrue($this->normalizer->supportsNormalization([]));
$this->assertFalse($this->normalizer->supportsNormalization([]));
}

public function testDelegatesNormalizeToDecorated() {
// given
$resource = new \stdClass();
$delegatedResult = [
'hello' => 'world',
];
$this->decoratedMock->expects($this->once())
->method('normalize')
->willReturn($delegatedResult)
;

// when
$result = $this->normalizer->normalize($resource, null, ['resource_class' => \stdClass::class]);

// then
$this->assertEquals($delegatedResult, $result);
}

public function testNormalizeAddsEntityPath() {
// given
$contentType = new ContentType();
$contentType->entityClass = 'App\Entity\ContentNode\DummyContentNode';

$delegatedResult = [
'hello' => 'world',
];
$this->decoratedMock->expects($this->once())
->method('normalize')
->willReturn($delegatedResult)
;
$this->uriTemplateFactory->expects($this->once())
->method('createFromResourceClass')
->willReturn(['/templatedUri', 'true'])
;

$this->uriTemplate->expects($this->once())
->method('expand')
->willReturn('/expandedUri')
;

// when
$result = $this->normalizer->normalize($contentType, null, ['resource_class' => ContentType::class]);

// then
$expectedResult = [
'hello' => 'world',
'_links' => [
'contentNodes' => [
'href' => '/expandedUri',
],
],
];
$this->assertEquals($expectedResult, $result);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -184,8 +184,8 @@ public function testNormalizeReplacesLinkArrayWithSingleFilteredCollectionLinkBa
$this->propertyAccessor->method('getValue')->willReturn('value');
$this->uriTemplateFactory
->expects($this->once())
->method('create')
->with('scheduleEntry')
->method('createFromResourceClass')
->with('App\\Entity\\DummyEntity')
->willReturn(['/relatedEntities{/id}{?test_param}', true])
;
$this->uriTemplate
Expand Down Expand Up @@ -497,7 +497,7 @@ public function getFilterValue(): string {
return '';
}

#[RelatedCollectionLink('scheduleEntry', ['test_param' => 'filterValue'])]
#[RelatedCollectionLink('App\\Entity\\DummyEntity', ['test_param' => 'filterValue'])]
public function getRelatedEntities(): array {
return [];
}
Expand Down
8 changes: 4 additions & 4 deletions api/tests/Serializer/Normalizer/UriTemplateNormalizerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public function testCreateNotTemplatedLinkIfNoParameters() {
'camp' => ['href' => '/camps'],
]]);
$resource = new Entrypoint(new ResourceNameCollection([Camp::class]));
$this->uriTemplateFactory->expects($this->once())->method('create')->willReturn(['/camps', false]);
$this->uriTemplateFactory->expects($this->once())->method('createFromShortname')->willReturn(['/camps', false]);

// when
$normalize = $this->uriTemplateNormalizer->normalize($resource);
Expand All @@ -68,7 +68,7 @@ public function testCreateTemplatedLinkIfPathParameters() {
'camp' => ['href' => '/camps'],
]]);
$resource = new Entrypoint(new ResourceNameCollection([Camp::class]));
$this->uriTemplateFactory->expects($this->once())->method('create')->willReturn(['/camps{/id}', true]);
$this->uriTemplateFactory->expects($this->once())->method('createFromShortname')->willReturn(['/camps{/id}', true]);

// when
$normalize = $this->uriTemplateNormalizer->normalize($resource);
Expand Down Expand Up @@ -96,7 +96,7 @@ public function testCreateTemplatedLinkForQueryParameters() {
'activity' => ['href' => '/activities'],
]]);
$resource = new Entrypoint(new ResourceNameCollection([Activity::class]));
$this->uriTemplateFactory->expects($this->once())->method('create')->willReturn(['/activities{?camp,camp[]}', true]);
$this->uriTemplateFactory->expects($this->once())->method('createFromShortname')->willReturn(['/activities{?camp,camp[]}', true]);

// when
$normalize = $this->uriTemplateNormalizer->normalize($resource);
Expand Down Expand Up @@ -124,7 +124,7 @@ public function testMergePathAndQueryParameter() {
'activity' => ['href' => '/activities'],
]]);
$resource = new Entrypoint(new ResourceNameCollection([Activity::class]));
$this->uriTemplateFactory->expects($this->once())->method('create')->willReturn(['/activities{/id}{?camp,camp[]}', true]);
$this->uriTemplateFactory->expects($this->once())->method('createFromShortname')->willReturn(['/activities{/id}{?camp,camp[]}', true]);

// when
$normalize = $this->uriTemplateNormalizer->normalize($resource);
Expand Down

0 comments on commit a441368

Please sign in to comment.