Skip to content

Commit ad17c40

Browse files
committed
Issue #430: PostgreSQL implementation
Signed-off-by: alexmerlin <[email protected]>
1 parent d8b6963 commit ad17c40

File tree

16 files changed

+278
-21
lines changed

16 files changed

+278
-21
lines changed

config/autoload/local.php.dist

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ $databases = [
2222
return [
2323
'application' => [
2424
'name' => 'Dotkernel API',
25-
'version' => 6.0,
25+
'version' => 7.0,
2626
'url' => $baseUrl,
2727
'versioning' => [
2828
'documentation_url' => 'https://docs.dotkernel.org/api-documentation/v6/core-features/versioning',

config/cli-config.php

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,35 @@
22

33
declare(strict_types=1);
44

5+
use Core\App\Doctrine\MigrationsMigratedSubscriber;
56
use Core\App\Event\TablePrefixEventListener;
67
use Doctrine\Migrations\Configuration\EntityManager\ExistingEntityManager;
78
use Doctrine\Migrations\Configuration\Migration\ConfigurationArray;
89
use Doctrine\Migrations\DependencyFactory;
910
use Doctrine\ORM\EntityManager;
1011
use Doctrine\ORM\Events;
12+
use Dot\Log\LoggerInterface;
13+
use Psr\Container\ContainerExceptionInterface;
1114

1215
$container = require 'config/container.php';
1316

14-
$entityManager = $container->get(EntityManager::class);
15-
$entityManager->getEventManager()
16-
->addEventListener(Events::loadClassMetadata, $container->get(TablePrefixEventListener::class));
17+
try {
18+
$entityManager = $container->get(EntityManager::class);
19+
$entityManager->getEventManager()
20+
->addEventListener(Events::loadClassMetadata, $container->get(TablePrefixEventListener::class));
21+
$entityManager->getEventManager()
22+
->addEventSubscriber(new MigrationsMigratedSubscriber($container));
1723

18-
return DependencyFactory::fromEntityManager(
19-
new ConfigurationArray($container->get('config')['doctrine']['migrations']),
20-
new ExistingEntityManager($entityManager)
21-
);
24+
return DependencyFactory::fromEntityManager(
25+
new ConfigurationArray($container->get('config')['doctrine']['migrations']),
26+
new ExistingEntityManager($entityManager)
27+
);
28+
} catch (ContainerExceptionInterface $exception) {
29+
try {
30+
/** @var LoggerInterface $logger */
31+
$logger = $container->get('dot-log.default_logger');
32+
$logger->err($exception->getMessage());
33+
} catch (ContainerExceptionInterface $exception) {
34+
error_log($exception->getMessage());
35+
}
36+
}

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

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
}

0 commit comments

Comments
 (0)