diff --git a/composer.json b/composer.json index fcb02e3e..9b97646f 100644 --- a/composer.json +++ b/composer.json @@ -48,6 +48,7 @@ "ext-gettext": "*", "ext-json": "*", "dotkernel/dot-authorization": "^3.4.1", + "dotkernel/dot-cache": "^4.1", "dotkernel/dot-controller": "^3.4.3", "dotkernel/dot-data-fixtures": "^1.1.3", "dotkernel/dot-dependency-injection": "^1.0.0", @@ -93,7 +94,6 @@ "autoload-dev": { "psr-4": { "FrontendTest\\Common\\": "test/Common", - "FrontendTest\\Functional\\": "test/Functional/", "FrontendTest\\Unit\\": "test/Unit" } }, diff --git a/config/autoload/authentication.global.php b/config/autoload/authentication.global.php index 041afd13..e637f772 100644 --- a/config/autoload/authentication.global.php +++ b/config/autoload/authentication.global.php @@ -4,12 +4,13 @@ use Frontend\App\Common\Message; use Frontend\User\Entity\User; +use Frontend\User\Enum\UserStatusEnum; return [ 'doctrine' => [ 'authentication' => [ 'orm_default' => [ - 'object_manager' => 'doctrine.entitymanager.orm_default', + 'object_manager' => 'doctrine.entity_manager.orm_default', 'identity_class' => User::class, 'identity_property' => 'identity', 'credential_property' => 'password', @@ -20,7 +21,7 @@ ], 'options' => [ 'status' => [ - 'value' => User::STATUS_ACTIVE, + 'value' => UserStatusEnum::Active, 'message' => Message::USER_NOT_ACTIVATED, ], 'isDeleted' => [ diff --git a/config/autoload/doctrine.global.php b/config/autoload/doctrine.global.php index 48eda1e7..51c4b0ce 100644 --- a/config/autoload/doctrine.global.php +++ b/config/autoload/doctrine.global.php @@ -2,30 +2,43 @@ declare(strict_types=1); -use Doctrine\ORM\EntityManager; -use Doctrine\ORM\EntityManagerInterface; use Doctrine\Persistence\Mapping\Driver\MappingDriverChain; +use Dot\Cache\Adapter\ArrayAdapter; +use Dot\Cache\Adapter\FilesystemAdapter; use Frontend\App\Resolver\EntityListenerResolver; +use Frontend\User\DBAL\Types\UserResetPasswordStatusEnumType; +use Frontend\User\DBAL\Types\UserStatusEnumType; use Ramsey\Uuid\Doctrine\UuidBinaryOrderedTimeType; use Ramsey\Uuid\Doctrine\UuidBinaryType; use Ramsey\Uuid\Doctrine\UuidType; -use Roave\PsrContainerDoctrine\EntityManagerFactory; return [ - 'dependencies' => [ - 'factories' => [ - 'doctrine.entity_manager.orm_default' => EntityManagerFactory::class, - ], - 'aliases' => [ - EntityManager::class => 'doctrine.entity_manager.orm_default', - EntityManagerInterface::class => 'doctrine.entity_manager.orm_default', - 'doctrine.entitymanager.orm_default' => 'doctrine.entity_manager.orm_default', - ], - ], 'doctrine' => [ + 'cache' => [ + 'array' => [ + 'class' => ArrayAdapter::class, + ], + 'filesystem' => [ + 'class' => FilesystemAdapter::class, + 'directory' => getcwd() . '/data/cache', + 'namespace' => 'doctrine', + ], + ], 'configuration' => [ 'orm_default' => [ 'entity_listener_resolver' => EntityListenerResolver::class, + 'result_cache' => 'filesystem', + 'metadata_cache' => 'filesystem', + 'query_cache' => 'filesystem', + 'hydration_cache' => 'array', + 'typed_field_mapper' => null, + 'second_level_cache' => [ + 'enabled' => true, + 'default_lifetime' => 3600, + 'default_lock_lifetime' => 60, + 'file_lock_region_directory' => '', + 'regions' => [], + ], ], ], 'connection' => [ @@ -45,9 +58,11 @@ ], ], 'types' => [ - UuidType::NAME => UuidType::class, - UuidBinaryType::NAME => UuidBinaryType::class, - UuidBinaryOrderedTimeType::NAME => UuidBinaryOrderedTimeType::class, + UuidType::NAME => UuidType::class, + UuidBinaryType::NAME => UuidBinaryType::class, + UuidBinaryOrderedTimeType::NAME => UuidBinaryOrderedTimeType::class, + UserStatusEnumType::NAME => UserStatusEnumType::class, + UserResetPasswordStatusEnumType::NAME => UserResetPasswordStatusEnumType::class, ], 'fixtures' => getcwd() . '/data/doctrine/fixtures', ], diff --git a/config/cli-config.php b/config/cli-config.php index c39570bd..7385bed1 100644 --- a/config/cli-config.php +++ b/config/cli-config.php @@ -13,7 +13,4 @@ $entityManager = $container->get(EntityManager::class); -// register enum type for doctrine -$entityManager->getConnection()->getDatabasePlatform()->registerDoctrineTypeMapping('enum', 'string'); - return DependencyFactory::fromEntityManager($config, new ExistingEntityManager($entityManager)); diff --git a/config/config.php b/config/config.php index 3d872b3a..1640febd 100644 --- a/config/config.php +++ b/config/config.php @@ -44,6 +44,7 @@ class_exists(\Mezzio\Swoole\ConfigProvider::class) \Dot\Rbac\Guard\ConfigProvider::class, \Dot\ResponseHeader\ConfigProvider::class, \Dot\DataFixtures\ConfigProvider::class, + \Dot\Cache\ConfigProvider::class, // Default App module config \Frontend\App\ConfigProvider::class, diff --git a/data/doctrine/migrations/Version20240806123413.php b/data/doctrine/migrations/Version20241120160406.php similarity index 86% rename from data/doctrine/migrations/Version20240806123413.php rename to data/doctrine/migrations/Version20241120160406.php index d50bae81..89697f69 100644 --- a/data/doctrine/migrations/Version20240806123413.php +++ b/data/doctrine/migrations/Version20241120160406.php @@ -10,7 +10,7 @@ /** * Auto-generated Migration: Please modify to your needs! */ -final class Version20240806123413 extends AbstractMigration +final class Version20241120160406 extends AbstractMigration { public function getDescription(): string { @@ -21,12 +21,12 @@ public function up(Schema $schema): void { // this up() migration is auto-generated, please modify it to your needs $this->addSql('CREATE TABLE contact_message (uuid BINARY(16) NOT NULL, email VARCHAR(150) NOT NULL, name VARCHAR(150) NOT NULL, subject LONGTEXT NOT NULL, message LONGTEXT NOT NULL, platform LONGTEXT NOT NULL, created DATETIME NOT NULL, updated DATETIME DEFAULT NULL, PRIMARY KEY(uuid)) DEFAULT CHARACTER SET utf8mb4'); - $this->addSql('CREATE TABLE user (uuid BINARY(16) NOT NULL, identity VARCHAR(191) NOT NULL, password VARCHAR(191) NOT NULL, status ENUM(\'pending\', \'active\'), isDeleted TINYINT(1) NOT NULL, hash VARCHAR(64) NOT NULL, created DATETIME NOT NULL, updated DATETIME DEFAULT NULL, UNIQUE INDEX UNIQ_8D93D6496A95E9C4 (identity), UNIQUE INDEX UNIQ_8D93D649D1B862B8 (hash), PRIMARY KEY(uuid)) DEFAULT CHARACTER SET utf8mb4'); + $this->addSql('CREATE TABLE user (uuid BINARY(16) NOT NULL, identity VARCHAR(191) NOT NULL, password VARCHAR(191) NOT NULL, status ENUM(\'active\', \'pending\') DEFAULT \'pending\' NOT NULL, isDeleted TINYINT(1) NOT NULL, hash VARCHAR(64) NOT NULL, created DATETIME NOT NULL, updated DATETIME DEFAULT NULL, UNIQUE INDEX UNIQ_8D93D6496A95E9C4 (identity), UNIQUE INDEX UNIQ_8D93D649D1B862B8 (hash), PRIMARY KEY(uuid)) DEFAULT CHARACTER SET utf8mb4'); $this->addSql('CREATE TABLE user_roles (userUuid BINARY(16) NOT NULL, roleUuid BINARY(16) NOT NULL, INDEX IDX_54FCD59FD73087E9 (userUuid), INDEX IDX_54FCD59F88446210 (roleUuid), PRIMARY KEY(userUuid, roleUuid)) DEFAULT CHARACTER SET utf8mb4'); $this->addSql('CREATE TABLE user_avatar (uuid BINARY(16) NOT NULL, name VARCHAR(191) NOT NULL, created DATETIME NOT NULL, updated DATETIME DEFAULT NULL, userUuid BINARY(16) NOT NULL, UNIQUE INDEX UNIQ_73256912D73087E9 (userUuid), PRIMARY KEY(uuid)) DEFAULT CHARACTER SET utf8mb4'); $this->addSql('CREATE TABLE user_detail (uuid BINARY(16) NOT NULL, firstName VARCHAR(191) DEFAULT NULL, lastName VARCHAR(191) DEFAULT NULL, created DATETIME NOT NULL, updated DATETIME DEFAULT NULL, userUuid BINARY(16) NOT NULL, UNIQUE INDEX UNIQ_4B5464AED73087E9 (userUuid), PRIMARY KEY(uuid)) DEFAULT CHARACTER SET utf8mb4'); $this->addSql('CREATE TABLE user_remember_me (uuid BINARY(16) NOT NULL, rememberMeToken VARCHAR(100) NOT NULL, userAgent LONGTEXT NOT NULL, expireDate DATETIME NOT NULL, created DATETIME NOT NULL, updated DATETIME DEFAULT NULL, userUuid BINARY(16) NOT NULL, UNIQUE INDEX UNIQ_D3E96EBD1BBB86A0 (rememberMeToken), INDEX IDX_D3E96EBDD73087E9 (userUuid), PRIMARY KEY(uuid)) DEFAULT CHARACTER SET utf8mb4'); - $this->addSql('CREATE TABLE user_reset_password (uuid BINARY(16) NOT NULL, expires DATETIME NOT NULL, hash VARCHAR(64) NOT NULL, status VARCHAR(20) NOT NULL, created DATETIME NOT NULL, updated DATETIME DEFAULT NULL, userUuid BINARY(16) NOT NULL, UNIQUE INDEX UNIQ_D21DE3BCD1B862B8 (hash), INDEX IDX_D21DE3BCD73087E9 (userUuid), PRIMARY KEY(uuid)) DEFAULT CHARACTER SET utf8mb4'); + $this->addSql('CREATE TABLE user_reset_password (uuid BINARY(16) NOT NULL, expires DATETIME NOT NULL, hash VARCHAR(64) NOT NULL, status ENUM(\'completed\', \'requested\') DEFAULT \'requested\' NOT NULL, created DATETIME NOT NULL, updated DATETIME DEFAULT NULL, userUuid BINARY(16) NOT NULL, UNIQUE INDEX UNIQ_D21DE3BCD1B862B8 (hash), INDEX IDX_D21DE3BCD73087E9 (userUuid), PRIMARY KEY(uuid)) DEFAULT CHARACTER SET utf8mb4'); $this->addSql('CREATE TABLE user_role (uuid BINARY(16) NOT NULL, name VARCHAR(30) NOT NULL, created DATETIME NOT NULL, updated DATETIME DEFAULT NULL, UNIQUE INDEX UNIQ_2DE8C6A35E237E06 (name), PRIMARY KEY(uuid)) DEFAULT CHARACTER SET utf8mb4'); $this->addSql('ALTER TABLE user_roles ADD CONSTRAINT FK_54FCD59FD73087E9 FOREIGN KEY (userUuid) REFERENCES user (uuid)'); $this->addSql('ALTER TABLE user_roles ADD CONSTRAINT FK_54FCD59F88446210 FOREIGN KEY (roleUuid) REFERENCES user_role (uuid)'); diff --git a/src/App/src/DBAL/Types/AbstractEnumType.php b/src/App/src/DBAL/Types/AbstractEnumType.php new file mode 100644 index 00000000..d6517774 --- /dev/null +++ b/src/App/src/DBAL/Types/AbstractEnumType.php @@ -0,0 +1,67 @@ + "'{$case->value}'", $this->getEnumValues()); + + return sprintf('ENUM(%s)', implode(', ', $values)); + } + + public function convertToPHPValue(mixed $value, AbstractPlatform $platform): mixed + { + if ($value === null) { + return null; + } + + return $this->getEnumClass()::from($value); + } + + public function convertToDatabaseValue(mixed $value, AbstractPlatform $platform): mixed + { + if ($value === null) { + return null; + } + + if (! $value instanceof BackedEnum) { + throw new InvalidArgumentException(sprintf( + 'Expected instance of %s, got %s', + $this->getEnumClass(), + is_object($value) ? $value::class : gettype($value) + )); + } + + return $value->value; + } + + /** + * @return class-string + */ + abstract protected function getEnumClass(): string; + + private function getEnumValues(): array + { + return $this->getEnumClass()::cases(); + } +} diff --git a/src/User/src/Controller/AccountController.php b/src/User/src/Controller/AccountController.php index b8e3ee32..3bf88ee4 100644 --- a/src/User/src/Controller/AccountController.php +++ b/src/User/src/Controller/AccountController.php @@ -66,7 +66,7 @@ public function activateAction(): ResponseInterface return new RedirectResponse($this->router->generateUri('user', ['action' => 'login'])); } - if ($user->getStatus() === User::STATUS_ACTIVE) { + if ($user->isActive()) { $this->messenger->addError(Message::USER_ALREADY_ACTIVATED, 'user-login'); return new RedirectResponse($this->router->generateUri('user', ['action' => 'login'])); } @@ -101,7 +101,7 @@ public function unregisterAction(): ResponseInterface return new RedirectResponse($this->router->generateUri('user', ['action' => 'login'])); } - if ($user->getStatus() !== User::STATUS_PENDING) { + if (! $user->isPending()) { $this->messenger->addError(Message::USER_UNREGISTER_STATUS, 'user-login'); return new RedirectResponse($this->router->generateUri('user', ['action' => 'login'])); } diff --git a/src/User/src/DBAL/Types/UserResetPasswordStatusEnumType.php b/src/User/src/DBAL/Types/UserResetPasswordStatusEnumType.php new file mode 100644 index 00000000..b5344afa --- /dev/null +++ b/src/User/src/DBAL/Types/UserResetPasswordStatusEnumType.php @@ -0,0 +1,23 @@ + UserStatusEnum::Pending])] + protected UserStatusEnum $status = UserStatusEnum::Pending; #[ORM\Column(name: 'isDeleted', type: 'boolean')] protected bool $isDeleted = self::IS_DELETED_NO; @@ -64,8 +58,8 @@ class User extends AbstractEntity implements UserInterface protected Collection $roles; #[ORM\OneToMany( - mappedBy: 'user', targetEntity: UserResetPassword::class, + mappedBy: 'user', cascade: ['persist', 'remove'], fetch: 'EXTRA_LAZY' )] @@ -132,12 +126,12 @@ public function setPassword(string $password): self return $this; } - public function getStatus(): string + public function getStatus(): UserStatusEnum { return $this->status; } - public function setStatus(string $status): self + public function setStatus(UserStatusEnum $status): self { $this->status = $status; @@ -211,7 +205,12 @@ public static function generateHash(): string public function isActive(): bool { - return $this->status === self::STATUS_ACTIVE; + return $this->status === UserStatusEnum::Active; + } + + public function isPending(): bool + { + return $this->status === UserStatusEnum::Pending; } public function markAsDeleted(): self @@ -228,7 +227,7 @@ public function getName(): string public function activate(): self { - return $this->setStatus(self::STATUS_ACTIVE); + return $this->setStatus(UserStatusEnum::Active); } public function resetRoles(): self diff --git a/src/User/src/Entity/UserAvatar.php b/src/User/src/Entity/UserAvatar.php index c89cf4c0..fc788628 100644 --- a/src/User/src/Entity/UserAvatar.php +++ b/src/User/src/Entity/UserAvatar.php @@ -18,7 +18,7 @@ class UserAvatar extends AbstractEntity { use TimestampsTrait; - #[ORM\OneToOne(inversedBy: 'avatar', targetEntity: User::class)] + #[ORM\OneToOne(targetEntity: User::class, inversedBy: 'avatar')] #[ORM\JoinColumn(name: 'userUuid', referencedColumnName: 'uuid', nullable: false)] protected UserInterface $user; diff --git a/src/User/src/Entity/UserDetail.php b/src/User/src/Entity/UserDetail.php index b8b61cd6..c747ec6d 100644 --- a/src/User/src/Entity/UserDetail.php +++ b/src/User/src/Entity/UserDetail.php @@ -16,7 +16,7 @@ class UserDetail extends AbstractEntity { use TimestampsTrait; - #[ORM\OneToOne(inversedBy: 'detail', targetEntity: User::class)] + #[ORM\OneToOne(targetEntity: User::class, inversedBy: 'detail')] #[ORM\JoinColumn(name: 'userUuid', referencedColumnName: 'uuid', nullable: false)] protected UserInterface $user; diff --git a/src/User/src/Entity/UserInterface.php b/src/User/src/Entity/UserInterface.php index 7c7ae985..892a2914 100644 --- a/src/User/src/Entity/UserInterface.php +++ b/src/User/src/Entity/UserInterface.php @@ -5,6 +5,7 @@ namespace Frontend\User\Entity; use Doctrine\Common\Collections\Collection; +use Frontend\User\Enum\UserStatusEnum; use Ramsey\Uuid\UuidInterface; interface UserInterface @@ -27,9 +28,9 @@ public function getPassword(): string; public function setPassword(string $password): UserInterface; - public function getStatus(): string; + public function getStatus(): UserStatusEnum; - public function setStatus(string $status): UserInterface; + public function setStatus(UserStatusEnum $status): UserInterface; public function getRoles(): Collection; diff --git a/src/User/src/Entity/UserResetPassword.php b/src/User/src/Entity/UserResetPassword.php index 684cc94a..918aa575 100644 --- a/src/User/src/Entity/UserResetPassword.php +++ b/src/User/src/Entity/UserResetPassword.php @@ -11,6 +11,7 @@ use Exception; use Frontend\App\Entity\AbstractEntity; use Frontend\App\Entity\TimestampsTrait; +use Frontend\User\Enum\UserResetPasswordStatusEnum; #[ORM\Entity] #[ORM\Table(name: 'user_reset_password')] @@ -19,13 +20,6 @@ class UserResetPassword extends AbstractEntity { use TimestampsTrait; - public const STATUS_COMPLETED = 'completed'; - public const STATUS_REQUESTED = 'requested'; - public const STATUSES = [ - self::STATUS_COMPLETED, - self::STATUS_REQUESTED, - ]; - #[ORM\ManyToOne(targetEntity: User::class, cascade: ['persist', 'remove'], inversedBy: 'resetPasswords')] #[ORM\JoinColumn(name: 'userUuid', referencedColumnName: 'uuid', nullable: false)] protected User $user; @@ -36,8 +30,11 @@ class UserResetPassword extends AbstractEntity #[ORM\Column(name: 'hash', type: 'string', length: 64, unique: true, nullable: false)] protected string $hash; - #[ORM\Column(name: 'status', type: 'string', length: 20, nullable: false)] - protected string $status = self::STATUS_REQUESTED; + #[ORM\Column( + type: 'user_reset_password_status_enum', + options: ['default' => UserResetPasswordStatusEnum::Requested], + )] + protected UserResetPasswordStatusEnum $status = UserResetPasswordStatusEnum::Requested; public function __construct() { @@ -84,12 +81,12 @@ public function setHash(string $hash): self return $this; } - public function getStatus(): string + public function getStatus(): UserResetPasswordStatusEnum { return $this->status; } - public function setStatus(string $status): self + public function setStatus(UserResetPasswordStatusEnum $status): self { $this->status = $status; @@ -98,7 +95,7 @@ public function setStatus(string $status): self public function isCompleted(): bool { - return $this->getStatus() === self::STATUS_COMPLETED; + return $this->getStatus() === UserResetPasswordStatusEnum::Completed; } public function isValid(): bool @@ -113,7 +110,7 @@ public function isValid(): bool public function markAsCompleted(): self { - $this->status = self::STATUS_COMPLETED; + $this->status = UserResetPasswordStatusEnum::Completed; return $this; } diff --git a/src/User/src/Enum/UserResetPasswordStatusEnum.php b/src/User/src/Enum/UserResetPasswordStatusEnum.php new file mode 100644 index 00000000..5c702414 --- /dev/null +++ b/src/User/src/Enum/UserResetPasswordStatusEnum.php @@ -0,0 +1,11 @@ +setDetail($detail) ->setIdentity($data['email']) ->setPassword(password_hash($data['password'], PASSWORD_DEFAULT)) - ->setStatus($data['status'] ?? User::STATUS_PENDING); + ->setStatus($data['status'] ?? UserStatusEnum::Pending); $detail->setUser($user); diff --git a/test/Unit/User/Adapter/AuthenticationAdapterTest.php b/test/Unit/User/Adapter/AuthenticationAdapterTest.php index 53871f75..7411ddd3 100644 --- a/test/Unit/User/Adapter/AuthenticationAdapterTest.php +++ b/test/Unit/User/Adapter/AuthenticationAdapterTest.php @@ -10,6 +10,7 @@ use Frontend\User\Entity\User; use Frontend\User\Entity\UserDetail; use Frontend\User\Entity\UserRole; +use Frontend\User\Enum\UserStatusEnum; use Frontend\User\Exception\AuthenticationAdapterException; use Laminas\Authentication\Adapter\AdapterInterface; use Laminas\Authentication\Adapter\Exception\ExceptionInterface; @@ -314,7 +315,7 @@ public function testExtraAuthenticationOptionsInvalidMessageProvided(): void [ 'options' => [ 'status' => [ - 'value' => User::STATUS_ACTIVE, + 'value' => UserStatusEnum::Active, 'message' => '', ], ], @@ -338,8 +339,7 @@ public function testExtraAuthenticationOptionsNonMatchingValueProvided(): void { $class = (new User()) ->setIdentity('test@dotkernel.com') - ->setPassword(password_hash('password', PASSWORD_DEFAULT)) - ->setStatus('invalid_status'); + ->setPassword(password_hash('password', PASSWORD_DEFAULT)); $repository = $this->createMock(EntityRepository::class); @@ -350,7 +350,7 @@ public function testExtraAuthenticationOptionsNonMatchingValueProvided(): void [ 'options' => [ 'status' => [ - 'value' => User::STATUS_ACTIVE, + 'value' => UserStatusEnum::Active, 'message' => 'test message', ], ],