Skip to content
Open
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
18 changes: 15 additions & 3 deletions src/Form/EventListener/CrudAutocompleteSubscriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@

namespace EasyCorp\Bundle\EasyAdminBundle\Form\EventListener;

use Doctrine\DBAL\Types\Type;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Bridge\Doctrine\Types\UlidType;
use Symfony\Bridge\Doctrine\Types\UuidType;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
Expand Down Expand Up @@ -91,15 +94,24 @@ public function preSubmit(FormEvent $event): void
/** @phpstan-ignore-next-line function.alreadyNarrowedType */
$idFieldType = property_exists($idFieldMapping, 'type') ? $idFieldMapping->type : $idFieldMapping['type'];

// third-party packages can register their own Doctrine type for the 'uuid' and 'ulid' type
// names with a different storage format (e.g. ramsey/uuid-doctrine stores UUIDs as strings);
// convert the submitted values only when the registered type is the Symfony UID one, whose
// storage format the conversion below assumes; otherwise, pass the values through unchanged
// and let the registered type convert them when running the query
$registeredIdType = Type::hasType($idFieldType) ? Type::getType($idFieldType) : null;
$isSymfonyUlidType = null === $registeredIdType || $registeredIdType instanceof UlidType;
$isSymfonyUuidType = null === $registeredIdType || $registeredIdType instanceof UuidType;

$data['autocomplete'] = array_map(
static function ($v) use ($options, $idFieldType) {
static function ($v) use ($options, $idFieldType, $isSymfonyUlidType, $isSymfonyUuidType) {
// TODO: replace 'ulid' by Symfony\Bridge\Doctrine\Types\UlidType::NAME when Symfony 5.4 is no longer supported
if ('ulid' === $idFieldType && class_exists(Ulid::class) && Ulid::isValid($v)) {
if ('ulid' === $idFieldType && $isSymfonyUlidType && class_exists(Ulid::class) && Ulid::isValid($v)) {
return Ulid::fromBase32($v)->toRfc4122();
}

// TODO: replace 'uuid' by Symfony\Bridge\Doctrine\Types\UuidType::NAME when Symfony 5.4 is no longer supported
if ('uuid' === $idFieldType && class_exists(Uuid::class) && Uuid::isValid($v)) {
if ('uuid' === $idFieldType && $isSymfonyUuidType && class_exists(Uuid::class) && Uuid::isValid($v)) {
// Use RFC4122 format for platforms with native GUID type (e.g., PostgreSQL),
// and binary format for platforms without native GUID type (e.g., MySQL, SQLite)
$platform = $options['em']->getConnection()->getDatabasePlatform();
Expand Down
67 changes: 67 additions & 0 deletions tests/Unit/Form/CrudAutocompleteSubscriberTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
use Doctrine\DBAL\Types\GuidType;
use Doctrine\DBAL\Types\Type;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Mapping\ClassMetadata;
Expand All @@ -13,6 +15,8 @@
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Symfony\Bridge\Doctrine\Form\ChoiceList\IdReader;
use Symfony\Bridge\Doctrine\Types\UlidType;
use Symfony\Bridge\Doctrine\Types\UuidType;
use Symfony\Component\Form\FormConfigInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormInterface;
Expand All @@ -30,6 +34,11 @@ protected function setUp(): void
if (!class_exists(PostgreSQLPlatform::class) || !class_exists(MySQLPlatform::class)) {
$this->markTestSkipped('Doctrine DBAL 3.x+ is required (PostgreSQLPlatform/MySQLPlatform classes).');
}

// the subscriber inspects the globally registered Doctrine types, so reset
// 'uuid' and 'ulid' to the Symfony UID types to not depend on the test execution order
self::registerDoctrineType('uuid', UuidType::class);
self::registerDoctrineType('ulid', UlidType::class);
}

/** @dataProvider idFieldInDifferentPlatformsData */
Expand Down Expand Up @@ -115,6 +124,64 @@ public static function idFieldInDifferentPlatformsData(): array
];
}

/** @dataProvider idFieldWithNonSymfonyUidTypeData */
public function testIdFieldWithNonSymfonyUidType(
string $fieldType,
string $fieldValue,
string $platformClass,
): void {
// third-party types registered for the 'uuid'/'ulid' type names may use a different
// storage format than the Symfony UID types (e.g. ramsey/uuid-doctrine stores UUIDs
// as strings), so the subscriber must pass the submitted values through unchanged
Type::overrideType($fieldType, GuidType::class);

$platform = new $platformClass();
$capturedData = null;

$repo = $this->createMock(EntityRepository::class);
$repo
->expects($this->once())
->method('findBy')
->willReturnCallback(static function (array $criteria) use (&$capturedData) {
$capturedData = $criteria['id'][0];

return [];
});

$subscriber = new CrudAutocompleteSubscriber($this->createMock(Environment::class));
$subscriber->preSubmit(
$this->createFormEvent($platform, $repo, new FieldMapping($fieldType, 'id', 'id'), $fieldValue)
);

$this->assertSame($fieldValue, $capturedData, sprintf('%s must not convert the id of a non-Symfony "%s" type', $platform::class, $fieldType));
}

public static function idFieldWithNonSymfonyUidTypeData(): array
{
return [
'MySQL with UUID' => [
'uuid',
self::UUID_STRING,
'Doctrine\DBAL\Platforms\MySQLPlatform',
],
'PostgreSQL with UUID' => [
'uuid',
self::UUID_STRING,
'Doctrine\DBAL\Platforms\PostgreSQLPlatform',
],
'MySQL with ULID' => [
'ulid',
self::ULID_STRING,
'Doctrine\DBAL\Platforms\MySQLPlatform',
],
];
}

private static function registerDoctrineType(string $name, string $className): void
{
Type::hasType($name) ? Type::overrideType($name, $className) : Type::addType($name, $className);
}

private function createFormEvent(
object $platform,
EntityRepository $repository,
Expand Down
Loading