Skip to content

Commit 139e251

Browse files
TatevikGrtatevikg1
andauthored
Subscriber history endpoint (#153)
* Subscriber history endpoint * Refactor: Coupling between objects --------- Co-authored-by: Tatevik <[email protected]>
1 parent f80cba4 commit 139e251

File tree

7 files changed

+299
-23
lines changed

7 files changed

+299
-23
lines changed

config/services/services.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
services:
2+
PhpList\RestBundle\Subscription\Service\SubscriberService:
3+
autowire: true
4+
autoconfigure: true
5+
6+
PhpList\RestBundle\Subscription\Service\SubscriberHistoryService:
7+
autowire: true
8+
autoconfigure: true

src/Common/Service/Provider/PaginatedDataProvider.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,11 @@ public function getPaginatedList(
3737
throw new RuntimeException('Repository not found');
3838
}
3939

40-
$items = $repository->getFilteredAfterId($pagination->afterId, $pagination->limit, $filter);
40+
$items = $repository->getFilteredAfterId(
41+
lastId: $pagination->afterId,
42+
limit: $pagination->limit,
43+
filter: $filter,
44+
);
4145
$total = $repository->count();
4246

4347
$normalizedItems = array_map(

src/Subscription/Controller/SubscriberController.php

Lines changed: 114 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,16 @@
77
use OpenApi\Attributes as OA;
88
use PhpList\Core\Domain\Identity\Model\PrivilegeFlag;
99
use PhpList\Core\Domain\Subscription\Model\Subscriber;
10-
use PhpList\Core\Domain\Subscription\Service\Manager\SubscriberManager;
1110
use PhpList\Core\Security\Authentication;
1211
use PhpList\RestBundle\Common\Controller\BaseController;
1312
use PhpList\RestBundle\Common\Validator\RequestValidator;
1413
use PhpList\RestBundle\Subscription\Request\CreateSubscriberRequest;
1514
use PhpList\RestBundle\Subscription\Request\UpdateSubscriberRequest;
16-
use PhpList\RestBundle\Subscription\Serializer\SubscriberNormalizer;
15+
use PhpList\RestBundle\Subscription\Service\SubscriberService;
1716
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
1817
use Symfony\Component\HttpFoundation\JsonResponse;
1918
use Symfony\Component\HttpFoundation\Request;
2019
use Symfony\Component\HttpFoundation\Response;
21-
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
2220
use Symfony\Component\Routing\Attribute\Route;
2321

2422
/**
@@ -30,19 +28,16 @@
3028
#[Route('/subscribers', name: 'subscriber_')]
3129
class SubscriberController extends BaseController
3230
{
33-
private SubscriberManager $subscriberManager;
34-
private SubscriberNormalizer $subscriberNormalizer;
31+
private SubscriberService $subscriberService;
3532

3633
public function __construct(
3734
Authentication $authentication,
3835
RequestValidator $validator,
39-
SubscriberManager $subscriberManager,
40-
SubscriberNormalizer $subscriberNormalizer,
36+
SubscriberService $subscriberService,
4137
) {
4238
parent::__construct($authentication, $validator);
4339
$this->authentication = $authentication;
44-
$this->subscriberManager = $subscriberManager;
45-
$this->subscriberNormalizer = $subscriberNormalizer;
40+
$this->subscriberService = $subscriberService;
4641
}
4742

4843
#[Route('', name: 'create', methods: ['POST'])]
@@ -98,12 +93,9 @@ public function createSubscriber(Request $request): JsonResponse
9893

9994
/** @var CreateSubscriberRequest $subscriberRequest */
10095
$subscriberRequest = $this->validator->validate($request, CreateSubscriberRequest::class);
101-
$subscriber = $this->subscriberManager->createSubscriber($subscriberRequest->getDto());
96+
$subscriberData = $this->subscriberService->createSubscriber($subscriberRequest);
10297

103-
return $this->json(
104-
$this->subscriberNormalizer->normalize($subscriber, 'json'),
105-
Response::HTTP_CREATED
106-
);
98+
return $this->json($subscriberData, Response::HTTP_CREATED);
10799
}
108100

109101
#[Route('/{subscriberId}', name: 'update', requirements: ['subscriberId' => '\d+'], methods: ['PUT'])]
@@ -171,9 +163,9 @@ public function updateSubscriber(
171163
}
172164
/** @var UpdateSubscriberRequest $updateSubscriberRequest */
173165
$updateSubscriberRequest = $this->validator->validate($request, UpdateSubscriberRequest::class);
174-
$subscriber = $this->subscriberManager->updateSubscriber($updateSubscriberRequest->getDto());
166+
$subscriberData = $this->subscriberService->updateSubscriber($updateSubscriberRequest);
175167

176-
return $this->json($this->subscriberNormalizer->normalize($subscriber, 'json'), Response::HTTP_OK);
168+
return $this->json($subscriberData, Response::HTTP_OK);
177169
}
178170

179171
#[Route('/{subscriberId}', name: 'get_one', requirements: ['subscriberId' => '\d+'], methods: ['GET'])]
@@ -221,11 +213,111 @@ public function getSubscriber(Request $request, int $subscriberId): JsonResponse
221213
{
222214
$this->requireAuthentication($request);
223215

224-
$subscriber = $this->subscriberManager->getSubscriber($subscriberId);
216+
$subscriberData = $this->subscriberService->getSubscriber($subscriberId);
225217

226-
return $this->json($this->subscriberNormalizer->normalize($subscriber), Response::HTTP_OK);
218+
return $this->json($subscriberData, Response::HTTP_OK);
227219
}
228220

221+
#[Route('/{subscriberId}/history', name: 'history', requirements: ['subscriberId' => '\d+'], methods: ['GET'])]
222+
#[OA\Get(
223+
path: '/api/v2/subscribers/{subscriberId}/history',
224+
description: '🚧 **Status: Beta** – This method is under development. Avoid using in production. ',
225+
summary: 'Get subscriber event history',
226+
tags: ['subscribers'],
227+
parameters: [
228+
new OA\Parameter(
229+
name: 'php-auth-pw',
230+
description: 'Session key obtained from login',
231+
in: 'header',
232+
required: true,
233+
schema: new OA\Schema(type: 'string')
234+
),
235+
new OA\Parameter(
236+
name: 'subscriberId',
237+
description: 'Subscriber ID',
238+
in: 'path',
239+
required: true,
240+
schema: new OA\Schema(type: 'integer')
241+
),
242+
new OA\Parameter(
243+
name: 'after_id',
244+
description: 'Page number (pagination)',
245+
in: 'query',
246+
required: false,
247+
schema: new OA\Schema(type: 'integer', default: 1)
248+
),
249+
new OA\Parameter(
250+
name: 'limit',
251+
description: 'Max items per page',
252+
in: 'query',
253+
required: false,
254+
schema: new OA\Schema(type: 'integer', default: 25)
255+
),
256+
new OA\Parameter(
257+
name: 'ip',
258+
description: 'Filter by IP address',
259+
in: 'query',
260+
required: false,
261+
schema: new OA\Schema(type: 'string')
262+
),
263+
new OA\Parameter(
264+
name: 'date_from',
265+
description: 'Filter by date (format: Y-m-d)',
266+
in: 'query',
267+
required: false,
268+
schema: new OA\Schema(type: 'string', format: 'date')
269+
),
270+
new OA\Parameter(
271+
name: 'summery',
272+
description: 'Filter by summary text',
273+
in: 'query',
274+
required: false,
275+
schema: new OA\Schema(type: 'string')
276+
)
277+
],
278+
responses: [
279+
new OA\Response(
280+
response: 200,
281+
description: 'Paginated list of subscriber events',
282+
content: new OA\JsonContent(
283+
properties: [
284+
new OA\Property(
285+
property: 'items',
286+
type: 'array',
287+
items: new OA\Items(ref: '#/components/schemas/SubscriberHistory')
288+
),
289+
new OA\Property(property: 'pagination', ref: '#/components/schemas/CursorPagination')
290+
],
291+
type: 'object'
292+
)
293+
),
294+
new OA\Response(
295+
response: 403,
296+
description: 'Unauthorized',
297+
content: new OA\JsonContent(ref: '#/components/schemas/UnauthorizedResponse')
298+
),
299+
new OA\Response(
300+
response: 404,
301+
description: 'Not Found',
302+
content: new OA\JsonContent(ref: '#/components/schemas/NotFoundErrorResponse')
303+
)
304+
]
305+
)]
306+
public function getSubscriberHistory(
307+
Request $request,
308+
#[MapEntity(mapping: ['subscriberId' => 'id'])] ?Subscriber $subscriber = null,
309+
): JsonResponse {
310+
$this->requireAuthentication($request);
311+
312+
$historyData = $this->subscriberService->getSubscriberHistory($request, $subscriber);
313+
314+
return $this->json(
315+
data: $historyData,
316+
status: Response::HTTP_OK,
317+
);
318+
}
319+
320+
229321
#[Route('/{subscriberId}', name: 'delete', requirements: ['subscriberId' => '\d+'], methods: ['DELETE'])]
230322
#[OA\Delete(
231323
path: '/api/v2/subscribers/{subscriberId}',
@@ -278,7 +370,7 @@ public function deleteSubscriber(
278370
if (!$subscriber) {
279371
throw $this->createNotFoundException('Subscriber not found.');
280372
}
281-
$this->subscriberManager->deleteSubscriber($subscriber);
373+
$this->subscriberService->deleteSubscriber($subscriber);
282374

283375
return $this->json(null, Response::HTTP_NO_CONTENT);
284376
}
@@ -323,9 +415,9 @@ public function setSubscriberAsConfirmed(Request $request): Response
323415
return new Response('<h1>Missing confirmation code.</h1>', 400);
324416
}
325417

326-
try {
327-
$this->subscriberManager->markAsConfirmedByUniqueId($uniqueId);
328-
} catch (NotFoundHttpException) {
418+
$subscriber = $this->subscriberService->confirmSubscriber($uniqueId);
419+
420+
if (!$subscriber) {
329421
return new Response('<h1>Subscriber isn\'t found or already confirmed.</h1>', 404);
330422
}
331423

src/Subscription/OpenApi/SwaggerSchemasResponse.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,23 @@
105105
new OA\Property(property: 'value', type: 'string', example: 'United States'),
106106
],
107107
)]
108+
#[OA\Schema(
109+
schema: 'SubscriberHistory',
110+
properties: [
111+
new OA\Property(property: 'id', type: 'integer', example: 1),
112+
new OA\Property(property: 'ip', type: 'string', example: '127.0.0.1'),
113+
new OA\Property(
114+
property: 'created_at',
115+
type: 'string',
116+
format: 'date-time',
117+
example: '2022-12-01T10:00:00Z'
118+
),
119+
new OA\Property(property: 'summery', type: 'string', example: 'Added by admin'),
120+
new OA\Property(property: 'detail', type: 'string', example: 'Added with add-email on test'),
121+
new OA\Property(property: 'system_info', type: 'string', example: 'HTTP_USER_AGENT = Mozilla/5.0'),
122+
],
123+
type: 'object'
124+
)]
108125
class SwaggerSchemasResponse
109126
{
110127
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpList\RestBundle\Subscription\Serializer;
6+
7+
use PhpList\Core\Domain\Subscription\Model\SubscriberHistory;
8+
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
9+
10+
class SubscriberHistoryNormalizer implements NormalizerInterface
11+
{
12+
/**
13+
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
14+
*/
15+
public function normalize($object, string $format = null, array $context = []): array
16+
{
17+
if (!$object instanceof SubscriberHistory) {
18+
return [];
19+
}
20+
21+
return [
22+
'id' => $object->getId(),
23+
'ip' => $object->getIp(),
24+
'created_at' => $object->getCreatedAt()->format('Y-m-d\TH:i:sP'),
25+
'summery' => $object->getSummary(),
26+
'detail' => $object->getDetail(),
27+
'system_info' => $object->getSystemInfo(),
28+
];
29+
}
30+
31+
/**
32+
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
33+
*/
34+
public function supportsNormalization($data, string $format = null): bool
35+
{
36+
return $data instanceof SubscriberHistory;
37+
}
38+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpList\RestBundle\Subscription\Service;
6+
7+
use DateTimeImmutable;
8+
use Exception;
9+
use PhpList\Core\Domain\Subscription\Model\Filter\SubscriberHistoryFilter;
10+
use PhpList\Core\Domain\Subscription\Model\Subscriber;
11+
use PhpList\Core\Domain\Subscription\Model\SubscriberHistory;
12+
use PhpList\RestBundle\Common\Service\Provider\PaginatedDataProvider;
13+
use Symfony\Component\HttpFoundation\Request;
14+
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
15+
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
16+
use Symfony\Component\Validator\Exception\ValidatorException;
17+
18+
class SubscriberHistoryService
19+
{
20+
public function __construct(
21+
private readonly PaginatedDataProvider $paginatedDataProvider,
22+
private readonly NormalizerInterface $serializer,
23+
) {
24+
}
25+
26+
public function getSubscriberHistory(Request $request, ?Subscriber $subscriber): array
27+
{
28+
if (!$subscriber) {
29+
throw new NotFoundHttpException('Subscriber not found.');
30+
}
31+
32+
try {
33+
$dateFrom = $request->query->get('date_from');
34+
$dateFromFormated = $dateFrom ? new DateTimeImmutable($dateFrom) : null;
35+
} catch (Exception $e) {
36+
throw new ValidatorException('Invalid date format. Use format: Y-m-d');
37+
}
38+
39+
$filter = new SubscriberHistoryFilter(
40+
subscriber: $subscriber,
41+
ip: $request->query->get('ip'),
42+
dateFrom: $dateFromFormated,
43+
summery: $request->query->get('summery'),
44+
);
45+
46+
return $this->paginatedDataProvider->getPaginatedList(
47+
request: $request,
48+
normalizer: $this->serializer,
49+
className: SubscriberHistory::class,
50+
filter: $filter
51+
);
52+
}
53+
}

0 commit comments

Comments
 (0)