Skip to content

Commit a9de148

Browse files
committed
Issue #430: PostgreSQL implementation
Signed-off-by: x <[email protected]>
1 parent 1a9383f commit a9de148

File tree

16 files changed

+260
-16
lines changed

16 files changed

+260
-16
lines changed

config/cli-config.php

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,34 @@
22

33
declare(strict_types=1);
44

5+
use Core\App\Doctrine\MigrationsMigratedSubscriber;
56
use Doctrine\Migrations\Configuration\EntityManager\ExistingEntityManager;
67
use Doctrine\Migrations\Configuration\Migration\ConfigurationArray;
78
use Doctrine\Migrations\DependencyFactory;
89
use Doctrine\ORM\EntityManager;
10+
use Dot\Log\LoggerInterface;
11+
use Psr\Container\ContainerExceptionInterface;
12+
use Psr\Container\NotFoundExceptionInterface;
913

1014
$container = require 'config/container.php';
1115

12-
return DependencyFactory::fromEntityManager(
16+
$config = DependencyFactory::fromEntityManager(
1317
new ConfigurationArray($container->get('config')['doctrine']['migrations']),
1418
new ExistingEntityManager(
1519
$container->get(EntityManager::class)
1620
)
1721
);
22+
23+
try {
24+
$config->getEntityManager()->getEventManager()->addEventSubscriber(new MigrationsMigratedSubscriber($container));
25+
} catch (ContainerExceptionInterface|NotFoundExceptionInterface $e) {
26+
try {
27+
/** @var LoggerInterface $logger */
28+
$logger = $container->get('dot-log.default_logger');
29+
$logger->err($e->getMessage());
30+
} catch (ContainerExceptionInterface|NotFoundExceptionInterface $e) {
31+
error_log($e->getMessage());
32+
}
33+
}
34+
35+
return $config;

src/Core/src/Admin/src/DBAL/Types/AdminRoleEnumType.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ class AdminRoleEnumType extends AbstractEnumType
1111
{
1212
public const NAME = 'admin_role_enum';
1313

14-
protected function getEnumClass(): string
14+
public function getEnumClass(): string
1515
{
1616
return AdminRoleEnum::class;
1717
}

src/Core/src/Admin/src/DBAL/Types/AdminStatusEnumType.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ class AdminStatusEnumType extends AbstractEnumType
1111
{
1212
public const NAME = 'admin_status_enum';
1313

14-
protected function getEnumClass(): string
14+
public function getEnumClass(): string
1515
{
1616
return AdminStatusEnum::class;
1717
}

src/Core/src/App/src/DBAL/Types/AbstractEnumType.php

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use BackedEnum;
88
use Doctrine\DBAL\Platforms\AbstractPlatform;
9+
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
910
use Doctrine\DBAL\Platforms\SQLitePlatform;
1011
use Doctrine\DBAL\Types\Type;
1112

@@ -17,11 +18,15 @@ abstract class AbstractEnumType extends Type
1718
{
1819
public function getSQLDeclaration(array $column, AbstractPlatform $platform): string
1920
{
21+
if ($platform instanceof PostgreSQLPlatform) {
22+
return static::NAME;
23+
}
24+
2025
if ($platform instanceof SQLitePlatform) {
2126
return 'TEXT';
2227
}
2328

24-
$values = array_map(fn($case) => "'$case->value'", $this->getEnumValues());
29+
$values = array_map(fn($case) => "'$case->value'", $this->getEnumCases());
2530

2631
return sprintf('ENUM(%s)', implode(', ', $values));
2732
}
@@ -39,17 +44,30 @@ public function convertToDatabaseValue(mixed $value, AbstractPlatform $platform)
3944
/**
4045
* @return class-string
4146
*/
42-
abstract protected function getEnumClass(): string;
47+
abstract public function getEnumClass(): string;
48+
49+
/**
50+
* @return non-empty-string
51+
*/
52+
abstract public function getName(): string;
4353

4454
/**
45-
* @return BackedEnum[]
55+
* @return list<BackedEnum>
4656
*/
47-
private function getEnumValues(): array
57+
public function getEnumCases(): array
4858
{
4959
return $this->getEnumClass()::cases();
5060
}
5161

52-
private function getValue(mixed $value): mixed
62+
/**
63+
* @return list<BackedEnum>
64+
*/
65+
public function getEnumValues(): array
66+
{
67+
return $this->getEnumClass()::values();
68+
}
69+
70+
public function getValue(mixed $value): mixed
5371
{
5472
if (! $value instanceof BackedEnum) {
5573
return $value;

src/Core/src/App/src/DBAL/Types/SuccessFailureEnumType.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ class SuccessFailureEnumType extends AbstractEnumType
1010
{
1111
public const NAME = 'success_failure_enum';
1212

13-
protected function getEnumClass(): string
13+
public function getEnumClass(): string
1414
{
1515
return SuccessFailureEnum::class;
1616
}

src/Core/src/App/src/DBAL/Types/YesNoEnumType.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ class YesNoEnumType extends AbstractEnumType
1010
{
1111
public const NAME = 'yes_no_enum';
1212

13-
protected function getEnumClass(): string
13+
public function getEnumClass(): string
1414
{
1515
return YesNoEnum::class;
1616
}
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
<?php
2+
3+
namespace Core\App\Doctrine;
4+
5+
use Core\App\DBAL\Types\AbstractEnumType;
6+
use Doctrine\Common\EventSubscriber;
7+
use Doctrine\DBAL\Connection;
8+
use Doctrine\DBAL\Exception;
9+
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
10+
use Doctrine\Migrations\Events;
11+
use Doctrine\ORM\EntityManagerInterface;
12+
use Psr\Container\ContainerExceptionInterface;
13+
use Psr\Container\ContainerInterface;
14+
use Psr\Container\NotFoundExceptionInterface;
15+
16+
use function array_column;
17+
use function array_key_exists;
18+
use function implode;
19+
use function in_array;
20+
use function sprintf;
21+
22+
class MigrationsMigratedSubscriber implements EventSubscriber
23+
{
24+
private EntityManagerInterface $entityManager;
25+
private Connection $connection;
26+
27+
/**
28+
* @throws ContainerExceptionInterface
29+
* @throws NotFoundExceptionInterface
30+
*/
31+
public function __construct(
32+
private readonly ContainerInterface $container,
33+
) {
34+
$this->entityManager = $container->get('doctrine.entity_manager.orm_default');
35+
$this->connection = $this->entityManager->getConnection();
36+
}
37+
38+
/**
39+
* @throws Exception
40+
*/
41+
public function getSubscribedEvents(): array
42+
{
43+
if (! $this->connection->getDatabasePlatform() instanceof PostgreSQLPlatform) {
44+
return [];
45+
}
46+
47+
return [
48+
Events::onMigrationsMigrating,
49+
];
50+
}
51+
52+
/**
53+
* @throws ContainerExceptionInterface
54+
* @throws NotFoundExceptionInterface
55+
* @throws Exception
56+
*/
57+
public function onMigrationsMigrating(): void
58+
{
59+
$dbEnumTypes = $this->getCustomEnumTypesFromTheDatabase();
60+
$fsEnumTypes = $this->getCustomEnumTypesFromTheFileSystem();
61+
62+
$enumTypes = $this->mergeCustomEnumTypes($dbEnumTypes, $fsEnumTypes);
63+
foreach ($enumTypes as $action => $enums) {
64+
foreach ($enums as $type => $values) {
65+
match ($action) {
66+
'create' => $this->createDatabaseType($type, $values),
67+
'delete' => $this->deleteDatabaseType($type),
68+
'update' => $this->updateDatabaseType($type, $values),
69+
default => null,
70+
};
71+
}
72+
}
73+
}
74+
75+
/**
76+
* @return array<non-empty-string, object<AbstractEnumType>>
77+
* @throws ContainerExceptionInterface
78+
* @throws NotFoundExceptionInterface
79+
*/
80+
private function getCustomEnumTypesFromTheFileSystem(): array
81+
{
82+
$enumTypes = [];
83+
84+
$customTypes = $this->container->get('config')['doctrine']['types'] ?? [];
85+
foreach ($customTypes as $type => $class) {
86+
$class = new $class();
87+
if (! $class instanceof AbstractEnumType) {
88+
continue;
89+
}
90+
$enumTypes[$type] = $class;
91+
}
92+
93+
return $enumTypes;
94+
}
95+
96+
/**
97+
* @throws Exception
98+
*/
99+
private function getDatabaseTypeValues(string $type): array
100+
{
101+
$results = $this->connection->executeQuery(
102+
"SELECT e.enumlabel FROM pg_type t JOIN pg_enum e ON t.oid = e.enumtypid WHERE t.typname = '$type';"
103+
)->fetchAllAssociative();
104+
105+
return array_column($results, 'enumlabel');
106+
}
107+
108+
/**
109+
* @return list<non-empty-string>
110+
* @throws Exception
111+
*/
112+
private function getCustomEnumTypesFromTheDatabase(): array
113+
{
114+
return $this->connection->executeQuery(
115+
'SELECT t.typname FROM pg_type t JOIN pg_enum e ON t.oid = e.enumtypid GROUP BY t.typname'
116+
)->fetchFirstColumn();
117+
}
118+
119+
/**
120+
* @param list<non-empty-string> $dbEnumTypes
121+
* @param array<non-empty-string, object<AbstractEnumType>> $fsEnumTypes
122+
* @return array<non-empty-string, array<non-empty-string, list<non-empty-string>>>
123+
* @throws Exception
124+
*/
125+
private function mergeCustomEnumTypes(array $dbEnumTypes, array $fsEnumTypes): array
126+
{
127+
$enumTypes = [
128+
'create' => [],
129+
'delete' => [],
130+
'skip' => [],
131+
'update' => [],
132+
];
133+
134+
/** @var AbstractEnumType $class */
135+
foreach ($fsEnumTypes as $type => $class) {
136+
$fsTypeValues = $class->getEnumValues();
137+
if (in_array($type, $dbEnumTypes)) {
138+
$dbTypeValues = $this->getDatabaseTypeValues($type);
139+
if ($dbTypeValues === $fsTypeValues) {
140+
$enumTypes['skip'][$type] = $fsTypeValues;
141+
} else {
142+
$enumTypes['update'][$type] = $fsTypeValues;
143+
}
144+
} else {
145+
$enumTypes['create'][$type] = $fsTypeValues;
146+
}
147+
}
148+
149+
foreach ($dbEnumTypes as $type) {
150+
if (! array_key_exists($type, $fsEnumTypes)) {
151+
$enumTypes['delete'][$type] = $this->getDatabaseTypeValues($type);
152+
}
153+
}
154+
155+
return $enumTypes;
156+
}
157+
158+
/**
159+
* @throws Exception
160+
*/
161+
private function createDatabaseType(string $type, array $values): void
162+
{
163+
$sql = sprintf("CREATE TYPE %s AS ENUM ('%s');", $type, implode("', '", $values));
164+
$this->connection->executeQuery($sql);
165+
}
166+
167+
/**
168+
* @throws Exception
169+
*/
170+
private function deleteDatabaseType(string $type): void
171+
{
172+
$sql = sprintf('DROP TYPE %s;', $type);
173+
$this->connection->executeQuery($sql);
174+
}
175+
176+
/**
177+
* @throws Exception
178+
*/
179+
private function updateDatabaseType(string $type, array $values): void
180+
{
181+
$this->deleteDatabaseType($type);
182+
$this->createDatabaseType($type, $values);
183+
}
184+
}

src/Core/src/Setting/src/DBAL/Types/SettingIdentifierEnumType.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ class SettingIdentifierEnumType extends AbstractEnumType
1111
{
1212
public const NAME = 'setting_enum';
1313

14-
protected function getEnumClass(): string
14+
public function getEnumClass(): string
1515
{
1616
return SettingIdentifierEnum::class;
1717
}

src/Core/src/User/src/DBAL/Types/UserResetPasswordStatusEnumType.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ class UserResetPasswordStatusEnumType extends AbstractEnumType
1111
{
1212
public const NAME = 'user_reset_password_status_enum';
1313

14-
protected function getEnumClass(): string
14+
public function getEnumClass(): string
1515
{
1616
return UserResetPasswordStatusEnum::class;
1717
}

src/Core/src/User/src/DBAL/Types/UserRoleEnumType.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ class UserRoleEnumType extends AbstractEnumType
1111
{
1212
public const NAME = 'user_role_enum';
1313

14-
protected function getEnumClass(): string
14+
public function getEnumClass(): string
1515
{
1616
return UserRoleEnum::class;
1717
}

0 commit comments

Comments
 (0)