Skip to content

Commit 3d12deb

Browse files
committed
ISSUE-345: list cursor pagination
1 parent 33d3eee commit 3d12deb

16 files changed

+239
-74
lines changed

config/services/factories.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
services:
2+
_defaults:
3+
autowire: true
4+
autoconfigure: true
5+
public: false
6+
7+
PhpList\RestBundle\Service\Factory\PaginationCursorRequestFactory:
8+
autowire: true
9+
autoconfigure: true

config/services/normalizers.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,6 @@ services:
4242
PhpList\RestBundle\Serializer\AdministratorNormalizer:
4343
tags: [ 'serializer.normalizer' ]
4444
autowire: true
45+
46+
PhpList\RestBundle\Serializer\CursorPaginationNormalizer:
47+
autowire: true

config/services/providers.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
services:
2+
_defaults:
3+
autowire: true
4+
autoconfigure: true
5+
public: false
6+
7+
PhpList\RestBundle\Service\Provider\SubscriberListProvider:
8+
autowire: true
9+
autoconfigure: true

src/Controller/ListController.php

Lines changed: 39 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010
use PhpList\RestBundle\Controller\Traits\AuthenticationTrait;
1111
use PhpList\RestBundle\Entity\Request\CreateSubscriberListRequest;
1212
use PhpList\RestBundle\Serializer\SubscriberListNormalizer;
13+
use PhpList\RestBundle\Service\Factory\PaginationCursorRequestFactory;
1314
use PhpList\RestBundle\Service\Manager\SubscriberListManager;
15+
use PhpList\RestBundle\Service\Provider\SubscriberListProvider;
1416
use PhpList\RestBundle\Validator\RequestValidator;
1517
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
1618
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
@@ -35,17 +37,23 @@ class ListController extends AbstractController
3537
private SubscriberListNormalizer $normalizer;
3638
private SubscriberListManager $subscriberListManager;
3739
private RequestValidator $validator;
40+
private PaginationCursorRequestFactory $paginationFactory;
41+
private SubscriberListProvider $subscriberListProvider;
3842

3943
public function __construct(
4044
Authentication $authentication,
4145
SubscriberListNormalizer $normalizer,
4246
RequestValidator $validator,
43-
SubscriberListManager $subscriberListManager
47+
SubscriberListManager $subscriberListManager,
48+
PaginationCursorRequestFactory $paginationFactory,
49+
SubscriberListProvider $subscriberListProvider
4450
) {
4551
$this->authentication = $authentication;
4652
$this->normalizer = $normalizer;
4753
$this->validator = $validator;
4854
$this->subscriberListManager = $subscriberListManager;
55+
$this->paginationFactory = $paginationFactory;
56+
$this->subscriberListProvider = $subscriberListProvider;
4957
}
5058

5159
#[Route('', name: 'get_lists', methods: ['GET'])]
@@ -63,36 +71,36 @@ public function __construct(
6371
schema: new OA\Schema(
6472
type: 'string'
6573
)
74+
),
75+
new OA\Parameter(
76+
name: 'after_id',
77+
description: 'Last id (starting from 0)',
78+
in: 'query',
79+
required: false,
80+
schema: new OA\Schema(type: 'integer', default: 1, minimum: 1)
81+
),
82+
new OA\Parameter(
83+
name: 'limit',
84+
description: 'Number of results per page',
85+
in: 'query',
86+
required: false,
87+
schema: new OA\Schema(type: 'integer', default: 25, maximum: 100, minimum: 1)
6688
)
6789
],
6890
responses: [
6991
new OA\Response(
7092
response: 200,
7193
description: 'Success',
7294
content: new OA\JsonContent(
73-
type: 'array',
74-
items: new OA\Items(
75-
properties: [
76-
new OA\Property(property: 'name', type: 'string', example: 'News'),
77-
new OA\Property(
78-
property: 'description',
79-
type: 'string',
80-
example: 'News (and some fun stuff)'
81-
),
82-
new OA\Property(
83-
property: 'creation_date',
84-
type: 'string',
85-
format: 'date-time',
86-
example: '2016-06-22T15:01:17+00:00'
87-
),
88-
new OA\Property(property: 'list_position', type: 'integer', example: 12),
89-
new OA\Property(property: 'subject_prefix', type: 'string', example: 'phpList'),
90-
new OA\Property(property: 'public', type: 'boolean', example: true),
91-
new OA\Property(property: 'category', type: 'string', example: 'news'),
92-
new OA\Property(property: 'id', type: 'integer', example: 1)
93-
],
94-
type: 'object'
95-
)
95+
properties: [
96+
new OA\Property(
97+
property: 'items',
98+
type: 'array',
99+
items: new OA\Items(ref: '#/components/schemas/SubscriberList')
100+
),
101+
new OA\Property(property: 'pagination', ref: '#/components/schemas/CursorPagination')
102+
],
103+
type: 'object'
96104
)
97105
),
98106
new OA\Response(
@@ -105,13 +113,12 @@ public function __construct(
105113
public function getLists(Request $request): JsonResponse
106114
{
107115
$this->requireAuthentication($request);
108-
$data = $this->subscriberListManager->getAll();
109-
110-
$normalized = array_map(function ($item) {
111-
return $this->normalizer->normalize($item);
112-
}, $data);
116+
$pagination = $this->paginationFactory->fromRequest($request);
113117

114-
return new JsonResponse($normalized, Response::HTTP_OK);
118+
return new JsonResponse(
119+
$this->subscriberListProvider->getPaginatedList($pagination),
120+
Response::HTTP_OK
121+
);
115122
}
116123

117124
#[Route('/{listId}', name: 'get_list', methods: ['GET'])]
@@ -140,19 +147,7 @@ public function getLists(Request $request): JsonResponse
140147
new OA\Response(
141148
response: 200,
142149
description: 'Success',
143-
content: new OA\JsonContent(
144-
type: 'object',
145-
example: [
146-
'name' => 'News',
147-
'description' => 'News (and some fun stuff)',
148-
'creation_date' => '2016-06-22T15:01:17+00:00',
149-
'list_position' => 12,
150-
'subject_prefix' => 'phpList',
151-
'public' => true,
152-
'category' => 'news',
153-
'id' => 1
154-
]
155-
)
150+
content: new OA\JsonContent(ref: '#/components/schemas/SubscriberList')
156151
),
157152
new OA\Response(
158153
response: 403,
@@ -276,19 +271,7 @@ public function deleteList(
276271
new OA\Response(
277272
response: 201,
278273
description: 'Success',
279-
content: new OA\JsonContent(
280-
type: 'object',
281-
example: [
282-
'name' => 'News',
283-
'description' => 'News (and some fun stuff)',
284-
'creation_date' => '2016-06-22T15:01:17+00:00',
285-
'list_position' => 12,
286-
'subject_prefix' => 'phpList',
287-
'public' => true,
288-
'category' => 'news',
289-
'id' => 1
290-
]
291-
)
274+
content: new OA\JsonContent(ref: '#/components/schemas/SubscriberList')
292275
),
293276
new OA\Response(
294277
response: 403,
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpList\RestBundle\Entity\Dto;
6+
7+
class CursorPaginationResult
8+
{
9+
public function __construct(
10+
public readonly array $items,
11+
public readonly int $limit,
12+
public readonly int $total,
13+
) {
14+
}
15+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpList\RestBundle\Entity\Request;
6+
7+
use Symfony\Component\HttpFoundation\Request;
8+
9+
class PaginationCursorRequest
10+
{
11+
public ?int $afterId;
12+
public int $limit;
13+
14+
public function __construct(?int $afterId = null, int $limit = 25)
15+
{
16+
$this->afterId = $afterId;
17+
$this->limit = min(100, max(1, $limit));
18+
}
19+
20+
public static function fromRequest(Request $request): self
21+
{
22+
return new self(
23+
$request->query->get('after_id') ? (int)$request->query->get('after_id') : 0,
24+
$request->query->getInt('limit', 25)
25+
);
26+
}
27+
}

src/OpenApi/SwaggerSchemasResponse.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,17 @@
6262
],
6363
type: 'object'
6464
)]
65+
66+
#[OA\Schema(
67+
schema: 'CursorPagination',
68+
properties: [
69+
new OA\Property(property: 'total', type: 'integer', example: 100),
70+
new OA\Property(property: 'limit', type: 'integer', example: 25),
71+
new OA\Property(property: 'has_more', type: 'boolean', example: true),
72+
new OA\Property(property: 'next_cursor', type: 'integer', example: 129)
73+
],
74+
type: 'object'
75+
)]
6576
class SwaggerSchemasResponse
6677
{
6778
}

src/OpenApi/SwaggerSchemasResponseEntity.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
new OA\Property(property: 'name', type: 'string', example: 'Newsletter'),
1414
new OA\Property(property: 'description', type: 'string', example: 'Monthly updates'),
1515
new OA\Property(
16-
property: 'creation_date',
16+
property: 'created_at',
1717
type: 'string',
1818
format: 'date-time',
1919
example: '2022-12-01T10:00:00Z'
@@ -28,7 +28,7 @@
2828
new OA\Property(property: 'id', type: 'integer', example: 1),
2929
new OA\Property(property: 'email', type: 'string', example: '[email protected]'),
3030
new OA\Property(
31-
property: 'creation_date',
31+
property: 'created_at',
3232
type: 'string',
3333
format: 'date-time',
3434
example: '2023-01-01T12:00:00Z',
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpList\RestBundle\Serializer;
6+
7+
use PhpList\RestBundle\Entity\Dto\CursorPaginationResult;
8+
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
9+
10+
class CursorPaginationNormalizer implements NormalizerInterface
11+
{
12+
/**
13+
* @param CursorPaginationResult $object
14+
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
15+
*/
16+
public function normalize($object, string $format = null, array $context = []): array
17+
{
18+
$items = $object->items;
19+
$limit = $object->limit;
20+
$total = $object->total;
21+
$hasNext = !empty($items) && isset($items[array_key_last($items)]['id']);
22+
23+
return [
24+
'items' => $items,
25+
'pagination' => [
26+
'total' => $total,
27+
'limit' => $limit,
28+
'has_more' => count($items) === $limit,
29+
'next_cursor' => $hasNext ? $items[array_key_last($items)]['id'] : null,
30+
],
31+
];
32+
}
33+
34+
/**
35+
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
36+
*/
37+
public function supportsNormalization($data, string $format = null): bool
38+
{
39+
return $data instanceof CursorPaginationResult;
40+
}
41+
}

src/Serializer/SubscriberListNormalizer.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public function normalize($object, string $format = null, array $context = []):
2121
return [
2222
'id' => $object->getId(),
2323
'name' => $object->getName(),
24-
'creation_date' => $object->getCreatedAt()->format('Y-m-d\TH:i:sP'),
24+
'created_at' => $object->getCreatedAt()->format('Y-m-d\TH:i:sP'),
2525
'description' => $object->getDescription(),
2626
'list_position' => $object->getListPosition(),
2727
'subject_prefix' => $object->getSubjectPrefix(),
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpList\RestBundle\Service\Factory;
6+
7+
use PhpList\RestBundle\Entity\Request\PaginationCursorRequest;
8+
use Symfony\Component\HttpFoundation\Request;
9+
10+
class PaginationCursorRequestFactory
11+
{
12+
public function fromRequest(Request $request): PaginationCursorRequest
13+
{
14+
return new PaginationCursorRequest(
15+
$request->query->getInt('after_id'),
16+
$request->query->getInt('limit', 25)
17+
);
18+
}
19+
}

src/Service/Manager/SubscriberListManager.php

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use PhpList\Core\Domain\Model\Subscription\SubscriberList;
99
use PhpList\Core\Domain\Repository\Subscription\SubscriberListRepository;
1010
use PhpList\RestBundle\Entity\Request\CreateSubscriberListRequest;
11+
use PhpList\RestBundle\Entity\Request\PaginationCursorRequest;
1112

1213
class SubscriberListManager
1314
{
@@ -34,10 +35,17 @@ public function createSubscriberList(
3435
return $subscriberList;
3536
}
3637

37-
/** @return SubscriberList[] */
38-
public function getAll(): array
38+
/**
39+
* @return SubscriberList[]
40+
*/
41+
public function getPaginated(PaginationCursorRequest $pagination): array
3942
{
40-
return $this->subscriberListRepository->findAll();
43+
return $this->subscriberListRepository->getAfterId($pagination->afterId, $pagination->limit);
44+
}
45+
46+
public function getTotalCount(): int
47+
{
48+
return $this->subscriberListRepository->count();
4149
}
4250

4351
public function delete(SubscriberList $subscriberList): void
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpList\RestBundle\Service\Provider;
6+
7+
use PhpList\RestBundle\Entity\Dto\CursorPaginationResult;
8+
use PhpList\RestBundle\Entity\Request\PaginationCursorRequest;
9+
use PhpList\RestBundle\Serializer\CursorPaginationNormalizer;
10+
use PhpList\RestBundle\Serializer\SubscriberListNormalizer;
11+
use PhpList\RestBundle\Service\Manager\SubscriberListManager;
12+
13+
class SubscriberListProvider
14+
{
15+
public function __construct(
16+
private readonly SubscriberListManager $subscriberListManager,
17+
private readonly SubscriberListNormalizer $normalizer,
18+
private readonly CursorPaginationNormalizer $paginationNormalizer
19+
) {
20+
}
21+
22+
public function getPaginatedList(PaginationCursorRequest $pagination): array
23+
{
24+
$lists = $this->subscriberListManager->getPaginated($pagination);
25+
$total = $this->subscriberListManager->getTotalCount();
26+
27+
$normalized = array_map(fn($item) => $this->normalizer->normalize($item), $lists);
28+
29+
return $this->paginationNormalizer->normalize(
30+
new CursorPaginationResult($normalized, $pagination->limit, $total)
31+
);
32+
}
33+
}

0 commit comments

Comments
 (0)