From 68dd167449de88bddde937b0c24f1c6b369d19e1 Mon Sep 17 00:00:00 2001 From: Tatevik Date: Tue, 6 May 2025 21:45:05 +0400 Subject: [PATCH 01/15] ISSUE-345: move managers to core --- composer.json | 3 +- .../SubscriptionCreationException.php | 23 ++++ .../Dto}/Filter/FilterRequestInterface.php | 2 +- .../{ => Model/Dto}/Filter/MessageFilter.php | 2 +- .../Dto}/Filter/SubscriberFilter.php | 2 +- src/Domain/Model/Dto/MessageContext.php | 25 +++++ src/Domain/Model/Dto/ValidationContext.php | 27 +++++ .../Identity/Dto/CreateAdministratorDto.php | 15 +++ .../Identity/Dto/UpdateAdministratorDto.php | 16 +++ .../Model/Messaging/Dto/CreateMessageDto.php | 31 ++++++ .../Model/Messaging/Dto/CreateTemplateDto.php | 18 +++ .../Dto/Message/MessageContentDto.php | 15 +++ .../Dto/Message/MessageFormatDto.php | 14 +++ .../Dto/Message/MessageMetadataDto.php | 12 ++ .../Dto/Message/MessageOptionsDto.php | 15 +++ .../Dto/Message/MessageScheduleDto.php | 16 +++ .../Messaging/Dto/MessageDtoInterface.php | 24 ++++ .../Model/Messaging/Dto/UpdateMessageDto.php | 31 ++++++ .../Messaging/Dto/UpdateSubscriberDto.php | 18 +++ .../Subscription/Dto/CreateSubscriberDto.php | 14 +++ .../Dto/CreateSubscriberListDto.php | 15 +++ .../Subscription/Dto/UpdateSubscriberDto.php | 18 +++ .../Repository/CursorPaginationTrait.php | 2 +- .../PaginatableRepositoryInterface.php | 2 +- .../Messaging/MessageRepository.php | 4 +- .../Subscription/SubscriberRepository.php | 4 +- src/Domain/Service/Builder/MessageBuilder.php | 52 +++++++++ .../Service/Builder/MessageContentBuilder.php | 26 +++++ .../Service/Builder/MessageFormatBuilder.php | 25 +++++ .../Service/Builder/MessageOptionsBuilder.php | 27 +++++ .../Builder/MessageScheduleBuilder.php | 28 +++++ .../Service/Manager/AdministratorManager.php | 63 +++++++++++ src/Domain/Service/Manager/MessageManager.php | 57 ++++++++++ src/Domain/Service/Manager/SessionManager.php | 45 ++++++++ .../Service/Manager/SubscriberListManager.php | 54 +++++++++ .../Service/Manager/SubscriberManager.php | 72 ++++++++++++ .../Service/Manager/SubscriptionManager.php | 90 +++++++++++++++ .../Service/Manager/TemplateImageManager.php | 104 ++++++++++++++++++ .../Service/Manager/TemplateManager.php | 87 +++++++++++++++ .../Validator/TemplateImageValidator.php | 72 ++++++++++++ .../Validator/TemplateLinkValidator.php | 62 +++++++++++ .../Service/Validator/ValidatorInterface.php | 12 ++ .../Repository/CursorPaginationTraitTest.php | 2 +- 43 files changed, 1235 insertions(+), 11 deletions(-) create mode 100644 src/Domain/Exception/SubscriptionCreationException.php rename src/Domain/{ => Model/Dto}/Filter/FilterRequestInterface.php (60%) rename src/Domain/{ => Model/Dto}/Filter/MessageFilter.php (89%) rename src/Domain/{ => Model/Dto}/Filter/SubscriberFilter.php (87%) create mode 100644 src/Domain/Model/Dto/MessageContext.php create mode 100644 src/Domain/Model/Dto/ValidationContext.php create mode 100644 src/Domain/Model/Identity/Dto/CreateAdministratorDto.php create mode 100644 src/Domain/Model/Identity/Dto/UpdateAdministratorDto.php create mode 100644 src/Domain/Model/Messaging/Dto/CreateMessageDto.php create mode 100644 src/Domain/Model/Messaging/Dto/CreateTemplateDto.php create mode 100644 src/Domain/Model/Messaging/Dto/Message/MessageContentDto.php create mode 100644 src/Domain/Model/Messaging/Dto/Message/MessageFormatDto.php create mode 100644 src/Domain/Model/Messaging/Dto/Message/MessageMetadataDto.php create mode 100644 src/Domain/Model/Messaging/Dto/Message/MessageOptionsDto.php create mode 100644 src/Domain/Model/Messaging/Dto/Message/MessageScheduleDto.php create mode 100644 src/Domain/Model/Messaging/Dto/MessageDtoInterface.php create mode 100644 src/Domain/Model/Messaging/Dto/UpdateMessageDto.php create mode 100644 src/Domain/Model/Messaging/Dto/UpdateSubscriberDto.php create mode 100644 src/Domain/Model/Subscription/Dto/CreateSubscriberDto.php create mode 100644 src/Domain/Model/Subscription/Dto/CreateSubscriberListDto.php create mode 100644 src/Domain/Model/Subscription/Dto/UpdateSubscriberDto.php create mode 100644 src/Domain/Service/Builder/MessageBuilder.php create mode 100644 src/Domain/Service/Builder/MessageContentBuilder.php create mode 100644 src/Domain/Service/Builder/MessageFormatBuilder.php create mode 100644 src/Domain/Service/Builder/MessageOptionsBuilder.php create mode 100644 src/Domain/Service/Builder/MessageScheduleBuilder.php create mode 100644 src/Domain/Service/Manager/AdministratorManager.php create mode 100644 src/Domain/Service/Manager/MessageManager.php create mode 100644 src/Domain/Service/Manager/SessionManager.php create mode 100644 src/Domain/Service/Manager/SubscriberListManager.php create mode 100644 src/Domain/Service/Manager/SubscriberManager.php create mode 100644 src/Domain/Service/Manager/SubscriptionManager.php create mode 100644 src/Domain/Service/Manager/TemplateImageManager.php create mode 100644 src/Domain/Service/Manager/TemplateManager.php create mode 100644 src/Domain/Service/Validator/TemplateImageValidator.php create mode 100644 src/Domain/Service/Validator/TemplateLinkValidator.php create mode 100644 src/Domain/Service/Validator/ValidatorInterface.php diff --git a/composer.json b/composer.json index 285f0d3e..ebb65d90 100644 --- a/composer.json +++ b/composer.json @@ -56,7 +56,8 @@ "symfony/validator": "^6.4", "doctrine/doctrine-fixtures-bundle": "^3.7", "doctrine/instantiator": "^2.0", - "masterminds/html5": "^2.9" + "masterminds/html5": "^2.9", + "ext-dom": "*" }, "require-dev": { "phpunit/phpunit": "^9.5", diff --git a/src/Domain/Exception/SubscriptionCreationException.php b/src/Domain/Exception/SubscriptionCreationException.php new file mode 100644 index 00000000..87cf1a8c --- /dev/null +++ b/src/Domain/Exception/SubscriptionCreationException.php @@ -0,0 +1,23 @@ +statusCode = $statusCode; + } + + public function getStatusCode(): int + { + return $this->statusCode; + } +} diff --git a/src/Domain/Filter/FilterRequestInterface.php b/src/Domain/Model/Dto/Filter/FilterRequestInterface.php similarity index 60% rename from src/Domain/Filter/FilterRequestInterface.php rename to src/Domain/Model/Dto/Filter/FilterRequestInterface.php index cfe3710a..eac3dff3 100644 --- a/src/Domain/Filter/FilterRequestInterface.php +++ b/src/Domain/Model/Dto/Filter/FilterRequestInterface.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace PhpList\Core\Domain\Filter; +namespace PhpList\Core\Domain\Model\Dto\Filter; interface FilterRequestInterface { diff --git a/src/Domain/Filter/MessageFilter.php b/src/Domain/Model/Dto/Filter/MessageFilter.php similarity index 89% rename from src/Domain/Filter/MessageFilter.php rename to src/Domain/Model/Dto/Filter/MessageFilter.php index a1ec4736..cda34763 100644 --- a/src/Domain/Filter/MessageFilter.php +++ b/src/Domain/Model/Dto/Filter/MessageFilter.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace PhpList\Core\Domain\Filter; +namespace PhpList\Core\Domain\Model\Dto\Filter; use PhpList\Core\Domain\Model\Identity\Administrator; diff --git a/src/Domain/Filter/SubscriberFilter.php b/src/Domain/Model/Dto/Filter/SubscriberFilter.php similarity index 87% rename from src/Domain/Filter/SubscriberFilter.php rename to src/Domain/Model/Dto/Filter/SubscriberFilter.php index 7fae293b..475d0d97 100644 --- a/src/Domain/Filter/SubscriberFilter.php +++ b/src/Domain/Model/Dto/Filter/SubscriberFilter.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace PhpList\Core\Domain\Filter; +namespace PhpList\Core\Domain\Model\Dto\Filter; class SubscriberFilter implements FilterRequestInterface { diff --git a/src/Domain/Model/Dto/MessageContext.php b/src/Domain/Model/Dto/MessageContext.php new file mode 100644 index 00000000..3003d400 --- /dev/null +++ b/src/Domain/Model/Dto/MessageContext.php @@ -0,0 +1,25 @@ +user; + } + + public function getExisting(): ?Message + { + return $this->existing; + } +} diff --git a/src/Domain/Model/Dto/ValidationContext.php b/src/Domain/Model/Dto/ValidationContext.php new file mode 100644 index 00000000..29317475 --- /dev/null +++ b/src/Domain/Model/Dto/ValidationContext.php @@ -0,0 +1,27 @@ +options[$key] = $value; + + return $this; + } + + public function get(string $key, mixed $default = null): mixed + { + return $this->options[$key] ?? $default; + } + + public function has(string $key): bool + { + return array_key_exists($key, $this->options); + } +} diff --git a/src/Domain/Model/Identity/Dto/CreateAdministratorDto.php b/src/Domain/Model/Identity/Dto/CreateAdministratorDto.php new file mode 100644 index 00000000..e7b7a4b9 --- /dev/null +++ b/src/Domain/Model/Identity/Dto/CreateAdministratorDto.php @@ -0,0 +1,15 @@ +content; } + public function getFormat(): MessageFormatDto { return $this->format; } + public function getMetadata(): MessageMetadataDto { return $this->metadata; } + public function getOptions(): MessageOptionsDto { return $this->options; } + public function getSchedule(): MessageScheduleDto { return $this->schedule; } + public function getTemplateId(): ?int { return $this->templateId; } +} diff --git a/src/Domain/Model/Messaging/Dto/CreateTemplateDto.php b/src/Domain/Model/Messaging/Dto/CreateTemplateDto.php new file mode 100644 index 00000000..7ca0412b --- /dev/null +++ b/src/Domain/Model/Messaging/Dto/CreateTemplateDto.php @@ -0,0 +1,18 @@ +content; } + public function getFormat(): MessageFormatDto { return $this->format; } + public function getMetadata(): MessageMetadataDto { return $this->metadata; } + public function getOptions(): MessageOptionsDto { return $this->options; } + public function getSchedule(): MessageScheduleDto { return $this->schedule; } + public function getTemplateId(): ?int { return $this->templateId; } +} diff --git a/src/Domain/Model/Messaging/Dto/UpdateSubscriberDto.php b/src/Domain/Model/Messaging/Dto/UpdateSubscriberDto.php new file mode 100644 index 00000000..de8f3a8d --- /dev/null +++ b/src/Domain/Model/Messaging/Dto/UpdateSubscriberDto.php @@ -0,0 +1,18 @@ +messageFormatBuilder->build($createMessageDto->getFormat()); + $schedule = $this->messageScheduleBuilder->build($createMessageDto->getSchedule()); + $content = $this->messageContentBuilder->build($createMessageDto->getContent()); + $options = $this->messageOptionsBuilder->build($createMessageDto->getOptions()); + $template = null; + if (isset($createMessageDto->templateId)) { + $template = $this->templateRepository->find($createMessageDto->templateId); + } + + if ($context->getExisting()) { + $context->getExisting()->setFormat($format); + $context->getExisting()->setSchedule($schedule); + $context->getExisting()->setContent($content); + $context->getExisting()->setOptions($options); + $context->getExisting()->setTemplate($template); + return $context->getExisting(); + } + + $metadata = new Message\MessageMetadata($createMessageDto->getMetadata()->status); + + return new Message($format, $schedule, $metadata, $content, $options, $context->getOwner(), $template); + } +} diff --git a/src/Domain/Service/Builder/MessageContentBuilder.php b/src/Domain/Service/Builder/MessageContentBuilder.php new file mode 100644 index 00000000..e95eecdd --- /dev/null +++ b/src/Domain/Service/Builder/MessageContentBuilder.php @@ -0,0 +1,26 @@ +subject, + $dto->text, + $dto->textMessage, + $dto->footer + ); + } +} diff --git a/src/Domain/Service/Builder/MessageFormatBuilder.php b/src/Domain/Service/Builder/MessageFormatBuilder.php new file mode 100644 index 00000000..5352d30e --- /dev/null +++ b/src/Domain/Service/Builder/MessageFormatBuilder.php @@ -0,0 +1,25 @@ +htmlFormated, + $dto->sendFormat, + $dto->formatOptions + ); + } +} diff --git a/src/Domain/Service/Builder/MessageOptionsBuilder.php b/src/Domain/Service/Builder/MessageOptionsBuilder.php new file mode 100644 index 00000000..eed308a4 --- /dev/null +++ b/src/Domain/Service/Builder/MessageOptionsBuilder.php @@ -0,0 +1,27 @@ +fromField ?? '', + $dto->toField ?? '', + $dto->replyTo ?? '', + $dto->userSelection, + null, + ); + } +} diff --git a/src/Domain/Service/Builder/MessageScheduleBuilder.php b/src/Domain/Service/Builder/MessageScheduleBuilder.php new file mode 100644 index 00000000..464054e5 --- /dev/null +++ b/src/Domain/Service/Builder/MessageScheduleBuilder.php @@ -0,0 +1,28 @@ +repeatInterval, + new DateTime($dto->repeatUntil), + $dto->requeueInterval, + new DateTime($dto->requeueUntil), + new DateTime($dto->embargo) + ); + } +} diff --git a/src/Domain/Service/Manager/AdministratorManager.php b/src/Domain/Service/Manager/AdministratorManager.php new file mode 100644 index 00000000..b340c586 --- /dev/null +++ b/src/Domain/Service/Manager/AdministratorManager.php @@ -0,0 +1,63 @@ +entityManager = $entityManager; + $this->hashGenerator = $hashGenerator; + } + + public function createAdministrator(CreateAdministratorDto $dto): Administrator + { + $administrator = new Administrator(); + $administrator->setLoginName($dto->loginName); + $administrator->setEmail($dto->email); + $administrator->setSuperUser($dto->superUser); + $hashedPassword = $this->hashGenerator->createPasswordHash($dto->password); + $administrator->setPasswordHash($hashedPassword); + + $this->entityManager->persist($administrator); + $this->entityManager->flush(); + + return $administrator; + } + + public function updateAdministrator(Administrator $administrator, UpdateAdministratorDto $dto): void + { + if ($dto->loginName !== null) { + $administrator->setLoginName($dto->loginName); + } + if ($dto->email !== null) { + $administrator->setEmail($dto->email); + } + if ($dto->superAdmin !== null) { + $administrator->setSuperUser($dto->superAdmin); + } + if ($dto->password !== null) { + $hashedPassword = $this->hashGenerator->createPasswordHash($dto->password); + $administrator->setPasswordHash($hashedPassword); + } + + $this->entityManager->flush(); + } + + public function deleteAdministrator(Administrator $administrator): void + { + $this->entityManager->remove($administrator); + $this->entityManager->flush(); + } +} diff --git a/src/Domain/Service/Manager/MessageManager.php b/src/Domain/Service/Manager/MessageManager.php new file mode 100644 index 00000000..e612e192 --- /dev/null +++ b/src/Domain/Service/Manager/MessageManager.php @@ -0,0 +1,57 @@ +messageRepository = $messageRepository; + $this->messageBuilder = $messageBuilder; + } + + public function createMessage(CreateMessageDto $createMessageDto, Administrator $authUser): Message + { + $context = new MessageContext($authUser); + $message = $this->messageBuilder->build($createMessageDto, $context); + $this->messageRepository->save($message); + + return $message; + } + + public function updateMessage( + UpdateMessageDto $updateMessageDto, + Message $message, + Administrator $authUser + ): Message { + $context = new MessageContext($authUser, $message); + $message = $this->messageBuilder->build($updateMessageDto, $context); + $this->messageRepository->save($message); + + return $message; + } + + public function delete(Message $message): void + { + $this->messageRepository->remove($message); + } + + /** @return Message[] */ + public function getMessagesByOwner(Administrator $owner): array + { + return $this->messageRepository->getByOwnerId($owner->getId()); + } +} diff --git a/src/Domain/Service/Manager/SessionManager.php b/src/Domain/Service/Manager/SessionManager.php new file mode 100644 index 00000000..71ee7b92 --- /dev/null +++ b/src/Domain/Service/Manager/SessionManager.php @@ -0,0 +1,45 @@ +tokenRepository = $tokenRepository; + $this->administratorRepository = $administratorRepository; + } + + public function createSession(string $loginName, string $password): AdministratorToken + { + $administrator = $this->administratorRepository->findOneByLoginCredentials($loginName, $password); + if ($administrator === null) { + throw new UnauthorizedHttpException('', 'Not authorized', null, 1500567098); + } + + $token = new AdministratorToken(); + $token->setAdministrator($administrator); + $token->generateExpiry(); + $token->generateKey(); + $this->tokenRepository->save($token); + + return $token; + } + + public function deleteSession(AdministratorToken $token): void + { + $this->tokenRepository->remove($token); + } +} diff --git a/src/Domain/Service/Manager/SubscriberListManager.php b/src/Domain/Service/Manager/SubscriberListManager.php new file mode 100644 index 00000000..73cb23ba --- /dev/null +++ b/src/Domain/Service/Manager/SubscriberListManager.php @@ -0,0 +1,54 @@ +subscriberListRepository = $subscriberListRepository; + } + + public function createSubscriberList( + CreateSubscriberListDto $subscriberListDto, + Administrator $authUser + ): SubscriberList { + $subscriberList = (new SubscriberList()) + ->setName($subscriberListDto->name) + ->setOwner($authUser) + ->setDescription($subscriberListDto->description) + ->setListPosition($subscriberListDto->listPosition) + ->setPublic($subscriberListDto->public); + + $this->subscriberListRepository->save($subscriberList); + + return $subscriberList; + } + + /** + * @return SubscriberList[] + */ + public function getPaginated(int $afterId, int $limit): array + { + return $this->subscriberListRepository->getAfterId($afterId, $limit); + } + + public function getTotalCount(): int + { + return $this->subscriberListRepository->count(); + } + + public function delete(SubscriberList $subscriberList): void + { + $this->subscriberListRepository->remove($subscriberList); + } +} diff --git a/src/Domain/Service/Manager/SubscriberManager.php b/src/Domain/Service/Manager/SubscriberManager.php new file mode 100644 index 00000000..dced9518 --- /dev/null +++ b/src/Domain/Service/Manager/SubscriberManager.php @@ -0,0 +1,72 @@ +subscriberRepository = $subscriberRepository; + $this->entityManager = $entityManager; + } + + public function createSubscriber(CreateSubscriberDto $subscriberDto): Subscriber + { + $subscriber = new Subscriber(); + $subscriber->setEmail($subscriberDto->email); + $confirmed = (bool)$subscriberDto->requestConfirmation; + $subscriber->setConfirmed(!$confirmed); + $subscriber->setBlacklisted(false); + $subscriber->setHtmlEmail((bool)$subscriberDto->htmlEmail); + $subscriber->setDisabled(false); + + $this->subscriberRepository->save($subscriber); + + return $subscriber; + } + + public function getSubscriber(int $subscriberId): Subscriber + { + $subscriber = $this->subscriberRepository->findSubscriberWithSubscriptions($subscriberId); + + if (!$subscriber) { + throw new NotFoundHttpException('Subscriber not found'); + } + + return $subscriber; + } + + public function updateSubscriber(UpdateSubscriberDto $subscriberDto): Subscriber + { + /** @var Subscriber $subscriber */ + $subscriber = $this->subscriberRepository->find($subscriberDto->subscriberId); + + $subscriber->setEmail($subscriberDto->email); + $subscriber->setConfirmed($subscriberDto->confirmed); + $subscriber->setBlacklisted($subscriberDto->blacklisted); + $subscriber->setHtmlEmail($subscriberDto->htmlEmail); + $subscriber->setDisabled($subscriberDto->disabled); + $subscriber->setExtraData($subscriberDto->additionalData); + + $this->entityManager->flush(); + + return $subscriber; + } + + public function deleteSubscriber(Subscriber $subscriber): void + { + $this->subscriberRepository->remove($subscriber); + } +} diff --git a/src/Domain/Service/Manager/SubscriptionManager.php b/src/Domain/Service/Manager/SubscriptionManager.php new file mode 100644 index 00000000..a46caeb6 --- /dev/null +++ b/src/Domain/Service/Manager/SubscriptionManager.php @@ -0,0 +1,90 @@ +subscriptionRepository = $subscriptionRepository; + $this->subscriberRepository = $subscriberRepository; + } + + /** @return Subscription[] */ + public function createSubscriptions(SubscriberList $subscriberList, array $emails): array + { + $subscriptions = []; + foreach ($emails as $email) { + $subscriptions[] = $this->createSubscription($subscriberList, $email); + } + + return $subscriptions; + } + + private function createSubscription(SubscriberList $subscriberList, string $email): Subscription + { + $subscriber = $this->subscriberRepository->findOneBy(['email' => $email]); + if (!$subscriber) { + throw new SubscriptionCreationException('Subscriber does not exists.', 404); + } + + $existingSubscription = $this->subscriptionRepository + ->findOneBySubscriberListAndSubscriber($subscriberList, $subscriber); + if ($existingSubscription) { + return $existingSubscription; + } + + $subscription = new Subscription(); + $subscription->setSubscriber($subscriber); + $subscription->setSubscriberList($subscriberList); + + $this->subscriptionRepository->save($subscription); + + return $subscription; + } + + public function deleteSubscriptions(SubscriberList $subscriberList, array $emails): void + { + foreach ($emails as $email) { + try { + $this->deleteSubscription($subscriberList, $email); + } catch (SubscriptionCreationException $e) { + if ($e->getStatusCode() !== 404) { + throw $e; + } + } + } + } + + private function deleteSubscription(SubscriberList $subscriberList, string $email): void + { + $subscription = $this->subscriptionRepository + ->findOneBySubscriberEmailAndListId($subscriberList->getId(), $email); + + if (!$subscription) { + throw new SubscriptionCreationException('Subscription not found for this subscriber and list.', 404); + } + + $this->subscriptionRepository->remove($subscription); + } + + /** @return Subscriber[] */ + public function getSubscriberListMembers(SubscriberList $list): array + { + return $this->subscriberRepository->getSubscribersBySubscribedListId($list->getId()); + } +} diff --git a/src/Domain/Service/Manager/TemplateImageManager.php b/src/Domain/Service/Manager/TemplateImageManager.php new file mode 100644 index 00000000..99d88904 --- /dev/null +++ b/src/Domain/Service/Manager/TemplateImageManager.php @@ -0,0 +1,104 @@ + 'image/gif', + 'jpg' => 'image/jpeg', + 'jpeg' => 'image/jpeg', + 'jpe' => 'image/jpeg', + 'bmp' => 'image/bmp', + 'png' => 'image/png', + 'tif' => 'image/tiff', + 'tiff' => 'image/tiff', + 'swf' => 'application/x-shockwave-flash', + ]; + + private TemplateImageRepository $templateImageRepository; + private EntityManagerInterface $entityManager; + + public function __construct( + TemplateImageRepository $templateImageRepository, + EntityManagerInterface $entityManager + ) { + $this->templateImageRepository = $templateImageRepository; + $this->entityManager = $entityManager; + } + + /** @return TemplateImage[] */ + public function createImagesFromImagePaths(array $imagePaths, Template $template): array + { + $templateImages = []; + foreach ($imagePaths as $path) { + $image = new TemplateImage(); + $image->setTemplate($template); + $image->setFilename($path); + $image->setMimeType($this->guessMimeType($path)); + $image->setData(null); + + $this->entityManager->persist($image); + $templateImages[] = $image; + } + + $this->entityManager->flush(); + + return $templateImages; + } + + private function guessMimeType(string $filename): string + { + $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); + return self::IMAGE_MIME_TYPES[$ext] ?? 'application/octet-stream'; + } + + public function extractAllImages(string $html): array + { + $fromRegex = array_keys( + $this->extractTemplateImagesFromContent($html) + ); + + $fromDom = $this->extractImagesFromHtml($html); + + return array_values(array_unique(array_merge($fromRegex, $fromDom))); + } + + private function extractTemplateImagesFromContent(string $content): array + { + $regexp = sprintf('/"([^"]+\.(%s))"/Ui', implode('|', array_keys(self::IMAGE_MIME_TYPES))); + preg_match_all($regexp, stripslashes($content), $images); + + return array_count_values($images[1]); + } + + private function extractImagesFromHtml(string $html): array + { + $dom = new DOMDocument(); + // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged + @$dom->loadHTML($html); + $images = []; + + foreach ($dom->getElementsByTagName('img') as $img) { + $src = $img->getAttribute('src'); + if ($src) { + $images[] = $src; + } + } + + return $images; + } + + public function delete(TemplateImage $templateImage): void + { + $this->templateImageRepository->remove($templateImage); + } +} diff --git a/src/Domain/Service/Manager/TemplateManager.php b/src/Domain/Service/Manager/TemplateManager.php new file mode 100644 index 00000000..c38d25bf --- /dev/null +++ b/src/Domain/Service/Manager/TemplateManager.php @@ -0,0 +1,87 @@ +templateRepository = $templateRepository; + $this->entityManager = $entityManager; + $this->templateImageManager = $templateImageManager; + $this->templateLinkValidator = $templateLinkValidator; + $this->templateImageValidator = $templateImageValidator; + } + + public function create(CreateTemplateDto $createTemplateDto): Template + { + $template = (new Template($createTemplateDto->title)) + ->setContent($createTemplateDto->content) + ->setText($createTemplateDto->text); + + if ($createTemplateDto->fileContent) { + $template->setContent($createTemplateDto->fileContent); + } + + $context = (new ValidationContext()) + ->set('checkLinks', $createTemplateDto->checkLinks) + ->set('checkImages', $createTemplateDto->checkImages) + ->set('checkExternalImages', $createTemplateDto->checkExternalImages); + + $this->templateLinkValidator->validate($template->getContent() ?? '', $context); + + $imageUrls = $this->templateImageManager->extractAllImages($template->getContent() ?? ''); + $this->templateImageValidator->validate($imageUrls, $context); + + $this->templateRepository->save($template); + + $this->templateImageManager->createImagesFromImagePaths($imageUrls, $template); + + return $template; + } + + public function update(UpdateSubscriberDto $updateSubscriberDto): Subscriber + { + /** @var Subscriber $subscriber */ + $subscriber = $this->templateRepository->find($updateSubscriberDto->subscriberId); + + $subscriber->setEmail($updateSubscriberDto->email); + $subscriber->setConfirmed($updateSubscriberDto->confirmed); + $subscriber->setBlacklisted($updateSubscriberDto->blacklisted); + $subscriber->setHtmlEmail($updateSubscriberDto->htmlEmail); + $subscriber->setDisabled($updateSubscriberDto->disabled); + $subscriber->setExtraData($updateSubscriberDto->additionalData); + + $this->entityManager->flush(); + + return $subscriber; + } + + public function delete(Template $template): void + { + $this->templateRepository->remove($template); + } +} diff --git a/src/Domain/Service/Validator/TemplateImageValidator.php b/src/Domain/Service/Validator/TemplateImageValidator.php new file mode 100644 index 00000000..fffca408 --- /dev/null +++ b/src/Domain/Service/Validator/TemplateImageValidator.php @@ -0,0 +1,72 @@ +get('checkImages', false); + $checkExist = $context?->get('checkExternalImages', false); + + $errors = array_merge( + $checkFull ? $this->validateFullUrls($value) : [], + $checkExist ? $this->validateExistence($value) : [] + ); + + if (!empty($errors)) { + throw new ValidatorException(implode("\n", $errors)); + } + } + + private function validateFullUrls(array $urls): array + { + $errors = []; + + foreach ($urls as $url) { + if (!preg_match('#^https?://#i', $url)) { + $errors[] = sprintf('Image "%s" is not a full URL.', $url); + } + } + + return $errors; + } + + private function validateExistence(array $urls): array + { + $errors = []; + + foreach ($urls as $url) { + if (!preg_match('#^https?://#i', $url)) { + continue; + } + + try { + $response = $this->httpClient->request('HEAD', $url); + if ($response->getStatusCode() !== 200) { + $errors[] = sprintf('Image "%s" does not exist (HTTP %s)', $url, $response->getStatusCode()); + } + } catch (Throwable $e) { + $errors[] = sprintf('Image "%s" could not be validated: %s', $url, $e->getMessage()); + } + } + + return $errors; + } +} diff --git a/src/Domain/Service/Validator/TemplateLinkValidator.php b/src/Domain/Service/Validator/TemplateLinkValidator.php new file mode 100644 index 00000000..e4d37a81 --- /dev/null +++ b/src/Domain/Service/Validator/TemplateLinkValidator.php @@ -0,0 +1,62 @@ +get('checkLinks', false)) { + return; + } + $links = $this->extractLinks($value); + $invalid = []; + + foreach ($links as $link) { + if (!preg_match('#^https?://#i', $link) && + !preg_match('#^mailto:#i', $link) && + !in_array(strtoupper($link), self::PLACEHOLDERS, true) + ) { + $invalid[] = $link; + } + } + + if (!empty($invalid)) { + throw new ValidatorException(sprintf( + 'Not full URLs: %s', + implode(', ', $invalid) + )); + } + } + + private function extractLinks(string $html): array + { + $dom = new DOMDocument(); + // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged + @$dom->loadHTML($html); + $links = []; + + foreach ($dom->getElementsByTagName('a') as $node) { + $href = $node->getAttribute('href'); + if ($href) { + $links[] = $href; + } + } + + return $links; + } +} diff --git a/src/Domain/Service/Validator/ValidatorInterface.php b/src/Domain/Service/Validator/ValidatorInterface.php new file mode 100644 index 00000000..a287059f --- /dev/null +++ b/src/Domain/Service/Validator/ValidatorInterface.php @@ -0,0 +1,12 @@ + Date: Wed, 7 May 2025 20:45:30 +0400 Subject: [PATCH 02/15] ISSUE-345: move tests to core --- .../Identity/Dto/CreateAdministratorDto.php | 8 +- .../Identity/Dto/UpdateAdministratorDto.php | 3 +- .../Model/Messaging/Dto/CreateMessageDto.php | 43 +++-- .../Model/Messaging/Dto/CreateTemplateDto.php | 14 +- .../Dto/Message/MessageContentDto.php | 5 +- .../Dto/Message/MessageFormatDto.php | 5 +- .../Dto/Message/MessageMetadataDto.php | 5 +- .../Dto/Message/MessageOptionsDto.php | 5 +- .../Dto/Message/MessageScheduleDto.php | 5 +- .../Messaging/Dto/MessageDtoInterface.php | 3 - .../Model/Messaging/Dto/UpdateMessageDto.php | 40 ++++- .../Messaging/Dto/UpdateSubscriberDto.php | 3 +- .../Subscription/Dto/CreateSubscriberDto.php | 3 +- .../Dto/CreateSubscriberListDto.php | 10 +- .../Subscription/Dto/UpdateSubscriberDto.php | 3 +- .../Service/Manager/AdministratorManager.php | 2 +- src/Domain/Service/Manager/SessionManager.php | 2 +- .../Service/Manager/SubscriberListManager.php | 2 +- .../Service/Manager/TemplateManager.php | 6 +- .../Service/Builder/MessageBuilderTest.php | 168 ++++++++++++++++++ .../Builder/MessageContentBuilderTest.php | 45 +++++ .../Builder/MessageFormatBuilderTest.php | 38 ++++ .../Builder/MessageOptionsBuilderTest.php | 45 +++++ .../Builder/MessageScheduleBuilderTest.php | 48 +++++ .../Manager/AdministratorManagerTest.php | 96 ++++++++++ .../Service/Manager/MessageManagerTest.php | 141 +++++++++++++++ .../Service/Manager/SessionManagerTest.php | 49 +++++ .../Manager/SubscriberListManagerTest.php | 77 ++++++++ .../Service/Manager/SubscriberManagerTest.php | 44 +++++ .../Manager/SubscriptionManagerTest.php | 107 +++++++++++ .../Manager/TemplateImageManagerTest.php | 88 +++++++++ .../Service/Manager/TemplateManagerTest.php | 91 ++++++++++ .../Validator/TemplateImageValidatorTest.php | 87 +++++++++ .../Validator/TemplateLinkValidatorTest.php | 66 +++++++ 34 files changed, 1306 insertions(+), 51 deletions(-) create mode 100644 tests/Unit/Domain/Service/Builder/MessageBuilderTest.php create mode 100644 tests/Unit/Domain/Service/Builder/MessageContentBuilderTest.php create mode 100644 tests/Unit/Domain/Service/Builder/MessageFormatBuilderTest.php create mode 100644 tests/Unit/Domain/Service/Builder/MessageOptionsBuilderTest.php create mode 100644 tests/Unit/Domain/Service/Builder/MessageScheduleBuilderTest.php create mode 100644 tests/Unit/Domain/Service/Manager/AdministratorManagerTest.php create mode 100644 tests/Unit/Domain/Service/Manager/MessageManagerTest.php create mode 100644 tests/Unit/Domain/Service/Manager/SessionManagerTest.php create mode 100644 tests/Unit/Domain/Service/Manager/SubscriberListManagerTest.php create mode 100644 tests/Unit/Domain/Service/Manager/SubscriberManagerTest.php create mode 100644 tests/Unit/Domain/Service/Manager/SubscriptionManagerTest.php create mode 100644 tests/Unit/Domain/Service/Manager/TemplateImageManagerTest.php create mode 100644 tests/Unit/Domain/Service/Manager/TemplateManagerTest.php create mode 100644 tests/Unit/Domain/Service/Validator/TemplateImageValidatorTest.php create mode 100644 tests/Unit/Domain/Service/Validator/TemplateLinkValidatorTest.php diff --git a/src/Domain/Model/Identity/Dto/CreateAdministratorDto.php b/src/Domain/Model/Identity/Dto/CreateAdministratorDto.php index e7b7a4b9..bb77e2f2 100644 --- a/src/Domain/Model/Identity/Dto/CreateAdministratorDto.php +++ b/src/Domain/Model/Identity/Dto/CreateAdministratorDto.php @@ -6,10 +6,14 @@ final class CreateAdministratorDto { + /** + * @SuppressWarnings("BooleanArgumentFlag") + */ public function __construct( public readonly string $loginName, public readonly string $password, public readonly string $email, - public readonly bool $superUser = false, - ) {} + public readonly bool $isSuperUser = false, + ) { + } } diff --git a/src/Domain/Model/Identity/Dto/UpdateAdministratorDto.php b/src/Domain/Model/Identity/Dto/UpdateAdministratorDto.php index 06ef7882..c5260974 100644 --- a/src/Domain/Model/Identity/Dto/UpdateAdministratorDto.php +++ b/src/Domain/Model/Identity/Dto/UpdateAdministratorDto.php @@ -12,5 +12,6 @@ public function __construct( public readonly ?string $password = null, public readonly ?string $email = null, public readonly ?bool $superAdmin = null, - ) {} + ) { + } } diff --git a/src/Domain/Model/Messaging/Dto/CreateMessageDto.php b/src/Domain/Model/Messaging/Dto/CreateMessageDto.php index e3dedeec..e699b885 100644 --- a/src/Domain/Model/Messaging/Dto/CreateMessageDto.php +++ b/src/Domain/Model/Messaging/Dto/CreateMessageDto.php @@ -4,14 +4,13 @@ namespace PhpList\Core\Domain\Model\Messaging\Dto; - use PhpList\Core\Domain\Model\Messaging\Dto\Message\MessageContentDto; use PhpList\Core\Domain\Model\Messaging\Dto\Message\MessageFormatDto; use PhpList\Core\Domain\Model\Messaging\Dto\Message\MessageMetadataDto; use PhpList\Core\Domain\Model\Messaging\Dto\Message\MessageOptionsDto; use PhpList\Core\Domain\Model\Messaging\Dto\Message\MessageScheduleDto; -final class CreateMessageDto implements MessageDtoInterface +class CreateMessageDto implements MessageDtoInterface { public function __construct( public readonly MessageContentDto $content, @@ -20,12 +19,36 @@ public function __construct( public readonly MessageOptionsDto $options, public readonly MessageScheduleDto $schedule, public readonly ?int $templateId = null, - ) {} - - public function getContent(): MessageContentDto { return $this->content; } - public function getFormat(): MessageFormatDto { return $this->format; } - public function getMetadata(): MessageMetadataDto { return $this->metadata; } - public function getOptions(): MessageOptionsDto { return $this->options; } - public function getSchedule(): MessageScheduleDto { return $this->schedule; } - public function getTemplateId(): ?int { return $this->templateId; } + ) { + } + + public function getContent(): MessageContentDto + { + return $this->content; + } + + public function getFormat(): MessageFormatDto + { + return $this->format; + } + + public function getMetadata(): MessageMetadataDto + { + return $this->metadata; + } + + public function getOptions(): MessageOptionsDto + { + return $this->options; + } + + public function getSchedule(): MessageScheduleDto + { + return $this->schedule; + } + + public function getTemplateId(): ?int + { + return $this->templateId; + } } diff --git a/src/Domain/Model/Messaging/Dto/CreateTemplateDto.php b/src/Domain/Model/Messaging/Dto/CreateTemplateDto.php index 7ca0412b..bb8f9598 100644 --- a/src/Domain/Model/Messaging/Dto/CreateTemplateDto.php +++ b/src/Domain/Model/Messaging/Dto/CreateTemplateDto.php @@ -4,15 +4,19 @@ namespace PhpList\Core\Domain\Model\Messaging\Dto; -final class CreateTemplateDto +class CreateTemplateDto { + /** + * @SuppressWarnings("BooleanArgumentFlag") + */ public function __construct( public readonly string $title, public readonly string $content, public readonly ?string $text = null, public readonly ?string $fileContent = null, - public readonly bool $checkLinks = false, - public readonly bool $checkImages = false, - public readonly bool $checkExternalImages = false, - ) {} + public readonly bool $shouldCheckLinks = false, + public readonly bool $shouldCheckImages = false, + public readonly bool $shouldCheckExternalImages = false, + ) { + } } diff --git a/src/Domain/Model/Messaging/Dto/Message/MessageContentDto.php b/src/Domain/Model/Messaging/Dto/Message/MessageContentDto.php index 29128a83..2ddab9e8 100644 --- a/src/Domain/Model/Messaging/Dto/Message/MessageContentDto.php +++ b/src/Domain/Model/Messaging/Dto/Message/MessageContentDto.php @@ -4,12 +4,13 @@ namespace PhpList\Core\Domain\Model\Messaging\Dto\Message; -final class MessageContentDto +class MessageContentDto { public function __construct( public readonly string $subject, public readonly string $text, public readonly string $textMessage, public readonly string $footer, - ) {} + ) { + } } diff --git a/src/Domain/Model/Messaging/Dto/Message/MessageFormatDto.php b/src/Domain/Model/Messaging/Dto/Message/MessageFormatDto.php index 8db7bbcc..55399d45 100644 --- a/src/Domain/Model/Messaging/Dto/Message/MessageFormatDto.php +++ b/src/Domain/Model/Messaging/Dto/Message/MessageFormatDto.php @@ -4,11 +4,12 @@ namespace PhpList\Core\Domain\Model\Messaging\Dto\Message; -final class MessageFormatDto +class MessageFormatDto { public function __construct( public readonly bool $htmlFormated, public readonly string $sendFormat, public readonly array $formatOptions, - ) {} + ) { + } } diff --git a/src/Domain/Model/Messaging/Dto/Message/MessageMetadataDto.php b/src/Domain/Model/Messaging/Dto/Message/MessageMetadataDto.php index ec62050b..c425b498 100644 --- a/src/Domain/Model/Messaging/Dto/Message/MessageMetadataDto.php +++ b/src/Domain/Model/Messaging/Dto/Message/MessageMetadataDto.php @@ -4,9 +4,10 @@ namespace PhpList\Core\Domain\Model\Messaging\Dto\Message; -final class MessageMetadataDto +class MessageMetadataDto { public function __construct( public readonly string $status, - ) {} + ) { + } } diff --git a/src/Domain/Model/Messaging/Dto/Message/MessageOptionsDto.php b/src/Domain/Model/Messaging/Dto/Message/MessageOptionsDto.php index c7263382..c2ce9e50 100644 --- a/src/Domain/Model/Messaging/Dto/Message/MessageOptionsDto.php +++ b/src/Domain/Model/Messaging/Dto/Message/MessageOptionsDto.php @@ -4,12 +4,13 @@ namespace PhpList\Core\Domain\Model\Messaging\Dto\Message; -final class MessageOptionsDto +class MessageOptionsDto { public function __construct( public readonly string $fromField, public readonly ?string $toField = null, public readonly ?string $replyTo = null, public readonly ?string $userSelection = null, - ) {} + ) { + } } diff --git a/src/Domain/Model/Messaging/Dto/Message/MessageScheduleDto.php b/src/Domain/Model/Messaging/Dto/Message/MessageScheduleDto.php index 58de0688..957a1158 100644 --- a/src/Domain/Model/Messaging/Dto/Message/MessageScheduleDto.php +++ b/src/Domain/Model/Messaging/Dto/Message/MessageScheduleDto.php @@ -4,7 +4,7 @@ namespace PhpList\Core\Domain\Model\Messaging\Dto\Message; -final class MessageScheduleDto +class MessageScheduleDto { public function __construct( public readonly string $embargo, @@ -12,5 +12,6 @@ public function __construct( public readonly ?string $repeatUntil = null, public readonly ?int $requeueInterval = null, public readonly ?string $requeueUntil = null, - ) {} + ) { + } } diff --git a/src/Domain/Model/Messaging/Dto/MessageDtoInterface.php b/src/Domain/Model/Messaging/Dto/MessageDtoInterface.php index ae815871..8b3a54b2 100644 --- a/src/Domain/Model/Messaging/Dto/MessageDtoInterface.php +++ b/src/Domain/Model/Messaging/Dto/MessageDtoInterface.php @@ -4,8 +4,6 @@ namespace PhpList\Core\Domain\Model\Messaging\Dto; -namespace PhpList\Core\Domain\Model\Messaging\Dto; - use PhpList\Core\Domain\Model\Messaging\Dto\Message\MessageContentDto; use PhpList\Core\Domain\Model\Messaging\Dto\Message\MessageFormatDto; use PhpList\Core\Domain\Model\Messaging\Dto\Message\MessageMetadataDto; @@ -21,4 +19,3 @@ public function getOptions(): MessageOptionsDto; public function getSchedule(): MessageScheduleDto; public function getTemplateId(): ?int; } - diff --git a/src/Domain/Model/Messaging/Dto/UpdateMessageDto.php b/src/Domain/Model/Messaging/Dto/UpdateMessageDto.php index 3f656f1d..ff4f1c3f 100644 --- a/src/Domain/Model/Messaging/Dto/UpdateMessageDto.php +++ b/src/Domain/Model/Messaging/Dto/UpdateMessageDto.php @@ -20,12 +20,36 @@ public function __construct( public readonly MessageOptionsDto $options, public readonly MessageScheduleDto $schedule, public readonly ?int $templateId = null, - ) {} - - public function getContent(): MessageContentDto { return $this->content; } - public function getFormat(): MessageFormatDto { return $this->format; } - public function getMetadata(): MessageMetadataDto { return $this->metadata; } - public function getOptions(): MessageOptionsDto { return $this->options; } - public function getSchedule(): MessageScheduleDto { return $this->schedule; } - public function getTemplateId(): ?int { return $this->templateId; } + ) { + } + + public function getContent(): MessageContentDto + { + return $this->content; + } + + public function getFormat(): MessageFormatDto + { + return $this->format; + } + + public function getMetadata(): MessageMetadataDto + { + return $this->metadata; + } + + public function getOptions(): MessageOptionsDto + { + return $this->options; + } + + public function getSchedule(): MessageScheduleDto + { + return $this->schedule; + } + + public function getTemplateId(): ?int + { + return $this->templateId; + } } diff --git a/src/Domain/Model/Messaging/Dto/UpdateSubscriberDto.php b/src/Domain/Model/Messaging/Dto/UpdateSubscriberDto.php index de8f3a8d..0a57883f 100644 --- a/src/Domain/Model/Messaging/Dto/UpdateSubscriberDto.php +++ b/src/Domain/Model/Messaging/Dto/UpdateSubscriberDto.php @@ -14,5 +14,6 @@ public function __construct( public readonly bool $htmlEmail, public readonly bool $disabled, public readonly string $additionalData, - ) {} + ) { + } } diff --git a/src/Domain/Model/Subscription/Dto/CreateSubscriberDto.php b/src/Domain/Model/Subscription/Dto/CreateSubscriberDto.php index f0b08d37..1b1306da 100644 --- a/src/Domain/Model/Subscription/Dto/CreateSubscriberDto.php +++ b/src/Domain/Model/Subscription/Dto/CreateSubscriberDto.php @@ -10,5 +10,6 @@ public function __construct( public readonly string $email, public readonly ?bool $requestConfirmation = null, public readonly ?bool $htmlEmail = null, - ) {} + ) { + } } diff --git a/src/Domain/Model/Subscription/Dto/CreateSubscriberListDto.php b/src/Domain/Model/Subscription/Dto/CreateSubscriberListDto.php index 1a2b90e1..e573b891 100644 --- a/src/Domain/Model/Subscription/Dto/CreateSubscriberListDto.php +++ b/src/Domain/Model/Subscription/Dto/CreateSubscriberListDto.php @@ -4,12 +4,16 @@ namespace PhpList\Core\Domain\Model\Subscription\Dto; -final class CreateSubscriberListDto +class CreateSubscriberListDto { + /** + * @SuppressWarnings("BooleanArgumentFlag") + */ public function __construct( public readonly string $name, - public readonly bool $public = false, + public readonly bool $isPublic = false, public readonly ?int $listPosition = null, public readonly ?string $description = null, - ) {} + ) { + } } diff --git a/src/Domain/Model/Subscription/Dto/UpdateSubscriberDto.php b/src/Domain/Model/Subscription/Dto/UpdateSubscriberDto.php index 767fb75b..8eb9ac16 100644 --- a/src/Domain/Model/Subscription/Dto/UpdateSubscriberDto.php +++ b/src/Domain/Model/Subscription/Dto/UpdateSubscriberDto.php @@ -14,5 +14,6 @@ public function __construct( public readonly bool $htmlEmail, public readonly bool $disabled, public readonly string $additionalData, - ) {} + ) { + } } diff --git a/src/Domain/Service/Manager/AdministratorManager.php b/src/Domain/Service/Manager/AdministratorManager.php index b340c586..42e0bd8f 100644 --- a/src/Domain/Service/Manager/AdministratorManager.php +++ b/src/Domain/Service/Manager/AdministratorManager.php @@ -26,7 +26,7 @@ public function createAdministrator(CreateAdministratorDto $dto): Administrator $administrator = new Administrator(); $administrator->setLoginName($dto->loginName); $administrator->setEmail($dto->email); - $administrator->setSuperUser($dto->superUser); + $administrator->setSuperUser($dto->isSuperUser); $hashedPassword = $this->hashGenerator->createPasswordHash($dto->password); $administrator->setPasswordHash($hashedPassword); diff --git a/src/Domain/Service/Manager/SessionManager.php b/src/Domain/Service/Manager/SessionManager.php index 71ee7b92..e2244de8 100644 --- a/src/Domain/Service/Manager/SessionManager.php +++ b/src/Domain/Service/Manager/SessionManager.php @@ -22,7 +22,7 @@ public function __construct( $this->administratorRepository = $administratorRepository; } - public function createSession(string $loginName, string $password): AdministratorToken + public function createSession(string $loginName, string $password): AdministratorToken { $administrator = $this->administratorRepository->findOneByLoginCredentials($loginName, $password); if ($administrator === null) { diff --git a/src/Domain/Service/Manager/SubscriberListManager.php b/src/Domain/Service/Manager/SubscriberListManager.php index 73cb23ba..8b23923d 100644 --- a/src/Domain/Service/Manager/SubscriberListManager.php +++ b/src/Domain/Service/Manager/SubscriberListManager.php @@ -27,7 +27,7 @@ public function createSubscriberList( ->setOwner($authUser) ->setDescription($subscriberListDto->description) ->setListPosition($subscriberListDto->listPosition) - ->setPublic($subscriberListDto->public); + ->setPublic($subscriberListDto->isPublic); $this->subscriberListRepository->save($subscriberList); diff --git a/src/Domain/Service/Manager/TemplateManager.php b/src/Domain/Service/Manager/TemplateManager.php index c38d25bf..a047416a 100644 --- a/src/Domain/Service/Manager/TemplateManager.php +++ b/src/Domain/Service/Manager/TemplateManager.php @@ -47,9 +47,9 @@ public function create(CreateTemplateDto $createTemplateDto): Template } $context = (new ValidationContext()) - ->set('checkLinks', $createTemplateDto->checkLinks) - ->set('checkImages', $createTemplateDto->checkImages) - ->set('checkExternalImages', $createTemplateDto->checkExternalImages); + ->set('checkLinks', $createTemplateDto->shouldCheckLinks) + ->set('checkImages', $createTemplateDto->shouldCheckImages) + ->set('checkExternalImages', $createTemplateDto->shouldCheckExternalImages); $this->templateLinkValidator->validate($template->getContent() ?? '', $context); diff --git a/tests/Unit/Domain/Service/Builder/MessageBuilderTest.php b/tests/Unit/Domain/Service/Builder/MessageBuilderTest.php new file mode 100644 index 00000000..9d3eab89 --- /dev/null +++ b/tests/Unit/Domain/Service/Builder/MessageBuilderTest.php @@ -0,0 +1,168 @@ +createMock(TemplateRepository::class); + $this->formatBuilder = $this->createMock(MessageFormatBuilder::class); + $this->scheduleBuilder = $this->createMock(MessageScheduleBuilder::class); + $this->contentBuilder = $this->createMock(MessageContentBuilder::class); + $this->optionsBuilder = $this->createMock(MessageOptionsBuilder::class); + + $this->builder = new MessageBuilder( + $templateRepository, + $this->formatBuilder, + $this->scheduleBuilder, + $this->contentBuilder, + $this->optionsBuilder + ); + } + + private function createRequest(): CreateMessageDto + { + return new CreateMessageDto( + content: new MessageContentDto( + subject: '', + text: '', + textMessage: '', + footer: '' + ), + format: new MessageFormatDto( + htmlFormated: false, + sendFormat: 'text', + formatOptions: [] + ), + metadata: new MessageMetadataDto( + status: 'draft' + ), + options: new MessageOptionsDto( + fromField: '', + toField: null, + replyTo: null, + userSelection: null + ), + schedule: new MessageScheduleDto( + embargo: '', + repeatInterval: null, + repeatUntil: null, + requeueInterval: null, + requeueUntil: null + ), + templateId: 0 + ); + } + + private function mockBuildCalls(CreateMessageDto $createMessageDto): void + { + $this->formatBuilder->expects($this->once()) + ->method('build') + ->with($createMessageDto->format) + ->willReturn($this->createMock(Message\MessageFormat::class)); + + $this->scheduleBuilder->expects($this->once()) + ->method('build') + ->with($createMessageDto->schedule) + ->willReturn($this->createMock(Message\MessageSchedule::class)); + + $this->contentBuilder->expects($this->once()) + ->method('build') + ->with($createMessageDto->content) + ->willReturn($this->createMock(Message\MessageContent::class)); + + $this->optionsBuilder->expects($this->once()) + ->method('build') + ->with($createMessageDto->options) + ->willReturn($this->createMock(Message\MessageOptions::class)); + } + + public function testBuildsNewMessage(): void + { + $request = $this->createRequest(); + $admin = $this->createMock(Administrator::class); + $context = new MessageContext($admin); + + $this->mockBuildCalls($request); + + $this->builder->build($request, $context); + } + + public function testThrowsExceptionOnInvalidRequest(): void + { + $this->expectException(Error::class); + + $this->builder->build( + $this->createMock(CreateMessageDto::class), + new MessageContext($this->createMock(Administrator::class)) + ); + } + + public function testThrowsExceptionOnInvalidContext(): void + { + $this->expectException(InvalidArgumentException::class); + + $this->builder->build($this->createMock(CreateMessageDto::class), new \stdClass()); + } + + public function testUpdatesExistingMessage(): void + { + $request = $this->createRequest(); + $admin = $this->createMock(Administrator::class); + $existingMessage = $this->createMock(Message::class); + $context = new MessageContext($admin, $existingMessage); + + $this->mockBuildCalls($request); + + $existingMessage + ->expects($this->once()) + ->method('setFormat') + ->with($this->isInstanceOf(Message\MessageFormat::class)); + $existingMessage + ->expects($this->once()) + ->method('setSchedule') + ->with($this->isInstanceOf(Message\MessageSchedule::class)); + $existingMessage + ->expects($this->once()) + ->method('setContent') + ->with($this->isInstanceOf(Message\MessageContent::class)); + $existingMessage + ->expects($this->once()) + ->method('setOptions') + ->with($this->isInstanceOf(Message\MessageOptions::class)); + $existingMessage->expects($this->once())->method('setTemplate')->with(null); + + $result = $this->builder->build($request, $context); + + $this->assertSame($existingMessage, $result); + } +} diff --git a/tests/Unit/Domain/Service/Builder/MessageContentBuilderTest.php b/tests/Unit/Domain/Service/Builder/MessageContentBuilderTest.php new file mode 100644 index 00000000..e9c63112 --- /dev/null +++ b/tests/Unit/Domain/Service/Builder/MessageContentBuilderTest.php @@ -0,0 +1,45 @@ +builder = new MessageContentBuilder(); + } + + public function testBuildsMessageContentSuccessfully(): void + { + $dto = new MessageContentDto( + subject: 'Test Subject', + text: 'Full text content', + textMessage: 'Short text version', + footer: 'Footer text' + ); + + $messageContent = $this->builder->build($dto); + + $this->assertSame('Test Subject', $messageContent->getSubject()); + $this->assertSame('Full text content', $messageContent->getText()); + $this->assertSame('Short text version', $messageContent->getTextMessage()); + $this->assertSame('Footer text', $messageContent->getFooter()); + } + + public function testThrowsExceptionOnInvalidDto(): void + { + $this->expectException(InvalidArgumentException::class); + + $invalidDto = new \stdClass(); + $this->builder->build($invalidDto); + } +} diff --git a/tests/Unit/Domain/Service/Builder/MessageFormatBuilderTest.php b/tests/Unit/Domain/Service/Builder/MessageFormatBuilderTest.php new file mode 100644 index 00000000..acca15e4 --- /dev/null +++ b/tests/Unit/Domain/Service/Builder/MessageFormatBuilderTest.php @@ -0,0 +1,38 @@ +builder = new MessageFormatBuilder(); + } + + public function testBuildsMessageFormatSuccessfully(): void + { + $dto = new MessageFormatDto(htmlFormated: true, sendFormat: 'html', formatOptions: ['html', 'text']); + $messageFormat = $this->builder->build($dto); + + $this->assertSame(true, $messageFormat->isHtmlFormatted()); + $this->assertSame('html', $messageFormat->getSendFormat()); + $this->assertEqualsCanonicalizing(['html', 'text'], $messageFormat->getFormatOptions()); + } + + public function testThrowsExceptionOnInvalidDto(): void + { + $this->expectException(InvalidArgumentException::class); + + $invalidDto = new \stdClass(); + $this->builder->build($invalidDto); + } +} diff --git a/tests/Unit/Domain/Service/Builder/MessageOptionsBuilderTest.php b/tests/Unit/Domain/Service/Builder/MessageOptionsBuilderTest.php new file mode 100644 index 00000000..03eeaf21 --- /dev/null +++ b/tests/Unit/Domain/Service/Builder/MessageOptionsBuilderTest.php @@ -0,0 +1,45 @@ +builder = new MessageOptionsBuilder(); + } + + public function testBuildsMessageOptionsSuccessfully(): void + { + $dto = new MessageOptionsDto( + fromField: 'info@example.com', + toField: 'user@example.com', + replyTo: 'reply@example.com', + userSelection: 'all-users' + ); + + $messageOptions = $this->builder->build($dto); + + $this->assertSame('info@example.com', $messageOptions->getFromField()); + $this->assertSame('user@example.com', $messageOptions->getToField()); + $this->assertSame('reply@example.com', $messageOptions->getReplyTo()); + $this->assertSame('all-users', $messageOptions->getUserSelection()); + } + + public function testThrowsExceptionOnInvalidDto(): void + { + $this->expectException(InvalidArgumentException::class); + + $invalidDto = new \stdClass(); + $this->builder->build($invalidDto); + } +} diff --git a/tests/Unit/Domain/Service/Builder/MessageScheduleBuilderTest.php b/tests/Unit/Domain/Service/Builder/MessageScheduleBuilderTest.php new file mode 100644 index 00000000..b21d810c --- /dev/null +++ b/tests/Unit/Domain/Service/Builder/MessageScheduleBuilderTest.php @@ -0,0 +1,48 @@ +builder = new MessageScheduleBuilder(); + } + + public function testBuildsMessageScheduleSuccessfully(): void + { + $dto = new MessageScheduleDto( + embargo: '2025-04-17T09:00:00+00:00', + repeatInterval: 1440, + repeatUntil: '2025-04-30T00:00:00+00:00', + requeueInterval: 720, + requeueUntil: '2025-04-20T00:00:00+00:00' + ); + + $messageSchedule = $this->builder->build($dto); + + $this->assertSame(1440, $messageSchedule->getRepeatInterval()); + $this->assertEquals(new DateTime('2025-04-30T00:00:00+00:00'), $messageSchedule->getRepeatUntil()); + $this->assertSame(720, $messageSchedule->getRequeueInterval()); + $this->assertEquals(new DateTime('2025-04-20T00:00:00+00:00'), $messageSchedule->getRequeueUntil()); + $this->assertEquals(new DateTime('2025-04-17T09:00:00+00:00'), $messageSchedule->getEmbargo()); + } + + public function testThrowsExceptionOnInvalidDto(): void + { + $this->expectException(InvalidArgumentException::class); + + $invalidDto = new \stdClass(); + $this->builder->build($invalidDto); + } +} diff --git a/tests/Unit/Domain/Service/Manager/AdministratorManagerTest.php b/tests/Unit/Domain/Service/Manager/AdministratorManagerTest.php new file mode 100644 index 00000000..f61d3b80 --- /dev/null +++ b/tests/Unit/Domain/Service/Manager/AdministratorManagerTest.php @@ -0,0 +1,96 @@ +createMock(EntityManagerInterface::class); + $hashGenerator = $this->createMock(HashGenerator::class); + + $dto = new CreateAdministratorDto( + loginName: 'admin', + password: 'securepass', + email: 'admin@example.com', + isSuperUser: true + ); + + $hashGenerator->expects($this->once()) + ->method('createPasswordHash') + ->with('securepass') + ->willReturn('hashed_pass'); + + $entityManager->expects($this->once())->method('persist'); + $entityManager->expects($this->once())->method('flush'); + + $manager = new AdministratorManager($entityManager, $hashGenerator); + $admin = $manager->createAdministrator($dto); + + $this->assertEquals('admin', $admin->getLoginName()); + $this->assertEquals('admin@example.com', $admin->getEmail()); + $this->assertTrue($admin->isSuperUser()); + $this->assertEquals('hashed_pass', $admin->getPasswordHash()); + } + + public function testUpdateAdministrator(): void + { + $entityManager = $this->createMock(EntityManagerInterface::class); + $hashGenerator = $this->createMock(HashGenerator::class); + + $admin = new Administrator(); + $admin->setLoginName('old'); + $admin->setEmail('old@example.com'); + $admin->setSuperUser(false); + $admin->setPasswordHash('old_hash'); + + $dto = new UpdateAdministratorDto( + administratorId: 1, + loginName: 'new', + password: 'newpass', + email: 'new@example.com', + superAdmin: true + ); + + $hashGenerator->expects($this->once()) + ->method('createPasswordHash') + ->with('newpass') + ->willReturn('new_hash'); + + $entityManager->expects($this->once())->method('flush'); + + $manager = new AdministratorManager($entityManager, $hashGenerator); + $manager->updateAdministrator($admin, $dto); + + $this->assertEquals('new', $admin->getLoginName()); + $this->assertEquals('new@example.com', $admin->getEmail()); + $this->assertTrue($admin->isSuperUser()); + $this->assertEquals('new_hash', $admin->getPasswordHash()); + } + + public function testDeleteAdministrator(): void + { + $entityManager = $this->createMock(EntityManagerInterface::class); + $hashGenerator = $this->createMock(HashGenerator::class); + + $admin = $this->createMock(Administrator::class); + + $entityManager->expects($this->once())->method('remove')->with($admin); + $entityManager->expects($this->once())->method('flush'); + + $manager = new AdministratorManager($entityManager, $hashGenerator); + $manager->deleteAdministrator($admin); + + $this->assertTrue(true); + } +} diff --git a/tests/Unit/Domain/Service/Manager/MessageManagerTest.php b/tests/Unit/Domain/Service/Manager/MessageManagerTest.php new file mode 100644 index 00000000..e9ac714f --- /dev/null +++ b/tests/Unit/Domain/Service/Manager/MessageManagerTest.php @@ -0,0 +1,141 @@ +createMock(MessageRepository::class); + $messageBuilder = $this->createMock(MessageBuilder::class); + $manager = new MessageManager($messageRepository, $messageBuilder); + + $format = new MessageFormatDto(true, 'html', ['html']); + $schedule = new MessageScheduleDto( + embargo: '2025-04-17T09:00:00+00:00', + repeatInterval: 60 * 24, + repeatUntil: '2025-04-30T00:00:00+00:00', + requeueInterval: 60 * 12, + requeueUntil: '2025-04-20T00:00:00+00:00', + ); + $metadata = new MessageMetadataDto('draft'); + $content = new MessageContentDto('Subject', 'Full text', 'Short text', 'Footer'); + $options = new MessageOptionsDto('from@example.com', 'to@example.com', 'reply@example.com', 'all-users'); + + $request = new CreateMessageDto( + content: $content, + format: $format, + metadata: $metadata, + options: $options, + schedule: $schedule, + templateId: 0 + ); + + $authUser = $this->createMock(Administrator::class); + + $expectedMessage = $this->createMock(Message::class); + $expectedContent = $this->createMock(Message\MessageContent::class); + $expectedMetadata = $this->createMock(Message\MessageMetadata::class); + + $expectedContent->method('getSubject')->willReturn('Subject'); + $expectedMetadata->method('getStatus')->willReturn('draft'); + + $expectedMessage->method('getContent')->willReturn($expectedContent); + $expectedMessage->method('getMetadata')->willReturn($expectedMetadata); + + $messageBuilder->expects($this->once()) + ->method('build') + ->with($request, $this->anything()) + ->willReturn($expectedMessage); + + $messageRepository->expects($this->once()) + ->method('save') + ->with($expectedMessage); + + $message = $manager->createMessage($request, $authUser); + + $this->assertSame('Subject', $message->getContent()->getSubject()); + $this->assertSame('draft', $message->getMetadata()->getStatus()); + } + + public function testUpdateMessageReturnsUpdatedMessage(): void + { + $messageRepository = $this->createMock(MessageRepository::class); + $messageBuilder = $this->createMock(MessageBuilder::class); + $manager = new MessageManager($messageRepository, $messageBuilder); + + $format = new MessageFormatDto(false, 'text', ['text']); + $schedule = new MessageScheduleDto( + embargo: '2025-04-17T09:00:00+00:00', + repeatInterval: 0, + repeatUntil: '2025-04-30T00:00:00+00:00', + requeueInterval: 0, + requeueUntil: '2025-04-20T00:00:00+00:00', + ); + $metadata = new MessageMetadataDto('draft'); + $content = new MessageContentDto( + 'Updated Subject', + 'Updated Full text', + 'Updated Short text', + 'Updated Footer' + ); + $options = new MessageOptionsDto( + 'newfrom@example.com', + 'newto@example.com', + 'newreply@example.com', + 'active-users' + ); + + $updateRequest = new UpdateMessageDto( + messageId: 1, + content: $content, + format: $format, + metadata: $metadata, + options: $options, + schedule: $schedule, + templateId: 2 + ); + + $authUser = $this->createMock(Administrator::class); + + $existingMessage = $this->createMock(Message::class); + $expectedContent = $this->createMock(Message\MessageContent::class); + $expectedMetadata = $this->createMock(Message\MessageMetadata::class); + + $expectedContent->method('getSubject')->willReturn('Updated Subject'); + $expectedMetadata->method('getStatus')->willReturn('draft'); + + $existingMessage->method('getContent')->willReturn($expectedContent); + $existingMessage->method('getMetadata')->willReturn($expectedMetadata); + + $messageBuilder->expects($this->once()) + ->method('build') + ->with($updateRequest, $this->anything()) + ->willReturn($existingMessage); + + $messageRepository->expects($this->once()) + ->method('save') + ->with($existingMessage); + + $message = $manager->updateMessage($updateRequest, $existingMessage, $authUser); + + $this->assertSame('Updated Subject', $message->getContent()->getSubject()); + $this->assertSame('draft', $message->getMetadata()->getStatus()); + } +} diff --git a/tests/Unit/Domain/Service/Manager/SessionManagerTest.php b/tests/Unit/Domain/Service/Manager/SessionManagerTest.php new file mode 100644 index 00000000..02b1266b --- /dev/null +++ b/tests/Unit/Domain/Service/Manager/SessionManagerTest.php @@ -0,0 +1,49 @@ +createMock(AdministratorRepository::class); + $adminRepo->expects(self::once()) + ->method('findOneByLoginCredentials') + ->with('admin', 'wrong') + ->willReturn(null); + + $tokenRepo = $this->createMock(AdministratorTokenRepository::class); + $tokenRepo->expects(self::never())->method('save'); + + $manager = new SessionManager($tokenRepo, $adminRepo); + + $this->expectException(UnauthorizedHttpException::class); + $this->expectExceptionMessage('Not authorized'); + + $manager->createSession('admin', 'wrong'); + } + + public function testDeleteSessionCallsRemove(): void + { + $token = $this->createMock(AdministratorToken::class); + + $tokenRepo = $this->createMock(AdministratorTokenRepository::class); + $tokenRepo->expects(self::once()) + ->method('remove') + ->with($token); + + $adminRepo = $this->createMock(AdministratorRepository::class); + + $manager = new SessionManager($tokenRepo, $adminRepo); + $manager->deleteSession($token); + } +} diff --git a/tests/Unit/Domain/Service/Manager/SubscriberListManagerTest.php b/tests/Unit/Domain/Service/Manager/SubscriberListManagerTest.php new file mode 100644 index 00000000..9e78fd08 --- /dev/null +++ b/tests/Unit/Domain/Service/Manager/SubscriberListManagerTest.php @@ -0,0 +1,77 @@ +subscriberListRepository = $this->createMock(SubscriberListRepository::class); + $this->manager = new SubscriberListManager($this->subscriberListRepository); + } + + public function testCreateSubscriberList(): void + { + $request = new CreateSubscriberListDto( + name: 'New List', + isPublic: true, + listPosition: 3, + description: 'Description' + ); + + $admin = new Administrator(); + + $this->subscriberListRepository + ->expects($this->once()) + ->method('save') + ->with($this->isInstanceOf(SubscriberList::class)); + + $result = $this->manager->createSubscriberList($request, $admin); + + $this->assertSame('New List', $result->getName()); + $this->assertSame('Description', $result->getDescription()); + $this->assertSame(3, $result->getListPosition()); + $this->assertTrue($result->isPublic()); + $this->assertSame($admin, $result->getOwner()); + } + + public function testGetPaginated(): void + { + $list = new SubscriberList(); + $this->subscriberListRepository + ->expects($this->once()) + ->method('getAfterId') + ->willReturn([$list]); + + $result = $this->manager->getPaginated(0, 1); + + $this->assertIsArray($result); + $this->assertCount(1, $result); + $this->assertSame($list, $result[0]); + } + + public function testDeleteSubscriberList(): void + { + $subscriberList = new SubscriberList(); + + $this->subscriberListRepository + ->expects($this->once()) + ->method('remove') + ->with($subscriberList); + + $this->manager->delete($subscriberList); + } +} diff --git a/tests/Unit/Domain/Service/Manager/SubscriberManagerTest.php b/tests/Unit/Domain/Service/Manager/SubscriberManagerTest.php new file mode 100644 index 00000000..ac1e927b --- /dev/null +++ b/tests/Unit/Domain/Service/Manager/SubscriberManagerTest.php @@ -0,0 +1,44 @@ +createMock(SubscriberRepository::class); + $emMock = $this->createMock(EntityManagerInterface::class); + $repoMock + ->expects($this->once()) + ->method('save') + ->with($this->callback(function (Subscriber $sub): bool { + return $sub->getEmail() === 'foo@bar.com' + && $sub->isConfirmed() === false + && $sub->isBlacklisted() === false + && $sub->hasHtmlEmail() === true + && $sub->isDisabled() === false; + })); + + $manager = new SubscriberManager($repoMock, $emMock); + + $dto = new CreateSubscriberDto(email: 'foo@bar.com', requestConfirmation: true, htmlEmail: true); + + $result = $manager->createSubscriber($dto); + + $this->assertInstanceOf(Subscriber::class, $result); + $this->assertSame('foo@bar.com', $result->getEmail()); + $this->assertFalse($result->isConfirmed()); + $this->assertFalse($result->isBlacklisted()); + $this->assertTrue($result->hasHtmlEmail()); + $this->assertFalse($result->isDisabled()); + } +} diff --git a/tests/Unit/Domain/Service/Manager/SubscriptionManagerTest.php b/tests/Unit/Domain/Service/Manager/SubscriptionManagerTest.php new file mode 100644 index 00000000..8dcfc390 --- /dev/null +++ b/tests/Unit/Domain/Service/Manager/SubscriptionManagerTest.php @@ -0,0 +1,107 @@ +subscriptionRepository = $this->createMock(SubscriptionRepository::class); + $this->subscriberRepository = $this->createMock(SubscriberRepository::class); + $this->manager = new SubscriptionManager($this->subscriptionRepository, $this->subscriberRepository); + } + + public function testCreateSubscriptionWhenSubscriberExists(): void + { + $email = 'test@example.com'; + $subscriber = new Subscriber(); + $list = new SubscriberList(); + + $this->subscriberRepository->method('findOneBy')->with(['email' => $email])->willReturn($subscriber); + $this->subscriptionRepository->method('findOneBySubscriberListAndSubscriber')->willReturn(null); + $this->subscriptionRepository->expects($this->once())->method('save'); + + $subscriptions = $this->manager->createSubscriptions($list, [$email]); + + $this->assertCount(1, $subscriptions); + $this->assertInstanceOf(Subscription::class, $subscriptions[0]); + } + + public function testCreateSubscriptionThrowsWhenSubscriberMissing(): void + { + $this->expectException(SubscriptionCreationException::class); + $this->expectExceptionMessage('Subscriber does not exists.'); + + $list = new SubscriberList(); + + $this->subscriberRepository->method('findOneBy')->willReturn(null); + + $this->manager->createSubscriptions($list, ['missing@example.com']); + } + + public function testDeleteSubscriptionSuccessfully(): void + { + $email = 'user@example.com'; + $subscriberList = $this->createMock(SubscriberList::class); + $subscriberList->method('getId')->willReturn(1); + $subscription = new Subscription(); + + $this->subscriptionRepository + ->method('findOneBySubscriberEmailAndListId') + ->with($subscriberList->getId(), $email) + ->willReturn($subscription); + + $this->subscriptionRepository->expects($this->once())->method('remove')->with($subscription); + + $this->manager->deleteSubscriptions($subscriberList, [$email]); + } + + public function testDeleteSubscriptionSkipsNotFound(): void + { + $email = 'missing@example.com'; + $subscriberList = $this->createMock(SubscriberList::class); + $subscriberList->method('getId')->willReturn(1); + + $this->subscriptionRepository + ->method('findOneBySubscriberEmailAndListId') + ->willReturn(null); + + $this->manager->deleteSubscriptions($subscriberList, [$email]); + + $this->addToAssertionCount(1); + } + + public function testGetSubscriberListMembersReturnsList(): void + { + $subscriberList = $this->createMock(SubscriberList::class); + $subscriberList->method('getId')->willReturn(1); + $subscriber = new Subscriber(); + + $this->subscriberRepository + ->method('getSubscribersBySubscribedListId') + ->with($subscriberList->getId()) + ->willReturn([$subscriber]); + + $result = $this->manager->getSubscriberListMembers($subscriberList); + + $this->assertIsArray($result); + $this->assertCount(1, $result); + $this->assertInstanceOf(Subscriber::class, $result[0]); + } +} diff --git a/tests/Unit/Domain/Service/Manager/TemplateImageManagerTest.php b/tests/Unit/Domain/Service/Manager/TemplateImageManagerTest.php new file mode 100644 index 00000000..ef03fcc8 --- /dev/null +++ b/tests/Unit/Domain/Service/Manager/TemplateImageManagerTest.php @@ -0,0 +1,88 @@ +templateImageRepository = $this->createMock(TemplateImageRepository::class); + $this->entityManager = $this->createMock(EntityManagerInterface::class); + + $this->manager = new TemplateImageManager( + $this->templateImageRepository, + $this->entityManager + ); + } + + public function testCreateImagesFromImagePaths(): void + { + $template = $this->createMock(Template::class); + + $this->entityManager->expects($this->exactly(2)) + ->method('persist') + ->with($this->isInstanceOf(TemplateImage::class)); + + $this->entityManager->expects($this->once()) + ->method('flush'); + + $images = $this->manager->createImagesFromImagePaths(['image1.jpg', 'image2.png'], $template); + + $this->assertCount(2, $images); + foreach ($images as $image) { + $this->assertInstanceOf(TemplateImage::class, $image); + } + } + + public function testGuessMimeType(): void + { + $reflection = new \ReflectionClass($this->manager); + $method = $reflection->getMethod('guessMimeType'); + + $this->assertSame('image/jpeg', $method->invoke($this->manager, 'photo.jpg')); + $this->assertSame('image/png', $method->invoke($this->manager, 'picture.png')); + $this->assertSame('application/octet-stream', $method->invoke($this->manager, 'file.unknownext')); + } + + public function testExtractAllImages(): void + { + $html = '' . + '' . + '' . + '' . + 'Download' . + '' . + ''; + + $result = $this->manager->extractAllImages($html); + + $this->assertIsArray($result); + $this->assertContains('image1.jpg', $result); + $this->assertContains('https://example.com/image2.png', $result); + } + + public function testDeleteTemplateImage(): void + { + $templateImage = $this->createMock(TemplateImage::class); + + $this->templateImageRepository->expects($this->once()) + ->method('remove') + ->with($templateImage); + + $this->manager->delete($templateImage); + } +} diff --git a/tests/Unit/Domain/Service/Manager/TemplateManagerTest.php b/tests/Unit/Domain/Service/Manager/TemplateManagerTest.php new file mode 100644 index 00000000..d2fbb38e --- /dev/null +++ b/tests/Unit/Domain/Service/Manager/TemplateManagerTest.php @@ -0,0 +1,91 @@ +templateRepository = $this->createMock(TemplateRepository::class); + $entityManager = $this->createMock(EntityManagerInterface::class); + $this->templateImageManager = $this->createMock(TemplateImageManager::class); + $this->templateLinkValidator = $this->createMock(TemplateLinkValidator::class); + $this->templateImageValidator = $this->createMock(TemplateImageValidator::class); + + $this->manager = new TemplateManager( + $this->templateRepository, + $entityManager, + $this->templateImageManager, + $this->templateLinkValidator, + $this->templateImageValidator + ); + } + + public function testCreateTemplateSuccessfully(): void + { + $request = new CreateTemplateDto( + title: 'Test Template', + content: 'Content', + text: 'Plain text', + fileContent: null, + shouldCheckLinks: true, + shouldCheckImages: true, + shouldCheckExternalImages: true + ); + + $this->templateLinkValidator->expects($this->once()) + ->method('validate') + ->with($request->content, $this->anything()); + + $this->templateImageManager->expects($this->once()) + ->method('extractAllImages') + ->with($request->content) + ->willReturn([]); + + $this->templateImageValidator->expects($this->once()) + ->method('validate') + ->with([], $this->anything()); + + $this->templateRepository->expects($this->once()) + ->method('save') + ->with($this->isInstanceOf(Template::class)); + + $this->templateImageManager->expects($this->once()) + ->method('createImagesFromImagePaths') + ->with([], $this->isInstanceOf(Template::class)); + + $template = $this->manager->create($request); + + $this->assertSame('Test Template', $template->getTitle()); + } + + public function testDeleteTemplate(): void + { + $template = $this->createMock(Template::class); + + $this->templateRepository->expects($this->once()) + ->method('remove') + ->with($template); + + $this->manager->delete($template); + } +} diff --git a/tests/Unit/Domain/Service/Validator/TemplateImageValidatorTest.php b/tests/Unit/Domain/Service/Validator/TemplateImageValidatorTest.php new file mode 100644 index 00000000..ad4b03bd --- /dev/null +++ b/tests/Unit/Domain/Service/Validator/TemplateImageValidatorTest.php @@ -0,0 +1,87 @@ +httpClient = $this->createMock(ClientInterface::class); + $this->validator = new TemplateImageValidator($this->httpClient); + } + + public function testThrowsExceptionIfValueIsNotArray(): void + { + $this->expectException(InvalidArgumentException::class); + + $this->validator->validate('not-an-array'); + } + + public function testValidatesFullUrls(): void + { + $context = (new ValidationContext())->set('checkImages', true); + + $this->expectException(ValidatorException::class); + $this->expectExceptionMessageMatches('/not-a-url/'); + + $this->validator->validate(['not-a-url', 'https://valid.url/image.jpg'], $context); + } + + public function testValidatesExistenceWithHttp200(): void + { + $context = (new ValidationContext())->set('checkExternalImages', true); + + $this->httpClient->expects($this->once()) + ->method('request') + ->with('HEAD', 'https://example.com/image.jpg') + ->willReturn(new Response(200)); + + $this->validator->validate(['https://example.com/image.jpg'], $context); + + $this->assertTrue(true); + } + + public function testValidatesExistenceWithHttp404(): void + { + $context = (new ValidationContext())->set('checkExternalImages', true); + + $this->httpClient->expects($this->once()) + ->method('request') + ->with('HEAD', 'https://example.com/missing.jpg') + ->willReturn(new Response(404)); + + $this->expectException(ValidatorException::class); + $this->expectExceptionMessageMatches('/does not exist/'); + + $this->validator->validate(['https://example.com/missing.jpg'], $context); + } + + public function testValidatesExistenceThrowsHttpException(): void + { + $context = (new ValidationContext())->set('checkExternalImages', true); + + $this->httpClient->expects($this->once()) + ->method('request') + ->willThrowException(new Exception('Connection failed')); + + $this->expectException(ValidatorException::class); + $this->expectExceptionMessageMatches('/could not be validated/'); + + $this->validator->validate(['https://example.com/broken.jpg'], $context); + } +} diff --git a/tests/Unit/Domain/Service/Validator/TemplateLinkValidatorTest.php b/tests/Unit/Domain/Service/Validator/TemplateLinkValidatorTest.php new file mode 100644 index 00000000..e75552c9 --- /dev/null +++ b/tests/Unit/Domain/Service/Validator/TemplateLinkValidatorTest.php @@ -0,0 +1,66 @@ +validator = new TemplateLinkValidator(); + } + + public function testSkipsValidationIfNotString(): void + { + $context = (new ValidationContext())->set('checkLinks', true); + + $this->validator->validate(['not', 'a', 'string'], $context); + + $this->assertTrue(true); + } + + public function testSkipsValidationIfCheckLinksIsFalse(): void + { + $context = (new ValidationContext())->set('checkLinks', false); + + $this->validator->validate('Broken link', $context); + + $this->assertTrue(true); + } + + public function testValidatesInvalidLinks(): void + { + $context = (new ValidationContext())->set('checkLinks', true); + + $html = 'Broken'; + + $this->expectException(ValidatorException::class); + $this->expectExceptionMessageMatches('/invalid-link/'); + + $this->validator->validate($html, $context); + } + + public function testAllowsValidLinksAndPlaceholders(): void + { + $context = (new ValidationContext())->set('checkLinks', true); + + $html = '' . + 'Valid Link' . + 'Valid Link' . + 'Email Link' . + 'Placeholder' . + ''; + + $this->validator->validate($html, $context); + + $this->assertTrue(true); + } +} From 4bed3c84b8b0f13925ad35ebd9db3bd1ac1c3e45 Mon Sep 17 00:00:00 2001 From: Tatevik Date: Wed, 7 May 2025 22:27:18 +0400 Subject: [PATCH 03/15] ISSUE-345: interface --- config/services.yml | 2 + config/services/builders.yml | 25 +++++++++++++ config/services/managers.yml | 37 +++++++++++++++++++ config/services/validators.yml | 8 ++++ .../Model/Messaging/Dto/UpdateMessageDto.php | 2 +- .../Messaging/Dto/UpdateSubscriberDto.php | 19 ---------- .../Subscription/Dto/UpdateSubscriberDto.php | 2 +- src/Domain/Service/Manager/MessageManager.php | 7 ++-- .../Service/Manager/TemplateManager.php | 2 +- 9 files changed, 78 insertions(+), 26 deletions(-) create mode 100644 config/services/builders.yml create mode 100644 config/services/managers.yml create mode 100644 config/services/validators.yml delete mode 100644 src/Domain/Model/Messaging/Dto/UpdateSubscriberDto.php diff --git a/config/services.yml b/config/services.yml index d7982241..19b58a94 100644 --- a/config/services.yml +++ b/config/services.yml @@ -1,3 +1,5 @@ +imports: + - { resource: 'services/*.yml' } services: # default configuration for services in *this* file _defaults: diff --git a/config/services/builders.yml b/config/services/builders.yml new file mode 100644 index 00000000..ab05342a --- /dev/null +++ b/config/services/builders.yml @@ -0,0 +1,25 @@ +services: + _defaults: + autowire: true + autoconfigure: true + public: false + + PhpList\Core\Domain\Service\Builder\MessageBuilder: + autowire: true + autoconfigure: true + + PhpList\Core\Domain\Service\Builder\MessageFormatBuilder: + autowire: true + autoconfigure: true + + PhpList\Core\Domain\Service\Builder\MessageScheduleBuilder: + autowire: true + autoconfigure: true + + PhpList\Core\Domain\Service\Builder\MessageContentBuilder: + autowire: true + autoconfigure: true + + PhpListPhpList\Core\Domain\Service\Builder\MessageOptionsBuilder: + autowire: true + autoconfigure: true diff --git a/config/services/managers.yml b/config/services/managers.yml new file mode 100644 index 00000000..1bf93802 --- /dev/null +++ b/config/services/managers.yml @@ -0,0 +1,37 @@ +services: + _defaults: + autowire: true + autoconfigure: true + public: false + + PhpList\Core\Domain\Service\Manager\SubscriberManager: + autowire: true + autoconfigure: true + + PhpList\Core\Domain\Service\Manager\SessionManager: + autowire: true + autoconfigure: true + + PhpList\Core\Domain\Service\Manager\SubscriberListManager: + autowire: true + autoconfigure: true + + PhpList\Core\Domain\Service\Manager\SubscriptionManager: + autowire: true + autoconfigure: true + + PhpList\Core\Domain\Service\Manager\MessageManager: + autowire: true + autoconfigure: true + + PhpList\Core\Domain\Service\Manager\TemplateManager: + autowire: true + autoconfigure: true + + PhpList\Core\Domain\Service\Manager\TemplateImageManager: + autowire: true + autoconfigure: true + + PhpList\Core\Domain\Service\Manager\AdministratorManager: + autowire: true + autoconfigure: true diff --git a/config/services/validators.yml b/config/services/validators.yml new file mode 100644 index 00000000..de7e4912 --- /dev/null +++ b/config/services/validators.yml @@ -0,0 +1,8 @@ +services: + PhpList\Core\Domain\Service\Validator\TemplateLinkValidator: + autowire: true + autoconfigure: true + + PhpList\Core\Domain\Service\Validator\TemplateImageValidator: + autowire: true + autoconfigure: true diff --git a/src/Domain/Model/Messaging/Dto/UpdateMessageDto.php b/src/Domain/Model/Messaging/Dto/UpdateMessageDto.php index ff4f1c3f..1c6d1ab3 100644 --- a/src/Domain/Model/Messaging/Dto/UpdateMessageDto.php +++ b/src/Domain/Model/Messaging/Dto/UpdateMessageDto.php @@ -10,7 +10,7 @@ use PhpList\Core\Domain\Model\Messaging\Dto\Message\MessageOptionsDto; use PhpList\Core\Domain\Model\Messaging\Dto\Message\MessageScheduleDto; -final class UpdateMessageDto implements MessageDtoInterface +class UpdateMessageDto implements MessageDtoInterface { public function __construct( public readonly int $messageId, diff --git a/src/Domain/Model/Messaging/Dto/UpdateSubscriberDto.php b/src/Domain/Model/Messaging/Dto/UpdateSubscriberDto.php deleted file mode 100644 index 0a57883f..00000000 --- a/src/Domain/Model/Messaging/Dto/UpdateSubscriberDto.php +++ /dev/null @@ -1,19 +0,0 @@ -messageBuilder = $messageBuilder; } - public function createMessage(CreateMessageDto $createMessageDto, Administrator $authUser): Message + public function createMessage(MessageDtoInterface $createMessageDto, Administrator $authUser): Message { $context = new MessageContext($authUser); $message = $this->messageBuilder->build($createMessageDto, $context); @@ -33,7 +32,7 @@ public function createMessage(CreateMessageDto $createMessageDto, Administrator } public function updateMessage( - UpdateMessageDto $updateMessageDto, + MessageDtoInterface $updateMessageDto, Message $message, Administrator $authUser ): Message { diff --git a/src/Domain/Service/Manager/TemplateManager.php b/src/Domain/Service/Manager/TemplateManager.php index a047416a..e27df8fc 100644 --- a/src/Domain/Service/Manager/TemplateManager.php +++ b/src/Domain/Service/Manager/TemplateManager.php @@ -7,8 +7,8 @@ use Doctrine\ORM\EntityManagerInterface; use PhpList\Core\Domain\Model\Dto\ValidationContext; use PhpList\Core\Domain\Model\Messaging\Dto\CreateTemplateDto; -use PhpList\Core\Domain\Model\Messaging\Dto\UpdateSubscriberDto; use PhpList\Core\Domain\Model\Messaging\Template; +use PhpList\Core\Domain\Model\Subscription\Dto\UpdateSubscriberDto; use PhpList\Core\Domain\Model\Subscription\Subscriber; use PhpList\Core\Domain\Repository\Messaging\TemplateRepository; use PhpList\Core\Domain\Service\Validator\TemplateImageValidator; From 82b254fd5e1a4d909fce2b8f3d8d27ebbe33008c Mon Sep 17 00:00:00 2001 From: Tatevik Date: Sat, 10 May 2025 17:49:10 +0400 Subject: [PATCH 04/15] ISSUE-345: config provider --- config/config.yml | 1 - config/packages/app.yml | 10 +++++ config/services.yml | 10 ++--- config/{ => services}/repositories.yml | 0 src/Core/ConfigProvider.php | 23 +++++++++++ src/Domain/Repository/AbstractRepository.php | 2 - tests/Integration/Core/ConfigProviderTest.php | 40 +++++++++++++++++++ 7 files changed, 78 insertions(+), 8 deletions(-) create mode 100644 config/packages/app.yml rename config/{ => services}/repositories.yml (100%) create mode 100644 src/Core/ConfigProvider.php create mode 100644 tests/Integration/Core/ConfigProviderTest.php diff --git a/config/config.yml b/config/config.yml index 7d4125b3..f4cf974a 100644 --- a/config/config.yml +++ b/config/config.yml @@ -1,6 +1,5 @@ imports: - { resource: services.yml } - - { resource: repositories.yml } - { resource: doctrine.yml } # Put parameters here that don't need to change on each machine where the app is deployed diff --git a/config/packages/app.yml b/config/packages/app.yml new file mode 100644 index 00000000..61cf7460 --- /dev/null +++ b/config/packages/app.yml @@ -0,0 +1,10 @@ +app: + config: + message_from_address: 'news@example.com' + admin_address: 'admin@example.com' + default_message_age: 15768000 + message_footer: 'Thanks for reading' + forward_footer: 'Forwarded message' + notify_start_default: 'start@example.com' + notify_end_default: 'end@example.com' + always_add_google_tracking: true diff --git a/config/services.yml b/config/services.yml index 19b58a94..80369fd5 100644 --- a/config/services.yml +++ b/config/services.yml @@ -1,16 +1,16 @@ imports: - { resource: 'services/*.yml' } + services: - # default configuration for services in *this* file _defaults: - # automatically injects dependencies in your services autowire: true - # automatically registers your services as commands, event subscribers, etc. autoconfigure: true - # this means you cannot fetch services directly from the container via $container->get() - # if you need to do this, you can override this setting on individual services public: false + PhpList\Core\Core\ConfigProvider: + arguments: + $config: '%app.config%' + PhpList\Core\Core\ApplicationStructure: public: true diff --git a/config/repositories.yml b/config/services/repositories.yml similarity index 100% rename from config/repositories.yml rename to config/services/repositories.yml diff --git a/src/Core/ConfigProvider.php b/src/Core/ConfigProvider.php new file mode 100644 index 00000000..6df52592 --- /dev/null +++ b/src/Core/ConfigProvider.php @@ -0,0 +1,23 @@ +config[$key] ?? $default; + } + + public function all(): array + { + return $this->config; + } +} + diff --git a/src/Domain/Repository/AbstractRepository.php b/src/Domain/Repository/AbstractRepository.php index 18328e49..b5d56385 100644 --- a/src/Domain/Repository/AbstractRepository.php +++ b/src/Domain/Repository/AbstractRepository.php @@ -15,8 +15,6 @@ */ abstract class AbstractRepository extends EntityRepository { - protected ?string $alias = null; - /** * Persists $model and flushes the entity manager change list. * diff --git a/tests/Integration/Core/ConfigProviderTest.php b/tests/Integration/Core/ConfigProviderTest.php new file mode 100644 index 00000000..d2fdf896 --- /dev/null +++ b/tests/Integration/Core/ConfigProviderTest.php @@ -0,0 +1,40 @@ + 'phpList', + 'debug' => true, + ]); + + $this->assertSame('phpList', $provider->get('site_name')); + $this->assertTrue($provider->get('debug')); + } + + public function testReturnsDefaultIfKeyMissing(): void + { + $provider = new ConfigProvider([ + 'site_name' => 'phpList', + ]); + + $this->assertNull($provider->get('nonexistent')); + $this->assertSame('default', $provider->get('nonexistent', 'default')); + } + + public function testReturnsAllConfig(): void + { + $data = ['a' => 1, 'b' => 2]; + $provider = new ConfigProvider($data); + + $this->assertSame($data, $provider->all()); + } +} From b54858178737cd372c78deace30011120d6be657 Mon Sep 17 00:00:00 2001 From: Tatevik Date: Sat, 10 May 2025 20:49:29 +0400 Subject: [PATCH 05/15] ISSUE-345: attribute definition --- config/services/repositories.yml | 10 +++ .../AttributeDefinitionCreationException.php | 23 ++++++ ...Definition.php => AttributeDefinition.php} | 6 +- .../Dto/AttributeDefinitionDto.php | 15 ++++ .../Subscription/Dto/CreateSubscriberDto.php | 2 +- .../Subscription/SubscriberAttribute.php | 8 +- ....php => AttributeDefinitionRepository.php} | 6 +- .../Manager/AttributeDefinitionManager.php | 77 +++++++++++++++++++ 8 files changed, 138 insertions(+), 9 deletions(-) create mode 100644 src/Domain/Exception/AttributeDefinitionCreationException.php rename src/Domain/Model/Subscription/{SubscriberAttributeDefinition.php => AttributeDefinition.php} (91%) create mode 100644 src/Domain/Model/Subscription/Dto/AttributeDefinitionDto.php rename src/Domain/Repository/Subscription/{SubscriberAttributeDefinitionRepository.php => AttributeDefinitionRepository.php} (56%) create mode 100644 src/Domain/Service/Manager/AttributeDefinitionManager.php diff --git a/config/services/repositories.yml b/config/services/repositories.yml index 2373b0dc..de76c269 100644 --- a/config/services/repositories.yml +++ b/config/services/repositories.yml @@ -21,6 +21,16 @@ services: arguments: - PhpList\Core\Domain\Model\Subscription\Subscriber + PhpList\Core\Domain\Repository\Subscription\SubscriberAttributeRepository: + parent: PhpList\Core\Domain\Repository + arguments: + - PhpList\Core\Domain\Model\Subscription\SubscriberAttribute + + PhpList\Core\Domain\Repository\Subscription\AttributeDefinitionRepository: + parent: PhpList\Core\Domain\Repository + arguments: + - PhpList\Core\Domain\Model\Subscription\AttributeDefinition + PhpList\Core\Domain\Repository\Subscription\SubscriptionRepository: parent: PhpList\Core\Domain\Repository arguments: diff --git a/src/Domain/Exception/AttributeDefinitionCreationException.php b/src/Domain/Exception/AttributeDefinitionCreationException.php new file mode 100644 index 00000000..03699945 --- /dev/null +++ b/src/Domain/Exception/AttributeDefinitionCreationException.php @@ -0,0 +1,23 @@ +statusCode = $statusCode; + } + + public function getStatusCode(): int + { + return $this->statusCode; + } +} diff --git a/src/Domain/Model/Subscription/SubscriberAttributeDefinition.php b/src/Domain/Model/Subscription/AttributeDefinition.php similarity index 91% rename from src/Domain/Model/Subscription/SubscriberAttributeDefinition.php rename to src/Domain/Model/Subscription/AttributeDefinition.php index 9c9f51b4..40fff036 100644 --- a/src/Domain/Model/Subscription/SubscriberAttributeDefinition.php +++ b/src/Domain/Model/Subscription/AttributeDefinition.php @@ -7,13 +7,13 @@ use Doctrine\ORM\Mapping as ORM; use PhpList\Core\Domain\Model\Interfaces\DomainModel; use PhpList\Core\Domain\Model\Interfaces\Identity; -use PhpList\Core\Domain\Repository\Subscription\SubscriberAttributeDefinitionRepository; +use PhpList\Core\Domain\Repository\Subscription\AttributeDefinitionRepository; -#[ORM\Entity(repositoryClass: SubscriberAttributeDefinitionRepository::class)] +#[ORM\Entity(repositoryClass: AttributeDefinitionRepository::class)] #[ORM\Table(name: 'phplist_user_attribute')] #[ORM\Index(name: 'idnameindex', columns: ['id', 'name'])] #[ORM\Index(name: 'nameindex', columns: ['name'])] -class SubscriberAttributeDefinition implements DomainModel, Identity +class AttributeDefinition implements DomainModel, Identity { #[ORM\Id] #[ORM\Column(type: 'integer')] diff --git a/src/Domain/Model/Subscription/Dto/AttributeDefinitionDto.php b/src/Domain/Model/Subscription/Dto/AttributeDefinitionDto.php new file mode 100644 index 00000000..a1315846 --- /dev/null +++ b/src/Domain/Model/Subscription/Dto/AttributeDefinitionDto.php @@ -0,0 +1,15 @@ +attributeDefinition = $attributeDefinition; $this->subscriber = $subscriber; } - public function getAttributeDefinition(): SubscriberAttributeDefinition + public function getAttributeDefinition(): AttributeDefinition { return $this->attributeDefinition; } diff --git a/src/Domain/Repository/Subscription/SubscriberAttributeDefinitionRepository.php b/src/Domain/Repository/Subscription/AttributeDefinitionRepository.php similarity index 56% rename from src/Domain/Repository/Subscription/SubscriberAttributeDefinitionRepository.php rename to src/Domain/Repository/Subscription/AttributeDefinitionRepository.php index eac62fca..86fa23e1 100644 --- a/src/Domain/Repository/Subscription/SubscriberAttributeDefinitionRepository.php +++ b/src/Domain/Repository/Subscription/AttributeDefinitionRepository.php @@ -4,11 +4,15 @@ namespace PhpList\Core\Domain\Repository\Subscription; +use PhpList\Core\Domain\Model\Subscription\AttributeDefinition; use PhpList\Core\Domain\Repository\AbstractRepository; use PhpList\Core\Domain\Repository\CursorPaginationTrait; use PhpList\Core\Domain\Repository\Interfaces\PaginatableRepositoryInterface; -class SubscriberAttributeDefinitionRepository extends AbstractRepository implements PaginatableRepositoryInterface +/** + * @method AttributeDefinition|null findOneByName(string $name) + */ +class AttributeDefinitionRepository extends AbstractRepository implements PaginatableRepositoryInterface { use CursorPaginationTrait; } diff --git a/src/Domain/Service/Manager/AttributeDefinitionManager.php b/src/Domain/Service/Manager/AttributeDefinitionManager.php new file mode 100644 index 00000000..b8bea8cf --- /dev/null +++ b/src/Domain/Service/Manager/AttributeDefinitionManager.php @@ -0,0 +1,77 @@ +attributeDefinitionRepository = $attributeDefinitionRepository; + } + + public function create(AttributeDefinitionDto $attributeDefinitionDto): AttributeDefinition + { + $existingAttribute = $this->attributeDefinitionRepository->findOneByName($attributeDefinitionDto->name); + if ($existingAttribute) { + throw new AttributeDefinitionCreationException('Attribute definition already exists', 409); + } + + $attributeDefinition = (new AttributeDefinition()) + ->setName($attributeDefinitionDto->name) + ->setType($attributeDefinitionDto->type) + ->setListOrder($attributeDefinitionDto->listOrder) + ->setRequired($attributeDefinitionDto->required) + ->setDefaultValue($attributeDefinitionDto->defaultValue) + ->setTableName($attributeDefinitionDto->tableName); + + $this->attributeDefinitionRepository->save($attributeDefinition); + + return $attributeDefinition; + } + + public function update( + AttributeDefinition $attributeDefinition, + AttributeDefinitionDto $attributeDefinitionDto + ): AttributeDefinition { + $existingAttribute = $this->attributeDefinitionRepository->findOneByName($attributeDefinitionDto->name); + if ($existingAttribute && $existingAttribute->getId() !== $attributeDefinition->getId()) { + throw new AttributeDefinitionCreationException('Another attribute with this name already exists.', 409); + } + + $attributeDefinition + ->setName($attributeDefinitionDto->name) + ->setType($attributeDefinitionDto->type) + ->setListOrder($attributeDefinitionDto->listOrder) + ->setRequired($attributeDefinitionDto->required) + ->setDefaultValue($attributeDefinitionDto->defaultValue) + ->setTableName($attributeDefinitionDto->tableName); + + $this->attributeDefinitionRepository->save($attributeDefinition); + + return $attributeDefinition; + } + + public function delete(AttributeDefinition $attributeDefinition): void + { + $this->attributeDefinitionRepository->remove($attributeDefinition); + } + + public function getTotalCount(): int + { + return $this->attributeDefinitionRepository->count(); + } + + public function getAttributesAfterId(int $afterId, int $limit): array + { + return $this->attributeDefinitionRepository->getAfterId($afterId, $limit); + } +} From eaa4860ca29083672eb147aa4421b2c98eae33e7 Mon Sep 17 00:00:00 2001 From: Tatevik Date: Sat, 10 May 2025 22:02:04 +0400 Subject: [PATCH 06/15] ISSUE-345: subscriber attribute --- config/services/managers.yml | 8 + src/Core/ConfigProvider.php | 1 - .../SubscriberAttributeCreationException.php | 23 +++ .../Dto/AttributeDefinitionDto.php | 18 +- .../Dto/SubscriberAttributeDto.php | 15 ++ .../AttributeDefinitionRepository.php | 8 +- .../SubscriberAttributeRepository.php | 27 +++ .../Manager/AttributeDefinitionManager.php | 20 +-- .../Manager/SubscriberAttributeManager.php | 64 +++++++ .../AttributeDefinitionManagerTest.php | 151 ++++++++++++++++ .../SubscriberAttributeManagerTest.php | 167 ++++++++++++++++++ 11 files changed, 482 insertions(+), 20 deletions(-) create mode 100644 src/Domain/Exception/SubscriberAttributeCreationException.php create mode 100644 src/Domain/Model/Subscription/Dto/SubscriberAttributeDto.php create mode 100644 src/Domain/Service/Manager/SubscriberAttributeManager.php create mode 100644 tests/Unit/Domain/Service/Manager/AttributeDefinitionManagerTest.php create mode 100644 tests/Unit/Domain/Service/Manager/SubscriberAttributeManagerTest.php diff --git a/config/services/managers.yml b/config/services/managers.yml index 1bf93802..4fc14a76 100644 --- a/config/services/managers.yml +++ b/config/services/managers.yml @@ -35,3 +35,11 @@ services: PhpList\Core\Domain\Service\Manager\AdministratorManager: autowire: true autoconfigure: true + + PhpList\Core\Domain\Service\Manager\AttributeDefinitionManager: + autowire: true + autoconfigure: true + + PhpList\Core\Domain\Service\Manager\SubscriberAttributeManager: + autowire: true + autoconfigure: true diff --git a/src/Core/ConfigProvider.php b/src/Core/ConfigProvider.php index 6df52592..b78f365f 100644 --- a/src/Core/ConfigProvider.php +++ b/src/Core/ConfigProvider.php @@ -20,4 +20,3 @@ public function all(): array return $this->config; } } - diff --git a/src/Domain/Exception/SubscriberAttributeCreationException.php b/src/Domain/Exception/SubscriberAttributeCreationException.php new file mode 100644 index 00000000..66367e90 --- /dev/null +++ b/src/Domain/Exception/SubscriberAttributeCreationException.php @@ -0,0 +1,23 @@ +statusCode = $statusCode; + } + + public function getStatusCode(): int + { + return $this->statusCode; + } +} diff --git a/src/Domain/Model/Subscription/Dto/AttributeDefinitionDto.php b/src/Domain/Model/Subscription/Dto/AttributeDefinitionDto.php index a1315846..ca89d352 100644 --- a/src/Domain/Model/Subscription/Dto/AttributeDefinitionDto.php +++ b/src/Domain/Model/Subscription/Dto/AttributeDefinitionDto.php @@ -6,10 +6,16 @@ class AttributeDefinitionDto { - public string $name; - public ?string $type = null; - public ?int $listOrder = null; - public ?string $defaultValue = null; - public ?bool $required = null; - public ?string $tableName = null; + /** + * @SuppressWarnings("BooleanArgumentFlag") + */ + public function __construct( + public readonly string $name, + public readonly ?string $type = null, + public readonly ?int $listOrder = null, + public readonly ?string $defaultValue = null, + public readonly ?bool $required = false, + public readonly ?string $tableName = null, + ) { + } } diff --git a/src/Domain/Model/Subscription/Dto/SubscriberAttributeDto.php b/src/Domain/Model/Subscription/Dto/SubscriberAttributeDto.php new file mode 100644 index 00000000..5b39543d --- /dev/null +++ b/src/Domain/Model/Subscription/Dto/SubscriberAttributeDto.php @@ -0,0 +1,15 @@ +findOneBy(['name' => $name]); + } } diff --git a/src/Domain/Repository/Subscription/SubscriberAttributeRepository.php b/src/Domain/Repository/Subscription/SubscriberAttributeRepository.php index 7c60a2d8..1da20654 100644 --- a/src/Domain/Repository/Subscription/SubscriberAttributeRepository.php +++ b/src/Domain/Repository/Subscription/SubscriberAttributeRepository.php @@ -4,8 +4,35 @@ namespace PhpList\Core\Domain\Repository\Subscription; +use PhpList\Core\Domain\Model\Subscription\AttributeDefinition; +use PhpList\Core\Domain\Model\Subscription\Subscriber; +use PhpList\Core\Domain\Model\Subscription\SubscriberAttribute; use PhpList\Core\Domain\Repository\AbstractRepository; class SubscriberAttributeRepository extends AbstractRepository { + public function findOneBySubscriberAndAttribute( + Subscriber $subscriber, + AttributeDefinition $attributeDefinition + ): ?SubscriberAttribute { + return $this->findOneBy([ + 'subscriber' => $subscriber, + 'attributeDefinition' => $attributeDefinition, + ]); + } + + public function findOneBySubscriberIdAndAttributeId( + int $subscriberId, + int $attributeDefinitionId + ): ?SubscriberAttribute { + return $this->createQueryBuilder('sa') + ->join('sa.subscriber', 's') + ->join('sa.attributeDefinition', 'ad') + ->where('s.id = :subscriberId') + ->andWhere('ad.id = :attributeDefinitionId') + ->setParameter('subscriberId', $subscriberId) + ->setParameter('attributeDefinitionId', $attributeDefinitionId) + ->getQuery() + ->getOneOrNullResult(); + } } diff --git a/src/Domain/Service/Manager/AttributeDefinitionManager.php b/src/Domain/Service/Manager/AttributeDefinitionManager.php index b8bea8cf..c9b2f871 100644 --- a/src/Domain/Service/Manager/AttributeDefinitionManager.php +++ b/src/Domain/Service/Manager/AttributeDefinitionManager.php @@ -11,16 +11,16 @@ class AttributeDefinitionManager { - private AttributeDefinitionRepository $attributeDefinitionRepository; + private AttributeDefinitionRepository $definitionRepository; - public function __construct(AttributeDefinitionRepository $attributeDefinitionRepository) + public function __construct(AttributeDefinitionRepository $definitionRepository) { - $this->attributeDefinitionRepository = $attributeDefinitionRepository; + $this->definitionRepository = $definitionRepository; } public function create(AttributeDefinitionDto $attributeDefinitionDto): AttributeDefinition { - $existingAttribute = $this->attributeDefinitionRepository->findOneByName($attributeDefinitionDto->name); + $existingAttribute = $this->definitionRepository->findOneByName($attributeDefinitionDto->name); if ($existingAttribute) { throw new AttributeDefinitionCreationException('Attribute definition already exists', 409); } @@ -33,7 +33,7 @@ public function create(AttributeDefinitionDto $attributeDefinitionDto): Attribut ->setDefaultValue($attributeDefinitionDto->defaultValue) ->setTableName($attributeDefinitionDto->tableName); - $this->attributeDefinitionRepository->save($attributeDefinition); + $this->definitionRepository->save($attributeDefinition); return $attributeDefinition; } @@ -42,7 +42,7 @@ public function update( AttributeDefinition $attributeDefinition, AttributeDefinitionDto $attributeDefinitionDto ): AttributeDefinition { - $existingAttribute = $this->attributeDefinitionRepository->findOneByName($attributeDefinitionDto->name); + $existingAttribute = $this->definitionRepository->findOneByName($attributeDefinitionDto->name); if ($existingAttribute && $existingAttribute->getId() !== $attributeDefinition->getId()) { throw new AttributeDefinitionCreationException('Another attribute with this name already exists.', 409); } @@ -55,23 +55,23 @@ public function update( ->setDefaultValue($attributeDefinitionDto->defaultValue) ->setTableName($attributeDefinitionDto->tableName); - $this->attributeDefinitionRepository->save($attributeDefinition); + $this->definitionRepository->save($attributeDefinition); return $attributeDefinition; } public function delete(AttributeDefinition $attributeDefinition): void { - $this->attributeDefinitionRepository->remove($attributeDefinition); + $this->definitionRepository->remove($attributeDefinition); } public function getTotalCount(): int { - return $this->attributeDefinitionRepository->count(); + return $this->definitionRepository->count(); } public function getAttributesAfterId(int $afterId, int $limit): array { - return $this->attributeDefinitionRepository->getAfterId($afterId, $limit); + return $this->definitionRepository->getAfterId($afterId, $limit); } } diff --git a/src/Domain/Service/Manager/SubscriberAttributeManager.php b/src/Domain/Service/Manager/SubscriberAttributeManager.php new file mode 100644 index 00000000..f4e545aa --- /dev/null +++ b/src/Domain/Service/Manager/SubscriberAttributeManager.php @@ -0,0 +1,64 @@ +definitionRepository = $definitionRepository; + $this->attributeRepository = $attributeRepository; + $this->subscriberRepository = $subscriberRepository; + } + + public function createOrUpdate(SubscriberAttributeDto $dto): SubscriberAttribute + { + $subscriber = $this->subscriberRepository->find($dto->subscriberId); + if (!$subscriber) { + throw new SubscriberAttributeCreationException('Subscriber does not exist', 404); + } + + $attributeDefinition = $this->definitionRepository->find($dto->attributeDefinitionId); + if (!$attributeDefinition) { + throw new SubscriberAttributeCreationException('Attribute definition does not exist', 404); + } + + $subscriberAttribute = $this->attributeRepository + ->findOneBySubscriberAndAttribute($subscriber, $attributeDefinition); + + if (!$subscriberAttribute) { + $subscriberAttribute = new SubscriberAttribute($attributeDefinition, $subscriber); + } + + $subscriberAttribute->setValue($dto->value); + $this->attributeRepository->save($subscriberAttribute); + + return $subscriberAttribute; + } + + public function getSubscriberAttribute(int $subscriberId, int $attributeDefinitionId): SubscriberAttribute + { + return $this->attributeRepository->findOneBySubscriberIdAndAttributeId($subscriberId, $attributeDefinitionId); + } + + public function delete(SubscriberAttribute $attribute): void + { + $this->definitionRepository->remove($attribute); + } +} diff --git a/tests/Unit/Domain/Service/Manager/AttributeDefinitionManagerTest.php b/tests/Unit/Domain/Service/Manager/AttributeDefinitionManagerTest.php new file mode 100644 index 00000000..395504cc --- /dev/null +++ b/tests/Unit/Domain/Service/Manager/AttributeDefinitionManagerTest.php @@ -0,0 +1,151 @@ +createMock(AttributeDefinitionRepository::class); + $manager = new AttributeDefinitionManager($repository); + + $dto = new AttributeDefinitionDto( + name: 'Country', + type: 'checkbox', + listOrder: 1, + defaultValue: 'US', + required: true, + tableName: 'user_attribute' + ); + + $repository->expects($this->once()) + ->method('findOneByName') + ->with('Country') + ->willReturn(null); + + $repository->expects($this->once())->method('save'); + + $attribute = $manager->create($dto); + + $this->assertInstanceOf(AttributeDefinition::class, $attribute); + $this->assertSame('Country', $attribute->getName()); + $this->assertSame('checkbox', $attribute->getType()); + $this->assertSame(1, $attribute->getListOrder()); + $this->assertSame('US', $attribute->getDefaultValue()); + $this->assertTrue($attribute->isRequired()); + $this->assertSame('user_attribute', $attribute->getTableName()); + } + + public function testCreateThrowsWhenAttributeAlreadyExists(): void + { + $repository = $this->createMock(AttributeDefinitionRepository::class); + $manager = new AttributeDefinitionManager($repository); + + $dto = new AttributeDefinitionDto( + name: 'Country', + type: 'checkbox', + listOrder: 1, + defaultValue: 'US', + required: true, + tableName: 'user_attribute' + ); + + $existing = $this->createMock(AttributeDefinition::class); + + $repository->expects($this->once()) + ->method('findOneByName') + ->with('Country') + ->willReturn($existing); + + $this->expectException(AttributeDefinitionCreationException::class); + + $manager->create($dto); + } + + public function testUpdateAttributeDefinition(): void + { + $repository = $this->createMock(AttributeDefinitionRepository::class); + $manager = new AttributeDefinitionManager($repository); + + $attribute = new AttributeDefinition(); + $attribute->setName('Old'); + + $dto = new AttributeDefinitionDto( + name: 'New', + type: 'text', + listOrder: 5, + defaultValue: 'Canada', + required: false, + tableName: 'custom_attrs' + ); + + $repository->expects($this->once()) + ->method('findOneByName') + ->with('New') + ->willReturn(null); + + $repository->expects($this->once())->method('save')->with($attribute); + + $updated = $manager->update($attribute, $dto); + + $this->assertSame('New', $updated->getName()); + $this->assertSame('text', $updated->getType()); + $this->assertSame(5, $updated->getListOrder()); + $this->assertSame('Canada', $updated->getDefaultValue()); + $this->assertFalse($updated->isRequired()); + $this->assertSame('custom_attrs', $updated->getTableName()); + } + + public function testUpdateThrowsWhenAnotherAttributeWithSameNameExists(): void + { + $repository = $this->createMock(AttributeDefinitionRepository::class); + $manager = new AttributeDefinitionManager($repository); + + $dto = new AttributeDefinitionDto( + name: 'Existing', + type: 'text', + listOrder: 5, + defaultValue: 'Canada', + required: false, + tableName: 'custom_attrs' + ); + + $current = new AttributeDefinition(); + $current->setName('Old'); + + $other = $this->createMock(AttributeDefinition::class); + $other->method('getId')->willReturn(999); + + $repository->expects($this->once()) + ->method('findOneByName') + ->with('Existing') + ->willReturn($other); + + $this->expectException(AttributeDefinitionCreationException::class); + + $manager->update($current, $dto); + } + + public function testDeleteAttributeDefinition(): void + { + $repository = $this->createMock(AttributeDefinitionRepository::class); + $manager = new AttributeDefinitionManager($repository); + + $attribute = new AttributeDefinition(); + + $repository->expects($this->once())->method('remove')->with($attribute); + + $manager->delete($attribute); + + $this->assertTrue(true); + } +} diff --git a/tests/Unit/Domain/Service/Manager/SubscriberAttributeManagerTest.php b/tests/Unit/Domain/Service/Manager/SubscriberAttributeManagerTest.php new file mode 100644 index 00000000..9e3cbe12 --- /dev/null +++ b/tests/Unit/Domain/Service/Manager/SubscriberAttributeManagerTest.php @@ -0,0 +1,167 @@ +createMock(SubscriberRepository::class); + $attributeDefRepo = $this->createMock(AttributeDefinitionRepository::class); + $subscriberAttrRepo = $this->createMock(SubscriberAttributeRepository::class); + + $subscriberRepo->expects(self::once()) + ->method('find') + ->with(1) + ->willReturn($subscriber); + + $attributeDefRepo->expects(self::once()) + ->method('find') + ->with(2) + ->willReturn($definition); + + $subscriberAttrRepo->expects(self::once()) + ->method('findOneBySubscriberAndAttribute') + ->with($subscriber, $definition) + ->willReturn(null); + + $subscriberAttrRepo->expects(self::once()) + ->method('save') + ->with(self::callback(function (SubscriberAttribute $attr) { + return $attr->getValue() === 'US'; + })); + + $manager = new SubscriberAttributeManager($attributeDefRepo, $subscriberAttrRepo, $subscriberRepo); + $attribute = $manager->createOrUpdate($dto); + + self::assertInstanceOf(SubscriberAttribute::class, $attribute); + self::assertSame('US', $attribute->getValue()); + } + + public function testUpdateExistingSubscriberAttribute(): void + { + $subscriber = new Subscriber(); + $definition = new AttributeDefinition(); + + $existing = new SubscriberAttribute($definition, $subscriber); + $existing->setValue('Old'); + + $dto = new SubscriberAttributeDto(1, 2, 'Updated'); + + $subscriberRepo = $this->createMock(SubscriberRepository::class); + $attributeDefRepo = $this->createMock(AttributeDefinitionRepository::class); + $subscriberAttrRepo = $this->createMock(SubscriberAttributeRepository::class); + + $subscriberRepo->method('find')->willReturn($subscriber); + $attributeDefRepo->method('find')->willReturn($definition); + + $subscriberAttrRepo->expects(self::once()) + ->method('findOneBySubscriberAndAttribute') + ->with($subscriber, $definition) + ->willReturn($existing); + + $subscriberAttrRepo->expects(self::once())->method('save')->with($existing); + + $manager = new SubscriberAttributeManager($attributeDefRepo, $subscriberAttrRepo, $subscriberRepo); + $result = $manager->createOrUpdate($dto); + + self::assertSame('Updated', $result->getValue()); + } + + public function testCreateFailsIfSubscriberNotFound(): void + { + $subscriberRepo = $this->createMock(SubscriberRepository::class); + $attributeDefRepo = $this->createMock(AttributeDefinitionRepository::class); + $subscriberAttrRepo = $this->createMock(SubscriberAttributeRepository::class); + + $subscriberRepo->method('find')->willReturn(null); + + $dto = new SubscriberAttributeDto(1, 2, 'US'); + + $manager = new SubscriberAttributeManager($attributeDefRepo, $subscriberAttrRepo, $subscriberRepo); + + $this->expectException(SubscriberAttributeCreationException::class); + $this->expectExceptionMessage('Subscriber does not exist'); + + $manager->createOrUpdate($dto); + } + + public function testCreateFailsIfAttributeDefinitionNotFound(): void + { + $subscriber = new Subscriber(); + + $subscriberRepo = $this->createMock(SubscriberRepository::class); + $attributeDefRepo = $this->createMock(AttributeDefinitionRepository::class); + $subscriberAttrRepo = $this->createMock(SubscriberAttributeRepository::class); + + $subscriberRepo->method('find')->willReturn($subscriber); + $attributeDefRepo->method('find')->willReturn(null); + + $dto = new SubscriberAttributeDto(1, 2, 'US'); + + $manager = new SubscriberAttributeManager($attributeDefRepo, $subscriberAttrRepo, $subscriberRepo); + + $this->expectException(SubscriberAttributeCreationException::class); + $this->expectExceptionMessage('Attribute definition does not exist'); + + $manager->createOrUpdate($dto); + } + + public function testGetSubscriberAttribute(): void + { + $expected = new SubscriberAttribute(new AttributeDefinition(), new Subscriber()); + + $subscriberRepo = $this->createMock(SubscriberRepository::class); + $attributeDefRepo = $this->createMock(AttributeDefinitionRepository::class); + $subscriberAttrRepo = $this->createMock(SubscriberAttributeRepository::class); + + $subscriberAttrRepo->expects(self::once()) + ->method('findOneBySubscriberIdAndAttributeId') + ->with(5, 10) + ->willReturn($expected); + + $manager = new SubscriberAttributeManager($attributeDefRepo, $subscriberAttrRepo, $subscriberRepo); + + $result = $manager->getSubscriberAttribute(5, 10); + + self::assertSame($expected, $result); + } + + public function testDeleteSubscriberAttribute(): void + { + $subscriberRepo = $this->createMock(SubscriberRepository::class); + $attributeDefRepo = $this->createMock(AttributeDefinitionRepository::class); + $subscriberAttrRepo = $this->createMock(SubscriberAttributeRepository::class); + + $attribute = $this->createMock(SubscriberAttribute::class); + + $attributeDefRepo->expects(self::once())->method('remove')->with($attribute); + + $manager = new SubscriberAttributeManager($attributeDefRepo, $subscriberAttrRepo, $subscriberRepo); + $manager->delete($attribute); + + self::assertTrue(true); + } +} From 0df04719326e5c7172ff6f1ca7fc1a218ca064f5 Mon Sep 17 00:00:00 2001 From: Tatevik Date: Sun, 11 May 2025 14:30:18 +0400 Subject: [PATCH 07/15] ISSUE-345: naming --- config/services/repositories.yml | 8 ++-- ...ibute.php => AdminAttributeDefinition.php} | 8 ++-- ...teRelation.php => AdminAttributeValue.php} | 6 +-- src/Domain/Model/Subscription/Subscriber.php | 8 ++-- ....php => SubscriberAttributeDefinition.php} | 6 +-- ...ibute.php => SubscriberAttributeValue.php} | 14 +++--- ...=> AdminAttributeDefinitionRepository.php} | 2 +- ....php => AdminAttributeValueRepository.php} | 2 +- ...bscriberAttributeDefinitionRepository.php} | 6 +-- ...=> SubscriberAttributeValueRepository.php} | 12 ++--- .../Manager/AttributeDefinitionManager.php | 20 ++++---- .../Manager/SubscriberAttributeManager.php | 24 +++++----- .../AttributeDefinitionManagerTest.php | 26 +++++------ .../SubscriberAttributeManagerTest.php | 46 +++++++++---------- 14 files changed, 94 insertions(+), 94 deletions(-) rename src/Domain/Model/Identity/{AdminAttribute.php => AdminAttributeDefinition.php} (91%) rename src/Domain/Model/Identity/{AdminAttributeRelation.php => AdminAttributeValue.php} (85%) rename src/Domain/Model/Subscription/{AttributeDefinition.php => SubscriberAttributeDefinition.php} (91%) rename src/Domain/Model/Subscription/{SubscriberAttribute.php => SubscriberAttributeValue.php} (74%) rename src/Domain/Repository/Identity/{AdminAttributeRepository.php => AdminAttributeDefinitionRepository.php} (73%) rename src/Domain/Repository/Identity/{AdminAttributeRelationRepository.php => AdminAttributeValueRepository.php} (68%) rename src/Domain/Repository/Subscription/{AttributeDefinitionRepository.php => SubscriberAttributeDefinitionRepository.php} (58%) rename src/Domain/Repository/Subscription/{SubscriberAttributeRepository.php => SubscriberAttributeValueRepository.php} (74%) diff --git a/config/services/repositories.yml b/config/services/repositories.yml index de76c269..e7193a48 100644 --- a/config/services/repositories.yml +++ b/config/services/repositories.yml @@ -21,15 +21,15 @@ services: arguments: - PhpList\Core\Domain\Model\Subscription\Subscriber - PhpList\Core\Domain\Repository\Subscription\SubscriberAttributeRepository: + PhpList\Core\Domain\Repository\Subscription\SubscriberAttributeValueRepository: parent: PhpList\Core\Domain\Repository arguments: - - PhpList\Core\Domain\Model\Subscription\SubscriberAttribute + - PhpList\Core\Domain\Model\Subscription\SubscriberAttributeValue - PhpList\Core\Domain\Repository\Subscription\AttributeDefinitionRepository: + PhpList\Core\Domain\Repository\Subscription\SubscriberAttributeDefinitionRepository: parent: PhpList\Core\Domain\Repository arguments: - - PhpList\Core\Domain\Model\Subscription\AttributeDefinition + - PhpList\Core\Domain\Model\Subscription\SubscriberAttributeDefinition PhpList\Core\Domain\Repository\Subscription\SubscriptionRepository: parent: PhpList\Core\Domain\Repository diff --git a/src/Domain/Model/Identity/AdminAttribute.php b/src/Domain/Model/Identity/AdminAttributeDefinition.php similarity index 91% rename from src/Domain/Model/Identity/AdminAttribute.php rename to src/Domain/Model/Identity/AdminAttributeDefinition.php index be740895..c39a44a3 100644 --- a/src/Domain/Model/Identity/AdminAttribute.php +++ b/src/Domain/Model/Identity/AdminAttributeDefinition.php @@ -7,12 +7,12 @@ use Doctrine\ORM\Mapping as ORM; use PhpList\Core\Domain\Model\Interfaces\DomainModel; use PhpList\Core\Domain\Model\Interfaces\Identity; -use PhpList\Core\Domain\Repository\Identity\AdminAttributeRepository; +use PhpList\Core\Domain\Repository\Identity\AdminAttributeDefinitionRepository; -#[ORM\Entity(repositoryClass: AdminAttributeRepository::class)] -#[ORM\Table(name: 'phplist_admin_attribute')] +#[ORM\Entity(repositoryClass: AdminAttributeDefinitionRepository::class)] +#[ORM\Table(name: 'phplist_adminattribute')] #[ORM\HasLifecycleCallbacks] -class AdminAttribute implements DomainModel, Identity +class AdminAttributeDefinition implements DomainModel, Identity { #[ORM\Id] #[ORM\Column(type: 'integer')] diff --git a/src/Domain/Model/Identity/AdminAttributeRelation.php b/src/Domain/Model/Identity/AdminAttributeValue.php similarity index 85% rename from src/Domain/Model/Identity/AdminAttributeRelation.php rename to src/Domain/Model/Identity/AdminAttributeValue.php index 454e8800..472dc073 100644 --- a/src/Domain/Model/Identity/AdminAttributeRelation.php +++ b/src/Domain/Model/Identity/AdminAttributeValue.php @@ -6,12 +6,12 @@ use Doctrine\ORM\Mapping as ORM; use PhpList\Core\Domain\Model\Interfaces\DomainModel; -use PhpList\Core\Domain\Repository\Identity\AdminAttributeRelationRepository; +use PhpList\Core\Domain\Repository\Identity\AdminAttributeValueRepository; -#[ORM\Entity(repositoryClass: AdminAttributeRelationRepository::class)] +#[ORM\Entity(repositoryClass: AdminAttributeValueRepository::class)] #[ORM\Table(name: 'phplist_admin_attribute')] #[ORM\HasLifecycleCallbacks] -class AdminAttributeRelation implements DomainModel +class AdminAttributeValue implements DomainModel { #[ORM\Id] #[ORM\Column(name: 'adminattributeid', type: 'integer', options: ['unsigned' => true])] diff --git a/src/Domain/Model/Subscription/Subscriber.php b/src/Domain/Model/Subscription/Subscriber.php index f06972ec..f4c97837 100644 --- a/src/Domain/Model/Subscription/Subscriber.php +++ b/src/Domain/Model/Subscription/Subscriber.php @@ -73,10 +73,10 @@ class Subscriber implements DomainModel, Identity, CreationDate, ModificationDat private Collection $subscriptions; /** - * @var Collection + * @var Collection */ #[ORM\OneToMany( - targetEntity: SubscriberAttribute::class, + targetEntity: SubscriberAttributeValue::class, mappedBy: 'subscriber', cascade: ['persist', 'remove'], orphanRemoval: true @@ -267,7 +267,7 @@ public function getAttributes(): Collection return $this->attributes; } - public function addAttribute(SubscriberAttribute $attribute): self + public function addAttribute(SubscriberAttributeValue $attribute): self { if (!$this->attributes->contains($attribute)) { $this->attributes[] = $attribute; @@ -276,7 +276,7 @@ public function addAttribute(SubscriberAttribute $attribute): self return $this; } - public function removeAttribute(SubscriberAttribute $attribute): self + public function removeAttribute(SubscriberAttributeValue $attribute): self { $this->attributes->removeElement($attribute); return $this; diff --git a/src/Domain/Model/Subscription/AttributeDefinition.php b/src/Domain/Model/Subscription/SubscriberAttributeDefinition.php similarity index 91% rename from src/Domain/Model/Subscription/AttributeDefinition.php rename to src/Domain/Model/Subscription/SubscriberAttributeDefinition.php index 40fff036..9c9f51b4 100644 --- a/src/Domain/Model/Subscription/AttributeDefinition.php +++ b/src/Domain/Model/Subscription/SubscriberAttributeDefinition.php @@ -7,13 +7,13 @@ use Doctrine\ORM\Mapping as ORM; use PhpList\Core\Domain\Model\Interfaces\DomainModel; use PhpList\Core\Domain\Model\Interfaces\Identity; -use PhpList\Core\Domain\Repository\Subscription\AttributeDefinitionRepository; +use PhpList\Core\Domain\Repository\Subscription\SubscriberAttributeDefinitionRepository; -#[ORM\Entity(repositoryClass: AttributeDefinitionRepository::class)] +#[ORM\Entity(repositoryClass: SubscriberAttributeDefinitionRepository::class)] #[ORM\Table(name: 'phplist_user_attribute')] #[ORM\Index(name: 'idnameindex', columns: ['id', 'name'])] #[ORM\Index(name: 'nameindex', columns: ['name'])] -class AttributeDefinition implements DomainModel, Identity +class SubscriberAttributeDefinition implements DomainModel, Identity { #[ORM\Id] #[ORM\Column(type: 'integer')] diff --git a/src/Domain/Model/Subscription/SubscriberAttribute.php b/src/Domain/Model/Subscription/SubscriberAttributeValue.php similarity index 74% rename from src/Domain/Model/Subscription/SubscriberAttribute.php rename to src/Domain/Model/Subscription/SubscriberAttributeValue.php index 7dfd7670..2dbffe45 100644 --- a/src/Domain/Model/Subscription/SubscriberAttribute.php +++ b/src/Domain/Model/Subscription/SubscriberAttributeValue.php @@ -6,19 +6,19 @@ use Doctrine\ORM\Mapping as ORM; use PhpList\Core\Domain\Model\Interfaces\DomainModel; -use PhpList\Core\Domain\Repository\Subscription\SubscriberAttributeRepository; +use PhpList\Core\Domain\Repository\Subscription\SubscriberAttributeValueRepository; -#[ORM\Entity(repositoryClass: SubscriberAttributeRepository::class)] +#[ORM\Entity(repositoryClass: SubscriberAttributeValueRepository::class)] #[ORM\Table(name: 'phplist_user_user_attribute')] #[ORM\Index(name: 'attindex', columns: ['attributeid'])] #[ORM\Index(name: 'attuserid', columns: ['userid', 'attributeid'])] #[ORM\Index(name: 'userindex', columns: ['userid'])] -class SubscriberAttribute implements DomainModel +class SubscriberAttributeValue implements DomainModel { #[ORM\Id] - #[ORM\ManyToOne(targetEntity: AttributeDefinition::class)] + #[ORM\ManyToOne(targetEntity: SubscriberAttributeDefinition::class)] #[ORM\JoinColumn(name: 'attributeid', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')] - private AttributeDefinition $attributeDefinition; + private SubscriberAttributeDefinition $attributeDefinition; #[ORM\Id] #[ORM\ManyToOne(targetEntity: Subscriber::class, inversedBy: 'attributes')] @@ -28,13 +28,13 @@ class SubscriberAttribute implements DomainModel #[ORM\Column(name: 'value', type: 'text', nullable: true)] private ?string $value = null; - public function __construct(AttributeDefinition $attributeDefinition, Subscriber $subscriber) + public function __construct(SubscriberAttributeDefinition $attributeDefinition, Subscriber $subscriber) { $this->attributeDefinition = $attributeDefinition; $this->subscriber = $subscriber; } - public function getAttributeDefinition(): AttributeDefinition + public function getAttributeDefinition(): SubscriberAttributeDefinition { return $this->attributeDefinition; } diff --git a/src/Domain/Repository/Identity/AdminAttributeRepository.php b/src/Domain/Repository/Identity/AdminAttributeDefinitionRepository.php similarity index 73% rename from src/Domain/Repository/Identity/AdminAttributeRepository.php rename to src/Domain/Repository/Identity/AdminAttributeDefinitionRepository.php index c47797ba..67c55878 100644 --- a/src/Domain/Repository/Identity/AdminAttributeRepository.php +++ b/src/Domain/Repository/Identity/AdminAttributeDefinitionRepository.php @@ -8,7 +8,7 @@ use PhpList\Core\Domain\Repository\CursorPaginationTrait; use PhpList\Core\Domain\Repository\Interfaces\PaginatableRepositoryInterface; -class AdminAttributeRepository extends AbstractRepository implements PaginatableRepositoryInterface +class AdminAttributeDefinitionRepository extends AbstractRepository implements PaginatableRepositoryInterface { use CursorPaginationTrait; } diff --git a/src/Domain/Repository/Identity/AdminAttributeRelationRepository.php b/src/Domain/Repository/Identity/AdminAttributeValueRepository.php similarity index 68% rename from src/Domain/Repository/Identity/AdminAttributeRelationRepository.php rename to src/Domain/Repository/Identity/AdminAttributeValueRepository.php index 568cb66d..f9a047f9 100644 --- a/src/Domain/Repository/Identity/AdminAttributeRelationRepository.php +++ b/src/Domain/Repository/Identity/AdminAttributeValueRepository.php @@ -6,6 +6,6 @@ use PhpList\Core\Domain\Repository\AbstractRepository; -class AdminAttributeRelationRepository extends AbstractRepository +class AdminAttributeValueRepository extends AbstractRepository { } diff --git a/src/Domain/Repository/Subscription/AttributeDefinitionRepository.php b/src/Domain/Repository/Subscription/SubscriberAttributeDefinitionRepository.php similarity index 58% rename from src/Domain/Repository/Subscription/AttributeDefinitionRepository.php rename to src/Domain/Repository/Subscription/SubscriberAttributeDefinitionRepository.php index 630ce37a..af9b1146 100644 --- a/src/Domain/Repository/Subscription/AttributeDefinitionRepository.php +++ b/src/Domain/Repository/Subscription/SubscriberAttributeDefinitionRepository.php @@ -4,16 +4,16 @@ namespace PhpList\Core\Domain\Repository\Subscription; -use PhpList\Core\Domain\Model\Subscription\AttributeDefinition; +use PhpList\Core\Domain\Model\Subscription\SubscriberAttributeDefinition; use PhpList\Core\Domain\Repository\AbstractRepository; use PhpList\Core\Domain\Repository\CursorPaginationTrait; use PhpList\Core\Domain\Repository\Interfaces\PaginatableRepositoryInterface; -class AttributeDefinitionRepository extends AbstractRepository implements PaginatableRepositoryInterface +class SubscriberAttributeDefinitionRepository extends AbstractRepository implements PaginatableRepositoryInterface { use CursorPaginationTrait; - public function findOneByName(string $name): ?AttributeDefinition + public function findOneByName(string $name): ?SubscriberAttributeDefinition { return $this->findOneBy(['name' => $name]); } diff --git a/src/Domain/Repository/Subscription/SubscriberAttributeRepository.php b/src/Domain/Repository/Subscription/SubscriberAttributeValueRepository.php similarity index 74% rename from src/Domain/Repository/Subscription/SubscriberAttributeRepository.php rename to src/Domain/Repository/Subscription/SubscriberAttributeValueRepository.php index 1da20654..893e6874 100644 --- a/src/Domain/Repository/Subscription/SubscriberAttributeRepository.php +++ b/src/Domain/Repository/Subscription/SubscriberAttributeValueRepository.php @@ -4,17 +4,17 @@ namespace PhpList\Core\Domain\Repository\Subscription; -use PhpList\Core\Domain\Model\Subscription\AttributeDefinition; +use PhpList\Core\Domain\Model\Subscription\SubscriberAttributeDefinition; use PhpList\Core\Domain\Model\Subscription\Subscriber; -use PhpList\Core\Domain\Model\Subscription\SubscriberAttribute; +use PhpList\Core\Domain\Model\Subscription\SubscriberAttributeValue; use PhpList\Core\Domain\Repository\AbstractRepository; -class SubscriberAttributeRepository extends AbstractRepository +class SubscriberAttributeValueRepository extends AbstractRepository { public function findOneBySubscriberAndAttribute( Subscriber $subscriber, - AttributeDefinition $attributeDefinition - ): ?SubscriberAttribute { + SubscriberAttributeDefinition $attributeDefinition + ): ?SubscriberAttributeValue { return $this->findOneBy([ 'subscriber' => $subscriber, 'attributeDefinition' => $attributeDefinition, @@ -24,7 +24,7 @@ public function findOneBySubscriberAndAttribute( public function findOneBySubscriberIdAndAttributeId( int $subscriberId, int $attributeDefinitionId - ): ?SubscriberAttribute { + ): ?SubscriberAttributeValue { return $this->createQueryBuilder('sa') ->join('sa.subscriber', 's') ->join('sa.attributeDefinition', 'ad') diff --git a/src/Domain/Service/Manager/AttributeDefinitionManager.php b/src/Domain/Service/Manager/AttributeDefinitionManager.php index c9b2f871..238234a8 100644 --- a/src/Domain/Service/Manager/AttributeDefinitionManager.php +++ b/src/Domain/Service/Manager/AttributeDefinitionManager.php @@ -5,27 +5,27 @@ namespace PhpList\Core\Domain\Service\Manager; use PhpList\Core\Domain\Exception\AttributeDefinitionCreationException; -use PhpList\Core\Domain\Model\Subscription\AttributeDefinition; +use PhpList\Core\Domain\Model\Subscription\SubscriberAttributeDefinition; use PhpList\Core\Domain\Model\Subscription\Dto\AttributeDefinitionDto; -use PhpList\Core\Domain\Repository\Subscription\AttributeDefinitionRepository; +use PhpList\Core\Domain\Repository\Subscription\SubscriberAttributeDefinitionRepository; class AttributeDefinitionManager { - private AttributeDefinitionRepository $definitionRepository; + private SubscriberAttributeDefinitionRepository $definitionRepository; - public function __construct(AttributeDefinitionRepository $definitionRepository) + public function __construct(SubscriberAttributeDefinitionRepository $definitionRepository) { $this->definitionRepository = $definitionRepository; } - public function create(AttributeDefinitionDto $attributeDefinitionDto): AttributeDefinition + public function create(AttributeDefinitionDto $attributeDefinitionDto): SubscriberAttributeDefinition { $existingAttribute = $this->definitionRepository->findOneByName($attributeDefinitionDto->name); if ($existingAttribute) { throw new AttributeDefinitionCreationException('Attribute definition already exists', 409); } - $attributeDefinition = (new AttributeDefinition()) + $attributeDefinition = (new SubscriberAttributeDefinition()) ->setName($attributeDefinitionDto->name) ->setType($attributeDefinitionDto->type) ->setListOrder($attributeDefinitionDto->listOrder) @@ -39,9 +39,9 @@ public function create(AttributeDefinitionDto $attributeDefinitionDto): Attribut } public function update( - AttributeDefinition $attributeDefinition, - AttributeDefinitionDto $attributeDefinitionDto - ): AttributeDefinition { + SubscriberAttributeDefinition $attributeDefinition, + AttributeDefinitionDto $attributeDefinitionDto + ): SubscriberAttributeDefinition { $existingAttribute = $this->definitionRepository->findOneByName($attributeDefinitionDto->name); if ($existingAttribute && $existingAttribute->getId() !== $attributeDefinition->getId()) { throw new AttributeDefinitionCreationException('Another attribute with this name already exists.', 409); @@ -60,7 +60,7 @@ public function update( return $attributeDefinition; } - public function delete(AttributeDefinition $attributeDefinition): void + public function delete(SubscriberAttributeDefinition $attributeDefinition): void { $this->definitionRepository->remove($attributeDefinition); } diff --git a/src/Domain/Service/Manager/SubscriberAttributeManager.php b/src/Domain/Service/Manager/SubscriberAttributeManager.php index f4e545aa..2bd93400 100644 --- a/src/Domain/Service/Manager/SubscriberAttributeManager.php +++ b/src/Domain/Service/Manager/SubscriberAttributeManager.php @@ -6,28 +6,28 @@ use PhpList\Core\Domain\Exception\SubscriberAttributeCreationException; use PhpList\Core\Domain\Model\Subscription\Dto\SubscriberAttributeDto; -use PhpList\Core\Domain\Model\Subscription\SubscriberAttribute; -use PhpList\Core\Domain\Repository\Subscription\AttributeDefinitionRepository; -use PhpList\Core\Domain\Repository\Subscription\SubscriberAttributeRepository; +use PhpList\Core\Domain\Model\Subscription\SubscriberAttributeValue; +use PhpList\Core\Domain\Repository\Subscription\SubscriberAttributeDefinitionRepository; +use PhpList\Core\Domain\Repository\Subscription\SubscriberAttributeValueRepository; use PhpList\Core\Domain\Repository\Subscription\SubscriberRepository; class SubscriberAttributeManager { - private AttributeDefinitionRepository $definitionRepository; - private SubscriberAttributeRepository $attributeRepository; + private SubscriberAttributeDefinitionRepository $definitionRepository; + private SubscriberAttributeValueRepository $attributeRepository; private SubscriberRepository $subscriberRepository; public function __construct( - AttributeDefinitionRepository $definitionRepository, - SubscriberAttributeRepository $attributeRepository, - SubscriberRepository $subscriberRepository, + SubscriberAttributeDefinitionRepository $definitionRepository, + SubscriberAttributeValueRepository $attributeRepository, + SubscriberRepository $subscriberRepository, ) { $this->definitionRepository = $definitionRepository; $this->attributeRepository = $attributeRepository; $this->subscriberRepository = $subscriberRepository; } - public function createOrUpdate(SubscriberAttributeDto $dto): SubscriberAttribute + public function createOrUpdate(SubscriberAttributeDto $dto): SubscriberAttributeValue { $subscriber = $this->subscriberRepository->find($dto->subscriberId); if (!$subscriber) { @@ -43,7 +43,7 @@ public function createOrUpdate(SubscriberAttributeDto $dto): SubscriberAttribute ->findOneBySubscriberAndAttribute($subscriber, $attributeDefinition); if (!$subscriberAttribute) { - $subscriberAttribute = new SubscriberAttribute($attributeDefinition, $subscriber); + $subscriberAttribute = new SubscriberAttributeValue($attributeDefinition, $subscriber); } $subscriberAttribute->setValue($dto->value); @@ -52,12 +52,12 @@ public function createOrUpdate(SubscriberAttributeDto $dto): SubscriberAttribute return $subscriberAttribute; } - public function getSubscriberAttribute(int $subscriberId, int $attributeDefinitionId): SubscriberAttribute + public function getSubscriberAttribute(int $subscriberId, int $attributeDefinitionId): SubscriberAttributeValue { return $this->attributeRepository->findOneBySubscriberIdAndAttributeId($subscriberId, $attributeDefinitionId); } - public function delete(SubscriberAttribute $attribute): void + public function delete(SubscriberAttributeValue $attribute): void { $this->definitionRepository->remove($attribute); } diff --git a/tests/Unit/Domain/Service/Manager/AttributeDefinitionManagerTest.php b/tests/Unit/Domain/Service/Manager/AttributeDefinitionManagerTest.php index 395504cc..d8783e6a 100644 --- a/tests/Unit/Domain/Service/Manager/AttributeDefinitionManagerTest.php +++ b/tests/Unit/Domain/Service/Manager/AttributeDefinitionManagerTest.php @@ -5,9 +5,9 @@ namespace PhpList\Core\Tests\Unit\Domain\Service\Manager; use PhpList\Core\Domain\Exception\AttributeDefinitionCreationException; -use PhpList\Core\Domain\Model\Subscription\AttributeDefinition; +use PhpList\Core\Domain\Model\Subscription\SubscriberAttributeDefinition; use PhpList\Core\Domain\Model\Subscription\Dto\AttributeDefinitionDto; -use PhpList\Core\Domain\Repository\Subscription\AttributeDefinitionRepository; +use PhpList\Core\Domain\Repository\Subscription\SubscriberAttributeDefinitionRepository; use PhpList\Core\Domain\Service\Manager\AttributeDefinitionManager; use PHPUnit\Framework\TestCase; @@ -15,7 +15,7 @@ class AttributeDefinitionManagerTest extends TestCase { public function testCreateAttributeDefinition(): void { - $repository = $this->createMock(AttributeDefinitionRepository::class); + $repository = $this->createMock(SubscriberAttributeDefinitionRepository::class); $manager = new AttributeDefinitionManager($repository); $dto = new AttributeDefinitionDto( @@ -36,7 +36,7 @@ public function testCreateAttributeDefinition(): void $attribute = $manager->create($dto); - $this->assertInstanceOf(AttributeDefinition::class, $attribute); + $this->assertInstanceOf(SubscriberAttributeDefinition::class, $attribute); $this->assertSame('Country', $attribute->getName()); $this->assertSame('checkbox', $attribute->getType()); $this->assertSame(1, $attribute->getListOrder()); @@ -47,7 +47,7 @@ public function testCreateAttributeDefinition(): void public function testCreateThrowsWhenAttributeAlreadyExists(): void { - $repository = $this->createMock(AttributeDefinitionRepository::class); + $repository = $this->createMock(SubscriberAttributeDefinitionRepository::class); $manager = new AttributeDefinitionManager($repository); $dto = new AttributeDefinitionDto( @@ -59,7 +59,7 @@ public function testCreateThrowsWhenAttributeAlreadyExists(): void tableName: 'user_attribute' ); - $existing = $this->createMock(AttributeDefinition::class); + $existing = $this->createMock(SubscriberAttributeDefinition::class); $repository->expects($this->once()) ->method('findOneByName') @@ -73,10 +73,10 @@ public function testCreateThrowsWhenAttributeAlreadyExists(): void public function testUpdateAttributeDefinition(): void { - $repository = $this->createMock(AttributeDefinitionRepository::class); + $repository = $this->createMock(SubscriberAttributeDefinitionRepository::class); $manager = new AttributeDefinitionManager($repository); - $attribute = new AttributeDefinition(); + $attribute = new SubscriberAttributeDefinition(); $attribute->setName('Old'); $dto = new AttributeDefinitionDto( @@ -107,7 +107,7 @@ public function testUpdateAttributeDefinition(): void public function testUpdateThrowsWhenAnotherAttributeWithSameNameExists(): void { - $repository = $this->createMock(AttributeDefinitionRepository::class); + $repository = $this->createMock(SubscriberAttributeDefinitionRepository::class); $manager = new AttributeDefinitionManager($repository); $dto = new AttributeDefinitionDto( @@ -119,10 +119,10 @@ public function testUpdateThrowsWhenAnotherAttributeWithSameNameExists(): void tableName: 'custom_attrs' ); - $current = new AttributeDefinition(); + $current = new SubscriberAttributeDefinition(); $current->setName('Old'); - $other = $this->createMock(AttributeDefinition::class); + $other = $this->createMock(SubscriberAttributeDefinition::class); $other->method('getId')->willReturn(999); $repository->expects($this->once()) @@ -137,10 +137,10 @@ public function testUpdateThrowsWhenAnotherAttributeWithSameNameExists(): void public function testDeleteAttributeDefinition(): void { - $repository = $this->createMock(AttributeDefinitionRepository::class); + $repository = $this->createMock(SubscriberAttributeDefinitionRepository::class); $manager = new AttributeDefinitionManager($repository); - $attribute = new AttributeDefinition(); + $attribute = new SubscriberAttributeDefinition(); $repository->expects($this->once())->method('remove')->with($attribute); diff --git a/tests/Unit/Domain/Service/Manager/SubscriberAttributeManagerTest.php b/tests/Unit/Domain/Service/Manager/SubscriberAttributeManagerTest.php index 9e3cbe12..e5b73cfe 100644 --- a/tests/Unit/Domain/Service/Manager/SubscriberAttributeManagerTest.php +++ b/tests/Unit/Domain/Service/Manager/SubscriberAttributeManagerTest.php @@ -5,12 +5,12 @@ namespace PhpList\Core\Tests\Unit\Domain\Service\Manager; use PhpList\Core\Domain\Exception\SubscriberAttributeCreationException; -use PhpList\Core\Domain\Model\Subscription\AttributeDefinition; +use PhpList\Core\Domain\Model\Subscription\SubscriberAttributeDefinition; use PhpList\Core\Domain\Model\Subscription\Dto\SubscriberAttributeDto; use PhpList\Core\Domain\Model\Subscription\Subscriber; -use PhpList\Core\Domain\Model\Subscription\SubscriberAttribute; -use PhpList\Core\Domain\Repository\Subscription\AttributeDefinitionRepository; -use PhpList\Core\Domain\Repository\Subscription\SubscriberAttributeRepository; +use PhpList\Core\Domain\Model\Subscription\SubscriberAttributeValue; +use PhpList\Core\Domain\Repository\Subscription\SubscriberAttributeDefinitionRepository; +use PhpList\Core\Domain\Repository\Subscription\SubscriberAttributeValueRepository; use PhpList\Core\Domain\Repository\Subscription\SubscriberRepository; use PhpList\Core\Domain\Service\Manager\SubscriberAttributeManager; use PHPUnit\Framework\TestCase; @@ -20,7 +20,7 @@ class SubscriberAttributeManagerTest extends TestCase public function testCreateNewSubscriberAttribute(): void { $subscriber = new Subscriber(); - $definition = new AttributeDefinition(); + $definition = new SubscriberAttributeDefinition(); $dto = new SubscriberAttributeDto( subscriberId: 1, @@ -29,8 +29,8 @@ public function testCreateNewSubscriberAttribute(): void ); $subscriberRepo = $this->createMock(SubscriberRepository::class); - $attributeDefRepo = $this->createMock(AttributeDefinitionRepository::class); - $subscriberAttrRepo = $this->createMock(SubscriberAttributeRepository::class); + $attributeDefRepo = $this->createMock(SubscriberAttributeDefinitionRepository::class); + $subscriberAttrRepo = $this->createMock(SubscriberAttributeValueRepository::class); $subscriberRepo->expects(self::once()) ->method('find') @@ -49,30 +49,30 @@ public function testCreateNewSubscriberAttribute(): void $subscriberAttrRepo->expects(self::once()) ->method('save') - ->with(self::callback(function (SubscriberAttribute $attr) { + ->with(self::callback(function (SubscriberAttributeValue $attr) { return $attr->getValue() === 'US'; })); $manager = new SubscriberAttributeManager($attributeDefRepo, $subscriberAttrRepo, $subscriberRepo); $attribute = $manager->createOrUpdate($dto); - self::assertInstanceOf(SubscriberAttribute::class, $attribute); + self::assertInstanceOf(SubscriberAttributeValue::class, $attribute); self::assertSame('US', $attribute->getValue()); } public function testUpdateExistingSubscriberAttribute(): void { $subscriber = new Subscriber(); - $definition = new AttributeDefinition(); + $definition = new SubscriberAttributeDefinition(); - $existing = new SubscriberAttribute($definition, $subscriber); + $existing = new SubscriberAttributeValue($definition, $subscriber); $existing->setValue('Old'); $dto = new SubscriberAttributeDto(1, 2, 'Updated'); $subscriberRepo = $this->createMock(SubscriberRepository::class); - $attributeDefRepo = $this->createMock(AttributeDefinitionRepository::class); - $subscriberAttrRepo = $this->createMock(SubscriberAttributeRepository::class); + $attributeDefRepo = $this->createMock(SubscriberAttributeDefinitionRepository::class); + $subscriberAttrRepo = $this->createMock(SubscriberAttributeValueRepository::class); $subscriberRepo->method('find')->willReturn($subscriber); $attributeDefRepo->method('find')->willReturn($definition); @@ -93,8 +93,8 @@ public function testUpdateExistingSubscriberAttribute(): void public function testCreateFailsIfSubscriberNotFound(): void { $subscriberRepo = $this->createMock(SubscriberRepository::class); - $attributeDefRepo = $this->createMock(AttributeDefinitionRepository::class); - $subscriberAttrRepo = $this->createMock(SubscriberAttributeRepository::class); + $attributeDefRepo = $this->createMock(SubscriberAttributeDefinitionRepository::class); + $subscriberAttrRepo = $this->createMock(SubscriberAttributeValueRepository::class); $subscriberRepo->method('find')->willReturn(null); @@ -113,8 +113,8 @@ public function testCreateFailsIfAttributeDefinitionNotFound(): void $subscriber = new Subscriber(); $subscriberRepo = $this->createMock(SubscriberRepository::class); - $attributeDefRepo = $this->createMock(AttributeDefinitionRepository::class); - $subscriberAttrRepo = $this->createMock(SubscriberAttributeRepository::class); + $attributeDefRepo = $this->createMock(SubscriberAttributeDefinitionRepository::class); + $subscriberAttrRepo = $this->createMock(SubscriberAttributeValueRepository::class); $subscriberRepo->method('find')->willReturn($subscriber); $attributeDefRepo->method('find')->willReturn(null); @@ -131,11 +131,11 @@ public function testCreateFailsIfAttributeDefinitionNotFound(): void public function testGetSubscriberAttribute(): void { - $expected = new SubscriberAttribute(new AttributeDefinition(), new Subscriber()); + $expected = new SubscriberAttributeValue(new SubscriberAttributeDefinition(), new Subscriber()); $subscriberRepo = $this->createMock(SubscriberRepository::class); - $attributeDefRepo = $this->createMock(AttributeDefinitionRepository::class); - $subscriberAttrRepo = $this->createMock(SubscriberAttributeRepository::class); + $attributeDefRepo = $this->createMock(SubscriberAttributeDefinitionRepository::class); + $subscriberAttrRepo = $this->createMock(SubscriberAttributeValueRepository::class); $subscriberAttrRepo->expects(self::once()) ->method('findOneBySubscriberIdAndAttributeId') @@ -152,10 +152,10 @@ public function testGetSubscriberAttribute(): void public function testDeleteSubscriberAttribute(): void { $subscriberRepo = $this->createMock(SubscriberRepository::class); - $attributeDefRepo = $this->createMock(AttributeDefinitionRepository::class); - $subscriberAttrRepo = $this->createMock(SubscriberAttributeRepository::class); + $attributeDefRepo = $this->createMock(SubscriberAttributeDefinitionRepository::class); + $subscriberAttrRepo = $this->createMock(SubscriberAttributeValueRepository::class); - $attribute = $this->createMock(SubscriberAttribute::class); + $attribute = $this->createMock(SubscriberAttributeValue::class); $attributeDefRepo->expects(self::once())->method('remove')->with($attribute); From eca466bb834dbf3bf312ae75061e298abaafd83b Mon Sep 17 00:00:00 2001 From: Tatevik Date: Sun, 11 May 2025 17:40:15 +0400 Subject: [PATCH 08/15] ISSUE-345: Feature-based structure --- config/doctrine.yml | 8 +-- config/services.yml | 2 +- config/services/builders.yml | 10 ++-- config/services/managers.yml | 20 +++---- config/services/repositories.yml | 60 +++++++++---------- config/services/validators.yml | 4 +- src/Core/ApplicationKernel.php | 1 + src/Core/DoctrineMappingPass.php | 45 ++++++++++++++ .../Model}/LinkTrack.php | 8 +-- .../Model}/LinkTrackForward.php | 8 +-- .../Model}/LinkTrackMl.php | 6 +- .../Model}/LinkTrackUmlClick.php | 8 +-- .../Model}/LinkTrackUserClick.php | 6 +- .../Model}/UserMessageView.php | 8 +-- .../Model}/UserStats.php | 8 +-- .../Repository/LinkTrackForwardRepository.php | 14 +++++ .../Repository/LinkTrackMlRepository.php | 11 ++++ .../Repository/LinkTrackRepository.php | 14 +++++ .../LinkTrackUmlClickRepository.php | 14 +++++ .../LinkTrackUserClickRepository.php | 11 ++++ .../Repository/UserMessageViewRepository.php | 14 +++++ .../Repository/UserStatsRepository.php | 14 +++++ .../Model}/Filter/FilterRequestInterface.php | 2 +- .../Model/Interfaces/CreationDate.php | 3 +- .../Model/Interfaces/DomainModel.php | 2 +- .../Model/Interfaces/EmbeddableInterface.php | 2 +- .../Model/Interfaces/Identity.php | 2 +- .../Model/Interfaces/ModificationDate.php | 2 +- .../Model}/ValidationContext.php | 2 +- .../Repository/AbstractRepository.php | 4 +- .../Repository/CursorPaginationTrait.php | 6 +- .../PaginatableRepositoryInterface.php | 4 +- .../Validator/ValidatorInterface.php | 4 +- .../Model}/Config.php | 6 +- .../Model}/EventLog.php | 8 +-- .../Model}/I18n.php | 6 +- .../Model}/UrlCache.php | 8 +-- .../Repository/ConfigRepository.php | 11 ++++ .../Repository/EventLogRepository.php | 14 +++++ .../Repository/I18nRepository.php | 11 ++++ .../Repository/UrlCacheRepository.php | 14 +++++ .../Model}/AdminAttributeDefinition.php | 8 +-- .../Model}/AdminAttributeValue.php | 6 +- .../Model}/AdminLogin.php | 8 +-- .../Model}/AdminPasswordRequest.php | 8 +-- .../Model}/Administrator.php | 12 ++-- .../Model}/AdministratorToken.php | 10 ++-- .../Model}/Dto/CreateAdministratorDto.php | 2 +- .../Model}/Dto/UpdateAdministratorDto.php | 2 +- .../Model}/UserBlacklist.php | 6 +- .../Model}/UserBlacklistData.php | 6 +- .../AdminAttributeDefinitionRepository.php | 14 +++++ .../AdminAttributeValueRepository.php | 11 ++++ .../Repository/AdminLoginRepository.php | 14 +++++ .../AdminPasswordRequestRepository.php | 14 +++++ .../Repository}/AdministratorRepository.php | 10 ++-- .../AdministratorTokenRepository.php | 10 ++-- .../UserBlacklistDataRepository.php | 11 ++++ .../Repository/UserBlacklistRepository.php | 11 ++++ .../Service}/AdministratorManager.php | 8 +-- .../Service}/SessionManager.php | 8 +-- .../Model}/Attachment.php | 8 +-- .../Messaging => Messaging/Model}/Bounce.php | 10 ++-- .../Model}/BounceRegex.php | 8 +-- .../Model}/BounceRegexBounce.php | 6 +- .../Model}/Dto/CreateMessageDto.php | 12 ++-- .../Model}/Dto/CreateTemplateDto.php | 2 +- .../Model}/Dto/Message/MessageContentDto.php | 2 +- .../Model}/Dto/Message/MessageFormatDto.php | 2 +- .../Model}/Dto/Message/MessageMetadataDto.php | 2 +- .../Model}/Dto/Message/MessageOptionsDto.php | 2 +- .../Model}/Dto/Message/MessageScheduleDto.php | 2 +- .../Model/Dto/MessageContext.php | 6 +- .../Model}/Dto/MessageDtoInterface.php | 12 ++-- .../Model}/Dto/UpdateMessageDto.php | 12 ++-- .../Model}/Filter/MessageFilter.php | 5 +- .../Model}/ListMessage.php | 10 ++-- .../Messaging => Messaging/Model}/Message.php | 22 +++---- .../Model}/Message/MessageContent.php | 4 +- .../Model}/Message/MessageFormat.php | 4 +- .../Model}/Message/MessageMetadata.php | 4 +- .../Model}/Message/MessageOptions.php | 4 +- .../Model}/Message/MessageSchedule.php | 4 +- .../Model}/MessageAttachment.php | 6 +- .../Model}/MessageData.php | 6 +- .../Model}/SendProcess.php | 10 ++-- .../Model}/Template.php | 8 +-- .../Model}/TemplateImage.php | 8 +-- .../Model}/UserMessage.php | 8 +-- .../Model}/UserMessageBounce.php | 8 +-- .../Model}/UserMessageForward.php | 8 +-- .../Repository/AttachmentRepository.php | 14 +++++ .../BounceRegexBounceRepository.php | 11 ++++ .../Repository/BounceRegexRepository.php | 14 +++++ .../Messaging/Repository/BounceRepository.php | 14 +++++ .../Repository/ListMessageRepository.php | 14 +++++ .../MessageAttachmentRepository.php | 14 +++++ .../Repository/MessageDataRepository.php | 14 +++++ .../Repository}/MessageRepository.php | 12 ++-- .../Repository/SendProcessRepository.php | 14 +++++ .../Repository/TemplateImageRepository.php | 14 +++++ .../Repository/TemplateRepository.php | 14 +++++ .../UserMessageBounceRepository.php | 14 +++++ .../UserMessageForwardRepository.php | 14 +++++ .../Repository/UserMessageRepository.php | 11 ++++ .../Service/Builder/MessageBuilder.php | 10 ++-- .../Service/Builder/MessageContentBuilder.php | 6 +- .../Service/Builder/MessageFormatBuilder.php | 6 +- .../Service/Builder/MessageOptionsBuilder.php | 6 +- .../Builder/MessageScheduleBuilder.php | 6 +- .../Service}/MessageManager.php | 16 ++--- .../Service}/TemplateImageManager.php | 8 +-- .../Service}/TemplateManager.php | 18 +++--- .../Analytics/LinkTrackForwardRepository.php | 14 ----- .../Analytics/LinkTrackMlRepository.php | 11 ---- .../Analytics/LinkTrackRepository.php | 14 ----- .../Analytics/LinkTrackUmlClickRepository.php | 14 ----- .../LinkTrackUserClickRepository.php | 11 ---- .../Analytics/UserMessageViewRepository.php | 14 ----- .../Analytics/UserStatsRepository.php | 14 ----- .../Configuration/ConfigRepository.php | 11 ---- .../Configuration/EventLogRepository.php | 14 ----- .../Configuration/I18nRepository.php | 11 ---- .../Configuration/UrlCacheRepository.php | 14 ----- .../AdminAttributeDefinitionRepository.php | 14 ----- .../AdminAttributeValueRepository.php | 11 ---- .../Identity/AdminLoginRepository.php | 14 ----- .../AdminPasswordRequestRepository.php | 14 ----- .../Identity/UserBlacklistDataRepository.php | 11 ---- .../Identity/UserBlacklistRepository.php | 11 ---- .../Messaging/AttachmentRepository.php | 14 ----- .../Messaging/BounceRegexBounceRepository.php | 11 ---- .../Messaging/BounceRegexRepository.php | 14 ----- .../Repository/Messaging/BounceRepository.php | 14 ----- .../Messaging/ListMessageRepository.php | 14 ----- .../Messaging/MessageAttachmentRepository.php | 14 ----- .../Messaging/MessageDataRepository.php | 14 ----- .../Messaging/SendProcessRepository.php | 14 ----- .../Messaging/TemplateImageRepository.php | 14 ----- .../Messaging/TemplateRepository.php | 14 ----- .../Messaging/UserMessageBounceRepository.php | 14 ----- .../UserMessageForwardRepository.php | 14 ----- .../Messaging/UserMessageRepository.php | 11 ---- .../SubscriberHistoryRepository.php | 14 ----- .../SubscriberPageDataRepository.php | 14 ----- .../Subscription/SubscriberPageRepository.php | 14 ----- .../AttributeDefinitionCreationException.php | 2 +- .../SubscriberAttributeCreationException.php | 2 +- .../SubscriptionCreationException.php | 2 +- .../Model}/Dto/AttributeDefinitionDto.php | 2 +- .../Model}/Dto/CreateSubscriberDto.php | 2 +- .../Model}/Dto/CreateSubscriberListDto.php | 2 +- .../Model}/Dto/SubscriberAttributeDto.php | 2 +- .../Model}/Dto/UpdateSubscriberDto.php | 2 +- .../Model}/Filter/SubscriberFilter.php | 4 +- .../Model}/SubscribePage.php | 8 +-- .../Model}/SubscribePageData.php | 6 +- .../Model}/Subscriber.php | 12 ++-- .../Model}/SubscriberAttributeDefinition.php | 8 +-- .../Model}/SubscriberAttributeValue.php | 6 +- .../Model}/SubscriberHistory.php | 8 +-- .../Model}/SubscriberList.php | 14 ++--- .../Model}/Subscription.php | 10 ++-- ...ubscriberAttributeDefinitionRepository.php | 10 ++-- .../SubscriberAttributeValueRepository.php | 10 ++-- .../SubscriberHistoryRepository.php | 14 +++++ .../Repository}/SubscriberListRepository.php | 12 ++-- .../SubscriberPageDataRepository.php | 14 +++++ .../Repository/SubscriberPageRepository.php | 14 +++++ .../Repository}/SubscriberRepository.php | 12 ++-- .../Repository}/SubscriptionRepository.php | 10 ++-- .../Service}/AttributeDefinitionManager.php | 10 ++-- .../Service}/SubscriberAttributeManager.php | 14 ++--- .../Service}/SubscriberListManager.php | 10 ++-- .../Service}/SubscriberManager.php | 10 ++-- .../Service}/SubscriptionManager.php | 16 ++--- .../Validator/TemplateImageValidator.php | 5 +- .../Validator/TemplateLinkValidator.php | 5 +- src/Security/Authentication.php | 4 +- src/TestingSupport/Traits/ModelTestTrait.php | 2 +- .../Fixtures}/Administrator.csv | 0 .../Fixtures}/AdministratorFixture.php | 4 +- .../AdministratorTokenWithAdministrator.csv | 0 ...nistratorTokenWithAdministratorFixture.php | 6 +- .../DetachedAdministratorTokenFixture.php | 4 +- .../Fixtures}/DetachedAdministratorTokens.csv | 0 .../AdministratorRepositoryTest.php | 8 +-- .../AdministratorTokenRepositoryTest.php | 14 ++--- .../Fixtures}/Message.csv | 0 .../Fixtures}/MessageFixture.php | 18 +++--- .../Fixtures}/Template.csv | 0 .../Fixtures}/TemplateFixture.php | 4 +- .../Repository}/MessageRepositoryTest.php | 18 +++--- .../SubscriberListRepositoryTest.php | 24 ++++---- .../Repository}/TemplateRepositoryTest.php | 8 +-- .../Fixtures}/Subscriber.csv | 0 .../Fixtures}/SubscriberFixture.php | 4 +- .../Fixtures}/SubscriberList.csv | 0 .../Fixtures}/SubscriberListFixture.php | 6 +- .../Fixtures}/Subscription.csv | 0 .../Fixtures}/SubscriptionFixture.php | 8 +-- .../Repository}/SubscriberRepositoryTest.php | 20 +++---- .../SubscriptionRepositoryTest.php | 20 +++---- .../Security/AuthenticationTest.php | 6 +- .../Repository/CursorPaginationTraitTest.php | 4 +- .../Repository/DummyRepository.php | 4 +- .../Model}/AdministratorTest.php | 6 +- .../Model}/AdministratorTokenTest.php | 8 +-- .../AdministratorRepositoryTest.php | 7 ++- .../AdministratorTokenRepositoryTest.php | 7 ++- .../Service}/AdministratorManagerTest.php | 10 ++-- .../Service}/SessionManagerTest.php | 10 ++-- .../Model}/MessageTest.php | 22 +++---- .../Model}/SubscriberListTest.php | 12 ++-- .../Repository}/MessageRepositoryTest.php | 4 +- .../SubscriberListRepositoryTest.php | 6 +- .../Service/Builder/MessageBuilderTest.php | 38 ++++++------ .../Builder/MessageContentBuilderTest.php | 4 +- .../Builder/MessageFormatBuilderTest.php | 4 +- .../Builder/MessageOptionsBuilderTest.php | 4 +- .../Builder/MessageScheduleBuilderTest.php | 4 +- .../Service}/MessageManagerTest.php | 32 +++++----- .../Service}/TemplateImageManagerTest.php | 10 ++-- .../Service}/TemplateManagerTest.php | 16 ++--- .../Validator/TemplateImageValidatorTest.php | 8 +-- .../Validator/TemplateLinkValidatorTest.php | 6 +- .../Model}/SubscriberTest.php | 10 ++-- .../Model}/SubscriptionTest.php | 10 ++-- .../Repository}/SubscriberRepositoryTest.php | 7 ++- .../SubscriptionRepositoryTest.php | 7 ++- .../AttributeDefinitionManagerTest.php | 12 ++-- .../SubscriberAttributeManagerTest.php | 22 +++---- .../Service}/SubscriberListManagerTest.php | 12 ++-- .../Service}/SubscriberManagerTest.php | 10 ++-- .../Service}/SubscriptionManagerTest.php | 18 +++--- tests/Unit/Security/AuthenticationTest.php | 6 +- 236 files changed, 1159 insertions(+), 1105 deletions(-) create mode 100644 src/Core/DoctrineMappingPass.php rename src/Domain/{Model/Analytics => Analytics/Model}/LinkTrack.php (92%) rename src/Domain/{Model/Analytics => Analytics/Model}/LinkTrackForward.php (89%) rename src/Domain/{Model/Analytics => Analytics/Model}/LinkTrackMl.php (94%) rename src/Domain/{Model/Analytics => Analytics/Model}/LinkTrackUmlClick.php (93%) rename src/Domain/{Model/Analytics => Analytics/Model}/LinkTrackUserClick.php (93%) rename src/Domain/{Model/Analytics => Analytics/Model}/UserMessageView.php (90%) rename src/Domain/{Model/Analytics => Analytics/Model}/UserStats.php (89%) create mode 100644 src/Domain/Analytics/Repository/LinkTrackForwardRepository.php create mode 100644 src/Domain/Analytics/Repository/LinkTrackMlRepository.php create mode 100644 src/Domain/Analytics/Repository/LinkTrackRepository.php create mode 100644 src/Domain/Analytics/Repository/LinkTrackUmlClickRepository.php create mode 100644 src/Domain/Analytics/Repository/LinkTrackUserClickRepository.php create mode 100644 src/Domain/Analytics/Repository/UserMessageViewRepository.php create mode 100644 src/Domain/Analytics/Repository/UserStatsRepository.php rename src/Domain/{Model/Dto => Common/Model}/Filter/FilterRequestInterface.php (58%) rename src/Domain/{ => Common}/Model/Interfaces/CreationDate.php (82%) rename src/Domain/{ => Common}/Model/Interfaces/DomainModel.php (84%) rename src/Domain/{ => Common}/Model/Interfaces/EmbeddableInterface.php (55%) rename src/Domain/{ => Common}/Model/Interfaces/Identity.php (83%) rename src/Domain/{ => Common}/Model/Interfaces/ModificationDate.php (91%) rename src/Domain/{Model/Dto => Common/Model}/ValidationContext.php (91%) rename src/Domain/{ => Common}/Repository/AbstractRepository.php (91%) rename src/Domain/{ => Common}/Repository/CursorPaginationTrait.php (84%) rename src/Domain/{ => Common}/Repository/Interfaces/PaginatableRepositoryInterface.php (66%) rename src/Domain/{Service => Common}/Validator/ValidatorInterface.php (59%) rename src/Domain/{Model/Configuration => Configuration/Model}/Config.php (88%) rename src/Domain/{Model/Configuration => Configuration/Model}/EventLog.php (85%) rename src/Domain/{Model/Configuration => Configuration/Model}/I18n.php (87%) rename src/Domain/{Model/Configuration => Configuration/Model}/UrlCache.php (87%) create mode 100644 src/Domain/Configuration/Repository/ConfigRepository.php create mode 100644 src/Domain/Configuration/Repository/EventLogRepository.php create mode 100644 src/Domain/Configuration/Repository/I18nRepository.php create mode 100644 src/Domain/Configuration/Repository/UrlCacheRepository.php rename src/Domain/{Model/Identity => Identity/Model}/AdminAttributeDefinition.php (92%) rename src/Domain/{Model/Identity => Identity/Model}/AdminAttributeValue.php (88%) rename src/Domain/{Model/Identity => Identity/Model}/AdminLogin.php (90%) rename src/Domain/{Model/Identity => Identity/Model}/AdminPasswordRequest.php (85%) rename src/Domain/{Model/Identity => Identity/Model}/Administrator.php (92%) rename src/Domain/{Model/Identity => Identity/Model}/AdministratorToken.php (90%) rename src/Domain/{Model/Identity => Identity/Model}/Dto/CreateAdministratorDto.php (87%) rename src/Domain/{Model/Identity => Identity/Model}/Dto/UpdateAdministratorDto.php (87%) rename src/Domain/{Model/Identity => Identity/Model}/UserBlacklist.php (84%) rename src/Domain/{Model/Identity => Identity/Model}/UserBlacklistData.php (87%) create mode 100644 src/Domain/Identity/Repository/AdminAttributeDefinitionRepository.php create mode 100644 src/Domain/Identity/Repository/AdminAttributeValueRepository.php create mode 100644 src/Domain/Identity/Repository/AdminLoginRepository.php create mode 100644 src/Domain/Identity/Repository/AdminPasswordRequestRepository.php rename src/Domain/{Repository/Identity => Identity/Repository}/AdministratorRepository.php (82%) rename src/Domain/{Repository/Identity => Identity/Repository}/AdministratorTokenRepository.php (85%) create mode 100644 src/Domain/Identity/Repository/UserBlacklistDataRepository.php create mode 100644 src/Domain/Identity/Repository/UserBlacklistRepository.php rename src/Domain/{Service/Manager => Identity/Service}/AdministratorManager.php (89%) rename src/Domain/{Service/Manager => Identity/Service}/SessionManager.php (84%) rename src/Domain/{Model/Messaging => Messaging/Model}/Attachment.php (91%) rename src/Domain/{Model/Messaging => Messaging/Model}/Bounce.php (90%) rename src/Domain/{Model/Messaging => Messaging/Model}/BounceRegex.php (93%) rename src/Domain/{Model/Messaging => Messaging/Model}/BounceRegexBounce.php (84%) rename src/Domain/{Model/Messaging => Messaging/Model}/Dto/CreateMessageDto.php (74%) rename src/Domain/{Model/Messaging => Messaging/Model}/Dto/CreateTemplateDto.php (91%) rename src/Domain/{Model/Messaging => Messaging/Model}/Dto/Message/MessageContentDto.php (82%) rename src/Domain/{Model/Messaging => Messaging/Model}/Dto/Message/MessageFormatDto.php (80%) rename src/Domain/{Model/Messaging => Messaging/Model}/Dto/Message/MessageMetadataDto.php (71%) rename src/Domain/{Model/Messaging => Messaging/Model}/Dto/Message/MessageOptionsDto.php (83%) rename src/Domain/{Model/Messaging => Messaging/Model}/Dto/Message/MessageScheduleDto.php (86%) rename src/Domain/{ => Messaging}/Model/Dto/MessageContext.php (68%) rename src/Domain/{Model/Messaging => Messaging/Model}/Dto/MessageDtoInterface.php (52%) rename src/Domain/{Model/Messaging => Messaging/Model}/Dto/UpdateMessageDto.php (74%) rename src/Domain/{Model/Dto => Messaging/Model}/Filter/MessageFilter.php (66%) rename src/Domain/{Model/Messaging => Messaging/Model}/ListMessage.php (86%) rename src/Domain/{Model/Messaging => Messaging/Model}/Message.php (86%) rename src/Domain/{Model/Messaging => Messaging/Model}/Message/MessageContent.php (93%) rename src/Domain/{Model/Messaging => Messaging/Model}/Message/MessageFormat.php (96%) rename src/Domain/{Model/Messaging => Messaging/Model}/Message/MessageMetadata.php (95%) rename src/Domain/{Model/Messaging => Messaging/Model}/Message/MessageOptions.php (94%) rename src/Domain/{Model/Messaging => Messaging/Model}/Message/MessageSchedule.php (95%) rename src/Domain/{Model/Messaging => Messaging/Model}/MessageAttachment.php (87%) rename src/Domain/{Model/Messaging => Messaging/Model}/MessageData.php (86%) rename src/Domain/{Model/Messaging => Messaging/Model}/SendProcess.php (87%) rename src/Domain/{Model/Messaging => Messaging/Model}/Template.php (91%) rename src/Domain/{Model/Messaging => Messaging/Model}/TemplateImage.php (91%) rename src/Domain/{Model/Messaging => Messaging/Model}/UserMessage.php (90%) rename src/Domain/{Model/Messaging => Messaging/Model}/UserMessageBounce.php (88%) rename src/Domain/{Model/Messaging => Messaging/Model}/UserMessageForward.php (90%) create mode 100644 src/Domain/Messaging/Repository/AttachmentRepository.php create mode 100644 src/Domain/Messaging/Repository/BounceRegexBounceRepository.php create mode 100644 src/Domain/Messaging/Repository/BounceRegexRepository.php create mode 100644 src/Domain/Messaging/Repository/BounceRepository.php create mode 100644 src/Domain/Messaging/Repository/ListMessageRepository.php create mode 100644 src/Domain/Messaging/Repository/MessageAttachmentRepository.php create mode 100644 src/Domain/Messaging/Repository/MessageDataRepository.php rename src/Domain/{Repository/Messaging => Messaging/Repository}/MessageRepository.php (73%) create mode 100644 src/Domain/Messaging/Repository/SendProcessRepository.php create mode 100644 src/Domain/Messaging/Repository/TemplateImageRepository.php create mode 100644 src/Domain/Messaging/Repository/TemplateRepository.php create mode 100644 src/Domain/Messaging/Repository/UserMessageBounceRepository.php create mode 100644 src/Domain/Messaging/Repository/UserMessageForwardRepository.php create mode 100644 src/Domain/Messaging/Repository/UserMessageRepository.php rename src/Domain/{ => Messaging}/Service/Builder/MessageBuilder.php (86%) rename src/Domain/{ => Messaging}/Service/Builder/MessageContentBuilder.php (72%) rename src/Domain/{ => Messaging}/Service/Builder/MessageFormatBuilder.php (71%) rename src/Domain/{ => Messaging}/Service/Builder/MessageOptionsBuilder.php (73%) rename src/Domain/{ => Messaging}/Service/Builder/MessageScheduleBuilder.php (76%) rename src/Domain/{Service/Manager => Messaging/Service}/MessageManager.php (77%) rename src/Domain/{Service/Manager => Messaging/Service}/TemplateImageManager.php (92%) rename src/Domain/{Service/Manager => Messaging/Service}/TemplateManager.php (84%) delete mode 100644 src/Domain/Repository/Analytics/LinkTrackForwardRepository.php delete mode 100644 src/Domain/Repository/Analytics/LinkTrackMlRepository.php delete mode 100644 src/Domain/Repository/Analytics/LinkTrackRepository.php delete mode 100644 src/Domain/Repository/Analytics/LinkTrackUmlClickRepository.php delete mode 100644 src/Domain/Repository/Analytics/LinkTrackUserClickRepository.php delete mode 100644 src/Domain/Repository/Analytics/UserMessageViewRepository.php delete mode 100644 src/Domain/Repository/Analytics/UserStatsRepository.php delete mode 100644 src/Domain/Repository/Configuration/ConfigRepository.php delete mode 100644 src/Domain/Repository/Configuration/EventLogRepository.php delete mode 100644 src/Domain/Repository/Configuration/I18nRepository.php delete mode 100644 src/Domain/Repository/Configuration/UrlCacheRepository.php delete mode 100644 src/Domain/Repository/Identity/AdminAttributeDefinitionRepository.php delete mode 100644 src/Domain/Repository/Identity/AdminAttributeValueRepository.php delete mode 100644 src/Domain/Repository/Identity/AdminLoginRepository.php delete mode 100644 src/Domain/Repository/Identity/AdminPasswordRequestRepository.php delete mode 100644 src/Domain/Repository/Identity/UserBlacklistDataRepository.php delete mode 100644 src/Domain/Repository/Identity/UserBlacklistRepository.php delete mode 100644 src/Domain/Repository/Messaging/AttachmentRepository.php delete mode 100644 src/Domain/Repository/Messaging/BounceRegexBounceRepository.php delete mode 100644 src/Domain/Repository/Messaging/BounceRegexRepository.php delete mode 100644 src/Domain/Repository/Messaging/BounceRepository.php delete mode 100644 src/Domain/Repository/Messaging/ListMessageRepository.php delete mode 100644 src/Domain/Repository/Messaging/MessageAttachmentRepository.php delete mode 100644 src/Domain/Repository/Messaging/MessageDataRepository.php delete mode 100644 src/Domain/Repository/Messaging/SendProcessRepository.php delete mode 100644 src/Domain/Repository/Messaging/TemplateImageRepository.php delete mode 100644 src/Domain/Repository/Messaging/TemplateRepository.php delete mode 100644 src/Domain/Repository/Messaging/UserMessageBounceRepository.php delete mode 100644 src/Domain/Repository/Messaging/UserMessageForwardRepository.php delete mode 100644 src/Domain/Repository/Messaging/UserMessageRepository.php delete mode 100644 src/Domain/Repository/Subscription/SubscriberHistoryRepository.php delete mode 100644 src/Domain/Repository/Subscription/SubscriberPageDataRepository.php delete mode 100644 src/Domain/Repository/Subscription/SubscriberPageRepository.php rename src/Domain/{ => Subscription}/Exception/AttributeDefinitionCreationException.php (88%) rename src/Domain/{ => Subscription}/Exception/SubscriberAttributeCreationException.php (88%) rename src/Domain/{ => Subscription}/Exception/SubscriptionCreationException.php (88%) rename src/Domain/{Model/Subscription => Subscription/Model}/Dto/AttributeDefinitionDto.php (89%) rename src/Domain/{Model/Subscription => Subscription/Model}/Dto/CreateSubscriberDto.php (82%) rename src/Domain/{Model/Subscription => Subscription/Model}/Dto/CreateSubscriberListDto.php (87%) rename src/Domain/{Model/Subscription => Subscription/Model}/Dto/SubscriberAttributeDto.php (82%) rename src/Domain/{Model/Subscription => Subscription/Model}/Dto/UpdateSubscriberDto.php (88%) rename src/Domain/{Model/Dto => Subscription/Model}/Filter/SubscriberFilter.php (72%) rename src/Domain/{Model/Subscription => Subscription/Model}/SubscribePage.php (84%) rename src/Domain/{Model/Subscription => Subscription/Model}/SubscribePageData.php (86%) rename src/Domain/{Model/Subscription => Subscription/Model}/Subscriber.php (94%) rename src/Domain/{Model/Subscription => Subscription/Model}/SubscriberAttributeDefinition.php (91%) rename src/Domain/{Model/Subscription => Subscription/Model}/SubscriberAttributeValue.php (90%) rename src/Domain/{Model/Subscription => Subscription/Model}/SubscriberHistory.php (91%) rename src/Domain/{Model/Subscription => Subscription/Model}/SubscriberList.php (92%) rename src/Domain/{Model/Subscription => Subscription/Model}/Subscription.php (90%) rename src/Domain/{Repository/Subscription => Subscription/Repository}/SubscriberAttributeDefinitionRepository.php (50%) rename src/Domain/{Repository/Subscription => Subscription/Repository}/SubscriberAttributeValueRepository.php (77%) create mode 100644 src/Domain/Subscription/Repository/SubscriberHistoryRepository.php rename src/Domain/{Repository/Subscription => Subscription/Repository}/SubscriberListRepository.php (64%) create mode 100644 src/Domain/Subscription/Repository/SubscriberPageDataRepository.php create mode 100644 src/Domain/Subscription/Repository/SubscriberPageRepository.php rename src/Domain/{Repository/Subscription => Subscription/Repository}/SubscriberRepository.php (86%) rename src/Domain/{Repository/Subscription => Subscription/Repository}/SubscriptionRepository.php (80%) rename src/Domain/{Service/Manager => Subscription/Service}/AttributeDefinitionManager.php (89%) rename src/Domain/{Service/Manager => Subscription/Service}/SubscriberAttributeManager.php (82%) rename src/Domain/{Service/Manager => Subscription/Service}/SubscriberListManager.php (82%) rename src/Domain/{Service/Manager => Subscription/Service}/SubscriberManager.php (87%) rename src/Domain/{Service/Manager => Subscription/Service}/SubscriptionManager.php (86%) rename src/Domain/{Service => Subscription}/Validator/TemplateImageValidator.php (91%) rename src/Domain/{Service => Subscription}/Validator/TemplateLinkValidator.php (89%) rename tests/Integration/Domain/{Repository/Fixtures/Identity => Identity/Fixtures}/Administrator.csv (100%) rename tests/Integration/Domain/{Repository/Fixtures/Identity => Identity/Fixtures}/AdministratorFixture.php (92%) rename tests/Integration/Domain/{Repository/Fixtures/Identity => Identity/Fixtures}/AdministratorTokenWithAdministrator.csv (100%) rename tests/Integration/Domain/{Repository/Fixtures/Identity => Identity/Fixtures}/AdministratorTokenWithAdministratorFixture.php (90%) rename tests/Integration/Domain/{Repository/Fixtures/Identity => Identity/Fixtures}/DetachedAdministratorTokenFixture.php (91%) rename tests/Integration/Domain/{Repository/Fixtures/Identity => Identity/Fixtures}/DetachedAdministratorTokens.csv (100%) rename tests/Integration/Domain/{Repository/Identity => Identity/Repository}/AdministratorRepositoryTest.php (95%) rename tests/Integration/Domain/{Repository/Identity => Identity/Repository}/AdministratorTokenRepositoryTest.php (92%) rename tests/Integration/Domain/{Repository/Fixtures/Messaging => Messaging/Fixtures}/Message.csv (100%) rename tests/Integration/Domain/{Repository/Fixtures/Messaging => Messaging/Fixtures}/MessageFixture.php (85%) rename tests/Integration/Domain/{Repository/Fixtures/Messaging => Messaging/Fixtures}/Template.csv (100%) rename tests/Integration/Domain/{Repository/Fixtures/Messaging => Messaging/Fixtures}/TemplateFixture.php (90%) rename tests/Integration/Domain/{Repository/Messaging => Messaging/Repository}/MessageRepositoryTest.php (87%) rename tests/Integration/Domain/{Repository/Messaging => Messaging/Repository}/SubscriberListRepositoryTest.php (89%) rename tests/Integration/Domain/{Repository/Messaging => Messaging/Repository}/TemplateRepositoryTest.php (88%) rename tests/Integration/Domain/{Repository/Fixtures/Subscription => Subscription/Fixtures}/Subscriber.csv (100%) rename tests/Integration/Domain/{Repository/Fixtures/Subscription => Subscription/Fixtures}/SubscriberFixture.php (93%) rename tests/Integration/Domain/{Repository/Fixtures/Subscription => Subscription/Fixtures}/SubscriberList.csv (100%) rename tests/Integration/Domain/{Repository/Fixtures/Subscription => Subscription/Fixtures}/SubscriberListFixture.php (91%) rename tests/Integration/Domain/{Repository/Fixtures/Subscription => Subscription/Fixtures}/Subscription.csv (100%) rename tests/Integration/Domain/{Repository/Fixtures/Subscription => Subscription/Fixtures}/SubscriptionFixture.php (87%) rename tests/Integration/Domain/{Repository/Subscription => Subscription/Repository}/SubscriberRepositoryTest.php (91%) rename tests/Integration/Domain/{Repository/Subscription => Subscription/Repository}/SubscriptionRepositoryTest.php (92%) rename tests/Unit/Domain/{ => Common}/Repository/CursorPaginationTraitTest.php (93%) rename tests/Unit/Domain/{ => Common}/Repository/DummyRepository.php (78%) rename tests/Unit/Domain/{Model/Identity => Identity/Model}/AdministratorTest.php (95%) rename tests/Unit/Domain/{Model/Identity => Identity/Model}/AdministratorTokenTest.php (92%) rename tests/Unit/Domain/{Repository/Identity => Identity/Rpository}/AdministratorRepositoryTest.php (76%) rename tests/Unit/Domain/{Repository/Identity => Identity/Rpository}/AdministratorTokenRepositoryTest.php (76%) rename tests/Unit/Domain/{Service/Manager => Identity/Service}/AdministratorManagerTest.php (91%) rename tests/Unit/Domain/{Service/Manager => Identity/Service}/SessionManagerTest.php (82%) rename tests/Unit/Domain/{Model/Messaging => Messaging/Model}/MessageTest.php (79%) rename tests/Unit/Domain/{Model/Messaging => Messaging/Model}/SubscriberListTest.php (93%) rename tests/Unit/Domain/{Repository/Messaging => Messaging/Repository}/MessageRepositoryTest.php (86%) rename tests/Unit/Domain/{Repository/Messaging => Messaging/Repository}/SubscriberListRepositoryTest.php (81%) rename tests/Unit/Domain/{ => Messaging}/Service/Builder/MessageBuilderTest.php (77%) rename tests/Unit/Domain/{ => Messaging}/Service/Builder/MessageContentBuilderTest.php (89%) rename tests/Unit/Domain/{ => Messaging}/Service/Builder/MessageFormatBuilderTest.php (88%) rename tests/Unit/Domain/{ => Messaging}/Service/Builder/MessageOptionsBuilderTest.php (90%) rename tests/Unit/Domain/{ => Messaging}/Service/Builder/MessageScheduleBuilderTest.php (91%) rename tests/Unit/Domain/{Service/Manager => Messaging/Service}/MessageManagerTest.php (82%) rename tests/Unit/Domain/{Service/Manager => Messaging/Service}/TemplateImageManagerTest.php (90%) rename tests/Unit/Domain/{Service/Manager => Messaging/Service}/TemplateManagerTest.php (84%) rename tests/Unit/Domain/{Service => Messaging}/Validator/TemplateImageValidatorTest.php (93%) rename tests/Unit/Domain/{Service => Messaging}/Validator/TemplateLinkValidatorTest.php (90%) rename tests/Unit/Domain/{Model/Subscription => Subscription/Model}/SubscriberTest.php (95%) rename tests/Unit/Domain/{Model/Subscription => Subscription/Model}/SubscriptionTest.php (86%) rename tests/Unit/Domain/{Repository/Subscription => Subscription/Repository}/SubscriberRepositoryTest.php (76%) rename tests/Unit/Domain/{Repository/Subscription => Subscription/Repository}/SubscriptionRepositoryTest.php (76%) rename tests/Unit/Domain/{Service/Manager => Subscription/Service}/AttributeDefinitionManagerTest.php (92%) rename tests/Unit/Domain/{Service/Manager => Subscription/Service}/SubscriberAttributeManagerTest.php (90%) rename tests/Unit/Domain/{Service/Manager => Subscription/Service}/SubscriberListManagerTest.php (84%) rename tests/Unit/Domain/{Service/Manager => Subscription/Service}/SubscriberManagerTest.php (81%) rename tests/Unit/Domain/{Service/Manager => Subscription/Service}/SubscriptionManagerTest.php (87%) diff --git a/config/doctrine.yml b/config/doctrine.yml index 10f7e1c0..327cf305 100644 --- a/config/doctrine.yml +++ b/config/doctrine.yml @@ -15,12 +15,12 @@ doctrine: orm: auto_generate_proxy_classes: '%kernel.debug%' naming_strategy: doctrine.orm.naming_strategy.underscore - auto_mapping: true + auto_mapping: false mappings: - PhpList\Core\Domain\Model: + Identity: is_bundle: false type: attribute - dir: '%kernel.project_dir%/src/Domain/Model/' - prefix: 'PhpList\Core\Domain\Model\' + dir: '%kernel.project_dir%/src/Domain/Identity/Model' + prefix: 'PhpList\Core\Domain\Identity\Model' controller_resolver: auto_mapping: false diff --git a/config/services.yml b/config/services.yml index 80369fd5..b83adce3 100644 --- a/config/services.yml +++ b/config/services.yml @@ -23,7 +23,7 @@ services: PhpList\Core\Routing\ExtraLoader: tags: [routing.loader] - PhpList\Core\Domain\Repository: + PhpList\Core\Domain\Common\Repository\AbstractRepository: abstract: true autowire: true autoconfigure: false diff --git a/config/services/builders.yml b/config/services/builders.yml index ab05342a..c18961d6 100644 --- a/config/services/builders.yml +++ b/config/services/builders.yml @@ -4,22 +4,22 @@ services: autoconfigure: true public: false - PhpList\Core\Domain\Service\Builder\MessageBuilder: + PhpList\Core\Domain\Messaging\Service\Builder\MessageBuilder: autowire: true autoconfigure: true - PhpList\Core\Domain\Service\Builder\MessageFormatBuilder: + PhpList\Core\Domain\Messaging\Service\Builder\MessageFormatBuilder: autowire: true autoconfigure: true - PhpList\Core\Domain\Service\Builder\MessageScheduleBuilder: + PhpList\Core\Domain\Messaging\Service\Builder\MessageScheduleBuilder: autowire: true autoconfigure: true - PhpList\Core\Domain\Service\Builder\MessageContentBuilder: + PhpList\Core\Domain\Messaging\Service\Builder\MessageContentBuilder: autowire: true autoconfigure: true - PhpListPhpList\Core\Domain\Service\Builder\MessageOptionsBuilder: + PhpListPhpList\Core\Domain\Messaging\Service\Builder\MessageOptionsBuilder: autowire: true autoconfigure: true diff --git a/config/services/managers.yml b/config/services/managers.yml index 4fc14a76..f2a9ed29 100644 --- a/config/services/managers.yml +++ b/config/services/managers.yml @@ -4,42 +4,42 @@ services: autoconfigure: true public: false - PhpList\Core\Domain\Service\Manager\SubscriberManager: + PhpList\Core\Domain\Subscription\Service\SubscriberManager: autowire: true autoconfigure: true - PhpList\Core\Domain\Service\Manager\SessionManager: + PhpList\Core\Domain\Identity\Service\SessionManager: autowire: true autoconfigure: true - PhpList\Core\Domain\Service\Manager\SubscriberListManager: + PhpList\Core\Domain\Subscription\Service\SubscriberListManager: autowire: true autoconfigure: true - PhpList\Core\Domain\Service\Manager\SubscriptionManager: + PhpList\Core\Domain\Subscription\Service\SubscriptionManager: autowire: true autoconfigure: true - PhpList\Core\Domain\Service\Manager\MessageManager: + PhpList\Core\Domain\Messaging\Service\MessageManager: autowire: true autoconfigure: true - PhpList\Core\Domain\Service\Manager\TemplateManager: + PhpList\Core\Domain\Messaging\Service\TemplateManager: autowire: true autoconfigure: true - PhpList\Core\Domain\Service\Manager\TemplateImageManager: + PhpList\Core\Domain\Messaging\Service\TemplateImageManager: autowire: true autoconfigure: true - PhpList\Core\Domain\Service\Manager\AdministratorManager: + PhpList\Core\Domain\Identity\Service\AdministratorManager: autowire: true autoconfigure: true - PhpList\Core\Domain\Service\Manager\AttributeDefinitionManager: + PhpList\Core\Domain\Subscription\Service\AttributeDefinitionManager: autowire: true autoconfigure: true - PhpList\Core\Domain\Service\Manager\SubscriberAttributeManager: + PhpList\Core\Domain\Subscription\Service\SubscriberAttributeManager: autowire: true autoconfigure: true diff --git a/config/services/repositories.yml b/config/services/repositories.yml index e7193a48..16831bd7 100644 --- a/config/services/repositories.yml +++ b/config/services/repositories.yml @@ -1,52 +1,52 @@ services: - PhpList\Core\Domain\Repository\Identity\AdministratorRepository: - parent: PhpList\Core\Domain\Repository + PhpList\Core\Domain\Identity\Repository\AdministratorRepository: + parent: PhpList\Core\Domain\Common\Repository\AbstractRepository arguments: - - PhpList\Core\Domain\Model\Identity\Administrator + - PhpList\Core\Domain\Identity\Model\Administrator - Doctrine\ORM\Mapping\ClassMetadata\ClassMetadata - PhpList\Core\Security\HashGenerator - PhpList\Core\Domain\Repository\Identity\AdministratorTokenRepository: - parent: PhpList\Core\Domain\Repository + PhpList\Core\Domain\Identity\Repository\AdministratorTokenRepository: + parent: PhpList\Core\Domain\Common\Repository\AbstractRepository arguments: - - PhpList\Core\Domain\Model\Identity\AdministratorToken + - PhpList\Core\Domain\Identity\Model\AdministratorToken - PhpList\Core\Domain\Repository\Subscription\SubscriberListRepository: - parent: PhpList\Core\Domain\Repository + PhpList\Core\Domain\Subscription\Repository\SubscriberListRepository: + parent: PhpList\Core\Domain\Common\Repository\AbstractRepository arguments: - - PhpList\Core\Domain\Model\Subscription\SubscriberList + - PhpList\Core\Domain\Subscription\Model\SubscriberList - PhpList\Core\Domain\Repository\Subscription\SubscriberRepository: - parent: PhpList\Core\Domain\Repository + PhpList\Core\Domain\Subscription\Repository\SubscriberRepository: + parent: PhpList\Core\Domain\Common\Repository\AbstractRepository arguments: - - PhpList\Core\Domain\Model\Subscription\Subscriber + - PhpList\Core\Domain\Subscription\Model\Subscriber - PhpList\Core\Domain\Repository\Subscription\SubscriberAttributeValueRepository: - parent: PhpList\Core\Domain\Repository + PhpList\Core\Domain\Subscription\Repository\SubscriberAttributeValueRepository: + parent: PhpList\Core\Domain\Common\Repository\AbstractRepository arguments: - - PhpList\Core\Domain\Model\Subscription\SubscriberAttributeValue + - PhpList\Core\Domain\Subscription\Model\SubscriberAttributeValue - PhpList\Core\Domain\Repository\Subscription\SubscriberAttributeDefinitionRepository: - parent: PhpList\Core\Domain\Repository + PhpList\Core\Domain\Subscription\Repository\SubscriberAttributeDefinitionRepository: + parent: PhpList\Core\Domain\Common\Repository\AbstractRepository arguments: - - PhpList\Core\Domain\Model\Subscription\SubscriberAttributeDefinition + - PhpList\Core\Domain\Subscription\Model\SubscriberAttributeDefinition - PhpList\Core\Domain\Repository\Subscription\SubscriptionRepository: - parent: PhpList\Core\Domain\Repository + PhpList\Core\Domain\Subscription\Repository\SubscriptionRepository: + parent: PhpList\Core\Domain\Common\Repository\AbstractRepository arguments: - - PhpList\Core\Domain\Model\Subscription\Subscription + - PhpList\Core\Domain\Subscription\Model\Subscription - PhpList\Core\Domain\Repository\Messaging\MessageRepository: - parent: PhpList\Core\Domain\Repository + PhpList\Core\Domain\Messaging\Repository\MessageRepository: + parent: PhpList\Core\Domain\Common\Repository\AbstractRepository arguments: - - PhpList\Core\Domain\Model\Messaging\Message + - PhpList\Core\Domain\Messaging\Model\Message - PhpList\Core\Domain\Repository\Messaging\TemplateRepository: - parent: PhpList\Core\Domain\Repository + PhpList\Core\Domain\Messaging\Repository\TemplateRepository: + parent: PhpList\Core\Domain\Common\Repository\AbstractRepository arguments: - - PhpList\Core\Domain\Model\Messaging\Template + - PhpList\Core\Domain\Messaging\Model\Template - PhpList\Core\Domain\Repository\Messaging\TemplateImageRepository: - parent: PhpList\Core\Domain\Repository + PhpList\Core\Domain\Messaging\Repository\TemplateImageRepository: + parent: PhpList\Core\Domain\Common\Repository\AbstractRepository arguments: - - PhpList\Core\Domain\Model\Messaging\TemplateImage + - PhpList\Core\Domain\Messaging\Model\TemplateImage diff --git a/config/services/validators.yml b/config/services/validators.yml index de7e4912..77749d39 100644 --- a/config/services/validators.yml +++ b/config/services/validators.yml @@ -1,8 +1,8 @@ services: - PhpList\Core\Domain\Service\Validator\TemplateLinkValidator: + PhpList\Core\Domain\Subscription\Validator\TemplateLinkValidator: autowire: true autoconfigure: true - PhpList\Core\Domain\Service\Validator\TemplateImageValidator: + PhpList\Core\Domain\Subscription\Validator\TemplateImageValidator: autowire: true autoconfigure: true diff --git a/src/Core/ApplicationKernel.php b/src/Core/ApplicationKernel.php index 695520a1..1d3c64ab 100644 --- a/src/Core/ApplicationKernel.php +++ b/src/Core/ApplicationKernel.php @@ -105,6 +105,7 @@ private function getAndCreateApplicationStructure(): ApplicationStructure protected function build(ContainerBuilder $container): void { $container->setParameter('kernel.application_dir', $this->getApplicationDir()); + $container->addCompilerPass(new DoctrineMappingPass()); } /** diff --git a/src/Core/DoctrineMappingPass.php b/src/Core/DoctrineMappingPass.php new file mode 100644 index 00000000..3d60a886 --- /dev/null +++ b/src/Core/DoctrineMappingPass.php @@ -0,0 +1,45 @@ +getParameter('kernel.project_dir'); + $basePath = $projectDir . '/src/Domain'; + + $driverDefinition = $container->getDefinition('doctrine.orm.default_metadata_driver'); + + foreach (scandir($basePath) as $dir) { + if ($dir === '.' || $dir === '..') { + continue; + } + + $modelPath = $basePath . '/' . $dir . '/Model'; + if (!is_dir($modelPath)) { + continue; + } + + $namespace = 'PhpList\\Core\\Domain\\' . $dir . '\\Model'; + + $attributeDriverDef = new Definition(AttributeDriver::class, [[$modelPath]]); + $attributeDriverId = 'doctrine.orm.driver.' . $dir; + + $container->setDefinition($attributeDriverId, $attributeDriverDef); + + $driverDefinition->addMethodCall('addDriver', [ + new Reference($attributeDriverId), + $namespace, + ]); + } + } +} diff --git a/src/Domain/Model/Analytics/LinkTrack.php b/src/Domain/Analytics/Model/LinkTrack.php similarity index 92% rename from src/Domain/Model/Analytics/LinkTrack.php rename to src/Domain/Analytics/Model/LinkTrack.php index af8219bc..2dc32de6 100644 --- a/src/Domain/Model/Analytics/LinkTrack.php +++ b/src/Domain/Analytics/Model/LinkTrack.php @@ -2,13 +2,13 @@ declare(strict_types=1); -namespace PhpList\Core\Domain\Model\Analytics; +namespace PhpList\Core\Domain\Analytics\Model; use DateTimeInterface; use Doctrine\ORM\Mapping as ORM; -use PhpList\Core\Domain\Model\Interfaces\DomainModel; -use PhpList\Core\Domain\Model\Interfaces\Identity; -use PhpList\Core\Domain\Repository\Analytics\LinkTrackRepository; +use PhpList\Core\Domain\Analytics\Repository\LinkTrackRepository; +use PhpList\Core\Domain\Common\Model\Interfaces\DomainModel; +use PhpList\Core\Domain\Common\Model\Interfaces\Identity; #[ORM\Entity(repositoryClass: LinkTrackRepository::class)] #[ORM\Table(name: 'phplist_linktrack')] diff --git a/src/Domain/Model/Analytics/LinkTrackForward.php b/src/Domain/Analytics/Model/LinkTrackForward.php similarity index 89% rename from src/Domain/Model/Analytics/LinkTrackForward.php rename to src/Domain/Analytics/Model/LinkTrackForward.php index 213ff342..0dcf7d55 100644 --- a/src/Domain/Model/Analytics/LinkTrackForward.php +++ b/src/Domain/Analytics/Model/LinkTrackForward.php @@ -2,12 +2,12 @@ declare(strict_types=1); -namespace PhpList\Core\Domain\Model\Analytics; +namespace PhpList\Core\Domain\Analytics\Model; use Doctrine\ORM\Mapping as ORM; -use PhpList\Core\Domain\Model\Interfaces\DomainModel; -use PhpList\Core\Domain\Model\Interfaces\Identity; -use PhpList\Core\Domain\Repository\Analytics\LinkTrackForwardRepository; +use PhpList\Core\Domain\Analytics\Repository\LinkTrackForwardRepository; +use PhpList\Core\Domain\Common\Model\Interfaces\DomainModel; +use PhpList\Core\Domain\Common\Model\Interfaces\Identity; #[ORM\Entity(repositoryClass: LinkTrackForwardRepository::class)] #[ORM\Table(name: 'phplist_linktrack_forward')] diff --git a/src/Domain/Model/Analytics/LinkTrackMl.php b/src/Domain/Analytics/Model/LinkTrackMl.php similarity index 94% rename from src/Domain/Model/Analytics/LinkTrackMl.php rename to src/Domain/Analytics/Model/LinkTrackMl.php index 72bdabcc..8569fa14 100644 --- a/src/Domain/Model/Analytics/LinkTrackMl.php +++ b/src/Domain/Analytics/Model/LinkTrackMl.php @@ -2,12 +2,12 @@ declare(strict_types=1); -namespace PhpList\Core\Domain\Model\Analytics; +namespace PhpList\Core\Domain\Analytics\Model; use DateTimeInterface; use Doctrine\ORM\Mapping as ORM; -use PhpList\Core\Domain\Model\Interfaces\DomainModel; -use PhpList\Core\Domain\Repository\Analytics\LinkTrackMlRepository; +use PhpList\Core\Domain\Analytics\Repository\LinkTrackMlRepository; +use PhpList\Core\Domain\Common\Model\Interfaces\DomainModel; #[ORM\Entity(repositoryClass: LinkTrackMlRepository::class)] #[ORM\Table(name: 'phplist_linktrack_ml')] diff --git a/src/Domain/Model/Analytics/LinkTrackUmlClick.php b/src/Domain/Analytics/Model/LinkTrackUmlClick.php similarity index 93% rename from src/Domain/Model/Analytics/LinkTrackUmlClick.php rename to src/Domain/Analytics/Model/LinkTrackUmlClick.php index 8cff6c1b..2b8c8068 100644 --- a/src/Domain/Model/Analytics/LinkTrackUmlClick.php +++ b/src/Domain/Analytics/Model/LinkTrackUmlClick.php @@ -2,13 +2,13 @@ declare(strict_types=1); -namespace PhpList\Core\Domain\Model\Analytics; +namespace PhpList\Core\Domain\Analytics\Model; use DateTimeInterface; use Doctrine\ORM\Mapping as ORM; -use PhpList\Core\Domain\Model\Interfaces\DomainModel; -use PhpList\Core\Domain\Model\Interfaces\Identity; -use PhpList\Core\Domain\Repository\Analytics\LinkTrackUmlClickRepository; +use PhpList\Core\Domain\Analytics\Repository\LinkTrackUmlClickRepository; +use PhpList\Core\Domain\Common\Model\Interfaces\DomainModel; +use PhpList\Core\Domain\Common\Model\Interfaces\Identity; #[ORM\Entity(repositoryClass: LinkTrackUmlClickRepository::class)] #[ORM\Table(name: 'phplist_linktrack_uml_click')] diff --git a/src/Domain/Model/Analytics/LinkTrackUserClick.php b/src/Domain/Analytics/Model/LinkTrackUserClick.php similarity index 93% rename from src/Domain/Model/Analytics/LinkTrackUserClick.php rename to src/Domain/Analytics/Model/LinkTrackUserClick.php index e2e33cce..464ae3e0 100644 --- a/src/Domain/Model/Analytics/LinkTrackUserClick.php +++ b/src/Domain/Analytics/Model/LinkTrackUserClick.php @@ -2,12 +2,12 @@ declare(strict_types=1); -namespace PhpList\Core\Domain\Model\Analytics; +namespace PhpList\Core\Domain\Analytics\Model; use DateTimeInterface; use Doctrine\ORM\Mapping as ORM; -use PhpList\Core\Domain\Model\Interfaces\DomainModel; -use PhpList\Core\Domain\Repository\Analytics\LinkTrackUserClickRepository; +use PhpList\Core\Domain\Analytics\Repository\LinkTrackUserClickRepository; +use PhpList\Core\Domain\Common\Model\Interfaces\DomainModel; #[ORM\Entity(repositoryClass: LinkTrackUserClickRepository::class)] #[ORM\Table(name: 'phplist_linktrack_userclick')] diff --git a/src/Domain/Model/Analytics/UserMessageView.php b/src/Domain/Analytics/Model/UserMessageView.php similarity index 90% rename from src/Domain/Model/Analytics/UserMessageView.php rename to src/Domain/Analytics/Model/UserMessageView.php index d933e545..6b80b75e 100644 --- a/src/Domain/Model/Analytics/UserMessageView.php +++ b/src/Domain/Analytics/Model/UserMessageView.php @@ -2,13 +2,13 @@ declare(strict_types=1); -namespace PhpList\Core\Domain\Model\Analytics; +namespace PhpList\Core\Domain\Analytics\Model; use DateTime; use Doctrine\ORM\Mapping as ORM; -use PhpList\Core\Domain\Model\Interfaces\DomainModel; -use PhpList\Core\Domain\Model\Interfaces\Identity; -use PhpList\Core\Domain\Repository\Analytics\UserMessageViewRepository; +use PhpList\Core\Domain\Analytics\Repository\UserMessageViewRepository; +use PhpList\Core\Domain\Common\Model\Interfaces\DomainModel; +use PhpList\Core\Domain\Common\Model\Interfaces\Identity; #[ORM\Entity(repositoryClass: UserMessageViewRepository::class)] #[ORM\Table(name: 'phplist_user_message_view')] diff --git a/src/Domain/Model/Analytics/UserStats.php b/src/Domain/Analytics/Model/UserStats.php similarity index 89% rename from src/Domain/Model/Analytics/UserStats.php rename to src/Domain/Analytics/Model/UserStats.php index 4907bb52..a2f835bf 100644 --- a/src/Domain/Model/Analytics/UserStats.php +++ b/src/Domain/Analytics/Model/UserStats.php @@ -2,12 +2,12 @@ declare(strict_types=1); -namespace PhpList\Core\Domain\Model\Analytics; +namespace PhpList\Core\Domain\Analytics\Model; use Doctrine\ORM\Mapping as ORM; -use PhpList\Core\Domain\Model\Interfaces\DomainModel; -use PhpList\Core\Domain\Model\Interfaces\Identity; -use PhpList\Core\Domain\Repository\Analytics\UserStatsRepository; +use PhpList\Core\Domain\Analytics\Repository\UserStatsRepository; +use PhpList\Core\Domain\Common\Model\Interfaces\DomainModel; +use PhpList\Core\Domain\Common\Model\Interfaces\Identity; #[ORM\Entity(repositoryClass: UserStatsRepository::class)] #[ORM\Table(name: 'phplist_userstats')] diff --git a/src/Domain/Analytics/Repository/LinkTrackForwardRepository.php b/src/Domain/Analytics/Repository/LinkTrackForwardRepository.php new file mode 100644 index 00000000..38de3f74 --- /dev/null +++ b/src/Domain/Analytics/Repository/LinkTrackForwardRepository.php @@ -0,0 +1,14 @@ +createMock(EntityManager::class); $classMetadata = $this->createMock(ClassMetadata::class); - $classMetadata->name = 'PhpList\Core\Domain\Model\Identity\Administrator'; + $classMetadata->name = Administrator::class; $this->subject = new AdministratorRepository($entityManager, $classMetadata); } diff --git a/tests/Unit/Domain/Repository/Identity/AdministratorTokenRepositoryTest.php b/tests/Unit/Domain/Identity/Rpository/AdministratorTokenRepositoryTest.php similarity index 76% rename from tests/Unit/Domain/Repository/Identity/AdministratorTokenRepositoryTest.php rename to tests/Unit/Domain/Identity/Rpository/AdministratorTokenRepositoryTest.php index 1b913b40..408f75ca 100644 --- a/tests/Unit/Domain/Repository/Identity/AdministratorTokenRepositoryTest.php +++ b/tests/Unit/Domain/Identity/Rpository/AdministratorTokenRepositoryTest.php @@ -2,12 +2,13 @@ declare(strict_types=1); -namespace PhpList\Core\Tests\Unit\Domain\Repository\Identity; +namespace PhpList\Core\Tests\Unit\Domain\Identity\Rpository; use Doctrine\ORM\EntityManager; use Doctrine\ORM\EntityRepository; use Doctrine\ORM\Mapping\ClassMetadata; -use PhpList\Core\Domain\Repository\Identity\AdministratorTokenRepository; +use PhpList\Core\Domain\Identity\Model\AdministratorToken; +use PhpList\Core\Domain\Identity\Repository\AdministratorTokenRepository; use PHPUnit\Framework\TestCase; /** @@ -23,7 +24,7 @@ protected function setUp(): void { $entityManager = $this->createMock(EntityManager::class); $classMetadata = $this->createMock(ClassMetadata::class); - $classMetadata->name = 'PhpList\Core\Domain\Model\Identity\AdministratorToken'; + $classMetadata->name = AdministratorToken::class; $this->subject = new AdministratorTokenRepository($entityManager, $classMetadata); } diff --git a/tests/Unit/Domain/Service/Manager/AdministratorManagerTest.php b/tests/Unit/Domain/Identity/Service/AdministratorManagerTest.php similarity index 91% rename from tests/Unit/Domain/Service/Manager/AdministratorManagerTest.php rename to tests/Unit/Domain/Identity/Service/AdministratorManagerTest.php index f61d3b80..11c3f378 100644 --- a/tests/Unit/Domain/Service/Manager/AdministratorManagerTest.php +++ b/tests/Unit/Domain/Identity/Service/AdministratorManagerTest.php @@ -2,13 +2,13 @@ declare(strict_types=1); -namespace PhpList\Core\Tests\Unit\Domain\Service\Manager; +namespace PhpList\Core\Tests\Unit\Domain\Identity\Service; use Doctrine\ORM\EntityManagerInterface; -use PhpList\Core\Domain\Model\Identity\Administrator; -use PhpList\Core\Domain\Model\Identity\Dto\CreateAdministratorDto; -use PhpList\Core\Domain\Model\Identity\Dto\UpdateAdministratorDto; -use PhpList\Core\Domain\Service\Manager\AdministratorManager; +use PhpList\Core\Domain\Identity\Model\Administrator; +use PhpList\Core\Domain\Identity\Model\Dto\CreateAdministratorDto; +use PhpList\Core\Domain\Identity\Model\Dto\UpdateAdministratorDto; +use PhpList\Core\Domain\Identity\Service\AdministratorManager; use PhpList\Core\Security\HashGenerator; use PHPUnit\Framework\TestCase; diff --git a/tests/Unit/Domain/Service/Manager/SessionManagerTest.php b/tests/Unit/Domain/Identity/Service/SessionManagerTest.php similarity index 82% rename from tests/Unit/Domain/Service/Manager/SessionManagerTest.php rename to tests/Unit/Domain/Identity/Service/SessionManagerTest.php index 02b1266b..44072452 100644 --- a/tests/Unit/Domain/Service/Manager/SessionManagerTest.php +++ b/tests/Unit/Domain/Identity/Service/SessionManagerTest.php @@ -2,12 +2,12 @@ declare(strict_types=1); -namespace PhpList\Core\Tests\Unit\Domain\Service\Manager; +namespace PhpList\Core\Tests\Unit\Domain\Identity\Service; -use PhpList\Core\Domain\Model\Identity\AdministratorToken; -use PhpList\Core\Domain\Repository\Identity\AdministratorRepository; -use PhpList\Core\Domain\Repository\Identity\AdministratorTokenRepository; -use PhpList\Core\Domain\Service\Manager\SessionManager; +use PhpList\Core\Domain\Identity\Model\AdministratorToken; +use PhpList\Core\Domain\Identity\Repository\AdministratorRepository; +use PhpList\Core\Domain\Identity\Repository\AdministratorTokenRepository; +use PhpList\Core\Domain\Identity\Service\SessionManager; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException; diff --git a/tests/Unit/Domain/Model/Messaging/MessageTest.php b/tests/Unit/Domain/Messaging/Model/MessageTest.php similarity index 79% rename from tests/Unit/Domain/Model/Messaging/MessageTest.php rename to tests/Unit/Domain/Messaging/Model/MessageTest.php index 92ec6245..0201f08b 100644 --- a/tests/Unit/Domain/Model/Messaging/MessageTest.php +++ b/tests/Unit/Domain/Messaging/Model/MessageTest.php @@ -2,19 +2,19 @@ declare(strict_types=1); -namespace PhpList\Core\Tests\Unit\Domain\Model\Messaging; +namespace PhpList\Core\Tests\Unit\Domain\Messaging\Model; use DateTime; -use PhpList\Core\Domain\Model\Identity\Administrator; -use PhpList\Core\Domain\Model\Messaging\Message; -use PhpList\Core\Domain\Model\Messaging\Message\MessageContent; -use PhpList\Core\Domain\Model\Messaging\Message\MessageFormat; -use PhpList\Core\Domain\Model\Messaging\Message\MessageMetadata; -use PhpList\Core\Domain\Model\Messaging\Message\MessageOptions; -use PhpList\Core\Domain\Model\Messaging\Message\MessageSchedule; -use PhpList\Core\Domain\Model\Interfaces\DomainModel; -use PhpList\Core\Domain\Model\Interfaces\Identity; -use PhpList\Core\Domain\Model\Interfaces\ModificationDate; +use PhpList\Core\Domain\Common\Model\Interfaces\DomainModel; +use PhpList\Core\Domain\Common\Model\Interfaces\Identity; +use PhpList\Core\Domain\Common\Model\Interfaces\ModificationDate; +use PhpList\Core\Domain\Identity\Model\Administrator; +use PhpList\Core\Domain\Messaging\Model\Message; +use PhpList\Core\Domain\Messaging\Model\Message\MessageContent; +use PhpList\Core\Domain\Messaging\Model\Message\MessageFormat; +use PhpList\Core\Domain\Messaging\Model\Message\MessageMetadata; +use PhpList\Core\Domain\Messaging\Model\Message\MessageOptions; +use PhpList\Core\Domain\Messaging\Model\Message\MessageSchedule; use PHPUnit\Framework\TestCase; class MessageTest extends TestCase diff --git a/tests/Unit/Domain/Model/Messaging/SubscriberListTest.php b/tests/Unit/Domain/Messaging/Model/SubscriberListTest.php similarity index 93% rename from tests/Unit/Domain/Model/Messaging/SubscriberListTest.php rename to tests/Unit/Domain/Messaging/Model/SubscriberListTest.php index 8b7afa5b..e62dfc22 100644 --- a/tests/Unit/Domain/Model/Messaging/SubscriberListTest.php +++ b/tests/Unit/Domain/Messaging/Model/SubscriberListTest.php @@ -2,15 +2,15 @@ declare(strict_types=1); -namespace PhpList\Core\Tests\Unit\Domain\Model\Messaging; +namespace PhpList\Core\Tests\Unit\Domain\Messaging\Model; use DateTime; use Doctrine\Common\Collections\Collection; -use PhpList\Core\Domain\Model\Identity\Administrator; -use PhpList\Core\Domain\Model\Interfaces\DomainModel; -use PhpList\Core\Domain\Model\Subscription\Subscriber; -use PhpList\Core\Domain\Model\Subscription\SubscriberList; -use PhpList\Core\Domain\Model\Subscription\Subscription; +use PhpList\Core\Domain\Common\Model\Interfaces\DomainModel; +use PhpList\Core\Domain\Identity\Model\Administrator; +use PhpList\Core\Domain\Subscription\Model\Subscriber; +use PhpList\Core\Domain\Subscription\Model\SubscriberList; +use PhpList\Core\Domain\Subscription\Model\Subscription; use PhpList\Core\TestingSupport\Traits\ModelTestTrait; use PhpList\Core\TestingSupport\Traits\SimilarDatesAssertionTrait; use PHPUnit\Framework\TestCase; diff --git a/tests/Unit/Domain/Repository/Messaging/MessageRepositoryTest.php b/tests/Unit/Domain/Messaging/Repository/MessageRepositoryTest.php similarity index 86% rename from tests/Unit/Domain/Repository/Messaging/MessageRepositoryTest.php rename to tests/Unit/Domain/Messaging/Repository/MessageRepositoryTest.php index 067ecb73..c593a9ef 100644 --- a/tests/Unit/Domain/Repository/Messaging/MessageRepositoryTest.php +++ b/tests/Unit/Domain/Messaging/Repository/MessageRepositoryTest.php @@ -2,12 +2,12 @@ declare(strict_types=1); -namespace PhpList\Core\Tests\Unit\Domain\Repository\Messaging; +namespace PhpList\Core\Tests\Unit\Domain\Messaging\Repository; use Doctrine\ORM\EntityManager; use Doctrine\ORM\EntityRepository; use Doctrine\ORM\Mapping\ClassMetadata; -use PhpList\Core\Domain\Repository\Messaging\MessageRepository; +use PhpList\Core\Domain\Messaging\Repository\MessageRepository; use PHPUnit\Framework\TestCase; /** diff --git a/tests/Unit/Domain/Repository/Messaging/SubscriberListRepositoryTest.php b/tests/Unit/Domain/Messaging/Repository/SubscriberListRepositoryTest.php similarity index 81% rename from tests/Unit/Domain/Repository/Messaging/SubscriberListRepositoryTest.php rename to tests/Unit/Domain/Messaging/Repository/SubscriberListRepositoryTest.php index f1f6a56b..f7808e2b 100644 --- a/tests/Unit/Domain/Repository/Messaging/SubscriberListRepositoryTest.php +++ b/tests/Unit/Domain/Messaging/Repository/SubscriberListRepositoryTest.php @@ -2,13 +2,13 @@ declare(strict_types=1); -namespace PhpList\Core\Tests\Unit\Domain\Repository\Messaging; +namespace PhpList\Core\Tests\Unit\Domain\Messaging\Repository; use Doctrine\ORM\EntityManager; use Doctrine\ORM\EntityRepository; use Doctrine\ORM\Mapping\ClassMetadata; -use PhpList\Core\Domain\Model\Subscription\SubscriberList; -use PhpList\Core\Domain\Repository\Subscription\SubscriberListRepository; +use PhpList\Core\Domain\Subscription\Model\SubscriberList; +use PhpList\Core\Domain\Subscription\Repository\SubscriberListRepository; use PHPUnit\Framework\TestCase; /** diff --git a/tests/Unit/Domain/Service/Builder/MessageBuilderTest.php b/tests/Unit/Domain/Messaging/Service/Builder/MessageBuilderTest.php similarity index 77% rename from tests/Unit/Domain/Service/Builder/MessageBuilderTest.php rename to tests/Unit/Domain/Messaging/Service/Builder/MessageBuilderTest.php index 9d3eab89..564bd34d 100644 --- a/tests/Unit/Domain/Service/Builder/MessageBuilderTest.php +++ b/tests/Unit/Domain/Messaging/Service/Builder/MessageBuilderTest.php @@ -6,21 +6,21 @@ use Error; use InvalidArgumentException; -use PhpList\Core\Domain\Model\Dto\MessageContext; -use PhpList\Core\Domain\Model\Identity\Administrator; -use PhpList\Core\Domain\Model\Messaging\Dto\CreateMessageDto; -use PhpList\Core\Domain\Model\Messaging\Dto\Message\MessageContentDto; -use PhpList\Core\Domain\Model\Messaging\Dto\Message\MessageFormatDto; -use PhpList\Core\Domain\Model\Messaging\Dto\Message\MessageMetadataDto; -use PhpList\Core\Domain\Model\Messaging\Dto\Message\MessageOptionsDto; -use PhpList\Core\Domain\Model\Messaging\Dto\Message\MessageScheduleDto; -use PhpList\Core\Domain\Model\Messaging\Message; -use PhpList\Core\Domain\Repository\Messaging\TemplateRepository; -use PhpList\Core\Domain\Service\Builder\MessageBuilder; -use PhpList\Core\Domain\Service\Builder\MessageContentBuilder; -use PhpList\Core\Domain\Service\Builder\MessageFormatBuilder; -use PhpList\Core\Domain\Service\Builder\MessageOptionsBuilder; -use PhpList\Core\Domain\Service\Builder\MessageScheduleBuilder; +use PhpList\Core\Domain\Identity\Model\Administrator; +use PhpList\Core\Domain\Messaging\Model\Dto\CreateMessageDto; +use PhpList\Core\Domain\Messaging\Model\Dto\Message\MessageContentDto; +use PhpList\Core\Domain\Messaging\Model\Dto\Message\MessageFormatDto; +use PhpList\Core\Domain\Messaging\Model\Dto\Message\MessageMetadataDto; +use PhpList\Core\Domain\Messaging\Model\Dto\Message\MessageOptionsDto; +use PhpList\Core\Domain\Messaging\Model\Dto\Message\MessageScheduleDto; +use PhpList\Core\Domain\Messaging\Model\Dto\MessageContext; +use PhpList\Core\Domain\Messaging\Model\Message; +use PhpList\Core\Domain\Messaging\Repository\TemplateRepository; +use PhpList\Core\Domain\Messaging\Service\Builder\MessageBuilder; +use PhpList\Core\Domain\Messaging\Service\Builder\MessageContentBuilder; +use PhpList\Core\Domain\Messaging\Service\Builder\MessageFormatBuilder; +use PhpList\Core\Domain\Messaging\Service\Builder\MessageOptionsBuilder; +use PhpList\Core\Domain\Messaging\Service\Builder\MessageScheduleBuilder; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -93,12 +93,12 @@ private function mockBuildCalls(CreateMessageDto $createMessageDto): void $this->scheduleBuilder->expects($this->once()) ->method('build') ->with($createMessageDto->schedule) - ->willReturn($this->createMock(Message\MessageSchedule::class)); + ->willReturn($this->createMock(\PhpList\Core\Domain\Messaging\Model\Message\MessageSchedule::class)); $this->contentBuilder->expects($this->once()) ->method('build') ->with($createMessageDto->content) - ->willReturn($this->createMock(Message\MessageContent::class)); + ->willReturn($this->createMock(\PhpList\Core\Domain\Messaging\Model\Message\MessageContent::class)); $this->optionsBuilder->expects($this->once()) ->method('build') @@ -150,11 +150,11 @@ public function testUpdatesExistingMessage(): void $existingMessage ->expects($this->once()) ->method('setSchedule') - ->with($this->isInstanceOf(Message\MessageSchedule::class)); + ->with($this->isInstanceOf(\PhpList\Core\Domain\Messaging\Model\Message\MessageSchedule::class)); $existingMessage ->expects($this->once()) ->method('setContent') - ->with($this->isInstanceOf(Message\MessageContent::class)); + ->with($this->isInstanceOf(\PhpList\Core\Domain\Messaging\Model\Message\MessageContent::class)); $existingMessage ->expects($this->once()) ->method('setOptions') diff --git a/tests/Unit/Domain/Service/Builder/MessageContentBuilderTest.php b/tests/Unit/Domain/Messaging/Service/Builder/MessageContentBuilderTest.php similarity index 89% rename from tests/Unit/Domain/Service/Builder/MessageContentBuilderTest.php rename to tests/Unit/Domain/Messaging/Service/Builder/MessageContentBuilderTest.php index e9c63112..2b1aa771 100644 --- a/tests/Unit/Domain/Service/Builder/MessageContentBuilderTest.php +++ b/tests/Unit/Domain/Messaging/Service/Builder/MessageContentBuilderTest.php @@ -5,8 +5,8 @@ namespace PhpList\Core\Tests\Unit\Domain\Service\Builder; use InvalidArgumentException; -use PhpList\Core\Domain\Model\Messaging\Dto\Message\MessageContentDto; -use PhpList\Core\Domain\Service\Builder\MessageContentBuilder; +use PhpList\Core\Domain\Messaging\Model\Dto\Message\MessageContentDto; +use PhpList\Core\Domain\Messaging\Service\Builder\MessageContentBuilder; use PHPUnit\Framework\TestCase; class MessageContentBuilderTest extends TestCase diff --git a/tests/Unit/Domain/Service/Builder/MessageFormatBuilderTest.php b/tests/Unit/Domain/Messaging/Service/Builder/MessageFormatBuilderTest.php similarity index 88% rename from tests/Unit/Domain/Service/Builder/MessageFormatBuilderTest.php rename to tests/Unit/Domain/Messaging/Service/Builder/MessageFormatBuilderTest.php index acca15e4..8d9320a0 100644 --- a/tests/Unit/Domain/Service/Builder/MessageFormatBuilderTest.php +++ b/tests/Unit/Domain/Messaging/Service/Builder/MessageFormatBuilderTest.php @@ -5,8 +5,8 @@ namespace PhpList\Core\Tests\Unit\Domain\Service\Builder; use InvalidArgumentException; -use PhpList\Core\Domain\Model\Messaging\Dto\Message\MessageFormatDto; -use PhpList\Core\Domain\Service\Builder\MessageFormatBuilder; +use PhpList\Core\Domain\Messaging\Model\Dto\Message\MessageFormatDto; +use PhpList\Core\Domain\Messaging\Service\Builder\MessageFormatBuilder; use PHPUnit\Framework\TestCase; class MessageFormatBuilderTest extends TestCase diff --git a/tests/Unit/Domain/Service/Builder/MessageOptionsBuilderTest.php b/tests/Unit/Domain/Messaging/Service/Builder/MessageOptionsBuilderTest.php similarity index 90% rename from tests/Unit/Domain/Service/Builder/MessageOptionsBuilderTest.php rename to tests/Unit/Domain/Messaging/Service/Builder/MessageOptionsBuilderTest.php index 03eeaf21..c6795d29 100644 --- a/tests/Unit/Domain/Service/Builder/MessageOptionsBuilderTest.php +++ b/tests/Unit/Domain/Messaging/Service/Builder/MessageOptionsBuilderTest.php @@ -5,8 +5,8 @@ namespace PhpList\Core\Tests\Unit\Domain\Service\Builder; use InvalidArgumentException; -use PhpList\Core\Domain\Model\Messaging\Dto\Message\MessageOptionsDto; -use PhpList\Core\Domain\Service\Builder\MessageOptionsBuilder; +use PhpList\Core\Domain\Messaging\Model\Dto\Message\MessageOptionsDto; +use PhpList\Core\Domain\Messaging\Service\Builder\MessageOptionsBuilder; use PHPUnit\Framework\TestCase; class MessageOptionsBuilderTest extends TestCase diff --git a/tests/Unit/Domain/Service/Builder/MessageScheduleBuilderTest.php b/tests/Unit/Domain/Messaging/Service/Builder/MessageScheduleBuilderTest.php similarity index 91% rename from tests/Unit/Domain/Service/Builder/MessageScheduleBuilderTest.php rename to tests/Unit/Domain/Messaging/Service/Builder/MessageScheduleBuilderTest.php index b21d810c..38f04338 100644 --- a/tests/Unit/Domain/Service/Builder/MessageScheduleBuilderTest.php +++ b/tests/Unit/Domain/Messaging/Service/Builder/MessageScheduleBuilderTest.php @@ -6,8 +6,8 @@ use DateTime; use InvalidArgumentException; -use PhpList\Core\Domain\Model\Messaging\Dto\Message\MessageScheduleDto; -use PhpList\Core\Domain\Service\Builder\MessageScheduleBuilder; +use PhpList\Core\Domain\Messaging\Model\Dto\Message\MessageScheduleDto; +use PhpList\Core\Domain\Messaging\Service\Builder\MessageScheduleBuilder; use PHPUnit\Framework\TestCase; class MessageScheduleBuilderTest extends TestCase diff --git a/tests/Unit/Domain/Service/Manager/MessageManagerTest.php b/tests/Unit/Domain/Messaging/Service/MessageManagerTest.php similarity index 82% rename from tests/Unit/Domain/Service/Manager/MessageManagerTest.php rename to tests/Unit/Domain/Messaging/Service/MessageManagerTest.php index e9ac714f..8ee85915 100644 --- a/tests/Unit/Domain/Service/Manager/MessageManagerTest.php +++ b/tests/Unit/Domain/Messaging/Service/MessageManagerTest.php @@ -2,20 +2,20 @@ declare(strict_types=1); -namespace PhpList\Core\Tests\Unit\Domain\Service\Manager; - -use PhpList\Core\Domain\Model\Identity\Administrator; -use PhpList\Core\Domain\Model\Messaging\Dto\CreateMessageDto; -use PhpList\Core\Domain\Model\Messaging\Dto\Message\MessageContentDto; -use PhpList\Core\Domain\Model\Messaging\Dto\Message\MessageFormatDto; -use PhpList\Core\Domain\Model\Messaging\Dto\Message\MessageMetadataDto; -use PhpList\Core\Domain\Model\Messaging\Dto\Message\MessageOptionsDto; -use PhpList\Core\Domain\Model\Messaging\Dto\Message\MessageScheduleDto; -use PhpList\Core\Domain\Model\Messaging\Dto\UpdateMessageDto; -use PhpList\Core\Domain\Model\Messaging\Message; -use PhpList\Core\Domain\Repository\Messaging\MessageRepository; -use PhpList\Core\Domain\Service\Builder\MessageBuilder; -use PhpList\Core\Domain\Service\Manager\MessageManager; +namespace PhpList\Core\Tests\Unit\Domain\Messaging\Service; + +use PhpList\Core\Domain\Identity\Model\Administrator; +use PhpList\Core\Domain\Messaging\Model\Dto\CreateMessageDto; +use PhpList\Core\Domain\Messaging\Model\Dto\Message\MessageContentDto; +use PhpList\Core\Domain\Messaging\Model\Dto\Message\MessageFormatDto; +use PhpList\Core\Domain\Messaging\Model\Dto\Message\MessageMetadataDto; +use PhpList\Core\Domain\Messaging\Model\Dto\Message\MessageOptionsDto; +use PhpList\Core\Domain\Messaging\Model\Dto\Message\MessageScheduleDto; +use PhpList\Core\Domain\Messaging\Model\Dto\UpdateMessageDto; +use PhpList\Core\Domain\Messaging\Model\Message; +use PhpList\Core\Domain\Messaging\Repository\MessageRepository; +use PhpList\Core\Domain\Messaging\Service\Builder\MessageBuilder; +use PhpList\Core\Domain\Messaging\Service\MessageManager; use PHPUnit\Framework\TestCase; class MessageManagerTest extends TestCase @@ -50,7 +50,7 @@ public function testCreateMessageReturnsPersistedMessage(): void $authUser = $this->createMock(Administrator::class); $expectedMessage = $this->createMock(Message::class); - $expectedContent = $this->createMock(Message\MessageContent::class); + $expectedContent = $this->createMock(\PhpList\Core\Domain\Messaging\Model\Message\MessageContent::class); $expectedMetadata = $this->createMock(Message\MessageMetadata::class); $expectedContent->method('getSubject')->willReturn('Subject'); @@ -115,7 +115,7 @@ public function testUpdateMessageReturnsUpdatedMessage(): void $authUser = $this->createMock(Administrator::class); $existingMessage = $this->createMock(Message::class); - $expectedContent = $this->createMock(Message\MessageContent::class); + $expectedContent = $this->createMock(\PhpList\Core\Domain\Messaging\Model\Message\MessageContent::class); $expectedMetadata = $this->createMock(Message\MessageMetadata::class); $expectedContent->method('getSubject')->willReturn('Updated Subject'); diff --git a/tests/Unit/Domain/Service/Manager/TemplateImageManagerTest.php b/tests/Unit/Domain/Messaging/Service/TemplateImageManagerTest.php similarity index 90% rename from tests/Unit/Domain/Service/Manager/TemplateImageManagerTest.php rename to tests/Unit/Domain/Messaging/Service/TemplateImageManagerTest.php index ef03fcc8..bde3569a 100644 --- a/tests/Unit/Domain/Service/Manager/TemplateImageManagerTest.php +++ b/tests/Unit/Domain/Messaging/Service/TemplateImageManagerTest.php @@ -2,13 +2,13 @@ declare(strict_types=1); -namespace PhpList\Core\Tests\Unit\Domain\Service\Manager; +namespace PhpList\Core\Tests\Unit\Domain\Messaging\Service; use Doctrine\ORM\EntityManagerInterface; -use PhpList\Core\Domain\Model\Messaging\Template; -use PhpList\Core\Domain\Model\Messaging\TemplateImage; -use PhpList\Core\Domain\Repository\Messaging\TemplateImageRepository; -use PhpList\Core\Domain\Service\Manager\TemplateImageManager; +use PhpList\Core\Domain\Messaging\Model\Template; +use PhpList\Core\Domain\Messaging\Model\TemplateImage; +use PhpList\Core\Domain\Messaging\Repository\TemplateImageRepository; +use PhpList\Core\Domain\Messaging\Service\TemplateImageManager; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; diff --git a/tests/Unit/Domain/Service/Manager/TemplateManagerTest.php b/tests/Unit/Domain/Messaging/Service/TemplateManagerTest.php similarity index 84% rename from tests/Unit/Domain/Service/Manager/TemplateManagerTest.php rename to tests/Unit/Domain/Messaging/Service/TemplateManagerTest.php index d2fbb38e..18650fb3 100644 --- a/tests/Unit/Domain/Service/Manager/TemplateManagerTest.php +++ b/tests/Unit/Domain/Messaging/Service/TemplateManagerTest.php @@ -2,16 +2,16 @@ declare(strict_types=1); -namespace PhpList\Core\Tests\Unit\Domain\Service\Manager; +namespace PhpList\Core\Tests\Unit\Domain\Messaging\Service; use Doctrine\ORM\EntityManagerInterface; -use PhpList\Core\Domain\Model\Messaging\Dto\CreateTemplateDto; -use PhpList\Core\Domain\Model\Messaging\Template; -use PhpList\Core\Domain\Repository\Messaging\TemplateRepository; -use PhpList\Core\Domain\Service\Manager\TemplateImageManager; -use PhpList\Core\Domain\Service\Manager\TemplateManager; -use PhpList\Core\Domain\Service\Validator\TemplateImageValidator; -use PhpList\Core\Domain\Service\Validator\TemplateLinkValidator; +use PhpList\Core\Domain\Messaging\Model\Dto\CreateTemplateDto; +use PhpList\Core\Domain\Messaging\Model\Template; +use PhpList\Core\Domain\Messaging\Repository\TemplateRepository; +use PhpList\Core\Domain\Messaging\Service\TemplateImageManager; +use PhpList\Core\Domain\Messaging\Service\TemplateManager; +use PhpList\Core\Domain\Subscription\Validator\TemplateImageValidator; +use PhpList\Core\Domain\Subscription\Validator\TemplateLinkValidator; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; diff --git a/tests/Unit/Domain/Service/Validator/TemplateImageValidatorTest.php b/tests/Unit/Domain/Messaging/Validator/TemplateImageValidatorTest.php similarity index 93% rename from tests/Unit/Domain/Service/Validator/TemplateImageValidatorTest.php rename to tests/Unit/Domain/Messaging/Validator/TemplateImageValidatorTest.php index ad4b03bd..b921f5c0 100644 --- a/tests/Unit/Domain/Service/Validator/TemplateImageValidatorTest.php +++ b/tests/Unit/Domain/Messaging/Validator/TemplateImageValidatorTest.php @@ -2,17 +2,17 @@ declare(strict_types=1); -namespace PhpList\Core\Tests\Unit\Domain\Service\Validator; +namespace PhpList\Core\Tests\Unit\Domain\Messaging\Validator; use Exception; use GuzzleHttp\ClientInterface; use GuzzleHttp\Psr7\Response; -use PhpList\Core\Domain\Model\Dto\ValidationContext; -use PhpList\Core\Domain\Service\Validator\TemplateImageValidator; +use InvalidArgumentException; +use PhpList\Core\Domain\Common\Model\ValidationContext; +use PhpList\Core\Domain\Subscription\Validator\TemplateImageValidator; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\Validator\Exception\ValidatorException; -use InvalidArgumentException; class TemplateImageValidatorTest extends TestCase { diff --git a/tests/Unit/Domain/Service/Validator/TemplateLinkValidatorTest.php b/tests/Unit/Domain/Messaging/Validator/TemplateLinkValidatorTest.php similarity index 90% rename from tests/Unit/Domain/Service/Validator/TemplateLinkValidatorTest.php rename to tests/Unit/Domain/Messaging/Validator/TemplateLinkValidatorTest.php index e75552c9..b036208a 100644 --- a/tests/Unit/Domain/Service/Validator/TemplateLinkValidatorTest.php +++ b/tests/Unit/Domain/Messaging/Validator/TemplateLinkValidatorTest.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace PhpList\Core\Tests\Unit\Domain\Service\Validator; +namespace PhpList\Core\Tests\Unit\Domain\Messaging\Validator; -use PhpList\Core\Domain\Model\Dto\ValidationContext; -use PhpList\Core\Domain\Service\Validator\TemplateLinkValidator; +use PhpList\Core\Domain\Common\Model\ValidationContext; +use PhpList\Core\Domain\Subscription\Validator\TemplateLinkValidator; use PHPUnit\Framework\TestCase; use Symfony\Component\Validator\Exception\ValidatorException; diff --git a/tests/Unit/Domain/Model/Subscription/SubscriberTest.php b/tests/Unit/Domain/Subscription/Model/SubscriberTest.php similarity index 95% rename from tests/Unit/Domain/Model/Subscription/SubscriberTest.php rename to tests/Unit/Domain/Subscription/Model/SubscriberTest.php index f61c3192..5a60c5de 100644 --- a/tests/Unit/Domain/Model/Subscription/SubscriberTest.php +++ b/tests/Unit/Domain/Subscription/Model/SubscriberTest.php @@ -2,14 +2,14 @@ declare(strict_types=1); -namespace PhpList\Core\Tests\Unit\Domain\Model\Subscription; +namespace PhpList\Core\Tests\Unit\Domain\Subscription\Model; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; -use PhpList\Core\Domain\Model\Interfaces\DomainModel; -use PhpList\Core\Domain\Model\Subscription\Subscriber; -use PhpList\Core\Domain\Model\Subscription\SubscriberList; -use PhpList\Core\Domain\Model\Subscription\Subscription; +use PhpList\Core\Domain\Common\Model\Interfaces\DomainModel; +use PhpList\Core\Domain\Subscription\Model\Subscriber; +use PhpList\Core\Domain\Subscription\Model\SubscriberList; +use PhpList\Core\Domain\Subscription\Model\Subscription; use PhpList\Core\TestingSupport\Traits\ModelTestTrait; use PhpList\Core\TestingSupport\Traits\SimilarDatesAssertionTrait; use PHPUnit\Framework\TestCase; diff --git a/tests/Unit/Domain/Model/Subscription/SubscriptionTest.php b/tests/Unit/Domain/Subscription/Model/SubscriptionTest.php similarity index 86% rename from tests/Unit/Domain/Model/Subscription/SubscriptionTest.php rename to tests/Unit/Domain/Subscription/Model/SubscriptionTest.php index 5cdf3c03..d6abfe09 100644 --- a/tests/Unit/Domain/Model/Subscription/SubscriptionTest.php +++ b/tests/Unit/Domain/Subscription/Model/SubscriptionTest.php @@ -2,13 +2,13 @@ declare(strict_types=1); -namespace PhpList\Core\Tests\Unit\Domain\Model\Subscription; +namespace PhpList\Core\Tests\Unit\Domain\Subscription\Model; use DateTime; -use PhpList\Core\Domain\Model\Interfaces\DomainModel; -use PhpList\Core\Domain\Model\Subscription\Subscriber; -use PhpList\Core\Domain\Model\Subscription\SubscriberList; -use PhpList\Core\Domain\Model\Subscription\Subscription; +use PhpList\Core\Domain\Common\Model\Interfaces\DomainModel; +use PhpList\Core\Domain\Subscription\Model\Subscriber; +use PhpList\Core\Domain\Subscription\Model\SubscriberList; +use PhpList\Core\Domain\Subscription\Model\Subscription; use PhpList\Core\TestingSupport\Traits\ModelTestTrait; use PhpList\Core\TestingSupport\Traits\SimilarDatesAssertionTrait; use PHPUnit\Framework\TestCase; diff --git a/tests/Unit/Domain/Repository/Subscription/SubscriberRepositoryTest.php b/tests/Unit/Domain/Subscription/Repository/SubscriberRepositoryTest.php similarity index 76% rename from tests/Unit/Domain/Repository/Subscription/SubscriberRepositoryTest.php rename to tests/Unit/Domain/Subscription/Repository/SubscriberRepositoryTest.php index 03048703..6cfde555 100644 --- a/tests/Unit/Domain/Repository/Subscription/SubscriberRepositoryTest.php +++ b/tests/Unit/Domain/Subscription/Repository/SubscriberRepositoryTest.php @@ -2,12 +2,13 @@ declare(strict_types=1); -namespace PhpList\Core\Tests\Unit\Domain\Repository\Subscription; +namespace PhpList\Core\Tests\Unit\Domain\Subscription\Repository; use Doctrine\ORM\EntityManager; use Doctrine\ORM\EntityRepository; use Doctrine\ORM\Mapping\ClassMetadata; -use PhpList\Core\Domain\Repository\Subscription\SubscriberRepository; +use PhpList\Core\Domain\Subscription\Repository\SubscriberRepository; +use PhpList\Core\Domain\Subscription\Model\Subscriber; use PHPUnit\Framework\TestCase; /** @@ -24,7 +25,7 @@ protected function setUp(): void $entityManager = $this->createMock(EntityManager::class); $classMetadata = $this->createMock(ClassMetadata::class); - $classMetadata->name = 'PhpList\Core\Domain\Model\Subscription\Subscriber'; + $classMetadata->name = Subscriber::class; $this->subject = new SubscriberRepository($entityManager, $classMetadata); } diff --git a/tests/Unit/Domain/Repository/Subscription/SubscriptionRepositoryTest.php b/tests/Unit/Domain/Subscription/Repository/SubscriptionRepositoryTest.php similarity index 76% rename from tests/Unit/Domain/Repository/Subscription/SubscriptionRepositoryTest.php rename to tests/Unit/Domain/Subscription/Repository/SubscriptionRepositoryTest.php index fff307c0..00242293 100644 --- a/tests/Unit/Domain/Repository/Subscription/SubscriptionRepositoryTest.php +++ b/tests/Unit/Domain/Subscription/Repository/SubscriptionRepositoryTest.php @@ -2,12 +2,13 @@ declare(strict_types=1); -namespace PhpList\Core\Tests\Unit\Domain\Repository\Subscription; +namespace PhpList\Core\Tests\Unit\Domain\Subscription\Repository; use Doctrine\ORM\EntityManager; use Doctrine\ORM\EntityRepository; use Doctrine\ORM\Mapping\ClassMetadata; -use PhpList\Core\Domain\Repository\Subscription\SubscriptionRepository; +use PhpList\Core\Domain\Subscription\Model\Subscription; +use PhpList\Core\Domain\Subscription\Repository\SubscriptionRepository; use PHPUnit\Framework\TestCase; /** @@ -24,7 +25,7 @@ protected function setUp(): void $entityManager = $this->createMock(EntityManager::class); $classMetadata = $this->createMock(ClassMetadata::class); - $classMetadata->name = 'PhpList\Core\Domain\Model\Subscription\Subscription'; + $classMetadata->name = Subscription::class; $this->subject = new SubscriptionRepository($entityManager, $classMetadata); } diff --git a/tests/Unit/Domain/Service/Manager/AttributeDefinitionManagerTest.php b/tests/Unit/Domain/Subscription/Service/AttributeDefinitionManagerTest.php similarity index 92% rename from tests/Unit/Domain/Service/Manager/AttributeDefinitionManagerTest.php rename to tests/Unit/Domain/Subscription/Service/AttributeDefinitionManagerTest.php index d8783e6a..3c650eb7 100644 --- a/tests/Unit/Domain/Service/Manager/AttributeDefinitionManagerTest.php +++ b/tests/Unit/Domain/Subscription/Service/AttributeDefinitionManagerTest.php @@ -2,13 +2,13 @@ declare(strict_types=1); -namespace PhpList\Core\Tests\Unit\Domain\Service\Manager; +namespace PhpList\Core\Tests\Unit\Domain\Subscription\Service; -use PhpList\Core\Domain\Exception\AttributeDefinitionCreationException; -use PhpList\Core\Domain\Model\Subscription\SubscriberAttributeDefinition; -use PhpList\Core\Domain\Model\Subscription\Dto\AttributeDefinitionDto; -use PhpList\Core\Domain\Repository\Subscription\SubscriberAttributeDefinitionRepository; -use PhpList\Core\Domain\Service\Manager\AttributeDefinitionManager; +use PhpList\Core\Domain\Subscription\Exception\AttributeDefinitionCreationException; +use PhpList\Core\Domain\Subscription\Model\Dto\AttributeDefinitionDto; +use PhpList\Core\Domain\Subscription\Model\SubscriberAttributeDefinition; +use PhpList\Core\Domain\Subscription\Repository\SubscriberAttributeDefinitionRepository; +use PhpList\Core\Domain\Subscription\Service\AttributeDefinitionManager; use PHPUnit\Framework\TestCase; class AttributeDefinitionManagerTest extends TestCase diff --git a/tests/Unit/Domain/Service/Manager/SubscriberAttributeManagerTest.php b/tests/Unit/Domain/Subscription/Service/SubscriberAttributeManagerTest.php similarity index 90% rename from tests/Unit/Domain/Service/Manager/SubscriberAttributeManagerTest.php rename to tests/Unit/Domain/Subscription/Service/SubscriberAttributeManagerTest.php index e5b73cfe..38c444e6 100644 --- a/tests/Unit/Domain/Service/Manager/SubscriberAttributeManagerTest.php +++ b/tests/Unit/Domain/Subscription/Service/SubscriberAttributeManagerTest.php @@ -2,17 +2,17 @@ declare(strict_types=1); -namespace PhpList\Core\Tests\Unit\Domain\Service\Manager; - -use PhpList\Core\Domain\Exception\SubscriberAttributeCreationException; -use PhpList\Core\Domain\Model\Subscription\SubscriberAttributeDefinition; -use PhpList\Core\Domain\Model\Subscription\Dto\SubscriberAttributeDto; -use PhpList\Core\Domain\Model\Subscription\Subscriber; -use PhpList\Core\Domain\Model\Subscription\SubscriberAttributeValue; -use PhpList\Core\Domain\Repository\Subscription\SubscriberAttributeDefinitionRepository; -use PhpList\Core\Domain\Repository\Subscription\SubscriberAttributeValueRepository; -use PhpList\Core\Domain\Repository\Subscription\SubscriberRepository; -use PhpList\Core\Domain\Service\Manager\SubscriberAttributeManager; +namespace PhpList\Core\Tests\Unit\Domain\Subscription\Service; + +use PhpList\Core\Domain\Subscription\Exception\SubscriberAttributeCreationException; +use PhpList\Core\Domain\Subscription\Model\Dto\SubscriberAttributeDto; +use PhpList\Core\Domain\Subscription\Model\Subscriber; +use PhpList\Core\Domain\Subscription\Model\SubscriberAttributeDefinition; +use PhpList\Core\Domain\Subscription\Model\SubscriberAttributeValue; +use PhpList\Core\Domain\Subscription\Repository\SubscriberAttributeDefinitionRepository; +use PhpList\Core\Domain\Subscription\Repository\SubscriberAttributeValueRepository; +use PhpList\Core\Domain\Subscription\Repository\SubscriberRepository; +use PhpList\Core\Domain\Subscription\Service\SubscriberAttributeManager; use PHPUnit\Framework\TestCase; class SubscriberAttributeManagerTest extends TestCase diff --git a/tests/Unit/Domain/Service/Manager/SubscriberListManagerTest.php b/tests/Unit/Domain/Subscription/Service/SubscriberListManagerTest.php similarity index 84% rename from tests/Unit/Domain/Service/Manager/SubscriberListManagerTest.php rename to tests/Unit/Domain/Subscription/Service/SubscriberListManagerTest.php index 9e78fd08..2f8e15fd 100644 --- a/tests/Unit/Domain/Service/Manager/SubscriberListManagerTest.php +++ b/tests/Unit/Domain/Subscription/Service/SubscriberListManagerTest.php @@ -2,13 +2,13 @@ declare(strict_types=1); -namespace PhpList\Core\Tests\Unit\Domain\Service\Manager; +namespace PhpList\Core\Tests\Unit\Domain\Subscription\Service; -use PhpList\Core\Domain\Model\Identity\Administrator; -use PhpList\Core\Domain\Model\Subscription\Dto\CreateSubscriberListDto; -use PhpList\Core\Domain\Model\Subscription\SubscriberList; -use PhpList\Core\Domain\Repository\Subscription\SubscriberListRepository; -use PhpList\Core\Domain\Service\Manager\SubscriberListManager; +use PhpList\Core\Domain\Identity\Model\Administrator; +use PhpList\Core\Domain\Subscription\Model\Dto\CreateSubscriberListDto; +use PhpList\Core\Domain\Subscription\Model\SubscriberList; +use PhpList\Core\Domain\Subscription\Repository\SubscriberListRepository; +use PhpList\Core\Domain\Subscription\Service\SubscriberListManager; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; diff --git a/tests/Unit/Domain/Service/Manager/SubscriberManagerTest.php b/tests/Unit/Domain/Subscription/Service/SubscriberManagerTest.php similarity index 81% rename from tests/Unit/Domain/Service/Manager/SubscriberManagerTest.php rename to tests/Unit/Domain/Subscription/Service/SubscriberManagerTest.php index ac1e927b..d243f4a3 100644 --- a/tests/Unit/Domain/Service/Manager/SubscriberManagerTest.php +++ b/tests/Unit/Domain/Subscription/Service/SubscriberManagerTest.php @@ -2,13 +2,13 @@ declare(strict_types=1); -namespace PhpList\Core\Tests\Unit\Domain\Service\Manager; +namespace PhpList\Core\Tests\Unit\Domain\Subscription\Service; use Doctrine\ORM\EntityManagerInterface; -use PhpList\Core\Domain\Model\Subscription\Dto\CreateSubscriberDto; -use PhpList\Core\Domain\Model\Subscription\Subscriber; -use PhpList\Core\Domain\Repository\Subscription\SubscriberRepository; -use PhpList\Core\Domain\Service\Manager\SubscriberManager; +use PhpList\Core\Domain\Subscription\Model\Dto\CreateSubscriberDto; +use PhpList\Core\Domain\Subscription\Model\Subscriber; +use PhpList\Core\Domain\Subscription\Repository\SubscriberRepository; +use PhpList\Core\Domain\Subscription\Service\SubscriberManager; use PHPUnit\Framework\TestCase; class SubscriberManagerTest extends TestCase diff --git a/tests/Unit/Domain/Service/Manager/SubscriptionManagerTest.php b/tests/Unit/Domain/Subscription/Service/SubscriptionManagerTest.php similarity index 87% rename from tests/Unit/Domain/Service/Manager/SubscriptionManagerTest.php rename to tests/Unit/Domain/Subscription/Service/SubscriptionManagerTest.php index 8dcfc390..1abbf38d 100644 --- a/tests/Unit/Domain/Service/Manager/SubscriptionManagerTest.php +++ b/tests/Unit/Domain/Subscription/Service/SubscriptionManagerTest.php @@ -2,15 +2,15 @@ declare(strict_types=1); -namespace PhpList\Core\Tests\Unit\Domain\Service\Manager; - -use PhpList\Core\Domain\Exception\SubscriptionCreationException; -use PhpList\Core\Domain\Model\Subscription\Subscriber; -use PhpList\Core\Domain\Model\Subscription\SubscriberList; -use PhpList\Core\Domain\Model\Subscription\Subscription; -use PhpList\Core\Domain\Repository\Subscription\SubscriberRepository; -use PhpList\Core\Domain\Repository\Subscription\SubscriptionRepository; -use PhpList\Core\Domain\Service\Manager\SubscriptionManager; +namespace PhpList\Core\Tests\Unit\Domain\Subscription\Service; + +use PhpList\Core\Domain\Subscription\Exception\SubscriptionCreationException; +use PhpList\Core\Domain\Subscription\Model\Subscriber; +use PhpList\Core\Domain\Subscription\Model\SubscriberList; +use PhpList\Core\Domain\Subscription\Model\Subscription; +use PhpList\Core\Domain\Subscription\Repository\SubscriberRepository; +use PhpList\Core\Domain\Subscription\Repository\SubscriptionRepository; +use PhpList\Core\Domain\Subscription\Service\SubscriptionManager; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; diff --git a/tests/Unit/Security/AuthenticationTest.php b/tests/Unit/Security/AuthenticationTest.php index 86883763..0dad0bec 100644 --- a/tests/Unit/Security/AuthenticationTest.php +++ b/tests/Unit/Security/AuthenticationTest.php @@ -4,9 +4,9 @@ namespace PhpList\Core\Tests\Unit\Security; -use PhpList\Core\Domain\Model\Identity\Administrator; -use PhpList\Core\Domain\Model\Identity\AdministratorToken; -use PhpList\Core\Domain\Repository\Identity\AdministratorTokenRepository; +use PhpList\Core\Domain\Identity\Repository\AdministratorTokenRepository; +use PhpList\Core\Domain\Identity\Model\Administrator; +use PhpList\Core\Domain\Identity\Model\AdministratorToken; use PhpList\Core\Security\Authentication; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; From 05ed3d0d7717e7538ada683fd7170704fa208b8a Mon Sep 17 00:00:00 2001 From: Tatevik Date: Mon, 12 May 2025 21:03:44 +0400 Subject: [PATCH 09/15] ISSUE-345: subscriber attribute default value --- .../Subscription/Model/Dto/SubscriberAttributeDto.php | 2 +- .../Service/SubscriberAttributeManager.php | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/Domain/Subscription/Model/Dto/SubscriberAttributeDto.php b/src/Domain/Subscription/Model/Dto/SubscriberAttributeDto.php index dec14347..2ec31542 100644 --- a/src/Domain/Subscription/Model/Dto/SubscriberAttributeDto.php +++ b/src/Domain/Subscription/Model/Dto/SubscriberAttributeDto.php @@ -9,7 +9,7 @@ class SubscriberAttributeDto public function __construct( public readonly int $subscriberId, public readonly int $attributeDefinitionId, - public readonly string $value, + public readonly ?string $value = null, ) { } } diff --git a/src/Domain/Subscription/Service/SubscriberAttributeManager.php b/src/Domain/Subscription/Service/SubscriberAttributeManager.php index d7f7a589..3074f703 100644 --- a/src/Domain/Subscription/Service/SubscriberAttributeManager.php +++ b/src/Domain/Subscription/Service/SubscriberAttributeManager.php @@ -19,8 +19,8 @@ class SubscriberAttributeManager public function __construct( SubscriberAttributeDefinitionRepository $definitionRepository, - SubscriberAttributeValueRepository $attributeRepository, - SubscriberRepository $subscriberRepository, + SubscriberAttributeValueRepository$attributeRepository, + SubscriberRepository $subscriberRepository, ) { $this->definitionRepository = $definitionRepository; $this->attributeRepository = $attributeRepository; @@ -46,7 +46,12 @@ public function createOrUpdate(SubscriberAttributeDto $dto): SubscriberAttribute $subscriberAttribute = new SubscriberAttributeValue($attributeDefinition, $subscriber); } - $subscriberAttribute->setValue($dto->value); + $value = $dto->value ?? $attributeDefinition->getDefaultValue(); + if ($value === null) { + throw new SubscriberAttributeCreationException('Value is required', 400); + } + + $subscriberAttribute->setValue($value); $this->attributeRepository->save($subscriberAttribute); return $subscriberAttribute; From b8c1a865ba8af0a1abd72a9fab93a285425feb59 Mon Sep 17 00:00:00 2001 From: Tatevik Date: Mon, 12 May 2025 21:28:51 +0400 Subject: [PATCH 10/15] ISSUE-345: subscriber attribute pagination --- .../Model/Dto/SubscriberAttributeDto.php | 15 --- .../Filter/SubscriberAttributeValueFilter.php | 23 +++++ .../SubscriberAttributeValueRepository.php | 31 +++++- .../Service/SubscriberAttributeManager.php | 41 +++----- .../SubscriberAttributeManagerTest.php | 94 ++++--------------- 5 files changed, 85 insertions(+), 119 deletions(-) delete mode 100644 src/Domain/Subscription/Model/Dto/SubscriberAttributeDto.php create mode 100644 src/Domain/Subscription/Model/Filter/SubscriberAttributeValueFilter.php diff --git a/src/Domain/Subscription/Model/Dto/SubscriberAttributeDto.php b/src/Domain/Subscription/Model/Dto/SubscriberAttributeDto.php deleted file mode 100644 index 2ec31542..00000000 --- a/src/Domain/Subscription/Model/Dto/SubscriberAttributeDto.php +++ /dev/null @@ -1,15 +0,0 @@ -subscriberId = $subscriberId; + return $this; + } + + public function getSubscriberId(): ?int + { + return $this->subscriberId; + } +} diff --git a/src/Domain/Subscription/Repository/SubscriberAttributeValueRepository.php b/src/Domain/Subscription/Repository/SubscriberAttributeValueRepository.php index 0f26b851..8fca509b 100644 --- a/src/Domain/Subscription/Repository/SubscriberAttributeValueRepository.php +++ b/src/Domain/Subscription/Repository/SubscriberAttributeValueRepository.php @@ -4,12 +4,16 @@ namespace PhpList\Core\Domain\Subscription\Repository; +use InvalidArgumentException; +use PhpList\Core\Domain\Common\Model\Filter\FilterRequestInterface; use PhpList\Core\Domain\Common\Repository\AbstractRepository; +use PhpList\Core\Domain\Common\Repository\Interfaces\PaginatableRepositoryInterface; +use PhpList\Core\Domain\Subscription\Model\Filter\SubscriberAttributeValueFilter; use PhpList\Core\Domain\Subscription\Model\Subscriber; use PhpList\Core\Domain\Subscription\Model\SubscriberAttributeDefinition; use PhpList\Core\Domain\Subscription\Model\SubscriberAttributeValue; -class SubscriberAttributeValueRepository extends AbstractRepository +class SubscriberAttributeValueRepository extends AbstractRepository implements PaginatableRepositoryInterface { public function findOneBySubscriberAndAttribute( Subscriber $subscriber, @@ -35,4 +39,29 @@ public function findOneBySubscriberIdAndAttributeId( ->getQuery() ->getOneOrNullResult(); } + + /** + * @return SubscriberAttributeValue[] + * @throws InvalidArgumentException + */ + public function getFilteredAfterId(int $lastId, int $limit, ?FilterRequestInterface $filter = null): array + { + if (!$filter instanceof SubscriberAttributeValueFilter) { + throw new InvalidArgumentException('Expected SubscriberAttributeValueFilter.'); + } + $query = $this->createQueryBuilder('sa') + ->join('sa.subscriber', 's') + ->join('sa.attributeDefinition', 'ad') + ->where('ad.id > :lastId') + ->setParameter('lastId', $lastId); + + if ($filter->getSubscriberId() !== null) { + $query->andWhere('s.id = :subscriberId') + ->setParameter('subscriberId', $filter->getSubscriberId()); + } + return $query->orderBy('sa.id', 'ASC') + ->setMaxResults($limit) + ->getQuery() + ->getResult(); + } } diff --git a/src/Domain/Subscription/Service/SubscriberAttributeManager.php b/src/Domain/Subscription/Service/SubscriberAttributeManager.php index 3074f703..8f13e45f 100644 --- a/src/Domain/Subscription/Service/SubscriberAttributeManager.php +++ b/src/Domain/Subscription/Service/SubscriberAttributeManager.php @@ -5,48 +5,33 @@ namespace PhpList\Core\Domain\Subscription\Service; use PhpList\Core\Domain\Subscription\Exception\SubscriberAttributeCreationException; -use PhpList\Core\Domain\Subscription\Model\Dto\SubscriberAttributeDto; +use PhpList\Core\Domain\Subscription\Model\Subscriber; +use PhpList\Core\Domain\Subscription\Model\SubscriberAttributeDefinition; use PhpList\Core\Domain\Subscription\Model\SubscriberAttributeValue; -use PhpList\Core\Domain\Subscription\Repository\SubscriberAttributeDefinitionRepository; use PhpList\Core\Domain\Subscription\Repository\SubscriberAttributeValueRepository; -use PhpList\Core\Domain\Subscription\Repository\SubscriberRepository; class SubscriberAttributeManager { - private SubscriberAttributeDefinitionRepository $definitionRepository; private SubscriberAttributeValueRepository $attributeRepository; - private SubscriberRepository $subscriberRepository; - public function __construct( - SubscriberAttributeDefinitionRepository $definitionRepository, - SubscriberAttributeValueRepository$attributeRepository, - SubscriberRepository $subscriberRepository, - ) { - $this->definitionRepository = $definitionRepository; + public function __construct(SubscriberAttributeValueRepository $attributeRepository) + { $this->attributeRepository = $attributeRepository; - $this->subscriberRepository = $subscriberRepository; } - public function createOrUpdate(SubscriberAttributeDto $dto): SubscriberAttributeValue - { - $subscriber = $this->subscriberRepository->find($dto->subscriberId); - if (!$subscriber) { - throw new SubscriberAttributeCreationException('Subscriber does not exist', 404); - } - - $attributeDefinition = $this->definitionRepository->find($dto->attributeDefinitionId); - if (!$attributeDefinition) { - throw new SubscriberAttributeCreationException('Attribute definition does not exist', 404); - } - + public function createOrUpdate( + Subscriber $subscriber, + SubscriberAttributeDefinition $definition, + ?string $value = null + ): SubscriberAttributeValue { $subscriberAttribute = $this->attributeRepository - ->findOneBySubscriberAndAttribute($subscriber, $attributeDefinition); + ->findOneBySubscriberAndAttribute($subscriber, $definition); if (!$subscriberAttribute) { - $subscriberAttribute = new SubscriberAttributeValue($attributeDefinition, $subscriber); + $subscriberAttribute = new SubscriberAttributeValue($definition, $subscriber); } - $value = $dto->value ?? $attributeDefinition->getDefaultValue(); + $value = $value ?? $definition->getDefaultValue(); if ($value === null) { throw new SubscriberAttributeCreationException('Value is required', 400); } @@ -64,6 +49,6 @@ public function getSubscriberAttribute(int $subscriberId, int $attributeDefiniti public function delete(SubscriberAttributeValue $attribute): void { - $this->definitionRepository->remove($attribute); + $this->attributeRepository->remove($attribute); } } diff --git a/tests/Unit/Domain/Subscription/Service/SubscriberAttributeManagerTest.php b/tests/Unit/Domain/Subscription/Service/SubscriberAttributeManagerTest.php index 38c444e6..84682ca2 100644 --- a/tests/Unit/Domain/Subscription/Service/SubscriberAttributeManagerTest.php +++ b/tests/Unit/Domain/Subscription/Service/SubscriberAttributeManagerTest.php @@ -5,13 +5,10 @@ namespace PhpList\Core\Tests\Unit\Domain\Subscription\Service; use PhpList\Core\Domain\Subscription\Exception\SubscriberAttributeCreationException; -use PhpList\Core\Domain\Subscription\Model\Dto\SubscriberAttributeDto; use PhpList\Core\Domain\Subscription\Model\Subscriber; use PhpList\Core\Domain\Subscription\Model\SubscriberAttributeDefinition; use PhpList\Core\Domain\Subscription\Model\SubscriberAttributeValue; -use PhpList\Core\Domain\Subscription\Repository\SubscriberAttributeDefinitionRepository; use PhpList\Core\Domain\Subscription\Repository\SubscriberAttributeValueRepository; -use PhpList\Core\Domain\Subscription\Repository\SubscriberRepository; use PhpList\Core\Domain\Subscription\Service\SubscriberAttributeManager; use PHPUnit\Framework\TestCase; @@ -22,26 +19,8 @@ public function testCreateNewSubscriberAttribute(): void $subscriber = new Subscriber(); $definition = new SubscriberAttributeDefinition(); - $dto = new SubscriberAttributeDto( - subscriberId: 1, - attributeDefinitionId: 2, - value: 'US' - ); - - $subscriberRepo = $this->createMock(SubscriberRepository::class); - $attributeDefRepo = $this->createMock(SubscriberAttributeDefinitionRepository::class); $subscriberAttrRepo = $this->createMock(SubscriberAttributeValueRepository::class); - $subscriberRepo->expects(self::once()) - ->method('find') - ->with(1) - ->willReturn($subscriber); - - $attributeDefRepo->expects(self::once()) - ->method('find') - ->with(2) - ->willReturn($definition); - $subscriberAttrRepo->expects(self::once()) ->method('findOneBySubscriberAndAttribute') ->with($subscriber, $definition) @@ -53,8 +32,8 @@ public function testCreateNewSubscriberAttribute(): void return $attr->getValue() === 'US'; })); - $manager = new SubscriberAttributeManager($attributeDefRepo, $subscriberAttrRepo, $subscriberRepo); - $attribute = $manager->createOrUpdate($dto); + $manager = new SubscriberAttributeManager($subscriberAttrRepo); + $attribute = $manager->createOrUpdate($subscriber, $definition, 'US'); self::assertInstanceOf(SubscriberAttributeValue::class, $attribute); self::assertSame('US', $attribute->getValue()); @@ -64,86 +43,52 @@ public function testUpdateExistingSubscriberAttribute(): void { $subscriber = new Subscriber(); $definition = new SubscriberAttributeDefinition(); - $existing = new SubscriberAttributeValue($definition, $subscriber); $existing->setValue('Old'); - $dto = new SubscriberAttributeDto(1, 2, 'Updated'); - - $subscriberRepo = $this->createMock(SubscriberRepository::class); - $attributeDefRepo = $this->createMock(SubscriberAttributeDefinitionRepository::class); $subscriberAttrRepo = $this->createMock(SubscriberAttributeValueRepository::class); - - $subscriberRepo->method('find')->willReturn($subscriber); - $attributeDefRepo->method('find')->willReturn($definition); - $subscriberAttrRepo->expects(self::once()) ->method('findOneBySubscriberAndAttribute') ->with($subscriber, $definition) ->willReturn($existing); - $subscriberAttrRepo->expects(self::once())->method('save')->with($existing); + $subscriberAttrRepo->expects(self::once()) + ->method('save') + ->with($existing); - $manager = new SubscriberAttributeManager($attributeDefRepo, $subscriberAttrRepo, $subscriberRepo); - $result = $manager->createOrUpdate($dto); + $manager = new SubscriberAttributeManager($subscriberAttrRepo); + $result = $manager->createOrUpdate($subscriber, $definition, 'Updated'); self::assertSame('Updated', $result->getValue()); } - public function testCreateFailsIfSubscriberNotFound(): void - { - $subscriberRepo = $this->createMock(SubscriberRepository::class); - $attributeDefRepo = $this->createMock(SubscriberAttributeDefinitionRepository::class); - $subscriberAttrRepo = $this->createMock(SubscriberAttributeValueRepository::class); - - $subscriberRepo->method('find')->willReturn(null); - - $dto = new SubscriberAttributeDto(1, 2, 'US'); - - $manager = new SubscriberAttributeManager($attributeDefRepo, $subscriberAttrRepo, $subscriberRepo); - - $this->expectException(SubscriberAttributeCreationException::class); - $this->expectExceptionMessage('Subscriber does not exist'); - - $manager->createOrUpdate($dto); - } - - public function testCreateFailsIfAttributeDefinitionNotFound(): void + public function testCreateFailsWhenValueAndDefaultAreNull(): void { $subscriber = new Subscriber(); + $definition = new SubscriberAttributeDefinition(); - $subscriberRepo = $this->createMock(SubscriberRepository::class); - $attributeDefRepo = $this->createMock(SubscriberAttributeDefinitionRepository::class); $subscriberAttrRepo = $this->createMock(SubscriberAttributeValueRepository::class); + $subscriberAttrRepo->method('findOneBySubscriberAndAttribute')->willReturn(null); - $subscriberRepo->method('find')->willReturn($subscriber); - $attributeDefRepo->method('find')->willReturn(null); - - $dto = new SubscriberAttributeDto(1, 2, 'US'); - - $manager = new SubscriberAttributeManager($attributeDefRepo, $subscriberAttrRepo, $subscriberRepo); + $manager = new SubscriberAttributeManager($subscriberAttrRepo); $this->expectException(SubscriberAttributeCreationException::class); - $this->expectExceptionMessage('Attribute definition does not exist'); + $this->expectExceptionMessage('Value is required'); - $manager->createOrUpdate($dto); + $manager->createOrUpdate($subscriber, $definition, null); } public function testGetSubscriberAttribute(): void { - $expected = new SubscriberAttributeValue(new SubscriberAttributeDefinition(), new Subscriber()); - - $subscriberRepo = $this->createMock(SubscriberRepository::class); - $attributeDefRepo = $this->createMock(SubscriberAttributeDefinitionRepository::class); $subscriberAttrRepo = $this->createMock(SubscriberAttributeValueRepository::class); + $expected = new SubscriberAttributeValue(new SubscriberAttributeDefinition(), new Subscriber()); $subscriberAttrRepo->expects(self::once()) ->method('findOneBySubscriberIdAndAttributeId') ->with(5, 10) ->willReturn($expected); - $manager = new SubscriberAttributeManager($attributeDefRepo, $subscriberAttrRepo, $subscriberRepo); - + $manager = new SubscriberAttributeManager($subscriberAttrRepo); $result = $manager->getSubscriberAttribute(5, 10); self::assertSame($expected, $result); @@ -151,15 +96,14 @@ public function testGetSubscriberAttribute(): void public function testDeleteSubscriberAttribute(): void { - $subscriberRepo = $this->createMock(SubscriberRepository::class); - $attributeDefRepo = $this->createMock(SubscriberAttributeDefinitionRepository::class); $subscriberAttrRepo = $this->createMock(SubscriberAttributeValueRepository::class); - $attribute = $this->createMock(SubscriberAttributeValue::class); - $attributeDefRepo->expects(self::once())->method('remove')->with($attribute); + $subscriberAttrRepo->expects(self::once()) + ->method('remove') + ->with($attribute); - $manager = new SubscriberAttributeManager($attributeDefRepo, $subscriberAttrRepo, $subscriberRepo); + $manager = new SubscriberAttributeManager($subscriberAttrRepo); $manager->delete($attribute); self::assertTrue(true); From 8c73e39c7026e8cca2c0887b493ca7d95ebfbdb2 Mon Sep 17 00:00:00 2001 From: Tatevik Date: Tue, 13 May 2025 21:26:10 +0400 Subject: [PATCH 11/15] ISSUE-345: update folder --- config/services/validators.yml | 4 ++-- src/Domain/Messaging/Service/TemplateManager.php | 4 ++-- .../Validator/TemplateImageValidator.php | 2 +- .../Validator/TemplateLinkValidator.php | 2 +- .../Repository/SubscriberAttributeValueRepository.php | 2 +- .../Subscription/Service/SubscriberAttributeManager.php | 2 +- tests/Unit/Domain/Messaging/Service/TemplateManagerTest.php | 4 ++-- .../Domain/Messaging/Validator/TemplateImageValidatorTest.php | 2 +- .../Domain/Messaging/Validator/TemplateLinkValidatorTest.php | 2 +- 9 files changed, 12 insertions(+), 12 deletions(-) rename src/Domain/{Subscription => Messaging}/Validator/TemplateImageValidator.php (97%) rename src/Domain/{Subscription => Messaging}/Validator/TemplateLinkValidator.php (96%) diff --git a/config/services/validators.yml b/config/services/validators.yml index 77749d39..3d15e4a5 100644 --- a/config/services/validators.yml +++ b/config/services/validators.yml @@ -1,8 +1,8 @@ services: - PhpList\Core\Domain\Subscription\Validator\TemplateLinkValidator: + PhpList\Core\Domain\Messaging\Validator\TemplateLinkValidator: autowire: true autoconfigure: true - PhpList\Core\Domain\Subscription\Validator\TemplateImageValidator: + PhpList\Core\Domain\Messaging\Validator\TemplateImageValidator: autowire: true autoconfigure: true diff --git a/src/Domain/Messaging/Service/TemplateManager.php b/src/Domain/Messaging/Service/TemplateManager.php index 218c4784..35678484 100644 --- a/src/Domain/Messaging/Service/TemplateManager.php +++ b/src/Domain/Messaging/Service/TemplateManager.php @@ -9,10 +9,10 @@ use PhpList\Core\Domain\Messaging\Model\Dto\CreateTemplateDto; use PhpList\Core\Domain\Messaging\Model\Template; use PhpList\Core\Domain\Messaging\Repository\TemplateRepository; +use PhpList\Core\Domain\Messaging\Validator\TemplateImageValidator; +use PhpList\Core\Domain\Messaging\Validator\TemplateLinkValidator; use PhpList\Core\Domain\Subscription\Model\Dto\UpdateSubscriberDto; use PhpList\Core\Domain\Subscription\Model\Subscriber; -use PhpList\Core\Domain\Subscription\Validator\TemplateImageValidator; -use PhpList\Core\Domain\Subscription\Validator\TemplateLinkValidator; class TemplateManager { diff --git a/src/Domain/Subscription/Validator/TemplateImageValidator.php b/src/Domain/Messaging/Validator/TemplateImageValidator.php similarity index 97% rename from src/Domain/Subscription/Validator/TemplateImageValidator.php rename to src/Domain/Messaging/Validator/TemplateImageValidator.php index d981daf5..11bcc329 100644 --- a/src/Domain/Subscription/Validator/TemplateImageValidator.php +++ b/src/Domain/Messaging/Validator/TemplateImageValidator.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace PhpList\Core\Domain\Subscription\Validator; +namespace PhpList\Core\Domain\Messaging\Validator; use GuzzleHttp\ClientInterface; use InvalidArgumentException; diff --git a/src/Domain/Subscription/Validator/TemplateLinkValidator.php b/src/Domain/Messaging/Validator/TemplateLinkValidator.php similarity index 96% rename from src/Domain/Subscription/Validator/TemplateLinkValidator.php rename to src/Domain/Messaging/Validator/TemplateLinkValidator.php index 532cf49a..18c772df 100644 --- a/src/Domain/Subscription/Validator/TemplateLinkValidator.php +++ b/src/Domain/Messaging/Validator/TemplateLinkValidator.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace PhpList\Core\Domain\Subscription\Validator; +namespace PhpList\Core\Domain\Messaging\Validator; use DOMDocument; use PhpList\Core\Domain\Common\Model\ValidationContext; diff --git a/src/Domain/Subscription/Repository/SubscriberAttributeValueRepository.php b/src/Domain/Subscription/Repository/SubscriberAttributeValueRepository.php index 8fca509b..d29d56ff 100644 --- a/src/Domain/Subscription/Repository/SubscriberAttributeValueRepository.php +++ b/src/Domain/Subscription/Repository/SubscriberAttributeValueRepository.php @@ -59,7 +59,7 @@ public function getFilteredAfterId(int $lastId, int $limit, ?FilterRequestInterf $query->andWhere('s.id = :subscriberId') ->setParameter('subscriberId', $filter->getSubscriberId()); } - return $query->orderBy('sa.id', 'ASC') + return $query->orderBy('ad.id', 'ASC') ->setMaxResults($limit) ->getQuery() ->getResult(); diff --git a/src/Domain/Subscription/Service/SubscriberAttributeManager.php b/src/Domain/Subscription/Service/SubscriberAttributeManager.php index 8f13e45f..1cbfdd24 100644 --- a/src/Domain/Subscription/Service/SubscriberAttributeManager.php +++ b/src/Domain/Subscription/Service/SubscriberAttributeManager.php @@ -42,7 +42,7 @@ public function createOrUpdate( return $subscriberAttribute; } - public function getSubscriberAttribute(int $subscriberId, int $attributeDefinitionId): SubscriberAttributeValue + public function getSubscriberAttribute(int $subscriberId, int $attributeDefinitionId): ?SubscriberAttributeValue { return $this->attributeRepository->findOneBySubscriberIdAndAttributeId($subscriberId, $attributeDefinitionId); } diff --git a/tests/Unit/Domain/Messaging/Service/TemplateManagerTest.php b/tests/Unit/Domain/Messaging/Service/TemplateManagerTest.php index 18650fb3..fbbb4831 100644 --- a/tests/Unit/Domain/Messaging/Service/TemplateManagerTest.php +++ b/tests/Unit/Domain/Messaging/Service/TemplateManagerTest.php @@ -10,8 +10,8 @@ use PhpList\Core\Domain\Messaging\Repository\TemplateRepository; use PhpList\Core\Domain\Messaging\Service\TemplateImageManager; use PhpList\Core\Domain\Messaging\Service\TemplateManager; -use PhpList\Core\Domain\Subscription\Validator\TemplateImageValidator; -use PhpList\Core\Domain\Subscription\Validator\TemplateLinkValidator; +use PhpList\Core\Domain\Messaging\Validator\TemplateImageValidator; +use PhpList\Core\Domain\Messaging\Validator\TemplateLinkValidator; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; diff --git a/tests/Unit/Domain/Messaging/Validator/TemplateImageValidatorTest.php b/tests/Unit/Domain/Messaging/Validator/TemplateImageValidatorTest.php index b921f5c0..88af2c8c 100644 --- a/tests/Unit/Domain/Messaging/Validator/TemplateImageValidatorTest.php +++ b/tests/Unit/Domain/Messaging/Validator/TemplateImageValidatorTest.php @@ -9,7 +9,7 @@ use GuzzleHttp\Psr7\Response; use InvalidArgumentException; use PhpList\Core\Domain\Common\Model\ValidationContext; -use PhpList\Core\Domain\Subscription\Validator\TemplateImageValidator; +use PhpList\Core\Domain\Messaging\Validator\TemplateImageValidator; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\Validator\Exception\ValidatorException; diff --git a/tests/Unit/Domain/Messaging/Validator/TemplateLinkValidatorTest.php b/tests/Unit/Domain/Messaging/Validator/TemplateLinkValidatorTest.php index b036208a..d0ab6566 100644 --- a/tests/Unit/Domain/Messaging/Validator/TemplateLinkValidatorTest.php +++ b/tests/Unit/Domain/Messaging/Validator/TemplateLinkValidatorTest.php @@ -5,7 +5,7 @@ namespace PhpList\Core\Tests\Unit\Domain\Messaging\Validator; use PhpList\Core\Domain\Common\Model\ValidationContext; -use PhpList\Core\Domain\Subscription\Validator\TemplateLinkValidator; +use PhpList\Core\Domain\Messaging\Validator\TemplateLinkValidator; use PHPUnit\Framework\TestCase; use Symfony\Component\Validator\Exception\ValidatorException; From ab235f9f676fce6b89c3ba807fcd274a6f74a33d Mon Sep 17 00:00:00 2001 From: Tatevik Date: Sat, 17 May 2025 20:27:02 +0400 Subject: [PATCH 12/15] ISSUE-345: admin attribute --- config/services/managers.yml | 8 + config/services/repositories.yml | 10 + .../AdminAttributeCreationException.php | 23 ++ .../AttributeDefinitionCreationException.php | 23 ++ .../Identity/Model/AdminAttributeValue.php | 29 ++- .../Model/Dto/AdminAttributeDefinitionDto.php | 21 ++ .../Filter/AdminAttributeValueFilter.php | 23 ++ .../AdminAttributeDefinitionRepository.php | 6 + .../AdminAttributeValueRepository.php | 45 +++- .../AdminAttributeDefinitionManager.php | 76 +++++++ .../Service/AdminAttributeManager.php | 59 +++++ .../Model/AdminAttributeDefinitionTest.php | 211 ++++++++++++++++++ .../Model/AdminAttributeValueTest.php | 77 +++++++ .../AdminAttributeValueRepositoryTest.php | 111 +++++++++ .../AdminAttributeDefinitionManagerTest.php | 204 +++++++++++++++++ .../Service/AdminAttributeManagerTest.php | 171 ++++++++++++++ 16 files changed, 1084 insertions(+), 13 deletions(-) create mode 100644 src/Domain/Identity/Exception/AdminAttributeCreationException.php create mode 100644 src/Domain/Identity/Exception/AttributeDefinitionCreationException.php create mode 100644 src/Domain/Identity/Model/Dto/AdminAttributeDefinitionDto.php create mode 100644 src/Domain/Identity/Model/Filter/AdminAttributeValueFilter.php create mode 100644 src/Domain/Identity/Service/AdminAttributeDefinitionManager.php create mode 100644 src/Domain/Identity/Service/AdminAttributeManager.php create mode 100644 tests/Unit/Domain/Identity/Model/AdminAttributeDefinitionTest.php create mode 100644 tests/Unit/Domain/Identity/Model/AdminAttributeValueTest.php create mode 100644 tests/Unit/Domain/Identity/Repository/AdminAttributeValueRepositoryTest.php create mode 100644 tests/Unit/Domain/Identity/Service/AdminAttributeDefinitionManagerTest.php create mode 100644 tests/Unit/Domain/Identity/Service/AdminAttributeManagerTest.php diff --git a/config/services/managers.yml b/config/services/managers.yml index f2a9ed29..374172ca 100644 --- a/config/services/managers.yml +++ b/config/services/managers.yml @@ -36,6 +36,14 @@ services: autowire: true autoconfigure: true + PhpList\Core\Domain\Identity\Service\AdminAttributeDefinitionManager: + autowire: true + autoconfigure: true + + PhpList\Core\Domain\Identity\Service\AdminAttributeManager: + autowire: true + autoconfigure: true + PhpList\Core\Domain\Subscription\Service\AttributeDefinitionManager: autowire: true autoconfigure: true diff --git a/config/services/repositories.yml b/config/services/repositories.yml index 16831bd7..21ce7114 100644 --- a/config/services/repositories.yml +++ b/config/services/repositories.yml @@ -6,6 +6,16 @@ services: - Doctrine\ORM\Mapping\ClassMetadata\ClassMetadata - PhpList\Core\Security\HashGenerator + PhpList\Core\Domain\Identity\Repository\AdminAttributeValueRepository: + parent: PhpList\Core\Domain\Common\Repository\AbstractRepository + arguments: + - PhpList\Core\Domain\Identity\Model\AdminAttributeValue + + PhpList\Core\Domain\Identity\Repository\AdminAttributeDefinitionRepository: + parent: PhpList\Core\Domain\Common\Repository\AbstractRepository + arguments: + - PhpList\Core\Domain\Identity\Model\AdminAttributeDefinition + PhpList\Core\Domain\Identity\Repository\AdministratorTokenRepository: parent: PhpList\Core\Domain\Common\Repository\AbstractRepository arguments: diff --git a/src/Domain/Identity/Exception/AdminAttributeCreationException.php b/src/Domain/Identity/Exception/AdminAttributeCreationException.php new file mode 100644 index 00000000..3359024a --- /dev/null +++ b/src/Domain/Identity/Exception/AdminAttributeCreationException.php @@ -0,0 +1,23 @@ +statusCode = $statusCode; + } + + public function getStatusCode(): int + { + return $this->statusCode; + } +} diff --git a/src/Domain/Identity/Exception/AttributeDefinitionCreationException.php b/src/Domain/Identity/Exception/AttributeDefinitionCreationException.php new file mode 100644 index 00000000..5d105893 --- /dev/null +++ b/src/Domain/Identity/Exception/AttributeDefinitionCreationException.php @@ -0,0 +1,23 @@ +statusCode = $statusCode; + } + + public function getStatusCode(): int + { + return $this->statusCode; + } +} diff --git a/src/Domain/Identity/Model/AdminAttributeValue.php b/src/Domain/Identity/Model/AdminAttributeValue.php index 18893d45..3f4e6c68 100644 --- a/src/Domain/Identity/Model/AdminAttributeValue.php +++ b/src/Domain/Identity/Model/AdminAttributeValue.php @@ -14,31 +14,36 @@ class AdminAttributeValue implements DomainModel { #[ORM\Id] - #[ORM\Column(name: 'adminattributeid', type: 'integer', options: ['unsigned' => true])] - private int $adminAttributeId; + #[ORM\ManyToOne(targetEntity: AdminAttributeDefinition::class)] + #[ORM\JoinColumn(name: 'adminattributeid', referencedColumnName: 'id', nullable: false)] + private AdminAttributeDefinition $attributeDefinition; #[ORM\Id] - #[ORM\Column(name: 'adminid', type: 'integer', options: ['unsigned' => true])] - private int $adminId; + #[ORM\ManyToOne(targetEntity: Administrator::class)] + #[ORM\JoinColumn(name: 'adminid', referencedColumnName: 'id', nullable: false)] + private Administrator $administrator; #[ORM\Column(name: 'value', type: 'string', length: 255, nullable: true)] private ?string $value; - public function __construct(int $adminAttributeId, int $adminId, ?string $value = null) - { - $this->adminAttributeId = $adminAttributeId; - $this->adminId = $adminId; + public function __construct( + AdminAttributeDefinition $attributeDefinition, + Administrator $administrator, + ?string $value = null + ) { + $this->attributeDefinition = $attributeDefinition; + $this->administrator = $administrator; $this->value = $value; } - public function getAdminAttributeId(): int + public function getAttributeDefinition(): AdminAttributeDefinition { - return $this->adminAttributeId; + return $this->attributeDefinition; } - public function getAdminId(): int + public function getAdministrator(): Administrator { - return $this->adminId; + return $this->administrator; } public function getValue(): ?string diff --git a/src/Domain/Identity/Model/Dto/AdminAttributeDefinitionDto.php b/src/Domain/Identity/Model/Dto/AdminAttributeDefinitionDto.php new file mode 100644 index 00000000..429faccb --- /dev/null +++ b/src/Domain/Identity/Model/Dto/AdminAttributeDefinitionDto.php @@ -0,0 +1,21 @@ +adminId = $adminId; + return $this; + } + + public function getAdminId(): ?int + { + return $this->adminId; + } +} diff --git a/src/Domain/Identity/Repository/AdminAttributeDefinitionRepository.php b/src/Domain/Identity/Repository/AdminAttributeDefinitionRepository.php index f15c866d..53df9996 100644 --- a/src/Domain/Identity/Repository/AdminAttributeDefinitionRepository.php +++ b/src/Domain/Identity/Repository/AdminAttributeDefinitionRepository.php @@ -7,8 +7,14 @@ use PhpList\Core\Domain\Common\Repository\AbstractRepository; use PhpList\Core\Domain\Common\Repository\CursorPaginationTrait; use PhpList\Core\Domain\Common\Repository\Interfaces\PaginatableRepositoryInterface; +use PhpList\Core\Domain\Identity\Model\AdminAttributeDefinition; class AdminAttributeDefinitionRepository extends AbstractRepository implements PaginatableRepositoryInterface { use CursorPaginationTrait; + + public function findOneByName(string $name): ?AdminAttributeDefinition + { + return $this->findOneBy(['name' => $name]); + } } diff --git a/src/Domain/Identity/Repository/AdminAttributeValueRepository.php b/src/Domain/Identity/Repository/AdminAttributeValueRepository.php index 7c7db130..c38b6215 100644 --- a/src/Domain/Identity/Repository/AdminAttributeValueRepository.php +++ b/src/Domain/Identity/Repository/AdminAttributeValueRepository.php @@ -4,8 +4,51 @@ namespace PhpList\Core\Domain\Identity\Repository; +use InvalidArgumentException; +use PhpList\Core\Domain\Common\Model\Filter\FilterRequestInterface; use PhpList\Core\Domain\Common\Repository\AbstractRepository; +use PhpList\Core\Domain\Common\Repository\Interfaces\PaginatableRepositoryInterface; +use PhpList\Core\Domain\Identity\Model\AdminAttributeValue; +use PhpList\Core\Domain\Identity\Model\Filter\AdminAttributeValueFilter; -class AdminAttributeValueRepository extends AbstractRepository +class AdminAttributeValueRepository extends AbstractRepository implements PaginatableRepositoryInterface { + public function findOneByAdminIdAndAttributeId(int $adminId, int $definitionId): ?AdminAttributeValue + { + return $this->createQueryBuilder('aav') + ->join('aav.administrator', 'admin') + ->join('aav.attributeDefinition', 'attr') + ->where('admin.id = :adminId') + ->andWhere('attr.id = :attributeId') + ->setParameter('adminId', $adminId) + ->setParameter('attributeId', $definitionId) + ->getQuery() + ->getOneOrNullResult(); + } + + + /** + * @return AdminAttributeValue[] + * @throws InvalidArgumentException + */ + public function getFilteredAfterId(int $lastId, int $limit, ?FilterRequestInterface $filter = null): array + { + if (!$filter instanceof AdminAttributeValueFilter) { + throw new InvalidArgumentException('Expected AdminAttributeValueFilter.'); + } + $query = $this->createQueryBuilder('aav') + ->join('aav.administrator', 'a') + ->join('aav.attributeDefinition', 'ad') + ->where('ad.id > :lastId') + ->setParameter('lastId', $lastId); + + if ($filter->getAdminId() !== null) { + $query->andWhere('a.id = :adminId') + ->setParameter('adminId', $filter->getAdminId()); + } + return $query->orderBy('ad.id', 'ASC') + ->setMaxResults($limit) + ->getQuery() + ->getResult(); + } } diff --git a/src/Domain/Identity/Service/AdminAttributeDefinitionManager.php b/src/Domain/Identity/Service/AdminAttributeDefinitionManager.php new file mode 100644 index 00000000..c1397e09 --- /dev/null +++ b/src/Domain/Identity/Service/AdminAttributeDefinitionManager.php @@ -0,0 +1,76 @@ +definitionRepository = $definitionRepository; + } + + public function create(AdminAttributeDefinitionDto $attributeDefinitionDto): AdminAttributeDefinition + { + $existingAttribute = $this->definitionRepository->findOneByName($attributeDefinitionDto->name); + if ($existingAttribute) { + throw new AttributeDefinitionCreationException('Attribute definition already exists', 409); + } + + $attributeDefinition = (new AdminAttributeDefinition($attributeDefinitionDto->name)) + ->setType($attributeDefinitionDto->type) + ->setListOrder($attributeDefinitionDto->listOrder) + ->setRequired($attributeDefinitionDto->required) + ->setDefaultValue($attributeDefinitionDto->defaultValue) + ->setTableName($attributeDefinitionDto->tableName); + + $this->definitionRepository->save($attributeDefinition); + + return $attributeDefinition; + } + + public function update( + AdminAttributeDefinition $attributeDefinition, + AdminAttributeDefinitionDto $attributeDefinitionDto + ): AdminAttributeDefinition { + $existingAttribute = $this->definitionRepository->findOneByName($attributeDefinitionDto->name); + if ($existingAttribute && $existingAttribute->getId() !== $attributeDefinition->getId()) { + throw new AttributeDefinitionCreationException('Another attribute with this name already exists.', 409); + } + + $attributeDefinition + ->setName($attributeDefinitionDto->name) + ->setType($attributeDefinitionDto->type) + ->setListOrder($attributeDefinitionDto->listOrder) + ->setRequired($attributeDefinitionDto->required) + ->setDefaultValue($attributeDefinitionDto->defaultValue) + ->setTableName($attributeDefinitionDto->tableName); + + $this->definitionRepository->save($attributeDefinition); + + return $attributeDefinition; + } + + public function delete(AdminAttributeDefinition $attributeDefinition): void + { + $this->definitionRepository->remove($attributeDefinition); + } + + public function getTotalCount(): int + { + return $this->definitionRepository->count(); + } + + public function getAttributesAfterId(int $afterId, int $limit): array + { + return $this->definitionRepository->getAfterId($afterId, $limit); + } +} diff --git a/src/Domain/Identity/Service/AdminAttributeManager.php b/src/Domain/Identity/Service/AdminAttributeManager.php new file mode 100644 index 00000000..b5cb13f5 --- /dev/null +++ b/src/Domain/Identity/Service/AdminAttributeManager.php @@ -0,0 +1,59 @@ +attributeRepository = $attributeRepository; + } + + public function createOrUpdate( + Administrator $admin, + AdminAttributeDefinition $definition, + ?string $value = null + ): AdminAttributeValue { + $adminAttribute = $this->attributeRepository->findOneByAdminIdAndAttributeId( + adminId: $admin->getId(), + definitionId: $definition->getId() + ); + + if (!$adminAttribute) { + $adminAttribute = new AdminAttributeValue(attributeDefinition: $definition, administrator: $admin); + } + + $value = $value ?? $definition->getDefaultValue(); + if ($value === null) { + throw new AdminAttributeCreationException('Value is required', 400); + } + + $adminAttribute->setValue($value); + $this->attributeRepository->save($adminAttribute); + + return $adminAttribute; + } + + public function getAdminAttribute(int $adminId, int $attributeDefinitionId): ?AdminAttributeValue + { + return $this->attributeRepository->findOneByAdminIdAndAttributeId( + adminId: $adminId, + definitionId: $attributeDefinitionId + ); + } + + public function delete(AdminAttributeValue $attribute): void + { + $this->attributeRepository->remove($attribute); + } +} diff --git a/tests/Unit/Domain/Identity/Model/AdminAttributeDefinitionTest.php b/tests/Unit/Domain/Identity/Model/AdminAttributeDefinitionTest.php new file mode 100644 index 00000000..0343d179 --- /dev/null +++ b/tests/Unit/Domain/Identity/Model/AdminAttributeDefinitionTest.php @@ -0,0 +1,211 @@ +subject = new AdminAttributeDefinition( + $this->name, + $this->type, + $this->listOrder, + $this->defaultValue, + $this->required, + $this->tableName + ); + } + + public function testIsDomainModel(): void + { + self::assertInstanceOf(DomainModel::class, $this->subject); + } + + public function testGetIdInitiallyReturnsNull(): void + { + self::assertNull($this->subject->getId()); + } + + public function testGetIdReturnsId(): void + { + $id = 123456; + $this->setSubjectId($this->subject, $id); + + self::assertSame($id, $this->subject->getId()); + } + + public function testGetNameReturnsName(): void + { + self::assertSame($this->name, $this->subject->getName()); + } + + public function testSetNameSetsName(): void + { + $newName = 'new-name'; + $this->subject->setName($newName); + + self::assertSame($newName, $this->subject->getName()); + } + + public function testSetNameReturnsInstance(): void + { + $result = $this->subject->setName('new-name'); + + self::assertSame($this->subject, $result); + } + + public function testGetTypeReturnsType(): void + { + self::assertSame($this->type, $this->subject->getType()); + } + + public function testSetTypeSetsType(): void + { + $newType = 'checkbox'; + $this->subject->setType($newType); + + self::assertSame($newType, $this->subject->getType()); + } + + public function testSetTypeReturnsInstance(): void + { + $result = $this->subject->setType('checkbox'); + + self::assertSame($this->subject, $result); + } + + public function testSetTypeCanSetNull(): void + { + $this->subject->setType(null); + + self::assertNull($this->subject->getType()); + } + + public function testGetListOrderReturnsListOrder(): void + { + self::assertSame($this->listOrder, $this->subject->getListOrder()); + } + + public function testSetListOrderSetsListOrder(): void + { + $newListOrder = 20; + $this->subject->setListOrder($newListOrder); + + self::assertSame($newListOrder, $this->subject->getListOrder()); + } + + public function testSetListOrderReturnsInstance(): void + { + $result = $this->subject->setListOrder(20); + + self::assertSame($this->subject, $result); + } + + public function testSetListOrderCanSetNull(): void + { + $this->subject->setListOrder(null); + + self::assertNull($this->subject->getListOrder()); + } + + public function testGetDefaultValueReturnsDefaultValue(): void + { + self::assertSame($this->defaultValue, $this->subject->getDefaultValue()); + } + + public function testSetDefaultValueSetsDefaultValue(): void + { + $newDefaultValue = 'new-default'; + $this->subject->setDefaultValue($newDefaultValue); + + self::assertSame($newDefaultValue, $this->subject->getDefaultValue()); + } + + public function testSetDefaultValueReturnsInstance(): void + { + $result = $this->subject->setDefaultValue('new-default'); + + self::assertSame($this->subject, $result); + } + + public function testSetDefaultValueCanSetNull(): void + { + $this->subject->setDefaultValue(null); + + self::assertNull($this->subject->getDefaultValue()); + } + + public function testIsRequiredReturnsRequired(): void + { + self::assertSame($this->required, $this->subject->isRequired()); + } + + public function testSetRequiredSetsRequired(): void + { + $this->subject->setRequired(false); + + self::assertSame(false, $this->subject->isRequired()); + } + + public function testSetRequiredReturnsInstance(): void + { + $result = $this->subject->setRequired(false); + + self::assertSame($this->subject, $result); + } + + public function testSetRequiredCanSetNull(): void + { + $this->subject->setRequired(null); + + self::assertNull($this->subject->isRequired()); + } + + public function testGetTableNameReturnsTableName(): void + { + self::assertSame($this->tableName, $this->subject->getTableName()); + } + + public function testSetTableNameSetsTableName(): void + { + $newTableName = 'new_table'; + $this->subject->setTableName($newTableName); + + self::assertSame($newTableName, $this->subject->getTableName()); + } + + public function testSetTableNameReturnsInstance(): void + { + $result = $this->subject->setTableName('new_table'); + + self::assertSame($this->subject, $result); + } + + public function testSetTableNameCanSetNull(): void + { + $this->subject->setTableName(null); + + self::assertNull($this->subject->getTableName()); + } +} diff --git a/tests/Unit/Domain/Identity/Model/AdminAttributeValueTest.php b/tests/Unit/Domain/Identity/Model/AdminAttributeValueTest.php new file mode 100644 index 00000000..778f930c --- /dev/null +++ b/tests/Unit/Domain/Identity/Model/AdminAttributeValueTest.php @@ -0,0 +1,77 @@ +attributeDefinition = $this->createMock(AdminAttributeDefinition::class); + $this->attributeDefinition->method('getId')->willReturn($this->adminAttributeId); + + $this->administrator = $this->createMock(Administrator::class); + $this->administrator->method('getId')->willReturn($this->adminId); + + $this->subject = new AdminAttributeValue($this->attributeDefinition, $this->administrator, $this->value); + } + + public function testIsDomainModel(): void + { + self::assertInstanceOf(DomainModel::class, $this->subject); + } + + public function testGetAttributeDefinitionReturnsAttributeDefinition(): void + { + self::assertSame($this->attributeDefinition, $this->subject->getAttributeDefinition()); + } + + public function testGetAdministratorReturnsAdministrator(): void + { + self::assertSame($this->administrator, $this->subject->getAdministrator()); + } + + public function testGetValueReturnsValue(): void + { + self::assertSame($this->value, $this->subject->getValue()); + } + + public function testSetValueSetsValue(): void + { + $newValue = 'new-value'; + $this->subject->setValue($newValue); + + self::assertSame($newValue, $this->subject->getValue()); + } + + public function testSetValueReturnsInstance(): void + { + $result = $this->subject->setValue('new-value'); + + self::assertSame($this->subject, $result); + } + + public function testSetValueCanSetNull(): void + { + $this->subject->setValue(null); + + self::assertNull($this->subject->getValue()); + } +} diff --git a/tests/Unit/Domain/Identity/Repository/AdminAttributeValueRepositoryTest.php b/tests/Unit/Domain/Identity/Repository/AdminAttributeValueRepositoryTest.php new file mode 100644 index 00000000..99695df1 --- /dev/null +++ b/tests/Unit/Domain/Identity/Repository/AdminAttributeValueRepositoryTest.php @@ -0,0 +1,111 @@ +entityManager = $this->createMock(EntityManager::class); + $classMetadata = $this->createMock(ClassMetadata::class); + $classMetadata->name = AdminAttributeValue::class; + + $this->queryBuilder = $this->createMock(QueryBuilder::class); + $this->query = $this->createMock(Query::class); + + $this->subject = new AdminAttributeValueRepository($this->entityManager, $classMetadata); + } + + public function testIsAbstractRepository(): void + { + self::assertInstanceOf(AbstractRepository::class, $this->subject); + } + + public function testFindOneByAdminIdAndAttributeId(): void + { + $adminId = 1; + $attributeId = 2; + + $attributeDefinition = $this->createMock(AdminAttributeDefinition::class); + $attributeDefinition->method('getId')->willReturn($attributeId); + + $administrator = $this->createMock(Administrator::class); + $administrator->method('getId')->willReturn($adminId); + + $expectedResult = new AdminAttributeValue($attributeDefinition, $administrator, 'value'); + + $this->entityManager->expects($this->once()) + ->method('createQueryBuilder') + ->willReturn($this->queryBuilder); + + $this->queryBuilder->expects($this->once()) + ->method('select') + ->with('aav') + ->willReturn($this->queryBuilder); + + $this->queryBuilder->expects($this->once()) + ->method('from') + ->with(AdminAttributeValue::class, 'aav') + ->willReturn($this->queryBuilder); + + $this->queryBuilder->expects($this->exactly(2)) + ->method('join') + ->withConsecutive( + ['aav.administrator', 'admin'], + ['aav.attributeDefinition', 'attr'] + ) + ->willReturn($this->queryBuilder); + + $this->queryBuilder->expects($this->once()) + ->method('where') + ->with('admin.id = :adminId') + ->willReturn($this->queryBuilder); + + $this->queryBuilder->expects($this->once()) + ->method('andWhere') + ->with('attr.id = :attributeId') + ->willReturn($this->queryBuilder); + + $this->queryBuilder->expects($this->exactly(2)) + ->method('setParameter') + ->withConsecutive( + ['adminId', $adminId], + ['attributeId', $attributeId] + ) + ->willReturn($this->queryBuilder); + + $this->queryBuilder->expects($this->once()) + ->method('getQuery') + ->willReturn($this->query); + + $this->query->expects($this->once()) + ->method('getOneOrNullResult') + ->willReturn($expectedResult); + + $result = $this->subject->findOneByAdminIdAndAttributeId($adminId, $attributeId); + + $this->assertSame($expectedResult, $result); + } +} diff --git a/tests/Unit/Domain/Identity/Service/AdminAttributeDefinitionManagerTest.php b/tests/Unit/Domain/Identity/Service/AdminAttributeDefinitionManagerTest.php new file mode 100644 index 00000000..b557e2f0 --- /dev/null +++ b/tests/Unit/Domain/Identity/Service/AdminAttributeDefinitionManagerTest.php @@ -0,0 +1,204 @@ +repository = $this->createMock(AdminAttributeDefinitionRepository::class); + $this->subject = new AdminAttributeDefinitionManager($this->repository); + } + + public function testCreateCreatesNewAttributeDefinition(): void + { + $dto = new AdminAttributeDefinitionDto( + name: 'test-attribute', + type: 'text', + listOrder: 10, + defaultValue: 'default', + required: true, + tableName: 'test_table' + ); + + $this->repository->expects($this->once()) + ->method('findOneByName') + ->with('test-attribute') + ->willReturn(null); + + $this->repository->expects($this->once()) + ->method('save') + ->with($this->callback(function (AdminAttributeDefinition $definition) use ($dto) { + return $definition->getName() === $dto->name + && $definition->getType() === $dto->type + && $definition->getListOrder() === $dto->listOrder + && $definition->getDefaultValue() === $dto->defaultValue + && $definition->isRequired() === $dto->required + && $definition->getTableName() === $dto->tableName; + })); + + $result = $this->subject->create($dto); + + $this->assertInstanceOf(AdminAttributeDefinition::class, $result); + $this->assertEquals($dto->name, $result->getName()); + $this->assertEquals($dto->type, $result->getType()); + $this->assertEquals($dto->listOrder, $result->getListOrder()); + $this->assertEquals($dto->defaultValue, $result->getDefaultValue()); + $this->assertEquals($dto->required, $result->isRequired()); + $this->assertEquals($dto->tableName, $result->getTableName()); + } + + public function testCreateThrowsExceptionIfAttributeAlreadyExists(): void + { + $dto = new AdminAttributeDefinitionDto( + name: 'test-attribute' + ); + + $existingAttribute = $this->createMock(AdminAttributeDefinition::class); + + $this->repository->expects($this->once()) + ->method('findOneByName') + ->with('test-attribute') + ->willReturn($existingAttribute); + + $this->expectException(AttributeDefinitionCreationException::class); + $this->expectExceptionMessage('Attribute definition already exists'); + + $this->subject->create($dto); + } + + public function testUpdateUpdatesAttributeDefinition(): void + { + $attributeDefinition = $this->createMock(AdminAttributeDefinition::class); + $attributeDefinition->method('getId')->willReturn(1); + + $dto = new AdminAttributeDefinitionDto( + name: 'updated-attribute', + type: 'checkbox', + listOrder: 20, + defaultValue: 'new-default', + required: false, + tableName: 'new_table' + ); + + $this->repository->expects($this->once()) + ->method('findOneByName') + ->with('updated-attribute') + ->willReturn(null); + + $attributeDefinition->expects($this->once()) + ->method('setName') + ->with('updated-attribute') + ->willReturnSelf(); + + $attributeDefinition->expects($this->once()) + ->method('setType') + ->with('checkbox') + ->willReturnSelf(); + + $attributeDefinition->expects($this->once()) + ->method('setListOrder') + ->with(20) + ->willReturnSelf(); + + $attributeDefinition->expects($this->once()) + ->method('setDefaultValue') + ->with('new-default') + ->willReturnSelf(); + + $attributeDefinition->expects($this->once()) + ->method('setRequired') + ->with(false) + ->willReturnSelf(); + + $attributeDefinition->expects($this->once()) + ->method('setTableName') + ->with('new_table') + ->willReturnSelf(); + + $this->repository->expects($this->once()) + ->method('save') + ->with($attributeDefinition); + + $result = $this->subject->update($attributeDefinition, $dto); + + $this->assertSame($attributeDefinition, $result); + } + + public function testUpdateThrowsExceptionIfAnotherAttributeWithSameNameExists(): void + { + $attributeDefinition = $this->createMock(AdminAttributeDefinition::class); + $attributeDefinition->method('getId')->willReturn(1); + + $dto = new AdminAttributeDefinitionDto( + name: 'existing-attribute' + ); + + $existingAttribute = $this->createMock(AdminAttributeDefinition::class); + $existingAttribute->method('getId')->willReturn(2); + + $this->repository->expects($this->once()) + ->method('findOneByName') + ->with('existing-attribute') + ->willReturn($existingAttribute); + + $this->expectException(AttributeDefinitionCreationException::class); + $this->expectExceptionMessage('Another attribute with this name already exists.'); + + $this->subject->update($attributeDefinition, $dto); + } + + public function testDeleteCallsRemoveOnRepository(): void + { + $attributeDefinition = $this->createMock(AdminAttributeDefinition::class); + + $this->repository->expects($this->once()) + ->method('remove') + ->with($attributeDefinition); + + $this->subject->delete($attributeDefinition); + } + + public function testGetTotalCountReturnsCountFromRepository(): void + { + $this->repository->expects($this->once()) + ->method('count') + ->willReturn(42); + + $result = $this->subject->getTotalCount(); + + $this->assertEquals(42, $result); + } + + public function testGetAttributesAfterIdReturnsAttributesFromRepository(): void + { + $afterId = 10; + $limit = 20; + $attributes = [ + $this->createMock(AdminAttributeDefinition::class), + $this->createMock(AdminAttributeDefinition::class), + ]; + + $this->repository->expects($this->once()) + ->method('getAfterId') + ->with($afterId, $limit) + ->willReturn($attributes); + + $result = $this->subject->getAttributesAfterId($afterId, $limit); + + $this->assertSame($attributes, $result); + } +} diff --git a/tests/Unit/Domain/Identity/Service/AdminAttributeManagerTest.php b/tests/Unit/Domain/Identity/Service/AdminAttributeManagerTest.php new file mode 100644 index 00000000..9c5cac8f --- /dev/null +++ b/tests/Unit/Domain/Identity/Service/AdminAttributeManagerTest.php @@ -0,0 +1,171 @@ +repository = $this->createMock(AdminAttributeValueRepository::class); + $this->subject = new AdminAttributeManager($this->repository); + } + + public function testCreateOrUpdateCreatesNewAttributeIfNotExists(): void + { + $admin = $this->createMock(Administrator::class); + $admin->method('getId')->willReturn(1); + + $definition = $this->createMock(AdminAttributeDefinition::class); + $definition->method('getId')->willReturn(2); + $definition->method('getDefaultValue')->willReturn(null); + + $value = 'test-value'; + + $this->repository->expects($this->once()) + ->method('findOneByAdminIdAndAttributeId') + ->with(1, 2) + ->willReturn(null); + + $this->repository->expects($this->once()) + ->method('save') + ->with($this->callback(function (AdminAttributeValue $attribute) use ($value) { + return $attribute->getAdministrator()->getId() === 1 + && $attribute->getAttributeDefinition()->getId() === 2 + && $attribute->getValue() === $value; + })); + + $result = $this->subject->createOrUpdate($admin, $definition, $value); + + $this->assertInstanceOf(AdminAttributeValue::class, $result); + $this->assertEquals(1, $result->getAdministrator()->getId()); + $this->assertEquals(2, $result->getAttributeDefinition()->getId()); + $this->assertEquals($value, $result->getValue()); + } + + public function testCreateOrUpdateUpdatesExistingAttribute(): void + { + $admin = $this->createMock(Administrator::class); + $admin->method('getId')->willReturn(1); + + $definition = $this->createMock(AdminAttributeDefinition::class); + $definition->method('getId')->willReturn(2); + + $existingAttribute = new AdminAttributeValue($definition, $admin, 'old-value'); + $newValue = 'new-value'; + + $this->repository->expects($this->once()) + ->method('findOneByAdminIdAndAttributeId') + ->with(1, 2) + ->willReturn($existingAttribute); + + $this->repository->expects($this->once()) + ->method('save') + ->with($this->callback(function (AdminAttributeValue $attribute) use ($newValue) { + return $attribute->getValue() === $newValue; + })); + + $result = $this->subject->createOrUpdate($admin, $definition, $newValue); + + $this->assertSame($existingAttribute, $result); + $this->assertEquals($newValue, $result->getValue()); + } + + public function testCreateOrUpdateUsesDefaultValueIfValueIsNull(): void + { + $admin = $this->createMock(Administrator::class); + $admin->method('getId')->willReturn(1); + + $defaultValue = 'default-value'; + $definition = $this->createMock(AdminAttributeDefinition::class); + $definition->method('getId')->willReturn(2); + $definition->method('getDefaultValue')->willReturn($defaultValue); + + $this->repository->expects($this->once()) + ->method('findOneByAdminIdAndAttributeId') + ->willReturn(null); + + $this->repository->expects($this->once()) + ->method('save'); + + $result = $this->subject->createOrUpdate($admin, $definition); + + $this->assertEquals($defaultValue, $result->getValue()); + } + + public function testCreateOrUpdateThrowsExceptionIfValueAndDefaultValueAreNull(): void + { + $admin = $this->createMock(Administrator::class); + $admin->method('getId')->willReturn(1); + $definition = $this->createMock(AdminAttributeDefinition::class); + $definition->method('getId')->willReturn(2); + $definition->method('getDefaultValue')->willReturn(null); + + $this->repository->expects($this->once()) + ->method('findOneByAdminIdAndAttributeId') + ->willReturn(null); + + $this->expectException(AdminAttributeCreationException::class); + $this->expectExceptionMessage('Value is required'); + + $this->subject->createOrUpdate($admin, $definition); + } + + public function testGetAdminAttributeReturnsAttributeFromRepository(): void + { + $adminId = 1; + $attributeId = 2; + + $admin = $this->createMock(Administrator::class); + $admin->method('getId')->willReturn($adminId); + + $definition = $this->createMock(AdminAttributeDefinition::class); + $definition->method('getId')->willReturn($attributeId); + + $attribute = new AdminAttributeValue($definition, $admin, 'value'); + + $this->repository->expects($this->once()) + ->method('findOneByAdminIdAndAttributeId') + ->with($adminId, $attributeId) + ->willReturn($attribute); + + $result = $this->subject->getAdminAttribute($adminId, $attributeId); + + $this->assertSame($attribute, $result); + } + + public function testGetAdminAttributeReturnsNullIfNotFound(): void + { + $this->repository->expects($this->once()) + ->method('findOneByAdminIdAndAttributeId') + ->willReturn(null); + + $result = $this->subject->getAdminAttribute(1, 2); + + $this->assertNull($result); + } + + public function testDeleteCallsRemoveOnRepository(): void + { + $attribute = $this->createMock(AdminAttributeValue::class); + + $this->repository->expects($this->once()) + ->method('remove') + ->with($attribute); + + $this->subject->delete($attribute); + } +} From ba707358f70a752267bf631781474b2dfe2eaf59 Mon Sep 17 00:00:00 2001 From: Tatevik Date: Sun, 18 May 2025 20:37:36 +0400 Subject: [PATCH 13/15] ISSUE-345: import/export subscribers --- config/services/managers.yml | 5 + .../Repository/SubscriberRepository.php | 7 +- .../Service/SubscriberCsvManager.php | 277 ++++++++++++++++++ .../Service/SubscriberCsvManagerTest.php | 179 +++++++++++ .../Service/SubscriberCsvManagerTest.php | 258 ++++++++++++++++ 5 files changed, 724 insertions(+), 2 deletions(-) create mode 100644 src/Domain/Subscription/Service/SubscriberCsvManager.php create mode 100644 tests/Integration/Domain/Subscription/Service/SubscriberCsvManagerTest.php create mode 100644 tests/Unit/Domain/Subscription/Service/SubscriberCsvManagerTest.php diff --git a/config/services/managers.yml b/config/services/managers.yml index 374172ca..cdcc75a6 100644 --- a/config/services/managers.yml +++ b/config/services/managers.yml @@ -51,3 +51,8 @@ services: PhpList\Core\Domain\Subscription\Service\SubscriberAttributeManager: autowire: true autoconfigure: true + + PhpList\Core\Domain\Subscription\Service\SubscriberCsvManager: + autowire: true + autoconfigure: true + public: true diff --git a/src/Domain/Subscription/Repository/SubscriberRepository.php b/src/Domain/Subscription/Repository/SubscriberRepository.php index b559be8b..85270526 100644 --- a/src/Domain/Subscription/Repository/SubscriberRepository.php +++ b/src/Domain/Subscription/Repository/SubscriberRepository.php @@ -14,13 +14,16 @@ /** * Repository for Subscriber models. * - * @method Subscriber|null findOneByEmail(string $email) - * * @author Oliver Klee * @author Tatevik Grigoryan */ class SubscriberRepository extends AbstractRepository implements PaginatableRepositoryInterface { + public function findOneByEmail(string $email): ?Subscriber + { + return $this->findOneBy(['email' => $email]); + } + public function findSubscribersBySubscribedList(int $listId): ?Subscriber { return $this->createQueryBuilder('s') diff --git a/src/Domain/Subscription/Service/SubscriberCsvManager.php b/src/Domain/Subscription/Service/SubscriberCsvManager.php new file mode 100644 index 00000000..a1a90d4b --- /dev/null +++ b/src/Domain/Subscription/Service/SubscriberCsvManager.php @@ -0,0 +1,277 @@ +subscriberManager = $subscriberManager; + $this->attributeManager = $attributeManager; + $this->subscriberRepository = $subscriberRepository; + $this->attributeDefinitionRepository = $attributeDefinitionRepository; + } + + /** + * Import subscribers from a CSV file. + * + * @param UploadedFile $file The uploaded CSV file + * @param bool $updateExisting Whether to update existing subscribers + * @return array Import statistics + */ + public function importFromCsv(UploadedFile $file, bool $updateExisting = false): array + { + $stats = [ + 'created' => 0, + 'updated' => 0, + 'skipped' => 0, + 'errors' => [], + ]; + + $handle = fopen($file->getPathname(), 'r'); + if (!$handle) { + throw new RuntimeException('Could not open file for reading'); + } + + $headers = fgetcsv($handle); + if (!$headers) { + fclose($handle); + throw new RuntimeException('CSV file is empty or invalid'); + } + + if (!in_array('email', $headers, true)) { + fclose($handle); + throw new RuntimeException('CSV file must contain an "email" column'); + } + + $attributeDefinitions = []; + foreach ($headers as $index => $header) { + if (in_array($header, ['email', 'confirmed', 'blacklisted', 'html_email', 'disabled', 'extra_data'], true)) { + continue; + } + + $attributeDefinition = $this->attributeDefinitionRepository->findOneBy(['name' => $header]); + if ($attributeDefinition) { + $attributeDefinitions[$index] = $attributeDefinition; + } + } + + $lineNumber = 2; + while (($data = fgetcsv($handle)) !== false) { + try { + $email = trim($data[array_search('email', $headers, true)]); + if (empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) { + $stats['errors'][] = "Line $lineNumber: Invalid email address"; + $stats['skipped']++; + $lineNumber++; + continue; + } + + $existingSubscriber = $this->subscriberRepository->findOneByEmail($email); + + if ($existingSubscriber && !$updateExisting) { + $stats['skipped']++; + $lineNumber++; + continue; + } + + $confirmedIndex = array_search('confirmed', $headers, true); + if ($existingSubscriber) { + $confirmed = $confirmedIndex !== false && isset($data[$confirmedIndex]) + ? filter_var($data[$confirmedIndex], FILTER_VALIDATE_BOOLEAN) + : $existingSubscriber->isConfirmed(); + + $blacklistedIndex = array_search('blacklisted', $headers, true); + $blacklisted = $blacklistedIndex !== false && isset($data[$blacklistedIndex]) + ? filter_var($data[$blacklistedIndex], FILTER_VALIDATE_BOOLEAN) + : $existingSubscriber->isBlacklisted(); + + $htmlEmailIndex = array_search('html_email', $headers, true); + $htmlEmail = $htmlEmailIndex !== false && isset($data[$htmlEmailIndex]) + ? filter_var($data[$htmlEmailIndex], FILTER_VALIDATE_BOOLEAN) + : $existingSubscriber->hasHtmlEmail(); + + $disabledIndex = array_search('disabled', $headers, true); + $disabled = $disabledIndex !== false && isset($data[$disabledIndex]) + ? filter_var($data[$disabledIndex], FILTER_VALIDATE_BOOLEAN) + : $existingSubscriber->isDisabled(); + + $extraDataIndex = array_search('extra_data', $headers, true); + $additionalData = $extraDataIndex !== false && isset($data[$extraDataIndex]) + ? $data[$extraDataIndex] + : $existingSubscriber->getExtraData(); + + $dto = new UpdateSubscriberDto( + $existingSubscriber->getId(), + $email, + $confirmed, + $blacklisted, + $htmlEmail, + $disabled, + $additionalData + ); + + $subscriber = $this->subscriberManager->updateSubscriber($dto); + $stats['updated']++; + } else { + $requestConfirmation = !($confirmedIndex !== false && isset($data[$confirmedIndex]) && + filter_var($data[$confirmedIndex], FILTER_VALIDATE_BOOLEAN)); + + $htmlEmailIndex = array_search('html_email', $headers, true); + $htmlEmail = $htmlEmailIndex !== false && isset($data[$htmlEmailIndex]) && + filter_var($data[$htmlEmailIndex], FILTER_VALIDATE_BOOLEAN); + + $dto = new CreateSubscriberDto( + $email, + $requestConfirmation, + $htmlEmail + ); + + $subscriber = $this->subscriberManager->createSubscriber($dto); + + $blacklistedIndex = array_search('blacklisted', $headers, true); + if ($blacklistedIndex !== false && isset($data[$blacklistedIndex])) { + $subscriber->setBlacklisted(filter_var($data[$blacklistedIndex], FILTER_VALIDATE_BOOLEAN)); + } + + $disabledIndex = array_search('disabled', $headers, true); + if ($disabledIndex !== false && isset($data[$disabledIndex])) { + $subscriber->setDisabled(filter_var($data[$disabledIndex], FILTER_VALIDATE_BOOLEAN)); + } + + $extraDataIndex = array_search('extra_data', $headers, true); + if ($extraDataIndex !== false && isset($data[$extraDataIndex])) { + $subscriber->setExtraData($data[$extraDataIndex]); + } + + $this->subscriberRepository->save($subscriber); + $stats['created']++; + } + + foreach ($attributeDefinitions as $index => $attributeDefinition) { + if (isset($data[$index]) && $data[$index] !== '') { + $this->attributeManager->createOrUpdate( + $subscriber, + $attributeDefinition, + $data[$index] + ); + } + } + } catch (Exception $e) { + $stats['errors'][] = "Line $lineNumber: " . $e->getMessage(); + $stats['skipped']++; + } + + $lineNumber++; + } + + fclose($handle); + return $stats; + } + + /** + * Export subscribers to a CSV file. + * + * @param SubscriberFilter|null $filter Optional filter to apply + * @param int $batchSize Number of subscribers to process in each batch + * @return Response A streamed response with the CSV file + */ + public function exportToCsv(?SubscriberFilter $filter = null, int $batchSize = 1000): Response + { + if ($filter === null) { + $filter = new SubscriberFilter(); + } + + $response = new StreamedResponse(function () use ($filter, $batchSize) { + $handle = fopen('php://output', 'w'); + + $attributeDefinitions = $this->attributeDefinitionRepository->findAll(); + + $headers = [ + 'email', + 'confirmed', + 'blacklisted', + 'html_email', + 'disabled', + 'extra_data', + ]; + + foreach ($attributeDefinitions as $definition) { + $headers[] = $definition->getName(); + } + + fputcsv($handle, $headers); + + $lastId = 0; + + do { + $subscribers = $this->subscriberRepository->getFilteredAfterId( + lastId: $lastId, + limit: $batchSize, + filter: $filter + ); + + foreach ($subscribers as $subscriber) { + $row = [ + $subscriber->getEmail(), + $subscriber->isConfirmed() ? '1' : '0', + $subscriber->isBlacklisted() ? '1' : '0', + $subscriber->hasHtmlEmail() ? '1' : '0', + $subscriber->isDisabled() ? '1' : '0', + $subscriber->getExtraData(), + ]; + + foreach ($attributeDefinitions as $definition) { + $attributeValue = $this->attributeManager->getSubscriberAttribute( + subscriberId:$subscriber->getId(), + attributeDefinitionId: $definition->getId() + ); + $row[] = $attributeValue ? $attributeValue->getValue() : ''; + } + + fputcsv($handle, $row); + + $lastId = $subscriber->getId(); + } + + } while (count($subscribers) === $batchSize); + + fclose($handle); + }); + + $response->headers->set('Content-Type', 'text/csv; charset=utf-8'); + $disposition = $response->headers->makeDisposition( + ResponseHeaderBag::DISPOSITION_ATTACHMENT, + 'subscribers_export_' . date('Y-m-d') . '.csv' + ); + $response->headers->set('Content-Disposition', $disposition); + + return $response; + } +} diff --git a/tests/Integration/Domain/Subscription/Service/SubscriberCsvManagerTest.php b/tests/Integration/Domain/Subscription/Service/SubscriberCsvManagerTest.php new file mode 100644 index 00000000..d70da8bf --- /dev/null +++ b/tests/Integration/Domain/Subscription/Service/SubscriberCsvManagerTest.php @@ -0,0 +1,179 @@ +loadSchema(); + + $this->subscriberCsvManager = self::getContainer()->get(SubscriberCsvManager::class); + $this->subscriberRepository = self::getContainer()->get(SubscriberRepository::class); + } + + public function testImportFromCsvCreatesNewSubscribers(): void + { + $attributeDefinition = new SubscriberAttributeDefinition(); + $attributeDefinition->setName('first_name'); + $this->entityManager->persist($attributeDefinition); + $this->entityManager->flush(); + + $csvContent = "email,confirmed,html_email,blacklisted,disabled,extra_data,first_name\n"; + $csvContent .= "test@example.com,1,1,0,0,\"Some extra data\",John\n"; + $csvContent .= "another@example.com,0,0,1,1,\"More data\",Jane\n"; + + $tempFile = tempnam(sys_get_temp_dir(), 'csv_test'); + file_put_contents($tempFile, $csvContent); + + $uploadedFile = new UploadedFile( + $tempFile, + 'subscribers.csv', + 'text/csv', + null, + true + ); + + $subscriberCountBefore = count($this->subscriberRepository->findAll()); + + $result = $this->subscriberCsvManager->importFromCsv($uploadedFile); + + $subscriberCountAfter = count($this->subscriberRepository->findAll()); + + self::assertSame(2, $result['created']); + self::assertSame(0, $result['updated']); + self::assertSame(0, $result['skipped']); + self::assertEmpty($result['errors']); + self::assertSame($subscriberCountBefore + 2, $subscriberCountAfter); + + $subscriber1 = $this->subscriberRepository->findOneByEmail('test@example.com'); + self::assertInstanceOf(Subscriber::class, $subscriber1); + self::assertTrue($subscriber1->isConfirmed()); + self::assertTrue($subscriber1->hasHtmlEmail()); + self::assertFalse($subscriber1->isBlacklisted()); + self::assertFalse($subscriber1->isDisabled()); + self::assertSame('Some extra data', $subscriber1->getExtraData()); + + $subscriber2 = $this->subscriberRepository->findOneByEmail('another@example.com'); + self::assertInstanceOf(Subscriber::class, $subscriber2); + self::assertFalse($subscriber2->isConfirmed()); + self::assertFalse($subscriber2->hasHtmlEmail()); + self::assertTrue($subscriber2->isBlacklisted()); + self::assertTrue($subscriber2->isDisabled()); + self::assertSame('More data', $subscriber2->getExtraData()); + + unlink($tempFile); + } + + public function testImportFromCsvUpdatesExistingSubscribers(): void + { + $subscriber = new Subscriber(); + $subscriber->setEmail('existing@example.com'); + $subscriber->setConfirmed(false); + $subscriber->setHtmlEmail(false); + $subscriber->setBlacklisted(true); + $subscriber->setDisabled(true); + $subscriber->setExtraData('Old data'); + $this->entityManager->persist($subscriber); + $this->entityManager->flush(); + + $csvContent = "email,confirmed,html_email,blacklisted,disabled,extra_data\n"; + $csvContent .= "existing@example.com,1,1,0,0,\"Updated data\"\n"; + + $tempFile = tempnam(sys_get_temp_dir(), 'csv_test'); + file_put_contents($tempFile, $csvContent); + + $uploadedFile = new UploadedFile( + $tempFile, + 'subscribers.csv', + 'text/csv', + null, + true + ); + + $result = $this->subscriberCsvManager->importFromCsv($uploadedFile, true); + + self::assertSame(0, $result['created']); + self::assertSame(1, $result['updated']); + self::assertSame(0, $result['skipped']); + self::assertEmpty($result['errors']); + + $updatedSubscriber = $this->subscriberRepository->findOneByEmail('existing@example.com'); + self::assertInstanceOf(Subscriber::class, $updatedSubscriber); + self::assertTrue($updatedSubscriber->isConfirmed()); + self::assertTrue($updatedSubscriber->hasHtmlEmail()); + self::assertFalse($updatedSubscriber->isBlacklisted()); + self::assertFalse($updatedSubscriber->isDisabled()); + self::assertSame('Updated data', $updatedSubscriber->getExtraData()); + + unlink($tempFile); + } + + public function testExportToCsvReturnsStreamedResponse(): void + { + $subscriber1 = new Subscriber(); + $subscriber1->setEmail('test1@example.com'); + $subscriber1->setConfirmed(true); + $subscriber1->setHtmlEmail(true); + $subscriber1->setBlacklisted(false); + $subscriber1->setDisabled(false); + $subscriber1->setExtraData('Data 1'); + $this->entityManager->persist($subscriber1); + + $subscriber2 = new Subscriber(); + $subscriber2->setEmail('test2@example.com'); + $subscriber2->setConfirmed(false); + $subscriber2->setHtmlEmail(false); + $subscriber2->setBlacklisted(true); + $subscriber2->setDisabled(true); + $subscriber2->setExtraData('Data 2'); + $this->entityManager->persist($subscriber2); + + $this->entityManager->flush(); + + $savedSubscribers = $this->subscriberRepository->findAll(); + self::assertCount(2, $savedSubscribers); + + $filter = new SubscriberFilter(); + + $response = $this->subscriberCsvManager->exportToCsv($filter); + + self::assertInstanceOf(Response::class, $response); + self::assertSame('text/csv; charset=utf-8', $response->headers->get('Content-Type')); + self::assertStringContainsString( + 'attachment; filename=subscribers_export_', + $response->headers->get('Content-Disposition') + ); + + ob_start(); + $response->sendContent(); + $content = ob_get_clean(); + + echo "CSV Content: " . $content . "\n"; + + self::assertStringContainsString('email,confirmed,blacklisted,html_email,disabled,extra_data', $content); + } +} diff --git a/tests/Unit/Domain/Subscription/Service/SubscriberCsvManagerTest.php b/tests/Unit/Domain/Subscription/Service/SubscriberCsvManagerTest.php new file mode 100644 index 00000000..665c47ac --- /dev/null +++ b/tests/Unit/Domain/Subscription/Service/SubscriberCsvManagerTest.php @@ -0,0 +1,258 @@ +subscriberManagerMock = $this->createMock(SubscriberManager::class); + $this->attributeManagerMock = $this->createMock(SubscriberAttributeManager::class); + $this->subscriberRepositoryMock = $this->createMock(SubscriberRepository::class); + $this->attributeDefinitionRepositoryMock = $this->createMock(SubscriberAttributeDefinitionRepository::class); + + $this->subject = new SubscriberCsvManager( + subscriberManager: $this->subscriberManagerMock, + attributeManager: $this->attributeManagerMock, + subscriberRepository: $this->subscriberRepositoryMock, + attributeDefinitionRepository: $this->attributeDefinitionRepositoryMock + ); + } + + public function testImportFromCsvCreatesNewSubscribers(): void + { + $csvContent = "email,confirmed,html_email,blacklisted,disabled,extra_data,first_name\n"; + $csvContent .= "test@example.com,1,1,0,0,\"Some extra data\",John\n"; + $csvContent .= "another@example.com,0,0,1,1,\"More data\",Jane\n"; + + $tempFile = tempnam(sys_get_temp_dir(), 'csv_test'); + file_put_contents($tempFile, $csvContent); + + $uploadedFile = $this->createMock(UploadedFile::class); + $uploadedFile->method('getPathname')->willReturn($tempFile); + + $attributeDefinition = $this->createMock(SubscriberAttributeDefinition::class); + $attributeDefinition->method('getName')->willReturn('first_name'); + $attributeDefinition->method('getId')->willReturn(1); + + $this->attributeDefinitionRepositoryMock + ->method('findOneBy') + ->with(['name' => 'first_name']) + ->willReturn($attributeDefinition); + + $subscriber1 = $this->createMock(Subscriber::class); + $subscriber1->method('getId')->willReturn(1); + + $subscriber2 = $this->createMock(Subscriber::class); + $subscriber2->method('getId')->willReturn(2); + + $this->subscriberRepositoryMock + ->method('findOneByEmail') + ->willReturn(null); + + $this->subscriberManagerMock + ->expects($this->exactly(2)) + ->method('createSubscriber') + ->willReturnOnConsecutiveCalls($subscriber1, $subscriber2); + + $this->attributeManagerMock + ->expects($this->exactly(2)) + ->method('createOrUpdate') + ->withConsecutive( + [$subscriber1, $attributeDefinition, 'John'], + [$subscriber2, $attributeDefinition, 'Jane'] + ); + + $result = $this->subject->importFromCsv($uploadedFile); + + $this->assertSame(2, $result['created']); + $this->assertSame(0, $result['updated']); + $this->assertSame(0, $result['skipped']); + $this->assertEmpty($result['errors']); + + unlink($tempFile); + } + + public function testImportFromCsvUpdatesExistingSubscribers(): void + { + $csvContent = "email,confirmed,html_email,blacklisted,disabled,extra_data\n"; + $csvContent .= "existing@example.com,1,1,0,0,\"Updated data\"\n"; + + $tempFile = tempnam(sys_get_temp_dir(), 'csv_test'); + file_put_contents($tempFile, $csvContent); + + $uploadedFile = $this->createMock(UploadedFile::class); + $uploadedFile->method('getPathname')->willReturn($tempFile); + + $existingSubscriber = $this->createMock(Subscriber::class); + $existingSubscriber->method('getId')->willReturn(1); + $existingSubscriber->method('isConfirmed')->willReturn(false); + $existingSubscriber->method('hasHtmlEmail')->willReturn(false); + $existingSubscriber->method('isBlacklisted')->willReturn(true); + $existingSubscriber->method('isDisabled')->willReturn(true); + $existingSubscriber->method('getExtraData')->willReturn('Old data'); + + $this->subscriberRepositoryMock + ->method('findOneByEmail') + ->with('existing@example.com') + ->willReturn($existingSubscriber); + + $updatedSubscriber = $this->createMock(Subscriber::class); + + $this->subscriberManagerMock + ->expects($this->once()) + ->method('updateSubscriber') + ->with($this->callback(function (UpdateSubscriberDto $dto) { + return $dto->subscriberId === 1 + && $dto->email === 'existing@example.com' + && $dto->confirmed === true + && $dto->htmlEmail === true + && $dto->blacklisted === false + && $dto->disabled === false + && $dto->additionalData === 'Updated data'; + })) + ->willReturn($updatedSubscriber); + + $result = $this->subject->importFromCsv($uploadedFile, true); + + $this->assertSame(0, $result['created']); + $this->assertSame(1, $result['updated']); + $this->assertSame(0, $result['skipped']); + $this->assertEmpty($result['errors']); + + unlink($tempFile); + } + + public function testExportToCsvWithFilterReturnsStreamedResponse(): void + { + $subscriber1 = $this->createMock(Subscriber::class); + $subscriber1->method('getId')->willReturn(1); + $subscriber1->method('getEmail')->willReturn('test@example.com'); + $subscriber1->method('isConfirmed')->willReturn(true); + $subscriber1->method('isBlacklisted')->willReturn(false); + $subscriber1->method('hasHtmlEmail')->willReturn(true); + $subscriber1->method('isDisabled')->willReturn(false); + $subscriber1->method('getExtraData')->willReturn('Some data'); + + $subscriber2 = $this->createMock(Subscriber::class); + $subscriber2->method('getId')->willReturn(2); + $subscriber2->method('getEmail')->willReturn('another@example.com'); + $subscriber2->method('isConfirmed')->willReturn(false); + $subscriber2->method('isBlacklisted')->willReturn(true); + $subscriber2->method('hasHtmlEmail')->willReturn(false); + $subscriber2->method('isDisabled')->willReturn(true); + $subscriber2->method('getExtraData')->willReturn('More data'); + + $filter = new SubscriberFilter(); + $filter->setListId(1); + + $this->subscriberRepositoryMock + ->expects($this->exactly(2)) + ->method('getFilteredAfterId') + ->willReturnOnConsecutiveCalls( + [$subscriber1, $subscriber2], + [] + ); + + $attributeDefinition = $this->createMock(SubscriberAttributeDefinition::class); + $attributeDefinition->method('getName')->willReturn('first_name'); + $attributeDefinition->method('getId')->willReturn(1); + + $this->attributeDefinitionRepositoryMock + ->method('findAll') + ->willReturn([$attributeDefinition]); + + $attributeValue1 = $this->createMock(SubscriberAttributeValue::class); + $attributeValue1->method('getValue')->willReturn('John'); + + $attributeValue2 = $this->createMock(SubscriberAttributeValue::class); + $attributeValue2->method('getValue')->willReturn('Jane'); + + $this->attributeManagerMock + ->method('getSubscriberAttribute') + ->willReturnMap([ + [1, 1, $attributeValue1], + [2, 1, $attributeValue2], + ]); + + $response = $this->subject->exportToCsv($filter, 2); + $response->sendContent(); + + $this->assertInstanceOf(Response::class, $response); + $this->assertSame('text/csv; charset=utf-8', $response->headers->get('Content-Type')); + $this->assertStringContainsString( + needle: 'attachment; filename=subscribers_export_', + haystack: $response->headers->get('Content-Disposition') + ); + } + + public function testExportToCsvWithoutFilterCreatesDefaultFilter(): void + { + $subscriber1 = $this->createMock(Subscriber::class); + $subscriber1->method('getId')->willReturn(1); + $subscriber1->method('getEmail')->willReturn('test@example.com'); + $subscriber1->method('isConfirmed')->willReturn(true); + $subscriber1->method('isBlacklisted')->willReturn(false); + $subscriber1->method('hasHtmlEmail')->willReturn(true); + $subscriber1->method('isDisabled')->willReturn(false); + $subscriber1->method('getExtraData')->willReturn('Some data'); + + $this->subscriberRepositoryMock + ->expects($this->exactly(1)) + ->method('getFilteredAfterId') + ->willReturnOnConsecutiveCalls( + [$subscriber1], + [] + ); + + $attributeDefinition = $this->createMock(SubscriberAttributeDefinition::class); + $attributeDefinition->method('getName')->willReturn('first_name'); + $attributeDefinition->method('getId')->willReturn(1); + + $this->attributeDefinitionRepositoryMock + ->method('findAll') + ->willReturn([$attributeDefinition]); + + $attributeValue1 = $this->createMock(SubscriberAttributeValue::class); + $attributeValue1->method('getValue')->willReturn('John'); + + $this->attributeManagerMock + ->method('getSubscriberAttribute') + ->willReturnMap([ + [1, 1, $attributeValue1], + ]); + + $response = $this->subject->exportToCsv(); + $response->sendContent(); + + $this->assertInstanceOf(Response::class, $response); + $this->assertSame('text/csv; charset=utf-8', $response->headers->get('Content-Type')); + $this->assertStringContainsString( + needle: 'attachment; filename=subscribers_export_', + haystack: $response->headers->get('Content-Disposition') + ); + } +} From 7180d0a776ab2bb4f32f4871a911328ca5dc0776 Mon Sep 17 00:00:00 2001 From: Tatevik Date: Sun, 18 May 2025 22:11:48 +0400 Subject: [PATCH 14/15] ISSUE-345: refactor --- .../Service/SubscriberCsvManager.php | 551 +++++++++++++----- .../Service/SubscriberCsvManagerTest.php | 2 +- .../Service/SubscriberCsvManagerTest.php | 12 +- 3 files changed, 403 insertions(+), 162 deletions(-) diff --git a/src/Domain/Subscription/Service/SubscriberCsvManager.php b/src/Domain/Subscription/Service/SubscriberCsvManager.php index a1a90d4b..0bc3c5f2 100644 --- a/src/Domain/Subscription/Service/SubscriberCsvManager.php +++ b/src/Domain/Subscription/Service/SubscriberCsvManager.php @@ -8,6 +8,7 @@ use PhpList\Core\Domain\Subscription\Model\Dto\CreateSubscriberDto; use PhpList\Core\Domain\Subscription\Model\Dto\UpdateSubscriberDto; use PhpList\Core\Domain\Subscription\Model\Filter\SubscriberFilter; +use PhpList\Core\Domain\Subscription\Model\Subscriber; use PhpList\Core\Domain\Subscription\Repository\SubscriberAttributeDefinitionRepository; use PhpList\Core\Domain\Subscription\Repository\SubscriberRepository; use RuntimeException; @@ -24,7 +25,7 @@ class SubscriberCsvManager private SubscriberManager $subscriberManager; private SubscriberAttributeManager $attributeManager; private SubscriberRepository $subscriberRepository; - private SubscriberAttributeDefinitionRepository $attributeDefinitionRepository; + private SubscriberAttributeDefinitionRepository $attrDefRepository; public function __construct( SubscriberManager $subscriberManager, @@ -35,7 +36,7 @@ public function __construct( $this->subscriberManager = $subscriberManager; $this->attributeManager = $attributeManager; $this->subscriberRepository = $subscriberRepository; - $this->attributeDefinitionRepository = $attributeDefinitionRepository; + $this->attrDefRepository = $attributeDefinitionRepository; } /** @@ -54,6 +55,57 @@ public function importFromCsv(UploadedFile $file, bool $updateExisting = false): 'errors' => [], ]; + [$handle, $headers, $attributeDefinitions] = $this->prepareImport($file); + + $lineNumber = 2; + $data = fgetcsv($handle); + while ($data !== false) { + try { + $this->processRow($data, $headers, $attributeDefinitions, $updateExisting, $stats, $lineNumber); + } catch (Exception $e) { + $stats['errors'][] = 'Line ' . $lineNumber . ': ' . $e->getMessage(); + $stats['skipped']++; + } + + $lineNumber++; + $data = fgetcsv($handle); + } + + fclose($handle); + return $stats; + } + + /** + * Import subscribers with update strategy. + * + * @param UploadedFile $file The uploaded CSV file + * @return array Import statistics + */ + public function importAndUpdateFromCsv(UploadedFile $file): array + { + return $this->importFromCsv($file, true); + } + + /** + * Import subscribers without updating existing ones. + * + * @param UploadedFile $file The uploaded CSV file + * @return array Import statistics + */ + public function importNewFromCsv(UploadedFile $file): array + { + return $this->importFromCsv($file, false); + } + + /** + * Prepare for import by opening file and validating headers. + * + * @param UploadedFile $file The uploaded CSV file + * @return array [file handle, headers, attribute definitions] + * @throws RuntimeException If file cannot be opened or is invalid + */ + private function prepareImport(UploadedFile $file): array + { $handle = fopen($file->getPathname(), 'r'); if (!$handle) { throw new RuntimeException('Could not open file for reading'); @@ -70,129 +122,259 @@ public function importFromCsv(UploadedFile $file, bool $updateExisting = false): throw new RuntimeException('CSV file must contain an "email" column'); } + $attributeDefinitions = $this->getAttributeDefinitions($headers); + + return [$handle, $headers, $attributeDefinitions]; + } + + /** + * Get attribute definitions from headers. + * + * @param array $headers CSV headers + * @return array Attribute definitions indexed by column position + */ + private function getAttributeDefinitions(array $headers): array + { $attributeDefinitions = []; + $systemFields = ['email', 'confirmed', 'blacklisted', 'html_email', 'disabled', 'extra_data']; + foreach ($headers as $index => $header) { - if (in_array($header, ['email', 'confirmed', 'blacklisted', 'html_email', 'disabled', 'extra_data'], true)) { + if (in_array($header, $systemFields, true)) { continue; } - $attributeDefinition = $this->attributeDefinitionRepository->findOneBy(['name' => $header]); + $attributeDefinition = $this->attrDefRepository->findOneBy(['name' => $header]); if ($attributeDefinition) { $attributeDefinitions[$index] = $attributeDefinition; } } - $lineNumber = 2; - while (($data = fgetcsv($handle)) !== false) { - try { - $email = trim($data[array_search('email', $headers, true)]); - if (empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) { - $stats['errors'][] = "Line $lineNumber: Invalid email address"; - $stats['skipped']++; - $lineNumber++; - continue; - } - - $existingSubscriber = $this->subscriberRepository->findOneByEmail($email); - - if ($existingSubscriber && !$updateExisting) { - $stats['skipped']++; - $lineNumber++; - continue; - } - - $confirmedIndex = array_search('confirmed', $headers, true); - if ($existingSubscriber) { - $confirmed = $confirmedIndex !== false && isset($data[$confirmedIndex]) - ? filter_var($data[$confirmedIndex], FILTER_VALIDATE_BOOLEAN) - : $existingSubscriber->isConfirmed(); - - $blacklistedIndex = array_search('blacklisted', $headers, true); - $blacklisted = $blacklistedIndex !== false && isset($data[$blacklistedIndex]) - ? filter_var($data[$blacklistedIndex], FILTER_VALIDATE_BOOLEAN) - : $existingSubscriber->isBlacklisted(); - - $htmlEmailIndex = array_search('html_email', $headers, true); - $htmlEmail = $htmlEmailIndex !== false && isset($data[$htmlEmailIndex]) - ? filter_var($data[$htmlEmailIndex], FILTER_VALIDATE_BOOLEAN) - : $existingSubscriber->hasHtmlEmail(); - - $disabledIndex = array_search('disabled', $headers, true); - $disabled = $disabledIndex !== false && isset($data[$disabledIndex]) - ? filter_var($data[$disabledIndex], FILTER_VALIDATE_BOOLEAN) - : $existingSubscriber->isDisabled(); - - $extraDataIndex = array_search('extra_data', $headers, true); - $additionalData = $extraDataIndex !== false && isset($data[$extraDataIndex]) - ? $data[$extraDataIndex] - : $existingSubscriber->getExtraData(); - - $dto = new UpdateSubscriberDto( - $existingSubscriber->getId(), - $email, - $confirmed, - $blacklisted, - $htmlEmail, - $disabled, - $additionalData - ); - - $subscriber = $this->subscriberManager->updateSubscriber($dto); - $stats['updated']++; - } else { - $requestConfirmation = !($confirmedIndex !== false && isset($data[$confirmedIndex]) && - filter_var($data[$confirmedIndex], FILTER_VALIDATE_BOOLEAN)); - - $htmlEmailIndex = array_search('html_email', $headers, true); - $htmlEmail = $htmlEmailIndex !== false && isset($data[$htmlEmailIndex]) && - filter_var($data[$htmlEmailIndex], FILTER_VALIDATE_BOOLEAN); - - $dto = new CreateSubscriberDto( - $email, - $requestConfirmation, - $htmlEmail - ); - - $subscriber = $this->subscriberManager->createSubscriber($dto); - - $blacklistedIndex = array_search('blacklisted', $headers, true); - if ($blacklistedIndex !== false && isset($data[$blacklistedIndex])) { - $subscriber->setBlacklisted(filter_var($data[$blacklistedIndex], FILTER_VALIDATE_BOOLEAN)); - } - - $disabledIndex = array_search('disabled', $headers, true); - if ($disabledIndex !== false && isset($data[$disabledIndex])) { - $subscriber->setDisabled(filter_var($data[$disabledIndex], FILTER_VALIDATE_BOOLEAN)); - } - - $extraDataIndex = array_search('extra_data', $headers, true); - if ($extraDataIndex !== false && isset($data[$extraDataIndex])) { - $subscriber->setExtraData($data[$extraDataIndex]); - } - - $this->subscriberRepository->save($subscriber); - $stats['created']++; - } - - foreach ($attributeDefinitions as $index => $attributeDefinition) { - if (isset($data[$index]) && $data[$index] !== '') { - $this->attributeManager->createOrUpdate( - $subscriber, - $attributeDefinition, - $data[$index] - ); - } - } - } catch (Exception $e) { - $stats['errors'][] = "Line $lineNumber: " . $e->getMessage(); - $stats['skipped']++; - } + return $attributeDefinitions; + } - $lineNumber++; + /** + * Process a single row from the CSV file. + * + * @param array $data Row data + * @param array $headers CSV headers + * @param array $attributeDefinitions Attribute definitions + * @param bool $updateExisting Whether to update existing subscribers + * @param array $stats Statistics to update + * @param int $lineNumber Current line number for error reporting + */ + private function processRow( + array $data, + array $headers, + array $attributeDefinitions, + bool $updateExisting, + array &$stats, + int $lineNumber + ): void { + $email = trim($data[array_search('email', $headers, true)]); + if (empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) { + $stats['errors'][] = 'Line ' . $lineNumber . ': Invalid email address'; + $stats['skipped']++; + return; } - fclose($handle); - return $stats; + $existingSubscriber = $this->subscriberRepository->findOneByEmail($email); + + if ($existingSubscriber && !$updateExisting) { + $stats['skipped']++; + return; + } + + $subscriber = $this->createOrUpdateSubscriber( + $email, + $data, + $headers, + $existingSubscriber, + $stats + ); + + $this->processAttributes($subscriber, $data, $attributeDefinitions); + } + + /** + * Create a new subscriber or update an existing one. + * + * @param string $email Subscriber email + * @param array $data Row data + * @param array $headers CSV headers + * @param Subscriber|null $existingSubscriber Existing subscriber if found + * @param array $stats Statistics to update + * @return Subscriber The created or updated subscriber + */ + private function createOrUpdateSubscriber( + string $email, + array $data, + array $headers, + ?Subscriber $existingSubscriber, + array &$stats + ): Subscriber { + $confirmedIndex = array_search('confirmed', $headers, true); + + if ($existingSubscriber) { + $subscriber = $this->updateExistingSubscriber( + $existingSubscriber, + $email, + $data, + $headers, + $confirmedIndex + ); + $stats['updated']++; + } else { + $subscriber = $this->createNewSubscriber( + $email, + $data, + $headers, + $confirmedIndex + ); + $stats['created']++; + } + + return $subscriber; + } + + /** + * Update an existing subscriber. + * + * @param Subscriber $existingSubscriber The subscriber to update + * @param string $email Subscriber email + * @param array $data Row data + * @param array $headers CSV headers + * @param int|false $confirmedIndex Index of confirmed column + * @return Subscriber The updated subscriber + */ + private function updateExistingSubscriber( + Subscriber $existingSubscriber, + string $email, + array $data, + array $headers, + $confirmedIndex + ): Subscriber { + $confirmed = $this->isBooleanTrue($data, $confirmedIndex, $existingSubscriber->isConfirmed()); + + $blacklistedIndex = array_search('blacklisted', $headers, true); + $blacklisted = $this->isBooleanTrue($data, $blacklistedIndex, $existingSubscriber->isBlacklisted()); + + $htmlEmailIndex = array_search('html_email', $headers, true); + $htmlEmail = $this->isBooleanTrue($data, $htmlEmailIndex, $existingSubscriber->hasHtmlEmail()); + + $disabledIndex = array_search('disabled', $headers, true); + $disabled = $this->isBooleanTrue($data, $disabledIndex, $existingSubscriber->isDisabled()); + + $extraDataIndex = array_search('extra_data', $headers, true); + $additionalData = $extraDataIndex !== false && isset($data[$extraDataIndex]) ? $data[$extraDataIndex] : $existingSubscriber->getExtraData(); + + $dto = new UpdateSubscriberDto( + $existingSubscriber->getId(), + $email, + $confirmed, + $blacklisted, + $htmlEmail, + $disabled, + $additionalData + ); + + return $this->subscriberManager->updateSubscriber($dto); + } + + /** + * Create a new subscriber. + * + * @param string $email Subscriber email + * @param array $data Row data + * @param array $headers CSV headers + * @param int|false $confirmedIndex Index of confirmed column + * @return Subscriber The created subscriber + */ + private function createNewSubscriber( + string $email, + array $data, + array $headers, + $confirmedIndex + ): Subscriber { + $requestConfirmation = !($confirmedIndex !== false && isset($data[$confirmedIndex]) && + filter_var($data[$confirmedIndex], FILTER_VALIDATE_BOOLEAN)); + + $htmlEmailIndex = array_search('html_email', $headers, true); + $htmlEmail = $htmlEmailIndex !== false && isset($data[$htmlEmailIndex]) && + filter_var($data[$htmlEmailIndex], FILTER_VALIDATE_BOOLEAN); + + $dto = new CreateSubscriberDto( + $email, + $requestConfirmation, + $htmlEmail + ); + + $subscriber = $this->subscriberManager->createSubscriber($dto); + + $this->setOptionalBooleanField($subscriber, 'setBlacklisted', $data, $headers, 'blacklisted'); + $this->setOptionalBooleanField($subscriber, 'setDisabled', $data, $headers, 'disabled'); + + $extraDataIndex = array_search('extra_data', $headers, true); + if ($extraDataIndex !== false && isset($data[$extraDataIndex])) { + $subscriber->setExtraData($data[$extraDataIndex]); + } + + $this->subscriberRepository->save($subscriber); + return $subscriber; + } + + /** + * Set an optional boolean field on a subscriber if it exists in the data. + * + * @param Subscriber $subscriber The subscriber to update + * @param string $method The method to call on the subscriber + * @param array $data Row data + * @param array $headers CSV headers + * @param string $fieldName The field name to look for in headers + */ + private function setOptionalBooleanField( + Subscriber $subscriber, + string $method, + array $data, + array $headers, + string $fieldName + ): void { + $index = array_search($fieldName, $headers, true); + if ($index !== false && isset($data[$index])) { + $subscriber->$method(filter_var($data[$index], FILTER_VALIDATE_BOOLEAN)); + } + } + + /** + * Check if a boolean value is true in data, with fallback. + * + * @param array $data Row data + * @param int|false $index Index of the column + * @param bool $default Default value if not found + * @return bool The boolean value + */ + private function isBooleanTrue(array $data, $index, bool $default): bool + { + return $index !== false && isset($data[$index]) ? filter_var($data[$index], FILTER_VALIDATE_BOOLEAN) : $default; + } + + /** + * Process subscriber attributes. + * + * @param Subscriber $subscriber The subscriber + * @param array $data Row data + * @param array $attributeDefinitions Attribute definitions + */ + private function processAttributes(Subscriber $subscriber, array $data, array $attributeDefinitions): void + { + foreach ($attributeDefinitions as $index => $attributeDefinition) { + if (isset($data[$index]) && $data[$index] !== '') { + $this->attributeManager->createOrUpdate( + $subscriber, + $attributeDefinition, + $data[$index] + ); + } + } } /** @@ -209,62 +391,121 @@ public function exportToCsv(?SubscriberFilter $filter = null, int $batchSize = 1 } $response = new StreamedResponse(function () use ($filter, $batchSize) { - $handle = fopen('php://output', 'w'); + $this->generateCsvContent($filter, $batchSize); + }); - $attributeDefinitions = $this->attributeDefinitionRepository->findAll(); + return $this->configureResponse($response); + } - $headers = [ - 'email', - 'confirmed', - 'blacklisted', - 'html_email', - 'disabled', - 'extra_data', - ]; + /** + * Generate CSV content for the export. + * + * @param SubscriberFilter $filter Filter to apply + * @param int $batchSize Batch size for processing + */ + private function generateCsvContent(SubscriberFilter $filter, int $batchSize): void + { + $handle = fopen('php://output', 'w'); + $attributeDefinitions = $this->attrDefRepository->findAll(); - foreach ($attributeDefinitions as $definition) { - $headers[] = $definition->getName(); - } + $headers = $this->getExportHeaders($attributeDefinitions); + fputcsv($handle, $headers); - fputcsv($handle, $headers); + $this->exportSubscribers($handle, $filter, $batchSize, $attributeDefinitions); - $lastId = 0; + fclose($handle); + } - do { - $subscribers = $this->subscriberRepository->getFilteredAfterId( - lastId: $lastId, - limit: $batchSize, - filter: $filter - ); + /** + * Get headers for the export CSV. + * + * @param array $attributeDefinitions Attribute definitions + * @return array Headers + */ + private function getExportHeaders(array $attributeDefinitions): array + { + $headers = [ + 'email', + 'confirmed', + 'blacklisted', + 'html_email', + 'disabled', + 'extra_data', + ]; - foreach ($subscribers as $subscriber) { - $row = [ - $subscriber->getEmail(), - $subscriber->isConfirmed() ? '1' : '0', - $subscriber->isBlacklisted() ? '1' : '0', - $subscriber->hasHtmlEmail() ? '1' : '0', - $subscriber->isDisabled() ? '1' : '0', - $subscriber->getExtraData(), - ]; + foreach ($attributeDefinitions as $definition) { + $headers[] = $definition->getName(); + } + + return $headers; + } - foreach ($attributeDefinitions as $definition) { - $attributeValue = $this->attributeManager->getSubscriberAttribute( - subscriberId:$subscriber->getId(), - attributeDefinitionId: $definition->getId() - ); - $row[] = $attributeValue ? $attributeValue->getValue() : ''; - } + /** + * Export subscribers in batches. + * + * @param resource $handle File handle + * @param SubscriberFilter $filter Filter to apply + * @param int $batchSize Batch size + * @param array $attributeDefinitions Attribute definitions + */ + private function exportSubscribers($handle, SubscriberFilter $filter, int $batchSize, array $attributeDefinitions): void + { + $lastId = 0; + + do { + $subscribers = $this->subscriberRepository->getFilteredAfterId( + lastId: $lastId, + limit: $batchSize, + filter: $filter + ); + + foreach ($subscribers as $subscriber) { + $row = $this->getSubscriberRow($subscriber, $attributeDefinitions); + fputcsv($handle, $row); + $lastId = $subscriber->getId(); + } - fputcsv($handle, $row); + $subscriberCount = count($subscribers); + } while ($subscriberCount === $batchSize); + } - $lastId = $subscriber->getId(); - } + /** + * Get a row of data for a subscriber. + * + * @param Subscriber $subscriber The subscriber + * @param array $attributeDefinitions Attribute definitions + * @return array Row data + */ + private function getSubscriberRow(Subscriber $subscriber, array $attributeDefinitions): array + { + $row = [ + $subscriber->getEmail(), + $subscriber->isConfirmed() ? '1' : '0', + $subscriber->isBlacklisted() ? '1' : '0', + $subscriber->hasHtmlEmail() ? '1' : '0', + $subscriber->isDisabled() ? '1' : '0', + $subscriber->getExtraData(), + ]; - } while (count($subscribers) === $batchSize); + foreach ($attributeDefinitions as $definition) { + $attributeValue = $this->attributeManager->getSubscriberAttribute( + subscriberId: $subscriber->getId(), + attributeDefinitionId: $definition->getId() + ); + $row[] = $attributeValue ? $attributeValue->getValue() : ''; + } - fclose($handle); - }); + return $row; + } + /** + * Configure the response for CSV download. + * + * @param StreamedResponse $response The response + * @return Response The configured response + */ + private function configureResponse(StreamedResponse $response): Response + { $response->headers->set('Content-Type', 'text/csv; charset=utf-8'); $disposition = $response->headers->makeDisposition( ResponseHeaderBag::DISPOSITION_ATTACHMENT, diff --git a/tests/Integration/Domain/Subscription/Service/SubscriberCsvManagerTest.php b/tests/Integration/Domain/Subscription/Service/SubscriberCsvManagerTest.php index d70da8bf..d03a9ead 100644 --- a/tests/Integration/Domain/Subscription/Service/SubscriberCsvManagerTest.php +++ b/tests/Integration/Domain/Subscription/Service/SubscriberCsvManagerTest.php @@ -172,7 +172,7 @@ public function testExportToCsvReturnsStreamedResponse(): void $response->sendContent(); $content = ob_get_clean(); - echo "CSV Content: " . $content . "\n"; + echo 'CSV Content: ' . $content . "\n"; self::assertStringContainsString('email,confirmed,blacklisted,html_email,disabled,extra_data', $content); } diff --git a/tests/Unit/Domain/Subscription/Service/SubscriberCsvManagerTest.php b/tests/Unit/Domain/Subscription/Service/SubscriberCsvManagerTest.php index 665c47ac..a0c8b881 100644 --- a/tests/Unit/Domain/Subscription/Service/SubscriberCsvManagerTest.php +++ b/tests/Unit/Domain/Subscription/Service/SubscriberCsvManagerTest.php @@ -126,12 +126,12 @@ public function testImportFromCsvUpdatesExistingSubscribers(): void ->expects($this->once()) ->method('updateSubscriber') ->with($this->callback(function (UpdateSubscriberDto $dto) { - return $dto->subscriberId === 1 - && $dto->email === 'existing@example.com' - && $dto->confirmed === true - && $dto->htmlEmail === true - && $dto->blacklisted === false - && $dto->disabled === false + return $dto->subscriberId === 1 + && $dto->email === 'existing@example.com' + && $dto->confirmed === true + && $dto->htmlEmail === true + && $dto->blacklisted === false + && $dto->disabled === false && $dto->additionalData === 'Updated data'; })) ->willReturn($updatedSubscriber); From 44b328a1f2eea691596a9be2efa0d2e6b37f647c Mon Sep 17 00:00:00 2001 From: Tatevik Date: Mon, 19 May 2025 19:43:12 +0400 Subject: [PATCH 15/15] ISSUE-345: update tests --- config/services/managers.yml | 7 +- .../Model/Dto/SubscriberImportOptions.php | 16 ++ .../Service/SubscriberCsvExportManager.php | 176 +++++++++++++++++ ...ger.php => SubscriberCsvImportManager.php} | 183 +++--------------- .../SubscriberCsvExportManagerTest.php | 78 ++++++++ ...php => SubscriberCsvImportManagerTest.php} | 64 +----- ...php => SubscriberCsvExportManagerTest.php} | 124 +----------- .../SubscriberCsvImportManagerTest.php | 148 ++++++++++++++ 8 files changed, 467 insertions(+), 329 deletions(-) create mode 100644 src/Domain/Subscription/Model/Dto/SubscriberImportOptions.php create mode 100644 src/Domain/Subscription/Service/SubscriberCsvExportManager.php rename src/Domain/Subscription/Service/{SubscriberCsvManager.php => SubscriberCsvImportManager.php} (67%) create mode 100644 tests/Integration/Domain/Subscription/Service/SubscriberCsvExportManagerTest.php rename tests/Integration/Domain/Subscription/Service/{SubscriberCsvManagerTest.php => SubscriberCsvImportManagerTest.php} (67%) rename tests/Unit/Domain/Subscription/Service/{SubscriberCsvManagerTest.php => SubscriberCsvExportManagerTest.php} (54%) create mode 100644 tests/Unit/Domain/Subscription/Service/SubscriberCsvImportManagerTest.php diff --git a/config/services/managers.yml b/config/services/managers.yml index cdcc75a6..e038a06b 100644 --- a/config/services/managers.yml +++ b/config/services/managers.yml @@ -52,7 +52,12 @@ services: autowire: true autoconfigure: true - PhpList\Core\Domain\Subscription\Service\SubscriberCsvManager: + PhpList\Core\Domain\Subscription\Service\SubscriberCsvExportManager: + autowire: true + autoconfigure: true + public: true + + PhpList\Core\Domain\Subscription\Service\SubscriberCsvImportManager: autowire: true autoconfigure: true public: true diff --git a/src/Domain/Subscription/Model/Dto/SubscriberImportOptions.php b/src/Domain/Subscription/Model/Dto/SubscriberImportOptions.php new file mode 100644 index 00000000..7bfc1cc6 --- /dev/null +++ b/src/Domain/Subscription/Model/Dto/SubscriberImportOptions.php @@ -0,0 +1,16 @@ +attributeManager = $attributeManager; + $this->subscriberRepository = $subscriberRepository; + $this->definitionRepository = $definitionRepository; + } + + /** + * Export subscribers to a CSV file. + * + * @param SubscriberFilter|null $filter Optional filter to apply + * @param int $batchSize Number of subscribers to process in each batch + * @return Response A streamed response with the CSV file + */ + public function exportToCsv(?SubscriberFilter $filter = null, int $batchSize = 1000): Response + { + if ($filter === null) { + $filter = new SubscriberFilter(); + } + + $response = new StreamedResponse(function () use ($filter, $batchSize) { + $this->generateCsvContent($filter, $batchSize); + }); + + return $this->configureResponse($response); + } + + /** + * Generate CSV content for the export. + * + * @param SubscriberFilter $filter Filter to apply + * @param int $batchSize Batch size for processing + */ + private function generateCsvContent(SubscriberFilter $filter, int $batchSize): void + { + $handle = fopen('php://output', 'w'); + $attributeDefinitions = $this->definitionRepository->findAll(); + + $headers = $this->getExportHeaders($attributeDefinitions); + fputcsv($handle, $headers); + + $this->exportSubscribers($handle, $filter, $batchSize, $attributeDefinitions); + + fclose($handle); + } + + /** + * Get headers for the export CSV. + * + * @param array $attributeDefinitions Attribute definitions + * @return array Headers + */ + private function getExportHeaders(array $attributeDefinitions): array + { + $headers = [ + 'email', + 'confirmed', + 'blacklisted', + 'html_email', + 'disabled', + 'extra_data', + ]; + + foreach ($attributeDefinitions as $definition) { + $headers[] = $definition->getName(); + } + + return $headers; + } + + /** + * Export subscribers in batches. + * + * @param resource $handle File handle + * @param SubscriberFilter $filter Filter to apply + * @param int $batchSize Batch size + * @param array $attributeDefinitions Attribute definitions + */ + private function exportSubscribers( + $handle, + SubscriberFilter $filter, + int $batchSize, + array $attributeDefinitions + ): void { + $lastId = 0; + + do { + $subscribers = $this->subscriberRepository->getFilteredAfterId( + lastId: $lastId, + limit: $batchSize, + filter: $filter + ); + + foreach ($subscribers as $subscriber) { + $row = $this->getSubscriberRow($subscriber, $attributeDefinitions); + fputcsv($handle, $row); + $lastId = $subscriber->getId(); + } + + $subscriberCount = count($subscribers); + } while ($subscriberCount === $batchSize); + } + + /** + * Get a row of data for a subscriber. + * + * @param Subscriber $subscriber The subscriber + * @param array $attributeDefinitions Attribute definitions + * @return array Row data + */ + private function getSubscriberRow(Subscriber $subscriber, array $attributeDefinitions): array + { + $row = [ + $subscriber->getEmail(), + $subscriber->isConfirmed() ? '1' : '0', + $subscriber->isBlacklisted() ? '1' : '0', + $subscriber->hasHtmlEmail() ? '1' : '0', + $subscriber->isDisabled() ? '1' : '0', + $subscriber->getExtraData(), + ]; + + foreach ($attributeDefinitions as $definition) { + $attributeValue = $this->attributeManager->getSubscriberAttribute( + subscriberId: $subscriber->getId(), + attributeDefinitionId: $definition->getId() + ); + $row[] = $attributeValue ? $attributeValue->getValue() : ''; + } + + return $row; + } + + /** + * Configure the response for CSV download. + * + * @param StreamedResponse $response The response + * @return Response The configured response + */ + private function configureResponse(StreamedResponse $response): Response + { + $response->headers->set('Content-Type', 'text/csv; charset=utf-8'); + $disposition = $response->headers->makeDisposition( + ResponseHeaderBag::DISPOSITION_ATTACHMENT, + 'subscribers_export_' . date('Y-m-d') . '.csv' + ); + $response->headers->set('Content-Disposition', $disposition); + + return $response; + } +} diff --git a/src/Domain/Subscription/Service/SubscriberCsvManager.php b/src/Domain/Subscription/Service/SubscriberCsvImportManager.php similarity index 67% rename from src/Domain/Subscription/Service/SubscriberCsvManager.php rename to src/Domain/Subscription/Service/SubscriberCsvImportManager.php index 0bc3c5f2..a0a03def 100644 --- a/src/Domain/Subscription/Service/SubscriberCsvManager.php +++ b/src/Domain/Subscription/Service/SubscriberCsvImportManager.php @@ -6,47 +6,44 @@ use Exception; use PhpList\Core\Domain\Subscription\Model\Dto\CreateSubscriberDto; +use PhpList\Core\Domain\Subscription\Model\Dto\SubscriberImportOptions; use PhpList\Core\Domain\Subscription\Model\Dto\UpdateSubscriberDto; -use PhpList\Core\Domain\Subscription\Model\Filter\SubscriberFilter; use PhpList\Core\Domain\Subscription\Model\Subscriber; use PhpList\Core\Domain\Subscription\Repository\SubscriberAttributeDefinitionRepository; use PhpList\Core\Domain\Subscription\Repository\SubscriberRepository; use RuntimeException; use Symfony\Component\HttpFoundation\File\UploadedFile; -use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpFoundation\ResponseHeaderBag; -use Symfony\Component\HttpFoundation\StreamedResponse; /** * Service for importing and exporting subscribers from/to CSV files. */ -class SubscriberCsvManager +class SubscriberCsvImportManager { private SubscriberManager $subscriberManager; private SubscriberAttributeManager $attributeManager; private SubscriberRepository $subscriberRepository; - private SubscriberAttributeDefinitionRepository $attrDefRepository; + private SubscriberAttributeDefinitionRepository $definitionRepository; public function __construct( SubscriberManager $subscriberManager, SubscriberAttributeManager $attributeManager, SubscriberRepository $subscriberRepository, - SubscriberAttributeDefinitionRepository $attributeDefinitionRepository + SubscriberAttributeDefinitionRepository $definitionRepository ) { $this->subscriberManager = $subscriberManager; $this->attributeManager = $attributeManager; $this->subscriberRepository = $subscriberRepository; - $this->attrDefRepository = $attributeDefinitionRepository; + $this->definitionRepository = $definitionRepository; } /** * Import subscribers from a CSV file. * * @param UploadedFile $file The uploaded CSV file - * @param bool $updateExisting Whether to update existing subscribers + * @param SubscriberImportOptions $options * @return array Import statistics */ - public function importFromCsv(UploadedFile $file, bool $updateExisting = false): array + public function importFromCsv(UploadedFile $file, SubscriberImportOptions $options): array { $stats = [ 'created' => 0, @@ -61,7 +58,14 @@ public function importFromCsv(UploadedFile $file, bool $updateExisting = false): $data = fgetcsv($handle); while ($data !== false) { try { - $this->processRow($data, $headers, $attributeDefinitions, $updateExisting, $stats, $lineNumber); + $this->processRow( + data: $data, + headers: $headers, + attributeDefinitions: $attributeDefinitions, + options: $options, + stats: $stats, + lineNumber: $lineNumber + ); } catch (Exception $e) { $stats['errors'][] = 'Line ' . $lineNumber . ': ' . $e->getMessage(); $stats['skipped']++; @@ -83,7 +87,7 @@ public function importFromCsv(UploadedFile $file, bool $updateExisting = false): */ public function importAndUpdateFromCsv(UploadedFile $file): array { - return $this->importFromCsv($file, true); + return $this->importFromCsv($file, new SubscriberImportOptions(updateExisting: true)); } /** @@ -94,7 +98,7 @@ public function importAndUpdateFromCsv(UploadedFile $file): array */ public function importNewFromCsv(UploadedFile $file): array { - return $this->importFromCsv($file, false); + return $this->importFromCsv($file, new SubscriberImportOptions()); } /** @@ -143,7 +147,7 @@ private function getAttributeDefinitions(array $headers): array continue; } - $attributeDefinition = $this->attrDefRepository->findOneBy(['name' => $header]); + $attributeDefinition = $this->definitionRepository->findOneBy(['name' => $header]); if ($attributeDefinition) { $attributeDefinitions[$index] = $attributeDefinition; } @@ -158,7 +162,7 @@ private function getAttributeDefinitions(array $headers): array * @param array $data Row data * @param array $headers CSV headers * @param array $attributeDefinitions Attribute definitions - * @param bool $updateExisting Whether to update existing subscribers + * @param SubscriberImportOptions $options * @param array $stats Statistics to update * @param int $lineNumber Current line number for error reporting */ @@ -166,7 +170,7 @@ private function processRow( array $data, array $headers, array $attributeDefinitions, - bool $updateExisting, + SubscriberImportOptions $options, array &$stats, int $lineNumber ): void { @@ -179,7 +183,7 @@ private function processRow( $existingSubscriber = $this->subscriberRepository->findOneByEmail($email); - if ($existingSubscriber && !$updateExisting) { + if ($existingSubscriber && !$options->updateExisting) { $stats['skipped']++; return; } @@ -265,7 +269,11 @@ private function updateExistingSubscriber( $disabled = $this->isBooleanTrue($data, $disabledIndex, $existingSubscriber->isDisabled()); $extraDataIndex = array_search('extra_data', $headers, true); - $additionalData = $extraDataIndex !== false && isset($data[$extraDataIndex]) ? $data[$extraDataIndex] : $existingSubscriber->getExtraData(); + if ($extraDataIndex !== false && isset($data[$extraDataIndex])) { + $additionalData = $data[$extraDataIndex]; + } else { + $additionalData = $existingSubscriber->getExtraData(); + } $dto = new UpdateSubscriberDto( $existingSubscriber->getId(), @@ -376,143 +384,4 @@ private function processAttributes(Subscriber $subscriber, array $data, array $a } } } - - /** - * Export subscribers to a CSV file. - * - * @param SubscriberFilter|null $filter Optional filter to apply - * @param int $batchSize Number of subscribers to process in each batch - * @return Response A streamed response with the CSV file - */ - public function exportToCsv(?SubscriberFilter $filter = null, int $batchSize = 1000): Response - { - if ($filter === null) { - $filter = new SubscriberFilter(); - } - - $response = new StreamedResponse(function () use ($filter, $batchSize) { - $this->generateCsvContent($filter, $batchSize); - }); - - return $this->configureResponse($response); - } - - /** - * Generate CSV content for the export. - * - * @param SubscriberFilter $filter Filter to apply - * @param int $batchSize Batch size for processing - */ - private function generateCsvContent(SubscriberFilter $filter, int $batchSize): void - { - $handle = fopen('php://output', 'w'); - $attributeDefinitions = $this->attrDefRepository->findAll(); - - $headers = $this->getExportHeaders($attributeDefinitions); - fputcsv($handle, $headers); - - $this->exportSubscribers($handle, $filter, $batchSize, $attributeDefinitions); - - fclose($handle); - } - - /** - * Get headers for the export CSV. - * - * @param array $attributeDefinitions Attribute definitions - * @return array Headers - */ - private function getExportHeaders(array $attributeDefinitions): array - { - $headers = [ - 'email', - 'confirmed', - 'blacklisted', - 'html_email', - 'disabled', - 'extra_data', - ]; - - foreach ($attributeDefinitions as $definition) { - $headers[] = $definition->getName(); - } - - return $headers; - } - - /** - * Export subscribers in batches. - * - * @param resource $handle File handle - * @param SubscriberFilter $filter Filter to apply - * @param int $batchSize Batch size - * @param array $attributeDefinitions Attribute definitions - */ - private function exportSubscribers($handle, SubscriberFilter $filter, int $batchSize, array $attributeDefinitions): void - { - $lastId = 0; - - do { - $subscribers = $this->subscriberRepository->getFilteredAfterId( - lastId: $lastId, - limit: $batchSize, - filter: $filter - ); - - foreach ($subscribers as $subscriber) { - $row = $this->getSubscriberRow($subscriber, $attributeDefinitions); - fputcsv($handle, $row); - $lastId = $subscriber->getId(); - } - - $subscriberCount = count($subscribers); - } while ($subscriberCount === $batchSize); - } - - /** - * Get a row of data for a subscriber. - * - * @param Subscriber $subscriber The subscriber - * @param array $attributeDefinitions Attribute definitions - * @return array Row data - */ - private function getSubscriberRow(Subscriber $subscriber, array $attributeDefinitions): array - { - $row = [ - $subscriber->getEmail(), - $subscriber->isConfirmed() ? '1' : '0', - $subscriber->isBlacklisted() ? '1' : '0', - $subscriber->hasHtmlEmail() ? '1' : '0', - $subscriber->isDisabled() ? '1' : '0', - $subscriber->getExtraData(), - ]; - - foreach ($attributeDefinitions as $definition) { - $attributeValue = $this->attributeManager->getSubscriberAttribute( - subscriberId: $subscriber->getId(), - attributeDefinitionId: $definition->getId() - ); - $row[] = $attributeValue ? $attributeValue->getValue() : ''; - } - - return $row; - } - - /** - * Configure the response for CSV download. - * - * @param StreamedResponse $response The response - * @return Response The configured response - */ - private function configureResponse(StreamedResponse $response): Response - { - $response->headers->set('Content-Type', 'text/csv; charset=utf-8'); - $disposition = $response->headers->makeDisposition( - ResponseHeaderBag::DISPOSITION_ATTACHMENT, - 'subscribers_export_' . date('Y-m-d') . '.csv' - ); - $response->headers->set('Content-Disposition', $disposition); - - return $response; - } } diff --git a/tests/Integration/Domain/Subscription/Service/SubscriberCsvExportManagerTest.php b/tests/Integration/Domain/Subscription/Service/SubscriberCsvExportManagerTest.php new file mode 100644 index 00000000..ac9e3afb --- /dev/null +++ b/tests/Integration/Domain/Subscription/Service/SubscriberCsvExportManagerTest.php @@ -0,0 +1,78 @@ +loadSchema(); + + $this->subscriberCsvExportManager = self::getContainer()->get(SubscriberCsvExportManager::class); + $this->subscriberRepository = self::getContainer()->get(SubscriberRepository::class); + } + + public function testExportToCsvReturnsStreamedResponse(): void + { + $subscriber1 = new Subscriber(); + $subscriber1->setEmail('test1@example.com'); + $subscriber1->setConfirmed(true); + $subscriber1->setHtmlEmail(true); + $subscriber1->setBlacklisted(false); + $subscriber1->setDisabled(false); + $subscriber1->setExtraData('Data 1'); + $this->entityManager->persist($subscriber1); + + $subscriber2 = new Subscriber(); + $subscriber2->setEmail('test2@example.com'); + $subscriber2->setConfirmed(false); + $subscriber2->setHtmlEmail(false); + $subscriber2->setBlacklisted(true); + $subscriber2->setDisabled(true); + $subscriber2->setExtraData('Data 2'); + $this->entityManager->persist($subscriber2); + + $this->entityManager->flush(); + + $savedSubscribers = $this->subscriberRepository->findAll(); + self::assertCount(2, $savedSubscribers); + + $filter = new SubscriberFilter(); + + $response = $this->subscriberCsvExportManager->exportToCsv($filter); + + self::assertInstanceOf(Response::class, $response); + self::assertSame('text/csv; charset=utf-8', $response->headers->get('Content-Type')); + self::assertStringContainsString( + 'attachment; filename=subscribers_export_', + $response->headers->get('Content-Disposition') + ); + + ob_start(); + $response->sendContent(); + $content = ob_get_clean(); + + self::assertStringContainsString('email,confirmed,blacklisted,html_email,disabled,extra_data', $content); + } +} diff --git a/tests/Integration/Domain/Subscription/Service/SubscriberCsvManagerTest.php b/tests/Integration/Domain/Subscription/Service/SubscriberCsvImportManagerTest.php similarity index 67% rename from tests/Integration/Domain/Subscription/Service/SubscriberCsvManagerTest.php rename to tests/Integration/Domain/Subscription/Service/SubscriberCsvImportManagerTest.php index d03a9ead..4dcf68f8 100644 --- a/tests/Integration/Domain/Subscription/Service/SubscriberCsvManagerTest.php +++ b/tests/Integration/Domain/Subscription/Service/SubscriberCsvImportManagerTest.php @@ -4,26 +4,25 @@ namespace PhpList\Core\Tests\Integration\Domain\Subscription\Service; -use PhpList\Core\Domain\Subscription\Model\Filter\SubscriberFilter; +use PhpList\Core\Domain\Subscription\Model\Dto\SubscriberImportOptions; use PhpList\Core\Domain\Subscription\Model\Subscriber; use PhpList\Core\Domain\Subscription\Model\SubscriberAttributeDefinition; use PhpList\Core\Domain\Subscription\Repository\SubscriberRepository; -use PhpList\Core\Domain\Subscription\Service\SubscriberCsvManager; +use PhpList\Core\Domain\Subscription\Service\SubscriberCsvImportManager; use PhpList\Core\TestingSupport\Traits\DatabaseTestTrait; use PhpList\Core\TestingSupport\Traits\SimilarDatesAssertionTrait; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Component\HttpFoundation\File\UploadedFile; -use Symfony\Component\HttpFoundation\Response; /** - * Functional test for the SubscriberCsvManager. + * Functional test for the SubscriberCsvImportManager. */ -class SubscriberCsvManagerTest extends KernelTestCase +class SubscriberCsvImportManagerTest extends KernelTestCase { use DatabaseTestTrait; use SimilarDatesAssertionTrait; - private ?SubscriberCsvManager $subscriberCsvManager = null; + private ?SubscriberCsvImportManager $subscriberCsvImportManager = null; private ?SubscriberRepository $subscriberRepository = null; protected function setUp(): void @@ -31,7 +30,7 @@ protected function setUp(): void parent::setUp(); $this->loadSchema(); - $this->subscriberCsvManager = self::getContainer()->get(SubscriberCsvManager::class); + $this->subscriberCsvImportManager = self::getContainer()->get(SubscriberCsvImportManager::class); $this->subscriberRepository = self::getContainer()->get(SubscriberRepository::class); } @@ -59,7 +58,8 @@ public function testImportFromCsvCreatesNewSubscribers(): void $subscriberCountBefore = count($this->subscriberRepository->findAll()); - $result = $this->subscriberCsvManager->importFromCsv($uploadedFile); + $options = new SubscriberImportOptions(); + $result = $this->subscriberCsvImportManager->importFromCsv($uploadedFile, $options); $subscriberCountAfter = count($this->subscriberRepository->findAll()); @@ -114,7 +114,8 @@ public function testImportFromCsvUpdatesExistingSubscribers(): void true ); - $result = $this->subscriberCsvManager->importFromCsv($uploadedFile, true); + $options = new SubscriberImportOptions(updateExisting: true); + $result = $this->subscriberCsvImportManager->importFromCsv($uploadedFile, $options); self::assertSame(0, $result['created']); self::assertSame(1, $result['updated']); @@ -131,49 +132,4 @@ public function testImportFromCsvUpdatesExistingSubscribers(): void unlink($tempFile); } - - public function testExportToCsvReturnsStreamedResponse(): void - { - $subscriber1 = new Subscriber(); - $subscriber1->setEmail('test1@example.com'); - $subscriber1->setConfirmed(true); - $subscriber1->setHtmlEmail(true); - $subscriber1->setBlacklisted(false); - $subscriber1->setDisabled(false); - $subscriber1->setExtraData('Data 1'); - $this->entityManager->persist($subscriber1); - - $subscriber2 = new Subscriber(); - $subscriber2->setEmail('test2@example.com'); - $subscriber2->setConfirmed(false); - $subscriber2->setHtmlEmail(false); - $subscriber2->setBlacklisted(true); - $subscriber2->setDisabled(true); - $subscriber2->setExtraData('Data 2'); - $this->entityManager->persist($subscriber2); - - $this->entityManager->flush(); - - $savedSubscribers = $this->subscriberRepository->findAll(); - self::assertCount(2, $savedSubscribers); - - $filter = new SubscriberFilter(); - - $response = $this->subscriberCsvManager->exportToCsv($filter); - - self::assertInstanceOf(Response::class, $response); - self::assertSame('text/csv; charset=utf-8', $response->headers->get('Content-Type')); - self::assertStringContainsString( - 'attachment; filename=subscribers_export_', - $response->headers->get('Content-Disposition') - ); - - ob_start(); - $response->sendContent(); - $content = ob_get_clean(); - - echo 'CSV Content: ' . $content . "\n"; - - self::assertStringContainsString('email,confirmed,blacklisted,html_email,disabled,extra_data', $content); - } } diff --git a/tests/Unit/Domain/Subscription/Service/SubscriberCsvManagerTest.php b/tests/Unit/Domain/Subscription/Service/SubscriberCsvExportManagerTest.php similarity index 54% rename from tests/Unit/Domain/Subscription/Service/SubscriberCsvManagerTest.php rename to tests/Unit/Domain/Subscription/Service/SubscriberCsvExportManagerTest.php index a0c8b881..edbb5f63 100644 --- a/tests/Unit/Domain/Subscription/Service/SubscriberCsvManagerTest.php +++ b/tests/Unit/Domain/Subscription/Service/SubscriberCsvExportManagerTest.php @@ -4,7 +4,6 @@ namespace PhpList\Core\Tests\Unit\Domain\Subscription\Service; -use PhpList\Core\Domain\Subscription\Model\Dto\UpdateSubscriberDto; use PhpList\Core\Domain\Subscription\Model\Filter\SubscriberFilter; use PhpList\Core\Domain\Subscription\Model\Subscriber; use PhpList\Core\Domain\Subscription\Model\SubscriberAttributeDefinition; @@ -12,140 +11,31 @@ use PhpList\Core\Domain\Subscription\Repository\SubscriberAttributeDefinitionRepository; use PhpList\Core\Domain\Subscription\Repository\SubscriberRepository; use PhpList\Core\Domain\Subscription\Service\SubscriberAttributeManager; -use PhpList\Core\Domain\Subscription\Service\SubscriberCsvManager; -use PhpList\Core\Domain\Subscription\Service\SubscriberManager; +use PhpList\Core\Domain\Subscription\Service\SubscriberCsvExportManager; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; -use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\Response; -class SubscriberCsvManagerTest extends TestCase +class SubscriberCsvExportManagerTest extends TestCase { - private SubscriberManager&MockObject $subscriberManagerMock; private SubscriberAttributeManager&MockObject $attributeManagerMock; private SubscriberRepository&MockObject $subscriberRepositoryMock; private SubscriberAttributeDefinitionRepository&MockObject $attributeDefinitionRepositoryMock; - private SubscriberCsvManager $subject; + private SubscriberCsvExportManager $subject; protected function setUp(): void { - $this->subscriberManagerMock = $this->createMock(SubscriberManager::class); $this->attributeManagerMock = $this->createMock(SubscriberAttributeManager::class); $this->subscriberRepositoryMock = $this->createMock(SubscriberRepository::class); $this->attributeDefinitionRepositoryMock = $this->createMock(SubscriberAttributeDefinitionRepository::class); - $this->subject = new SubscriberCsvManager( - subscriberManager: $this->subscriberManagerMock, - attributeManager: $this->attributeManagerMock, - subscriberRepository: $this->subscriberRepositoryMock, - attributeDefinitionRepository: $this->attributeDefinitionRepositoryMock + $this->subject = new SubscriberCsvExportManager( + $this->attributeManagerMock, + $this->subscriberRepositoryMock, + $this->attributeDefinitionRepositoryMock ); } - public function testImportFromCsvCreatesNewSubscribers(): void - { - $csvContent = "email,confirmed,html_email,blacklisted,disabled,extra_data,first_name\n"; - $csvContent .= "test@example.com,1,1,0,0,\"Some extra data\",John\n"; - $csvContent .= "another@example.com,0,0,1,1,\"More data\",Jane\n"; - - $tempFile = tempnam(sys_get_temp_dir(), 'csv_test'); - file_put_contents($tempFile, $csvContent); - - $uploadedFile = $this->createMock(UploadedFile::class); - $uploadedFile->method('getPathname')->willReturn($tempFile); - - $attributeDefinition = $this->createMock(SubscriberAttributeDefinition::class); - $attributeDefinition->method('getName')->willReturn('first_name'); - $attributeDefinition->method('getId')->willReturn(1); - - $this->attributeDefinitionRepositoryMock - ->method('findOneBy') - ->with(['name' => 'first_name']) - ->willReturn($attributeDefinition); - - $subscriber1 = $this->createMock(Subscriber::class); - $subscriber1->method('getId')->willReturn(1); - - $subscriber2 = $this->createMock(Subscriber::class); - $subscriber2->method('getId')->willReturn(2); - - $this->subscriberRepositoryMock - ->method('findOneByEmail') - ->willReturn(null); - - $this->subscriberManagerMock - ->expects($this->exactly(2)) - ->method('createSubscriber') - ->willReturnOnConsecutiveCalls($subscriber1, $subscriber2); - - $this->attributeManagerMock - ->expects($this->exactly(2)) - ->method('createOrUpdate') - ->withConsecutive( - [$subscriber1, $attributeDefinition, 'John'], - [$subscriber2, $attributeDefinition, 'Jane'] - ); - - $result = $this->subject->importFromCsv($uploadedFile); - - $this->assertSame(2, $result['created']); - $this->assertSame(0, $result['updated']); - $this->assertSame(0, $result['skipped']); - $this->assertEmpty($result['errors']); - - unlink($tempFile); - } - - public function testImportFromCsvUpdatesExistingSubscribers(): void - { - $csvContent = "email,confirmed,html_email,blacklisted,disabled,extra_data\n"; - $csvContent .= "existing@example.com,1,1,0,0,\"Updated data\"\n"; - - $tempFile = tempnam(sys_get_temp_dir(), 'csv_test'); - file_put_contents($tempFile, $csvContent); - - $uploadedFile = $this->createMock(UploadedFile::class); - $uploadedFile->method('getPathname')->willReturn($tempFile); - - $existingSubscriber = $this->createMock(Subscriber::class); - $existingSubscriber->method('getId')->willReturn(1); - $existingSubscriber->method('isConfirmed')->willReturn(false); - $existingSubscriber->method('hasHtmlEmail')->willReturn(false); - $existingSubscriber->method('isBlacklisted')->willReturn(true); - $existingSubscriber->method('isDisabled')->willReturn(true); - $existingSubscriber->method('getExtraData')->willReturn('Old data'); - - $this->subscriberRepositoryMock - ->method('findOneByEmail') - ->with('existing@example.com') - ->willReturn($existingSubscriber); - - $updatedSubscriber = $this->createMock(Subscriber::class); - - $this->subscriberManagerMock - ->expects($this->once()) - ->method('updateSubscriber') - ->with($this->callback(function (UpdateSubscriberDto $dto) { - return $dto->subscriberId === 1 - && $dto->email === 'existing@example.com' - && $dto->confirmed === true - && $dto->htmlEmail === true - && $dto->blacklisted === false - && $dto->disabled === false - && $dto->additionalData === 'Updated data'; - })) - ->willReturn($updatedSubscriber); - - $result = $this->subject->importFromCsv($uploadedFile, true); - - $this->assertSame(0, $result['created']); - $this->assertSame(1, $result['updated']); - $this->assertSame(0, $result['skipped']); - $this->assertEmpty($result['errors']); - - unlink($tempFile); - } - public function testExportToCsvWithFilterReturnsStreamedResponse(): void { $subscriber1 = $this->createMock(Subscriber::class); diff --git a/tests/Unit/Domain/Subscription/Service/SubscriberCsvImportManagerTest.php b/tests/Unit/Domain/Subscription/Service/SubscriberCsvImportManagerTest.php new file mode 100644 index 00000000..c5231d7d --- /dev/null +++ b/tests/Unit/Domain/Subscription/Service/SubscriberCsvImportManagerTest.php @@ -0,0 +1,148 @@ +subscriberManagerMock = $this->createMock(SubscriberManager::class); + $this->attributeManagerMock = $this->createMock(SubscriberAttributeManager::class); + $this->subscriberRepositoryMock = $this->createMock(SubscriberRepository::class); + $this->attributeDefinitionRepositoryMock = $this->createMock(SubscriberAttributeDefinitionRepository::class); + + $this->subject = new SubscriberCsvImportManager( + $this->subscriberManagerMock, + $this->attributeManagerMock, + $this->subscriberRepositoryMock, + $this->attributeDefinitionRepositoryMock + ); + } + + public function testImportFromCsvCreatesNewSubscribers(): void + { + $csvContent = "email,confirmed,html_email,blacklisted,disabled,extra_data,first_name\n"; + $csvContent .= "test@example.com,1,1,0,0,\"Some extra data\",John\n"; + $csvContent .= "another@example.com,0,0,1,1,\"More data\",Jane\n"; + + $tempFile = tempnam(sys_get_temp_dir(), 'csv_test'); + file_put_contents($tempFile, $csvContent); + + $uploadedFile = $this->createMock(UploadedFile::class); + $uploadedFile->method('getPathname')->willReturn($tempFile); + + $attributeDefinition = $this->createMock(SubscriberAttributeDefinition::class); + $attributeDefinition->method('getName')->willReturn('first_name'); + $attributeDefinition->method('getId')->willReturn(1); + + $this->attributeDefinitionRepositoryMock + ->method('findOneBy') + ->with(['name' => 'first_name']) + ->willReturn($attributeDefinition); + + $subscriber1 = $this->createMock(Subscriber::class); + $subscriber1->method('getId')->willReturn(1); + + $subscriber2 = $this->createMock(Subscriber::class); + $subscriber2->method('getId')->willReturn(2); + + $this->subscriberRepositoryMock + ->method('findOneByEmail') + ->willReturn(null); + + $this->subscriberManagerMock + ->expects($this->exactly(2)) + ->method('createSubscriber') + ->willReturnOnConsecutiveCalls($subscriber1, $subscriber2); + + $this->attributeManagerMock + ->expects($this->exactly(2)) + ->method('createOrUpdate') + ->withConsecutive( + [$subscriber1, $attributeDefinition, 'John'], + [$subscriber2, $attributeDefinition, 'Jane'] + ); + + $options = new SubscriberImportOptions(); + $result = $this->subject->importFromCsv($uploadedFile, $options); + + $this->assertSame(2, $result['created']); + $this->assertSame(0, $result['updated']); + $this->assertSame(0, $result['skipped']); + $this->assertEmpty($result['errors']); + + unlink($tempFile); + } + + public function testImportFromCsvUpdatesExistingSubscribers(): void + { + $csvContent = "email,confirmed,html_email,blacklisted,disabled,extra_data\n"; + $csvContent .= "existing@example.com,1,1,0,0,\"Updated data\"\n"; + + $tempFile = tempnam(sys_get_temp_dir(), 'csv_test'); + file_put_contents($tempFile, $csvContent); + + $uploadedFile = $this->createMock(UploadedFile::class); + $uploadedFile->method('getPathname')->willReturn($tempFile); + + $existingSubscriber = $this->createMock(Subscriber::class); + $existingSubscriber->method('getId')->willReturn(1); + $existingSubscriber->method('isConfirmed')->willReturn(false); + $existingSubscriber->method('hasHtmlEmail')->willReturn(false); + $existingSubscriber->method('isBlacklisted')->willReturn(true); + $existingSubscriber->method('isDisabled')->willReturn(true); + $existingSubscriber->method('getExtraData')->willReturn('Old data'); + + $this->subscriberRepositoryMock + ->method('findOneByEmail') + ->with('existing@example.com') + ->willReturn($existingSubscriber); + + $updatedSubscriber = $this->createMock(Subscriber::class); + + $this->subscriberManagerMock + ->expects($this->once()) + ->method('updateSubscriber') + ->with($this->callback(function (UpdateSubscriberDto $dto) { + return $dto->subscriberId === 1 + && $dto->email === 'existing@example.com' + && $dto->confirmed === true + && $dto->htmlEmail === true + && $dto->blacklisted === false + && $dto->disabled === false + && $dto->additionalData === 'Updated data'; + })) + ->willReturn($updatedSubscriber); + + $options = new SubscriberImportOptions(updateExisting: true); + $result = $this->subject->importFromCsv($uploadedFile, $options); + + $this->assertSame(0, $result['created']); + $this->assertSame(1, $result['updated']); + $this->assertSame(0, $result['skipped']); + $this->assertEmpty($result['errors']); + + unlink($tempFile); + } +}