Skip to content

Commit 0f025e8

Browse files
authored
fix(state): handle partial pagination with object mapper (#7769)
| Q | A | ------------- | --- | Branch? | main | Tickets | ∅ | License | MIT | Doc PR | ∅ ObjectMapperProvider only handled PaginatorInterface, but ODM's PartialPaginator implements PartialPaginatorInterface which doesn't extend PaginatorInterface, causing mapping to fail on the paginator object itself.
1 parent e6c510e commit 0f025e8

5 files changed

Lines changed: 243 additions & 0 deletions

File tree

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <dunglas@gmail.com>
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\State\Pagination;
15+
16+
use Symfony\Component\ObjectMapper\ObjectMapperInterface;
17+
18+
final class MappedObjectPartialPaginator implements \IteratorAggregate, PartialPaginatorInterface
19+
{
20+
public function __construct(
21+
private readonly iterable $entities,
22+
private readonly ObjectMapperInterface $mapper,
23+
private readonly string $resourceClass,
24+
private readonly float $currentPage = 1.0,
25+
private readonly float $itemsPerPage = 30.0,
26+
private readonly int $count = 0,
27+
) {
28+
}
29+
30+
public function count(): int
31+
{
32+
return $this->count;
33+
}
34+
35+
public function getCurrentPage(): float
36+
{
37+
return $this->currentPage;
38+
}
39+
40+
public function getItemsPerPage(): float
41+
{
42+
return $this->itemsPerPage;
43+
}
44+
45+
public function getIterator(): \Traversable
46+
{
47+
foreach ($this->entities as $entity) {
48+
yield $this->mapper->map($entity, $this->resourceClass);
49+
}
50+
}
51+
}

src/State/Provider/ObjectMapperProvider.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616
use ApiPlatform\Metadata\Operation;
1717
use ApiPlatform\Metadata\Util\CloneTrait;
1818
use ApiPlatform\State\Pagination\MappedObjectPaginator;
19+
use ApiPlatform\State\Pagination\MappedObjectPartialPaginator;
1920
use ApiPlatform\State\Pagination\PaginatorInterface;
21+
use ApiPlatform\State\Pagination\PartialPaginatorInterface;
2022
use ApiPlatform\State\ProviderInterface;
2123
use Symfony\Component\ObjectMapper\ObjectMapperInterface;
2224

@@ -62,6 +64,15 @@ public function provide(Operation $operation, array $uriVariables = [], array $c
6264
$data->getLastPage(),
6365
$data->getItemsPerPage(),
6466
);
67+
} elseif ($data instanceof PartialPaginatorInterface) {
68+
$data = new MappedObjectPartialPaginator(
69+
$data,
70+
$this->objectMapper,
71+
$class,
72+
$data->getCurrentPage(),
73+
$data->getItemsPerPage(),
74+
\count($data),
75+
);
6576
} elseif (\is_array($data)) {
6677
foreach ($data as &$v) {
6778
if (\is_object($v)) {
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <dunglas@gmail.com>
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\Tests\Fixtures\TestBundle\ApiResource;
15+
16+
use ApiPlatform\Doctrine\Odm\State\Options;
17+
use ApiPlatform\JsonLd\ContextBuilder;
18+
use ApiPlatform\Metadata\ApiResource;
19+
use ApiPlatform\Metadata\GetCollection;
20+
use ApiPlatform\Tests\Fixtures\TestBundle\Document\PartialPaginationMappedDocument;
21+
use Symfony\Component\ObjectMapper\Attribute\Map;
22+
23+
#[ApiResource(
24+
operations: [
25+
new GetCollection(
26+
paginationPartial: true,
27+
paginationItemsPerPage: 3,
28+
normalizationContext: [ContextBuilder::HYDRA_CONTEXT_HAS_PREFIX => false],
29+
),
30+
],
31+
stateOptions: new Options(documentClass: PartialPaginationMappedDocument::class),
32+
)]
33+
#[Map(target: PartialPaginationMappedDocument::class)]
34+
final class PartialPaginationMappedResource
35+
{
36+
#[Map(if: false)]
37+
public ?int $id = null;
38+
39+
#[Map(target: 'name')]
40+
public string $title;
41+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <dunglas@gmail.com>
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\Tests\Fixtures\TestBundle\Document;
15+
16+
use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\PartialPaginationMappedResource;
17+
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
18+
use Symfony\Component\ObjectMapper\Attribute\Map;
19+
20+
#[ODM\Document]
21+
#[Map(target: PartialPaginationMappedResource::class)]
22+
class PartialPaginationMappedDocument
23+
{
24+
#[ODM\Id(strategy: 'INCREMENT', type: 'int')]
25+
private ?int $id = null;
26+
27+
#[Map(target: 'title')]
28+
#[ODM\Field(type: 'string')]
29+
private string $name;
30+
31+
public function getId(): ?int
32+
{
33+
return $this->id;
34+
}
35+
36+
public function getName(): string
37+
{
38+
return $this->name;
39+
}
40+
41+
public function setName(string $name): self
42+
{
43+
$this->name = $name;
44+
45+
return $this;
46+
}
47+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <dunglas@gmail.com>
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\Tests\Functional\Doctrine;
15+
16+
use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
17+
use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\PartialPaginationMappedResource;
18+
use ApiPlatform\Tests\Fixtures\TestBundle\Document\PartialPaginationMappedDocument;
19+
use ApiPlatform\Tests\RecreateSchemaTrait;
20+
use ApiPlatform\Tests\SetupClassResourcesTrait;
21+
22+
final class PartialPaginationMappedTest extends ApiTestCase
23+
{
24+
use RecreateSchemaTrait;
25+
use SetupClassResourcesTrait;
26+
27+
protected static ?bool $alwaysBootKernel = false;
28+
29+
/**
30+
* @return class-string[]
31+
*/
32+
public static function getResources(): array
33+
{
34+
return [PartialPaginationMappedResource::class];
35+
}
36+
37+
public function testPartialPaginationWithObjectMapper(): void
38+
{
39+
if (!$this->isMongoDB()) {
40+
$this->markTestSkipped('MongoDB only test.');
41+
}
42+
43+
if (!$this->getContainer()->has('api_platform.object_mapper')) {
44+
$this->markTestSkipped('ObjectMapper not installed');
45+
}
46+
47+
$this->recreateSchema([PartialPaginationMappedDocument::class]);
48+
$this->loadFixtures();
49+
50+
$client = self::createClient();
51+
$r = $client->request('GET', '/partial_pagination_mapped_resources');
52+
53+
$this->assertResponseIsSuccessful();
54+
$this->assertJsonContains([
55+
'@id' => '/partial_pagination_mapped_resources',
56+
'member' => [
57+
['title' => 'Item 1'],
58+
['title' => 'Item 2'],
59+
['title' => 'Item 3'],
60+
],
61+
'view' => [
62+
'@type' => 'PartialCollectionView',
63+
'next' => '/partial_pagination_mapped_resources?page=2',
64+
],
65+
]);
66+
$this->assertCount(3, $r->toArray()['member']);
67+
68+
$r = $client->request('GET', '/partial_pagination_mapped_resources?page=2');
69+
70+
$this->assertResponseIsSuccessful();
71+
$this->assertJsonContains([
72+
'view' => [
73+
'@type' => 'PartialCollectionView',
74+
'previous' => '/partial_pagination_mapped_resources?page=1',
75+
'next' => '/partial_pagination_mapped_resources?page=3',
76+
],
77+
]);
78+
$this->assertCount(3, $r->toArray()['member']);
79+
}
80+
81+
private function loadFixtures(): void
82+
{
83+
$manager = $this->getManager();
84+
85+
for ($i = 1; $i <= 10; ++$i) {
86+
$doc = new PartialPaginationMappedDocument();
87+
$doc->setName('Item '.$i);
88+
$manager->persist($doc);
89+
}
90+
91+
$manager->flush();
92+
}
93+
}

0 commit comments

Comments
 (0)