Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue #507: Implemented enums in database #508

Merged
merged 3 commits into from
Nov 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -93,7 +94,6 @@
"autoload-dev": {
"psr-4": {
"FrontendTest\\Common\\": "test/Common",
"FrontendTest\\Functional\\": "test/Functional/",
"FrontendTest\\Unit\\": "test/Unit"
}
},
Expand Down
5 changes: 3 additions & 2 deletions config/autoload/authentication.global.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -20,7 +21,7 @@
],
'options' => [
'status' => [
'value' => User::STATUS_ACTIVE,
'value' => UserStatusEnum::Active,
'message' => Message::USER_NOT_ACTIVATED,
],
'isDeleted' => [
Expand Down
47 changes: 31 additions & 16 deletions config/autoload/doctrine.global.php
Original file line number Diff line number Diff line change
Expand Up @@ -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' => [
Expand All @@ -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',
],
Expand Down
3 changes: 0 additions & 3 deletions config/cli-config.php
Original file line number Diff line number Diff line change
Expand Up @@ -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));
1 change: 1 addition & 0 deletions config/config.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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)');
Expand Down
67 changes: 67 additions & 0 deletions src/App/src/DBAL/Types/AbstractEnumType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

declare(strict_types=1);

namespace Frontend\App\DBAL\Types;

use BackedEnum;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Platforms\SQLitePlatform;
use Doctrine\DBAL\Types\Type;
use InvalidArgumentException;

use function array_map;
use function gettype;
use function implode;
use function is_object;
use function sprintf;

abstract class AbstractEnumType extends Type
{
public function getSQLDeclaration(array $column, AbstractPlatform $platform): string
{
if ($platform instanceof SQLitePlatform) {
return 'TEXT';
}

$values = array_map(fn($case) => "'{$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();
}
}
4 changes: 2 additions & 2 deletions src/User/src/Controller/AccountController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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']));
}
Expand Down Expand Up @@ -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']));
}
Expand Down
23 changes: 23 additions & 0 deletions src/User/src/DBAL/Types/UserResetPasswordStatusEnumType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace Frontend\User\DBAL\Types;

use Frontend\App\DBAL\Types\AbstractEnumType;
use Frontend\User\Enum\UserResetPasswordStatusEnum;

class UserResetPasswordStatusEnumType extends AbstractEnumType
{
public const NAME = 'user_reset_password_status_enum';

protected function getEnumClass(): string
{
return UserResetPasswordStatusEnum::class;
}

public function getName(): string
{
return self::NAME;
}
}
23 changes: 23 additions & 0 deletions src/User/src/DBAL/Types/UserStatusEnumType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace Frontend\User\DBAL\Types;

use Frontend\App\DBAL\Types\AbstractEnumType;
use Frontend\User\Enum\UserStatusEnum;

class UserStatusEnumType extends AbstractEnumType
{
public const NAME = 'user_status_enum';

protected function getEnumClass(): string
{
return UserStatusEnum::class;
}

public function getName(): string
{
return self::NAME;
}
}
31 changes: 15 additions & 16 deletions src/User/src/Entity/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use Exception;
use Frontend\App\Entity\AbstractEntity;
use Frontend\App\Entity\TimestampsTrait;
use Frontend\User\Enum\UserStatusEnum;
use Frontend\User\Repository\UserRepository;
use Ramsey\Uuid\Uuid;

Expand All @@ -24,22 +25,15 @@ class User extends AbstractEntity implements UserInterface
{
use TimestampsTrait;

public const STATUS_PENDING = 'pending';
public const STATUS_ACTIVE = 'active';
public const STATUSES = [
self::STATUS_PENDING,
self::STATUS_ACTIVE,
];

public const IS_DELETED_YES = true;
public const IS_DELETED_NO = false;

public const IS_DELETED = ['1', '0'];

#[ORM\OneToOne(mappedBy: 'user', targetEntity: UserDetail::class, cascade: ['persist', 'remove'])]
#[ORM\OneToOne(targetEntity: UserDetail::class, mappedBy: 'user', cascade: ['persist', 'remove'])]
protected UserDetail $detail;

#[ORM\OneToOne(mappedBy: 'user', targetEntity: UserAvatar::class, cascade: ['persist', 'remove'])]
#[ORM\OneToOne(targetEntity: UserAvatar::class, mappedBy: 'user', cascade: ['persist', 'remove'])]
protected ?UserAvatar $avatar;

#[ORM\Column(name: 'identity', type: 'string', length: 191, unique: true, nullable: false)]
Expand All @@ -48,8 +42,8 @@ class User extends AbstractEntity implements UserInterface
#[ORM\Column(name: 'password', type: 'string', length: 191, nullable: false)]
protected string $password;

#[ORM\Column(name: 'status', type: 'string', length: 20, columnDefinition: "ENUM('pending', 'active')")]
protected string $status = self::STATUS_PENDING;
#[ORM\Column(type: 'user_status_enum', options: ['default' => UserStatusEnum::Pending])]
protected UserStatusEnum $status = UserStatusEnum::Pending;

#[ORM\Column(name: 'isDeleted', type: 'boolean')]
protected bool $isDeleted = self::IS_DELETED_NO;
Expand All @@ -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'
)]
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/User/src/Entity/UserAvatar.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
2 changes: 1 addition & 1 deletion src/User/src/Entity/UserDetail.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Loading