Skip to content
Open

Dev #165

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
5658321
Add forward email endpoint (#164)
TatevikGr Feb 9, 2026
1419d83
After review 0
tatevikg1 Feb 9, 2026
cd8139f
Add tests
tatevikg1 Feb 9, 2026
8b1e424
Core from dev branch
tatevikg1 Feb 9, 2026
1be4006
Feat: attachement endpoint (#166)
TatevikGr Feb 11, 2026
aacaf11
Feat: track message view (#167)
TatevikGr Feb 13, 2026
65d3e3f
Add: get subscribers endpoint
tatevikg1 Feb 26, 2026
18d6b17
Fix test
tatevikg1 Feb 27, 2026
f49ce4e
Update guzzle version to ^7.2 add filter
tatevikg1 Feb 27, 2026
e5667e8
Subscriber filter in get subscribers endpoint
tatevikg1 Mar 4, 2026
fe79ae1
PaginatedResult
tatevikg1 Mar 10, 2026
9907f4e
Remove sorting
tatevikg1 Mar 10, 2026
953c350
Add: get auth user endpoint
tatevikg1 Mar 12, 2026
b879fc6
Add: history to subscriber object
tatevikg1 Mar 13, 2026
595fa6f
Allow origin frontend
tatevikg1 Mar 17, 2026
8801ad9
Remove additionalData property from UpdateSubscriberRequest
tatevikg1 Mar 17, 2026
97d5bbd
Add dashboard_statistics
tatevikg1 Mar 19, 2026
ddaf6bd
Add more data to dashboard_statistics
tatevikg1 Mar 19, 2026
462128a
Add: PaginatedFilter
tatevikg1 Mar 23, 2026
2943771
getLists with SubscriberListFilter
tatevikg1 Mar 23, 2026
646326b
Add: update list endpoint
tatevikg1 Mar 23, 2026
ff05834
Add: owner check to subscription endpoints
tatevikg1 Mar 23, 2026
7f54de3
Fix: listId validator
tatevikg1 Mar 25, 2026
86c72fa
Fix: tests
tatevikg1 Mar 25, 2026
5eb4ac1
Add: fields to export
tatevikg1 Mar 25, 2026
3ffdae8
Export: confirmed, blacklisted filters
tatevikg1 Mar 30, 2026
1c4d23d
Import: Add autoConfirm property
tatevikg1 Mar 30, 2026
ba61d06
Add: send_start to response campaign
tatevikg1 Mar 30, 2026
c675bca
Add: as_text as_html
tatevikg1 Mar 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions .coderabbit.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,11 @@ reviews:
high_level_summary_in_walkthrough: false
changed_files_summary: false
poem: false
finishing_touches:
docstrings:
enabled: false
auto_review:
enabled: true
base_branches:
- ".*"
drafts: false

checks:
docstring_coverage:
enabled: false
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,11 @@ contribute and how to run the unit tests and style checks locally.
This project adheres to a [Contributor Code of Conduct](CODE_OF_CONDUCT.md).
By participating in this project and its community, you are expected to uphold
this code.


### Code style checks
```bash
vendor/bin/phpstan analyse -l 5 src/ tests/
vendor/bin/phpmd src/ text vendor/phplist/core/config/PHPMD/rules.xml
vendor/bin/phpcs --standard=vendor/phplist/core/config/PhpCodeSniffer/ src/ tests/
```
9 changes: 6 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,20 @@
},
"require": {
"php": "^8.1",
"phplist/core": "dev-main",
"phplist/core": "dev-dev",
"friendsofsymfony/rest-bundle": "*",
"symfony/test-pack": "^1.0",
"symfony/process": "^6.4",
"zircote/swagger-php": "^4.11",
"ext-dom": "*",
"tatevikgr/rss-feed": "dev-main as 0.1.0"
"tatevikgr/rss-feed": "dev-main as 0.1.0",
"psr/simple-cache": "^3.0",
"symfony/expression-language": "^6.4",
"nelmio/cors-bundle": "^2.4"
},
"require-dev": {
"phpunit/phpunit": "^10.0",
"guzzlehttp/guzzle": "^6.3.0",
"guzzlehttp/guzzle": "^7.2.0",
"squizlabs/php_codesniffer": "^3.2.0",
"phpstan/phpstan": "^1.10",
"nette/caching": "^3.0.0",
Expand Down
100 changes: 6 additions & 94 deletions config/services/normalizers.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,104 +4,16 @@ services:
autoconfigure: true
public: false

_instanceof:
Symfony\Component\Serializer\Normalizer\NormalizerInterface:
tags: [ 'serializer.normalizer' ]

Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter: ~

Symfony\Component\Serializer\Normalizer\ObjectNormalizer:
arguments:
$classMetadataFactory: '@?serializer.mapping.class_metadata_factory'
$nameConverter: '@Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter'

PhpList\RestBundle\Subscription\Serializer\SubscriberNormalizer:
tags: [ 'serializer.normalizer' ]
autowire: true

PhpList\RestBundle\Subscription\Serializer\SubscriberOnlyNormalizer:
tags: [ 'serializer.normalizer' ]
autowire: true

PhpList\RestBundle\Identity\Serializer\AdministratorTokenNormalizer:
tags: [ 'serializer.normalizer' ]
autowire: true

PhpList\RestBundle\Subscription\Serializer\SubscriberListNormalizer:
tags: [ 'serializer.normalizer' ]
autowire: true

PhpList\RestBundle\Subscription\Serializer\SubscriberHistoryNormalizer:
tags: [ 'serializer.normalizer' ]
autowire: true

PhpList\RestBundle\Subscription\Serializer\SubscriptionNormalizer:
tags: [ 'serializer.normalizer' ]
autowire: true

PhpList\RestBundle\Messaging\Serializer\MessageNormalizer:
tags: [ 'serializer.normalizer' ]
autowire: true

PhpList\RestBundle\Messaging\Serializer\TemplateImageNormalizer:
tags: [ 'serializer.normalizer' ]
autowire: true

PhpList\RestBundle\Messaging\Serializer\TemplateNormalizer:
tags: [ 'serializer.normalizer' ]
autowire: true

PhpList\RestBundle\Messaging\Serializer\ListMessageNormalizer:
tags: [ 'serializer.normalizer' ]
autowire: true

PhpList\RestBundle\Identity\Serializer\AdministratorNormalizer:
tags: [ 'serializer.normalizer' ]
autowire: true

PhpList\RestBundle\Identity\Serializer\AdminAttributeDefinitionNormalizer:
tags: [ 'serializer.normalizer' ]
autowire: true

PhpList\RestBundle\Identity\Serializer\AdminAttributeValueNormalizer:
tags: [ 'serializer.normalizer' ]
autowire: true

PhpList\RestBundle\Subscription\Serializer\AttributeDefinitionNormalizer:
tags: [ 'serializer.normalizer' ]
autowire: true

PhpList\RestBundle\Subscription\Serializer\SubscriberAttributeValueNormalizer:
tags: [ 'serializer.normalizer' ]
autowire: true

PhpList\RestBundle\Common\Serializer\CursorPaginationNormalizer:
autowire: true

PhpList\RestBundle\Subscription\Serializer\SubscribersExportRequestNormalizer:
tags: [ 'serializer.normalizer' ]
autowire: true

PhpList\RestBundle\Statistics\Serializer\CampaignStatisticsNormalizer:
tags: [ 'serializer.normalizer' ]
autowire: true

PhpList\RestBundle\Statistics\Serializer\ViewOpensStatisticsNormalizer:
tags: [ 'serializer.normalizer' ]
autowire: true

PhpList\RestBundle\Statistics\Serializer\TopDomainsNormalizer:
tags: [ 'serializer.normalizer' ]
autowire: true

PhpList\RestBundle\Statistics\Serializer\TopLocalPartsNormalizer:
tags: [ 'serializer.normalizer' ]
autowire: true

PhpList\RestBundle\Subscription\Serializer\UserBlacklistNormalizer:
tags: [ 'serializer.normalizer' ]
autowire: true

PhpList\RestBundle\Subscription\Serializer\SubscribePageNormalizer:
tags: [ 'serializer.normalizer' ]
autowire: true

PhpList\RestBundle\Messaging\Serializer\BounceRegexNormalizer:
tags: [ 'serializer.normalizer' ]
autowire: true
PhpList\RestBundle\:
resource: '../../src/*/Serializer/*'
15 changes: 13 additions & 2 deletions config/services/services.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
services:
PhpList\RestBundle\Subscription\Service\SubscriberService:
PhpList\RestBundle\Subscription\Service\SubscriberHistoryService:
autowire: true
autoconfigure: true

PhpList\RestBundle\Subscription\Service\SubscriberHistoryService:
PhpList\Core\Domain\Messaging\Service\ForwardingGuard:
autowire: true
autoconfigure: true
public: false

PhpList\Core\Domain\Messaging\Service\ForwardDeliveryService:
autowire: true
autoconfigure: true
public: false

PhpList\Core\Domain\Messaging\Service\ForwardContentService:
autowire: true
autoconfigure: true
public: false
10 changes: 10 additions & 0 deletions config/services/validators.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,13 @@ services:
autowire: true
autoconfigure: true

PhpList\RestBundle\Messaging\Validator\Constraint\MaxForwardCountValidator:
autowire: true
autoconfigure: true
tags: [ 'validator.constraint_validator' ]

PhpList\RestBundle\Messaging\Validator\Constraint\MaxPersonalNoteSizeValidator:
autowire: true
autoconfigure: true
tags: [ 'validator.constraint_validator' ]

21 changes: 18 additions & 3 deletions src/Common/Dto/CursorPaginationResult.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,24 @@
class CursorPaginationResult
{
public function __construct(
public readonly array $items,
public readonly int $limit,
public readonly int $total,
private readonly array $items,
private readonly int $limit,
private readonly int $total,
) {
}

public function getItems(): array
{
return $this->items;
}

public function getLimit(): int
{
return $this->limit;
}

public function getTotal(): int
{
return $this->total;
}
}
81 changes: 40 additions & 41 deletions src/Common/EventListener/ExceptionListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@

use Exception;
use PhpList\Core\Domain\Identity\Exception\AdminAttributeCreationException;
use PhpList\Core\Domain\Messaging\Exception\AttachmentFileNotFoundException;
use PhpList\Core\Domain\Messaging\Exception\MessageNotReceivedException;
use PhpList\Core\Domain\Messaging\Exception\SubscriberNotFoundException;
use PhpList\Core\Domain\Subscription\Exception\AttributeDefinitionCreationException;
use PhpList\Core\Domain\Subscription\Exception\SubscriptionCreationException;
use Symfony\Component\HttpFoundation\JsonResponse;
Expand All @@ -17,53 +20,49 @@

class ExceptionListener
{
private const EXCEPTION_STATUS_MAP = [
SubscriptionCreationException::class => null,
AttributeDefinitionCreationException::class => null,
AdminAttributeCreationException::class => null,
ValidatorException::class => 400,
AccessDeniedException::class => 403,
AccessDeniedHttpException::class => 403,
AttachmentFileNotFoundException::class => 404,
SubscriberNotFoundException::class => 404,
MessageNotReceivedException::class => 422,
];

public function onKernelException(ExceptionEvent $event): void
{
$exception = $event->getThrowable();

if ($exception instanceof AccessDeniedHttpException) {
$response = new JsonResponse([
'message' => $exception->getMessage(),
], 403);

$event->setResponse($response);
} elseif ($exception instanceof HttpExceptionInterface) {
$response = new JsonResponse([
'message' => $exception->getMessage(),
], $exception->getStatusCode());
foreach (self::EXCEPTION_STATUS_MAP as $class => $statusCode) {
if ($exception instanceof $class) {
$status = $statusCode ?? $exception->getStatusCode();
$event->setResponse(
new JsonResponse([
'message' => $exception->getMessage()
], $status)
);
return;
}
}

$event->setResponse($response);
} elseif ($exception instanceof SubscriptionCreationException) {
$response = new JsonResponse([
'message' => $exception->getMessage(),
], $exception->getStatusCode());
$event->setResponse($response);
} elseif ($exception instanceof AdminAttributeCreationException) {
$response = new JsonResponse([
'message' => $exception->getMessage(),
], $exception->getStatusCode());
$event->setResponse($response);
} elseif ($exception instanceof AttributeDefinitionCreationException) {
$response = new JsonResponse([
'message' => $exception->getMessage(),
], $exception->getStatusCode());
$event->setResponse($response);
} elseif ($exception instanceof ValidatorException) {
$response = new JsonResponse([
'message' => $exception->getMessage(),
], 400);
$event->setResponse($response);
} elseif ($exception instanceof AccessDeniedException) {
$response = new JsonResponse([
'message' => $exception->getMessage(),
], 403);
$event->setResponse($response);
} elseif ($exception instanceof Exception) {
$response = new JsonResponse([
'message' => $exception->getMessage(),
], 500);
if ($exception instanceof HttpExceptionInterface) {
$event->setResponse(
new JsonResponse([
'message' => $exception->getMessage()
], $exception->getStatusCode())
);
return;
}

$event->setResponse($response);
if ($exception instanceof Exception) {
$event->setResponse(
new JsonResponse([
'message' => $exception->getMessage()
], 500)
);
}
}
}
6 changes: 3 additions & 3 deletions src/Common/Serializer/CursorPaginationNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ class CursorPaginationNormalizer implements NormalizerInterface
*/
public function normalize($object, string $format = null, array $context = []): array
{
$items = $object->items;
$limit = $object->limit;
$total = $object->total;
$items = $object->getItems();
$limit = $object->getLimit();
$total = $object->getTotal();
$hasNext = !empty($items) && isset($items[array_key_last($items)]['id']);

return [
Expand Down
20 changes: 11 additions & 9 deletions src/Common/Service/Provider/PaginatedDataProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public function getPaginatedList(
Request $request,
NormalizerInterface $normalizer,
string $className,
FilterRequestInterface $filter = null
FilterRequestInterface $filter
): array {
$pagination = $this->paginationFactory->fromRequest($request);

Expand All @@ -37,20 +37,22 @@ public function getPaginatedList(
throw new RuntimeException('Repository not found');
}

$items = $repository->getFilteredAfterId(
lastId: $pagination->afterId,
limit: $pagination->limit,
filter: $filter,
);
$total = $repository->count();
$filter->setLimit($pagination->limit);
$filter->setLastId($pagination->afterId);

$result = $repository->getFilteredAfterId(filter: $filter);

$normalizedItems = array_map(
fn($item) => $normalizer->normalize($item, 'json'),
$items
$result->getItems()
);

return $this->paginationNormalizer->normalize(
new CursorPaginationResult($normalizedItems, $pagination->limit, $total)
new CursorPaginationResult(
items: $normalizedItems,
limit: $result->getLimit(),
total: $result->getTotal(),
)
);
}
}
Loading
Loading