diff --git a/src/Operation/Factory/SubresourceOperationFactory.php b/src/Operation/Factory/SubresourceOperationFactory.php index ece32c2240f..227b9d494fb 100644 --- a/src/Operation/Factory/SubresourceOperationFactory.php +++ b/src/Operation/Factory/SubresourceOperationFactory.php @@ -85,12 +85,6 @@ private function computeSubresourceOperations(string $resourceClass, array &$tre $visiting = "$resourceClass $property $subresourceClass"; // Handle maxDepth - if ($rootResourceClass === $resourceClass) { - $maxDepth = $subresource->getMaxDepth(); - // reset depth when we return to rootResourceClass - $depth = 0; - } - if (null !== $maxDepth && $depth >= $maxDepth) { break; } @@ -183,7 +177,11 @@ private function computeSubresourceOperations(string $resourceClass, array &$tre $tree[$operation['route_name']] = $operation; - $this->computeSubresourceOperations($subresourceClass, $tree, $rootResourceClass, $operation, $visited + [$visiting => true], ++$depth, $maxDepth); + // Get the minimum maxDepth between the rootMaxDepth and the maxDepth of the to be visited Subresource + $currentMaxDepth = array_filter([$maxDepth, $subresource->getMaxDepth()], 'is_int'); + $currentMaxDepth = empty($currentMaxDepth) ? null : min($currentMaxDepth); + + $this->computeSubresourceOperations($subresourceClass, $tree, $rootResourceClass, $operation, $visited + [$visiting => true], $depth + 1, $currentMaxDepth); } } } diff --git a/tests/Operation/Factory/SubresourceOperationFactoryTest.php b/tests/Operation/Factory/SubresourceOperationFactoryTest.php index 74cd77841ff..292fcd4c478 100644 --- a/tests/Operation/Factory/SubresourceOperationFactoryTest.php +++ b/tests/Operation/Factory/SubresourceOperationFactoryTest.php @@ -465,7 +465,7 @@ public function testCreateWithMaxDepthMultipleSubresourcesSameMaxDepth() public function testCreateSelfReferencingSubresources() { /** - * DummyEntity -subresource-> DummyEntity -subresource-> DummyEntity ... + * DummyEntity -subresource-> DummyEntity --> DummyEntity ... */ $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); $resourceMetadataFactoryProphecy->create(DummyEntity::class)->shouldBeCalled()->willReturn(new ResourceMetadata('dummyEntity')); @@ -505,6 +505,83 @@ public function testCreateSelfReferencingSubresources() ], $subresourceOperationFactory->create(DummyEntity::class)); } + /** + * Test for issue: https://github.com/api-platform/core/issues/2533. + */ + public function testCreateWithDifferentMaxDepthSelfReferencingSubresources() + { + /** + * subresource: maxDepth = 2 + * secondSubresource: maxDepth = 1 + * DummyEntity -subresource-> DummyEntity -secondSubresource-> DummyEntity ... + * DummyEntity -secondSubresource-> DummyEntity !!!-subresource-> DummyEntity ... + */ + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); + $resourceMetadataFactoryProphecy->create(DummyEntity::class)->shouldBeCalled()->willReturn(new ResourceMetadata('dummyEntity')); + + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create(DummyEntity::class)->shouldBeCalled()->willReturn(new PropertyNameCollection(['subresource', 'secondSubresource'])); + + $subresourceWithMaxDepthMetadata = (new PropertyMetadata())->withSubresource(new SubresourceMetadata(DummyEntity::class, false, 2)); + $secondSubresourceWithMaxDepthMetadata = (new PropertyMetadata())->withSubresource(new SubresourceMetadata(DummyEntity::class, false, 1)); + + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create(DummyEntity::class, 'subresource')->shouldBeCalled()->willReturn($subresourceWithMaxDepthMetadata); + $propertyMetadataFactoryProphecy->create(DummyEntity::class, 'secondSubresource')->shouldBeCalled()->willReturn($secondSubresourceWithMaxDepthMetadata); + + $pathSegmentNameGeneratorProphecy = $this->prophesize(PathSegmentNameGeneratorInterface::class); + $pathSegmentNameGeneratorProphecy->getSegmentName('dummyEntity')->shouldBeCalled()->willReturn('dummy_entities'); + $pathSegmentNameGeneratorProphecy->getSegmentName('subresource', false)->shouldBeCalled()->willReturn('subresources'); + $pathSegmentNameGeneratorProphecy->getSegmentName('secondSubresource', false)->shouldBeCalled()->willReturn('second_subresources'); + + $subresourceOperationFactory = new SubresourceOperationFactory( + $resourceMetadataFactoryProphecy->reveal(), + $propertyNameCollectionFactoryProphecy->reveal(), + $propertyMetadataFactoryProphecy->reveal(), + $pathSegmentNameGeneratorProphecy->reveal() + ); + + $this->assertEquals([ + 'api_dummy_entities_subresource_get_subresource' => [ + 'property' => 'subresource', + 'collection' => false, + 'resource_class' => DummyEntity::class, + 'shortNames' => ['dummyEntity'], + 'identifiers' => [ + ['id', DummyEntity::class, true], + ], + 'route_name' => 'api_dummy_entities_subresource_get_subresource', + 'path' => '/dummy_entities/{id}/subresources.{_format}', + 'operation_name' => 'subresource_get_subresource', + ] + SubresourceOperationFactory::ROUTE_OPTIONS, + 'api_dummy_entities_subresource_second_subresource_get_subresource' => [ + 'property' => 'secondSubresource', + 'collection' => false, + 'resource_class' => DummyEntity::class, + 'shortNames' => ['dummyEntity'], + 'identifiers' => [ + ['id', DummyEntity::class, true], + ['subresource', DummyEntity::class, false], + ], + 'route_name' => 'api_dummy_entities_subresource_second_subresource_get_subresource', + 'path' => '/dummy_entities/{id}/subresources/second_subresources.{_format}', + 'operation_name' => 'subresource_second_subresource_get_subresource', + ] + SubresourceOperationFactory::ROUTE_OPTIONS, + 'api_dummy_entities_second_subresource_get_subresource' => [ + 'property' => 'secondSubresource', + 'collection' => false, + 'resource_class' => DummyEntity::class, + 'shortNames' => ['dummyEntity'], + 'identifiers' => [ + ['id', DummyEntity::class, true], + ], + 'route_name' => 'api_dummy_entities_second_subresource_get_subresource', + 'path' => '/dummy_entities/{id}/second_subresources.{_format}', + 'operation_name' => 'second_subresource_get_subresource', + ] + SubresourceOperationFactory::ROUTE_OPTIONS, + ], $subresourceOperationFactory->create(DummyEntity::class)); + } + public function testCreateWithEnd() { $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); @@ -652,4 +729,65 @@ public function testCreateWithRootResourcePrefix() ] + SubresourceOperationFactory::ROUTE_OPTIONS, ], $subresourceOperationFactory->create(DummyEntity::class)); } + + public function testCreateSelfReferencingSubresourcesWithSubresources() + { + /** + * DummyEntity -otherSubresource-> RelatedDummyEntity + * DummyEntity -subresource (maxDepth=1) -> DummyEntity -otherSubresource-> RelatedDummyEntity. + */ + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); + $resourceMetadataFactoryProphecy->create(DummyEntity::class)->shouldBeCalled()->willReturn(new ResourceMetadata('dummyEntity')); + $resourceMetadataFactoryProphecy->create(RelatedDummyEntity::class)->shouldBeCalled()->willReturn(new ResourceMetadata('relatedDummyEntity')); + + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create(DummyEntity::class)->shouldBeCalled()->willReturn(new PropertyNameCollection(['subresource', 'otherSubresource'])); + $propertyNameCollectionFactoryProphecy->create(RelatedDummyEntity::class)->shouldBeCalled()->willReturn(new PropertyNameCollection([])); + + $subresource = (new PropertyMetadata())->withSubresource(new SubresourceMetadata(DummyEntity::class, false, 1)); + $otherSubresourceSubresource = (new PropertyMetadata())->withSubresource(new SubresourceMetadata(RelatedDummyEntity::class, false)); + + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create(DummyEntity::class, 'subresource')->shouldBeCalled()->willReturn($subresource); + $propertyMetadataFactoryProphecy->create(DummyEntity::class, 'otherSubresource')->shouldBeCalled()->willReturn($otherSubresourceSubresource); + + $pathSegmentNameGeneratorProphecy = $this->prophesize(PathSegmentNameGeneratorInterface::class); + $pathSegmentNameGeneratorProphecy->getSegmentName('dummyEntity')->shouldBeCalled()->willReturn('dummy_entities'); + $pathSegmentNameGeneratorProphecy->getSegmentName('subresource', false)->shouldBeCalled()->willReturn('subresources'); + $pathSegmentNameGeneratorProphecy->getSegmentName('otherSubresource', false)->shouldBeCalled()->willReturn('other_subresources'); + + $subresourceOperationFactory = new SubresourceOperationFactory( + $resourceMetadataFactoryProphecy->reveal(), + $propertyNameCollectionFactoryProphecy->reveal(), + $propertyMetadataFactoryProphecy->reveal(), + $pathSegmentNameGeneratorProphecy->reveal() + ); + + $this->assertEquals([ + 'api_dummy_entities_subresource_get_subresource' => [ + 'property' => 'subresource', + 'collection' => false, + 'resource_class' => DummyEntity::class, + 'shortNames' => ['dummyEntity'], + 'identifiers' => [ + ['id', DummyEntity::class, true], + ], + 'route_name' => 'api_dummy_entities_subresource_get_subresource', + 'path' => '/dummy_entities/{id}/subresources.{_format}', + 'operation_name' => 'subresource_get_subresource', + ] + SubresourceOperationFactory::ROUTE_OPTIONS, + 'api_dummy_entities_other_subresource_get_subresource' => [ + 'property' => 'otherSubresource', + 'collection' => false, + 'resource_class' => RelatedDummyEntity::class, + 'shortNames' => ['relatedDummyEntity', 'dummyEntity'], + 'identifiers' => [ + ['id', DummyEntity::class, true], + ], + 'route_name' => 'api_dummy_entities_other_subresource_get_subresource', + 'path' => '/dummy_entities/{id}/other_subresources.{_format}', + 'operation_name' => 'other_subresource_get_subresource', + ] + SubresourceOperationFactory::ROUTE_OPTIONS, + ], $subresourceOperationFactory->create(DummyEntity::class)); + } }