Skip to content

Commit e480055

Browse files
authored
Merge pull request #462 from dotkernel/issue-430-v7
Issue #430: PostgreSQL implementation
2 parents 63af580 + c3f05f6 commit e480055

File tree

16 files changed

+293
-26
lines changed

16 files changed

+293
-26
lines changed

config/autoload/local.php.dist

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,24 @@ declare(strict_types=1);
55
$baseUrl = 'http://localhost:8080';
66

77
$databases = [
8-
'default' => [
8+
'mariadb' => [
99
'host' => 'localhost',
1010
'dbname' => 'dotkernel',
1111
'user' => '',
1212
'password' => '',
1313
'port' => 3306,
1414
'driver' => 'pdo_mysql',
15-
'charset' => 'utf8mb4',
16-
'collate' => 'utf8mb4_general_ci',
15+
'collation' => 'utf8mb4_general_ci',
16+
'table_prefix' => '',
17+
],
18+
'postgresql' => [
19+
'host' => 'localhost',
20+
'dbname' => 'dotkernel',
21+
'user' => '',
22+
'password' => '',
23+
'port' => 5432,
24+
'driver' => 'pdo_pgsql',
25+
'collation' => 'utf8mb4_general_ci',
1726
'table_prefix' => '',
1827
],
1928
// you can add more database connections to this array
@@ -22,10 +31,10 @@ $databases = [
2231
return [
2332
'application' => [
2433
'name' => 'Dotkernel API',
25-
'version' => 6.0,
34+
'version' => 7.0,
2635
'url' => $baseUrl,
2736
'versioning' => [
28-
'documentation_url' => 'https://docs.dotkernel.org/api-documentation/v6/core-features/versioning',
37+
'documentation_url' => 'https://docs.dotkernel.org/api-documentation/v7/tutorials/api-evolution/',
2938
],
3039
],
3140
'authentication' => [
@@ -51,7 +60,7 @@ return [
5160
'doctrine' => [
5261
'connection' => [
5362
'orm_default' => [
54-
'params' => $databases['default'],
63+
'params' => $databases['mariadb'],
5564
],
5665
],
5766
],

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

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
}

0 commit comments

Comments
 (0)