Skip to content

Commit e38d1db

Browse files
committed
add basic json model type
1 parent 33f6af8 commit e38d1db

File tree

5 files changed

+166
-1
lines changed

5 files changed

+166
-1
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,16 @@ Features
1212
* PostgreSQL enums support in DBAL and migrations
1313
* PHP8 enum support
1414
* Fix creating [default schema in down migrations for pgsql](https://github.com/doctrine/dbal/issues/1110)
15+
* [JSON(B) functions](https://www.postgresql.org/docs/current/functions-json.html) (in progress)
16+
* JSON(B) types based on object models (in progress, requires symfony/serializer)
1517

1618
Requirement
1719
-----------
1820
* PHP ^8.1
1921
* doctrine/dbal ^3.5.1
2022
* doctrine/migrations ^3.5.2
23+
* symfony/serializer >=5.4.* (optional for json models)
24+
* symfony/property-info >=5.4.* (optional)
2125

2226
Installation
2327
------------

composer.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
},
3232
"require-dev": {
3333
"friendsofphp/php-cs-fixer": "^3.13",
34-
"doctrine/orm": "^2.13"
34+
"doctrine/orm": "^2.13",
35+
"symfony/serializer": ">=5.4",
36+
"symfony/property-info": ">=5.4"
3537
}
3638
}

src/DBAL/Platform/PostgreSQLPlatform.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@
88
use Doctrine\DBAL\Exception;
99
use Doctrine\DBAL\Exception\InvalidArgumentException;
1010
use Doctrine\DBAL\Platforms\PostgreSQLPlatform as BasePlatform;
11+
use Doctrine\DBAL\Schema\Column;
1112
use Pfilsx\PostgreSQLDoctrine\DBAL\Schema\EnumTypeAsset;
1213
use Pfilsx\PostgreSQLDoctrine\DBAL\Schema\PostgreSQLSchemaManager;
1314
use Pfilsx\PostgreSQLDoctrine\DBAL\Type\EnumType;
15+
use Pfilsx\PostgreSQLDoctrine\DBAL\Type\JsonModelType;
1416

1517
final class PostgreSQLPlatform extends BasePlatform
1618
{
@@ -84,6 +86,23 @@ public function quoteEnumLabel(mixed $label): int|string
8486
}
8587
}
8688

89+
public function columnsEqual(Column $column1, Column $column2): bool
90+
{
91+
if (parent::columnsEqual($column1, $column2)) {
92+
return true;
93+
}
94+
95+
$type1 = $column1->getType();
96+
$type2 = $column2->getType();
97+
98+
if (!is_subclass_of($type1, JsonModelType::class) && !is_subclass_of($type2, JsonModelType::class)) {
99+
return false;
100+
}
101+
102+
return is_subclass_of($type1, $type2::class) || is_subclass_of($type2, $type1::class);
103+
104+
}
105+
87106
protected function initializeDoctrineTypeMappings(): void
88107
{
89108
parent::initializeDoctrineTypeMappings();

src/DBAL/Type/JsonModelType.php

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Pfilsx\PostgreSQLDoctrine\DBAL\Type;
6+
7+
use Doctrine\DBAL\Platforms\AbstractPlatform;
8+
use Doctrine\DBAL\Types\ConversionException;
9+
use Doctrine\DBAL\Types\JsonType;
10+
use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;
11+
12+
abstract class JsonModelType extends JsonType
13+
{
14+
private ?AbstractObjectNormalizer $normalizer = null;
15+
16+
abstract public static function getTypeName(): string;
17+
18+
/**
19+
* @return class-string
20+
*/
21+
abstract protected static function getModelClass(): string;
22+
23+
public function getName(): string
24+
{
25+
return static::getTypeName();
26+
}
27+
28+
public function convertToDatabaseValue(mixed $value, AbstractPlatform $platform): mixed
29+
{
30+
if ($value === null) {
31+
return null;
32+
}
33+
34+
if (!\is_object($value)) {
35+
throw ConversionException::conversionFailed($value, $this->getName());
36+
}
37+
38+
$array = $this->getObjectNormalizer()->normalize($value);
39+
40+
return parent::convertToDatabaseValue($array, $platform);
41+
}
42+
43+
public function convertToPHPValue(mixed $value, AbstractPlatform $platform): mixed
44+
{
45+
$value = parent::convertToPHPValue($value, $platform);
46+
47+
if ($value === null) {
48+
return null;
49+
}
50+
51+
return $this->getObjectNormalizer()->denormalize(
52+
$value,
53+
static::getModelClass()
54+
);
55+
}
56+
57+
public function setObjectNormalizer(AbstractObjectNormalizer $normalizer): void
58+
{
59+
$this->normalizer = $normalizer;
60+
}
61+
62+
protected function getObjectNormalizer(): AbstractObjectNormalizer
63+
{
64+
if ($this->normalizer === null) {
65+
throw new \RuntimeException('JsonModelType requires object normalizer to be set');
66+
}
67+
68+
return $this->normalizer;
69+
}
70+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Pfilsx\PostgreSQLDoctrine\DBAL\Type;
6+
7+
use Doctrine\DBAL\Exception;
8+
use Doctrine\DBAL\Types\Type;
9+
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
10+
use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter;
11+
use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;
12+
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
13+
14+
final class JsonModelTypeRegistry
15+
{
16+
private static AbstractObjectNormalizer $objectNormalizer;
17+
18+
private static array $typesMap = [];
19+
20+
/**
21+
* @param string $name
22+
* @param class-string $className
23+
* @param bool $override
24+
* @return void
25+
* @throws Exception
26+
*/
27+
public static function addType(string $name, string $className, bool $override = false): void
28+
{
29+
if (!$override && array_key_exists($name, self::$typesMap)) {
30+
throw Exception::typeExists($name);
31+
}
32+
33+
if (!class_exists($className) || !is_subclass_of($className, JsonModelType::class)) {
34+
throw new Exception(
35+
sprintf('Type class name should be a subclass of %s. %s provided', JsonModelType::class, $className)
36+
);
37+
}
38+
39+
self::$typesMap[$name] = $className;
40+
}
41+
42+
public static function hasType(string $name): bool
43+
{
44+
return array_key_exists($name, self::$typesMap);
45+
}
46+
47+
public static function getObjectNormalizer(): AbstractObjectNormalizer
48+
{
49+
return self::$objectNormalizer ??= new ObjectNormalizer(
50+
nameConverter: new CamelCaseToSnakeCaseNameConverter(),
51+
propertyTypeExtractor: new ReflectionExtractor()
52+
);
53+
}
54+
public static function setObjectNormalizer(AbstractObjectNormalizer $objectNormalizer): void
55+
{
56+
self::$objectNormalizer = $objectNormalizer;
57+
}
58+
public static function registerTypes(): void
59+
{
60+
$typeRegistry = Type::getTypeRegistry();
61+
$objectNormalizer = self::getObjectNormalizer();
62+
foreach (self::$typesMap as $name => $className) {
63+
/** @var JsonModelType $type */
64+
$type = new $className();
65+
$type->setObjectNormalizer($objectNormalizer);
66+
67+
$typeRegistry->register($name, $type);
68+
}
69+
}
70+
}

0 commit comments

Comments
 (0)