Skip to content

Commit 56e550e

Browse files
committed
ISSUE-345: campaign stat normalizer
1 parent 19bed91 commit 56e550e

File tree

6 files changed

+150
-27
lines changed

6 files changed

+150
-27
lines changed

config/services/normalizers.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,7 @@ services:
6969
PhpList\RestBundle\Subscription\Serializer\SubscribersExportRequestNormalizer:
7070
tags: [ 'serializer.normalizer' ]
7171
autowire: true
72+
73+
PhpList\RestBundle\Statistics\Serializer\CampaignStatisticsNormalizer:
74+
tags: [ 'serializer.normalizer' ]
75+
autowire: true

src/Statistics/Controller/AnalyticsController.php

Lines changed: 14 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use PhpList\Core\Security\Authentication;
1111
use PhpList\RestBundle\Common\Controller\BaseController;
1212
use PhpList\RestBundle\Common\Validator\RequestValidator;
13+
use PhpList\RestBundle\Statistics\Serializer\CampaignStatisticsNormalizer;
1314
use Symfony\Component\HttpFoundation\JsonResponse;
1415
use Symfony\Component\HttpFoundation\Request;
1516
use Symfony\Component\HttpFoundation\Response;
@@ -21,15 +22,19 @@
2122
#[Route('/analytics', name: 'analytics_')]
2223
class AnalyticsController extends BaseController
2324
{
25+
public const BATCH_SIZE = 20;
2426
private AnalyticsService $analyticsService;
27+
private CampaignStatisticsNormalizer $campaignStatisticsNormalizer;
2528

2629
public function __construct(
2730
Authentication $authentication,
2831
RequestValidator $validator,
29-
AnalyticsService $analyticsService
32+
AnalyticsService $analyticsService,
33+
CampaignStatisticsNormalizer $campaignStatisticsNormalizer
3034
) {
3135
parent::__construct($authentication, $validator);
3236
$this->analyticsService = $analyticsService;
37+
$this->campaignStatisticsNormalizer = $campaignStatisticsNormalizer;
3338
}
3439

3540
#[Route('/campaigns', name: 'campaign_statistics', methods: ['GET'])]
@@ -56,7 +61,7 @@ public function __construct(
5661
schema: new OA\Schema(type: 'integer', default: 50, maximum: 100, minimum: 1)
5762
),
5863
new OA\Parameter(
59-
name: 'last_id',
64+
name: 'after_id',
6065
description: 'Last seen campaign ID for pagination',
6166
in: 'query',
6267
required: false,
@@ -70,26 +75,11 @@ public function __construct(
7075
content: new OA\JsonContent(
7176
properties: [
7277
new OA\Property(
73-
property: 'campaigns',
78+
property: 'items',
7479
type: 'array',
75-
items: new OA\Items(
76-
properties: [
77-
new OA\Property(property: 'campaignId', type: 'integer'),
78-
new OA\Property(property: 'subject', type: 'string'),
79-
new OA\Property(property: 'dateSent', type: 'string', format: 'date-time'),
80-
new OA\Property(property: 'sent', type: 'integer'),
81-
new OA\Property(property: 'bounces', type: 'integer'),
82-
new OA\Property(property: 'forwards', type: 'integer'),
83-
new OA\Property(property: 'uniqueViews', type: 'integer'),
84-
new OA\Property(property: 'totalClicks', type: 'integer'),
85-
new OA\Property(property: 'uniqueClicks', type: 'integer'),
86-
],
87-
type: 'object'
88-
)
80+
items: new OA\Items(ref: '#/components/schemas/CampaignStatistics')
8981
),
90-
new OA\Property(property: 'total', type: 'integer'),
91-
new OA\Property(property: 'hasMore', type: 'boolean'),
92-
new OA\Property(property: 'lastId', type: 'integer'),
82+
new OA\Property(property: 'pagination', ref: '#/components/schemas/CursorPagination')
9383
],
9484
type: 'object'
9585
)
@@ -108,12 +98,13 @@ public function getCampaignStatistics(Request $request): JsonResponse
10898
throw $this->createAccessDeniedException('You are not allowed to access statistics.');
10999
}
110100

111-
$limit = (int) $request->query->get('limit', 50);
112-
$lastId = (int) $request->query->get('last_id', 0);
101+
$limit = (int) $request->query->get('limit', self::BATCH_SIZE);
102+
$lastId = (int) $request->query->get('after_id', 0);
113103

114104
$data = $this->analyticsService->getCampaignStatistics($limit, $lastId);
105+
$normalizedData = $this->campaignStatisticsNormalizer->normalize($data, null, ['limit' => $limit]);
115106

116-
return $this->json($data, Response::HTTP_OK);
107+
return $this->json($normalizedData, Response::HTTP_OK);
117108
}
118109

119110
#[Route('/view-opens', name: 'view_opens_statistics', methods: ['GET'])]
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpList\RestBundle\Statistics\OpenApi;
6+
7+
class SwaggerSchemasRequest
8+
{
9+
}
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\Statistics\OpenApi;
6+
7+
use OpenApi\Attributes as OA;
8+
9+
#[OA\Schema(
10+
schema: 'CampaignStatistics',
11+
properties: [
12+
new OA\Property(property: 'campaign_id', type: 'integer'),
13+
new OA\Property(property: 'subject', type: 'string'),
14+
new OA\Property(property: 'date_sent', type: 'string', format: 'date-time'),
15+
new OA\Property(property: 'sent', type: 'integer'),
16+
new OA\Property(property: 'bounces', type: 'integer'),
17+
new OA\Property(property: 'forwards', type: 'integer'),
18+
new OA\Property(property: 'unique_views', type: 'integer'),
19+
new OA\Property(property: 'total_clicks', type: 'integer'),
20+
new OA\Property(property: 'unique_clicks', type: 'integer'),
21+
],
22+
type: 'object',
23+
nullable: true
24+
)]
25+
class SwaggerSchemasResponse
26+
{
27+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpList\RestBundle\Statistics\Serializer;
6+
7+
use PhpList\RestBundle\Statistics\Controller\AnalyticsController;
8+
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
9+
10+
class CampaignStatisticsNormalizer implements NormalizerInterface
11+
{
12+
/**
13+
* Normalizes campaign statistics data into an array.
14+
*
15+
* @param mixed $object The object to normalize
16+
* @param string|null $format The format being (de)serialized from or into
17+
* @param array $context Context options for the normalizer
18+
*
19+
* @return array
20+
*/
21+
public function normalize(mixed $object, string $format = null, array $context = []): array
22+
{
23+
if (!is_array($object) || !isset($object['campaigns'])) {
24+
return [];
25+
}
26+
27+
return [
28+
'items' => array_map(function ($campaign) {
29+
return [
30+
'campaign_id' => $campaign['campaignId'] ?? 0,
31+
'subject' => $campaign['subject'] ?? '',
32+
'dateSent' => $campaign['dateSent'] ?? null,
33+
'sent' => $campaign['sent'] ?? 0,
34+
'bounces' => $campaign['bounces'] ?? 0,
35+
'forwards' => $campaign['forwards'] ?? 0,
36+
'unique_views' => $campaign['uniqueViews'] ?? 0,
37+
'total_clicks' => $campaign['totalClicks'] ?? 0,
38+
'unique_clicks' => $campaign['uniqueClicks'] ?? 0,
39+
];
40+
}, $object['campaigns']),
41+
'pagination' => [
42+
'total' => $object['total'] ?? 0,
43+
'limit' => $context['limit'] ?? AnalyticsController::BATCH_SIZE,
44+
'has_more' => $object['hasMore'] ?? false,
45+
'next_cursor' => $object['lastId'] ?? 0,
46+
],
47+
];
48+
}
49+
50+
/**
51+
* Checks whether the given class is supported for normalization by this normalizer.
52+
*
53+
* @param mixed $data Data to normalize
54+
* @param string|null $format The format being (de)serialized from or into
55+
* @param array $context Context options for the normalizer
56+
*
57+
* @return bool
58+
*/
59+
public function supportsNormalization(mixed $data, string $format = null, array $context = []): bool
60+
{
61+
return is_array($data) && isset($data['campaigns']);
62+
}
63+
}

tests/Unit/Statistics/Controller/AnalyticsControllerTest.php

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use PhpList\Core\Security\Authentication;
1212
use PhpList\RestBundle\Common\Validator\RequestValidator;
1313
use PhpList\RestBundle\Statistics\Controller\AnalyticsController;
14+
use PhpList\RestBundle\Statistics\Serializer\CampaignStatisticsNormalizer;
1415
use PHPUnit\Framework\MockObject\MockObject;
1516
use PHPUnit\Framework\TestCase;
1617
use Symfony\Component\HttpFoundation\JsonResponse;
@@ -35,6 +36,7 @@ class AnalyticsControllerTest extends TestCase
3536
private Authentication|MockObject $authentication;
3637
private RequestValidator|MockObject $validator;
3738
private AnalyticsService|MockObject $analyticsService;
39+
private CampaignStatisticsNormalizer|MockObject $campaignStatisticsNormalizer;
3840
private AnalyticsController $controller;
3941
private Administrator|MockObject $administrator;
4042
private Privileges|MockObject $privileges;
@@ -44,10 +46,12 @@ protected function setUp(): void
4446
$this->authentication = $this->createMock(Authentication::class);
4547
$this->validator = $this->createMock(RequestValidator::class);
4648
$this->analyticsService = $this->createMock(AnalyticsService::class);
49+
$this->campaignStatisticsNormalizer = $this->createMock(CampaignStatisticsNormalizer::class);
4750
$this->controller = new TestableAnalyticsController(
4851
$this->authentication,
4952
$this->validator,
50-
$this->analyticsService
53+
$this->analyticsService,
54+
$this->campaignStatisticsNormalizer
5155
);
5256

5357
$this->privileges = $this->createMock(Privileges::class);
@@ -83,7 +87,26 @@ public function testGetCampaignStatisticsReturnsJsonResponse(): void
8387
$request->query->set('limit', '20');
8488
$request->query->set('last_id', '10');
8589

86-
$expectedData = [
90+
$serviceData = [
91+
'campaigns' => [
92+
[
93+
'campaignId' => 1,
94+
'subject' => 'Test Campaign',
95+
'dateSent' => '2023-01-01T00:00:00+00:00',
96+
'sent' => 100,
97+
'bounces' => 5,
98+
'forwards' => 2,
99+
'uniqueViews' => 80,
100+
'totalClicks' => 150,
101+
'uniqueClicks' => 70,
102+
]
103+
],
104+
'total' => 1,
105+
'hasMore' => false,
106+
'lastId' => 1,
107+
];
108+
109+
$normalizedData = [
87110
'campaigns' => [
88111
[
89112
'campaignId' => 1,
@@ -118,13 +141,19 @@ public function testGetCampaignStatisticsReturnsJsonResponse(): void
118141
->expects(self::once())
119142
->method('getCampaignStatistics')
120143
->with(20, 10)
121-
->willReturn($expectedData);
144+
->willReturn($serviceData);
145+
146+
$this->campaignStatisticsNormalizer
147+
->expects(self::once())
148+
->method('normalize')
149+
->with($serviceData)
150+
->willReturn($normalizedData);
122151

123152
$response = $this->controller->getCampaignStatistics($request);
124153

125154
self::assertInstanceOf(JsonResponse::class, $response);
126155
self::assertEquals(Response::HTTP_OK, $response->getStatusCode());
127-
self::assertEquals($expectedData, json_decode($response->getContent(), true));
156+
self::assertEquals($normalizedData, json_decode($response->getContent(), true));
128157
}
129158

130159
public function testGetViewOpensStatisticsWithoutStatisticsPrivilegeThrowsException(): void

0 commit comments

Comments
 (0)