Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@
"phpstan/phpstan-strict-rules": "^2.0",
"phpunit/phpunit": "^9.6.20",
"ramsey/uuid": "^4.2",
"symfony/cache": "^5.4"
"symfony/cache": "^5.4",
"symfony/uid": "^5.4 || ^6.4 || ^7.3"
},
"config": {
"sort-packages": true,
Expand Down
10 changes: 10 additions & 0 deletions extension.neon
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,16 @@ services:
tags: [phpstan.doctrine.typeDescriptor]
arguments:
uuidTypeName: Ramsey\Uuid\Doctrine\UuidBinaryOrderedTimeType
-
class: PHPStan\Type\Doctrine\Descriptors\Symfony\UuidTypeDescriptor
tags: [phpstan.doctrine.typeDescriptor]
arguments:
uuidTypeName: Symfony\Bridge\Doctrine\Types\UuidType
-
class: PHPStan\Type\Doctrine\Descriptors\Symfony\UlidTypeDescriptor
tags: [phpstan.doctrine.typeDescriptor]
arguments:
uuidTypeName: Symfony\Bridge\Doctrine\Types\UlidType

# Doctrine Collection
-
Expand Down
27 changes: 5 additions & 22 deletions src/Type/Doctrine/Descriptors/Ramsey/UuidTypeDescriptor.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,46 +2,29 @@

namespace PHPStan\Type\Doctrine\Descriptors\Ramsey;

use PHPStan\Rules\Doctrine\ORM\FakeTestingUuidType;
use PHPStan\ShouldNotHappenException;
use PHPStan\Type\Doctrine\Descriptors\DoctrineTypeDescriptor;
use PHPStan\Type\ObjectType;
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use Ramsey\Uuid\UuidInterface;
use function in_array;
use function sprintf;

class UuidTypeDescriptor implements DoctrineTypeDescriptor
{

private const SUPPORTED_UUID_TYPES = [
'Ramsey\Uuid\Doctrine\UuidType',
'Ramsey\Uuid\Doctrine\UuidBinaryType',
'Ramsey\Uuid\Doctrine\UuidBinaryOrderedTimeType',
FakeTestingUuidType::class,
];

/** @var class-string<\Doctrine\DBAL\Types\Type> */
private string $uuidTypeName;

public function __construct(
string $uuidTypeName
)
/**
* @param class-string<\Doctrine\DBAL\Types\Type> $uuidTypeName
*/
public function __construct(string $uuidTypeName)
{
if (!in_array($uuidTypeName, self::SUPPORTED_UUID_TYPES, true)) {
throw new ShouldNotHappenException(sprintf(
'Unexpected UUID column type "%s" provided',
$uuidTypeName,
));
}

$this->uuidTypeName = $uuidTypeName;
}

public function getType(): string
{
/** @var class-string<\Doctrine\DBAL\Types\Type> */
return $this->uuidTypeName;
}

Expand Down
49 changes: 49 additions & 0 deletions src/Type/Doctrine/Descriptors/Symfony/UlidTypeDescriptor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Doctrine\Descriptors\Symfony;

use PHPStan\Type\Doctrine\Descriptors\DoctrineTypeDescriptor;
use PHPStan\Type\ObjectType;
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use Symfony\Component\Uid\Ulid;

class UlidTypeDescriptor implements DoctrineTypeDescriptor
{

/** @var class-string<\Doctrine\DBAL\Types\Type> */
private string $uuidTypeName;

/**
* @param class-string<\Doctrine\DBAL\Types\Type> $uuidTypeName
*/
public function __construct(string $uuidTypeName)
{
$this->uuidTypeName = $uuidTypeName;
}

public function getType(): string
{
return $this->uuidTypeName;
}

public function getWritableToPropertyType(): Type
{
return new ObjectType(Ulid::class);
}

public function getWritableToDatabaseType(): Type
{
return TypeCombinator::union(
new StringType(),
new ObjectType(Ulid::class),
);
}

public function getDatabaseInternalType(): Type
{
return new StringType();
}

}
49 changes: 49 additions & 0 deletions src/Type/Doctrine/Descriptors/Symfony/UuidTypeDescriptor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Doctrine\Descriptors\Symfony;

use PHPStan\Type\Doctrine\Descriptors\DoctrineTypeDescriptor;
use PHPStan\Type\ObjectType;
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use Symfony\Component\Uid\Uuid;

class UuidTypeDescriptor implements DoctrineTypeDescriptor
{

/** @var class-string<\Doctrine\DBAL\Types\Type> */
private string $uuidTypeName;

/**
* @param class-string<\Doctrine\DBAL\Types\Type> $uuidTypeName
*/
public function __construct(string $uuidTypeName)
{
$this->uuidTypeName = $uuidTypeName;
}

public function getType(): string
{
return $this->uuidTypeName;
}

public function getWritableToPropertyType(): Type
{
return new ObjectType(Uuid::class);
}

public function getWritableToDatabaseType(): Type
{
return TypeCombinator::union(
new StringType(),
new ObjectType(Uuid::class),
);
}

public function getDatabaseInternalType(): Type
{
return new StringType();
}

}
50 changes: 46 additions & 4 deletions tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@
use PHPStan\Type\Doctrine\Descriptors\EnumType;
use PHPStan\Type\Doctrine\Descriptors\IntegerType;
use PHPStan\Type\Doctrine\Descriptors\JsonType;
use PHPStan\Type\Doctrine\Descriptors\Ramsey\UuidTypeDescriptor;
use PHPStan\Type\Doctrine\Descriptors\Ramsey\UuidTypeDescriptor as RamseyUuidTypeDescriptor;
use PHPStan\Type\Doctrine\Descriptors\ReflectionDescriptor;
use PHPStan\Type\Doctrine\Descriptors\SimpleArrayType;
use PHPStan\Type\Doctrine\Descriptors\StringType;
use PHPStan\Type\Doctrine\Descriptors\Symfony\UlidTypeDescriptor as SymfonyUlidTypeDescriptor;
use PHPStan\Type\Doctrine\Descriptors\Symfony\UuidTypeDescriptor as SymfonyUuidTypeDescriptor;
use PHPStan\Type\Doctrine\ObjectMetadataResolver;
use function array_unshift;
use function class_exists;
Expand All @@ -41,6 +43,8 @@ class EntityColumnRuleTest extends RuleTestCase

private ?string $objectManagerLoader = null;

private bool $useSymfonyUuid = false;

protected function getRule(): Rule
{
if (!Type::hasType(CustomType::NAME)) {
Expand All @@ -49,8 +53,23 @@ protected function getRule(): Rule
if (!Type::hasType(CustomNumericType::NAME)) {
Type::addType(CustomNumericType::NAME, CustomNumericType::class);
}
if (!Type::hasType(FakeTestingUuidType::NAME)) {
Type::addType(FakeTestingUuidType::NAME, FakeTestingUuidType::class);
if ($this->useSymfonyUuid) {
if (!Type::hasType(FakeTestingSymfonyUuidType::NAME)) {
Type::addType(FakeTestingSymfonyUuidType::NAME, FakeTestingSymfonyUuidType::class);
} else {
// Override Ramsay definition
Type::overrideType(FakeTestingSymfonyUuidType::NAME, FakeTestingSymfonyUuidType::class);
}
if (!Type::hasType(FakeTestingSymfonyUlidType::NAME)) {
Type::addType(FakeTestingSymfonyUlidType::NAME, FakeTestingSymfonyUlidType::class);
}
} else {
if (!Type::hasType(FakeTestingRamseyUuidType::NAME)) {
Type::addType(FakeTestingRamseyUuidType::NAME, FakeTestingRamseyUuidType::class);
} else {
// Override Symfony definition
Type::overrideType(FakeTestingRamseyUuidType::NAME, FakeTestingRamseyUuidType::class);
}
}
if (!Type::hasType('carbon')) {
Type::addType('carbon', CarbonType::class);
Expand All @@ -76,8 +95,10 @@ protected function getRule(): Rule
new IntegerType(),
new StringType(),
new SimpleArrayType(),
new UuidTypeDescriptor(FakeTestingUuidType::class),
new EnumType(),
new RamseyUuidTypeDescriptor(FakeTestingRamseyUuidType::class),
new SymfonyUuidTypeDescriptor(FakeTestingSymfonyUuidType::class),
new SymfonyUlidTypeDescriptor(FakeTestingSymfonyUlidType::class),
new ReflectionDescriptor(CarbonImmutableType::class, $this->createReflectionProvider(), self::getContainer()),
new ReflectionDescriptor(CarbonType::class, $this->createReflectionProvider(), self::getContainer()),
new ReflectionDescriptor(CustomType::class, $this->createReflectionProvider(), self::getContainer()),
Expand Down Expand Up @@ -461,6 +482,27 @@ public function testBug677(?string $objectManagerLoader): void
$this->analyse([__DIR__ . '/data/bug-677.php'], []);
}

/**
* @dataProvider dataObjectManagerLoader
*/
public function testSymfonyUuid(?string $objectManagerLoader): void
{
$this->allowNullablePropertyForRequiredField = true;
$this->objectManagerLoader = $objectManagerLoader;
$this->useSymfonyUuid = true;

$this->analyse([__DIR__ . '/data/EntityWithSymfonyUid.php'], [
[
'Property PHPStan\Rules\Doctrine\ORM\EntityWithSymfonyUid::$uuidInvalidType type mapping mismatch: database can contain Symfony\Component\Uid\Uuid but property expects string.',
32,
],
[
'Property PHPStan\Rules\Doctrine\ORM\EntityWithSymfonyUid::$ulidInvalidType type mapping mismatch: database can contain Symfony\Component\Uid\Ulid but property expects string.',
44,
],
]);
}

/**
* @dataProvider dataObjectManagerLoader
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* From https://github.com/ramsey/uuid-doctrine/blob/fafebbe972cdaba9274c286ea8923e2de2579027/src/UuidType.php
* Copyright (c) 2012-2022 Ben Ramsey <[email protected]>
*/
final class FakeTestingUuidType extends GuidType
final class FakeTestingRamseyUuidType extends GuidType
{

public const NAME = 'uuid';
Expand Down
113 changes: 113 additions & 0 deletions tests/Rules/Doctrine/ORM/FakeTestingSymfonyUlidType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Doctrine\ORM;

use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\ConversionException;
use Doctrine\DBAL\Types\Type;
use InvalidArgumentException;
use Symfony\Component\Uid\AbstractUid;
use Symfony\Component\Uid\Ulid;
use function is_string;

/**
* From https://github.com/symfony/doctrine-bridge/blob/f5a2780640342e3bf0599fbfc9b3dd759db9b037/Types/UuidType.php
* Copyright (c) Fabien Potencier <[email protected]>
*/
final class FakeTestingSymfonyUlidType extends Type
{

public const NAME = 'ulid';

/**
* @not-deprecated
*/
public function getName(): string
{
return self::NAME;
}

protected function getUidClass(): string
{
return Ulid::class;
}

/**
* {@inheritdoc}
*/
public function getSQLDeclaration(array $column, AbstractPlatform $platform): string
{
if ($this->hasNativeGuidType($platform)) {
return $platform->getGuidTypeDeclarationSQL($column);
}

return $platform->getBinaryTypeDeclarationSQL([
'length' => '16',
'fixed' => true,
]);
}

/**
* {@inheritdoc}
*
* @throws ConversionException
*/
public function convertToPHPValue($value, AbstractPlatform $platform): ?AbstractUid
{
if ($value instanceof AbstractUid || $value === null) {
return $value;
}

if (!is_string($value)) {
throw ConversionException::conversionFailedInvalidType($value, $this->getName(), ['null', 'string', AbstractUid::class]);
}

try {
/** @phpstan-ignore-next-line method.dynamicName */
return $this->getUidClass()::fromString($value);
} catch (InvalidArgumentException $e) {
throw ConversionException::conversionFailed($value, $this->getName(), $e);
}
}

/**
* {@inheritdoc}
*
* @throws ConversionException
*/
public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string
{
$toString = $this->hasNativeGuidType($platform) ? 'toRfc4122' : 'toBinary';

if ($value instanceof AbstractUid) {
/** @phpstan-ignore-next-line method.dynamicName */
return $value->$toString();
}

if ($value === null || $value === '') {
return null;
}

if (!is_string($value)) {
throw ConversionException::conversionFailedInvalidType($value, $this->getName(), ['null', 'string', AbstractUid::class]);
}

try {
/** @phpstan-ignore-next-line method.dynamicName */
return $this->getUidClass()::fromString($value)->$toString();
} catch (InvalidArgumentException $e) {
throw ConversionException::conversionFailed($value, $this->getName());
}
}

public function requiresSQLCommentHint(AbstractPlatform $platform): bool
{
return true;
}

private function hasNativeGuidType(AbstractPlatform $platform): bool
{
return $platform->getGuidTypeDeclarationSQL([]) !== $platform->getStringTypeDeclarationSQL(['fixed' => true, 'length' => 36]);
}

}
Loading
Loading