diff --git a/src/GraphQl/Resolver/Stage/SerializeStage.php b/src/GraphQl/Resolver/Stage/SerializeStage.php index aed1266a369..27df556be7e 100644 --- a/src/GraphQl/Resolver/Stage/SerializeStage.php +++ b/src/GraphQl/Resolver/Stage/SerializeStage.php @@ -88,7 +88,7 @@ public function __invoke(object|array|null $itemOrCollection, string $resourceCl } else { $data = 'cursor' === $this->pagination->getGraphQlPaginationType($operation) ? $this->serializeCursorBasedPaginatedCollection($itemOrCollection, $normalizationContext, $context) : - $this->serializePageBasedPaginatedCollection($itemOrCollection, $normalizationContext); + $this->serializePageBasedPaginatedCollection($itemOrCollection, $normalizationContext, $context); } } @@ -117,53 +117,61 @@ private function serializeCursorBasedPaginatedCollection(iterable $collection, a throw new \LogicException(sprintf('Collection returned by the collection data provider must implement %s or %s.', PaginatorInterface::class, PartialPaginatorInterface::class)); } + $selection = $context['info']->getFieldSelection(1); + $offset = 0; $totalItems = 1; // For partial pagination, always consider there is at least one item. - $nbPageItems = $collection->count(); - if (isset($args['after'])) { - $after = base64_decode($args['after'], true); - if (false === $after || '' === $args['after']) { - throw new \UnexpectedValueException('' === $args['after'] ? 'Empty cursor is invalid' : sprintf('Cursor %s is invalid', $args['after'])); + $data = ['edges' => []]; + if (isset($selection['pageInfo']) || isset($selection['totalCount']) || isset($selection['edges']['cursor'])) { + $nbPageItems = $collection->count(); + if (isset($args['after'])) { + $after = base64_decode($args['after'], true); + if (false === $after || '' === $args['after']) { + throw new \UnexpectedValueException('' === $args['after'] ? 'Empty cursor is invalid' : sprintf('Cursor %s is invalid', $args['after'])); + } + $offset = 1 + (int) $after; } - $offset = 1 + (int) $after; - } - if ($collection instanceof PaginatorInterface) { - $totalItems = $collection->getTotalItems(); - - if (isset($args['before'])) { - $before = base64_decode($args['before'], true); - if (false === $before || '' === $args['before']) { - throw new \UnexpectedValueException('' === $args['before'] ? 'Empty cursor is invalid' : sprintf('Cursor %s is invalid', $args['before'])); + if ($collection instanceof PaginatorInterface && (isset($selection['pageInfo']) || isset($selection['totalCount']))) { + $totalItems = $collection->getTotalItems(); + if (isset($args['before'])) { + $before = base64_decode($args['before'], true); + if (false === $before || '' === $args['before']) { + throw new \UnexpectedValueException('' === $args['before'] ? 'Empty cursor is invalid' : sprintf('Cursor %s is invalid', $args['before'])); + } + $offset = (int) $before - $nbPageItems; + } + if (isset($args['last']) && !isset($args['before'])) { + $offset = $totalItems - $args['last']; } - $offset = (int) $before - $nbPageItems; - } - if (isset($args['last']) && !isset($args['before'])) { - $offset = $totalItems - $args['last']; } - } - $offset = 0 > $offset ? 0 : $offset; - - $data = $this->getDefaultCursorBasedPaginatedData(); - if ($totalItems > 0) { - $data['pageInfo']['startCursor'] = base64_encode((string) $offset); - $end = $offset + $nbPageItems - 1; - $data['pageInfo']['endCursor'] = base64_encode((string) ($end >= 0 ? $end : 0)); - $data['pageInfo']['hasPreviousPage'] = $offset > 0; - if ($collection instanceof PaginatorInterface) { - $data['totalCount'] = $totalItems; - $itemsPerPage = $collection->getItemsPerPage(); - $data['pageInfo']['hasNextPage'] = (float) ($itemsPerPage > 0 ? $offset % $itemsPerPage : $offset) + $itemsPerPage * $collection->getCurrentPage() < $totalItems; + $offset = max(0, $offset); + + $data = $this->getDefaultCursorBasedPaginatedData(); + if ((isset($selection['pageInfo']) || isset($selection['totalCount'])) && $totalItems > 0) { + isset($selection['pageInfo']['startCursor']) && $data['pageInfo']['startCursor'] = base64_encode((string) $offset); + $end = $offset + $nbPageItems - 1; + isset($selection['pageInfo']['endCursor']) && $data['pageInfo']['endCursor'] = base64_encode((string) max($end, 0)); + isset($selection['pageInfo']['hasPreviousPage']) && $data['pageInfo']['hasPreviousPage'] = $offset > 0; + if ($collection instanceof PaginatorInterface) { + isset($selection['totalCount']) && $data['totalCount'] = $totalItems; + + $itemsPerPage = $collection->getItemsPerPage(); + isset($selection['pageInfo']['hasNextPage']) && $data['pageInfo']['hasNextPage'] = (float) ($itemsPerPage > 0 ? $offset % $itemsPerPage : $offset) + $itemsPerPage * $collection->getCurrentPage() < $totalItems; + } } } $index = 0; foreach ($collection as $object) { - $data['edges'][$index] = [ + $edge = [ 'node' => $this->normalizer->normalize($object, ItemNormalizer::FORMAT, $normalizationContext), - 'cursor' => base64_encode((string) ($index + $offset)), ]; + if (isset($selection['edges']['cursor'])) { + $edge['cursor'] = base64_encode((string) ($index + $offset)); + } + $data['edges'][$index] = $edge; ++$index; } @@ -173,17 +181,33 @@ private function serializeCursorBasedPaginatedCollection(iterable $collection, a /** * @throws \LogicException */ - private function serializePageBasedPaginatedCollection(iterable $collection, array $normalizationContext): array + private function serializePageBasedPaginatedCollection(iterable $collection, array $normalizationContext, array $context): array { - if (!($collection instanceof PaginatorInterface)) { - throw new \LogicException(sprintf('Collection returned by the collection data provider must implement %s.', PaginatorInterface::class)); + $data = ['collection' => []]; + + $selection = $context['info']->getFieldSelection(1); + if (isset($selection['paginationInfo'])) { + $data['paginationInfo'] = []; + if (isset($selection['paginationInfo']['itemsPerPage'])) { + if (!($collection instanceof PartialPaginatorInterface)) { + throw new \LogicException(sprintf('Collection returned by the collection data provider must implement %s to return itemsPerPage field.', PartialPaginatorInterface::class)); + } + $data['paginationInfo']['itemsPerPage'] = $collection->getItemsPerPage(); + } + if (isset($selection['paginationInfo']['totalCount'])) { + if (!($collection instanceof PaginatorInterface)) { + throw new \LogicException(sprintf('Collection returned by the collection data provider must implement %s to return totalCount field.', PaginatorInterface::class)); + } + $data['paginationInfo']['totalCount'] = $collection->getTotalItems(); + } + if (isset($selection['paginationInfo']['lastPage'])) { + if (!($collection instanceof PaginatorInterface)) { + throw new \LogicException(sprintf('Collection returned by the collection data provider must implement %s to return lastPage field.', PaginatorInterface::class)); + } + $data['paginationInfo']['lastPage'] = $collection->getLastPage(); + } } - $data = $this->getDefaultPageBasedPaginatedData(); - $data['paginationInfo']['totalCount'] = $collection->getTotalItems(); - $data['paginationInfo']['lastPage'] = $collection->getLastPage(); - $data['paginationInfo']['itemsPerPage'] = $collection->getItemsPerPage(); - foreach ($collection as $object) { $data['collection'][] = $this->normalizer->normalize($object, ItemNormalizer::FORMAT, $normalizationContext); } diff --git a/src/GraphQl/State/Processor/NormalizeProcessor.php b/src/GraphQl/State/Processor/NormalizeProcessor.php index 58307b7b8af..31a6db19cdd 100644 --- a/src/GraphQl/State/Processor/NormalizeProcessor.php +++ b/src/GraphQl/State/Processor/NormalizeProcessor.php @@ -98,7 +98,7 @@ private function getData(mixed $itemOrCollection, GraphQlOperation $operation, a } else { $data = 'cursor' === $this->pagination->getGraphQlPaginationType($operation) ? $this->serializeCursorBasedPaginatedCollection($itemOrCollection, $normalizationContext, $context) : - $this->serializePageBasedPaginatedCollection($itemOrCollection, $normalizationContext); + $this->serializePageBasedPaginatedCollection($itemOrCollection, $normalizationContext, $context); } } @@ -129,53 +129,61 @@ private function serializeCursorBasedPaginatedCollection(iterable $collection, a throw new \LogicException(sprintf('Collection returned by the collection data provider must implement %s or %s.', PaginatorInterface::class, PartialPaginatorInterface::class)); } + $selection = $context['info']->getFieldSelection(1); + $offset = 0; $totalItems = 1; // For partial pagination, always consider there is at least one item. - $nbPageItems = $collection->count(); - if (isset($args['after'])) { - $after = base64_decode($args['after'], true); - if (false === $after || '' === $args['after']) { - throw new \UnexpectedValueException('' === $args['after'] ? 'Empty cursor is invalid' : sprintf('Cursor %s is invalid', $args['after'])); + $data = ['edges' => []]; + if (isset($selection['pageInfo']) || isset($selection['totalCount']) || isset($selection['edges']['cursor'])) { + $nbPageItems = $collection->count(); + if (isset($args['after'])) { + $after = base64_decode($args['after'], true); + if (false === $after || '' === $args['after']) { + throw new \UnexpectedValueException('' === $args['after'] ? 'Empty cursor is invalid' : sprintf('Cursor %s is invalid', $args['after'])); + } + $offset = 1 + (int) $after; } - $offset = 1 + (int) $after; - } - if ($collection instanceof PaginatorInterface) { - $totalItems = $collection->getTotalItems(); - - if (isset($args['before'])) { - $before = base64_decode($args['before'], true); - if (false === $before || '' === $args['before']) { - throw new \UnexpectedValueException('' === $args['before'] ? 'Empty cursor is invalid' : sprintf('Cursor %s is invalid', $args['before'])); + if ($collection instanceof PaginatorInterface && (isset($selection['pageInfo']) || isset($selection['totalCount']))) { + $totalItems = $collection->getTotalItems(); + if (isset($args['before'])) { + $before = base64_decode($args['before'], true); + if (false === $before || '' === $args['before']) { + throw new \UnexpectedValueException('' === $args['before'] ? 'Empty cursor is invalid' : sprintf('Cursor %s is invalid', $args['before'])); + } + $offset = (int) $before - $nbPageItems; + } + if (isset($args['last']) && !isset($args['before'])) { + $offset = $totalItems - $args['last']; } - $offset = (int) $before - $nbPageItems; - } - if (isset($args['last']) && !isset($args['before'])) { - $offset = $totalItems - $args['last']; } - } - $offset = 0 > $offset ? 0 : $offset; - - $data = $this->getDefaultCursorBasedPaginatedData(); - if ($totalItems > 0) { - $data['pageInfo']['startCursor'] = base64_encode((string) $offset); - $end = $offset + $nbPageItems - 1; - $data['pageInfo']['endCursor'] = base64_encode((string) ($end >= 0 ? $end : 0)); - $data['pageInfo']['hasPreviousPage'] = $offset > 0; - if ($collection instanceof PaginatorInterface) { - $data['totalCount'] = $totalItems; - $itemsPerPage = $collection->getItemsPerPage(); - $data['pageInfo']['hasNextPage'] = (float) ($itemsPerPage > 0 ? $offset % $itemsPerPage : $offset) + $itemsPerPage * $collection->getCurrentPage() < $totalItems; + $offset = max(0, $offset); + + $data = $this->getDefaultCursorBasedPaginatedData(); + if ((isset($selection['pageInfo']) || isset($selection['totalCount'])) && $totalItems > 0) { + isset($selection['pageInfo']['startCursor']) && $data['pageInfo']['startCursor'] = base64_encode((string) $offset); + $end = $offset + $nbPageItems - 1; + isset($selection['pageInfo']['endCursor']) && $data['pageInfo']['endCursor'] = base64_encode((string) max($end, 0)); + isset($selection['pageInfo']['hasPreviousPage']) && $data['pageInfo']['hasPreviousPage'] = $offset > 0; + if ($collection instanceof PaginatorInterface) { + isset($selection['totalCount']) && $data['totalCount'] = $totalItems; + + $itemsPerPage = $collection->getItemsPerPage(); + isset($selection['pageInfo']['hasNextPage']) && $data['pageInfo']['hasNextPage'] = (float) ($itemsPerPage > 0 ? $offset % $itemsPerPage : $offset) + $itemsPerPage * $collection->getCurrentPage() < $totalItems; + } } } $index = 0; foreach ($collection as $object) { - $data['edges'][$index] = [ + $edge = [ 'node' => $this->normalizer->normalize($object, ItemNormalizer::FORMAT, $normalizationContext), - 'cursor' => base64_encode((string) ($index + $offset)), ]; + if (isset($selection['edges']['cursor'])) { + $edge['cursor'] = base64_encode((string) ($index + $offset)); + } + $data['edges'][$index] = $edge; ++$index; } @@ -185,17 +193,33 @@ private function serializeCursorBasedPaginatedCollection(iterable $collection, a /** * @throws \LogicException */ - private function serializePageBasedPaginatedCollection(iterable $collection, array $normalizationContext): array + private function serializePageBasedPaginatedCollection(iterable $collection, array $normalizationContext, array $context): array { - if (!($collection instanceof PaginatorInterface)) { - throw new \LogicException(sprintf('Collection returned by the collection data provider must implement %s.', PaginatorInterface::class)); + $data = ['collection' => []]; + + $selection = $context['info']->getFieldSelection(1); + if (isset($selection['paginationInfo'])) { + $data['paginationInfo'] = []; + if (isset($selection['paginationInfo']['itemsPerPage'])) { + if (!($collection instanceof PartialPaginatorInterface)) { + throw new \LogicException(sprintf('Collection returned by the collection data provider must implement %s to return itemsPerPage field.', PartialPaginatorInterface::class)); + } + $data['paginationInfo']['itemsPerPage'] = $collection->getItemsPerPage(); + } + if (isset($selection['paginationInfo']['totalCount'])) { + if (!($collection instanceof PaginatorInterface)) { + throw new \LogicException(sprintf('Collection returned by the collection data provider must implement %s to return totalCount field.', PaginatorInterface::class)); + } + $data['paginationInfo']['totalCount'] = $collection->getTotalItems(); + } + if (isset($selection['paginationInfo']['lastPage'])) { + if (!($collection instanceof PaginatorInterface)) { + throw new \LogicException(sprintf('Collection returned by the collection data provider must implement %s to return lastPage field.', PaginatorInterface::class)); + } + $data['paginationInfo']['lastPage'] = $collection->getLastPage(); + } } - $data = $this->getDefaultPageBasedPaginatedData(); - $data['paginationInfo']['totalCount'] = $collection->getTotalItems(); - $data['paginationInfo']['lastPage'] = $collection->getLastPage(); - $data['paginationInfo']['itemsPerPage'] = $collection->getItemsPerPage(); - foreach ($collection as $object) { $data['collection'][] = $this->normalizer->normalize($object, ItemNormalizer::FORMAT, $normalizationContext); } diff --git a/src/GraphQl/Tests/Resolver/Factory/CollectionResolverFactoryTest.php b/src/GraphQl/Tests/Resolver/Factory/CollectionResolverFactoryTest.php index 53272f813d5..e5a4607ee30 100644 --- a/src/GraphQl/Tests/Resolver/Factory/CollectionResolverFactoryTest.php +++ b/src/GraphQl/Tests/Resolver/Factory/CollectionResolverFactoryTest.php @@ -68,7 +68,9 @@ public function testResolve(): void $operation = (new QueryCollection())->withName($operationName); $source = ['testField' => 0]; $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); + $infoProphecy = $this->prophesize(ResolveInfo::class); + $infoProphecy->getFieldSelection()->willReturn(['testField' => true]); + $info = $infoProphecy->reveal(); $info->fieldName = 'testField'; $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => true, 'is_mutation' => false, 'is_subscription' => false]; @@ -101,7 +103,9 @@ public function testResolveFieldNotInSource(): void $operation = (new QueryCollection())->withName($operationName); $source = ['source']; $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); + $infoProphecy = $this->prophesize(ResolveInfo::class); + $infoProphecy->getFieldSelection()->willReturn(['testField' => true]); + $info = $infoProphecy->reveal(); $info->fieldName = 'testField'; $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => true, 'is_mutation' => false, 'is_subscription' => false]; @@ -132,7 +136,9 @@ public function testResolveNullSource(): void $operation = (new QueryCollection())->withName($operationName); $source = null; $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); + $infoProphecy = $this->prophesize(ResolveInfo::class); + $infoProphecy->getFieldSelection()->willReturn([]); + $info = $infoProphecy->reveal(); $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => true, 'is_mutation' => false, 'is_subscription' => false]; $readStageCollection = [new \stdClass()]; @@ -164,7 +170,9 @@ public function testResolveNullResourceClass(): void $operation = (new QueryCollection())->withName($operationName); $source = ['source']; $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); + $infoProphecy = $this->prophesize(ResolveInfo::class); + $infoProphecy->getFieldSelection()->willReturn([]); + $info = $infoProphecy->reveal(); $this->assertNull(($this->collectionResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info)); } @@ -177,7 +185,9 @@ public function testResolveNullRootClass(): void $operation = (new QueryCollection())->withName($operationName); $source = ['source']; $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); + $infoProphecy = $this->prophesize(ResolveInfo::class); + $infoProphecy->getFieldSelection()->willReturn([]); + $info = $infoProphecy->reveal(); $this->assertNull(($this->collectionResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info)); } @@ -190,7 +200,9 @@ public function testResolveBadReadStageCollection(): void $operation = (new QueryCollection())->withName($operationName); $source = null; $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); + $infoProphecy = $this->prophesize(ResolveInfo::class); + $infoProphecy->getFieldSelection()->willReturn([]); + $info = $infoProphecy->reveal(); $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => true, 'is_mutation' => false, 'is_subscription' => false]; $readStageCollection = new \stdClass(); @@ -210,7 +222,9 @@ public function testResolveCustom(): void $operation = (new QueryCollection())->withResolver('query_resolver_id')->withName($operationName); $source = null; $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); + $infoProphecy = $this->prophesize(ResolveInfo::class); + $infoProphecy->getFieldSelection()->willReturn([]); + $info = $infoProphecy->reveal(); $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => true, 'is_mutation' => false, 'is_subscription' => false]; $readStageCollection = [new \stdClass()]; diff --git a/src/GraphQl/Tests/Resolver/Stage/SerializeStageTest.php b/src/GraphQl/Tests/Resolver/Stage/SerializeStageTest.php index 5f383ad3494..168123614f0 100644 --- a/src/GraphQl/Tests/Resolver/Stage/SerializeStageTest.php +++ b/src/GraphQl/Tests/Resolver/Stage/SerializeStageTest.php @@ -25,6 +25,7 @@ use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; use ApiPlatform\State\Pagination\ArrayPaginator; use ApiPlatform\State\Pagination\Pagination; +use ApiPlatform\State\Pagination\PaginatorInterface; use ApiPlatform\State\Pagination\PartialPaginatorInterface; use GraphQL\Type\Definition\ResolveInfo; use PHPUnit\Framework\TestCase; @@ -42,6 +43,7 @@ class SerializeStageTest extends TestCase private ObjectProphecy $normalizerProphecy; private ObjectProphecy $serializerContextBuilderProphecy; + private ObjectProphecy $resolveInfoProphecy; /** * {@inheritdoc} @@ -50,6 +52,7 @@ protected function setUp(): void { $this->normalizerProphecy = $this->prophesize(NormalizerInterface::class); $this->serializerContextBuilderProphecy = $this->prophesize(SerializerContextBuilderInterface::class); + $this->resolveInfoProphecy = $this->prophesize(ResolveInfo::class); } /** @@ -111,7 +114,7 @@ public static function applyProvider(): iterable { $defaultContextFactory = fn (self $that): array => [ 'args' => [], - 'info' => $that->prophesize(ResolveInfo::class)->reveal(), + 'info' => $that->resolveInfoProphecy->reveal(), ]; yield 'item' => [new \stdClass(), 'item_query', fn (self $that): array => $defaultContextFactory($that) + ['is_collection' => false, 'is_mutation' => false, 'is_subscription' => false], false, ['normalized_item']]; @@ -124,20 +127,24 @@ public static function applyProvider(): iterable /** * @dataProvider applyCollectionWithPaginationProvider */ - public function testApplyCollectionWithPagination(iterable|callable $collection, array $args, ?array $expectedResult, string $expectedExceptionClass = null, string $expectedExceptionMessage = null): void + public function testApplyCollectionWithPagination(iterable|callable $collection, array $args, ?array $expectedResult, bool $pageBasedPagination, array $getFieldSelection = [], string $expectedExceptionClass = null, string $expectedExceptionMessage = null): void { $operationName = 'collection_query'; $resourceClass = 'myResource'; + $this->resolveInfoProphecy->getFieldSelection(1)->willReturn($getFieldSelection); $context = [ 'is_collection' => true, 'is_mutation' => false, 'is_subscription' => false, 'args' => $args, - 'info' => self::createMock(ResolveInfo::class), + 'info' => $this->resolveInfoProphecy->reveal(), ]; /** @var Operation $operation */ $operation = (new QueryCollection())->withShortName('shortName')->withName($operationName); + if ($pageBasedPagination) { + $operation = $operation->withPaginationType('page'); + } $normalizationContext = ['normalization' => true]; $this->serializerContextBuilderProphecy->create($resourceClass, $operation, $context, true)->shouldBeCalled()->willReturn($normalizationContext); @@ -160,23 +167,86 @@ public static function applyCollectionWithPaginationProvider(): iterable $partialPaginatorProphecy = $that->prophesize(PartialPaginatorInterface::class); $partialPaginatorProphecy->count()->willReturn(2); $partialPaginatorProphecy->valid()->willReturn(false); + $partialPaginatorProphecy->getItemsPerPage()->willReturn(2.0); $partialPaginatorProphecy->rewind(); return $partialPaginatorProphecy->reveal(); }; - yield 'not paginator' => [[], [], null, \LogicException::class, 'Collection returned by the collection data provider must implement ApiPlatform\State\Pagination\PaginatorInterface']; - yield 'empty paginator' => [new ArrayPaginator([], 0, 0), [], ['totalCount' => 0., 'edges' => [], 'pageInfo' => ['startCursor' => null, 'endCursor' => null, 'hasNextPage' => false, 'hasPreviousPage' => false]]]; - yield 'paginator' => [new ArrayPaginator([new \stdClass(), new \stdClass(), new \stdClass()], 0, 2), [], ['totalCount' => 3., 'edges' => [['node' => ['normalized_item'], 'cursor' => 'MA=='], ['node' => ['normalized_item'], 'cursor' => 'MQ==']], 'pageInfo' => ['startCursor' => 'MA==', 'endCursor' => 'MQ==', 'hasNextPage' => true, 'hasPreviousPage' => false]]]; - yield 'paginator with after cursor' => [new ArrayPaginator([new \stdClass(), new \stdClass(), new \stdClass()], 1, 2), ['after' => 'MA=='], ['totalCount' => 3., 'edges' => [['node' => ['normalized_item'], 'cursor' => 'MQ=='], ['node' => ['normalized_item'], 'cursor' => 'Mg==']], 'pageInfo' => ['startCursor' => 'MQ==', 'endCursor' => 'Mg==', 'hasNextPage' => false, 'hasPreviousPage' => true]]]; - yield 'paginator with bad after cursor' => [new ArrayPaginator([], 0, 0), ['after' => '-'], null, \UnexpectedValueException::class, 'Cursor - is invalid']; - yield 'paginator with empty after cursor' => [new ArrayPaginator([], 0, 0), ['after' => ''], null, \UnexpectedValueException::class, 'Empty cursor is invalid']; - yield 'paginator with before cursor' => [new ArrayPaginator([new \stdClass(), new \stdClass(), new \stdClass()], 1, 1), ['before' => 'Mg=='], ['totalCount' => 3., 'edges' => [['node' => ['normalized_item'], 'cursor' => 'MQ==']], 'pageInfo' => ['startCursor' => 'MQ==', 'endCursor' => 'MQ==', 'hasNextPage' => true, 'hasPreviousPage' => true]]]; - yield 'paginator with bad before cursor' => [new ArrayPaginator([], 0, 0), ['before' => '-'], null, \UnexpectedValueException::class, 'Cursor - is invalid']; - yield 'paginator with empty before cursor' => [new ArrayPaginator([], 0, 0), ['before' => ''], null, \UnexpectedValueException::class, 'Empty cursor is invalid']; - yield 'paginator with last' => [new ArrayPaginator([new \stdClass(), new \stdClass(), new \stdClass()], 1, 2), ['last' => 2], ['totalCount' => 3., 'edges' => [['node' => ['normalized_item'], 'cursor' => 'MQ=='], ['node' => ['normalized_item'], 'cursor' => 'Mg==']], 'pageInfo' => ['startCursor' => 'MQ==', 'endCursor' => 'Mg==', 'hasNextPage' => false, 'hasPreviousPage' => true]]]; - yield 'partial paginator' => [$partialPaginatorFactory, [], ['totalCount' => 0., 'edges' => [], 'pageInfo' => ['startCursor' => 'MA==', 'endCursor' => 'MQ==', 'hasNextPage' => false, 'hasPreviousPage' => false]]]; - yield 'partial paginator with after cursor' => [$partialPaginatorFactory, ['after' => 'MA=='], ['totalCount' => 0., 'edges' => [], 'pageInfo' => ['startCursor' => 'MQ==', 'endCursor' => 'Mg==', 'hasNextPage' => false, 'hasPreviousPage' => true]]]; + yield 'cursor - not paginator' => [[], [], null, false, [], \LogicException::class, 'Collection returned by the collection data provider must implement ApiPlatform\State\Pagination\PaginatorInterface or ApiPlatform\State\Pagination\PartialPaginatorInterface.']; + yield 'cursor - empty paginator' => [new ArrayPaginator([], 0, 0), [], ['totalCount' => 0., 'edges' => [], 'pageInfo' => ['startCursor' => null, 'endCursor' => null, 'hasNextPage' => false, 'hasPreviousPage' => false]], false, ['totalCount' => true, 'edges' => [], 'pageInfo' => ['startCursor' => true, 'endCursor' => true, 'hasNextPage' => true, 'hasPreviousPage' => true]]]; + yield 'cursor - paginator' => [new ArrayPaginator([new \stdClass(), new \stdClass(), new \stdClass()], 0, 2), [], ['totalCount' => 3., 'edges' => [['node' => ['normalized_item'], 'cursor' => 'MA=='], ['node' => ['normalized_item'], 'cursor' => 'MQ==']], 'pageInfo' => ['startCursor' => 'MA==', 'endCursor' => 'MQ==', 'hasNextPage' => true, 'hasPreviousPage' => false]], false, ['totalCount' => true, 'edges' => ['cursor' => true], 'pageInfo' => ['startCursor' => true, 'endCursor' => true, 'hasNextPage' => true, 'hasPreviousPage' => true]]]; + yield 'cursor - paginator with after cursor' => [new ArrayPaginator([new \stdClass(), new \stdClass(), new \stdClass()], 1, 2), ['after' => 'MA=='], ['totalCount' => 3., 'edges' => [['node' => ['normalized_item'], 'cursor' => 'MQ=='], ['node' => ['normalized_item'], 'cursor' => 'Mg==']], 'pageInfo' => ['startCursor' => 'MQ==', 'endCursor' => 'Mg==', 'hasNextPage' => false, 'hasPreviousPage' => true]], false, ['totalCount' => true, 'edges' => ['cursor' => true], 'pageInfo' => ['startCursor' => true, 'endCursor' => true, 'hasNextPage' => true, 'hasPreviousPage' => true]]]; + yield 'cursor - paginator with bad after cursor' => [new ArrayPaginator([], 0, 0), ['after' => '-'], null, false, ['pageInfo' => ['endCursor' => true]], \UnexpectedValueException::class, 'Cursor - is invalid']; + yield 'cursor - paginator with empty after cursor' => [new ArrayPaginator([], 0, 0), ['after' => ''], null, false, ['pageInfo' => ['endCursor' => true]], \UnexpectedValueException::class, 'Empty cursor is invalid']; + yield 'cursor - paginator with before cursor' => [new ArrayPaginator([new \stdClass(), new \stdClass(), new \stdClass()], 1, 1), ['before' => 'Mg=='], ['totalCount' => 3., 'edges' => [['node' => ['normalized_item'], 'cursor' => 'MQ==']], 'pageInfo' => ['startCursor' => 'MQ==', 'endCursor' => 'MQ==', 'hasNextPage' => true, 'hasPreviousPage' => true]], false, ['totalCount' => true, 'edges' => ['cursor' => true], 'pageInfo' => ['startCursor' => true, 'endCursor' => true, 'hasNextPage' => true, 'hasPreviousPage' => true]]]; + yield 'cursor - paginator with bad before cursor' => [new ArrayPaginator([], 0, 0), ['before' => '-'], null, false, ['pageInfo' => ['endCursor' => true]], \UnexpectedValueException::class, 'Cursor - is invalid']; + yield 'cursor - paginator with empty before cursor' => [new ArrayPaginator([], 0, 0), ['before' => ''], null, false, ['pageInfo' => ['endCursor' => true]], \UnexpectedValueException::class, 'Empty cursor is invalid']; + yield 'cursor - paginator with last' => [new ArrayPaginator([new \stdClass(), new \stdClass(), new \stdClass()], 1, 2), ['last' => 2], ['totalCount' => 3., 'edges' => [['node' => ['normalized_item'], 'cursor' => 'MQ=='], ['node' => ['normalized_item'], 'cursor' => 'Mg==']], 'pageInfo' => ['startCursor' => 'MQ==', 'endCursor' => 'Mg==', 'hasNextPage' => false, 'hasPreviousPage' => true]], false, ['totalCount' => true, 'edges' => ['cursor' => true], 'pageInfo' => ['startCursor' => true, 'endCursor' => true, 'hasNextPage' => true, 'hasPreviousPage' => true]]]; + yield 'cursor - partial paginator' => [$partialPaginatorFactory, [], ['totalCount' => 0., 'edges' => [], 'pageInfo' => ['startCursor' => 'MA==', 'endCursor' => 'MQ==', 'hasNextPage' => false, 'hasPreviousPage' => false]], false, ['totalCount' => true, 'edges' => ['cursor' => true], 'pageInfo' => ['startCursor' => true, 'endCursor' => true, 'hasNextPage' => true, 'hasPreviousPage' => true]]]; + yield 'cursor - partial paginator with after cursor' => [$partialPaginatorFactory, ['after' => 'MA=='], ['totalCount' => 0., 'edges' => [], 'pageInfo' => ['startCursor' => 'MQ==', 'endCursor' => 'Mg==', 'hasNextPage' => false, 'hasPreviousPage' => true]], false, ['totalCount' => true, 'edges' => ['cursor' => true], 'pageInfo' => ['startCursor' => true, 'endCursor' => true, 'hasNextPage' => true, 'hasPreviousPage' => true]]]; + + yield 'page - not paginator, itemsPerPage requested' => [[], [], null, true, ['paginationInfo' => ['itemsPerPage' => true]], \LogicException::class, 'Collection returned by the collection data provider must implement ApiPlatform\State\Pagination\PartialPaginatorInterface to return itemsPerPage field.']; + yield 'page - not paginator, lastPage requested' => [[], [], null, true, ['paginationInfo' => ['lastPage' => true]], \LogicException::class, 'Collection returned by the collection data provider must implement ApiPlatform\State\Pagination\PaginatorInterface to return lastPage field.']; + yield 'page - not paginator, totalCount requested' => [[], [], null, true, ['paginationInfo' => ['totalCount' => true]], \LogicException::class, 'Collection returned by the collection data provider must implement ApiPlatform\State\Pagination\PaginatorInterface to return totalCount field.']; + yield 'page - empty paginator - itemsPerPage requested' => [new ArrayPaginator([], 0, 0), [], ['collection' => [], 'paginationInfo' => ['itemsPerPage' => .0]], true, ['paginationInfo' => ['itemsPerPage' => true]]]; + yield 'page - empty paginator - lastPage requested' => [new ArrayPaginator([], 0, 0), [], ['collection' => [], 'paginationInfo' => ['lastPage' => 1.0]], true, ['paginationInfo' => ['lastPage' => true]]]; + yield 'page - empty paginator - totalCount requested' => [new ArrayPaginator([], 0, 0), [], ['collection' => [], 'paginationInfo' => ['totalCount' => .0]], true, ['paginationInfo' => ['totalCount' => true]]]; + yield 'page - paginator page 1 - itemsPerPage requested' => [new ArrayPaginator([new \stdClass(), new \stdClass(), new \stdClass()], 0, 2), [], ['collection' => [['normalized_item'], ['normalized_item']], 'paginationInfo' => ['itemsPerPage' => 2.0]], true, ['paginationInfo' => ['itemsPerPage' => true]]]; + yield 'page - paginator page 1 - lastPage requested' => [new ArrayPaginator([new \stdClass(), new \stdClass(), new \stdClass()], 0, 2), [], ['collection' => [['normalized_item'], ['normalized_item']], 'paginationInfo' => ['lastPage' => 2.0]], true, ['paginationInfo' => ['lastPage' => true]]]; + yield 'page - paginator page 1 - totalCount requested' => [new ArrayPaginator([new \stdClass(), new \stdClass(), new \stdClass()], 0, 2), [], ['collection' => [['normalized_item'], ['normalized_item']], 'paginationInfo' => ['totalCount' => 3.0]], true, ['paginationInfo' => ['totalCount' => true]]]; + yield 'page - paginator with page - itemsPerPage requested' => [new ArrayPaginator([new \stdClass(), new \stdClass(), new \stdClass()], 2, 2), [], ['collection' => [['normalized_item']], 'paginationInfo' => ['itemsPerPage' => 2.0]], true, ['paginationInfo' => ['itemsPerPage' => true]]]; + yield 'page - paginator with page - lastPage requested' => [new ArrayPaginator([new \stdClass(), new \stdClass(), new \stdClass()], 2, 2), [], ['collection' => [['normalized_item']], 'paginationInfo' => ['lastPage' => 2.0]], true, ['paginationInfo' => ['lastPage' => true]]]; + yield 'page - paginator with page - totalCount requested' => [new ArrayPaginator([new \stdClass(), new \stdClass(), new \stdClass()], 2, 2), [], ['collection' => [['normalized_item']], 'paginationInfo' => ['totalCount' => 3.0]], true, ['paginationInfo' => ['totalCount' => true]]]; + } + + /** + * @dataProvider applyCollectionWithPaginationShouldNotCountItemsUnlessFieldRequestedProvider + */ + public function testApplyCollectionWithPaginationShouldNotCountItemsUnlessFieldRequested(bool $pageBasedPagination, array $getFieldSelection = [], bool $getTotalItemsCalled = false): void + { + $operationName = 'collection_query'; + $resourceClass = 'myResource'; + $this->resolveInfoProphecy->getFieldSelection(1)->willReturn($getFieldSelection); + $context = [ + 'is_collection' => true, + 'is_mutation' => false, + 'is_subscription' => false, + 'args' => [], + 'info' => $this->resolveInfoProphecy->reveal(), + ]; + $collectionProphecy = $this->prophesize(PaginatorInterface::class); + $collectionProphecy->getTotalItems()->willReturn(1); + $collectionProphecy->count()->willReturn(1); + $collectionProphecy->getItemsPerPage()->willReturn(20.0); + $collectionProphecy->valid()->willReturn(false); + $collectionProphecy->rewind(); + if ($getTotalItemsCalled) { + $collectionProphecy->getTotalItems()->shouldBeCalledOnce(); + } else { + $collectionProphecy->getTotalItems()->shouldNotBeCalled(); + } + + /** @var Operation $operation */ + $operation = (new QueryCollection())->withShortName('shortName')->withName($operationName); + if ($pageBasedPagination) { + $operation = $operation->withPaginationType('page'); + } + + $normalizationContext = ['normalization' => true]; + $this->serializerContextBuilderProphecy->create($resourceClass, $operation, $context, true)->shouldBeCalled()->willReturn($normalizationContext); + + $this->normalizerProphecy->normalize(Argument::type(\stdClass::class), ItemNormalizer::FORMAT, $normalizationContext)->willReturn(['normalized_item']); + + ($this->createSerializeStage(true))($collectionProphecy->reveal(), $resourceClass, $operation, $context); + } + + public static function applyCollectionWithPaginationShouldNotCountItemsUnlessFieldRequestedProvider(): iterable + { + yield 'cursor - totalCount requested' => [false, ['totalCount' => true], true]; + yield 'cursor - totalCount not requested' => [false, [], false]; + yield 'page - totalCount requested' => [true, ['paginationInfo' => ['totalCount' => true]], true]; + yield 'page - totalCount not requested' => [true, [], false]; } public function testApplyBadNormalizedData(): void diff --git a/src/GraphQl/Tests/State/Processor/NormalizeProcessorTest.php b/src/GraphQl/Tests/State/Processor/NormalizeProcessorTest.php index 34411efe66e..2ac140ead46 100644 --- a/src/GraphQl/Tests/State/Processor/NormalizeProcessorTest.php +++ b/src/GraphQl/Tests/State/Processor/NormalizeProcessorTest.php @@ -21,11 +21,27 @@ use ApiPlatform\Metadata\GraphQl\Subscription; use ApiPlatform\State\Pagination\ArrayPaginator; use ApiPlatform\State\Pagination\Pagination; +use ApiPlatform\State\Pagination\PartialPaginatorInterface; +use GraphQL\Type\Definition\ResolveInfo; use PHPUnit\Framework\TestCase; +use Prophecy\PhpUnit\ProphecyTrait; +use Prophecy\Prophecy\ObjectProphecy; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; class NormalizeProcessorTest extends TestCase { + use ProphecyTrait; + + private ObjectProphecy $resolveInfoProphecy; + + /** + * {@inheritdoc} + */ + protected function setUp(): void + { + $this->resolveInfoProphecy = $this->prophesize(ResolveInfo::class); + } + /** * @dataProvider processItems */ @@ -53,25 +69,65 @@ public function processItems(): array /** * @dataProvider processCollection */ - public function testProcessCollection($body, $operation): void + public function testProcessCollection($collection, $operation, $args, ?array $expectedResult, array $getFieldSelection, string $expectedExceptionClass = null, string $expectedExceptionMessage = null): void { - $context = ['args' => []]; + $this->resolveInfoProphecy->getFieldSelection(1)->willReturn($getFieldSelection); + $context = ['args' => $args, 'info' => $this->resolveInfoProphecy->reveal()]; $serializerContext = ['resource_class' => $operation->getClass()]; - $normalizer = $this->createMock(NormalizerInterface::class); + $normalizer = $this->prophesize(NormalizerInterface::class); + $serializerContextBuilder = $this->createMock(SerializerContextBuilderInterface::class); $serializerContextBuilder->expects($this->once())->method('create')->with($operation->getClass(), $operation, $context, normalization: true)->willReturn($serializerContext); - foreach ($body as $v) { - $normalizer->expects($this->once())->method('normalize')->with($v, 'graphql', $serializerContext); + foreach ($collection as $v) { + $normalizer->normalize($v, 'graphql', $serializerContext)->willReturn(['normalized_item'])->shouldBeCalledOnce(); } - $processor = new NormalizeProcessor($normalizer, $serializerContextBuilder, new Pagination()); - $processor->process($body, $operation, [], $context); + if ($expectedExceptionClass) { + $this->expectException($expectedExceptionClass); + $this->expectExceptionMessage($expectedExceptionMessage); + } + + $processor = new NormalizeProcessor($normalizer->reveal(), $serializerContextBuilder, new Pagination()); + $result = $processor->process(\is_callable($collection) ? $collection($this) : $collection, $operation, [], $context); + $this->assertSame($expectedResult, $result); } - public function processCollection(): array + public function processCollection(): iterable { - return [ - [new ArrayPaginator([new \stdClass()], 0, 1), new QueryCollection(class: 'foo')], - ]; + $partialPaginatorFactory = function (self $that): PartialPaginatorInterface { + $partialPaginatorProphecy = $that->prophesize(PartialPaginatorInterface::class); + $partialPaginatorProphecy->count()->willReturn(2); + $partialPaginatorProphecy->valid()->willReturn(false); + $partialPaginatorProphecy->getItemsPerPage()->willReturn(2.0); + $partialPaginatorProphecy->rewind(); + + return $partialPaginatorProphecy->reveal(); + }; + + yield 'cursor - not paginator' => [[], new QueryCollection(class: 'foo'), [], null, [], \LogicException::class, 'Collection returned by the collection data provider must implement ApiPlatform\State\Pagination\PaginatorInterface or ApiPlatform\State\Pagination\PartialPaginatorInterface.']; + yield 'cursor - empty paginator' => [new ArrayPaginator([], 0, 0), new QueryCollection(class: 'foo'), [], ['totalCount' => 0., 'edges' => [], 'pageInfo' => ['startCursor' => null, 'endCursor' => null, 'hasNextPage' => false, 'hasPreviousPage' => false]], ['totalCount' => true, 'edges' => ['cursor' => true], 'pageInfo' => ['startCursor' => true, 'endCursor' => true, 'hasNextPage' => true, 'hasPreviousPage' => true]]]; + yield 'cursor - paginator' => [new ArrayPaginator([(object) ['test' => 'a'], (object) ['test' => 'b'], (object) ['test' => 'c']], 0, 2), new QueryCollection(class: 'foo'), [], ['totalCount' => 3., 'edges' => [['node' => ['normalized_item'], 'cursor' => 'MA=='], ['node' => ['normalized_item'], 'cursor' => 'MQ==']], 'pageInfo' => ['startCursor' => 'MA==', 'endCursor' => 'MQ==', 'hasNextPage' => true, 'hasPreviousPage' => false]], ['totalCount' => true, 'edges' => ['cursor' => true], 'pageInfo' => ['startCursor' => true, 'endCursor' => true, 'hasNextPage' => true, 'hasPreviousPage' => true]]]; + yield 'cursor - paginator with after cursor' => [new ArrayPaginator([(object) ['test' => 'a'], (object) ['test' => 'b'], (object) ['test' => 'c']], 1, 2), new QueryCollection(class: 'foo'), ['after' => 'MA=='], ['totalCount' => 3., 'edges' => [['node' => ['normalized_item'], 'cursor' => 'MQ=='], ['node' => ['normalized_item'], 'cursor' => 'Mg==']], 'pageInfo' => ['startCursor' => 'MQ==', 'endCursor' => 'Mg==', 'hasNextPage' => false, 'hasPreviousPage' => true]], ['totalCount' => true, 'edges' => ['cursor' => true], 'pageInfo' => ['startCursor' => true, 'endCursor' => true, 'hasNextPage' => true, 'hasPreviousPage' => true]]]; + yield 'cursor - paginator with bad after cursor' => [new ArrayPaginator([], 0, 0), new QueryCollection(class: 'foo'), ['after' => '-'], null, ['edges' => ['cursor' => []]], \UnexpectedValueException::class, 'Cursor - is invalid']; + yield 'cursor - paginator with empty after cursor' => [new ArrayPaginator([], 0, 0), new QueryCollection(class: 'foo'), ['after' => ''], null, ['edges' => ['cursor' => []]], \UnexpectedValueException::class, 'Empty cursor is invalid']; + yield 'cursor - paginator with before cursor' => [new ArrayPaginator([(object) ['test' => 'a'], (object) ['test' => 'b'], (object) ['test' => 'c']], 1, 1), new QueryCollection(class: 'foo'), ['before' => 'Mg=='], ['totalCount' => 3., 'edges' => [['node' => ['normalized_item'], 'cursor' => 'MQ==']], 'pageInfo' => ['startCursor' => 'MQ==', 'endCursor' => 'MQ==', 'hasNextPage' => true, 'hasPreviousPage' => true]], ['totalCount' => true, 'edges' => ['cursor' => true], 'pageInfo' => ['startCursor' => true, 'endCursor' => true, 'hasNextPage' => true, 'hasPreviousPage' => true]]]; + yield 'cursor - paginator with bad before cursor' => [new ArrayPaginator([], 0, 0), new QueryCollection(class: 'foo'), ['before' => '-'], null, ['pageInfo' => ['endCursor' => true]], \UnexpectedValueException::class, 'Cursor - is invalid']; + yield 'cursor - paginator with empty before cursor' => [new ArrayPaginator([], 0, 0), new QueryCollection(class: 'foo'), ['before' => ''], null, ['pageInfo' => ['endCursor' => true]], \UnexpectedValueException::class, 'Empty cursor is invalid']; + yield 'cursor - paginator with last' => [new ArrayPaginator([(object) ['test' => 'a'], (object) ['test' => 'b'], (object) ['test' => 'c']], 1, 2), new QueryCollection(class: 'foo'), ['last' => 2], ['totalCount' => 3., 'edges' => [['node' => ['normalized_item'], 'cursor' => 'MQ=='], ['node' => ['normalized_item'], 'cursor' => 'Mg==']], 'pageInfo' => ['startCursor' => 'MQ==', 'endCursor' => 'Mg==', 'hasNextPage' => false, 'hasPreviousPage' => true]], ['totalCount' => true, 'edges' => ['cursor' => true], 'pageInfo' => ['startCursor' => true, 'endCursor' => true, 'hasNextPage' => true, 'hasPreviousPage' => true]]]; + yield 'cursor - partial paginator' => [$partialPaginatorFactory, new QueryCollection(class: 'foo'), [], ['totalCount' => 0., 'edges' => [], 'pageInfo' => ['startCursor' => 'MA==', 'endCursor' => 'MQ==', 'hasNextPage' => false, 'hasPreviousPage' => false]], ['totalCount' => true, 'edges' => ['cursor' => true], 'pageInfo' => ['startCursor' => true, 'endCursor' => true, 'hasNextPage' => true, 'hasPreviousPage' => true]]]; + yield 'cursor - partial paginator with after cursor' => [$partialPaginatorFactory, new QueryCollection(class: 'foo'), ['after' => 'MA=='], ['totalCount' => 0., 'edges' => [], 'pageInfo' => ['startCursor' => 'MQ==', 'endCursor' => 'Mg==', 'hasNextPage' => false, 'hasPreviousPage' => true]], ['totalCount' => true, 'edges' => ['cursor' => true], 'pageInfo' => ['startCursor' => true, 'endCursor' => true, 'hasNextPage' => true, 'hasPreviousPage' => true]]]; + + yield 'page - not paginator, itemsPerPage requested' => [[], (new QueryCollection(class: 'foo'))->withPaginationType('page'), [], [], ['paginationInfo' => ['itemsPerPage' => true]], \LogicException::class, 'Collection returned by the collection data provider must implement ApiPlatform\State\Pagination\PartialPaginatorInterface to return itemsPerPage field.']; + yield 'page - not paginator, lastPage requested' => [[], (new QueryCollection(class: 'foo'))->withPaginationType('page'), [], [], ['paginationInfo' => ['lastPage' => true]], \LogicException::class, 'Collection returned by the collection data provider must implement ApiPlatform\State\Pagination\PaginatorInterface to return lastPage field.']; + yield 'page - not paginator, totalCount requested' => [[], (new QueryCollection(class: 'foo'))->withPaginationType('page'), [], [], ['paginationInfo' => ['totalCount' => true]], \LogicException::class, 'Collection returned by the collection data provider must implement ApiPlatform\State\Pagination\PaginatorInterface to return totalCount field.']; + yield 'page - empty paginator - itemsPerPage requested' => [new ArrayPaginator([], 0, 0), (new QueryCollection(class: 'foo'))->withPaginationType('page'), [], ['collection' => [], 'paginationInfo' => ['itemsPerPage' => .0]], ['paginationInfo' => ['itemsPerPage' => true]]]; + yield 'page - empty paginator - lastPage requested' => [new ArrayPaginator([], 0, 0), (new QueryCollection(class: 'foo'))->withPaginationType('page'), [], ['collection' => [], 'paginationInfo' => ['lastPage' => 1.0]], ['paginationInfo' => ['lastPage' => true]]]; + yield 'page - empty paginator - totalCount requested' => [new ArrayPaginator([], 0, 0), (new QueryCollection(class: 'foo'))->withPaginationType('page'), [], ['collection' => [], 'paginationInfo' => ['totalCount' => .0]], ['paginationInfo' => ['totalCount' => true]]]; + yield 'page - paginator page 1 - itemsPerPage requested' => [new ArrayPaginator([(object) ['test' => 'a'], (object) ['test' => 'b'], (object) ['test' => 'c']], 0, 2), (new QueryCollection(class: 'foo'))->withPaginationType('page'), [], ['collection' => [['normalized_item'], ['normalized_item']], 'paginationInfo' => ['itemsPerPage' => 2.0]], ['paginationInfo' => ['itemsPerPage' => true]]]; + yield 'page - paginator page 1 - lastPage requested' => [new ArrayPaginator([(object) ['test' => 'a'], (object) ['test' => 'b'], (object) ['test' => 'c']], 0, 2), (new QueryCollection(class: 'foo'))->withPaginationType('page'), [], ['collection' => [['normalized_item'], ['normalized_item']], 'paginationInfo' => ['lastPage' => 2.0]], ['paginationInfo' => ['lastPage' => true]]]; + yield 'page - paginator page 1 - totalCount requested' => [new ArrayPaginator([(object) ['test' => 'a'], (object) ['test' => 'b'], (object) ['test' => 'c']], 0, 2), (new QueryCollection(class: 'foo'))->withPaginationType('page'), [], ['collection' => [['normalized_item'], ['normalized_item']], 'paginationInfo' => ['totalCount' => 3.0]], ['paginationInfo' => ['totalCount' => true]]]; + yield 'page - paginator with page - itemsPerPage requested' => [new ArrayPaginator([(object) ['test' => 'a'], (object) ['test' => 'b'], (object) ['test' => 'c']], 2, 2), (new QueryCollection(class: 'foo'))->withPaginationType('page'), [], ['collection' => [['normalized_item']], 'paginationInfo' => ['itemsPerPage' => 2.0]], ['paginationInfo' => ['itemsPerPage' => true]]]; + yield 'page - paginator with page - lastPage requested' => [new ArrayPaginator([(object) ['test' => 'a'], (object) ['test' => 'b'], (object) ['test' => 'c']], 2, 2), (new QueryCollection(class: 'foo'))->withPaginationType('page'), [], ['collection' => [['normalized_item']], 'paginationInfo' => ['lastPage' => 2.0]], ['paginationInfo' => ['lastPage' => true]]]; + yield 'page - paginator with page - totalCount requested' => [new ArrayPaginator([(object) ['test' => 'a'], (object) ['test' => 'b'], (object) ['test' => 'c']], 2, 2), (new QueryCollection(class: 'foo'))->withPaginationType('page'), [], ['collection' => [['normalized_item']], 'paginationInfo' => ['totalCount' => 3.0]], ['paginationInfo' => ['totalCount' => true]]]; } }