Skip to content

Commit c855415

Browse files
committed
added maxDepth to ApiSubresource
1 parent 7bc7f79 commit c855415

File tree

10 files changed

+189
-6
lines changed

10 files changed

+189
-6
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@
66
/vendor/
77
/tests/Fixtures/app/cache/*
88
/tests/Fixtures/app/logs/*
9+
/.idea

src/Annotation/ApiSubresource.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,8 @@
2323
*/
2424
final class ApiSubresource
2525
{
26+
/**
27+
* @var int
28+
*/
29+
public $maxDepth;
2630
}

src/Metadata/Property/Factory/AnnotationSubresourceMetadataFactory.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,8 @@ private function updateMetadata(ApiSubresource $annotation, PropertyMetadata $pr
8282
$type = $propertyMetadata->getType();
8383
$isCollection = $type->isCollection();
8484
$resourceClass = $isCollection ? $type->getCollectionValueType()->getClassName() : $type->getClassName();
85+
$maxDepth = $annotation->maxDepth ? $annotation->maxDepth : null;
8586

86-
return $propertyMetadata->withSubresource(new SubresourceMetadata($resourceClass, $isCollection));
87+
return $propertyMetadata->withSubresource(new SubresourceMetadata($resourceClass, $isCollection, $maxDepth));
8788
}
8889
}

src/Metadata/Property/SubresourceMetadata.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,13 @@ final class SubresourceMetadata
2222
{
2323
private $resourceClass;
2424
private $collection;
25+
private $maxDepth;
2526

26-
public function __construct(string $resourceClass, bool $collection = false)
27+
public function __construct(string $resourceClass, bool $collection = false, int $maxDepth = null)
2728
{
2829
$this->resourceClass = $resourceClass;
2930
$this->collection = $collection;
31+
$this->maxDepth = $maxDepth;
3032
}
3133

3234
public function getResourceClass(): string
@@ -54,4 +56,12 @@ public function withCollection(bool $collection): self
5456

5557
return $metadata;
5658
}
59+
60+
/**
61+
* @return int
62+
*/
63+
public function getMaxDepth()
64+
{
65+
return $this->maxDepth;
66+
}
5767
}

src/Metadata/schema/metadata.xsd

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
<xsd:complexType name="subresource" mixed="true">
8181
<xsd:attribute type="xsd:boolean" name="collection"/>
8282
<xsd:attribute type="xsd:string" name="resourceClass"/>
83+
<xsd:attribute type="xsd:integer" name="maxDepth"/>
8384
</xsd:complexType>
8485

8586
<xsd:complexType name="property">

src/Operation/Factory/SubresourceOperationFactory.php

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,10 @@ public function create(string $resourceClass): array
6060
* @param string $rootResourceClass null on the first iteration, it then keeps track of the origin resource class
6161
* @param array $parentOperation the previous call operation
6262
* @param array $visited
63+
* @param int $depth the number of visited
64+
* @param int $maxDepth
6365
*/
64-
private function computeSubresourceOperations(string $resourceClass, array &$tree, string $rootResourceClass = null, array $parentOperation = null, array $visited = [])
66+
private function computeSubresourceOperations(string $resourceClass, array &$tree, string $rootResourceClass = null, array $parentOperation = null, array $visited = [], int $depth = 0, int $maxDepth = null)
6567
{
6668
if (null === $rootResourceClass) {
6769
$rootResourceClass = $resourceClass;
@@ -79,6 +81,15 @@ private function computeSubresourceOperations(string $resourceClass, array &$tre
7981
$subresourceMetadata = $this->resourceMetadataFactory->create($subresourceClass);
8082

8183
$visiting = "$resourceClass $property $subresourceClass";
84+
85+
// Handle maxDepth
86+
if ($rootResourceClass === $resourceClass) {
87+
$maxDepth = $subresource->getMaxDepth();
88+
}
89+
90+
if(null !== $maxDepth && $depth >= $maxDepth) {
91+
continue;
92+
}
8293
if (isset($visited[$visiting])) {
8394
continue;
8495
}
@@ -154,7 +165,7 @@ private function computeSubresourceOperations(string $resourceClass, array &$tre
154165

155166
$tree[$operation['route_name']] = $operation;
156167

157-
$this->computeSubresourceOperations($subresourceClass, $tree, $rootResourceClass, $operation, $visited + [$visiting => true]);
168+
$this->computeSubresourceOperations($subresourceClass, $tree, $rootResourceClass, $operation, $visited + [$visiting => true], ++$depth, $maxDepth);
158169
}
159170
}
160171
}

tests/Fixtures/FileConfigurations/resources.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
writableLink="false"
5555
required="true"
5656
>
57-
<subresource collection="true" resourceClass="Foo" />
57+
<subresource collection="true" resourceClass="Foo" maxDepth="1" />
5858
<attribute name="foo">
5959
<attribute>Foo</attribute>
6060
</attribute>

tests/Fixtures/FileConfigurations/resources.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ resources:
2828
iri: 'someirischema'
2929
properties:
3030
'foo':
31-
subresource: {collection: true, resourceClass: 'Foo'}
31+
subresource: {collection: true, resourceClass: 'Foo', maxDepth: 1}
3232
description: 'The dummy foo'
3333
readable: true
3434
writable: true
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Core\Tests\Metadata\Property\Factory;
15+
16+
use ApiPlatform\Core\Annotation\ApiProperty;
17+
use ApiPlatform\Core\Exception\PropertyNotFoundException;
18+
use ApiPlatform\Core\Metadata\Property\Factory\AnnotationPropertyMetadataFactory;
19+
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
20+
use ApiPlatform\Core\Metadata\Property\PropertyMetadata;
21+
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy;
22+
use Doctrine\Common\Annotations\Reader;
23+
use PHPUnit\Framework\TestCase;
24+
use Prophecy\Argument;
25+
use Prophecy\Prophecy\ProphecyInterface;
26+
27+
/**
28+
* @author Titouan BENOIT <[email protected]>
29+
*/
30+
class AnnotationSubresourceMetadataFactoryTest extends TestCase
31+
{
32+
/**
33+
* @dataProvider getDependencies
34+
*/
35+
public function testCreateProperty(ProphecyInterface $reader, ProphecyInterface $decorated = null, string $description)
36+
{
37+
$factory = new AnnotationSubresourceMetadataFactory($reader->reveal(), $decorated ? $decorated->reveal() : null);
38+
$metadata = $factory->create(Dummy::class, 'subresource');
39+
40+
$this->assertEquals($description, $metadata->getDescription());
41+
$this->assertTrue($metadata->isReadable());
42+
$this->assertTrue($metadata->isWritable());
43+
$this->assertFalse($metadata->isReadableLink());
44+
$this->assertFalse($metadata->isWritableLink());
45+
$this->assertFalse($metadata->isIdentifier());
46+
$this->assertTrue($metadata->isRequired());
47+
$this->assertEquals('foo', $metadata->getIri());
48+
$this->assertEquals(['foo' => 'bar'], $metadata->getAttributes());
49+
}
50+
51+
public function getDependencies()
52+
{
53+
$annotation = new ApiProperty();
54+
$annotation->description = 'description';
55+
$annotation->readable = true;
56+
$annotation->writable = true;
57+
$annotation->readableLink = false;
58+
$annotation->writableLink = false;
59+
$annotation->identifier = false;
60+
$annotation->required = true;
61+
$annotation->iri = 'foo';
62+
$annotation->attributes = ['foo' => 'bar'];
63+
64+
$propertyReaderProphecy = $this->prophesize(Reader::class);
65+
$propertyReaderProphecy->getPropertyAnnotation(Argument::type(\ReflectionProperty::class), ApiProperty::class)->willReturn($annotation)->shouldBeCalled();
66+
67+
$getterReaderProphecy = $this->prophesize(Reader::class);
68+
$getterReaderProphecy->getPropertyAnnotation(Argument::type(\ReflectionProperty::class), ApiProperty::class)->willReturn(null)->shouldBeCalled();
69+
$getterReaderProphecy->getMethodAnnotation(Argument::type(\ReflectionMethod::class), ApiProperty::class)->willReturn($annotation)->shouldBeCalled();
70+
71+
$setterReaderProphecy = $this->prophesize(Reader::class);
72+
$setterReaderProphecy->getPropertyAnnotation(Argument::type(\ReflectionProperty::class), ApiProperty::class)->willReturn(null)->shouldBeCalled();
73+
$setterReaderProphecy->getMethodAnnotation(Argument::type(\ReflectionMethod::class), ApiProperty::class)->willReturn(null)->shouldBeCalled();
74+
$setterReaderProphecy->getMethodAnnotation(Argument::type(\ReflectionMethod::class), ApiProperty::class)->willReturn($annotation)->shouldBeCalled();
75+
76+
$decoratedThrowNotFoundProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
77+
$decoratedThrowNotFoundProphecy->create(Dummy::class, 'name', [])->willThrow(new PropertyNotFoundException())->shouldBeCalled();
78+
79+
$decoratedReturnProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
80+
$decoratedReturnProphecy->create(Dummy::class, 'name', [])->willReturn(new PropertyMetadata(null, 'Hi'))->shouldBeCalled();
81+
82+
return [
83+
[$propertyReaderProphecy, null, 'description'],
84+
[$getterReaderProphecy, $decoratedThrowNotFoundProphecy, 'description'],
85+
[$setterReaderProphecy, $decoratedThrowNotFoundProphecy, 'description'],
86+
[$setterReaderProphecy, $decoratedReturnProphecy, 'Hi'],
87+
];
88+
}
89+
90+
/**
91+
* @expectedException \ApiPlatform\Core\Exception\PropertyNotFoundException
92+
* @expectedExceptionMessage Property "foo" of class "\DoNotExist" not found.
93+
*/
94+
public function testClassNotFound()
95+
{
96+
$factory = new AnnotationPropertyMetadataFactory($this->prophesize(Reader::class)->reveal());
97+
$factory->create('\DoNotExist', 'foo');
98+
}
99+
100+
public function testClassNotFoundButParentFound()
101+
{
102+
$propertyMetadata = new PropertyMetadata();
103+
104+
$decoratedProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
105+
$decoratedProphecy->create('\DoNotExist', 'foo', [])->willReturn($propertyMetadata);
106+
107+
$factory = new AnnotationPropertyMetadataFactory($this->prophesize(Reader::class)->reveal(), $decoratedProphecy->reveal());
108+
$this->assertEquals($propertyMetadata, $factory->create('\DoNotExist', 'foo'));
109+
}
110+
}

tests/Operation/Factory/SubresourceOperationFactoryTest.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,4 +259,49 @@ public function testCreateByOverriding()
259259
] + SubresourceOperationFactory::ROUTE_OPTIONS,
260260
], $subresourceOperationFactory->create(DummyEntity::class));
261261
}
262+
263+
public function testCreateWithMaxDepth()
264+
{
265+
$resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
266+
$resourceMetadataFactoryProphecy->create(RelatedDummyEntity::class)->shouldBeCalled()->willReturn(new ResourceMetadata('relatedDummyEntity'));
267+
$resourceMetadataFactoryProphecy->create(DummyEntity::class)->shouldBeCalled()->willReturn(new ResourceMetadata('dummyEntity'));
268+
269+
$propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class);
270+
$propertyNameCollectionFactoryProphecy->create(DummyEntity::class)->shouldBeCalled()->willReturn(new PropertyNameCollection(['subresource']));
271+
$propertyNameCollectionFactoryProphecy->create(RelatedDummyEntity::class)->shouldBeCalled()->willReturn(new PropertyNameCollection(['bar', 'anotherSubresource']));
272+
273+
$subresourceMetadataCollectionWithMaxDepth = (new PropertyMetadata())->withSubresource(new SubresourceMetadata(RelatedDummyEntity::class, false, 1));
274+
$anotherSubresourceMetadata = (new PropertyMetadata())->withSubresource(new SubresourceMetadata(DummyEntity::class, false));
275+
276+
$propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
277+
$propertyMetadataFactoryProphecy->create(DummyEntity::class, 'subresource')->shouldBeCalled()->willReturn($subresourceMetadataCollectionWithMaxDepth);
278+
$propertyMetadataFactoryProphecy->create(RelatedDummyEntity::class, 'bar')->shouldBeCalled()->willReturn(new PropertyMetadata());
279+
$propertyMetadataFactoryProphecy->create(RelatedDummyEntity::class, 'anotherSubresource')->shouldBeCalled()->willReturn($anotherSubresourceMetadata);
280+
281+
$pathSegmentNameGeneratorProphecy = $this->prophesize(PathSegmentNameGeneratorInterface::class);
282+
$pathSegmentNameGeneratorProphecy->getSegmentName('dummyEntity', true)->shouldBeCalled()->willReturn('dummy_entities');
283+
$pathSegmentNameGeneratorProphecy->getSegmentName('subresource', false)->shouldBeCalled()->willReturn('subresource');
284+
285+
$subresourceOperationFactory = new SubresourceOperationFactory(
286+
$resourceMetadataFactoryProphecy->reveal(),
287+
$propertyNameCollectionFactoryProphecy->reveal(),
288+
$propertyMetadataFactoryProphecy->reveal(),
289+
$pathSegmentNameGeneratorProphecy->reveal()
290+
);
291+
292+
$this->assertEquals([
293+
'api_dummy_entities_subresource_get_subresource' => [
294+
'property' => 'subresource',
295+
'collection' => false,
296+
'resource_class' => RelatedDummyEntity::class,
297+
'shortNames' => ['relatedDummyEntity', 'dummyEntity'],
298+
'identifiers' => [
299+
['id', DummyEntity::class, true],
300+
],
301+
'route_name' => 'api_dummy_entities_subresource_get_subresource',
302+
'path' => '/dummy_entities/{id}/subresource.{_format}',
303+
'operation_name' => 'subresource_get_subresource',
304+
] + SubresourceOperationFactory::ROUTE_OPTIONS
305+
], $subresourceOperationFactory->create(DummyEntity::class));
306+
}
262307
}

0 commit comments

Comments
 (0)