Skip to content

Commit eb763b3

Browse files
committed
Subscriber filter in get subscribers endpoint
1 parent f49ce4e commit eb763b3

8 files changed

Lines changed: 219 additions & 27 deletions

File tree

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,11 @@ contribute and how to run the unit tests and style checks locally.
6060
This project adheres to a [Contributor Code of Conduct](CODE_OF_CONDUCT.md).
6161
By participating in this project and its community, you are expected to uphold
6262
this code.
63+
64+
65+
### Code style checks
66+
```bash
67+
vendor/bin/phpstan analyse -l 5 src/ tests/
68+
vendor/bin/phpmd src/ text vendor/phplist/core/config/PHPMD/rules.xml
69+
vendor/bin/phpcs --standard=vendor/phplist/core/config/PhpCodeSniffer/ src/ tests/
70+
```

src/Common/Validator/RequestValidator.php

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ public function __construct(
2323
public function validate(Request $request, string $dtoClass): RequestInterface
2424
{
2525
try {
26-
$body = json_decode($request->getContent(), true, 512, JSON_THROW_ON_ERROR);
26+
$content = $request->getContent();
27+
$body = ($content !== '' && $content !== '0') ? json_decode($content, true, 512, JSON_THROW_ON_ERROR) : [];
2728
} catch (Throwable $e) {
2829
throw new BadRequestHttpException('Invalid JSON: ' . $e->getMessage());
2930
}
@@ -33,7 +34,7 @@ public function validate(Request $request, string $dtoClass): RequestInterface
3334
$routeParams['listId'] = (int) $routeParams['listId'];
3435
}
3536

36-
$data = array_merge($routeParams, $body ?? []);
37+
$data = array_merge($routeParams, $request->query->all(), $body ?? []);
3738

3839
try {
3940
/** @var RequestInterface $dto */
@@ -44,7 +45,9 @@ public function validate(Request $request, string $dtoClass): RequestInterface
4445
['allow_extra_attributes' => true]
4546
);
4647
} catch (Throwable $e) {
47-
throw new BadRequestHttpException('Invalid request data: ' . $e->getMessage());
48+
throw new BadRequestHttpException(
49+
'Invalid request data: ' . $e->getMessage() . ' Data: ' . json_encode($data)
50+
);
4851
}
4952

5053
return $this->validateDto($dto);

src/Subscription/Controller/SubscriberController.php

Lines changed: 65 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@
77
use Doctrine\ORM\EntityManagerInterface;
88
use OpenApi\Attributes as OA;
99
use PhpList\Core\Domain\Identity\Model\PrivilegeFlag;
10-
use PhpList\Core\Domain\Subscription\Model\Filter\SubscriberFilter;
1110
use PhpList\Core\Domain\Subscription\Model\Subscriber;
1211
use PhpList\Core\Domain\Subscription\Service\Manager\SubscriberManager;
1312
use PhpList\Core\Security\Authentication;
1413
use PhpList\RestBundle\Common\Controller\BaseController;
1514
use PhpList\RestBundle\Common\Validator\RequestValidator;
1615
use PhpList\RestBundle\Common\Service\Provider\PaginatedDataProvider;
1716
use PhpList\RestBundle\Subscription\Request\CreateSubscriberRequest;
17+
use PhpList\RestBundle\Subscription\Request\SubscribersFilterRequest;
1818
use PhpList\RestBundle\Subscription\Request\UpdateSubscriberRequest;
1919
use PhpList\RestBundle\Subscription\Serializer\SubscriberNormalizer;
2020
use PhpList\RestBundle\Subscription\Service\SubscriberHistoryService;
@@ -75,7 +75,61 @@ public function __construct(
7575
in: 'query',
7676
required: false,
7777
schema: new OA\Schema(type: 'integer', default: 25, maximum: 100, minimum: 1)
78-
)
78+
),
79+
new OA\Parameter(
80+
name: 'is_confirmed',
81+
description: 'Filter by confirmed status',
82+
in: 'query',
83+
required: false,
84+
schema: new OA\Schema(
85+
type: 'string',
86+
enum: ['true', 'false', '0', '1'],
87+
example: '1'
88+
)
89+
),
90+
new OA\Parameter(
91+
name: 'is_blacklisted',
92+
description: 'Filter by blacklisted status',
93+
in: 'query',
94+
required: false,
95+
schema: new OA\Schema(
96+
type: 'string',
97+
enum: ['true', 'false', '0', '1'],
98+
example: '1'
99+
)
100+
),
101+
new OA\Parameter(
102+
name: 'sort_by',
103+
description: 'Column to sort by',
104+
in: 'query',
105+
required: false,
106+
schema: new OA\Schema(type: 'string', example: 'id')
107+
),
108+
new OA\Parameter(
109+
name: 'sort_direction',
110+
description: 'Sort direction',
111+
in: 'query',
112+
required: false,
113+
schema: new OA\Schema(
114+
type: 'string',
115+
enum: ['asc', 'desc'],
116+
example: 'desc'
117+
)
118+
),
119+
new OA\Parameter(
120+
name: 'find_column',
121+
description: 'Column to search in (requires find_value)',
122+
in: 'query',
123+
required: false,
124+
schema: new OA\Schema(type: 'string', example: 'email')
125+
),
126+
new OA\Parameter(
127+
name: 'find_value',
128+
description: 'Value to search for (requires find_column)',
129+
in: 'query',
130+
required: false,
131+
schema: new OA\Schema(type: 'string', example: 'email@example.com')
132+
),
79133
],
80134
responses: [
81135
new OA\Response(
@@ -104,14 +158,17 @@ public function getSubscribers(Request $request): JsonResponse
104158
{
105159
$this->requireAuthentication($request);
106160

161+
/** @var SubscribersFilterRequest $subscriberRequest */
162+
$subscriberRequest = $this->validator->validate($request, SubscribersFilterRequest::class);
163+
107164
return $this->json(
108-
$this->paginatedDataProvider->getPaginatedList(
109-
$request,
110-
$this->subscriberNormalizer,
111-
Subscriber::class,
112-
new SubscriberFilter(),
165+
data: $this->paginatedDataProvider->getPaginatedList(
166+
request: $request,
167+
normalizer: $this->subscriberNormalizer,
168+
className: Subscriber::class,
169+
filter: $subscriberRequest->getDto(),
113170
),
114-
Response::HTTP_OK
171+
status: Response::HTTP_OK
115172
);
116173
}
117174

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpList\RestBundle\Subscription\Request;
6+
7+
use PhpList\Core\Domain\Subscription\Model\Filter\SubscriberFilter;
8+
use PhpList\RestBundle\Common\Request\RequestInterface;
9+
use Symfony\Component\Validator\Constraints as Assert;
10+
11+
class SubscribersFilterRequest implements RequestInterface
12+
{
13+
#[Assert\Choice(choices: ['true', 'false', '1', '0'], message: 'Invalid boolean value')]
14+
public ?bool $isConfirmed = null;
15+
16+
#[Assert\Choice(choices: ['true', 'false', '1', '0'], message: 'Invalid boolean value')]
17+
public ?bool $isBlacklisted = null;
18+
19+
#[Assert\Type(type: 'string')]
20+
public ?string $sortBy = null;
21+
22+
#[Assert\Choice(choices: ['asc', 'desc'])]
23+
public ?string $sortDirection = null;
24+
25+
#[Assert\Type(type: 'string')]
26+
public ?string $findColumn = null;
27+
28+
#[Assert\Type(type: 'string')]
29+
public ?string $findValue = null;
30+
31+
public function getDto(): SubscriberFilter
32+
{
33+
$findColumn = $this->findColumn;
34+
$findValue = $this->findValue;
35+
if (($findColumn === null) xor ($findValue === null)) {
36+
$findColumn = null;
37+
$findValue = null;
38+
}
39+
40+
$isConfirmed = $this->isConfirmed !== null
41+
? filter_var($this->isConfirmed, FILTER_VALIDATE_BOOLEAN)
42+
: null;
43+
44+
$isBlacklisted = $this->isBlacklisted !== null
45+
? filter_var($this->isBlacklisted, FILTER_VALIDATE_BOOLEAN)
46+
: null;
47+
48+
return new SubscriberFilter(
49+
isConfirmed: $isConfirmed,
50+
isBlacklisted: $isBlacklisted,
51+
sortBy: $this->sortBy,
52+
sortDirection: $this->sortDirection,
53+
findColumn: $findColumn,
54+
findValue: $findValue,
55+
);
56+
}
57+
}

tests/Integration/Identity/Controller/PasswordResetControllerTest.php

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,11 @@ public function testControllerIsAvailableViaContainer(): void
1818
);
1919
}
2020

21-
public function testRequestPasswordResetWithNoJsonReturnsError400(): void
21+
public function testRequestPasswordResetWithNoJsonReturnsError422(): void
2222
{
2323
$this->jsonRequest('post', '/api/v2/password-reset/request');
2424

25-
$this->assertHttpBadRequest();
26-
$data = $this->getDecodedJsonResponseContent();
27-
$this->assertStringContainsString('Invalid JSON:', $data['message']);
25+
$this->assertHttpUnprocessableEntity();
2826
}
2927

3028
public function testRequestPasswordResetWithInvalidEmailReturnsError422(): void
@@ -55,13 +53,11 @@ public function testRequestPasswordResetWithValidEmailReturnsSuccess(): void
5553
$this->assertHttpNoContent();
5654
}
5755

58-
public function testValidateTokenWithNoJsonReturnsError400(): void
56+
public function testValidateTokenWithNoJsonReturnsError422(): void
5957
{
6058
$this->jsonRequest('post', '/api/v2/password-reset/validate');
6159

62-
$this->assertHttpBadRequest();
63-
$data = $this->getDecodedJsonResponseContent();
64-
$this->assertStringContainsString('Invalid JSON:', $data['message']);
60+
$this->assertHttpUnprocessableEntity();
6561
}
6662

6763
public function testValidateTokenWithInvalidTokenReturnsInvalidResult(): void
@@ -75,13 +71,11 @@ public function testValidateTokenWithInvalidTokenReturnsInvalidResult(): void
7571
$this->assertFalse($data['valid']);
7672
}
7773

78-
public function testResetPasswordWithNoJsonReturnsError400(): void
74+
public function testResetPasswordWithNoJsonReturnsError422(): void
7975
{
8076
$this->jsonRequest('post', '/api/v2/password-reset/reset');
8177

82-
$this->assertHttpBadRequest();
83-
$data = $this->getDecodedJsonResponseContent();
84-
$this->assertStringContainsString('Invalid JSON:', $data['message']);
78+
$this->assertHttpUnprocessableEntity();
8579
}
8680

8781
public function testResetPasswordWithInvalidTokenReturnsBadRequest(): void

tests/Integration/Identity/Controller/SessionControllerTest.php

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,11 @@ public function testGetSessionsIsNotAllowed()
4242
$this->assertHttpMethodNotAllowed();
4343
}
4444

45-
public function testPostSessionsWithNoJsonReturnsError400()
45+
public function testPostSessionsWithNoJsonReturnsError422()
4646
{
4747
$this->jsonRequest('post', '/api/v2/sessions');
4848

49-
$this->assertHttpBadRequest();
50-
$data = $this->getDecodedJsonResponseContent();
51-
$this->assertStringContainsString('Invalid JSON:', $data['message']);
49+
$this->assertHttpUnprocessableEntity();
5250
}
5351

5452
public function testPostSessionsWithInvalidJsonWithJsonContentTypeReturnsError400()

tests/Integration/Subscription/Controller/SubscriberControllerTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
* Testcase.
1616
*
1717
* @author Oliver Klee <oliver@phplist.com>
18+
* @author Tatevik Grigoryan <tatevik@phplist.com>
1819
*/
1920
class SubscriberControllerTest extends AbstractTestController
2021
{
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpList\RestBundle\Tests\Unit\Subscription\Request;
6+
7+
use PhpList\Core\Domain\Subscription\Model\Filter\SubscriberFilter;
8+
use PhpList\RestBundle\Subscription\Request\SubscribersFilterRequest;
9+
use PHPUnit\Framework\TestCase;
10+
11+
class SubscribersFilterRequestTest extends TestCase
12+
{
13+
public function testGetDtoReturnsCorrectDtoWithAllValues(): void
14+
{
15+
$request = new SubscribersFilterRequest();
16+
$request->isConfirmed = true;
17+
$request->isBlacklisted = false;
18+
$request->sortBy = 'email';
19+
$request->sortDirection = 'desc';
20+
$request->findColumn = 'email';
21+
$request->findValue = 'test@example.com';
22+
23+
$dto = $request->getDto();
24+
25+
$this->assertInstanceOf(SubscriberFilter::class, $dto);
26+
$this->assertTrue($dto->getIsConfirmed());
27+
$this->assertFalse($dto->getIsBlacklisted());
28+
$this->assertEquals('email', $dto->getSortBy());
29+
$this->assertEquals('desc', $dto->getSortDirection());
30+
$this->assertEquals('email', $dto->getFindColumn());
31+
$this->assertEquals('test@example.com', $dto->getFindValue());
32+
}
33+
34+
public function testGetDtoReturnsCorrectDtoWithNullValues(): void
35+
{
36+
$request = new SubscribersFilterRequest();
37+
38+
$dto = $request->getDto();
39+
40+
$this->assertInstanceOf(SubscriberFilter::class, $dto);
41+
$this->assertNull($dto->getIsConfirmed());
42+
$this->assertNull($dto->getIsBlacklisted());
43+
$this->assertNull($dto->getSortBy());
44+
$this->assertNull($dto->getSortDirection());
45+
$this->assertNull($dto->getFindColumn());
46+
$this->assertNull($dto->getFindValue());
47+
}
48+
49+
public function testGetDtoNulsFindColumnAndValueWhenOnlyColumnProvided(): void
50+
{
51+
$request = new SubscribersFilterRequest();
52+
$request->findColumn = 'email';
53+
$request->findValue = null;
54+
55+
$dto = $request->getDto();
56+
57+
$this->assertInstanceOf(SubscriberFilter::class, $dto);
58+
$this->assertNull($dto->getFindColumn());
59+
$this->assertNull($dto->getFindValue());
60+
}
61+
62+
public function testGetDtoNulsFindColumnAndValueWhenOnlyValueProvided(): void
63+
{
64+
$request = new SubscribersFilterRequest();
65+
$request->findColumn = null;
66+
$request->findValue = 'test@example.com';
67+
68+
$dto = $request->getDto();
69+
70+
$this->assertInstanceOf(SubscriberFilter::class, $dto);
71+
$this->assertNull($dto->getFindColumn());
72+
$this->assertNull($dto->getFindValue());
73+
}
74+
}

0 commit comments

Comments
 (0)