Skip to content

Commit 3786974

Browse files
committed
ISSUE-345: view open stat normalizer
1 parent 56e550e commit 3786974

File tree

7 files changed

+137
-51
lines changed

7 files changed

+137
-51
lines changed

config/services/normalizers.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,7 @@ services:
7373
PhpList\RestBundle\Statistics\Serializer\CampaignStatisticsNormalizer:
7474
tags: [ 'serializer.normalizer' ]
7575
autowire: true
76+
77+
PhpList\RestBundle\Statistics\Serializer\ViewOpensStatisticsNormalizer:
78+
tags: [ 'serializer.normalizer' ]
79+
autowire: true

src/Statistics/Controller/AnalyticsController.php

Lines changed: 20 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use PhpList\RestBundle\Common\Controller\BaseController;
1212
use PhpList\RestBundle\Common\Validator\RequestValidator;
1313
use PhpList\RestBundle\Statistics\Serializer\CampaignStatisticsNormalizer;
14+
use PhpList\RestBundle\Statistics\Serializer\ViewOpensStatisticsNormalizer;
1415
use Symfony\Component\HttpFoundation\JsonResponse;
1516
use Symfony\Component\HttpFoundation\Request;
1617
use Symfony\Component\HttpFoundation\Response;
@@ -25,16 +26,19 @@ class AnalyticsController extends BaseController
2526
public const BATCH_SIZE = 20;
2627
private AnalyticsService $analyticsService;
2728
private CampaignStatisticsNormalizer $campaignStatisticsNormalizer;
29+
private ViewOpensStatisticsNormalizer $viewOpensStatisticsNormalizer;
2830

2931
public function __construct(
3032
Authentication $authentication,
3133
RequestValidator $validator,
3234
AnalyticsService $analyticsService,
33-
CampaignStatisticsNormalizer $campaignStatisticsNormalizer
35+
CampaignStatisticsNormalizer $campaignStatisticsNormalizer,
36+
ViewOpensStatisticsNormalizer $viewOpensStatisticsNormalizer
3437
) {
3538
parent::__construct($authentication, $validator);
3639
$this->analyticsService = $analyticsService;
3740
$this->campaignStatisticsNormalizer = $campaignStatisticsNormalizer;
41+
$this->viewOpensStatisticsNormalizer = $viewOpensStatisticsNormalizer;
3842
}
3943

4044
#[Route('/campaigns', name: 'campaign_statistics', methods: ['GET'])]
@@ -58,7 +62,7 @@ public function __construct(
5862
description: 'Maximum number of campaigns to return',
5963
in: 'query',
6064
required: false,
61-
schema: new OA\Schema(type: 'integer', default: 50, maximum: 100, minimum: 1)
65+
schema: new OA\Schema(type: 'integer', default: 20, minimum: 1)
6266
),
6367
new OA\Parameter(
6468
name: 'after_id',
@@ -128,10 +132,10 @@ public function getCampaignStatistics(Request $request): JsonResponse
128132
description: 'Maximum number of campaigns to return',
129133
in: 'query',
130134
required: false,
131-
schema: new OA\Schema(type: 'integer', default: 50, maximum: 100, minimum: 1)
135+
schema: new OA\Schema(type: 'integer', default: 20, minimum: 1)
132136
),
133137
new OA\Parameter(
134-
name: 'last_id',
138+
name: 'after_id',
135139
description: 'Last seen campaign ID for pagination',
136140
in: 'query',
137141
required: false,
@@ -145,22 +149,11 @@ public function getCampaignStatistics(Request $request): JsonResponse
145149
content: new OA\JsonContent(
146150
properties: [
147151
new OA\Property(
148-
property: 'campaigns',
152+
property: 'items',
149153
type: 'array',
150-
items: new OA\Items(
151-
properties: [
152-
new OA\Property(property: 'campaignId', type: 'integer'),
153-
new OA\Property(property: 'subject', type: 'string'),
154-
new OA\Property(property: 'sent', type: 'integer'),
155-
new OA\Property(property: 'uniqueViews', type: 'integer'),
156-
new OA\Property(property: 'rate', type: 'number', format: 'float'),
157-
],
158-
type: 'object'
159-
)
154+
items: new OA\Items(ref: '#/components/schemas/ViewOpensStatistics')
160155
),
161-
new OA\Property(property: 'total', type: 'integer'),
162-
new OA\Property(property: 'hasMore', type: 'boolean'),
163-
new OA\Property(property: 'lastId', type: 'integer'),
156+
new OA\Property(property: 'pagination', ref: '#/components/schemas/CursorPagination')
164157
],
165158
type: 'object'
166159
)
@@ -179,12 +172,16 @@ public function getViewOpensStatistics(Request $request): JsonResponse
179172
throw $this->createAccessDeniedException('You are not allowed to access statistics.');
180173
}
181174

182-
$limit = (int) $request->query->get('limit', 50);
183-
$lastId = (int) $request->query->get('last_id', 0);
175+
$limit = (int) $request->query->get('limit', self::BATCH_SIZE);
176+
$lastId = (int) $request->query->get('after_id', 0);
184177

185178
$data = $this->analyticsService->getViewOpensStatistics($limit, $lastId);
179+
$normalizedData = $this->viewOpensStatisticsNormalizer->normalize($data, null, [
180+
'view_opens_statistics' => true,
181+
'limit' => $limit
182+
]);
186183

187-
return $this->json($data, Response::HTTP_OK);
184+
return $this->json($normalizedData, Response::HTTP_OK);
188185
}
189186

190187
#[Route('/domains/top', name: 'top_domains', methods: ['GET'])]
@@ -208,7 +205,7 @@ public function getViewOpensStatistics(Request $request): JsonResponse
208205
description: 'Maximum number of domains to return',
209206
in: 'query',
210207
required: false,
211-
schema: new OA\Schema(type: 'integer', default: 50, maximum: 100, minimum: 1)
208+
schema: new OA\Schema(type: 'integer', default: 20, minimum: 1)
212209
),
213210
new OA\Parameter(
214211
name: 'min_subscribers',
@@ -254,7 +251,7 @@ public function getTopDomains(Request $request): JsonResponse
254251
throw $this->createAccessDeniedException('You are not allowed to access statistics.');
255252
}
256253

257-
$limit = (int) $request->query->get('limit', 50);
254+
$limit = (int) $request->query->get('limit', self::BATCH_SIZE);
258255
$minSubscribers = (int) $request->query->get('min_subscribers', 5);
259256

260257
$data = $this->analyticsService->getTopDomains($limit, $minSubscribers);

src/Statistics/OpenApi/SwaggerSchemasResponse.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,18 @@
2222
type: 'object',
2323
nullable: true
2424
)]
25+
#[OA\Schema(
26+
schema: 'ViewOpensStatistics',
27+
properties: [
28+
new OA\Property(property: 'campaign_id', type: 'integer'),
29+
new OA\Property(property: 'subject', type: 'string'),
30+
new OA\Property(property: 'sent', type: 'integer'),
31+
new OA\Property(property: 'unique_views', type: 'integer'),
32+
new OA\Property(property: 'rate', type: 'number', format: 'float'),
33+
],
34+
type: 'object',
35+
nullable: true
36+
)]
2537
class SwaggerSchemasResponse
2638
{
2739
}

src/Statistics/Serializer/CampaignStatisticsNormalizer.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,20 +29,20 @@ public function normalize(mixed $object, string $format = null, array $context =
2929
return [
3030
'campaign_id' => $campaign['campaignId'] ?? 0,
3131
'subject' => $campaign['subject'] ?? '',
32-
'dateSent' => $campaign['dateSent'] ?? null,
3332
'sent' => $campaign['sent'] ?? 0,
3433
'bounces' => $campaign['bounces'] ?? 0,
3534
'forwards' => $campaign['forwards'] ?? 0,
3635
'unique_views' => $campaign['uniqueViews'] ?? 0,
3736
'total_clicks' => $campaign['totalClicks'] ?? 0,
3837
'unique_clicks' => $campaign['uniqueClicks'] ?? 0,
38+
'date_sent' => $campaign['dateSent'] ?? null,
3939
];
4040
}, $object['campaigns']),
4141
'pagination' => [
4242
'total' => $object['total'] ?? 0,
4343
'limit' => $context['limit'] ?? AnalyticsController::BATCH_SIZE,
4444
'has_more' => $object['hasMore'] ?? false,
45-
'next_cursor' => $object['lastId'] ?? 0,
45+
'next_cursor' => $object['lastId'] ? $object['lastId'] + 1 : 0,
4646
],
4747
];
4848
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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 ViewOpensStatisticsNormalizer implements NormalizerInterface
11+
{
12+
/**
13+
* Normalizes view opens 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 ($item) {
29+
return [
30+
'campaign_id' => $item['campaignId'] ?? 0,
31+
'subject' => $item['subject'] ?? '',
32+
'sent' => $item['sent'] ?? 0,
33+
'unique_views' => $item['uniqueViews'] ?? 0,
34+
'rate' => $item['rate'] ?? 0.0,
35+
];
36+
}, $object['campaigns']),
37+
'pagination' => [
38+
'total' => $object['total'] ?? 0,
39+
'limit' => $context['limit'] ?? AnalyticsController::BATCH_SIZE,
40+
'has_more' => $object['hasMore'] ?? false,
41+
'next_cursor' => $object['lastId'] ? $object['lastId'] + 1 : 0,
42+
],
43+
];
44+
}
45+
46+
/**
47+
* Checks whether the given class is supported for normalization by this normalizer.
48+
*
49+
* @param mixed $data Data to normalize
50+
* @param string|null $format The format being (de)serialized from or into
51+
* @param array $context Context options for the normalizer
52+
*
53+
* @return bool
54+
*/
55+
public function supportsNormalization(mixed $data, string $format = null, array $context = []): bool
56+
{
57+
return is_array($data) && isset($data['items']) && isset($context['view_opens_statistics']);
58+
}
59+
}

tests/Integration/Statistics/Controller/AnalyticsControllerTest.php

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,8 @@ public function testGetCampaignStatisticsReturnsCampaignData(): void
5555
$response = $this->getDecodedJsonResponseContent();
5656

5757
self::assertIsArray($response);
58-
self::assertArrayHasKey('campaigns', $response);
59-
self::assertArrayHasKey('total', $response);
60-
self::assertArrayHasKey('hasMore', $response);
61-
self::assertArrayHasKey('lastId', $response);
58+
self::assertArrayHasKey('items', $response);
59+
self::assertArrayHasKey('pagination', $response);
6260
}
6361

6462
public function testGetViewOpensStatisticsWithoutSessionKeyReturnsForbidden(): void

tests/Unit/Statistics/Controller/AnalyticsControllerTest.php

Lines changed: 38 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use PhpList\RestBundle\Common\Validator\RequestValidator;
1313
use PhpList\RestBundle\Statistics\Controller\AnalyticsController;
1414
use PhpList\RestBundle\Statistics\Serializer\CampaignStatisticsNormalizer;
15+
use PhpList\RestBundle\Statistics\Serializer\ViewOpensStatisticsNormalizer;
1516
use PHPUnit\Framework\MockObject\MockObject;
1617
use PHPUnit\Framework\TestCase;
1718
use Symfony\Component\HttpFoundation\JsonResponse;
@@ -46,12 +47,14 @@ protected function setUp(): void
4647
$this->authentication = $this->createMock(Authentication::class);
4748
$this->validator = $this->createMock(RequestValidator::class);
4849
$this->analyticsService = $this->createMock(AnalyticsService::class);
49-
$this->campaignStatisticsNormalizer = $this->createMock(CampaignStatisticsNormalizer::class);
50+
$this->campaignStatisticsNormalizer = new CampaignStatisticsNormalizer();
51+
$this->viewOpensStatisticsNormalizer = new ViewOpensStatisticsNormalizer();
5052
$this->controller = new TestableAnalyticsController(
5153
$this->authentication,
5254
$this->validator,
5355
$this->analyticsService,
54-
$this->campaignStatisticsNormalizer
56+
$this->campaignStatisticsNormalizer,
57+
$this->viewOpensStatisticsNormalizer,
5558
);
5659

5760
$this->privileges = $this->createMock(Privileges::class);
@@ -85,7 +88,7 @@ public function testGetCampaignStatisticsReturnsJsonResponse(): void
8588
{
8689
$request = new Request();
8790
$request->query->set('limit', '20');
88-
$request->query->set('last_id', '10');
91+
$request->query->set('after_id', '10');
8992

9093
$serviceData = [
9194
'campaigns' => [
@@ -107,22 +110,25 @@ public function testGetCampaignStatisticsReturnsJsonResponse(): void
107110
];
108111

109112
$normalizedData = [
110-
'campaigns' => [
113+
'items' => [
111114
[
112-
'campaignId' => 1,
115+
'campaign_id' => 1,
113116
'subject' => 'Test Campaign',
114-
'dateSent' => '2023-01-01T00:00:00+00:00',
117+
'date_sent' => '2023-01-01T00:00:00+00:00',
115118
'sent' => 100,
116119
'bounces' => 5,
117120
'forwards' => 2,
118-
'uniqueViews' => 80,
119-
'totalClicks' => 150,
120-
'uniqueClicks' => 70,
121+
'unique_views' => 80,
122+
'total_clicks' => 150,
123+
'unique_clicks' => 70,
121124
]
122125
],
123-
'total' => 1,
124-
'hasMore' => false,
125-
'lastId' => 1,
126+
'pagination' => [
127+
'total' => 1,
128+
'limit' => 20,
129+
'has_more' => false,
130+
'next_cursor' => 2,
131+
],
126132
];
127133

128134
$this->authentication
@@ -143,15 +149,8 @@ public function testGetCampaignStatisticsReturnsJsonResponse(): void
143149
->with(20, 10)
144150
->willReturn($serviceData);
145151

146-
$this->campaignStatisticsNormalizer
147-
->expects(self::once())
148-
->method('normalize')
149-
->with($serviceData)
150-
->willReturn($normalizedData);
151-
152152
$response = $this->controller->getCampaignStatistics($request);
153153

154-
self::assertInstanceOf(JsonResponse::class, $response);
155154
self::assertEquals(Response::HTTP_OK, $response->getStatusCode());
156155
self::assertEquals($normalizedData, json_decode($response->getContent(), true));
157156
}
@@ -182,7 +181,7 @@ public function testGetViewOpensStatisticsReturnsJsonResponse(): void
182181
{
183182
$request = new Request();
184183
$request->query->set('limit', '20');
185-
$request->query->set('last_id', '10');
184+
$request->query->set('after_id', '10');
186185

187186
$expectedData = [
188187
'campaigns' => [
@@ -199,6 +198,24 @@ public function testGetViewOpensStatisticsReturnsJsonResponse(): void
199198
'lastId' => 1,
200199
];
201200

201+
$normalizedData = [
202+
'items' => [
203+
[
204+
'campaign_id' => 1,
205+
'subject' => 'Test Campaign',
206+
'sent' => 100,
207+
'unique_views' => 80,
208+
'rate' => 80,
209+
]
210+
],
211+
'pagination' => [
212+
'total' => 1,
213+
'limit' => 20,
214+
'has_more' => false,
215+
'next_cursor' => 2,
216+
],
217+
];
218+
202219
$this->authentication
203220
->expects(self::once())
204221
->method('authenticateByApiKey')
@@ -219,9 +236,8 @@ public function testGetViewOpensStatisticsReturnsJsonResponse(): void
219236

220237
$response = $this->controller->getViewOpensStatistics($request);
221238

222-
self::assertInstanceOf(JsonResponse::class, $response);
223239
self::assertEquals(Response::HTTP_OK, $response->getStatusCode());
224-
self::assertEquals($expectedData, json_decode($response->getContent(), true));
240+
self::assertEquals($normalizedData, json_decode($response->getContent(), true));
225241
}
226242

227243
public function testGetTopDomainsWithoutStatisticsPrivilegeThrowsException(): void

0 commit comments

Comments
 (0)