diff --git a/config/services/services.yml b/config/services/services.yml new file mode 100644 index 0000000..f087aa6 --- /dev/null +++ b/config/services/services.yml @@ -0,0 +1,8 @@ +services: + PhpList\RestBundle\Subscription\Service\SubscriberService: + autowire: true + autoconfigure: true + + PhpList\RestBundle\Subscription\Service\SubscriberHistoryService: + autowire: true + autoconfigure: true diff --git a/src/Common/Service/Provider/PaginatedDataProvider.php b/src/Common/Service/Provider/PaginatedDataProvider.php index c5bf3a3..92ac53d 100644 --- a/src/Common/Service/Provider/PaginatedDataProvider.php +++ b/src/Common/Service/Provider/PaginatedDataProvider.php @@ -37,7 +37,11 @@ public function getPaginatedList( throw new RuntimeException('Repository not found'); } - $items = $repository->getFilteredAfterId($pagination->afterId, $pagination->limit, $filter); + $items = $repository->getFilteredAfterId( + lastId: $pagination->afterId, + limit: $pagination->limit, + filter: $filter, + ); $total = $repository->count(); $normalizedItems = array_map( diff --git a/src/Subscription/Controller/SubscriberController.php b/src/Subscription/Controller/SubscriberController.php index 9d3aecb..d633d51 100644 --- a/src/Subscription/Controller/SubscriberController.php +++ b/src/Subscription/Controller/SubscriberController.php @@ -7,18 +7,16 @@ use OpenApi\Attributes as OA; use PhpList\Core\Domain\Identity\Model\PrivilegeFlag; use PhpList\Core\Domain\Subscription\Model\Subscriber; -use PhpList\Core\Domain\Subscription\Service\Manager\SubscriberManager; use PhpList\Core\Security\Authentication; use PhpList\RestBundle\Common\Controller\BaseController; use PhpList\RestBundle\Common\Validator\RequestValidator; use PhpList\RestBundle\Subscription\Request\CreateSubscriberRequest; use PhpList\RestBundle\Subscription\Request\UpdateSubscriberRequest; -use PhpList\RestBundle\Subscription\Serializer\SubscriberNormalizer; +use PhpList\RestBundle\Subscription\Service\SubscriberService; use Symfony\Bridge\Doctrine\Attribute\MapEntity; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\Routing\Attribute\Route; /** @@ -30,19 +28,16 @@ #[Route('/subscribers', name: 'subscriber_')] class SubscriberController extends BaseController { - private SubscriberManager $subscriberManager; - private SubscriberNormalizer $subscriberNormalizer; + private SubscriberService $subscriberService; public function __construct( Authentication $authentication, RequestValidator $validator, - SubscriberManager $subscriberManager, - SubscriberNormalizer $subscriberNormalizer, + SubscriberService $subscriberService, ) { parent::__construct($authentication, $validator); $this->authentication = $authentication; - $this->subscriberManager = $subscriberManager; - $this->subscriberNormalizer = $subscriberNormalizer; + $this->subscriberService = $subscriberService; } #[Route('', name: 'create', methods: ['POST'])] @@ -98,12 +93,9 @@ public function createSubscriber(Request $request): JsonResponse /** @var CreateSubscriberRequest $subscriberRequest */ $subscriberRequest = $this->validator->validate($request, CreateSubscriberRequest::class); - $subscriber = $this->subscriberManager->createSubscriber($subscriberRequest->getDto()); + $subscriberData = $this->subscriberService->createSubscriber($subscriberRequest); - return $this->json( - $this->subscriberNormalizer->normalize($subscriber, 'json'), - Response::HTTP_CREATED - ); + return $this->json($subscriberData, Response::HTTP_CREATED); } #[Route('/{subscriberId}', name: 'update', requirements: ['subscriberId' => '\d+'], methods: ['PUT'])] @@ -171,9 +163,9 @@ public function updateSubscriber( } /** @var UpdateSubscriberRequest $updateSubscriberRequest */ $updateSubscriberRequest = $this->validator->validate($request, UpdateSubscriberRequest::class); - $subscriber = $this->subscriberManager->updateSubscriber($updateSubscriberRequest->getDto()); + $subscriberData = $this->subscriberService->updateSubscriber($updateSubscriberRequest); - return $this->json($this->subscriberNormalizer->normalize($subscriber, 'json'), Response::HTTP_OK); + return $this->json($subscriberData, Response::HTTP_OK); } #[Route('/{subscriberId}', name: 'get_one', requirements: ['subscriberId' => '\d+'], methods: ['GET'])] @@ -221,11 +213,111 @@ public function getSubscriber(Request $request, int $subscriberId): JsonResponse { $this->requireAuthentication($request); - $subscriber = $this->subscriberManager->getSubscriber($subscriberId); + $subscriberData = $this->subscriberService->getSubscriber($subscriberId); - return $this->json($this->subscriberNormalizer->normalize($subscriber), Response::HTTP_OK); + return $this->json($subscriberData, Response::HTTP_OK); } + #[Route('/{subscriberId}/history', name: 'history', requirements: ['subscriberId' => '\d+'], methods: ['GET'])] + #[OA\Get( + path: '/api/v2/subscribers/{subscriberId}/history', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production. ', + summary: 'Get subscriber event history', + tags: ['subscribers'], + parameters: [ + new OA\Parameter( + name: 'php-auth-pw', + description: 'Session key obtained from login', + in: 'header', + required: true, + schema: new OA\Schema(type: 'string') + ), + new OA\Parameter( + name: 'subscriberId', + description: 'Subscriber ID', + in: 'path', + required: true, + schema: new OA\Schema(type: 'integer') + ), + new OA\Parameter( + name: 'after_id', + description: 'Page number (pagination)', + in: 'query', + required: false, + schema: new OA\Schema(type: 'integer', default: 1) + ), + new OA\Parameter( + name: 'limit', + description: 'Max items per page', + in: 'query', + required: false, + schema: new OA\Schema(type: 'integer', default: 25) + ), + new OA\Parameter( + name: 'ip', + description: 'Filter by IP address', + in: 'query', + required: false, + schema: new OA\Schema(type: 'string') + ), + new OA\Parameter( + name: 'date_from', + description: 'Filter by date (format: Y-m-d)', + in: 'query', + required: false, + schema: new OA\Schema(type: 'string', format: 'date') + ), + new OA\Parameter( + name: 'summery', + description: 'Filter by summary text', + in: 'query', + required: false, + schema: new OA\Schema(type: 'string') + ) + ], + responses: [ + new OA\Response( + response: 200, + description: 'Paginated list of subscriber events', + content: new OA\JsonContent( + properties: [ + new OA\Property( + property: 'items', + type: 'array', + items: new OA\Items(ref: '#/components/schemas/SubscriberHistory') + ), + new OA\Property(property: 'pagination', ref: '#/components/schemas/CursorPagination') + ], + type: 'object' + ) + ), + new OA\Response( + response: 403, + description: 'Unauthorized', + content: new OA\JsonContent(ref: '#/components/schemas/UnauthorizedResponse') + ), + new OA\Response( + response: 404, + description: 'Not Found', + content: new OA\JsonContent(ref: '#/components/schemas/NotFoundErrorResponse') + ) + ] + )] + public function getSubscriberHistory( + Request $request, + #[MapEntity(mapping: ['subscriberId' => 'id'])] ?Subscriber $subscriber = null, + ): JsonResponse { + $this->requireAuthentication($request); + + $historyData = $this->subscriberService->getSubscriberHistory($request, $subscriber); + + return $this->json( + data: $historyData, + status: Response::HTTP_OK, + ); + } + + #[Route('/{subscriberId}', name: 'delete', requirements: ['subscriberId' => '\d+'], methods: ['DELETE'])] #[OA\Delete( path: '/api/v2/subscribers/{subscriberId}', @@ -278,7 +370,7 @@ public function deleteSubscriber( if (!$subscriber) { throw $this->createNotFoundException('Subscriber not found.'); } - $this->subscriberManager->deleteSubscriber($subscriber); + $this->subscriberService->deleteSubscriber($subscriber); return $this->json(null, Response::HTTP_NO_CONTENT); } @@ -323,9 +415,9 @@ public function setSubscriberAsConfirmed(Request $request): Response return new Response('

Missing confirmation code.

', 400); } - try { - $this->subscriberManager->markAsConfirmedByUniqueId($uniqueId); - } catch (NotFoundHttpException) { + $subscriber = $this->subscriberService->confirmSubscriber($uniqueId); + + if (!$subscriber) { return new Response('

Subscriber isn\'t found or already confirmed.

', 404); } diff --git a/src/Subscription/OpenApi/SwaggerSchemasResponse.php b/src/Subscription/OpenApi/SwaggerSchemasResponse.php index edc5ea1..e2f4c38 100644 --- a/src/Subscription/OpenApi/SwaggerSchemasResponse.php +++ b/src/Subscription/OpenApi/SwaggerSchemasResponse.php @@ -105,6 +105,23 @@ new OA\Property(property: 'value', type: 'string', example: 'United States'), ], )] +#[OA\Schema( + schema: 'SubscriberHistory', + properties: [ + new OA\Property(property: 'id', type: 'integer', example: 1), + new OA\Property(property: 'ip', type: 'string', example: '127.0.0.1'), + new OA\Property( + property: 'created_at', + type: 'string', + format: 'date-time', + example: '2022-12-01T10:00:00Z' + ), + new OA\Property(property: 'summery', type: 'string', example: 'Added by admin'), + new OA\Property(property: 'detail', type: 'string', example: 'Added with add-email on test'), + new OA\Property(property: 'system_info', type: 'string', example: 'HTTP_USER_AGENT = Mozilla/5.0'), + ], + type: 'object' +)] class SwaggerSchemasResponse { } diff --git a/src/Subscription/Serializer/SubscriberHistoryNormalizer.php b/src/Subscription/Serializer/SubscriberHistoryNormalizer.php new file mode 100644 index 0000000..dc875c6 --- /dev/null +++ b/src/Subscription/Serializer/SubscriberHistoryNormalizer.php @@ -0,0 +1,38 @@ + $object->getId(), + 'ip' => $object->getIp(), + 'created_at' => $object->getCreatedAt()->format('Y-m-d\TH:i:sP'), + 'summery' => $object->getSummary(), + 'detail' => $object->getDetail(), + 'system_info' => $object->getSystemInfo(), + ]; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function supportsNormalization($data, string $format = null): bool + { + return $data instanceof SubscriberHistory; + } +} diff --git a/src/Subscription/Service/SubscriberHistoryService.php b/src/Subscription/Service/SubscriberHistoryService.php new file mode 100644 index 0000000..c37d179 --- /dev/null +++ b/src/Subscription/Service/SubscriberHistoryService.php @@ -0,0 +1,53 @@ +query->get('date_from'); + $dateFromFormated = $dateFrom ? new DateTimeImmutable($dateFrom) : null; + } catch (Exception $e) { + throw new ValidatorException('Invalid date format. Use format: Y-m-d'); + } + + $filter = new SubscriberHistoryFilter( + subscriber: $subscriber, + ip: $request->query->get('ip'), + dateFrom: $dateFromFormated, + summery: $request->query->get('summery'), + ); + + return $this->paginatedDataProvider->getPaginatedList( + request: $request, + normalizer: $this->serializer, + className: SubscriberHistory::class, + filter: $filter + ); + } +} diff --git a/src/Subscription/Service/SubscriberService.php b/src/Subscription/Service/SubscriberService.php new file mode 100644 index 0000000..4a8fa97 --- /dev/null +++ b/src/Subscription/Service/SubscriberService.php @@ -0,0 +1,64 @@ +subscriberManager->createSubscriber($subscriberRequest->getDto()); + return $this->subscriberNormalizer->normalize($subscriber, 'json'); + } + + public function updateSubscriber(UpdateSubscriberRequest $updateSubscriberRequest): array + { + $subscriber = $this->subscriberManager->updateSubscriber($updateSubscriberRequest->getDto()); + return $this->subscriberNormalizer->normalize($subscriber, 'json'); + } + + public function getSubscriber(int $subscriberId): array + { + $subscriber = $this->subscriberManager->getSubscriber($subscriberId); + return $this->subscriberNormalizer->normalize($subscriber); + } + + public function getSubscriberHistory(Request $request, ?Subscriber $subscriber): array + { + return $this->subscriberHistoryService->getSubscriberHistory($request, $subscriber); + } + + public function deleteSubscriber(Subscriber $subscriber): void + { + $this->subscriberManager->deleteSubscriber($subscriber); + } + + public function confirmSubscriber(string $uniqueId): ?Subscriber + { + if (!$uniqueId) { + return null; + } + + try { + return $this->subscriberManager->markAsConfirmedByUniqueId($uniqueId); + } catch (NotFoundHttpException) { + return null; + } + } +}