Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge 2.17.x up into 2.18.x #11141

Merged
merged 4 commits into from
Dec 28, 2023
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
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 @@ -175,6 +175,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 @@ -93,18 +91,14 @@ public function addEdge($from, $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 @@ -147,7 +141,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 @@ -160,6 +154,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];
}
}
14 changes: 8 additions & 6 deletions lib/Doctrine/ORM/UnitOfWork.php
Original file line number Diff line number Diff line change
Expand Up @@ -1378,9 +1378,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 @@ -1432,9 +1433,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
146 changes: 146 additions & 0 deletions tests/Doctrine/Tests/ORM/Functional/Ticket/GH11058Test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
<?php

declare(strict_types=1);

namespace Doctrine\Tests\ORM\Functional\Ticket;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Tests\OrmFunctionalTestCase;

class GH11058Test extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();

$this->setUpEntitySchema([
GH11058Parent::class,
GH11058Child::class,
]);
}

public function testChildrenInsertedInOrderOfPersistCalls1WhenParentPersistedLast(): void
{
[$parent, $child1, $child2] = $this->createParentWithTwoChildEntities();

$this->_em->persist($child1);
$this->_em->persist($child2);
$this->_em->persist($parent);
$this->_em->flush();

self::assertTrue($child1->id < $child2->id);
}

public function testChildrenInsertedInOrderOfPersistCalls2WhenParentPersistedLast(): void
{
[$parent, $child1, $child2] = $this->createParentWithTwoChildEntities();

$this->_em->persist($child2);
$this->_em->persist($child1);
$this->_em->persist($parent);
$this->_em->flush();

self::assertTrue($child2->id < $child1->id);
}

public function testChildrenInsertedInOrderOfPersistCalls1WhenParentPersistedFirst(): void
{
[$parent, $child1, $child2] = $this->createParentWithTwoChildEntities();

$this->_em->persist($parent);
$this->_em->persist($child1);
$this->_em->persist($child2);
$this->_em->flush();

self::assertTrue($child1->id < $child2->id);
}

public function testChildrenInsertedInOrderOfPersistCalls2WhenParentPersistedFirst(): void
{
[$parent, $child1, $child2] = $this->createParentWithTwoChildEntities();

$this->_em->persist($parent);
$this->_em->persist($child2);
$this->_em->persist($child1);
$this->_em->flush();

self::assertTrue($child2->id < $child1->id);
}

private function createParentWithTwoChildEntities(): array
{
$parent = new GH11058Parent();
$child1 = new GH11058Child();
$child2 = new GH11058Child();

$parent->addChild($child1);
$parent->addChild($child2);

return [$parent, $child1, $child2];
}
}

/**
* @ORM\Entity()
*/
class GH11058Parent
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue
*
* @var int
*/
public $id;

/**
* @ORM\OneToMany(targetEntity="GH11058Child", mappedBy="parent")
*
* @var Collection<int, GH11058Child>
*/
public $children;

public function __construct()
{
$this->children = new ArrayCollection();
}

public function addChild(GH11058Child $child): void
{
if (! $this->children->contains($child)) {
$this->children->add($child);
$child->setParent($this);
}
}
}

/**
* @ORM\Entity()
*/
class GH11058Child
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue
*
* @var int
*/
public $id;

/**
* @ORM\ManyToOne(targetEntity="GH11058Parent", inversedBy="children")
*
* @var GH11058Parent
*/
public $parent;

public function setParent(GH11058Parent $parent): void
{
$this->parent = $parent;
$parent->addChild($this);
}
}
Loading