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/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/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/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/repositories.yml b/config/repositories.yml deleted file mode 100644 index 2373b0dc..00000000 --- a/config/repositories.yml +++ /dev/null @@ -1,42 +0,0 @@ -services: - PhpList\Core\Domain\Repository\Identity\AdministratorRepository: - parent: PhpList\Core\Domain\Repository - arguments: - - PhpList\Core\Domain\Model\Identity\Administrator - - Doctrine\ORM\Mapping\ClassMetadata\ClassMetadata - - PhpList\Core\Security\HashGenerator - - PhpList\Core\Domain\Repository\Identity\AdministratorTokenRepository: - parent: PhpList\Core\Domain\Repository - arguments: - - PhpList\Core\Domain\Model\Identity\AdministratorToken - - PhpList\Core\Domain\Repository\Subscription\SubscriberListRepository: - parent: PhpList\Core\Domain\Repository - arguments: - - PhpList\Core\Domain\Model\Subscription\SubscriberList - - PhpList\Core\Domain\Repository\Subscription\SubscriberRepository: - parent: PhpList\Core\Domain\Repository - arguments: - - PhpList\Core\Domain\Model\Subscription\Subscriber - - PhpList\Core\Domain\Repository\Subscription\SubscriptionRepository: - parent: PhpList\Core\Domain\Repository - arguments: - - PhpList\Core\Domain\Model\Subscription\Subscription - - PhpList\Core\Domain\Repository\Messaging\MessageRepository: - parent: PhpList\Core\Domain\Repository - arguments: - - PhpList\Core\Domain\Model\Messaging\Message - - PhpList\Core\Domain\Repository\Messaging\TemplateRepository: - parent: PhpList\Core\Domain\Repository - arguments: - - PhpList\Core\Domain\Model\Messaging\Template - - PhpList\Core\Domain\Repository\Messaging\TemplateImageRepository: - parent: PhpList\Core\Domain\Repository - arguments: - - PhpList\Core\Domain\Model\Messaging\TemplateImage diff --git a/config/services.yml b/config/services.yml index d7982241..b83adce3 100644 --- a/config/services.yml +++ b/config/services.yml @@ -1,14 +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 @@ -21,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 new file mode 100644 index 00000000..c18961d6 --- /dev/null +++ b/config/services/builders.yml @@ -0,0 +1,25 @@ +services: + _defaults: + autowire: true + autoconfigure: true + public: false + + PhpList\Core\Domain\Messaging\Service\Builder\MessageBuilder: + autowire: true + autoconfigure: true + + PhpList\Core\Domain\Messaging\Service\Builder\MessageFormatBuilder: + autowire: true + autoconfigure: true + + PhpList\Core\Domain\Messaging\Service\Builder\MessageScheduleBuilder: + autowire: true + autoconfigure: true + + PhpList\Core\Domain\Messaging\Service\Builder\MessageContentBuilder: + autowire: true + autoconfigure: true + + PhpListPhpList\Core\Domain\Messaging\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..e038a06b --- /dev/null +++ b/config/services/managers.yml @@ -0,0 +1,63 @@ +services: + _defaults: + autowire: true + autoconfigure: true + public: false + + PhpList\Core\Domain\Subscription\Service\SubscriberManager: + autowire: true + autoconfigure: true + + PhpList\Core\Domain\Identity\Service\SessionManager: + autowire: true + autoconfigure: true + + PhpList\Core\Domain\Subscription\Service\SubscriberListManager: + autowire: true + autoconfigure: true + + PhpList\Core\Domain\Subscription\Service\SubscriptionManager: + autowire: true + autoconfigure: true + + PhpList\Core\Domain\Messaging\Service\MessageManager: + autowire: true + autoconfigure: true + + PhpList\Core\Domain\Messaging\Service\TemplateManager: + autowire: true + autoconfigure: true + + PhpList\Core\Domain\Messaging\Service\TemplateImageManager: + autowire: true + autoconfigure: true + + PhpList\Core\Domain\Identity\Service\AdministratorManager: + 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 + + PhpList\Core\Domain\Subscription\Service\SubscriberAttributeManager: + autowire: true + autoconfigure: true + + 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/config/services/repositories.yml b/config/services/repositories.yml new file mode 100644 index 00000000..21ce7114 --- /dev/null +++ b/config/services/repositories.yml @@ -0,0 +1,62 @@ +services: + PhpList\Core\Domain\Identity\Repository\AdministratorRepository: + parent: PhpList\Core\Domain\Common\Repository\AbstractRepository + arguments: + - PhpList\Core\Domain\Identity\Model\Administrator + - 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: + - PhpList\Core\Domain\Identity\Model\AdministratorToken + + PhpList\Core\Domain\Subscription\Repository\SubscriberListRepository: + parent: PhpList\Core\Domain\Common\Repository\AbstractRepository + arguments: + - PhpList\Core\Domain\Subscription\Model\SubscriberList + + PhpList\Core\Domain\Subscription\Repository\SubscriberRepository: + parent: PhpList\Core\Domain\Common\Repository\AbstractRepository + arguments: + - PhpList\Core\Domain\Subscription\Model\Subscriber + + PhpList\Core\Domain\Subscription\Repository\SubscriberAttributeValueRepository: + parent: PhpList\Core\Domain\Common\Repository\AbstractRepository + arguments: + - PhpList\Core\Domain\Subscription\Model\SubscriberAttributeValue + + PhpList\Core\Domain\Subscription\Repository\SubscriberAttributeDefinitionRepository: + parent: PhpList\Core\Domain\Common\Repository\AbstractRepository + arguments: + - PhpList\Core\Domain\Subscription\Model\SubscriberAttributeDefinition + + PhpList\Core\Domain\Subscription\Repository\SubscriptionRepository: + parent: PhpList\Core\Domain\Common\Repository\AbstractRepository + arguments: + - PhpList\Core\Domain\Subscription\Model\Subscription + + PhpList\Core\Domain\Messaging\Repository\MessageRepository: + parent: PhpList\Core\Domain\Common\Repository\AbstractRepository + arguments: + - PhpList\Core\Domain\Messaging\Model\Message + + PhpList\Core\Domain\Messaging\Repository\TemplateRepository: + parent: PhpList\Core\Domain\Common\Repository\AbstractRepository + arguments: + - PhpList\Core\Domain\Messaging\Model\Template + + PhpList\Core\Domain\Messaging\Repository\TemplateImageRepository: + parent: PhpList\Core\Domain\Common\Repository\AbstractRepository + arguments: + - PhpList\Core\Domain\Messaging\Model\TemplateImage diff --git a/config/services/validators.yml b/config/services/validators.yml new file mode 100644 index 00000000..3d15e4a5 --- /dev/null +++ b/config/services/validators.yml @@ -0,0 +1,8 @@ +services: + PhpList\Core\Domain\Messaging\Validator\TemplateLinkValidator: + autowire: true + autoconfigure: true + + PhpList\Core\Domain\Messaging\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/ConfigProvider.php b/src/Core/ConfigProvider.php new file mode 100644 index 00000000..b78f365f --- /dev/null +++ b/src/Core/ConfigProvider.php @@ -0,0 +1,22 @@ +config[$key] ?? $default; + } + + public function all(): array + { + return $this->config; + } +} 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 @@ +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/Repository/AbstractRepository.php b/src/Domain/Common/Repository/AbstractRepository.php similarity index 89% rename from src/Domain/Repository/AbstractRepository.php rename to src/Domain/Common/Repository/AbstractRepository.php index 18328e49..284ef544 100644 --- a/src/Domain/Repository/AbstractRepository.php +++ b/src/Domain/Common/Repository/AbstractRepository.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace PhpList\Core\Domain\Repository; +namespace PhpList\Core\Domain\Common\Repository; use Doctrine\ORM\EntityRepository; -use PhpList\Core\Domain\Model\Interfaces\DomainModel; +use PhpList\Core\Domain\Common\Model\Interfaces\DomainModel; /** * Base class for repositories. @@ -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/src/Domain/Repository/CursorPaginationTrait.php b/src/Domain/Common/Repository/CursorPaginationTrait.php similarity index 84% rename from src/Domain/Repository/CursorPaginationTrait.php rename to src/Domain/Common/Repository/CursorPaginationTrait.php index 573435a1..8be64ee2 100644 --- a/src/Domain/Repository/CursorPaginationTrait.php +++ b/src/Domain/Common/Repository/CursorPaginationTrait.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace PhpList\Core\Domain\Repository; +namespace PhpList\Core\Domain\Common\Repository; -use PhpList\Core\Domain\Filter\FilterRequestInterface; -use PhpList\Core\Domain\Model\Interfaces\DomainModel; +use PhpList\Core\Domain\Common\Model\Filter\FilterRequestInterface; +use PhpList\Core\Domain\Common\Model\Interfaces\DomainModel; use RuntimeException; trait CursorPaginationTrait diff --git a/src/Domain/Repository/Interfaces/PaginatableRepositoryInterface.php b/src/Domain/Common/Repository/Interfaces/PaginatableRepositoryInterface.php similarity index 66% rename from src/Domain/Repository/Interfaces/PaginatableRepositoryInterface.php rename to src/Domain/Common/Repository/Interfaces/PaginatableRepositoryInterface.php index e32706f8..7bacc855 100644 --- a/src/Domain/Repository/Interfaces/PaginatableRepositoryInterface.php +++ b/src/Domain/Common/Repository/Interfaces/PaginatableRepositoryInterface.php @@ -2,9 +2,9 @@ namespace PhpList\Core\Domain\Repository\Interfaces; -namespace PhpList\Core\Domain\Repository\Interfaces; +namespace PhpList\Core\Domain\Common\Repository\Interfaces; -use PhpList\Core\Domain\Filter\FilterRequestInterface; +use PhpList\Core\Domain\Common\Model\Filter\FilterRequestInterface; interface PaginatableRepositoryInterface { diff --git a/src/Domain/Common/Validator/ValidatorInterface.php b/src/Domain/Common/Validator/ValidatorInterface.php new file mode 100644 index 00000000..71855bc4 --- /dev/null +++ b/src/Domain/Common/Validator/ValidatorInterface.php @@ -0,0 +1,12 @@ +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/Model/Identity/AdminAttribute.php b/src/Domain/Identity/Model/AdminAttributeDefinition.php similarity index 85% rename from src/Domain/Model/Identity/AdminAttribute.php rename to src/Domain/Identity/Model/AdminAttributeDefinition.php index be740895..b9a70346 100644 --- a/src/Domain/Model/Identity/AdminAttribute.php +++ b/src/Domain/Identity/Model/AdminAttributeDefinition.php @@ -2,17 +2,17 @@ declare(strict_types=1); -namespace PhpList\Core\Domain\Model\Identity; +namespace PhpList\Core\Domain\Identity\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\Identity\AdminAttributeRepository; +use PhpList\Core\Domain\Common\Model\Interfaces\DomainModel; +use PhpList\Core\Domain\Common\Model\Interfaces\Identity; +use PhpList\Core\Domain\Identity\Repository\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/Identity/Model/AdminAttributeValue.php b/src/Domain/Identity/Model/AdminAttributeValue.php new file mode 100644 index 00000000..3f4e6c68 --- /dev/null +++ b/src/Domain/Identity/Model/AdminAttributeValue.php @@ -0,0 +1,60 @@ +attributeDefinition = $attributeDefinition; + $this->administrator = $administrator; + $this->value = $value; + } + + public function getAttributeDefinition(): AdminAttributeDefinition + { + return $this->attributeDefinition; + } + + public function getAdministrator(): Administrator + { + return $this->administrator; + } + + public function getValue(): ?string + { + return $this->value; + } + + public function setValue(?string $value): self + { + $this->value = $value; + + return $this; + } +} diff --git a/src/Domain/Model/Identity/AdminLogin.php b/src/Domain/Identity/Model/AdminLogin.php similarity index 90% rename from src/Domain/Model/Identity/AdminLogin.php rename to src/Domain/Identity/Model/AdminLogin.php index a5afaebb..b7c30e58 100644 --- a/src/Domain/Model/Identity/AdminLogin.php +++ b/src/Domain/Identity/Model/AdminLogin.php @@ -2,14 +2,14 @@ declare(strict_types=1); -namespace PhpList\Core\Domain\Model\Identity; +namespace PhpList\Core\Domain\Identity\Model; use DateTimeImmutable; 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\Identity\AdminLoginRepository; +use PhpList\Core\Domain\Common\Model\Interfaces\DomainModel; +use PhpList\Core\Domain\Common\Model\Interfaces\Identity; +use PhpList\Core\Domain\Identity\Repository\AdminLoginRepository; #[ORM\Entity(repositoryClass: AdminLoginRepository::class)] #[ORM\Table(name: 'phplist_admin_login')] diff --git a/src/Domain/Model/Identity/AdminPasswordRequest.php b/src/Domain/Identity/Model/AdminPasswordRequest.php similarity index 85% rename from src/Domain/Model/Identity/AdminPasswordRequest.php rename to src/Domain/Identity/Model/AdminPasswordRequest.php index b2ae7bcb..c06a45eb 100644 --- a/src/Domain/Model/Identity/AdminPasswordRequest.php +++ b/src/Domain/Identity/Model/AdminPasswordRequest.php @@ -2,13 +2,13 @@ declare(strict_types=1); -namespace PhpList\Core\Domain\Model\Identity; +namespace PhpList\Core\Domain\Identity\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\Identity\AdminPasswordRequestRepository; +use PhpList\Core\Domain\Common\Model\Interfaces\DomainModel; +use PhpList\Core\Domain\Common\Model\Interfaces\Identity; +use PhpList\Core\Domain\Identity\Repository\AdminPasswordRequestRepository; #[ORM\Entity(repositoryClass: AdminPasswordRequestRepository::class)] #[ORM\Table(name: 'phplist_admin_password_request')] diff --git a/src/Domain/Model/Identity/Administrator.php b/src/Domain/Identity/Model/Administrator.php similarity index 92% rename from src/Domain/Model/Identity/Administrator.php rename to src/Domain/Identity/Model/Administrator.php index 9509269d..d45e6c0e 100644 --- a/src/Domain/Model/Identity/Administrator.php +++ b/src/Domain/Identity/Model/Administrator.php @@ -2,15 +2,15 @@ declare(strict_types=1); -namespace PhpList\Core\Domain\Model\Identity; +namespace PhpList\Core\Domain\Identity\Model; use DateTime; use Doctrine\ORM\Mapping as ORM; -use PhpList\Core\Domain\Model\Interfaces\CreationDate; -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\Repository\Identity\AdministratorRepository; +use PhpList\Core\Domain\Common\Model\Interfaces\CreationDate; +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\Repository\AdministratorRepository; /** * This class represents an administrator who can log to the system, is allowed to administer diff --git a/src/Domain/Model/Identity/AdministratorToken.php b/src/Domain/Identity/Model/AdministratorToken.php similarity index 90% rename from src/Domain/Model/Identity/AdministratorToken.php rename to src/Domain/Identity/Model/AdministratorToken.php index abac6261..41ff75aa 100644 --- a/src/Domain/Model/Identity/AdministratorToken.php +++ b/src/Domain/Identity/Model/AdministratorToken.php @@ -2,17 +2,17 @@ declare(strict_types=1); -namespace PhpList\Core\Domain\Model\Identity; +namespace PhpList\Core\Domain\Identity\Model; use DateTime; use DateTimeZone; use Doctrine\ORM\Mapping as ORM; use Doctrine\Persistence\Proxy; -use PhpList\Core\Domain\Repository\Identity\AdministratorTokenRepository; +use PhpList\Core\Domain\Common\Model\Interfaces\CreationDate; +use PhpList\Core\Domain\Common\Model\Interfaces\DomainModel; +use PhpList\Core\Domain\Common\Model\Interfaces\Identity; +use PhpList\Core\Domain\Identity\Repository\AdministratorTokenRepository; use Symfony\Component\Serializer\Annotation\SerializedName; -use PhpList\Core\Domain\Model\Interfaces\CreationDate; -use PhpList\Core\Domain\Model\Interfaces\DomainModel; -use PhpList\Core\Domain\Model\Interfaces\Identity; /** * This class represents an API authentication token for an administrator. 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/Model/Identity/UserBlacklist.php b/src/Domain/Identity/Model/UserBlacklist.php similarity index 84% rename from src/Domain/Model/Identity/UserBlacklist.php rename to src/Domain/Identity/Model/UserBlacklist.php index b0b60e9d..1c9d0a30 100644 --- a/src/Domain/Model/Identity/UserBlacklist.php +++ b/src/Domain/Identity/Model/UserBlacklist.php @@ -2,12 +2,12 @@ declare(strict_types=1); -namespace PhpList\Core\Domain\Model\Identity; +namespace PhpList\Core\Domain\Identity\Model; use DateTime; use Doctrine\ORM\Mapping as ORM; -use PhpList\Core\Domain\Model\Interfaces\DomainModel; -use PhpList\Core\Domain\Repository\Identity\UserBlacklistRepository; +use PhpList\Core\Domain\Common\Model\Interfaces\DomainModel; +use PhpList\Core\Domain\Identity\Repository\UserBlacklistRepository; #[ORM\Entity(repositoryClass: UserBlacklistRepository::class)] #[ORM\Table(name: 'phplist_user_blacklist')] diff --git a/src/Domain/Model/Identity/UserBlacklistData.php b/src/Domain/Identity/Model/UserBlacklistData.php similarity index 87% rename from src/Domain/Model/Identity/UserBlacklistData.php rename to src/Domain/Identity/Model/UserBlacklistData.php index bfb2e1e9..09697616 100644 --- a/src/Domain/Model/Identity/UserBlacklistData.php +++ b/src/Domain/Identity/Model/UserBlacklistData.php @@ -2,11 +2,11 @@ declare(strict_types=1); -namespace PhpList\Core\Domain\Model\Identity; +namespace PhpList\Core\Domain\Identity\Model; use Doctrine\ORM\Mapping as ORM; -use PhpList\Core\Domain\Model\Interfaces\DomainModel; -use PhpList\Core\Domain\Repository\Identity\UserBlacklistDataRepository; +use PhpList\Core\Domain\Common\Model\Interfaces\DomainModel; +use PhpList\Core\Domain\Identity\Repository\UserBlacklistDataRepository; #[ORM\Entity(repositoryClass: UserBlacklistDataRepository::class)] #[ORM\Table(name: 'phplist_user_blacklist_data')] diff --git a/src/Domain/Identity/Repository/AdminAttributeDefinitionRepository.php b/src/Domain/Identity/Repository/AdminAttributeDefinitionRepository.php new file mode 100644 index 00000000..53df9996 --- /dev/null +++ b/src/Domain/Identity/Repository/AdminAttributeDefinitionRepository.php @@ -0,0 +1,20 @@ +findOneBy(['name' => $name]); + } +} diff --git a/src/Domain/Identity/Repository/AdminAttributeValueRepository.php b/src/Domain/Identity/Repository/AdminAttributeValueRepository.php new file mode 100644 index 00000000..c38b6215 --- /dev/null +++ b/src/Domain/Identity/Repository/AdminAttributeValueRepository.php @@ -0,0 +1,54 @@ +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/Repository/AdminLoginRepository.php b/src/Domain/Identity/Repository/AdminLoginRepository.php new file mode 100644 index 00000000..051f78aa --- /dev/null +++ b/src/Domain/Identity/Repository/AdminLoginRepository.php @@ -0,0 +1,14 @@ +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/src/Domain/Identity/Service/AdministratorManager.php b/src/Domain/Identity/Service/AdministratorManager.php new file mode 100644 index 00000000..41731a14 --- /dev/null +++ b/src/Domain/Identity/Service/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->isSuperUser); + $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/Identity/Service/SessionManager.php b/src/Domain/Identity/Service/SessionManager.php new file mode 100644 index 00000000..966eecff --- /dev/null +++ b/src/Domain/Identity/Service/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/Model/Messaging/Attachment.php b/src/Domain/Messaging/Model/Attachment.php similarity index 91% rename from src/Domain/Model/Messaging/Attachment.php rename to src/Domain/Messaging/Model/Attachment.php index 99e205df..79fd5f95 100644 --- a/src/Domain/Model/Messaging/Attachment.php +++ b/src/Domain/Messaging/Model/Attachment.php @@ -2,12 +2,12 @@ declare(strict_types=1); -namespace PhpList\Core\Domain\Model\Messaging; +namespace PhpList\Core\Domain\Messaging\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\Messaging\AttachmentRepository; +use PhpList\Core\Domain\Common\Model\Interfaces\DomainModel; +use PhpList\Core\Domain\Common\Model\Interfaces\Identity; +use PhpList\Core\Domain\Messaging\Repository\AttachmentRepository; #[ORM\Entity(repositoryClass: AttachmentRepository::class)] #[ORM\Table(name: 'phplist_attachment')] diff --git a/src/Domain/Model/Messaging/Bounce.php b/src/Domain/Messaging/Model/Bounce.php similarity index 90% rename from src/Domain/Model/Messaging/Bounce.php rename to src/Domain/Messaging/Model/Bounce.php index 5611f9b5..8d665d72 100644 --- a/src/Domain/Model/Messaging/Bounce.php +++ b/src/Domain/Messaging/Model/Bounce.php @@ -2,13 +2,13 @@ declare(strict_types=1); -namespace PhpList\Core\Domain\Model\Messaging; +namespace PhpList\Core\Domain\Messaging\Model; -use Doctrine\ORM\Mapping as ORM; use DateTime; -use PhpList\Core\Domain\Model\Interfaces\DomainModel; -use PhpList\Core\Domain\Model\Interfaces\Identity; -use PhpList\Core\Domain\Repository\Messaging\BounceRepository; +use Doctrine\ORM\Mapping as ORM; +use PhpList\Core\Domain\Common\Model\Interfaces\DomainModel; +use PhpList\Core\Domain\Common\Model\Interfaces\Identity; +use PhpList\Core\Domain\Messaging\Repository\BounceRepository; #[ORM\Entity(repositoryClass: BounceRepository::class)] #[ORM\Table(name: 'phplist_bounce')] diff --git a/src/Domain/Model/Messaging/BounceRegex.php b/src/Domain/Messaging/Model/BounceRegex.php similarity index 93% rename from src/Domain/Model/Messaging/BounceRegex.php rename to src/Domain/Messaging/Model/BounceRegex.php index 88da280b..510aaad8 100644 --- a/src/Domain/Model/Messaging/BounceRegex.php +++ b/src/Domain/Messaging/Model/BounceRegex.php @@ -2,12 +2,12 @@ declare(strict_types=1); -namespace PhpList\Core\Domain\Model\Messaging; +namespace PhpList\Core\Domain\Messaging\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\Messaging\BounceRegexRepository; +use PhpList\Core\Domain\Common\Model\Interfaces\DomainModel; +use PhpList\Core\Domain\Common\Model\Interfaces\Identity; +use PhpList\Core\Domain\Messaging\Repository\BounceRegexRepository; #[ORM\Entity(repositoryClass: BounceRegexRepository::class)] #[ORM\Table(name: 'phplist_bounceregex')] diff --git a/src/Domain/Model/Messaging/BounceRegexBounce.php b/src/Domain/Messaging/Model/BounceRegexBounce.php similarity index 84% rename from src/Domain/Model/Messaging/BounceRegexBounce.php rename to src/Domain/Messaging/Model/BounceRegexBounce.php index f36a9f31..9dbd3168 100644 --- a/src/Domain/Model/Messaging/BounceRegexBounce.php +++ b/src/Domain/Messaging/Model/BounceRegexBounce.php @@ -2,11 +2,11 @@ declare(strict_types=1); -namespace PhpList\Core\Domain\Model\Messaging; +namespace PhpList\Core\Domain\Messaging\Model; use Doctrine\ORM\Mapping as ORM; -use PhpList\Core\Domain\Model\Interfaces\DomainModel; -use PhpList\Core\Domain\Repository\Messaging\BounceRegexBounceRepository; +use PhpList\Core\Domain\Common\Model\Interfaces\DomainModel; +use PhpList\Core\Domain\Messaging\Repository\BounceRegexBounceRepository; #[ORM\Entity(repositoryClass: BounceRegexBounceRepository::class)] #[ORM\Table(name: 'phplist_bounceregex_bounce')] diff --git a/src/Domain/Messaging/Model/Dto/CreateMessageDto.php b/src/Domain/Messaging/Model/Dto/CreateMessageDto.php new file mode 100644 index 00000000..c74690f3 --- /dev/null +++ b/src/Domain/Messaging/Model/Dto/CreateMessageDto.php @@ -0,0 +1,54 @@ +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/Messaging/Model/Dto/CreateTemplateDto.php b/src/Domain/Messaging/Model/Dto/CreateTemplateDto.php new file mode 100644 index 00000000..009c475f --- /dev/null +++ b/src/Domain/Messaging/Model/Dto/CreateTemplateDto.php @@ -0,0 +1,22 @@ +user; + } + + public function getExisting(): ?Message + { + return $this->existing; + } +} diff --git a/src/Domain/Messaging/Model/Dto/MessageDtoInterface.php b/src/Domain/Messaging/Model/Dto/MessageDtoInterface.php new file mode 100644 index 00000000..d3349f7d --- /dev/null +++ b/src/Domain/Messaging/Model/Dto/MessageDtoInterface.php @@ -0,0 +1,21 @@ +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/Filter/MessageFilter.php b/src/Domain/Messaging/Model/Filter/MessageFilter.php similarity index 66% rename from src/Domain/Filter/MessageFilter.php rename to src/Domain/Messaging/Model/Filter/MessageFilter.php index a1ec4736..d230a6a1 100644 --- a/src/Domain/Filter/MessageFilter.php +++ b/src/Domain/Messaging/Model/Filter/MessageFilter.php @@ -2,9 +2,10 @@ declare(strict_types=1); -namespace PhpList\Core\Domain\Filter; +namespace PhpList\Core\Domain\Messaging\Model\Filter; -use PhpList\Core\Domain\Model\Identity\Administrator; +use PhpList\Core\Domain\Common\Model\Filter\FilterRequestInterface; +use PhpList\Core\Domain\Identity\Model\Administrator; class MessageFilter implements FilterRequestInterface { diff --git a/src/Domain/Model/Messaging/ListMessage.php b/src/Domain/Messaging/Model/ListMessage.php similarity index 86% rename from src/Domain/Model/Messaging/ListMessage.php rename to src/Domain/Messaging/Model/ListMessage.php index 52a4dff0..9123bd46 100644 --- a/src/Domain/Model/Messaging/ListMessage.php +++ b/src/Domain/Messaging/Model/ListMessage.php @@ -2,15 +2,15 @@ declare(strict_types=1); -namespace PhpList\Core\Domain\Model\Messaging; +namespace PhpList\Core\Domain\Messaging\Model; use DateTime; 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\Model\Interfaces\ModificationDate; -use PhpList\Core\Domain\Repository\Messaging\ListMessageRepository; +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\Messaging\Repository\ListMessageRepository; #[ORM\Entity(repositoryClass: ListMessageRepository::class)] #[ORM\Table(name: 'phplist_listmessage')] diff --git a/src/Domain/Model/Messaging/Message.php b/src/Domain/Messaging/Model/Message.php similarity index 86% rename from src/Domain/Model/Messaging/Message.php rename to src/Domain/Messaging/Model/Message.php index 278b2f6e..4031a1ad 100644 --- a/src/Domain/Model/Messaging/Message.php +++ b/src/Domain/Messaging/Model/Message.php @@ -2,20 +2,20 @@ declare(strict_types=1); -namespace PhpList\Core\Domain\Model\Messaging; +namespace PhpList\Core\Domain\Messaging\Model; use DateTime; use Doctrine\ORM\Mapping as ORM; -use PhpList\Core\Domain\Model\Identity\Administrator; -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\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\Repository\Messaging\MessageRepository; +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\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 PhpList\Core\Domain\Messaging\Repository\MessageRepository; #[ORM\Entity(repositoryClass: MessageRepository::class)] #[ORM\Table(name: 'phplist_message')] diff --git a/src/Domain/Model/Messaging/Message/MessageContent.php b/src/Domain/Messaging/Model/Message/MessageContent.php similarity index 93% rename from src/Domain/Model/Messaging/Message/MessageContent.php rename to src/Domain/Messaging/Model/Message/MessageContent.php index b80580a6..df714be9 100644 --- a/src/Domain/Model/Messaging/Message/MessageContent.php +++ b/src/Domain/Messaging/Model/Message/MessageContent.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace PhpList\Core\Domain\Model\Messaging\Message; +namespace PhpList\Core\Domain\Messaging\Model\Message; use Doctrine\ORM\Mapping as ORM; -use PhpList\Core\Domain\Model\Interfaces\EmbeddableInterface; +use PhpList\Core\Domain\Common\Model\Interfaces\EmbeddableInterface; #[ORM\Embeddable] class MessageContent implements EmbeddableInterface diff --git a/src/Domain/Model/Messaging/Message/MessageFormat.php b/src/Domain/Messaging/Model/Message/MessageFormat.php similarity index 96% rename from src/Domain/Model/Messaging/Message/MessageFormat.php rename to src/Domain/Messaging/Model/Message/MessageFormat.php index de4836e0..00af6df0 100644 --- a/src/Domain/Model/Messaging/Message/MessageFormat.php +++ b/src/Domain/Messaging/Model/Message/MessageFormat.php @@ -2,11 +2,11 @@ declare(strict_types=1); -namespace PhpList\Core\Domain\Model\Messaging\Message; +namespace PhpList\Core\Domain\Messaging\Model\Message; use Doctrine\ORM\Mapping as ORM; use InvalidArgumentException; -use PhpList\Core\Domain\Model\Interfaces\EmbeddableInterface; +use PhpList\Core\Domain\Common\Model\Interfaces\EmbeddableInterface; #[ORM\Embeddable] class MessageFormat implements EmbeddableInterface diff --git a/src/Domain/Model/Messaging/Message/MessageMetadata.php b/src/Domain/Messaging/Model/Message/MessageMetadata.php similarity index 95% rename from src/Domain/Model/Messaging/Message/MessageMetadata.php rename to src/Domain/Messaging/Model/Message/MessageMetadata.php index 3e4452a1..123103ff 100644 --- a/src/Domain/Model/Messaging/Message/MessageMetadata.php +++ b/src/Domain/Messaging/Model/Message/MessageMetadata.php @@ -2,11 +2,11 @@ declare(strict_types=1); -namespace PhpList\Core\Domain\Model\Messaging\Message; +namespace PhpList\Core\Domain\Messaging\Model\Message; use DateTime; use Doctrine\ORM\Mapping as ORM; -use PhpList\Core\Domain\Model\Interfaces\EmbeddableInterface; +use PhpList\Core\Domain\Common\Model\Interfaces\EmbeddableInterface; #[ORM\Embeddable] class MessageMetadata implements EmbeddableInterface diff --git a/src/Domain/Model/Messaging/Message/MessageOptions.php b/src/Domain/Messaging/Model/Message/MessageOptions.php similarity index 94% rename from src/Domain/Model/Messaging/Message/MessageOptions.php rename to src/Domain/Messaging/Model/Message/MessageOptions.php index 002d3c9a..6a00c95e 100644 --- a/src/Domain/Model/Messaging/Message/MessageOptions.php +++ b/src/Domain/Messaging/Model/Message/MessageOptions.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace PhpList\Core\Domain\Model\Messaging\Message; +namespace PhpList\Core\Domain\Messaging\Model\Message; use Doctrine\ORM\Mapping as ORM; -use PhpList\Core\Domain\Model\Interfaces\EmbeddableInterface; +use PhpList\Core\Domain\Common\Model\Interfaces\EmbeddableInterface; #[ORM\Embeddable] class MessageOptions implements EmbeddableInterface diff --git a/src/Domain/Model/Messaging/Message/MessageSchedule.php b/src/Domain/Messaging/Model/Message/MessageSchedule.php similarity index 95% rename from src/Domain/Model/Messaging/Message/MessageSchedule.php rename to src/Domain/Messaging/Model/Message/MessageSchedule.php index 3448f8d1..7c157c1c 100644 --- a/src/Domain/Model/Messaging/Message/MessageSchedule.php +++ b/src/Domain/Messaging/Model/Message/MessageSchedule.php @@ -2,11 +2,11 @@ declare(strict_types=1); -namespace PhpList\Core\Domain\Model\Messaging\Message; +namespace PhpList\Core\Domain\Messaging\Model\Message; use DateTime; use Doctrine\ORM\Mapping as ORM; -use PhpList\Core\Domain\Model\Interfaces\EmbeddableInterface; +use PhpList\Core\Domain\Common\Model\Interfaces\EmbeddableInterface; #[ORM\Embeddable] class MessageSchedule implements EmbeddableInterface diff --git a/src/Domain/Model/Messaging/MessageAttachment.php b/src/Domain/Messaging/Model/MessageAttachment.php similarity index 87% rename from src/Domain/Model/Messaging/MessageAttachment.php rename to src/Domain/Messaging/Model/MessageAttachment.php index 1f65656e..2675790d 100644 --- a/src/Domain/Model/Messaging/MessageAttachment.php +++ b/src/Domain/Messaging/Model/MessageAttachment.php @@ -2,11 +2,11 @@ declare(strict_types=1); -namespace PhpList\Core\Domain\Model\Messaging; +namespace PhpList\Core\Domain\Messaging\Model; use Doctrine\ORM\Mapping as ORM; -use PhpList\Core\Domain\Model\Interfaces\Identity; -use PhpList\Core\Domain\Repository\Messaging\MessageAttachmentRepository; +use PhpList\Core\Domain\Common\Model\Interfaces\Identity; +use PhpList\Core\Domain\Messaging\Repository\MessageAttachmentRepository; #[ORM\Entity(repositoryClass: MessageAttachmentRepository::class)] #[ORM\Table(name: 'phplist_message_attachment')] diff --git a/src/Domain/Model/Messaging/MessageData.php b/src/Domain/Messaging/Model/MessageData.php similarity index 86% rename from src/Domain/Model/Messaging/MessageData.php rename to src/Domain/Messaging/Model/MessageData.php index 686436f4..56744251 100644 --- a/src/Domain/Model/Messaging/MessageData.php +++ b/src/Domain/Messaging/Model/MessageData.php @@ -2,11 +2,11 @@ declare(strict_types=1); -namespace PhpList\Core\Domain\Model\Messaging; +namespace PhpList\Core\Domain\Messaging\Model; use Doctrine\ORM\Mapping as ORM; -use PhpList\Core\Domain\Model\Interfaces\DomainModel; -use PhpList\Core\Domain\Repository\Messaging\MessageDataRepository; +use PhpList\Core\Domain\Common\Model\Interfaces\DomainModel; +use PhpList\Core\Domain\Messaging\Repository\MessageDataRepository; #[ORM\Entity(repositoryClass: MessageDataRepository::class)] #[ORM\Table(name: 'phplist_messagedata')] diff --git a/src/Domain/Model/Messaging/SendProcess.php b/src/Domain/Messaging/Model/SendProcess.php similarity index 87% rename from src/Domain/Model/Messaging/SendProcess.php rename to src/Domain/Messaging/Model/SendProcess.php index 0c86b941..7b2287d4 100644 --- a/src/Domain/Model/Messaging/SendProcess.php +++ b/src/Domain/Messaging/Model/SendProcess.php @@ -2,14 +2,14 @@ declare(strict_types=1); -namespace PhpList\Core\Domain\Model\Messaging; +namespace PhpList\Core\Domain\Messaging\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\Model\Interfaces\ModificationDate; -use PhpList\Core\Domain\Repository\Messaging\SendProcessRepository; +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\Messaging\Repository\SendProcessRepository; #[ORM\Entity(repositoryClass: SendProcessRepository::class)] #[ORM\Table(name: 'phplist_sendprocess')] diff --git a/src/Domain/Model/Messaging/Template.php b/src/Domain/Messaging/Model/Template.php similarity index 91% rename from src/Domain/Model/Messaging/Template.php rename to src/Domain/Messaging/Model/Template.php index 420ba9bc..f7e3f5d0 100644 --- a/src/Domain/Model/Messaging/Template.php +++ b/src/Domain/Messaging/Model/Template.php @@ -2,14 +2,14 @@ declare(strict_types=1); -namespace PhpList\Core\Domain\Model\Messaging; +namespace PhpList\Core\Domain\Messaging\Model; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; 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\Messaging\TemplateRepository; +use PhpList\Core\Domain\Common\Model\Interfaces\DomainModel; +use PhpList\Core\Domain\Common\Model\Interfaces\Identity; +use PhpList\Core\Domain\Messaging\Repository\TemplateRepository; #[ORM\Entity(repositoryClass: TemplateRepository::class)] #[ORM\Table(name: 'phplist_template')] diff --git a/src/Domain/Model/Messaging/TemplateImage.php b/src/Domain/Messaging/Model/TemplateImage.php similarity index 91% rename from src/Domain/Model/Messaging/TemplateImage.php rename to src/Domain/Messaging/Model/TemplateImage.php index 732bba4c..1ef7d7b5 100644 --- a/src/Domain/Model/Messaging/TemplateImage.php +++ b/src/Domain/Messaging/Model/TemplateImage.php @@ -2,12 +2,12 @@ declare(strict_types=1); -namespace PhpList\Core\Domain\Model\Messaging; +namespace PhpList\Core\Domain\Messaging\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\Messaging\TemplateImageRepository; +use PhpList\Core\Domain\Common\Model\Interfaces\DomainModel; +use PhpList\Core\Domain\Common\Model\Interfaces\Identity; +use PhpList\Core\Domain\Messaging\Repository\TemplateImageRepository; #[ORM\Entity(repositoryClass: TemplateImageRepository::class)] #[ORM\Table(name: 'phplist_templateimage')] diff --git a/src/Domain/Model/Messaging/UserMessage.php b/src/Domain/Messaging/Model/UserMessage.php similarity index 90% rename from src/Domain/Model/Messaging/UserMessage.php rename to src/Domain/Messaging/Model/UserMessage.php index fbe606fe..9be52903 100644 --- a/src/Domain/Model/Messaging/UserMessage.php +++ b/src/Domain/Messaging/Model/UserMessage.php @@ -2,13 +2,13 @@ declare(strict_types=1); -namespace PhpList\Core\Domain\Model\Messaging; +namespace PhpList\Core\Domain\Messaging\Model; use DateTime; use Doctrine\ORM\Mapping as ORM; -use PhpList\Core\Domain\Model\Interfaces\DomainModel; -use PhpList\Core\Domain\Model\Subscription\Subscriber; -use PhpList\Core\Domain\Repository\Messaging\UserMessageRepository; +use PhpList\Core\Domain\Common\Model\Interfaces\DomainModel; +use PhpList\Core\Domain\Messaging\Repository\UserMessageRepository; +use PhpList\Core\Domain\Subscription\Model\Subscriber; #[ORM\Entity(repositoryClass: UserMessageRepository::class)] #[ORM\Table(name: 'phplist_usermessage')] diff --git a/src/Domain/Model/Messaging/UserMessageBounce.php b/src/Domain/Messaging/Model/UserMessageBounce.php similarity index 88% rename from src/Domain/Model/Messaging/UserMessageBounce.php rename to src/Domain/Messaging/Model/UserMessageBounce.php index 7d9ce4c4..3469fe1b 100644 --- a/src/Domain/Model/Messaging/UserMessageBounce.php +++ b/src/Domain/Messaging/Model/UserMessageBounce.php @@ -2,13 +2,13 @@ declare(strict_types=1); -namespace PhpList\Core\Domain\Model\Messaging; +namespace PhpList\Core\Domain\Messaging\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\Messaging\UserMessageBounceRepository; +use PhpList\Core\Domain\Common\Model\Interfaces\DomainModel; +use PhpList\Core\Domain\Common\Model\Interfaces\Identity; +use PhpList\Core\Domain\Messaging\Repository\UserMessageBounceRepository; #[ORM\Entity(repositoryClass: UserMessageBounceRepository::class)] #[ORM\Table(name: 'phplist_user_message_bounce')] diff --git a/src/Domain/Model/Messaging/UserMessageForward.php b/src/Domain/Messaging/Model/UserMessageForward.php similarity index 90% rename from src/Domain/Model/Messaging/UserMessageForward.php rename to src/Domain/Messaging/Model/UserMessageForward.php index 1f69da23..6dbbcc15 100644 --- a/src/Domain/Model/Messaging/UserMessageForward.php +++ b/src/Domain/Messaging/Model/UserMessageForward.php @@ -2,13 +2,13 @@ declare(strict_types=1); -namespace PhpList\Core\Domain\Model\Messaging; +namespace PhpList\Core\Domain\Messaging\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\Messaging\UserMessageForwardRepository; +use PhpList\Core\Domain\Common\Model\Interfaces\DomainModel; +use PhpList\Core\Domain\Common\Model\Interfaces\Identity; +use PhpList\Core\Domain\Messaging\Repository\UserMessageForwardRepository; #[ORM\Entity(repositoryClass: UserMessageForwardRepository::class)] #[ORM\Table(name: 'phplist_user_message_forward')] diff --git a/src/Domain/Messaging/Repository/AttachmentRepository.php b/src/Domain/Messaging/Repository/AttachmentRepository.php new file mode 100644 index 00000000..393c8cb1 --- /dev/null +++ b/src/Domain/Messaging/Repository/AttachmentRepository.php @@ -0,0 +1,14 @@ +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/Messaging/Service/Builder/MessageContentBuilder.php b/src/Domain/Messaging/Service/Builder/MessageContentBuilder.php new file mode 100644 index 00000000..1e9e442d --- /dev/null +++ b/src/Domain/Messaging/Service/Builder/MessageContentBuilder.php @@ -0,0 +1,26 @@ +subject, + $dto->text, + $dto->textMessage, + $dto->footer + ); + } +} diff --git a/src/Domain/Messaging/Service/Builder/MessageFormatBuilder.php b/src/Domain/Messaging/Service/Builder/MessageFormatBuilder.php new file mode 100644 index 00000000..7bf9be8b --- /dev/null +++ b/src/Domain/Messaging/Service/Builder/MessageFormatBuilder.php @@ -0,0 +1,25 @@ +htmlFormated, + $dto->sendFormat, + $dto->formatOptions + ); + } +} diff --git a/src/Domain/Messaging/Service/Builder/MessageOptionsBuilder.php b/src/Domain/Messaging/Service/Builder/MessageOptionsBuilder.php new file mode 100644 index 00000000..0a241f0f --- /dev/null +++ b/src/Domain/Messaging/Service/Builder/MessageOptionsBuilder.php @@ -0,0 +1,27 @@ +fromField ?? '', + $dto->toField ?? '', + $dto->replyTo ?? '', + $dto->userSelection, + null, + ); + } +} diff --git a/src/Domain/Messaging/Service/Builder/MessageScheduleBuilder.php b/src/Domain/Messaging/Service/Builder/MessageScheduleBuilder.php new file mode 100644 index 00000000..df847eaf --- /dev/null +++ b/src/Domain/Messaging/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/Messaging/Service/MessageManager.php b/src/Domain/Messaging/Service/MessageManager.php new file mode 100644 index 00000000..9af4df0b --- /dev/null +++ b/src/Domain/Messaging/Service/MessageManager.php @@ -0,0 +1,56 @@ +messageRepository = $messageRepository; + $this->messageBuilder = $messageBuilder; + } + + public function createMessage(MessageDtoInterface $createMessageDto, Administrator $authUser): Message + { + $context = new MessageContext($authUser); + $message = $this->messageBuilder->build($createMessageDto, $context); + $this->messageRepository->save($message); + + return $message; + } + + public function updateMessage( + MessageDtoInterface $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/Messaging/Service/TemplateImageManager.php b/src/Domain/Messaging/Service/TemplateImageManager.php new file mode 100644 index 00000000..c5ebd3f4 --- /dev/null +++ b/src/Domain/Messaging/Service/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/Messaging/Service/TemplateManager.php b/src/Domain/Messaging/Service/TemplateManager.php new file mode 100644 index 00000000..35678484 --- /dev/null +++ b/src/Domain/Messaging/Service/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->shouldCheckLinks) + ->set('checkImages', $createTemplateDto->shouldCheckImages) + ->set('checkExternalImages', $createTemplateDto->shouldCheckExternalImages); + + $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/Messaging/Validator/TemplateImageValidator.php b/src/Domain/Messaging/Validator/TemplateImageValidator.php new file mode 100644 index 00000000..11bcc329 --- /dev/null +++ b/src/Domain/Messaging/Validator/TemplateImageValidator.php @@ -0,0 +1,73 @@ +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/Messaging/Validator/TemplateLinkValidator.php b/src/Domain/Messaging/Validator/TemplateLinkValidator.php new file mode 100644 index 00000000..18c772df --- /dev/null +++ b/src/Domain/Messaging/Validator/TemplateLinkValidator.php @@ -0,0 +1,63 @@ +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/Model/Identity/AdminAttributeRelation.php b/src/Domain/Model/Identity/AdminAttributeRelation.php deleted file mode 100644 index 454e8800..00000000 --- a/src/Domain/Model/Identity/AdminAttributeRelation.php +++ /dev/null @@ -1,55 +0,0 @@ - true])] - private int $adminAttributeId; - - #[ORM\Id] - #[ORM\Column(name: 'adminid', type: 'integer', options: ['unsigned' => true])] - private int $adminId; - - #[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; - $this->value = $value; - } - - public function getAdminAttributeId(): int - { - return $this->adminAttributeId; - } - - public function getAdminId(): int - { - return $this->adminId; - } - - public function getValue(): ?string - { - return $this->value; - } - - public function setValue(?string $value): self - { - $this->value = $value; - - return $this; - } -} diff --git a/src/Domain/Repository/Analytics/LinkTrackForwardRepository.php b/src/Domain/Repository/Analytics/LinkTrackForwardRepository.php deleted file mode 100644 index ff0b3c8d..00000000 --- a/src/Domain/Repository/Analytics/LinkTrackForwardRepository.php +++ /dev/null @@ -1,14 +0,0 @@ -statusCode = $statusCode; + } + + public function getStatusCode(): int + { + return $this->statusCode; + } +} diff --git a/src/Domain/Subscription/Exception/SubscriberAttributeCreationException.php b/src/Domain/Subscription/Exception/SubscriberAttributeCreationException.php new file mode 100644 index 00000000..da2b4f8e --- /dev/null +++ b/src/Domain/Subscription/Exception/SubscriberAttributeCreationException.php @@ -0,0 +1,23 @@ +statusCode = $statusCode; + } + + public function getStatusCode(): int + { + return $this->statusCode; + } +} diff --git a/src/Domain/Subscription/Exception/SubscriptionCreationException.php b/src/Domain/Subscription/Exception/SubscriptionCreationException.php new file mode 100644 index 00000000..3510801d --- /dev/null +++ b/src/Domain/Subscription/Exception/SubscriptionCreationException.php @@ -0,0 +1,23 @@ +statusCode = $statusCode; + } + + public function getStatusCode(): int + { + return $this->statusCode; + } +} diff --git a/src/Domain/Subscription/Model/Dto/AttributeDefinitionDto.php b/src/Domain/Subscription/Model/Dto/AttributeDefinitionDto.php new file mode 100644 index 00000000..4907e7c0 --- /dev/null +++ b/src/Domain/Subscription/Model/Dto/AttributeDefinitionDto.php @@ -0,0 +1,21 @@ +subscriberId = $subscriberId; + return $this; + } + + public function getSubscriberId(): ?int + { + return $this->subscriberId; + } +} diff --git a/src/Domain/Filter/SubscriberFilter.php b/src/Domain/Subscription/Model/Filter/SubscriberFilter.php similarity index 72% rename from src/Domain/Filter/SubscriberFilter.php rename to src/Domain/Subscription/Model/Filter/SubscriberFilter.php index 7fae293b..88cbc6b9 100644 --- a/src/Domain/Filter/SubscriberFilter.php +++ b/src/Domain/Subscription/Model/Filter/SubscriberFilter.php @@ -2,7 +2,9 @@ declare(strict_types=1); -namespace PhpList\Core\Domain\Filter; +namespace PhpList\Core\Domain\Subscription\Model\Filter; + +use PhpList\Core\Domain\Common\Model\Filter\FilterRequestInterface; class SubscriberFilter implements FilterRequestInterface { diff --git a/src/Domain/Model/Subscription/SubscribePage.php b/src/Domain/Subscription/Model/SubscribePage.php similarity index 84% rename from src/Domain/Model/Subscription/SubscribePage.php rename to src/Domain/Subscription/Model/SubscribePage.php index bd2d29cf..7ec518b2 100644 --- a/src/Domain/Model/Subscription/SubscribePage.php +++ b/src/Domain/Subscription/Model/SubscribePage.php @@ -2,12 +2,12 @@ declare(strict_types=1); -namespace PhpList\Core\Domain\Model\Subscription; +namespace PhpList\Core\Domain\Subscription\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\Subscription\SubscriberPageRepository; +use PhpList\Core\Domain\Common\Model\Interfaces\DomainModel; +use PhpList\Core\Domain\Common\Model\Interfaces\Identity; +use PhpList\Core\Domain\Subscription\Repository\SubscriberPageRepository; #[ORM\Entity(repositoryClass: SubscriberPageRepository::class)] #[ORM\Table(name: 'phplist_subscribepage')] diff --git a/src/Domain/Model/Subscription/SubscribePageData.php b/src/Domain/Subscription/Model/SubscribePageData.php similarity index 86% rename from src/Domain/Model/Subscription/SubscribePageData.php rename to src/Domain/Subscription/Model/SubscribePageData.php index 7c99db46..7d8dcd4e 100644 --- a/src/Domain/Model/Subscription/SubscribePageData.php +++ b/src/Domain/Subscription/Model/SubscribePageData.php @@ -2,11 +2,11 @@ declare(strict_types=1); -namespace PhpList\Core\Domain\Model\Subscription; +namespace PhpList\Core\Domain\Subscription\Model; use Doctrine\ORM\Mapping as ORM; -use PhpList\Core\Domain\Model\Interfaces\DomainModel; -use PhpList\Core\Domain\Repository\Subscription\SubscriberPageDataRepository; +use PhpList\Core\Domain\Common\Model\Interfaces\DomainModel; +use PhpList\Core\Domain\Subscription\Repository\SubscriberPageDataRepository; #[ORM\Entity(repositoryClass: SubscriberPageDataRepository::class)] #[ORM\Table(name: 'phplist_subscribepage_data')] diff --git a/src/Domain/Model/Subscription/Subscriber.php b/src/Domain/Subscription/Model/Subscriber.php similarity index 90% rename from src/Domain/Model/Subscription/Subscriber.php rename to src/Domain/Subscription/Model/Subscriber.php index f06972ec..d48e5730 100644 --- a/src/Domain/Model/Subscription/Subscriber.php +++ b/src/Domain/Subscription/Model/Subscriber.php @@ -2,17 +2,17 @@ declare(strict_types=1); -namespace PhpList\Core\Domain\Model\Subscription; +namespace PhpList\Core\Domain\Subscription\Model; use DateTime; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; -use PhpList\Core\Domain\Repository\Subscription\SubscriberRepository; -use PhpList\Core\Domain\Model\Interfaces\CreationDate; -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\CreationDate; +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\Subscription\Repository\SubscriberRepository; /** * This class represents subscriber who can subscribe to multiple subscriber lists and can receive email messages from @@ -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/SubscriberAttributeDefinition.php b/src/Domain/Subscription/Model/SubscriberAttributeDefinition.php similarity index 91% rename from src/Domain/Model/Subscription/SubscriberAttributeDefinition.php rename to src/Domain/Subscription/Model/SubscriberAttributeDefinition.php index 9c9f51b4..dc7259d6 100644 --- a/src/Domain/Model/Subscription/SubscriberAttributeDefinition.php +++ b/src/Domain/Subscription/Model/SubscriberAttributeDefinition.php @@ -2,12 +2,12 @@ declare(strict_types=1); -namespace PhpList\Core\Domain\Model\Subscription; +namespace PhpList\Core\Domain\Subscription\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\Subscription\SubscriberAttributeDefinitionRepository; +use PhpList\Core\Domain\Common\Model\Interfaces\DomainModel; +use PhpList\Core\Domain\Common\Model\Interfaces\Identity; +use PhpList\Core\Domain\Subscription\Repository\SubscriberAttributeDefinitionRepository; #[ORM\Entity(repositoryClass: SubscriberAttributeDefinitionRepository::class)] #[ORM\Table(name: 'phplist_user_attribute')] diff --git a/src/Domain/Model/Subscription/SubscriberAttribute.php b/src/Domain/Subscription/Model/SubscriberAttributeValue.php similarity index 82% rename from src/Domain/Model/Subscription/SubscriberAttribute.php rename to src/Domain/Subscription/Model/SubscriberAttributeValue.php index 72c23b65..05209709 100644 --- a/src/Domain/Model/Subscription/SubscriberAttribute.php +++ b/src/Domain/Subscription/Model/SubscriberAttributeValue.php @@ -2,18 +2,18 @@ declare(strict_types=1); -namespace PhpList\Core\Domain\Model\Subscription; +namespace PhpList\Core\Domain\Subscription\Model; use Doctrine\ORM\Mapping as ORM; -use PhpList\Core\Domain\Model\Interfaces\DomainModel; -use PhpList\Core\Domain\Repository\Subscription\SubscriberAttributeRepository; +use PhpList\Core\Domain\Common\Model\Interfaces\DomainModel; +use PhpList\Core\Domain\Subscription\Repository\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: SubscriberAttributeDefinition::class)] diff --git a/src/Domain/Model/Subscription/SubscriberHistory.php b/src/Domain/Subscription/Model/SubscriberHistory.php similarity index 91% rename from src/Domain/Model/Subscription/SubscriberHistory.php rename to src/Domain/Subscription/Model/SubscriberHistory.php index 5761a96a..0eaa3f7a 100644 --- a/src/Domain/Model/Subscription/SubscriberHistory.php +++ b/src/Domain/Subscription/Model/SubscriberHistory.php @@ -2,13 +2,13 @@ declare(strict_types=1); -namespace PhpList\Core\Domain\Model\Subscription; +namespace PhpList\Core\Domain\Subscription\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\Subscription\SubscriberHistoryRepository; +use PhpList\Core\Domain\Common\Model\Interfaces\DomainModel; +use PhpList\Core\Domain\Common\Model\Interfaces\Identity; +use PhpList\Core\Domain\Subscription\Repository\SubscriberHistoryRepository; #[ORM\Entity(repositoryClass: SubscriberHistoryRepository::class)] #[ORM\Table(name: 'phplist_user_user_history')] diff --git a/src/Domain/Model/Subscription/SubscriberList.php b/src/Domain/Subscription/Model/SubscriberList.php similarity index 92% rename from src/Domain/Model/Subscription/SubscriberList.php rename to src/Domain/Subscription/Model/SubscriberList.php index 1b58875e..65f7a9fa 100644 --- a/src/Domain/Model/Subscription/SubscriberList.php +++ b/src/Domain/Subscription/Model/SubscriberList.php @@ -2,18 +2,18 @@ declare(strict_types=1); -namespace PhpList\Core\Domain\Model\Subscription; +namespace PhpList\Core\Domain\Subscription\Model; use DateTime; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; -use PhpList\Core\Domain\Model\Identity\Administrator; -use PhpList\Core\Domain\Model\Interfaces\CreationDate; -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\Repository\Subscription\SubscriberListRepository; +use PhpList\Core\Domain\Common\Model\Interfaces\CreationDate; +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\Subscription\Repository\SubscriberListRepository; use Symfony\Component\Serializer\Attribute\MaxDepth; /** diff --git a/src/Domain/Model/Subscription/Subscription.php b/src/Domain/Subscription/Model/Subscription.php similarity index 90% rename from src/Domain/Model/Subscription/Subscription.php rename to src/Domain/Subscription/Model/Subscription.php index ba1ade79..94e79965 100644 --- a/src/Domain/Model/Subscription/Subscription.php +++ b/src/Domain/Subscription/Model/Subscription.php @@ -2,15 +2,15 @@ declare(strict_types=1); -namespace PhpList\Core\Domain\Model\Subscription; +namespace PhpList\Core\Domain\Subscription\Model; use DateTime; use Doctrine\ORM\Mapping as ORM; use Doctrine\Persistence\Proxy; -use PhpList\Core\Domain\Model\Interfaces\CreationDate; -use PhpList\Core\Domain\Model\Interfaces\DomainModel; -use PhpList\Core\Domain\Model\Interfaces\ModificationDate; -use PhpList\Core\Domain\Repository\Subscription\SubscriptionRepository; +use PhpList\Core\Domain\Common\Model\Interfaces\CreationDate; +use PhpList\Core\Domain\Common\Model\Interfaces\DomainModel; +use PhpList\Core\Domain\Common\Model\Interfaces\ModificationDate; +use PhpList\Core\Domain\Subscription\Repository\SubscriptionRepository; use Symfony\Component\Serializer\Annotation\Ignore; use Symfony\Component\Serializer\Annotation\SerializedName; use Symfony\Component\Serializer\Attribute\Groups; diff --git a/src/Domain/Subscription/Repository/SubscriberAttributeDefinitionRepository.php b/src/Domain/Subscription/Repository/SubscriberAttributeDefinitionRepository.php new file mode 100644 index 00000000..1db2e0e6 --- /dev/null +++ b/src/Domain/Subscription/Repository/SubscriberAttributeDefinitionRepository.php @@ -0,0 +1,20 @@ +findOneBy(['name' => $name]); + } +} diff --git a/src/Domain/Subscription/Repository/SubscriberAttributeValueRepository.php b/src/Domain/Subscription/Repository/SubscriberAttributeValueRepository.php new file mode 100644 index 00000000..d29d56ff --- /dev/null +++ b/src/Domain/Subscription/Repository/SubscriberAttributeValueRepository.php @@ -0,0 +1,67 @@ +findOneBy([ + 'subscriber' => $subscriber, + 'attributeDefinition' => $attributeDefinition, + ]); + } + + public function findOneBySubscriberIdAndAttributeId( + int $subscriberId, + int $attributeDefinitionId + ): ?SubscriberAttributeValue { + 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(); + } + + /** + * @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('ad.id', 'ASC') + ->setMaxResults($limit) + ->getQuery() + ->getResult(); + } +} diff --git a/src/Domain/Subscription/Repository/SubscriberHistoryRepository.php b/src/Domain/Subscription/Repository/SubscriberHistoryRepository.php new file mode 100644 index 00000000..a9f58ae5 --- /dev/null +++ b/src/Domain/Subscription/Repository/SubscriberHistoryRepository.php @@ -0,0 +1,14 @@ + * @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/Repository/Subscription/SubscriptionRepository.php b/src/Domain/Subscription/Repository/SubscriptionRepository.php similarity index 80% rename from src/Domain/Repository/Subscription/SubscriptionRepository.php rename to src/Domain/Subscription/Repository/SubscriptionRepository.php index e1e97827..de2bea56 100644 --- a/src/Domain/Repository/Subscription/SubscriptionRepository.php +++ b/src/Domain/Subscription/Repository/SubscriptionRepository.php @@ -2,12 +2,12 @@ declare(strict_types=1); -namespace PhpList\Core\Domain\Repository\Subscription; +namespace PhpList\Core\Domain\Subscription\Repository; -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\AbstractRepository; +use PhpList\Core\Domain\Common\Repository\AbstractRepository; +use PhpList\Core\Domain\Subscription\Model\Subscriber; +use PhpList\Core\Domain\Subscription\Model\SubscriberList; +use PhpList\Core\Domain\Subscription\Model\Subscription; /** * Repository for Subscription models. diff --git a/src/Domain/Subscription/Service/AttributeDefinitionManager.php b/src/Domain/Subscription/Service/AttributeDefinitionManager.php new file mode 100644 index 00000000..45d46821 --- /dev/null +++ b/src/Domain/Subscription/Service/AttributeDefinitionManager.php @@ -0,0 +1,77 @@ +definitionRepository = $definitionRepository; + } + + public function create(AttributeDefinitionDto $attributeDefinitionDto): SubscriberAttributeDefinition + { + $existingAttribute = $this->definitionRepository->findOneByName($attributeDefinitionDto->name); + if ($existingAttribute) { + throw new AttributeDefinitionCreationException('Attribute definition already exists', 409); + } + + $attributeDefinition = (new SubscriberAttributeDefinition()) + ->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 update( + 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); + } + + $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(SubscriberAttributeDefinition $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/Subscription/Service/SubscriberAttributeManager.php b/src/Domain/Subscription/Service/SubscriberAttributeManager.php new file mode 100644 index 00000000..1cbfdd24 --- /dev/null +++ b/src/Domain/Subscription/Service/SubscriberAttributeManager.php @@ -0,0 +1,54 @@ +attributeRepository = $attributeRepository; + } + + public function createOrUpdate( + Subscriber $subscriber, + SubscriberAttributeDefinition $definition, + ?string $value = null + ): SubscriberAttributeValue { + $subscriberAttribute = $this->attributeRepository + ->findOneBySubscriberAndAttribute($subscriber, $definition); + + if (!$subscriberAttribute) { + $subscriberAttribute = new SubscriberAttributeValue($definition, $subscriber); + } + + $value = $value ?? $definition->getDefaultValue(); + if ($value === null) { + throw new SubscriberAttributeCreationException('Value is required', 400); + } + + $subscriberAttribute->setValue($value); + $this->attributeRepository->save($subscriberAttribute); + + return $subscriberAttribute; + } + + public function getSubscriberAttribute(int $subscriberId, int $attributeDefinitionId): ?SubscriberAttributeValue + { + return $this->attributeRepository->findOneBySubscriberIdAndAttributeId($subscriberId, $attributeDefinitionId); + } + + public function delete(SubscriberAttributeValue $attribute): void + { + $this->attributeRepository->remove($attribute); + } +} diff --git a/src/Domain/Subscription/Service/SubscriberCsvExportManager.php b/src/Domain/Subscription/Service/SubscriberCsvExportManager.php new file mode 100644 index 00000000..2029ac85 --- /dev/null +++ b/src/Domain/Subscription/Service/SubscriberCsvExportManager.php @@ -0,0 +1,176 @@ +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/SubscriberCsvImportManager.php b/src/Domain/Subscription/Service/SubscriberCsvImportManager.php new file mode 100644 index 00000000..a0a03def --- /dev/null +++ b/src/Domain/Subscription/Service/SubscriberCsvImportManager.php @@ -0,0 +1,387 @@ +subscriberManager = $subscriberManager; + $this->attributeManager = $attributeManager; + $this->subscriberRepository = $subscriberRepository; + $this->definitionRepository = $definitionRepository; + } + + /** + * Import subscribers from a CSV file. + * + * @param UploadedFile $file The uploaded CSV file + * @param SubscriberImportOptions $options + * @return array Import statistics + */ + public function importFromCsv(UploadedFile $file, SubscriberImportOptions $options): array + { + $stats = [ + 'created' => 0, + 'updated' => 0, + 'skipped' => 0, + 'errors' => [], + ]; + + [$handle, $headers, $attributeDefinitions] = $this->prepareImport($file); + + $lineNumber = 2; + $data = fgetcsv($handle); + while ($data !== false) { + try { + $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']++; + } + + $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, new SubscriberImportOptions(updateExisting: 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, new SubscriberImportOptions()); + } + + /** + * 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'); + } + + $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 = $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, $systemFields, true)) { + continue; + } + + $attributeDefinition = $this->definitionRepository->findOneBy(['name' => $header]); + if ($attributeDefinition) { + $attributeDefinitions[$index] = $attributeDefinition; + } + } + + return $attributeDefinitions; + } + + /** + * Process a single row from the CSV file. + * + * @param array $data Row data + * @param array $headers CSV headers + * @param array $attributeDefinitions Attribute definitions + * @param SubscriberImportOptions $options + * @param array $stats Statistics to update + * @param int $lineNumber Current line number for error reporting + */ + private function processRow( + array $data, + array $headers, + array $attributeDefinitions, + SubscriberImportOptions $options, + 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; + } + + $existingSubscriber = $this->subscriberRepository->findOneByEmail($email); + + if ($existingSubscriber && !$options->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); + if ($extraDataIndex !== false && isset($data[$extraDataIndex])) { + $additionalData = $data[$extraDataIndex]; + } else { + $additionalData = $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] + ); + } + } + } +} diff --git a/src/Domain/Subscription/Service/SubscriberListManager.php b/src/Domain/Subscription/Service/SubscriberListManager.php new file mode 100644 index 00000000..10f9d065 --- /dev/null +++ b/src/Domain/Subscription/Service/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->isPublic); + + $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/Subscription/Service/SubscriberManager.php b/src/Domain/Subscription/Service/SubscriberManager.php new file mode 100644 index 00000000..94891b46 --- /dev/null +++ b/src/Domain/Subscription/Service/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/Subscription/Service/SubscriptionManager.php b/src/Domain/Subscription/Service/SubscriptionManager.php new file mode 100644 index 00000000..d360bc24 --- /dev/null +++ b/src/Domain/Subscription/Service/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/Security/Authentication.php b/src/Security/Authentication.php index 477476fc..5c6d69c4 100644 --- a/src/Security/Authentication.php +++ b/src/Security/Authentication.php @@ -5,8 +5,8 @@ namespace PhpList\Core\Security; use Doctrine\ORM\EntityNotFoundException; -use PhpList\Core\Domain\Model\Identity\Administrator; -use PhpList\Core\Domain\Repository\Identity\AdministratorTokenRepository; +use PhpList\Core\Domain\Identity\Repository\AdministratorTokenRepository; +use PhpList\Core\Domain\Identity\Model\Administrator; use Symfony\Component\HttpFoundation\Request; /** diff --git a/src/TestingSupport/Traits/ModelTestTrait.php b/src/TestingSupport/Traits/ModelTestTrait.php index 5dcfc5d5..10ccbae8 100644 --- a/src/TestingSupport/Traits/ModelTestTrait.php +++ b/src/TestingSupport/Traits/ModelTestTrait.php @@ -4,7 +4,7 @@ namespace PhpList\Core\TestingSupport\Traits; -use PhpList\Core\Domain\Model\Interfaces\DomainModel; +use PhpList\Core\Domain\Common\Model\Interfaces\DomainModel; use ReflectionObject; /** 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()); + } +} diff --git a/tests/Integration/Domain/Repository/Fixtures/Identity/Administrator.csv b/tests/Integration/Domain/Identity/Fixtures/Administrator.csv similarity index 100% rename from tests/Integration/Domain/Repository/Fixtures/Identity/Administrator.csv rename to tests/Integration/Domain/Identity/Fixtures/Administrator.csv diff --git a/tests/Integration/Domain/Repository/Fixtures/Identity/AdministratorFixture.php b/tests/Integration/Domain/Identity/Fixtures/AdministratorFixture.php similarity index 92% rename from tests/Integration/Domain/Repository/Fixtures/Identity/AdministratorFixture.php rename to tests/Integration/Domain/Identity/Fixtures/AdministratorFixture.php index 963d7648..cde421e6 100644 --- a/tests/Integration/Domain/Repository/Fixtures/Identity/AdministratorFixture.php +++ b/tests/Integration/Domain/Identity/Fixtures/AdministratorFixture.php @@ -2,12 +2,12 @@ declare(strict_types=1); -namespace PhpList\Core\Tests\Integration\Domain\Repository\Fixtures\Identity; +namespace PhpList\Core\Tests\Integration\Domain\Identity\Fixtures; use DateTime; use Doctrine\Bundle\FixturesBundle\Fixture; use Doctrine\Persistence\ObjectManager; -use PhpList\Core\Domain\Model\Identity\Administrator; +use PhpList\Core\Domain\Identity\Model\Administrator; use PhpList\Core\TestingSupport\Traits\ModelTestTrait; use RuntimeException; diff --git a/tests/Integration/Domain/Repository/Fixtures/Identity/AdministratorTokenWithAdministrator.csv b/tests/Integration/Domain/Identity/Fixtures/AdministratorTokenWithAdministrator.csv similarity index 100% rename from tests/Integration/Domain/Repository/Fixtures/Identity/AdministratorTokenWithAdministrator.csv rename to tests/Integration/Domain/Identity/Fixtures/AdministratorTokenWithAdministrator.csv diff --git a/tests/Integration/Domain/Repository/Fixtures/Identity/AdministratorTokenWithAdministratorFixture.php b/tests/Integration/Domain/Identity/Fixtures/AdministratorTokenWithAdministratorFixture.php similarity index 90% rename from tests/Integration/Domain/Repository/Fixtures/Identity/AdministratorTokenWithAdministratorFixture.php rename to tests/Integration/Domain/Identity/Fixtures/AdministratorTokenWithAdministratorFixture.php index 9805caea..e382a4c7 100644 --- a/tests/Integration/Domain/Repository/Fixtures/Identity/AdministratorTokenWithAdministratorFixture.php +++ b/tests/Integration/Domain/Identity/Fixtures/AdministratorTokenWithAdministratorFixture.php @@ -2,13 +2,13 @@ declare(strict_types=1); -namespace PhpList\Core\Tests\Integration\Domain\Repository\Fixtures\Identity; +namespace PhpList\Core\Tests\Integration\Domain\Identity\Fixtures; use DateTime; use Doctrine\Bundle\FixturesBundle\Fixture; use Doctrine\Persistence\ObjectManager; -use PhpList\Core\Domain\Model\Identity\Administrator; -use PhpList\Core\Domain\Model\Identity\AdministratorToken; +use PhpList\Core\Domain\Identity\Model\Administrator; +use PhpList\Core\Domain\Identity\Model\AdministratorToken; use PhpList\Core\TestingSupport\Traits\ModelTestTrait; use RuntimeException; diff --git a/tests/Integration/Domain/Repository/Fixtures/Identity/DetachedAdministratorTokenFixture.php b/tests/Integration/Domain/Identity/Fixtures/DetachedAdministratorTokenFixture.php similarity index 91% rename from tests/Integration/Domain/Repository/Fixtures/Identity/DetachedAdministratorTokenFixture.php rename to tests/Integration/Domain/Identity/Fixtures/DetachedAdministratorTokenFixture.php index cd4bc687..a3d16ea7 100644 --- a/tests/Integration/Domain/Repository/Fixtures/Identity/DetachedAdministratorTokenFixture.php +++ b/tests/Integration/Domain/Identity/Fixtures/DetachedAdministratorTokenFixture.php @@ -2,12 +2,12 @@ declare(strict_types=1); -namespace PhpList\Core\Tests\Integration\Domain\Repository\Fixtures\Identity; +namespace PhpList\Core\Tests\Integration\Domain\Identity\Fixtures; use DateTime; use Doctrine\Bundle\FixturesBundle\Fixture; use Doctrine\Persistence\ObjectManager; -use PhpList\Core\Domain\Model\Identity\AdministratorToken; +use PhpList\Core\Domain\Identity\Model\AdministratorToken; use PhpList\Core\TestingSupport\Traits\ModelTestTrait; use RuntimeException; diff --git a/tests/Integration/Domain/Repository/Fixtures/Identity/DetachedAdministratorTokens.csv b/tests/Integration/Domain/Identity/Fixtures/DetachedAdministratorTokens.csv similarity index 100% rename from tests/Integration/Domain/Repository/Fixtures/Identity/DetachedAdministratorTokens.csv rename to tests/Integration/Domain/Identity/Fixtures/DetachedAdministratorTokens.csv diff --git a/tests/Integration/Domain/Repository/Identity/AdministratorRepositoryTest.php b/tests/Integration/Domain/Identity/Repository/AdministratorRepositoryTest.php similarity index 95% rename from tests/Integration/Domain/Repository/Identity/AdministratorRepositoryTest.php rename to tests/Integration/Domain/Identity/Repository/AdministratorRepositoryTest.php index 05d0b83c..2732f736 100644 --- a/tests/Integration/Domain/Repository/Identity/AdministratorRepositoryTest.php +++ b/tests/Integration/Domain/Identity/Repository/AdministratorRepositoryTest.php @@ -2,15 +2,15 @@ declare(strict_types=1); -namespace PhpList\Core\Tests\Integration\Domain\Repository\Identity; +namespace PhpList\Core\Tests\Integration\Domain\Identity\Repository; use DateTime; use Doctrine\ORM\Tools\SchemaTool; -use PhpList\Core\Domain\Model\Identity\Administrator; -use PhpList\Core\Domain\Repository\Identity\AdministratorRepository; +use PhpList\Core\Domain\Identity\Model\Administrator; +use PhpList\Core\Domain\Identity\Repository\AdministratorRepository; use PhpList\Core\TestingSupport\Traits\DatabaseTestTrait; use PhpList\Core\TestingSupport\Traits\ModelTestTrait; -use PhpList\Core\Tests\Integration\Domain\Repository\Fixtures\Identity\AdministratorFixture; +use PhpList\Core\Tests\Integration\Domain\Identity\Fixtures\AdministratorFixture; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; /** diff --git a/tests/Integration/Domain/Repository/Identity/AdministratorTokenRepositoryTest.php b/tests/Integration/Domain/Identity/Repository/AdministratorTokenRepositoryTest.php similarity index 92% rename from tests/Integration/Domain/Repository/Identity/AdministratorTokenRepositoryTest.php rename to tests/Integration/Domain/Identity/Repository/AdministratorTokenRepositoryTest.php index b37d4e79..52feeb8c 100644 --- a/tests/Integration/Domain/Repository/Identity/AdministratorTokenRepositoryTest.php +++ b/tests/Integration/Domain/Identity/Repository/AdministratorTokenRepositoryTest.php @@ -2,18 +2,18 @@ declare(strict_types=1); -namespace PhpList\Core\Tests\Integration\Domain\Repository\Identity; +namespace PhpList\Core\Tests\Integration\Domain\Identity\Repository; use DateTime; use Doctrine\ORM\Tools\SchemaTool; -use PhpList\Core\Domain\Model\Identity\Administrator; -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\Identity\Model\Administrator; +use PhpList\Core\Domain\Identity\Model\AdministratorToken; +use PhpList\Core\Domain\Identity\Repository\AdministratorRepository; +use PhpList\Core\Domain\Identity\Repository\AdministratorTokenRepository; use PhpList\Core\TestingSupport\Traits\DatabaseTestTrait; use PhpList\Core\TestingSupport\Traits\SimilarDatesAssertionTrait; -use PhpList\Core\Tests\Integration\Domain\Repository\Fixtures\Identity\AdministratorFixture; -use PhpList\Core\Tests\Integration\Domain\Repository\Fixtures\Identity\DetachedAdministratorTokenFixture; +use PhpList\Core\Tests\Integration\Domain\Identity\Fixtures\DetachedAdministratorTokenFixture; +use PhpList\Core\Tests\Integration\Domain\Identity\Fixtures\AdministratorFixture; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; /** diff --git a/tests/Integration/Domain/Repository/Fixtures/Messaging/Message.csv b/tests/Integration/Domain/Messaging/Fixtures/Message.csv similarity index 100% rename from tests/Integration/Domain/Repository/Fixtures/Messaging/Message.csv rename to tests/Integration/Domain/Messaging/Fixtures/Message.csv diff --git a/tests/Integration/Domain/Repository/Fixtures/Messaging/MessageFixture.php b/tests/Integration/Domain/Messaging/Fixtures/MessageFixture.php similarity index 85% rename from tests/Integration/Domain/Repository/Fixtures/Messaging/MessageFixture.php rename to tests/Integration/Domain/Messaging/Fixtures/MessageFixture.php index b65e66c2..52930d14 100644 --- a/tests/Integration/Domain/Repository/Fixtures/Messaging/MessageFixture.php +++ b/tests/Integration/Domain/Messaging/Fixtures/MessageFixture.php @@ -2,19 +2,19 @@ declare(strict_types=1); -namespace PhpList\Core\Tests\Integration\Domain\Repository\Fixtures\Messaging; +namespace PhpList\Core\Tests\Integration\Domain\Messaging\Fixtures; use DateTime; use Doctrine\Bundle\FixturesBundle\Fixture; use Doctrine\Persistence\ObjectManager; -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\Messaging\Template; +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 PhpList\Core\Domain\Messaging\Model\Template; use PhpList\Core\TestingSupport\Traits\ModelTestTrait; use RuntimeException; diff --git a/tests/Integration/Domain/Repository/Fixtures/Messaging/Template.csv b/tests/Integration/Domain/Messaging/Fixtures/Template.csv similarity index 100% rename from tests/Integration/Domain/Repository/Fixtures/Messaging/Template.csv rename to tests/Integration/Domain/Messaging/Fixtures/Template.csv diff --git a/tests/Integration/Domain/Repository/Fixtures/Messaging/TemplateFixture.php b/tests/Integration/Domain/Messaging/Fixtures/TemplateFixture.php similarity index 90% rename from tests/Integration/Domain/Repository/Fixtures/Messaging/TemplateFixture.php rename to tests/Integration/Domain/Messaging/Fixtures/TemplateFixture.php index e5222290..f1456acb 100644 --- a/tests/Integration/Domain/Repository/Fixtures/Messaging/TemplateFixture.php +++ b/tests/Integration/Domain/Messaging/Fixtures/TemplateFixture.php @@ -2,11 +2,11 @@ declare(strict_types=1); -namespace PhpList\Core\Tests\Integration\Domain\Repository\Fixtures\Messaging; +namespace PhpList\Core\Tests\Integration\Domain\Messaging\Fixtures; use Doctrine\Bundle\FixturesBundle\Fixture; use Doctrine\Persistence\ObjectManager; -use PhpList\Core\Domain\Model\Messaging\Template; +use PhpList\Core\Domain\Messaging\Model\Template; use PhpList\Core\TestingSupport\Traits\ModelTestTrait; use RuntimeException; diff --git a/tests/Integration/Domain/Repository/Messaging/MessageRepositoryTest.php b/tests/Integration/Domain/Messaging/Repository/MessageRepositoryTest.php similarity index 87% rename from tests/Integration/Domain/Repository/Messaging/MessageRepositoryTest.php rename to tests/Integration/Domain/Messaging/Repository/MessageRepositoryTest.php index fc16a416..d7435815 100644 --- a/tests/Integration/Domain/Repository/Messaging/MessageRepositoryTest.php +++ b/tests/Integration/Domain/Messaging/Repository/MessageRepositoryTest.php @@ -2,18 +2,18 @@ declare(strict_types=1); -namespace PhpList\Core\Tests\Integration\Domain\Repository\Messaging; +namespace PhpList\Core\Tests\Integration\Domain\Messaging\Repository; use DateTime; use Doctrine\ORM\Tools\SchemaTool; -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\Repository\Messaging\MessageRepository; +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 PhpList\Core\Domain\Messaging\Repository\MessageRepository; use PhpList\Core\TestingSupport\Traits\DatabaseTestTrait; use PhpList\Core\TestingSupport\Traits\SimilarDatesAssertionTrait; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; diff --git a/tests/Integration/Domain/Repository/Messaging/SubscriberListRepositoryTest.php b/tests/Integration/Domain/Messaging/Repository/SubscriberListRepositoryTest.php similarity index 89% rename from tests/Integration/Domain/Repository/Messaging/SubscriberListRepositoryTest.php rename to tests/Integration/Domain/Messaging/Repository/SubscriberListRepositoryTest.php index c721cb57..0f122582 100644 --- a/tests/Integration/Domain/Repository/Messaging/SubscriberListRepositoryTest.php +++ b/tests/Integration/Domain/Messaging/Repository/SubscriberListRepositoryTest.php @@ -2,23 +2,23 @@ declare(strict_types=1); -namespace PhpList\Core\Tests\Integration\Domain\Repository\Messaging; +namespace PhpList\Core\Tests\Integration\Domain\Messaging\Repository; use DateTime; use Doctrine\ORM\Tools\SchemaTool; -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\Identity\AdministratorRepository; -use PhpList\Core\Domain\Repository\Subscription\SubscriberListRepository; -use PhpList\Core\Domain\Repository\Subscription\SubscriberRepository; -use PhpList\Core\Domain\Repository\Subscription\SubscriptionRepository; +use PhpList\Core\Domain\Identity\Repository\AdministratorRepository; +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\SubscriberListRepository; +use PhpList\Core\Domain\Subscription\Repository\SubscriberRepository; +use PhpList\Core\Domain\Subscription\Repository\SubscriptionRepository; use PhpList\Core\TestingSupport\Traits\DatabaseTestTrait; use PhpList\Core\TestingSupport\Traits\SimilarDatesAssertionTrait; -use PhpList\Core\Tests\Integration\Domain\Repository\Fixtures\Identity\AdministratorFixture; -use PhpList\Core\Tests\Integration\Domain\Repository\Fixtures\Subscription\SubscriberFixture; -use PhpList\Core\Tests\Integration\Domain\Repository\Fixtures\Subscription\SubscriberListFixture; -use PhpList\Core\Tests\Integration\Domain\Repository\Fixtures\Subscription\SubscriptionFixture; +use PhpList\Core\Tests\Integration\Domain\Identity\Fixtures\AdministratorFixture; +use PhpList\Core\Tests\Integration\Domain\Subscription\Fixtures\SubscriberFixture; +use PhpList\Core\Tests\Integration\Domain\Subscription\Fixtures\SubscriberListFixture; +use PhpList\Core\Tests\Integration\Domain\Subscription\Fixtures\SubscriptionFixture; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; /** diff --git a/tests/Integration/Domain/Repository/Messaging/TemplateRepositoryTest.php b/tests/Integration/Domain/Messaging/Repository/TemplateRepositoryTest.php similarity index 88% rename from tests/Integration/Domain/Repository/Messaging/TemplateRepositoryTest.php rename to tests/Integration/Domain/Messaging/Repository/TemplateRepositoryTest.php index 2d49df1e..672675b2 100644 --- a/tests/Integration/Domain/Repository/Messaging/TemplateRepositoryTest.php +++ b/tests/Integration/Domain/Messaging/Repository/TemplateRepositoryTest.php @@ -2,13 +2,13 @@ declare(strict_types=1); -namespace PhpList\Core\Tests\Integration\Domain\Repository\Messaging; +namespace PhpList\Core\Tests\Integration\Domain\Messaging\Repository; use Doctrine\ORM\Tools\SchemaTool; -use PhpList\Core\Domain\Model\Messaging\Template; -use PhpList\Core\Domain\Repository\Messaging\TemplateRepository; +use PhpList\Core\Domain\Messaging\Model\Template; +use PhpList\Core\Domain\Messaging\Repository\TemplateRepository; use PhpList\Core\TestingSupport\Traits\DatabaseTestTrait; -use PhpList\Core\Tests\Integration\Domain\Repository\Fixtures\Messaging\TemplateFixture; +use PhpList\Core\Tests\Integration\Domain\Messaging\Fixtures\TemplateFixture; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; class TemplateRepositoryTest extends KernelTestCase diff --git a/tests/Integration/Domain/Repository/Fixtures/Subscription/Subscriber.csv b/tests/Integration/Domain/Subscription/Fixtures/Subscriber.csv similarity index 100% rename from tests/Integration/Domain/Repository/Fixtures/Subscription/Subscriber.csv rename to tests/Integration/Domain/Subscription/Fixtures/Subscriber.csv diff --git a/tests/Integration/Domain/Repository/Fixtures/Subscription/SubscriberFixture.php b/tests/Integration/Domain/Subscription/Fixtures/SubscriberFixture.php similarity index 93% rename from tests/Integration/Domain/Repository/Fixtures/Subscription/SubscriberFixture.php rename to tests/Integration/Domain/Subscription/Fixtures/SubscriberFixture.php index a9fc5867..9c74dd1b 100644 --- a/tests/Integration/Domain/Repository/Fixtures/Subscription/SubscriberFixture.php +++ b/tests/Integration/Domain/Subscription/Fixtures/SubscriberFixture.php @@ -2,12 +2,12 @@ declare(strict_types=1); -namespace PhpList\Core\Tests\Integration\Domain\Repository\Fixtures\Subscription; +namespace PhpList\Core\Tests\Integration\Domain\Subscription\Fixtures; use DateTime; use Doctrine\Bundle\FixturesBundle\Fixture; use Doctrine\Persistence\ObjectManager; -use PhpList\Core\Domain\Model\Subscription\Subscriber; +use PhpList\Core\Domain\Subscription\Model\Subscriber; use PhpList\Core\TestingSupport\Traits\ModelTestTrait; use RuntimeException; diff --git a/tests/Integration/Domain/Repository/Fixtures/Subscription/SubscriberList.csv b/tests/Integration/Domain/Subscription/Fixtures/SubscriberList.csv similarity index 100% rename from tests/Integration/Domain/Repository/Fixtures/Subscription/SubscriberList.csv rename to tests/Integration/Domain/Subscription/Fixtures/SubscriberList.csv diff --git a/tests/Integration/Domain/Repository/Fixtures/Subscription/SubscriberListFixture.php b/tests/Integration/Domain/Subscription/Fixtures/SubscriberListFixture.php similarity index 91% rename from tests/Integration/Domain/Repository/Fixtures/Subscription/SubscriberListFixture.php rename to tests/Integration/Domain/Subscription/Fixtures/SubscriberListFixture.php index 954c3c0e..133d2248 100644 --- a/tests/Integration/Domain/Repository/Fixtures/Subscription/SubscriberListFixture.php +++ b/tests/Integration/Domain/Subscription/Fixtures/SubscriberListFixture.php @@ -2,13 +2,13 @@ declare(strict_types=1); -namespace PhpList\Core\Tests\Integration\Domain\Repository\Fixtures\Subscription; +namespace PhpList\Core\Tests\Integration\Domain\Subscription\Fixtures; use DateTime; use Doctrine\Bundle\FixturesBundle\Fixture; use Doctrine\Persistence\ObjectManager; -use PhpList\Core\Domain\Model\Identity\Administrator; -use PhpList\Core\Domain\Model\Subscription\SubscriberList; +use PhpList\Core\Domain\Identity\Model\Administrator; +use PhpList\Core\Domain\Subscription\Model\SubscriberList; use PhpList\Core\TestingSupport\Traits\ModelTestTrait; use RuntimeException; diff --git a/tests/Integration/Domain/Repository/Fixtures/Subscription/Subscription.csv b/tests/Integration/Domain/Subscription/Fixtures/Subscription.csv similarity index 100% rename from tests/Integration/Domain/Repository/Fixtures/Subscription/Subscription.csv rename to tests/Integration/Domain/Subscription/Fixtures/Subscription.csv diff --git a/tests/Integration/Domain/Repository/Fixtures/Subscription/SubscriptionFixture.php b/tests/Integration/Domain/Subscription/Fixtures/SubscriptionFixture.php similarity index 87% rename from tests/Integration/Domain/Repository/Fixtures/Subscription/SubscriptionFixture.php rename to tests/Integration/Domain/Subscription/Fixtures/SubscriptionFixture.php index df1741c5..72f15f7d 100644 --- a/tests/Integration/Domain/Repository/Fixtures/Subscription/SubscriptionFixture.php +++ b/tests/Integration/Domain/Subscription/Fixtures/SubscriptionFixture.php @@ -2,14 +2,14 @@ declare(strict_types=1); -namespace PhpList\Core\Tests\Integration\Domain\Repository\Fixtures\Subscription; +namespace PhpList\Core\Tests\Integration\Domain\Subscription\Fixtures; use DateTime; use Doctrine\Bundle\FixturesBundle\Fixture; use Doctrine\Persistence\ObjectManager; -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\Subscription\Model\Subscriber; +use PhpList\Core\Domain\Subscription\Model\SubscriberList; +use PhpList\Core\Domain\Subscription\Model\Subscription; use PhpList\Core\TestingSupport\Traits\ModelTestTrait; use RuntimeException; diff --git a/tests/Integration/Domain/Repository/Subscription/SubscriberRepositoryTest.php b/tests/Integration/Domain/Subscription/Repository/SubscriberRepositoryTest.php similarity index 91% rename from tests/Integration/Domain/Repository/Subscription/SubscriberRepositoryTest.php rename to tests/Integration/Domain/Subscription/Repository/SubscriberRepositoryTest.php index 7c779bfd..91d871d5 100644 --- a/tests/Integration/Domain/Repository/Subscription/SubscriberRepositoryTest.php +++ b/tests/Integration/Domain/Subscription/Repository/SubscriberRepositoryTest.php @@ -2,23 +2,23 @@ declare(strict_types=1); -namespace PhpList\Core\Tests\Integration\Domain\Repository\Subscription; +namespace PhpList\Core\Tests\Integration\Domain\Subscription\Repository; use DateTime; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\DBAL\Exception\UniqueConstraintViolationException; use Doctrine\ORM\Tools\SchemaTool; -use PhpList\Core\Domain\Model\Subscription\Subscriber; -use PhpList\Core\Domain\Model\Subscription\Subscription; -use PhpList\Core\Domain\Repository\Subscription\SubscriberListRepository; -use PhpList\Core\Domain\Repository\Subscription\SubscriberRepository; -use PhpList\Core\Domain\Repository\Subscription\SubscriptionRepository; +use PhpList\Core\Domain\Subscription\Model\Subscriber; +use PhpList\Core\Domain\Subscription\Model\Subscription; +use PhpList\Core\Domain\Subscription\Repository\SubscriberListRepository; +use PhpList\Core\Domain\Subscription\Repository\SubscriberRepository; +use PhpList\Core\Domain\Subscription\Repository\SubscriptionRepository; use PhpList\Core\TestingSupport\Traits\DatabaseTestTrait; use PhpList\Core\TestingSupport\Traits\SimilarDatesAssertionTrait; -use PhpList\Core\Tests\Integration\Domain\Repository\Fixtures\Identity\AdministratorFixture; -use PhpList\Core\Tests\Integration\Domain\Repository\Fixtures\Subscription\SubscriberFixture; -use PhpList\Core\Tests\Integration\Domain\Repository\Fixtures\Subscription\SubscriberListFixture; -use PhpList\Core\Tests\Integration\Domain\Repository\Fixtures\Subscription\SubscriptionFixture; +use PhpList\Core\Tests\Integration\Domain\Identity\Fixtures\AdministratorFixture; +use PhpList\Core\Tests\Integration\Domain\Subscription\Fixtures\SubscriberFixture; +use PhpList\Core\Tests\Integration\Domain\Subscription\Fixtures\SubscriberListFixture; +use PhpList\Core\Tests\Integration\Domain\Subscription\Fixtures\SubscriptionFixture; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; /** diff --git a/tests/Integration/Domain/Repository/Subscription/SubscriptionRepositoryTest.php b/tests/Integration/Domain/Subscription/Repository/SubscriptionRepositoryTest.php similarity index 92% rename from tests/Integration/Domain/Repository/Subscription/SubscriptionRepositoryTest.php rename to tests/Integration/Domain/Subscription/Repository/SubscriptionRepositoryTest.php index e394a3ff..de18894c 100644 --- a/tests/Integration/Domain/Repository/Subscription/SubscriptionRepositoryTest.php +++ b/tests/Integration/Domain/Subscription/Repository/SubscriptionRepositoryTest.php @@ -2,21 +2,21 @@ declare(strict_types=1); -namespace PhpList\Core\Tests\Integration\Domain\Repository\Subscription; +namespace PhpList\Core\Tests\Integration\Domain\Subscription\Repository; use DateTime; use Doctrine\ORM\Tools\SchemaTool; -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\SubscriberListRepository; -use PhpList\Core\Domain\Repository\Subscription\SubscriberRepository; -use PhpList\Core\Domain\Repository\Subscription\SubscriptionRepository; +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\SubscriberListRepository; +use PhpList\Core\Domain\Subscription\Repository\SubscriberRepository; +use PhpList\Core\Domain\Subscription\Repository\SubscriptionRepository; use PhpList\Core\TestingSupport\Traits\DatabaseTestTrait; use PhpList\Core\TestingSupport\Traits\SimilarDatesAssertionTrait; -use PhpList\Core\Tests\Integration\Domain\Repository\Fixtures\Subscription\SubscriberFixture; -use PhpList\Core\Tests\Integration\Domain\Repository\Fixtures\Subscription\SubscriberListFixture; -use PhpList\Core\Tests\Integration\Domain\Repository\Fixtures\Subscription\SubscriptionFixture; +use PhpList\Core\Tests\Integration\Domain\Subscription\Fixtures\SubscriberFixture; +use PhpList\Core\Tests\Integration\Domain\Subscription\Fixtures\SubscriberListFixture; +use PhpList\Core\Tests\Integration\Domain\Subscription\Fixtures\SubscriptionFixture; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; /** 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/SubscriberCsvImportManagerTest.php b/tests/Integration/Domain/Subscription/Service/SubscriberCsvImportManagerTest.php new file mode 100644 index 00000000..4dcf68f8 --- /dev/null +++ b/tests/Integration/Domain/Subscription/Service/SubscriberCsvImportManagerTest.php @@ -0,0 +1,135 @@ +loadSchema(); + + $this->subscriberCsvImportManager = self::getContainer()->get(SubscriberCsvImportManager::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()); + + $options = new SubscriberImportOptions(); + $result = $this->subscriberCsvImportManager->importFromCsv($uploadedFile, $options); + + $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 + ); + + $options = new SubscriberImportOptions(updateExisting: true); + $result = $this->subscriberCsvImportManager->importFromCsv($uploadedFile, $options); + + 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); + } +} diff --git a/tests/Integration/Security/AuthenticationTest.php b/tests/Integration/Security/AuthenticationTest.php index a46a6d93..8b1d2d0e 100644 --- a/tests/Integration/Security/AuthenticationTest.php +++ b/tests/Integration/Security/AuthenticationTest.php @@ -5,11 +5,11 @@ namespace PhpList\Core\Tests\Integration\Security; use Doctrine\ORM\Tools\SchemaTool; -use PhpList\Core\Domain\Model\Identity\Administrator; +use PhpList\Core\Domain\Identity\Model\Administrator; use PhpList\Core\Security\Authentication; use PhpList\Core\TestingSupport\Traits\DatabaseTestTrait; -use PhpList\Core\Tests\Integration\Domain\Repository\Fixtures\Identity\AdministratorFixture; -use PhpList\Core\Tests\Integration\Domain\Repository\Fixtures\Identity\AdministratorTokenWithAdministratorFixture; +use PhpList\Core\Tests\Integration\Domain\Identity\Fixtures\AdministratorFixture; +use PhpList\Core\Tests\Integration\Domain\Identity\Fixtures\AdministratorTokenWithAdministratorFixture; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Component\HttpFoundation\Request; diff --git a/tests/Unit/Domain/Repository/CursorPaginationTraitTest.php b/tests/Unit/Domain/Common/Repository/CursorPaginationTraitTest.php similarity index 93% rename from tests/Unit/Domain/Repository/CursorPaginationTraitTest.php rename to tests/Unit/Domain/Common/Repository/CursorPaginationTraitTest.php index 1639b633..137da779 100644 --- a/tests/Unit/Domain/Repository/CursorPaginationTraitTest.php +++ b/tests/Unit/Domain/Common/Repository/CursorPaginationTraitTest.php @@ -2,11 +2,11 @@ declare(strict_types=1); -namespace PhpList\Core\Tests\Unit\Domain\Repository; +namespace PhpList\Core\Tests\Unit\Domain\Common\Repository; use Doctrine\ORM\Query; use Doctrine\ORM\QueryBuilder; -use PhpList\Core\Domain\Filter\FilterRequestInterface; +use PhpList\Core\Domain\Common\Model\Filter\FilterRequestInterface; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use RuntimeException; diff --git a/tests/Unit/Domain/Repository/DummyRepository.php b/tests/Unit/Domain/Common/Repository/DummyRepository.php similarity index 78% rename from tests/Unit/Domain/Repository/DummyRepository.php rename to tests/Unit/Domain/Common/Repository/DummyRepository.php index e16b612e..7c06b537 100644 --- a/tests/Unit/Domain/Repository/DummyRepository.php +++ b/tests/Unit/Domain/Common/Repository/DummyRepository.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace PhpList\Core\Tests\Unit\Domain\Repository; +namespace PhpList\Core\Tests\Unit\Domain\Common\Repository; use Doctrine\ORM\QueryBuilder; -use PhpList\Core\Domain\Repository\CursorPaginationTrait; +use PhpList\Core\Domain\Common\Repository\CursorPaginationTrait; /** * Dummy repository that uses the trait 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/Model/Identity/AdministratorTest.php b/tests/Unit/Domain/Identity/Model/AdministratorTest.php similarity index 95% rename from tests/Unit/Domain/Model/Identity/AdministratorTest.php rename to tests/Unit/Domain/Identity/Model/AdministratorTest.php index e5565c64..10508b26 100644 --- a/tests/Unit/Domain/Model/Identity/AdministratorTest.php +++ b/tests/Unit/Domain/Identity/Model/AdministratorTest.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace PhpList\Core\Tests\Unit\Domain\Model\Identity; +namespace PhpList\Core\Tests\Unit\Domain\Identity\Model; -use PhpList\Core\Domain\Model\Identity\Administrator; -use PhpList\Core\Domain\Model\Interfaces\DomainModel; +use PhpList\Core\Domain\Common\Model\Interfaces\DomainModel; +use PhpList\Core\Domain\Identity\Model\Administrator; use PhpList\Core\TestingSupport\Traits\ModelTestTrait; use PhpList\Core\TestingSupport\Traits\SimilarDatesAssertionTrait; use PHPUnit\Framework\TestCase; diff --git a/tests/Unit/Domain/Model/Identity/AdministratorTokenTest.php b/tests/Unit/Domain/Identity/Model/AdministratorTokenTest.php similarity index 92% rename from tests/Unit/Domain/Model/Identity/AdministratorTokenTest.php rename to tests/Unit/Domain/Identity/Model/AdministratorTokenTest.php index 5318782f..da824845 100644 --- a/tests/Unit/Domain/Model/Identity/AdministratorTokenTest.php +++ b/tests/Unit/Domain/Identity/Model/AdministratorTokenTest.php @@ -2,12 +2,12 @@ declare(strict_types=1); -namespace PhpList\Core\Tests\Unit\Domain\Model\Identity; +namespace PhpList\Core\Tests\Unit\Domain\Identity\Model; use DateTime; -use PhpList\Core\Domain\Model\Identity\Administrator; -use PhpList\Core\Domain\Model\Identity\AdministratorToken; -use PhpList\Core\Domain\Model\Interfaces\DomainModel; +use PhpList\Core\Domain\Common\Model\Interfaces\DomainModel; +use PhpList\Core\Domain\Identity\Model\Administrator; +use PhpList\Core\Domain\Identity\Model\AdministratorToken; use PhpList\Core\TestingSupport\Traits\ModelTestTrait; use PhpList\Core\TestingSupport\Traits\SimilarDatesAssertionTrait; use PHPUnit\Framework\TestCase; 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/Repository/Identity/AdministratorRepositoryTest.php b/tests/Unit/Domain/Identity/Rpository/AdministratorRepositoryTest.php similarity index 76% rename from tests/Unit/Domain/Repository/Identity/AdministratorRepositoryTest.php rename to tests/Unit/Domain/Identity/Rpository/AdministratorRepositoryTest.php index 9acce7cf..d29282d2 100644 --- a/tests/Unit/Domain/Repository/Identity/AdministratorRepositoryTest.php +++ b/tests/Unit/Domain/Identity/Rpository/AdministratorRepositoryTest.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\AdministratorRepository; +use PhpList\Core\Domain\Identity\Model\Administrator; +use PhpList\Core\Domain\Identity\Repository\AdministratorRepository; 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\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/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); + } +} diff --git a/tests/Unit/Domain/Identity/Service/AdministratorManagerTest.php b/tests/Unit/Domain/Identity/Service/AdministratorManagerTest.php new file mode 100644 index 00000000..11c3f378 --- /dev/null +++ b/tests/Unit/Domain/Identity/Service/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/Identity/Service/SessionManagerTest.php b/tests/Unit/Domain/Identity/Service/SessionManagerTest.php new file mode 100644 index 00000000..44072452 --- /dev/null +++ b/tests/Unit/Domain/Identity/Service/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/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/Messaging/Service/Builder/MessageBuilderTest.php b/tests/Unit/Domain/Messaging/Service/Builder/MessageBuilderTest.php new file mode 100644 index 00000000..564bd34d --- /dev/null +++ b/tests/Unit/Domain/Messaging/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(\PhpList\Core\Domain\Messaging\Model\Message\MessageSchedule::class)); + + $this->contentBuilder->expects($this->once()) + ->method('build') + ->with($createMessageDto->content) + ->willReturn($this->createMock(\PhpList\Core\Domain\Messaging\Model\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(\PhpList\Core\Domain\Messaging\Model\Message\MessageSchedule::class)); + $existingMessage + ->expects($this->once()) + ->method('setContent') + ->with($this->isInstanceOf(\PhpList\Core\Domain\Messaging\Model\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/Messaging/Service/Builder/MessageContentBuilderTest.php b/tests/Unit/Domain/Messaging/Service/Builder/MessageContentBuilderTest.php new file mode 100644 index 00000000..2b1aa771 --- /dev/null +++ b/tests/Unit/Domain/Messaging/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/Messaging/Service/Builder/MessageFormatBuilderTest.php b/tests/Unit/Domain/Messaging/Service/Builder/MessageFormatBuilderTest.php new file mode 100644 index 00000000..8d9320a0 --- /dev/null +++ b/tests/Unit/Domain/Messaging/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/Messaging/Service/Builder/MessageOptionsBuilderTest.php b/tests/Unit/Domain/Messaging/Service/Builder/MessageOptionsBuilderTest.php new file mode 100644 index 00000000..c6795d29 --- /dev/null +++ b/tests/Unit/Domain/Messaging/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/Messaging/Service/Builder/MessageScheduleBuilderTest.php b/tests/Unit/Domain/Messaging/Service/Builder/MessageScheduleBuilderTest.php new file mode 100644 index 00000000..38f04338 --- /dev/null +++ b/tests/Unit/Domain/Messaging/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/Messaging/Service/MessageManagerTest.php b/tests/Unit/Domain/Messaging/Service/MessageManagerTest.php new file mode 100644 index 00000000..8ee85915 --- /dev/null +++ b/tests/Unit/Domain/Messaging/Service/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(\PhpList\Core\Domain\Messaging\Model\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(\PhpList\Core\Domain\Messaging\Model\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/Messaging/Service/TemplateImageManagerTest.php b/tests/Unit/Domain/Messaging/Service/TemplateImageManagerTest.php new file mode 100644 index 00000000..bde3569a --- /dev/null +++ b/tests/Unit/Domain/Messaging/Service/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/Messaging/Service/TemplateManagerTest.php b/tests/Unit/Domain/Messaging/Service/TemplateManagerTest.php new file mode 100644 index 00000000..fbbb4831 --- /dev/null +++ b/tests/Unit/Domain/Messaging/Service/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/Messaging/Validator/TemplateImageValidatorTest.php b/tests/Unit/Domain/Messaging/Validator/TemplateImageValidatorTest.php new file mode 100644 index 00000000..88af2c8c --- /dev/null +++ b/tests/Unit/Domain/Messaging/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/Messaging/Validator/TemplateLinkValidatorTest.php b/tests/Unit/Domain/Messaging/Validator/TemplateLinkValidatorTest.php new file mode 100644 index 00000000..d0ab6566 --- /dev/null +++ b/tests/Unit/Domain/Messaging/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); + } +} 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/Subscription/Service/AttributeDefinitionManagerTest.php b/tests/Unit/Domain/Subscription/Service/AttributeDefinitionManagerTest.php new file mode 100644 index 00000000..3c650eb7 --- /dev/null +++ b/tests/Unit/Domain/Subscription/Service/AttributeDefinitionManagerTest.php @@ -0,0 +1,151 @@ +createMock(SubscriberAttributeDefinitionRepository::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(SubscriberAttributeDefinition::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(SubscriberAttributeDefinitionRepository::class); + $manager = new AttributeDefinitionManager($repository); + + $dto = new AttributeDefinitionDto( + name: 'Country', + type: 'checkbox', + listOrder: 1, + defaultValue: 'US', + required: true, + tableName: 'user_attribute' + ); + + $existing = $this->createMock(SubscriberAttributeDefinition::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(SubscriberAttributeDefinitionRepository::class); + $manager = new AttributeDefinitionManager($repository); + + $attribute = new SubscriberAttributeDefinition(); + $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(SubscriberAttributeDefinitionRepository::class); + $manager = new AttributeDefinitionManager($repository); + + $dto = new AttributeDefinitionDto( + name: 'Existing', + type: 'text', + listOrder: 5, + defaultValue: 'Canada', + required: false, + tableName: 'custom_attrs' + ); + + $current = new SubscriberAttributeDefinition(); + $current->setName('Old'); + + $other = $this->createMock(SubscriberAttributeDefinition::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(SubscriberAttributeDefinitionRepository::class); + $manager = new AttributeDefinitionManager($repository); + + $attribute = new SubscriberAttributeDefinition(); + + $repository->expects($this->once())->method('remove')->with($attribute); + + $manager->delete($attribute); + + $this->assertTrue(true); + } +} diff --git a/tests/Unit/Domain/Subscription/Service/SubscriberAttributeManagerTest.php b/tests/Unit/Domain/Subscription/Service/SubscriberAttributeManagerTest.php new file mode 100644 index 00000000..84682ca2 --- /dev/null +++ b/tests/Unit/Domain/Subscription/Service/SubscriberAttributeManagerTest.php @@ -0,0 +1,111 @@ +createMock(SubscriberAttributeValueRepository::class); + + $subscriberAttrRepo->expects(self::once()) + ->method('findOneBySubscriberAndAttribute') + ->with($subscriber, $definition) + ->willReturn(null); + + $subscriberAttrRepo->expects(self::once()) + ->method('save') + ->with(self::callback(function (SubscriberAttributeValue $attr) { + return $attr->getValue() === 'US'; + })); + + $manager = new SubscriberAttributeManager($subscriberAttrRepo); + $attribute = $manager->createOrUpdate($subscriber, $definition, 'US'); + + self::assertInstanceOf(SubscriberAttributeValue::class, $attribute); + self::assertSame('US', $attribute->getValue()); + } + + public function testUpdateExistingSubscriberAttribute(): void + { + $subscriber = new Subscriber(); + $definition = new SubscriberAttributeDefinition(); + $existing = new SubscriberAttributeValue($definition, $subscriber); + $existing->setValue('Old'); + + $subscriberAttrRepo = $this->createMock(SubscriberAttributeValueRepository::class); + $subscriberAttrRepo->expects(self::once()) + ->method('findOneBySubscriberAndAttribute') + ->with($subscriber, $definition) + ->willReturn($existing); + + $subscriberAttrRepo->expects(self::once()) + ->method('save') + ->with($existing); + + $manager = new SubscriberAttributeManager($subscriberAttrRepo); + $result = $manager->createOrUpdate($subscriber, $definition, 'Updated'); + + self::assertSame('Updated', $result->getValue()); + } + + public function testCreateFailsWhenValueAndDefaultAreNull(): void + { + $subscriber = new Subscriber(); + $definition = new SubscriberAttributeDefinition(); + + $subscriberAttrRepo = $this->createMock(SubscriberAttributeValueRepository::class); + $subscriberAttrRepo->method('findOneBySubscriberAndAttribute')->willReturn(null); + + $manager = new SubscriberAttributeManager($subscriberAttrRepo); + + $this->expectException(SubscriberAttributeCreationException::class); + $this->expectExceptionMessage('Value is required'); + + $manager->createOrUpdate($subscriber, $definition, null); + } + + public function testGetSubscriberAttribute(): void + { + $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($subscriberAttrRepo); + $result = $manager->getSubscriberAttribute(5, 10); + + self::assertSame($expected, $result); + } + + public function testDeleteSubscriberAttribute(): void + { + $subscriberAttrRepo = $this->createMock(SubscriberAttributeValueRepository::class); + $attribute = $this->createMock(SubscriberAttributeValue::class); + + $subscriberAttrRepo->expects(self::once()) + ->method('remove') + ->with($attribute); + + $manager = new SubscriberAttributeManager($subscriberAttrRepo); + $manager->delete($attribute); + + self::assertTrue(true); + } +} diff --git a/tests/Unit/Domain/Subscription/Service/SubscriberCsvExportManagerTest.php b/tests/Unit/Domain/Subscription/Service/SubscriberCsvExportManagerTest.php new file mode 100644 index 00000000..edbb5f63 --- /dev/null +++ b/tests/Unit/Domain/Subscription/Service/SubscriberCsvExportManagerTest.php @@ -0,0 +1,148 @@ +attributeManagerMock = $this->createMock(SubscriberAttributeManager::class); + $this->subscriberRepositoryMock = $this->createMock(SubscriberRepository::class); + $this->attributeDefinitionRepositoryMock = $this->createMock(SubscriberAttributeDefinitionRepository::class); + + $this->subject = new SubscriberCsvExportManager( + $this->attributeManagerMock, + $this->subscriberRepositoryMock, + $this->attributeDefinitionRepositoryMock + ); + } + + 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') + ); + } +} 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); + } +} diff --git a/tests/Unit/Domain/Subscription/Service/SubscriberListManagerTest.php b/tests/Unit/Domain/Subscription/Service/SubscriberListManagerTest.php new file mode 100644 index 00000000..2f8e15fd --- /dev/null +++ b/tests/Unit/Domain/Subscription/Service/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/Subscription/Service/SubscriberManagerTest.php b/tests/Unit/Domain/Subscription/Service/SubscriberManagerTest.php new file mode 100644 index 00000000..d243f4a3 --- /dev/null +++ b/tests/Unit/Domain/Subscription/Service/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/Subscription/Service/SubscriptionManagerTest.php b/tests/Unit/Domain/Subscription/Service/SubscriptionManagerTest.php new file mode 100644 index 00000000..1abbf38d --- /dev/null +++ b/tests/Unit/Domain/Subscription/Service/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/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;