Skip to content

Commit fe96e38

Browse files
Add support for symfony uuid
1 parent 934f573 commit fe96e38

File tree

10 files changed

+460
-8
lines changed

10 files changed

+460
-8
lines changed

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@
3636
"phpstan/phpstan-strict-rules": "^2.0",
3737
"phpunit/phpunit": "^9.6.20",
3838
"ramsey/uuid": "^4.2",
39-
"symfony/cache": "^5.4"
39+
"symfony/cache": "^5.4",
40+
"symfony/uid": "^5.4 || ^6.4 || ^7.3"
4041
},
4142
"config": {
4243
"sort-packages": true,

extension.neon

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,16 @@ services:
418418
tags: [phpstan.doctrine.typeDescriptor]
419419
arguments:
420420
uuidTypeName: Ramsey\Uuid\Doctrine\UuidBinaryOrderedTimeType
421+
-
422+
class: PHPStan\Type\Doctrine\Descriptors\Symfony\UuidTypeDescriptor
423+
tags: [phpstan.doctrine.typeDescriptor]
424+
arguments:
425+
uuidTypeName: Symfony\Bridge\Doctrine\Types\UuidType
426+
-
427+
class: PHPStan\Type\Doctrine\Descriptors\Symfony\UlidTypeDescriptor
428+
tags: [phpstan.doctrine.typeDescriptor]
429+
arguments:
430+
uuidTypeName: Symfony\Bridge\Doctrine\Types\UlidType
421431

422432
# Doctrine Collection
423433
-

src/Type/Doctrine/Descriptors/Ramsey/UuidTypeDescriptor.php

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

33
namespace PHPStan\Type\Doctrine\Descriptors\Ramsey;
44

5-
use PHPStan\Rules\Doctrine\ORM\FakeTestingUuidType;
5+
use PHPStan\Rules\Doctrine\ORM\FakeTestingRamseyUuidType;
66
use PHPStan\ShouldNotHappenException;
77
use PHPStan\Type\Doctrine\Descriptors\DoctrineTypeDescriptor;
88
use PHPStan\Type\ObjectType;
@@ -20,7 +20,7 @@ class UuidTypeDescriptor implements DoctrineTypeDescriptor
2020
'Ramsey\Uuid\Doctrine\UuidType',
2121
'Ramsey\Uuid\Doctrine\UuidBinaryType',
2222
'Ramsey\Uuid\Doctrine\UuidBinaryOrderedTimeType',
23-
FakeTestingUuidType::class,
23+
FakeTestingRamseyUuidType::class,
2424
];
2525

2626
private string $uuidTypeName;
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Doctrine\Descriptors\Symfony;
4+
5+
use PHPStan\Rules\Doctrine\ORM\FakeTestingSymfonyUlidType;
6+
use PHPStan\ShouldNotHappenException;
7+
use PHPStan\Type\Doctrine\Descriptors\DoctrineTypeDescriptor;
8+
use PHPStan\Type\ObjectType;
9+
use PHPStan\Type\StringType;
10+
use PHPStan\Type\Type;
11+
use PHPStan\Type\TypeCombinator;
12+
use Symfony\Component\Uid\Ulid;
13+
use function in_array;
14+
use function sprintf;
15+
16+
class UlidTypeDescriptor implements DoctrineTypeDescriptor
17+
{
18+
19+
private const SUPPORTED_UUID_TYPES = [
20+
'Symfony\Bridge\Doctrine\Types\UlidType',
21+
FakeTestingSymfonyUlidType::class,
22+
];
23+
24+
private string $uuidTypeName;
25+
26+
public function __construct(
27+
string $uuidTypeName
28+
)
29+
{
30+
if (!in_array($uuidTypeName, self::SUPPORTED_UUID_TYPES, true)) {
31+
throw new ShouldNotHappenException(sprintf(
32+
'Unexpected UUID column type "%s" provided',
33+
$uuidTypeName,
34+
));
35+
}
36+
37+
$this->uuidTypeName = $uuidTypeName;
38+
}
39+
40+
public function getType(): string
41+
{
42+
/** @var class-string<\Doctrine\DBAL\Types\Type> */
43+
return $this->uuidTypeName;
44+
}
45+
46+
public function getWritableToPropertyType(): Type
47+
{
48+
return new ObjectType(Ulid::class);
49+
}
50+
51+
public function getWritableToDatabaseType(): Type
52+
{
53+
return TypeCombinator::union(
54+
new StringType(),
55+
new ObjectType(Ulid::class),
56+
);
57+
}
58+
59+
public function getDatabaseInternalType(): Type
60+
{
61+
return new StringType();
62+
}
63+
64+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Doctrine\Descriptors\Symfony;
4+
5+
use PHPStan\Rules\Doctrine\ORM\FakeTestingSymfonyUuidType;
6+
use PHPStan\ShouldNotHappenException;
7+
use PHPStan\Type\Doctrine\Descriptors\DoctrineTypeDescriptor;
8+
use PHPStan\Type\ObjectType;
9+
use PHPStan\Type\StringType;
10+
use PHPStan\Type\Type;
11+
use PHPStan\Type\TypeCombinator;
12+
use Symfony\Component\Uid\Uuid;
13+
use function in_array;
14+
use function sprintf;
15+
16+
class UuidTypeDescriptor implements DoctrineTypeDescriptor
17+
{
18+
19+
private const SUPPORTED_UUID_TYPES = [
20+
'Symfony\Bridge\Doctrine\Types\UuidType',
21+
FakeTestingSymfonyUuidType::class,
22+
];
23+
24+
private string $uuidTypeName;
25+
26+
public function __construct(
27+
string $uuidTypeName
28+
)
29+
{
30+
if (!in_array($uuidTypeName, self::SUPPORTED_UUID_TYPES, true)) {
31+
throw new ShouldNotHappenException(sprintf(
32+
'Unexpected UUID column type "%s" provided',
33+
$uuidTypeName,
34+
));
35+
}
36+
37+
$this->uuidTypeName = $uuidTypeName;
38+
}
39+
40+
public function getType(): string
41+
{
42+
/** @var class-string<\Doctrine\DBAL\Types\Type> */
43+
return $this->uuidTypeName;
44+
}
45+
46+
public function getWritableToPropertyType(): Type
47+
{
48+
return new ObjectType(Uuid::class);
49+
}
50+
51+
public function getWritableToDatabaseType(): Type
52+
{
53+
return TypeCombinator::union(
54+
new StringType(),
55+
new ObjectType(Uuid::class),
56+
);
57+
}
58+
59+
public function getDatabaseInternalType(): Type
60+
{
61+
return new StringType();
62+
}
63+
64+
}

tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,12 @@
2121
use PHPStan\Type\Doctrine\Descriptors\EnumType;
2222
use PHPStan\Type\Doctrine\Descriptors\IntegerType;
2323
use PHPStan\Type\Doctrine\Descriptors\JsonType;
24-
use PHPStan\Type\Doctrine\Descriptors\Ramsey\UuidTypeDescriptor;
24+
use PHPStan\Type\Doctrine\Descriptors\Ramsey\UuidTypeDescriptor as RamseyUuidTypeDescriptor;
2525
use PHPStan\Type\Doctrine\Descriptors\ReflectionDescriptor;
2626
use PHPStan\Type\Doctrine\Descriptors\SimpleArrayType;
2727
use PHPStan\Type\Doctrine\Descriptors\StringType;
28+
use PHPStan\Type\Doctrine\Descriptors\Symfony\UlidTypeDescriptor as SymfonyUlidTypeDescriptor;
29+
use PHPStan\Type\Doctrine\Descriptors\Symfony\UuidTypeDescriptor as SymfonyUuidTypeDescriptor;
2830
use PHPStan\Type\Doctrine\ObjectMetadataResolver;
2931
use function array_unshift;
3032
use function class_exists;
@@ -41,6 +43,8 @@ class EntityColumnRuleTest extends RuleTestCase
4143

4244
private ?string $objectManagerLoader = null;
4345

46+
private bool $useSymfonyUuid = false;
47+
4448
protected function getRule(): Rule
4549
{
4650
if (!Type::hasType(CustomType::NAME)) {
@@ -49,8 +53,23 @@ protected function getRule(): Rule
4953
if (!Type::hasType(CustomNumericType::NAME)) {
5054
Type::addType(CustomNumericType::NAME, CustomNumericType::class);
5155
}
52-
if (!Type::hasType(FakeTestingUuidType::NAME)) {
53-
Type::addType(FakeTestingUuidType::NAME, FakeTestingUuidType::class);
56+
if ($this->useSymfonyUuid) {
57+
if (!Type::hasType(FakeTestingSymfonyUuidType::NAME)) {
58+
Type::addType(FakeTestingSymfonyUuidType::NAME, FakeTestingSymfonyUuidType::class);
59+
} else {
60+
// Override Ramsay definition
61+
Type::overrideType(FakeTestingSymfonyUuidType::NAME, FakeTestingSymfonyUuidType::class);
62+
}
63+
if (!Type::hasType(FakeTestingSymfonyUlidType::NAME)) {
64+
Type::addType(FakeTestingSymfonyUlidType::NAME, FakeTestingSymfonyUlidType::class);
65+
}
66+
} else {
67+
if (!Type::hasType(FakeTestingRamseyUuidType::NAME)) {
68+
Type::addType(FakeTestingRamseyUuidType::NAME, FakeTestingRamseyUuidType::class);
69+
} else {
70+
// Override Symfony definition
71+
Type::overrideType(FakeTestingRamseyUuidType::NAME, FakeTestingRamseyUuidType::class);
72+
}
5473
}
5574
if (!Type::hasType('carbon')) {
5675
Type::addType('carbon', CarbonType::class);
@@ -76,8 +95,10 @@ protected function getRule(): Rule
7695
new IntegerType(),
7796
new StringType(),
7897
new SimpleArrayType(),
79-
new UuidTypeDescriptor(FakeTestingUuidType::class),
8098
new EnumType(),
99+
new RamseyUuidTypeDescriptor(FakeTestingRamseyUuidType::class),
100+
new SymfonyUuidTypeDescriptor(FakeTestingSymfonyUuidType::class),
101+
new SymfonyUlidTypeDescriptor(FakeTestingSymfonyUlidType::class),
81102
new ReflectionDescriptor(CarbonImmutableType::class, $this->createReflectionProvider(), self::getContainer()),
82103
new ReflectionDescriptor(CarbonType::class, $this->createReflectionProvider(), self::getContainer()),
83104
new ReflectionDescriptor(CustomType::class, $this->createReflectionProvider(), self::getContainer()),
@@ -461,6 +482,27 @@ public function testBug677(?string $objectManagerLoader): void
461482
$this->analyse([__DIR__ . '/data/bug-677.php'], []);
462483
}
463484

485+
/**
486+
* @dataProvider dataObjectManagerLoader
487+
*/
488+
public function testSymfonyUuid(?string $objectManagerLoader): void
489+
{
490+
$this->allowNullablePropertyForRequiredField = true;
491+
$this->objectManagerLoader = $objectManagerLoader;
492+
$this->useSymfonyUuid = true;
493+
494+
$this->analyse([__DIR__ . '/data/EntityWithSymfonyUid.php'], [
495+
[
496+
'Property PHPStan\Rules\Doctrine\ORM\EntityWithSymfonyUid::$uuidInvalidType type mapping mismatch: database can contain Symfony\Component\Uid\Uuid but property expects string.',
497+
32,
498+
],
499+
[
500+
'Property PHPStan\Rules\Doctrine\ORM\EntityWithSymfonyUid::$ulidInvalidType type mapping mismatch: database can contain Symfony\Component\Uid\Ulid but property expects string.',
501+
44,
502+
],
503+
]);
504+
}
505+
464506
/**
465507
* @dataProvider dataObjectManagerLoader
466508
*/

tests/Rules/Doctrine/ORM/FakeTestingUuidType.php renamed to tests/Rules/Doctrine/ORM/FakeTestingRamseyUuidType.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
* From https://github.com/ramsey/uuid-doctrine/blob/fafebbe972cdaba9274c286ea8923e2de2579027/src/UuidType.php
1414
* Copyright (c) 2012-2022 Ben Ramsey <[email protected]>
1515
*/
16-
final class FakeTestingUuidType extends GuidType
16+
final class FakeTestingRamseyUuidType extends GuidType
1717
{
1818

1919
public const NAME = 'uuid';
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Doctrine\ORM;
4+
5+
use Doctrine\DBAL\Platforms\AbstractPlatform;
6+
use Doctrine\DBAL\Types\ConversionException;
7+
use Doctrine\DBAL\Types\Type;
8+
use InvalidArgumentException;
9+
use Symfony\Component\Uid\AbstractUid;
10+
use Symfony\Component\Uid\Ulid;
11+
use function is_string;
12+
13+
/**
14+
* From https://github.com/symfony/doctrine-bridge/blob/f5a2780640342e3bf0599fbfc9b3dd759db9b037/Types/UuidType.php
15+
* Copyright (c) Fabien Potencier <[email protected]>
16+
*/
17+
final class FakeTestingSymfonyUlidType extends Type
18+
{
19+
20+
public const NAME = 'ulid';
21+
22+
/**
23+
* @not-deprecated
24+
*/
25+
public function getName(): string
26+
{
27+
return self::NAME;
28+
}
29+
30+
protected function getUidClass(): string
31+
{
32+
return Ulid::class;
33+
}
34+
35+
/**
36+
* {@inheritdoc}
37+
*/
38+
public function getSQLDeclaration(array $column, AbstractPlatform $platform): string
39+
{
40+
if ($this->hasNativeGuidType($platform)) {
41+
return $platform->getGuidTypeDeclarationSQL($column);
42+
}
43+
44+
return $platform->getBinaryTypeDeclarationSQL([
45+
'length' => '16',
46+
'fixed' => true,
47+
]);
48+
}
49+
50+
/**
51+
* {@inheritdoc}
52+
*
53+
* @throws ConversionException
54+
*/
55+
public function convertToPHPValue($value, AbstractPlatform $platform): ?AbstractUid
56+
{
57+
if ($value instanceof AbstractUid || $value === null) {
58+
return $value;
59+
}
60+
61+
if (!is_string($value)) {
62+
throw ConversionException::conversionFailedInvalidType($value, $this->getName(), ['null', 'string', AbstractUid::class]);
63+
}
64+
65+
try {
66+
/** @phpstan-ignore-next-line method.dynamicName */
67+
return $this->getUidClass()::fromString($value);
68+
} catch (InvalidArgumentException $e) {
69+
throw ConversionException::conversionFailed($value, $this->getName(), $e);
70+
}
71+
}
72+
73+
/**
74+
* {@inheritdoc}
75+
*
76+
* @throws ConversionException
77+
*/
78+
public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string
79+
{
80+
$toString = $this->hasNativeGuidType($platform) ? 'toRfc4122' : 'toBinary';
81+
82+
if ($value instanceof AbstractUid) {
83+
/** @phpstan-ignore-next-line method.dynamicName */
84+
return $value->$toString();
85+
}
86+
87+
if ($value === null || $value === '') {
88+
return null;
89+
}
90+
91+
if (!is_string($value)) {
92+
throw ConversionException::conversionFailedInvalidType($value, $this->getName(), ['null', 'string', AbstractUid::class]);
93+
}
94+
95+
try {
96+
/** @phpstan-ignore-next-line method.dynamicName */
97+
return $this->getUidClass()::fromString($value)->$toString();
98+
} catch (InvalidArgumentException $e) {
99+
throw ConversionException::conversionFailed($value, $this->getName());
100+
}
101+
}
102+
103+
public function requiresSQLCommentHint(AbstractPlatform $platform): bool
104+
{
105+
return true;
106+
}
107+
108+
private function hasNativeGuidType(AbstractPlatform $platform): bool
109+
{
110+
return $platform->getGuidTypeDeclarationSQL([]) !== $platform->getStringTypeDeclarationSQL(['fixed' => true, 'length' => 36]);
111+
}
112+
113+
}

0 commit comments

Comments
 (0)