Skip to content

Commit 30b8e48

Browse files
committed
ISSUE-345: top domains stats
1 parent e2245aa commit 30b8e48

File tree

5 files changed

+235
-7
lines changed

5 files changed

+235
-7
lines changed

config/services/normalizers.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,7 @@ services:
7777
PhpList\RestBundle\Statistics\Serializer\ViewOpensStatisticsNormalizer:
7878
tags: [ 'serializer.normalizer' ]
7979
autowire: true
80+
81+
PhpList\RestBundle\Statistics\Serializer\TopDomainsNormalizer:
82+
tags: [ 'serializer.normalizer' ]
83+
autowire: true

src/Statistics/Controller/AnalyticsController.php

Lines changed: 9 additions & 2 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\TopDomainsNormalizer;
1415
use PhpList\RestBundle\Statistics\Serializer\ViewOpensStatisticsNormalizer;
1516
use Symfony\Component\HttpFoundation\JsonResponse;
1617
use Symfony\Component\HttpFoundation\Request;
@@ -27,18 +28,21 @@ class AnalyticsController extends BaseController
2728
private AnalyticsService $analyticsService;
2829
private CampaignStatisticsNormalizer $campaignStatsNormalizer;
2930
private ViewOpensStatisticsNormalizer $viewOpensStatsNormalizer;
31+
private TopDomainsNormalizer $topDomainsNormalizer;
3032

3133
public function __construct(
3234
Authentication $authentication,
3335
RequestValidator $validator,
3436
AnalyticsService $analyticsService,
3537
CampaignStatisticsNormalizer $campaignStatsNormalizer,
36-
ViewOpensStatisticsNormalizer $viewOpensStatsNormalizer
38+
ViewOpensStatisticsNormalizer $viewOpensStatsNormalizer,
39+
TopDomainsNormalizer $topDomainsNormalizer
3740
) {
3841
parent::__construct($authentication, $validator);
3942
$this->analyticsService = $analyticsService;
4043
$this->campaignStatsNormalizer = $campaignStatsNormalizer;
4144
$this->viewOpensStatsNormalizer = $viewOpensStatsNormalizer;
45+
$this->topDomainsNormalizer = $topDomainsNormalizer;
4246
}
4347

4448
#[Route('/campaigns', name: 'campaign_statistics', methods: ['GET'])]
@@ -245,8 +249,11 @@ public function getTopDomains(Request $request): JsonResponse
245249
$minSubscribers = (int) $request->query->get('min_subscribers', 5);
246250

247251
$data = $this->analyticsService->getTopDomains($limit, $minSubscribers);
252+
$normalizedData = $this->topDomainsNormalizer->normalize($data, null, [
253+
'top_domains' => true,
254+
]);
248255

249-
return $this->json($data, Response::HTTP_OK);
256+
return $this->json($normalizedData, Response::HTTP_OK);
250257
}
251258

252259
#[Route('/domains/confirmation', name: 'domain_confirmation_statistics', methods: ['GET'])]
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\Statistics\Serializer;
6+
7+
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
8+
9+
class TopDomainsNormalizer implements NormalizerInterface
10+
{
11+
/**
12+
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
13+
*/
14+
public function normalize(mixed $object, string $format = null, array $context = []): array
15+
{
16+
if (!is_array($object)) {
17+
return [];
18+
}
19+
20+
$domains = [];
21+
foreach ($object['domains'] ?? [] as $domain) {
22+
$domains[] = [
23+
'domain' => $domain['domain'] ?? '',
24+
'subscribers' => $domain['subscribers'] ?? 0,
25+
];
26+
}
27+
28+
return [
29+
'domains' => $domains,
30+
'total' => $object['total'] ?? 0,
31+
];
32+
}
33+
34+
/**
35+
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
36+
*/
37+
public function supportsNormalization(mixed $data, string $format = null, array $context = []): bool
38+
{
39+
return is_array($data) && isset($context['top_domains']);
40+
}
41+
}

tests/Integration/Statistics/Controller/AnalyticsControllerTest.php

Lines changed: 68 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public function testGetCampaignStatisticsWithExpiredSessionKeyReturnsForbidden()
4242
public function testGetCampaignStatisticsWithValidSessionReturnsOkay(): void
4343
{
4444
$this->loadFixtures([AdministratorFixture::class, AdministratorTokenFixture::class, MessageFixture::class]);
45-
45+
4646
$this->authenticatedJsonRequest('GET', '/api/v2/analytics/campaigns');
4747
$this->assertHttpOkay();
4848
}
@@ -68,7 +68,7 @@ public function testGetViewOpensStatisticsWithoutSessionKeyReturnsForbidden(): v
6868
public function testGetViewOpensStatisticsWithValidSessionReturnsOkay(): void
6969
{
7070
$this->loadFixtures([AdministratorFixture::class, AdministratorTokenFixture::class, MessageFixture::class]);
71-
71+
7272
$this->authenticatedJsonRequest('GET', '/api/v2/analytics/view-opens');
7373
$this->assertHttpOkay();
7474
}
@@ -96,7 +96,7 @@ public function testGetTopDomainsWithoutSessionKeyReturnsForbidden(): void
9696
public function testGetTopDomainsWithValidSessionReturnsOkay(): void
9797
{
9898
$this->loadFixtures([AdministratorFixture::class, AdministratorTokenFixture::class, SubscriberFixture::class]);
99-
99+
100100
$this->authenticatedJsonRequest('GET', '/api/v2/analytics/domains/top');
101101
$this->assertHttpOkay();
102102
}
@@ -111,6 +111,69 @@ public function testGetTopDomainsReturnsDomainsData(): void
111111
self::assertIsArray($response);
112112
self::assertArrayHasKey('domains', $response);
113113
self::assertArrayHasKey('total', $response);
114+
self::assertIsArray($response['domains']);
115+
self::assertIsInt($response['total']);
116+
}
117+
118+
public function testGetTopDomainsWithLimitParameter(): void
119+
{
120+
$this->loadFixtures([AdministratorFixture::class, AdministratorTokenFixture::class, SubscriberFixture::class]);
121+
122+
$this->authenticatedJsonRequest('GET', '/api/v2/analytics/domains/top?limit=5');
123+
$response = $this->getDecodedJsonResponseContent();
124+
125+
self::assertIsArray($response);
126+
self::assertArrayHasKey('domains', $response);
127+
self::assertIsArray($response['domains']);
128+
self::assertLessThanOrEqual(5, count($response['domains']));
129+
}
130+
131+
public function testGetTopDomainsWithMinSubscribersParameter(): void
132+
{
133+
$this->loadFixtures([AdministratorFixture::class, AdministratorTokenFixture::class, SubscriberFixture::class]);
134+
135+
$this->authenticatedJsonRequest('GET', '/api/v2/analytics/domains/top?min_subscribers=10');
136+
$response = $this->getDecodedJsonResponseContent();
137+
138+
self::assertIsArray($response);
139+
self::assertArrayHasKey('domains', $response);
140+
self::assertIsArray($response['domains']);
141+
142+
// Verify all domains have at least 10 subscribers
143+
foreach ($response['domains'] as $domain) {
144+
self::assertArrayHasKey('subscribers', $domain);
145+
self::assertGreaterThanOrEqual(10, $domain['subscribers']);
146+
}
147+
}
148+
149+
public function testGetTopDomainsWithBothParameters(): void
150+
{
151+
$this->loadFixtures([AdministratorFixture::class, AdministratorTokenFixture::class, SubscriberFixture::class]);
152+
153+
$this->authenticatedJsonRequest('GET', '/api/v2/analytics/domains/top?limit=3&min_subscribers=10');
154+
$response = $this->getDecodedJsonResponseContent();
155+
156+
self::assertIsArray($response);
157+
self::assertArrayHasKey('domains', $response);
158+
self::assertIsArray($response['domains']);
159+
self::assertLessThanOrEqual(3, count($response['domains']));
160+
161+
foreach ($response['domains'] as $domain) {
162+
self::assertArrayHasKey('subscribers', $domain);
163+
self::assertGreaterThanOrEqual(10, $domain['subscribers']);
164+
}
165+
}
166+
167+
public function testGetTopDomainsWithInvalidLimitParameter(): void
168+
{
169+
$this->loadFixtures([AdministratorFixture::class, AdministratorTokenFixture::class, SubscriberFixture::class]);
170+
171+
$this->authenticatedJsonRequest('GET', '/api/v2/analytics/domains/top?limit=invalid');
172+
$response = $this->getDecodedJsonResponseContent();
173+
174+
self::assertIsArray($response);
175+
self::assertArrayHasKey('domains', $response);
176+
self::assertIsArray($response['domains']);
114177
}
115178

116179
public function testGetDomainConfirmationStatisticsWithoutSessionKeyReturnsForbidden(): void
@@ -122,7 +185,7 @@ public function testGetDomainConfirmationStatisticsWithoutSessionKeyReturnsForbi
122185
public function testGetDomainConfirmationStatisticsWithValidSessionReturnsOkay(): void
123186
{
124187
$this->loadFixtures([AdministratorFixture::class, AdministratorTokenFixture::class, SubscriberFixture::class]);
125-
188+
126189
$this->authenticatedJsonRequest('GET', '/api/v2/analytics/domains/confirmation');
127190
$this->assertHttpOkay();
128191
}
@@ -148,7 +211,7 @@ public function testGetTopLocalPartsWithoutSessionKeyReturnsForbidden(): void
148211
public function testGetTopLocalPartsWithValidSessionReturnsOkay(): void
149212
{
150213
$this->loadFixtures([AdministratorFixture::class, AdministratorTokenFixture::class, SubscriberFixture::class]);
151-
214+
152215
$this->authenticatedJsonRequest('GET', '/api/v2/analytics/local-parts/top');
153216
$this->assertHttpOkay();
154217
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpList\RestBundle\Tests\Unit\Statistics\Serializer;
6+
7+
use PhpList\RestBundle\Statistics\Serializer\TopDomainsNormalizer;
8+
use PHPUnit\Framework\TestCase;
9+
10+
class TopDomainsNormalizerTest extends TestCase
11+
{
12+
public function testNormalizeWithValidData(): void
13+
{
14+
$data = [
15+
'domains' => [
16+
['domain' => 'example.com', 'subscribers' => 100],
17+
['domain' => 'test.org', 'subscribers' => 50],
18+
],
19+
'total' => 150,
20+
];
21+
22+
$normalizer = new TopDomainsNormalizer();
23+
$result = $normalizer->normalize($data);
24+
25+
$this->assertIsArray($result);
26+
$this->assertArrayHasKey('domains', $result);
27+
$this->assertArrayHasKey('total', $result);
28+
$this->assertEquals(150, $result['total']);
29+
$this->assertCount(2, $result['domains']);
30+
$this->assertEquals('example.com', $result['domains'][0]['domain']);
31+
$this->assertEquals(100, $result['domains'][0]['subscribers']);
32+
$this->assertEquals('test.org', $result['domains'][1]['domain']);
33+
$this->assertEquals(50, $result['domains'][1]['subscribers']);
34+
}
35+
36+
public function testNormalizeWithMissingFields(): void
37+
{
38+
$data = [
39+
'domains' => [
40+
['domain' => 'example.com'],
41+
['subscribers' => 50],
42+
[],
43+
],
44+
];
45+
46+
$normalizer = new TopDomainsNormalizer();
47+
$result = $normalizer->normalize($data);
48+
49+
$this->assertIsArray($result);
50+
$this->assertArrayHasKey('domains', $result);
51+
$this->assertArrayHasKey('total', $result);
52+
$this->assertEquals(0, $result['total']);
53+
$this->assertCount(3, $result['domains']);
54+
$this->assertEquals('example.com', $result['domains'][0]['domain']);
55+
$this->assertEquals(0, $result['domains'][0]['subscribers']);
56+
$this->assertEquals('', $result['domains'][1]['domain']);
57+
$this->assertEquals(50, $result['domains'][1]['subscribers']);
58+
$this->assertEquals('', $result['domains'][2]['domain']);
59+
$this->assertEquals(0, $result['domains'][2]['subscribers']);
60+
}
61+
62+
public function testNormalizeWithEmptyDomains(): void
63+
{
64+
$data = [
65+
'domains' => [],
66+
'total' => 0,
67+
];
68+
69+
$normalizer = new TopDomainsNormalizer();
70+
$result = $normalizer->normalize($data);
71+
72+
$this->assertIsArray($result);
73+
$this->assertArrayHasKey('domains', $result);
74+
$this->assertArrayHasKey('total', $result);
75+
$this->assertEquals(0, $result['total']);
76+
$this->assertEmpty($result['domains']);
77+
}
78+
79+
public function testNormalizeWithNoDomains(): void
80+
{
81+
$data = [
82+
'total' => 100,
83+
];
84+
85+
$normalizer = new TopDomainsNormalizer();
86+
$result = $normalizer->normalize($data);
87+
88+
$this->assertIsArray($result);
89+
$this->assertArrayHasKey('domains', $result);
90+
$this->assertArrayHasKey('total', $result);
91+
$this->assertEquals(100, $result['total']);
92+
$this->assertEmpty($result['domains']);
93+
}
94+
95+
public function testNormalizeWithInvalidObject(): void
96+
{
97+
$normalizer = new TopDomainsNormalizer();
98+
$result = $normalizer->normalize('not an array');
99+
100+
$this->assertIsArray($result);
101+
$this->assertEmpty($result);
102+
}
103+
104+
public function testSupportsNormalization(): void
105+
{
106+
$normalizer = new TopDomainsNormalizer();
107+
108+
$this->assertTrue($normalizer->supportsNormalization([], null, ['top_domains' => true]));
109+
$this->assertFalse($normalizer->supportsNormalization([], null, []));
110+
$this->assertFalse($normalizer->supportsNormalization('not an array', null, ['top_domains' => true]));
111+
$this->assertFalse($normalizer->supportsNormalization(new \stdClass(), null, ['top_domains' => true]));
112+
}
113+
}

0 commit comments

Comments
 (0)