Skip to content

Commit

Permalink
feature #6140 Add Doctrine ORM 3.x compatibility (javiereguiluz)
Browse files Browse the repository at this point in the history
This PR was squashed before being merged into the 4.x branch.

Discussion
----------

Add Doctrine ORM 3.x compatibility

Fixes #6133.

Commits
-------

d8e7099 Add Doctrine ORM 3.x compatibility
  • Loading branch information
javiereguiluz committed Feb 7, 2024
2 parents b23a0b5 + d8e7099 commit 1932ed3
Show file tree
Hide file tree
Showing 12 changed files with 85 additions and 38 deletions.
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"php": ">=8.0.2",
"ext-json": "*",
"doctrine/doctrine-bundle": "^2.5",
"doctrine/orm": "^2.10",
"doctrine/orm": "^2.10|^3.0",
"symfony/asset": "^5.4|^6.0|^7.0",
"symfony/cache": "^5.4|^6.0|^7.0",
"symfony/config": "^5.4|^6.0|^7.0",
Expand Down
46 changes: 36 additions & 10 deletions src/Dto/EntityDto.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@

namespace EasyCorp\Bundle\EasyAdminBundle\Dto;

use Doctrine\ORM\Mapping\ClassMetadataInfo;
use Doctrine\Persistence\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\FieldMapping;
use Doctrine\ORM\Mapping\ManyToManyAssociationMapping;
use Doctrine\ORM\Mapping\ManyToOneAssociationMapping;
use Doctrine\ORM\Mapping\OneToManyAssociationMapping;
use Doctrine\ORM\Mapping\OneToOneAssociationMapping;
use EasyCorp\Bundle\EasyAdminBundle\Collection\ActionCollection;
use EasyCorp\Bundle\EasyAdminBundle\Collection\FieldCollection;
use EasyCorp\Bundle\EasyAdminBundle\Config\KeyValueStore;
Expand All @@ -17,7 +21,6 @@ final class EntityDto
{
private bool $isAccessible = true;
private string $fqcn;
/** @var ClassMetadataInfo */
private ClassMetadata $metadata;
private $instance;
private $primaryKeyName;
Expand All @@ -26,9 +29,6 @@ final class EntityDto
private ?FieldCollection $fields = null;
private ?ActionCollection $actions = null;

/**
* @param ClassMetadata&ClassMetadataInfo $entityMetadata
*/
public function __construct(string $entityFqcn, ClassMetadata $entityMetadata, string|Expression|null $entityPermission = null, /* ?object */ $entityInstance = null)
{
if (!\is_object($entityInstance)
Expand Down Expand Up @@ -162,11 +162,37 @@ public function getAllPropertyNames(): array
public function getPropertyMetadata(string $propertyName): KeyValueStore
{
if (\array_key_exists($propertyName, $this->metadata->fieldMappings)) {
return KeyValueStore::new($this->metadata->fieldMappings[$propertyName]);
/** @var FieldMapping|array $fieldMapping */
$fieldMapping = $this->metadata->fieldMappings[$propertyName];
// Doctrine ORM 2.x returns an array and Doctrine ORM 3.x returns a FieldMapping object
if ($fieldMapping instanceof FieldMapping) {
$fieldMapping = (array) $fieldMapping;
}

return KeyValueStore::new($fieldMapping);
}

if (\array_key_exists($propertyName, $this->metadata->associationMappings)) {
return KeyValueStore::new($this->metadata->associationMappings[$propertyName]);
/** @var OneToOneAssociationMapping|OneToManyAssociationMapping|ManyToOneAssociationMapping|ManyToManyAssociationMapping|array $associationMapping */
$associationMapping = $this->metadata->associationMappings[$propertyName];
// Doctrine ORM 2.x returns an array and Doctrine ORM 3.x returns one of the many *Mapping objects
// there's not a single interface implemented by all of them, so let's only check if it's an object
if (\is_object($associationMapping)) {
// Doctrine ORM 3.x doesn't include the 'type' key that tells the type of association
// recreate that key to keep the code compatible with both versions
$associationType = match (true) {
$associationMapping instanceof OneToOneAssociationMapping => ClassMetadata::ONE_TO_ONE,
$associationMapping instanceof OneToManyAssociationMapping => ClassMetadata::ONE_TO_MANY,
$associationMapping instanceof ManyToOneAssociationMapping => ClassMetadata::MANY_TO_ONE,
$associationMapping instanceof ManyToManyAssociationMapping => ClassMetadata::MANY_TO_MANY,
default => null,
};

$associationMapping = (array) $associationMapping;
$associationMapping['type'] = $associationType;
}

return KeyValueStore::new($associationMapping);
}

throw new \InvalidArgumentException(sprintf('The "%s" field does not exist in the "%s" entity.', $propertyName, $this->getFqcn()));
Expand All @@ -193,14 +219,14 @@ public function isToOneAssociation(string $propertyName): bool
{
$associationType = $this->getPropertyMetadata($propertyName)->get('type');

return \in_array($associationType, [ClassMetadataInfo::ONE_TO_ONE, ClassMetadataInfo::MANY_TO_ONE], true);
return \in_array($associationType, [ClassMetadata::ONE_TO_ONE, ClassMetadata::MANY_TO_ONE], true);
}

public function isToManyAssociation(string $propertyName): bool
{
$associationType = $this->getPropertyMetadata($propertyName)->get('type');

return \in_array($associationType, [ClassMetadataInfo::ONE_TO_MANY, ClassMetadataInfo::MANY_TO_MANY], true);
return \in_array($associationType, [ClassMetadata::ONE_TO_MANY, ClassMetadata::MANY_TO_MANY], true);
}

public function isEmbeddedClassProperty(string $propertyName): bool
Expand Down
25 changes: 17 additions & 8 deletions src/Factory/EntityFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@

namespace EasyCorp\Bundle\EasyAdminBundle\Factory;

use Doctrine\Common\Util\ClassUtils;
use Doctrine\ORM\Mapping\ClassMetadataInfo;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\Persistence\ManagerRegistry;
use Doctrine\Persistence\Mapping\ClassMetadata;
use Doctrine\Persistence\ObjectManager;
use Doctrine\Persistence\Proxy;
use EasyCorp\Bundle\EasyAdminBundle\Collection\ActionCollection;
Expand Down Expand Up @@ -94,13 +92,10 @@ public function createCollection(EntityDto $entityDto, ?iterable $entityInstance
return EntityCollection::new($entityDtos);
}

/**
* @return ClassMetadata&ClassMetadataInfo
*/
public function getEntityMetadata(string $entityFqcn): ClassMetadata
{
$entityManager = $this->getEntityManager($entityFqcn);
/** @var ClassMetadata&ClassMetadataInfo $entityMetadata */
/** @var ClassMetadata $entityMetadata */
$entityMetadata = $entityManager->getClassMetadata($entityFqcn);

if (1 !== \count($entityMetadata->getIdentifierFieldNames())) {
Expand All @@ -121,7 +116,7 @@ private function doCreate(?string $entityFqcn = null, $entityId = null, string|E
$entityInstance->__load();
}

$entityFqcn = ClassUtils::getClass($entityInstance);
$entityFqcn = $this->getRealClass($entityInstance::class);
}

$entityMetadata = $this->getEntityMetadata($entityFqcn);
Expand Down Expand Up @@ -156,4 +151,18 @@ private function getEntityInstance(string $entityFqcn, $entityIdValue): object

return $entityInstance;
}

/**
* Code copied from Symfony\Bridge\Doctrine\Form\DoctrineOrmTypeGuesser
* because Doctrine ORM 3.x removed the ClassUtil class where this method was defined
* (c) Fabien Potencier <fabien@symfony.com> - MIT License.
*/
private function getRealClass(string $class): string
{
if (false === $pos = strrpos($class, '\\'.Proxy::MARKER.'\\')) {
return $class;
}

return substr($class, $pos + Proxy::MARKER_LENGTH + 2);
}
}
4 changes: 2 additions & 2 deletions src/Factory/FieldFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
final class FieldFactory
{
private static array $doctrineTypeToFieldFqcn = [
Types::ARRAY => ArrayField::class,
'array' => ArrayField::class, // don't use Types::ARRAY because it was removed in Doctrine ORM 3.0
Types::BIGINT => TextField::class,
Types::BINARY => TextareaField::class,
Types::BLOB => TextareaField::class,
Expand All @@ -46,7 +46,7 @@ final class FieldFactory
Types::GUID => TextField::class,
Types::INTEGER => IntegerField::class,
Types::JSON => TextField::class,
Types::OBJECT => TextField::class,
'object' => TextField::class, // don't use Types::OBJECT because it was removed in Doctrine ORM 3.0
Types::SIMPLE_ARRAY => ArrayField::class,
Types::SMALLINT => IntegerField::class,
Types::STRING => TextField::class,
Expand Down
4 changes: 2 additions & 2 deletions src/Factory/FilterFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ final class FilterFactory
private static array $doctrineTypeToFilterClass = [
'json_array' => ArrayFilter::class,
Types::SIMPLE_ARRAY => ArrayFilter::class,
Types::ARRAY => ArrayFilter::class,
'array' => ArrayFilter::class, // don't use Types::ARRAY because it was removed in Doctrine ORM 3.0
Types::JSON => TextFilter::class,
Types::BOOLEAN => BooleanFilter::class,
Types::DATE_MUTABLE => DateTimeFilter::class,
Expand All @@ -48,7 +48,7 @@ final class FilterFactory
Types::GUID => TextFilter::class,
Types::STRING => TextFilter::class,
Types::BLOB => TextFilter::class,
Types::OBJECT => TextFilter::class,
'object' => TextFilter::class, // don't use Types::OBJECT because it was removed in Doctrine ORM 3.0
Types::TEXT => TextFilter::class,
];

Expand Down
3 changes: 2 additions & 1 deletion src/Filter/Configurator/TextConfigurator.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ public function configure(FilterDto $filterDto, ?FieldDto $fieldDto, EntityDto $
$filterDto->setFormTypeOption('value_type', TextareaType::class);
}

if (\in_array($propertyType, [Types::BLOB, Types::OBJECT, Types::TEXT], true)) {
// don't use Types::OBJECT because it was removed in Doctrine ORM 3.0
if (\in_array($propertyType, [Types::BLOB, 'object', Types::TEXT], true)) {
$filterDto->setFormTypeOptionIfNotSet('value_type', TextareaType::class);
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/Orm/EntityRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\ClassMetadataInfo;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Query\Expr\Orx;
use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ManagerRegistry;
Expand Down Expand Up @@ -162,7 +162,7 @@ private function addOrderClause(QueryBuilder $queryBuilder, SearchDto $searchDto
$entityManager = $this->doctrine->getManagerForClass($entityDto->getFqcn());
$countQueryBuilder = $entityManager->createQueryBuilder();

if (ClassMetadataInfo::MANY_TO_MANY === $metadata->get('type')) {
if (ClassMetadata::MANY_TO_MANY === $metadata->get('type')) {
// many-to-many relation
$countQueryBuilder
->select($queryBuilder->expr()->count('subQueryEntity'))
Expand Down
16 changes: 13 additions & 3 deletions src/Orm/Escaper.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace EasyCorp\Bundle\EasyAdminBundle\Orm;

use Doctrine\ORM\Query\Lexer;
use Doctrine\ORM\Query\TokenType;

class Escaper
{
Expand Down Expand Up @@ -40,10 +41,19 @@ private static function isDqlReservedKeyword(string $string): bool
/** @phpstan-ignore-next-line */
$type = \is_array($token) ? $token['type'] : $token->type;

if (200 <= $type) {
return true;
// Doctrine ORM 3.x changed this and the type is now a TokenType object
if ($type instanceof TokenType) {
$type = $type->value;
}

return false;
// tokens that are not valid identifiers (e.g. T_OPEN_PARENTHESIS, T_EQUALS) are < 100
// see https://www.doctrine-project.org/projects/doctrine-lexer/en/3.1/dql-parser.html
if ($type < 100) {
throw new \RuntimeException(sprintf('The "%s" string is not a valid identifier in Doctrine queries.', $string));
}

// tokens that are keywords (e.g. T_AND, T_JOIN, T_ORDER) are >= 200
// see https://www.doctrine-project.org/projects/doctrine-lexer/en/3.1/dql-parser.html
return $type >= 200;
}
}
9 changes: 5 additions & 4 deletions src/Provider/FieldProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,12 @@ public function getDefaultFields(string $pageName): array
$maxNumProperties = Crud::PAGE_INDEX === $pageName ? 7 : \PHP_INT_MAX;
$entityDto = $this->adminContextProvider->getContext()->getEntity();

// don't use Types::OBJECT because it was removed in Doctrine ORM 3.0
$excludedPropertyTypes = [
Crud::PAGE_EDIT => [Types::BINARY, Types::BLOB, Types::JSON, Types::OBJECT],
Crud::PAGE_INDEX => [Types::BINARY, Types::BLOB, Types::GUID, Types::JSON, Types::OBJECT, Types::TEXT],
Crud::PAGE_NEW => [Types::BINARY, Types::BLOB, Types::JSON, Types::OBJECT],
Crud::PAGE_DETAIL => [Types::BINARY, Types::JSON, Types::OBJECT],
Crud::PAGE_EDIT => [Types::BINARY, Types::BLOB, Types::JSON, 'object'],
Crud::PAGE_INDEX => [Types::BINARY, Types::BLOB, Types::GUID, Types::JSON, 'object', Types::TEXT],
Crud::PAGE_NEW => [Types::BINARY, Types::BLOB, Types::JSON, 'object'],
Crud::PAGE_DETAIL => [Types::BINARY, Types::JSON, 'object'],
];

$excludedPropertyNames = [
Expand Down
4 changes: 2 additions & 2 deletions tests/Field/Configurator/ChoiceConfiguratorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace EasyCorp\Bundle\EasyAdminBundle\Tests\Field\Configurator;

use Doctrine\ORM\Mapping\ClassMetadataInfo;
use Doctrine\ORM\Mapping\ClassMetadata;
use EasyCorp\Bundle\EasyAdminBundle\Dto\EntityDto;
use EasyCorp\Bundle\EasyAdminBundle\Dto\FieldDto;
use EasyCorp\Bundle\EasyAdminBundle\Field\ChoiceField;
Expand All @@ -26,7 +26,7 @@ protected function setUp(): void

$this->configurator = new ChoiceConfigurator();

$metadata = new ClassMetadataInfo(self::ENTITY_CLASS);
$metadata = new ClassMetadata(self::ENTITY_CLASS);
$metadata->setIdentifier(['id']);
$this->entity = new EntityDto(self::ENTITY_CLASS, $metadata);
}
Expand Down
2 changes: 1 addition & 1 deletion tests/TestApplication/src/DataFixtures/AppFixtures.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public function load(ObjectManager $manager)
->setTitle('Blog Post '.$i)
->setSlug('blog-post-'.$i)
->setContent('Lorem Ipsum Dolor Sit Amet.')
->setCreatedAt(new \DateTime('2020-11-'.($i + 1).' 09:00:00'))
->setCreatedAt(new \DateTimeImmutable('2020-11-'.($i + 1).' 09:00:00'))
->setPublishedAt(new \DateTimeImmutable('2020-11-'.($i + 1).' 11:00:00'))
->addCategory($this->getReference('category'.($i % 10), Category::class))
->setAuthor($this->getReference('user'.($i % 5), User::class));
Expand Down
4 changes: 2 additions & 2 deletions tests/TestApplication/src/Entity/BlogPost.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class BlogPost
#[ORM\ManyToMany(targetEntity: Category::class, inversedBy: 'blogPosts')]
private $categories;

#[ORM\Column(type: 'datetime')]
#[ORM\Column(type: 'datetime_immutable')]
private $createdAt;

#[ORM\Column(type: 'datetime_immutable', nullable: true)]
Expand Down Expand Up @@ -114,7 +114,7 @@ public function getCreatedAt(): ?\DateTimeInterface
return $this->createdAt;
}

public function setCreatedAt(\DateTimeInterface $createdAt): self
public function setCreatedAt(\DateTimeImmutable $createdAt): self
{
$this->createdAt = $createdAt;

Expand Down

0 comments on commit 1932ed3

Please sign in to comment.