Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/2.18.x' into 3.0.x
Browse files Browse the repository at this point in the history
  • Loading branch information
greg0ire committed Jan 2, 2024
2 parents 28d03e4 + a98e306 commit 43c74c9
Show file tree
Hide file tree
Showing 19 changed files with 420 additions and 54 deletions.
2 changes: 1 addition & 1 deletion docs/en/cookbook/resolve-target-entity-listener.rst
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ the targetEntity resolution will occur reliably:
// Add the ResolveTargetEntityListener
$evm->addEventListener(Doctrine\ORM\Events::loadClassMetadata, $rtel);
$connection = \Doctrine\DBAL\DriverManager::createConnection($connectionOptions, $config, $evm);
$connection = \Doctrine\DBAL\DriverManager::getConnection($connectionOptions, $config, $evm);
$em = new \Doctrine\ORM\EntityManager($connection, $config, $evm);
Final Thoughts
Expand Down
5 changes: 5 additions & 0 deletions docs/en/reference/dql-doctrine-query-language.rst
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,11 @@ hierarchies:
$query = $em->createQuery('SELECT u FROM Doctrine\Tests\Models\Company\CompanyPerson u WHERE u INSTANCE OF Doctrine\Tests\Models\Company\CompanyEmployee');
$query = $em->createQuery('SELECT u FROM Doctrine\Tests\Models\Company\CompanyPerson u WHERE u INSTANCE OF ?1');
$query = $em->createQuery('SELECT u FROM Doctrine\Tests\Models\Company\CompanyPerson u WHERE u NOT INSTANCE OF ?1');
$query->setParameter(0, $em->getClassMetadata(CompanyEmployee::class));
.. note::
To use a class as parameter, you have to bind its class metadata:
``$query->setParameter(0, $em->getClassMetadata(CompanyEmployee::class);``.

Get all users visible on a given website that have chosen certain gender:

Expand Down
16 changes: 14 additions & 2 deletions docs/en/reference/limitations-and-known-issues.rst
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
Limitations and Known Issues
============================

We try to make using Doctrine2 a very pleasant experience.
We try to make using Doctrine ORM a very pleasant experience.
Therefore we think it is very important to be honest about the
current limitations to our users. Much like every other piece of
software Doctrine2 is not perfect and far from feature complete.
software the ORM is not perfect and far from feature complete.
This section should give you an overview of current limitations of
Doctrine ORM as well as critical known issues that you should know
about.
Expand Down Expand Up @@ -166,6 +166,18 @@ due to complexity.
XML mapping configuration probably needs to completely re-configure or otherwise
copy-and-paste configuration for fields used from traits.

Mapping multiple private fields of the same name
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

When two classes, say a mapped superclass and an entity inheriting from it,
both contain a ``private`` field of the same name, this will lead to a ``MappingException``.

Since the fields are ``private``, both are technically separate and can contain
different values at the same time. However, the ``ClassMetadata`` configuration used
internally by the ORM currently refers to fields by their name only, without taking the
class containing the field into consideration. This makes it impossible to keep separate
mapping configuration for both fields.

Known Issues
------------

Expand Down
16 changes: 5 additions & 11 deletions lib/Doctrine/ORM/Internal/TopologicalSort.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@
use Doctrine\ORM\Internal\TopologicalSort\CycleDetectedException;

use function array_keys;
use function array_reverse;
use function array_unshift;
use function spl_object_id;

/**
Expand Down Expand Up @@ -89,18 +87,14 @@ public function addEdge(object $from, object $to, bool $optional): void

/**
* Returns a topological sort of all nodes. When we have an edge A->B between two nodes
* A and B, then A will be listed before B in the result.
* A and B, then B will be listed before A in the result. Visually speaking, when ordering
* the nodes in the result order from left to right, all edges point to the left.
*
* @return list<object>
*/
public function sort(): array
{
/*
* When possible, keep objects in the result in the same order in which they were added as nodes.
* Since nodes are unshifted into $this->>sortResult (see the visit() method), that means we
* need to work them in array_reverse order here.
*/
foreach (array_reverse(array_keys($this->nodes)) as $oid) {
foreach (array_keys($this->nodes) as $oid) {
if ($this->states[$oid] === self::NOT_VISITED) {
$this->visit($oid);
}
Expand Down Expand Up @@ -143,7 +137,7 @@ private function visit(int $oid): void
}

// We have found a cycle and cannot break it at $edge. Best we can do
// is to retreat from the current vertex, hoping that somewhere up the
// is to backtrack from the current vertex, hoping that somewhere up the
// stack this can be salvaged.
$this->states[$oid] = self::NOT_VISITED;
$exception->addToCycle($this->nodes[$oid]);
Expand All @@ -156,6 +150,6 @@ private function visit(int $oid): void
// So we're done with this vertex as well.

$this->states[$oid] = self::VISITED;
array_unshift($this->sortResult, $this->nodes[$oid]);
$this->sortResult[] = $this->nodes[$oid];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ protected function configure(): void
->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on')
->addOption('skip-mapping', null, InputOption::VALUE_NONE, 'Skip the mapping validation check')
->addOption('skip-sync', null, InputOption::VALUE_NONE, 'Skip checking if the mapping is in sync with the database')
->addOption('skip-property-types', null, InputOption::VALUE_NONE, 'Skip checking if property types match the Doctrine types')
->setHelp('Validate that the mapping files are correct and in sync with the database.');
}

Expand All @@ -35,7 +36,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$ui = (new SymfonyStyle($input, $output))->getErrorStyle();

$em = $this->getEntityManager($input);
$validator = new SchemaValidator($em);
$validator = new SchemaValidator($em, ! $input->getOption('skip-property-types'));
$exit = 0;

$ui->section('Mapping');
Expand Down
58 changes: 50 additions & 8 deletions lib/Doctrine/ORM/Tools/SchemaValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
use function get_class;
use function implode;
use function in_array;
use function interface_exists;
use function is_a;
use function sprintf;

Expand Down Expand Up @@ -68,8 +69,10 @@ class SchemaValidator
TextType::class => 'string',
];

public function __construct(private readonly EntityManagerInterface $em)
{
public function __construct(
private readonly EntityManagerInterface $em,
private readonly bool $validatePropertyTypes = true,
) {
}

/**
Expand Down Expand Up @@ -118,7 +121,7 @@ public function validateClass(ClassMetadata $class): array
}

// PHP 7.4 introduces the ability to type properties, so we can't validate them in previous versions
if (PHP_VERSION_ID >= 70400) {
if (PHP_VERSION_ID >= 70400 && $this->validatePropertyTypes) {
array_push($ce, ...$this->validatePropertiesTypes($class));
}

Expand Down Expand Up @@ -340,7 +343,7 @@ function (FieldMapping $fieldMapping) use ($class): string|null {
}

// If the property type is not a named type, we cannot check it
if (! ($propertyType instanceof ReflectionNamedType)) {
if (! ($propertyType instanceof ReflectionNamedType) || $propertyType->getName() === 'mixed') {
return null;
}

Expand All @@ -358,10 +361,20 @@ function (FieldMapping $fieldMapping) use ($class): string|null {
return null;
}

if (
is_a($propertyType, BackedEnum::class, true)
&& $metadataFieldType === (string) (new ReflectionEnum($propertyType))->getBackingType()
) {
if (is_a($propertyType, BackedEnum::class, true)) {
$backingType = (string) (new ReflectionEnum($propertyType))->getBackingType();

if ($metadataFieldType !== $backingType) {
return sprintf(
"The field '%s#%s' has the property type '%s' with a backing type of '%s' that differs from the metadata field type '%s'.",
$class->name,
$fieldName,
$propertyType,
$backingType,
$metadataFieldType,
);
}

if (! isset($fieldMapping['enumType']) || $propertyType === $fieldMapping['enumType']) {
return null;
}
Expand All @@ -375,6 +388,35 @@ function (FieldMapping $fieldMapping) use ($class): string|null {
);
}

if (
isset($fieldMapping['enumType'])
&& $propertyType !== $fieldMapping['enumType']
&& interface_exists($propertyType)
&& is_a($fieldMapping['enumType'], $propertyType, true)
) {
$backingType = (string) (new ReflectionEnum($fieldMapping['enumType']))->getBackingType();

if ($metadataFieldType === $backingType) {
return null;
}

return sprintf(
"The field '%s#%s' has the metadata enumType '%s' with a backing type of '%s' that differs from the metadata field type '%s'.",
$class->name,
$fieldName,
$fieldMapping['enumType'],
$backingType,
$metadataFieldType,
);
}

if (
$fieldMapping['type'] === 'json'
&& in_array($propertyType, ['string', 'int', 'float', 'bool', 'true', 'false', 'null'], true)
) {
return null;
}

return sprintf(
"The field '%s#%s' has the property type '%s' that differs from the metadata field type '%s' returned by the '%s' DBAL type.",
$class->name,
Expand Down
14 changes: 8 additions & 6 deletions lib/Doctrine/ORM/UnitOfWork.php
Original file line number Diff line number Diff line change
Expand Up @@ -1229,9 +1229,10 @@ private function computeInsertExecutionOrder(): array
$joinColumns = reset($assoc->joinColumns);
$isNullable = ! isset($joinColumns->nullable) || $joinColumns->nullable;

// Add dependency. The dependency direction implies that "$targetEntity has to go before $entity",
// so we can work through the topo sort result from left to right (with all edges pointing right).
$sort->addEdge($targetEntity, $entity, $isNullable);
// Add dependency. The dependency direction implies that "$entity depends on $targetEntity". The
// topological sort result will output the depended-upon nodes first, which means we can insert
// entities in that order.
$sort->addEdge($entity, $targetEntity, $isNullable);
}
}

Expand Down Expand Up @@ -1282,9 +1283,10 @@ private function computeDeleteExecutionOrder(): array
continue;
}

// Add dependency. The dependency direction implies that "$entity has to be removed before $targetEntity",
// so we can work through the topo sort result from left to right (with all edges pointing right).
$sort->addEdge($entity, $targetEntity, false);
// Add dependency. The dependency direction implies that "$targetEntity depends on $entity
// being deleted first". The topological sort will output the depended-upon nodes first,
// so we can work through the result in the returned order.
$sort->addEdge($targetEntity, $entity, false);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public function testSerializeParserResult(Closure $toSerializedAndBack): void
}

/** @return Generator<string, array{Closure(ParserResult): ParserResult}> */
public function provideToSerializedAndBack(): Generator
public static function provideToSerializedAndBack(): Generator
{
yield 'native serialization function' => [
static function (ParserResult $parserResult): ParserResult {
Expand All @@ -57,7 +57,7 @@ static function (ParserResult $parserResult): ParserResult {

$instantiatorMethod = new ReflectionMethod(Instantiator::class, 'instantiate');
if ($instantiatorMethod->getReturnType() === null) {
$this->markTestSkipped('symfony/var-exporter 5.4+ is required.');
self::markTestSkipped('symfony/var-exporter 5.4+ is required.');
}

yield 'symfony/var-exporter' => [
Expand Down
27 changes: 18 additions & 9 deletions tests/Doctrine/Tests/ORM/Functional/Ticket/GH10661/GH10661Test.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,32 +13,36 @@ final class GH10661Test extends OrmTestCase
/** @var EntityManagerInterface */
private $em;

/** @var SchemaValidator */
private $validator;

protected function setUp(): void
{
$this->em = $this->getTestEntityManager();
$this->validator = new SchemaValidator($this->em);
$this->em = $this->getTestEntityManager();
}

public function testMetadataFieldTypeNotCoherentWithEntityPropertyType(): void
{
$class = $this->em->getClassMetadata(InvalidEntity::class);
$ce = $this->validator->validateClass($class);
$ce = $this->bootstrapValidator()->validateClass($class);

self::assertEquals(
self::assertSame(
["The field 'Doctrine\Tests\ORM\Functional\Ticket\GH10661\InvalidEntity#property1' has the property type 'float' that differs from the metadata field type 'string' returned by the 'decimal' DBAL type."],
$ce,
);
}

public function testPropertyTypeErrorsCanBeSilenced(): void
{
$class = $this->em->getClassMetadata(InvalidEntity::class);
$ce = $this->bootstrapValidator(false)->validateClass($class);

self::assertSame([], $ce);
}

public function testMetadataFieldTypeNotCoherentWithEntityPropertyTypeWithInheritance(): void
{
$class = $this->em->getClassMetadata(InvalidChildEntity::class);
$ce = $this->validator->validateClass($class);
$ce = $this->bootstrapValidator()->validateClass($class);

self::assertEquals(
self::assertSame(
[
"The field 'Doctrine\Tests\ORM\Functional\Ticket\GH10661\InvalidChildEntity#property1' has the property type 'float' that differs from the metadata field type 'string' returned by the 'decimal' DBAL type.",
"The field 'Doctrine\Tests\ORM\Functional\Ticket\GH10661\InvalidChildEntity#property2' has the property type 'int' that differs from the metadata field type 'string' returned by the 'string' DBAL type.",
Expand All @@ -47,4 +51,9 @@ public function testMetadataFieldTypeNotCoherentWithEntityPropertyTypeWithInheri
$ce,
);
}

private function bootstrapValidator(bool $validatePropertyTypes = true): SchemaValidator
{
return new SchemaValidator($this->em, $validatePropertyTypes);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace Doctrine\Tests\ORM\Functional\Ticket\GH11037;

interface EntityStatus
{
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,9 @@ public function testMetadataFieldTypeNotCoherentWithEntityPropertyType(): void

self::assertEquals(
[
"The field 'Doctrine\Tests\ORM\Functional\Ticket\GH11037\InvalidEntityWithTypedEnum#status1' has the property type 'Doctrine\Tests\ORM\Functional\Ticket\GH11037\StringEntityStatus' that differs from the metadata field type 'int' returned by the 'integer' DBAL type.",
"The field 'Doctrine\Tests\ORM\Functional\Ticket\GH11037\InvalidEntityWithTypedEnum#status1' has the property type 'Doctrine\Tests\ORM\Functional\Ticket\GH11037\StringEntityStatus' with a backing type of 'string' that differs from the metadata field type 'int'.",
"The field 'Doctrine\Tests\ORM\Functional\Ticket\GH11037\InvalidEntityWithTypedEnum#status2' has the property type 'Doctrine\Tests\ORM\Functional\Ticket\GH11037\IntEntityStatus' that differs from the metadata enumType 'Doctrine\Tests\ORM\Functional\Ticket\GH11037\StringEntityStatus'.",
"The field 'Doctrine\Tests\ORM\Functional\Ticket\GH11037\InvalidEntityWithTypedEnum#status3' has the metadata enumType 'Doctrine\Tests\ORM\Functional\Ticket\GH11037\StringEntityStatus' with a backing type of 'string' that differs from the metadata field type 'int'.",
],
$ce,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,7 @@ class InvalidEntityWithTypedEnum

#[Column(type: 'integer', enumType: StringEntityStatus::class)]
protected IntEntityStatus $status2;

#[Column(type: 'integer', enumType: StringEntityStatus::class)]
protected EntityStatus $status3;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

namespace Doctrine\Tests\ORM\Functional\Ticket\GH11037;

enum StringEntityStatus: string
enum StringEntityStatus: string implements EntityStatus
{
case ACTIVE = 'active';
case INACTIVE = 'inactive';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,7 @@ class ValidEntityWithTypedEnum

#[Column(type: 'smallint', enumType: IntEntityStatus::class)]
protected IntEntityStatus $status2;

/** @Column(type="string", enumType=StringEntityStatus::class) */
protected EntityStatus $status3;
}
Loading

0 comments on commit 43c74c9

Please sign in to comment.