mirror of
https://github.com/doctrine/orm.git
synced 2026-03-24 06:52:09 +01:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
398ab0547a | ||
|
|
8f15337b03 | ||
|
|
a8632aca8f | ||
|
|
3dd3d38857 | ||
|
|
c6b3509aa9 | ||
|
|
a32578b7ea | ||
|
|
e585a92763 | ||
|
|
26f47cb8d3 | ||
|
|
ebb101009c | ||
|
|
f80ef66ffb | ||
|
|
85d78f8b0d | ||
|
|
c2886478e8 | ||
|
|
108fa30db2 | ||
|
|
e5ab18ff80 | ||
|
|
665ccf1376 |
@@ -173,6 +173,19 @@ Events Overview
|
||||
| :ref:`onClear<reference-events-on-clear>` | ``$em->clear()`` | No | `OnClearEventArgs`_ |
|
||||
+-----------------------------------------------------------------+-----------------------+-----------+-------------------------------------+
|
||||
|
||||
.. warning::
|
||||
|
||||
Making changes to entities and calling ``EntityManager::flush()`` from within
|
||||
event handlers dispatched by ``EntityManager::flush()`` itself is strongly
|
||||
discouraged, and might be deprecated and eventually prevented in the future.
|
||||
|
||||
The reason is that it causes re-entrance into ``UnitOfWork::commit()`` while a commit
|
||||
is currently being processed. The ``UnitOfWork`` was never designed to support this,
|
||||
and its behavior in this situation is not covered by any tests.
|
||||
|
||||
This may lead to entity or collection updates being missed, applied only in parts and
|
||||
changes being lost at the end of the commit phase.
|
||||
|
||||
Naming convention
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -699,30 +712,33 @@ Restrictions for this event:
|
||||
postUpdate, postRemove, postPersist
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
These three post* events are called inside ``EntityManager::flush()``.
|
||||
These three ``post*`` events are called inside ``EntityManager::flush()``.
|
||||
Changes in here are not relevant to the persistence in the
|
||||
database, but you can use these events to alter non-persistable items,
|
||||
like non-mapped fields, logging or even associated classes that are
|
||||
not directly mapped by Doctrine.
|
||||
|
||||
- The ``postUpdate`` event occurs after the database
|
||||
update operations to entity data. It is not called for a DQL
|
||||
``UPDATE`` statement.
|
||||
update operations to entity data, but before the database transaction
|
||||
has been committed. It is not called for a DQL ``UPDATE`` statement.
|
||||
- The ``postPersist`` event occurs for an entity after the entity has
|
||||
been made persistent. It will be invoked after all database insert
|
||||
operations for new entities have been performed. Generated primary
|
||||
key values will be available for all entities at the time this
|
||||
event is triggered.
|
||||
operations for new entities have been performed, but before the database
|
||||
transaction has been committed. Generated primary key values will be
|
||||
available for all entities at the time this event is triggered.
|
||||
- The ``postRemove`` event occurs for an entity after the
|
||||
entity has been deleted. It will be invoked after all database
|
||||
delete operations for entity rows have been executed. This event is
|
||||
not called for a DQL ``DELETE`` statement.
|
||||
delete operations for entity rows have been executed, but before the
|
||||
database transaction has been committed. This event is not called for
|
||||
a DQL ``DELETE`` statement.
|
||||
|
||||
.. note::
|
||||
|
||||
At the time ``postPersist`` is called, there may still be collection and/or
|
||||
"extra" updates pending. The database may not yet be completely in
|
||||
sync with the entity states in memory, not even for the new entities.
|
||||
sync with the entity states in memory, not even for the new entities. Similarly,
|
||||
also at the time ``postUpdate`` and ``postRemove`` are called, in-memory collections
|
||||
may still be in a "dirty" state or still contain removed entities.
|
||||
|
||||
.. warning::
|
||||
|
||||
@@ -731,19 +747,6 @@ not directly mapped by Doctrine.
|
||||
cascade remove relations. In this case, you should load yourself the proxy in
|
||||
the associated ``pre*`` event.
|
||||
|
||||
.. warning::
|
||||
|
||||
Making changes to entities and calling ``EntityManager::flush()`` from within
|
||||
``post*`` event handlers is strongly discouraged, and might be deprecated and
|
||||
eventually prevented in the future.
|
||||
|
||||
The reason is that it causes re-entrance into ``UnitOfWork::commit()`` while a commit
|
||||
is currently being processed. The ``UnitOfWork`` was never designed to support this,
|
||||
and its behavior in this situation is not covered by any tests.
|
||||
|
||||
This may lead to entity or collection updates being missed, applied only in parts and
|
||||
changes being lost at the end of the commit phase.
|
||||
|
||||
.. _reference-events-post-load:
|
||||
|
||||
postLoad
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
------------
|
||||
|
||||
|
||||
@@ -148,7 +148,6 @@
|
||||
<xs:choice minOccurs="0" maxOccurs="unbounded">
|
||||
<xs:element name="entity-result" type="orm:entity-result"/>
|
||||
<xs:element name="column-result" type="orm:column-result"/>
|
||||
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
|
||||
</xs:choice>
|
||||
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
|
||||
</xs:choice>
|
||||
@@ -226,22 +225,13 @@
|
||||
|
||||
<xs:complexType name="mapped-superclass" >
|
||||
<xs:complexContent>
|
||||
<xs:extension base="orm:entity">
|
||||
<xs:choice minOccurs="0" maxOccurs="unbounded">
|
||||
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
|
||||
</xs:choice>
|
||||
<xs:anyAttribute namespace="##other"/>
|
||||
</xs:extension>
|
||||
<xs:extension base="orm:entity"/>
|
||||
</xs:complexContent>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="embeddable">
|
||||
<xs:complexContent>
|
||||
<xs:extension base="orm:entity">
|
||||
<xs:choice minOccurs="0" maxOccurs="unbounded">
|
||||
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
|
||||
</xs:choice>
|
||||
</xs:extension>
|
||||
<xs:extension base="orm:entity"/>
|
||||
</xs:complexContent>
|
||||
</xs:complexType>
|
||||
|
||||
@@ -565,7 +555,6 @@
|
||||
<xs:choice minOccurs="0" maxOccurs="1">
|
||||
<xs:element name="join-column" type="orm:join-column"/>
|
||||
<xs:element name="join-columns" type="orm:join-columns"/>
|
||||
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
|
||||
</xs:choice>
|
||||
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
|
||||
</xs:choice>
|
||||
@@ -583,7 +572,6 @@
|
||||
<xs:choice minOccurs="0" maxOccurs="1">
|
||||
<xs:element name="join-column" type="orm:join-column"/>
|
||||
<xs:element name="join-columns" type="orm:join-columns"/>
|
||||
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
|
||||
</xs:choice>
|
||||
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
|
||||
</xs:choice>
|
||||
|
||||
170
lib/Doctrine/ORM/Internal/StronglyConnectedComponents.php
Normal file
170
lib/Doctrine/ORM/Internal/StronglyConnectedComponents.php
Normal file
@@ -0,0 +1,170 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Internal;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
use function array_keys;
|
||||
use function array_pop;
|
||||
use function array_push;
|
||||
use function min;
|
||||
use function spl_object_id;
|
||||
|
||||
/**
|
||||
* StronglyConnectedComponents implements Tarjan's algorithm to find strongly connected
|
||||
* components (SCC) in a directed graph. This algorithm has a linear running time based on
|
||||
* nodes (V) and edges between the nodes (E), resulting in a computational complexity
|
||||
* of O(V + E).
|
||||
*
|
||||
* See https://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm
|
||||
* for an explanation and the meaning of the DFS and lowlink numbers.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class StronglyConnectedComponents
|
||||
{
|
||||
private const NOT_VISITED = 1;
|
||||
private const IN_PROGRESS = 2;
|
||||
private const VISITED = 3;
|
||||
|
||||
/**
|
||||
* Array of all nodes, indexed by object ids.
|
||||
*
|
||||
* @var array<int, object>
|
||||
*/
|
||||
private $nodes = [];
|
||||
|
||||
/**
|
||||
* DFS state for the different nodes, indexed by node object id and using one of
|
||||
* this class' constants as value.
|
||||
*
|
||||
* @var array<int, self::*>
|
||||
*/
|
||||
private $states = [];
|
||||
|
||||
/**
|
||||
* Edges between the nodes. The first-level key is the object id of the outgoing
|
||||
* node; the second array maps the destination node by object id as key.
|
||||
*
|
||||
* @var array<int, array<int, bool>>
|
||||
*/
|
||||
private $edges = [];
|
||||
|
||||
/**
|
||||
* DFS numbers, by object ID
|
||||
*
|
||||
* @var array<int, int>
|
||||
*/
|
||||
private $dfs = [];
|
||||
|
||||
/**
|
||||
* lowlink numbers, by object ID
|
||||
*
|
||||
* @var array<int, int>
|
||||
*/
|
||||
private $lowlink = [];
|
||||
|
||||
/** @var int */
|
||||
private $maxdfs = 0;
|
||||
|
||||
/**
|
||||
* Nodes representing the SCC another node is in, indexed by lookup-node object ID
|
||||
*
|
||||
* @var array<int, object>
|
||||
*/
|
||||
private $representingNodes = [];
|
||||
|
||||
/**
|
||||
* Stack with OIDs of nodes visited in the current state of the DFS
|
||||
*
|
||||
* @var list<int>
|
||||
*/
|
||||
private $stack = [];
|
||||
|
||||
/** @param object $node */
|
||||
public function addNode($node): void
|
||||
{
|
||||
$id = spl_object_id($node);
|
||||
$this->nodes[$id] = $node;
|
||||
$this->states[$id] = self::NOT_VISITED;
|
||||
$this->edges[$id] = [];
|
||||
}
|
||||
|
||||
/** @param object $node */
|
||||
public function hasNode($node): bool
|
||||
{
|
||||
return isset($this->nodes[spl_object_id($node)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new edge between two nodes to the graph
|
||||
*
|
||||
* @param object $from
|
||||
* @param object $to
|
||||
*/
|
||||
public function addEdge($from, $to): void
|
||||
{
|
||||
$fromId = spl_object_id($from);
|
||||
$toId = spl_object_id($to);
|
||||
|
||||
$this->edges[$fromId][$toId] = true;
|
||||
}
|
||||
|
||||
public function findStronglyConnectedComponents(): void
|
||||
{
|
||||
foreach (array_keys($this->nodes) as $oid) {
|
||||
if ($this->states[$oid] === self::NOT_VISITED) {
|
||||
$this->tarjan($oid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function tarjan(int $oid): void
|
||||
{
|
||||
$this->dfs[$oid] = $this->lowlink[$oid] = $this->maxdfs++;
|
||||
$this->states[$oid] = self::IN_PROGRESS;
|
||||
array_push($this->stack, $oid);
|
||||
|
||||
foreach ($this->edges[$oid] as $adjacentId => $ignored) {
|
||||
if ($this->states[$adjacentId] === self::NOT_VISITED) {
|
||||
$this->tarjan($adjacentId);
|
||||
$this->lowlink[$oid] = min($this->lowlink[$oid], $this->lowlink[$adjacentId]);
|
||||
} elseif ($this->states[$adjacentId] === self::IN_PROGRESS) {
|
||||
$this->lowlink[$oid] = min($this->lowlink[$oid], $this->dfs[$adjacentId]);
|
||||
}
|
||||
}
|
||||
|
||||
$lowlink = $this->lowlink[$oid];
|
||||
if ($lowlink === $this->dfs[$oid]) {
|
||||
$representingNode = null;
|
||||
do {
|
||||
$unwindOid = array_pop($this->stack);
|
||||
|
||||
if (! $representingNode) {
|
||||
$representingNode = $this->nodes[$unwindOid];
|
||||
}
|
||||
|
||||
$this->representingNodes[$unwindOid] = $representingNode;
|
||||
$this->states[$unwindOid] = self::VISITED;
|
||||
} while ($unwindOid !== $oid);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param object $node
|
||||
*
|
||||
* @return object
|
||||
*/
|
||||
public function getNodeRepresentingStronglyConnectedComponent($node)
|
||||
{
|
||||
$oid = spl_object_id($node);
|
||||
|
||||
if (! isset($this->representingNodes[$oid])) {
|
||||
throw new InvalidArgumentException('unknown node');
|
||||
}
|
||||
|
||||
return $this->representingNodes[$oid];
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,6 @@ namespace Doctrine\ORM\Internal;
|
||||
use Doctrine\ORM\Internal\TopologicalSort\CycleDetectedException;
|
||||
|
||||
use function array_keys;
|
||||
use function array_reverse;
|
||||
use function array_unshift;
|
||||
use function spl_object_id;
|
||||
|
||||
/**
|
||||
@@ -93,18 +91,14 @@ final class TopologicalSort
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
@@ -147,7 +141,7 @@ final class TopologicalSort
|
||||
}
|
||||
|
||||
// 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]);
|
||||
@@ -160,6 +154,6 @@ final class TopologicalSort
|
||||
// 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];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ class ClassMetadataFactory extends AbstractClassMetadataFactory
|
||||
if ($parent) {
|
||||
$class->setInheritanceType($parent->inheritanceType);
|
||||
$class->setDiscriminatorColumn($parent->discriminatorColumn);
|
||||
$this->inheritIdGeneratorMapping($class, $parent);
|
||||
$class->setIdGeneratorType($parent->generatorType);
|
||||
$this->addInheritedFields($class, $parent);
|
||||
$this->addInheritedRelations($class, $parent);
|
||||
$this->addInheritedEmbeddedClasses($class, $parent);
|
||||
@@ -151,8 +151,12 @@ class ClassMetadataFactory extends AbstractClassMetadataFactory
|
||||
throw MappingException::reflectionFailure($class->getName(), $e);
|
||||
}
|
||||
|
||||
// Complete id generator mapping when the generator was declared/added in this class
|
||||
if ($class->identifier && (! $parent || ! $parent->identifier)) {
|
||||
// If this class has a parent the id generator strategy is inherited.
|
||||
// However this is only true if the hierarchy of parents contains the root entity,
|
||||
// if it consists of mapped superclasses these don't necessarily include the id field.
|
||||
if ($parent && $rootEntityFound) {
|
||||
$this->inheritIdGeneratorMapping($class, $parent);
|
||||
} else {
|
||||
$this->completeIdGeneratorMapping($class);
|
||||
}
|
||||
|
||||
|
||||
@@ -2558,6 +2558,10 @@ class ClassMetadataInfo implements ClassMetadata
|
||||
$overrideMapping['id'] = $mapping['id'];
|
||||
}
|
||||
|
||||
if (isset($mapping['declared'])) {
|
||||
$overrideMapping['declared'] = $mapping['declared'];
|
||||
}
|
||||
|
||||
if (! isset($overrideMapping['type'])) {
|
||||
$overrideMapping['type'] = $mapping['type'];
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ use Doctrine\ORM\Exception\ORMException;
|
||||
use Doctrine\ORM\Exception\UnexpectedAssociationValue;
|
||||
use Doctrine\ORM\Id\AssignedGenerator;
|
||||
use Doctrine\ORM\Internal\HydrationCompleteHandler;
|
||||
use Doctrine\ORM\Internal\StronglyConnectedComponents;
|
||||
use Doctrine\ORM\Internal\TopologicalSort;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Doctrine\ORM\Mapping\MappingException;
|
||||
@@ -1378,9 +1379,10 @@ class UnitOfWork implements PropertyChangedListener
|
||||
$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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1390,14 +1392,19 @@ class UnitOfWork implements PropertyChangedListener
|
||||
/** @return list<object> */
|
||||
private function computeDeleteExecutionOrder(): array
|
||||
{
|
||||
$sort = new TopologicalSort();
|
||||
$stronglyConnectedComponents = new StronglyConnectedComponents();
|
||||
$sort = new TopologicalSort();
|
||||
|
||||
// First make sure we have all the nodes
|
||||
foreach ($this->entityDeletions as $entity) {
|
||||
$stronglyConnectedComponents->addNode($entity);
|
||||
$sort->addNode($entity);
|
||||
}
|
||||
|
||||
// Now add edges
|
||||
// First, consider only "on delete cascade" associations between entities
|
||||
// and find strongly connected groups. Once we delete any one of the entities
|
||||
// in such a group, _all_ of the other entities will be removed as well. So,
|
||||
// we need to treat those groups like a single entity when performing delete
|
||||
// order topological sorting.
|
||||
foreach ($this->entityDeletions as $entity) {
|
||||
$class = $this->em->getClassMetadata(get_class($entity));
|
||||
|
||||
@@ -1409,16 +1416,65 @@ class UnitOfWork implements PropertyChangedListener
|
||||
continue;
|
||||
}
|
||||
|
||||
// For associations that implement a database-level cascade/set null operation,
|
||||
assert(isset($assoc['joinColumns']));
|
||||
$joinColumns = reset($assoc['joinColumns']);
|
||||
if (! isset($joinColumns['onDelete'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$onDeleteOption = strtolower($joinColumns['onDelete']);
|
||||
if ($onDeleteOption !== 'cascade') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$targetEntity = $class->getFieldValue($entity, $assoc['fieldName']);
|
||||
|
||||
// If the association does not refer to another entity or that entity
|
||||
// is not to be deleted, there is no ordering problem and we can
|
||||
// skip this particular association.
|
||||
if ($targetEntity === null || ! $stronglyConnectedComponents->hasNode($targetEntity)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$stronglyConnectedComponents->addEdge($entity, $targetEntity);
|
||||
}
|
||||
}
|
||||
|
||||
$stronglyConnectedComponents->findStronglyConnectedComponents();
|
||||
|
||||
// Now do the actual topological sorting to find the delete order.
|
||||
foreach ($this->entityDeletions as $entity) {
|
||||
$class = $this->em->getClassMetadata(get_class($entity));
|
||||
|
||||
// Get the entities representing the SCC
|
||||
$entityComponent = $stronglyConnectedComponents->getNodeRepresentingStronglyConnectedComponent($entity);
|
||||
|
||||
// When $entity is part of a non-trivial strongly connected component group
|
||||
// (a group containing not only those entities alone), make sure we process it _after_ the
|
||||
// entity representing the group.
|
||||
// The dependency direction implies that "$entity depends on $entityComponent
|
||||
// being deleted first". The topological sort will output the depended-upon nodes first.
|
||||
if ($entityComponent !== $entity) {
|
||||
$sort->addEdge($entity, $entityComponent, false);
|
||||
}
|
||||
|
||||
foreach ($class->associationMappings as $assoc) {
|
||||
// We only need to consider the owning sides of to-one associations,
|
||||
// since many-to-many associations can always be (and have already been)
|
||||
// deleted in a preceding step.
|
||||
if (! ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// For associations that implement a database-level set null operation,
|
||||
// we do not have to follow a particular order: If the referred-to entity is
|
||||
// deleted first, the DBMS will either delete the current $entity right away
|
||||
// (CASCADE) or temporarily set the foreign key to NULL (SET NULL).
|
||||
// Either way, we can skip it in the computation.
|
||||
// deleted first, the DBMS will temporarily set the foreign key to NULL (SET NULL).
|
||||
// So, we can skip it in the computation.
|
||||
assert(isset($assoc['joinColumns']));
|
||||
$joinColumns = reset($assoc['joinColumns']);
|
||||
if (isset($joinColumns['onDelete'])) {
|
||||
$onDeleteOption = strtolower($joinColumns['onDelete']);
|
||||
if ($onDeleteOption === 'cascade' || $onDeleteOption === 'set null') {
|
||||
if ($onDeleteOption === 'set null') {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -1432,9 +1488,17 @@ class UnitOfWork implements PropertyChangedListener
|
||||
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);
|
||||
// Get the entities representing the SCC
|
||||
$targetEntityComponent = $stronglyConnectedComponents->getNodeRepresentingStronglyConnectedComponent($targetEntity);
|
||||
|
||||
// When we have a dependency between two different groups of strongly connected nodes,
|
||||
// add it to the computation.
|
||||
// The dependency direction implies that "$targetEntityComponent depends on $entityComponent
|
||||
// being deleted first". The topological sort will output the depended-upon nodes first,
|
||||
// so we can work through the result in the returned order.
|
||||
if ($targetEntityComponent !== $entityComponent) {
|
||||
$sort->addEdge($targetEntityComponent, $entityComponent, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3104,9 +3168,10 @@ EXCEPTION
|
||||
$reflField->setValue($entity, $pColl);
|
||||
|
||||
if ($hints['fetchMode'][$class->name][$field] === ClassMetadata::FETCH_EAGER) {
|
||||
if ($assoc['type'] === ClassMetadata::ONE_TO_MANY) {
|
||||
$isIteration = isset($hints[Query::HINT_INTERNAL_ITERATION]) && $hints[Query::HINT_INTERNAL_ITERATION];
|
||||
if (! $isIteration && $assoc['type'] === ClassMetadata::ONE_TO_MANY) {
|
||||
$this->scheduleCollectionForBatchLoading($pColl, $class);
|
||||
} elseif ($assoc['type'] === ClassMetadata::MANY_TO_MANY) {
|
||||
} elseif (($isIteration && $assoc['type'] === ClassMetadata::ONE_TO_MANY) || $assoc['type'] === ClassMetadata::MANY_TO_MANY) {
|
||||
$this->loadCollection($pColl);
|
||||
$pColl->takeSnapshot();
|
||||
}
|
||||
|
||||
@@ -84,6 +84,18 @@ class EagerFetchCollectionTest extends OrmFunctionalTestCase
|
||||
$query->getResult();
|
||||
}
|
||||
|
||||
public function testEagerFetchWithIterable(): void
|
||||
{
|
||||
$this->createOwnerWithChildren(2);
|
||||
$this->_em->flush();
|
||||
$this->_em->clear();
|
||||
|
||||
$iterable = $this->_em->getRepository(EagerFetchOwner::class)->createQueryBuilder('o')->getQuery()->toIterable();
|
||||
$owner = $iterable->current();
|
||||
|
||||
$this->assertCount(2, $owner->children);
|
||||
}
|
||||
|
||||
protected function createOwnerWithChildren(int $children): EagerFetchOwner
|
||||
{
|
||||
$owner = new EagerFetchOwner();
|
||||
|
||||
165
tests/Doctrine/Tests/ORM/Functional/Ticket/GH10912Test.php
Normal file
165
tests/Doctrine/Tests/ORM/Functional/Ticket/GH10912Test.php
Normal file
@@ -0,0 +1,165 @@
|
||||
<?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;
|
||||
|
||||
use function array_filter;
|
||||
use function array_values;
|
||||
use function strpos;
|
||||
|
||||
class GH10912Test extends OrmFunctionalTestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->setUpEntitySchema([
|
||||
GH10912User::class,
|
||||
GH10912Profile::class,
|
||||
GH10912Room::class,
|
||||
]);
|
||||
}
|
||||
|
||||
public function testIssue(): void
|
||||
{
|
||||
$user = new GH10912User();
|
||||
$profile = new GH10912Profile();
|
||||
$room = new GH10912Room();
|
||||
|
||||
$user->rooms->add($room);
|
||||
$user->profile = $profile;
|
||||
$profile->user = $user;
|
||||
$room->user = $user;
|
||||
|
||||
$this->_em->persist($room);
|
||||
$this->_em->persist($user);
|
||||
$this->_em->persist($profile);
|
||||
$this->_em->flush();
|
||||
|
||||
/*
|
||||
* This issue is about finding a special deletion order:
|
||||
* $user and $profile cross-reference each other with ON DELETE CASCADE.
|
||||
* So, whichever one gets deleted first, the DBMS will immediately dispose
|
||||
* of the other one as well.
|
||||
*
|
||||
* $user -> $room is the unproblematic (irrelevant) inverse side of
|
||||
* a OneToMany association.
|
||||
*
|
||||
* $room -> $user is a not-nullable, no DBMS-level-cascade, owning side
|
||||
* of ManyToOne. We *must* remove the $room _before_ the $user can be
|
||||
* deleted. And remember, $user deletion happens either when we DELETE the
|
||||
* user (direct deletion), or when we delete the $profile (ON DELETE CASCADE
|
||||
* propagates to the user).
|
||||
*
|
||||
* In the original bug report, the ordering of fields in the entities was
|
||||
* relevant, in combination with a cascade=persist configuration.
|
||||
*
|
||||
* But, for the sake of clarity, let's put these features away and create
|
||||
* the problematic sequence in UnitOfWork::$entityDeletions directly:
|
||||
*/
|
||||
$this->_em->remove($profile);
|
||||
$this->_em->remove($user);
|
||||
$this->_em->remove($room);
|
||||
|
||||
$queryLog = $this->getQueryLog();
|
||||
$queryLog->reset()->enable();
|
||||
|
||||
$this->_em->flush();
|
||||
|
||||
$queries = array_values(array_filter($queryLog->queries, static function (array $entry): bool {
|
||||
return strpos($entry['sql'], 'DELETE') === 0;
|
||||
}));
|
||||
|
||||
self::assertCount(3, $queries);
|
||||
|
||||
// we do not care about the order of $user vs. $profile, so do not check them.
|
||||
self::assertSame('DELETE FROM GH10912Room WHERE id = ?', $queries[0]['sql'], '$room deletion is the first query');
|
||||
|
||||
// The EntityManager is aware that all three entities have been deleted (sanity check)
|
||||
$im = $this->_em->getUnitOfWork()->getIdentityMap();
|
||||
self::assertEmpty($im[GH10912Profile::class]);
|
||||
self::assertEmpty($im[GH10912User::class]);
|
||||
self::assertEmpty($im[GH10912Room::class]);
|
||||
}
|
||||
}
|
||||
|
||||
/** @ORM\Entity */
|
||||
class GH10912User
|
||||
{
|
||||
/**
|
||||
* @ORM\Id
|
||||
* @ORM\Column(type="integer")
|
||||
* @ORM\GeneratedValue
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $id;
|
||||
|
||||
/**
|
||||
* @ORM\OneToMany(targetEntity=GH10912Room::class, mappedBy="user")
|
||||
*
|
||||
* @var Collection<int, GH10912Room>
|
||||
*/
|
||||
public $rooms;
|
||||
|
||||
/**
|
||||
* @ORM\OneToOne(targetEntity=GH10912Profile::class)
|
||||
* @ORM\JoinColumn(onDelete="cascade")
|
||||
*
|
||||
* @var GH10912Profile
|
||||
*/
|
||||
public $profile;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->rooms = new ArrayCollection();
|
||||
}
|
||||
}
|
||||
|
||||
/** @ORM\Entity */
|
||||
class GH10912Profile
|
||||
{
|
||||
/**
|
||||
* @ORM\Id
|
||||
* @ORM\Column(type="integer")
|
||||
* @ORM\GeneratedValue
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $id;
|
||||
|
||||
/**
|
||||
* @ORM\OneToOne(targetEntity=GH10912User::class)
|
||||
* @ORM\JoinColumn(onDelete="cascade")
|
||||
*
|
||||
* @var GH10912User
|
||||
*/
|
||||
public $user;
|
||||
}
|
||||
|
||||
/** @ORM\Entity */
|
||||
class GH10912Room
|
||||
{
|
||||
/**
|
||||
* @ORM\Id
|
||||
* @ORM\Column(type="integer")
|
||||
* @ORM\GeneratedValue
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $id;
|
||||
|
||||
/**
|
||||
* @ORM\ManyToOne(targetEntity=GH10912User::class, inversedBy="rooms")
|
||||
* @ORM\JoinColumn(nullable=false)
|
||||
*
|
||||
* @var GH10912User
|
||||
*/
|
||||
public $user;
|
||||
}
|
||||
167
tests/Doctrine/Tests/ORM/Functional/Ticket/GH10913Test.php
Normal file
167
tests/Doctrine/Tests/ORM/Functional/Ticket/GH10913Test.php
Normal file
@@ -0,0 +1,167 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\ORM\Functional\Ticket;
|
||||
|
||||
use Doctrine\ORM\Internal\TopologicalSort\CycleDetectedException;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Doctrine\Tests\OrmFunctionalTestCase;
|
||||
|
||||
use function array_filter;
|
||||
use function array_values;
|
||||
use function strpos;
|
||||
|
||||
class GH10913Test extends OrmFunctionalTestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->setUpEntitySchema([
|
||||
GH10913Entity::class,
|
||||
]);
|
||||
}
|
||||
|
||||
public function testExample1(): void
|
||||
{
|
||||
[$a, $b, $c] = $this->createEntities(3);
|
||||
|
||||
$c->ref = $b;
|
||||
$b->odc = $a;
|
||||
|
||||
$this->_em->persist($a);
|
||||
$this->_em->persist($b);
|
||||
$this->_em->persist($c);
|
||||
$this->_em->flush();
|
||||
|
||||
$this->_em->remove($a);
|
||||
$this->_em->remove($b);
|
||||
$this->_em->remove($c);
|
||||
|
||||
$this->flushAndAssertNumberOfDeleteQueries(3);
|
||||
}
|
||||
|
||||
public function testExample2(): void
|
||||
{
|
||||
[$a, $b, $c] = $this->createEntities(3);
|
||||
|
||||
$a->odc = $b;
|
||||
$b->odc = $a;
|
||||
$c->ref = $b;
|
||||
|
||||
$this->_em->persist($a);
|
||||
$this->_em->persist($b);
|
||||
$this->_em->persist($c);
|
||||
$this->_em->flush();
|
||||
|
||||
$this->_em->remove($a);
|
||||
$this->_em->remove($b);
|
||||
$this->_em->remove($c);
|
||||
|
||||
$this->flushAndAssertNumberOfDeleteQueries(3);
|
||||
}
|
||||
|
||||
public function testExample3(): void
|
||||
{
|
||||
[$a, $b, $c] = $this->createEntities(3);
|
||||
|
||||
$a->odc = $b;
|
||||
$a->ref = $c;
|
||||
$c->ref = $b;
|
||||
$b->odc = $a;
|
||||
|
||||
$this->_em->persist($a);
|
||||
$this->_em->persist($b);
|
||||
$this->_em->persist($c);
|
||||
$this->_em->flush();
|
||||
|
||||
$this->_em->remove($a);
|
||||
$this->_em->remove($b);
|
||||
$this->_em->remove($c);
|
||||
|
||||
self::expectException(CycleDetectedException::class);
|
||||
|
||||
$this->_em->flush();
|
||||
}
|
||||
|
||||
public function testExample4(): void
|
||||
{
|
||||
[$a, $b, $c, $d] = $this->createEntities(4);
|
||||
|
||||
$a->ref = $b;
|
||||
$b->odc = $c;
|
||||
$c->odc = $b;
|
||||
$d->ref = $c;
|
||||
|
||||
$this->_em->persist($a);
|
||||
$this->_em->persist($b);
|
||||
$this->_em->persist($c);
|
||||
$this->_em->persist($d);
|
||||
$this->_em->flush();
|
||||
|
||||
$this->_em->remove($b);
|
||||
$this->_em->remove($c);
|
||||
$this->_em->remove($d);
|
||||
$this->_em->remove($a);
|
||||
|
||||
$this->flushAndAssertNumberOfDeleteQueries(4);
|
||||
}
|
||||
|
||||
private function flushAndAssertNumberOfDeleteQueries(int $expectedCount): void
|
||||
{
|
||||
$queryLog = $this->getQueryLog();
|
||||
$queryLog->reset()->enable();
|
||||
|
||||
$this->_em->flush();
|
||||
|
||||
$queries = array_values(array_filter($queryLog->queries, static function (array $entry): bool {
|
||||
return strpos($entry['sql'], 'DELETE') === 0;
|
||||
}));
|
||||
|
||||
self::assertCount($expectedCount, $queries);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<GH10913Entity>
|
||||
*/
|
||||
private function createEntities(int $count = 1): array
|
||||
{
|
||||
$result = [];
|
||||
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
$result[] = new GH10913Entity();
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
/** @ORM\Entity */
|
||||
class GH10913Entity
|
||||
{
|
||||
/**
|
||||
* @ORM\Id
|
||||
* @ORM\Column(type="integer")
|
||||
* @ORM\GeneratedValue
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $id;
|
||||
|
||||
/**
|
||||
* @ORM\ManyToOne(targetEntity=GH10913Entity::class)
|
||||
* @ORM\JoinColumn(nullable=true, onDelete="CASCADE")
|
||||
*
|
||||
* @var GH10913Entity
|
||||
*/
|
||||
public $odc;
|
||||
|
||||
/**
|
||||
* @ORM\ManyToOne(targetEntity=GH10913Entity::class)
|
||||
* @ORM\JoinColumn(nullable=true)
|
||||
*
|
||||
* @var GH10913Entity
|
||||
*/
|
||||
public $ref;
|
||||
}
|
||||
122
tests/Doctrine/Tests/ORM/Functional/Ticket/GH10927Test.php
Normal file
122
tests/Doctrine/Tests/ORM/Functional/Ticket/GH10927Test.php
Normal file
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\ORM\Functional\Ticket;
|
||||
|
||||
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Doctrine\Tests\OrmFunctionalTestCase;
|
||||
|
||||
/**
|
||||
* @group GH-10927
|
||||
*/
|
||||
class GH10927Test extends OrmFunctionalTestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$platform = $this->_em->getConnection()->getDatabasePlatform();
|
||||
if (! $platform instanceof PostgreSQLPlatform) {
|
||||
self::markTestSkipped('The ' . self::class . ' requires the use of postgresql.');
|
||||
}
|
||||
|
||||
$this->setUpEntitySchema([
|
||||
GH10927RootMappedSuperclass::class,
|
||||
GH10927InheritedMappedSuperclass::class,
|
||||
GH10927EntityA::class,
|
||||
GH10927EntityB::class,
|
||||
GH10927EntityC::class,
|
||||
]);
|
||||
}
|
||||
|
||||
public function testSequenceGeneratorDefinitionForRootMappedSuperclass(): void
|
||||
{
|
||||
$metadata = $this->_em->getClassMetadata(GH10927RootMappedSuperclass::class);
|
||||
|
||||
self::assertNull($metadata->sequenceGeneratorDefinition);
|
||||
}
|
||||
|
||||
public function testSequenceGeneratorDefinitionForEntityA(): void
|
||||
{
|
||||
$metadata = $this->_em->getClassMetadata(GH10927EntityA::class);
|
||||
|
||||
self::assertSame('GH10927EntityA_id_seq', $metadata->sequenceGeneratorDefinition['sequenceName']);
|
||||
}
|
||||
|
||||
public function testSequenceGeneratorDefinitionForInheritedMappedSuperclass(): void
|
||||
{
|
||||
$metadata = $this->_em->getClassMetadata(GH10927InheritedMappedSuperclass::class);
|
||||
|
||||
self::assertSame('GH10927InheritedMappedSuperclass_id_seq', $metadata->sequenceGeneratorDefinition['sequenceName']);
|
||||
}
|
||||
|
||||
public function testSequenceGeneratorDefinitionForEntityB(): void
|
||||
{
|
||||
$metadata = $this->_em->getClassMetadata(GH10927EntityB::class);
|
||||
|
||||
self::assertSame('GH10927EntityB_id_seq', $metadata->sequenceGeneratorDefinition['sequenceName']);
|
||||
}
|
||||
|
||||
public function testSequenceGeneratorDefinitionForEntityC(): void
|
||||
{
|
||||
$metadata = $this->_em->getClassMetadata(GH10927EntityC::class);
|
||||
|
||||
self::assertSame('GH10927EntityB_id_seq', $metadata->sequenceGeneratorDefinition['sequenceName']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @ORM\MappedSuperclass()
|
||||
*/
|
||||
class GH10927RootMappedSuperclass
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @ORM\Entity()
|
||||
*/
|
||||
class GH10927EntityA extends GH10927RootMappedSuperclass
|
||||
{
|
||||
/**
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue(strategy="SEQUENCE")
|
||||
* @ORM\Column(type="integer")
|
||||
*
|
||||
* @var int|null
|
||||
*/
|
||||
private $id = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @ORM\MappedSuperclass()
|
||||
*/
|
||||
class GH10927InheritedMappedSuperclass extends GH10927RootMappedSuperclass
|
||||
{
|
||||
/**
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue(strategy="SEQUENCE")
|
||||
* @ORM\Column(type="integer")
|
||||
*
|
||||
* @var int|null
|
||||
*/
|
||||
private $id = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @ORM\Entity()
|
||||
* @ORM\InheritanceType("JOINED")
|
||||
* @ORM\DiscriminatorColumn(name="discr", type="string")
|
||||
* @ORM\DiscriminatorMap({"B" = "GH10927EntityB", "C" = "GH10927EntityC"})
|
||||
*/
|
||||
class GH10927EntityB extends GH10927InheritedMappedSuperclass
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @ORM\Entity()
|
||||
*/
|
||||
class GH10927EntityC extends GH10927EntityB
|
||||
{
|
||||
}
|
||||
146
tests/Doctrine/Tests/ORM/Functional/Ticket/GH11058Test.php
Normal file
146
tests/Doctrine/Tests/ORM/Functional/Ticket/GH11058Test.php
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -6,12 +6,22 @@ namespace Doctrine\Tests\ORM\Functional\Ticket\GH11072;
|
||||
|
||||
use Doctrine\ORM\Mapping\Column;
|
||||
use Doctrine\ORM\Mapping\Entity;
|
||||
use Doctrine\ORM\Mapping\GeneratedValue;
|
||||
use Doctrine\ORM\Mapping\Id;
|
||||
|
||||
/**
|
||||
* @Entity
|
||||
*/
|
||||
class GH11072EntityAdvanced extends GH11072EntityBasic
|
||||
class GH11072EntityAdvanced
|
||||
{
|
||||
/**
|
||||
* @Id
|
||||
* @Column(type="integer")
|
||||
* @GeneratedValue
|
||||
* @var int
|
||||
*/
|
||||
public $id;
|
||||
|
||||
/** @Column(type="json") */
|
||||
public mixed $anything;
|
||||
|
||||
|
||||
73
tests/Doctrine/Tests/ORM/Functional/Ticket/GH11135Test.php
Normal file
73
tests/Doctrine/Tests/ORM/Functional/Ticket/GH11135Test.php
Normal file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\ORM\Functional\Ticket;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Doctrine\Tests\OrmFunctionalTestCase;
|
||||
|
||||
class GH11135Test extends OrmFunctionalTestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->setUpEntitySchema([
|
||||
GH11135MappedSuperclass::class,
|
||||
GH11135EntityWithOverride::class,
|
||||
GH11135EntityWithoutOverride::class,
|
||||
]);
|
||||
}
|
||||
|
||||
public function testOverrideInheritsDeclaringClass(): void
|
||||
{
|
||||
$cm1 = $this->_em->getClassMetadata(GH11135EntityWithOverride::class);
|
||||
$cm2 = $this->_em->getClassMetadata(GH11135EntityWithoutOverride::class);
|
||||
|
||||
self::assertSame($cm1->getFieldMapping('id')['declared'], $cm2->getFieldMapping('id')['declared']);
|
||||
self::assertSame($cm1->getAssociationMapping('ref')['declared'], $cm2->getAssociationMapping('ref')['declared']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @ORM\MappedSuperclass
|
||||
*/
|
||||
class GH11135MappedSuperclass
|
||||
{
|
||||
/**
|
||||
* @ORM\Id
|
||||
* @ORM\Column(type="integer")
|
||||
* @ORM\GeneratedValue
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @ORM\ManyToOne(targetEntity="GH11135EntityWithoutOverride")
|
||||
*
|
||||
* @var GH11135EntityWithoutOverride
|
||||
*/
|
||||
private $ref;
|
||||
}
|
||||
|
||||
/**
|
||||
* @ORM\Entity()
|
||||
* @ORM\AttributeOverrides({
|
||||
* @ORM\AttributeOverride(name="id", column=@ORM\Column(name="id_overridden"))
|
||||
* })
|
||||
* @ORM\AssociationOverrides({
|
||||
* @ORM\AssociationOverride(name="ref", joinColumns=@ORM\JoinColumn(name="ref_overridden", referencedColumnName="id"))
|
||||
* })
|
||||
*/
|
||||
class GH11135EntityWithOverride extends GH11135MappedSuperclass
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @ORM\Entity()
|
||||
*/
|
||||
class GH11135EntityWithoutOverride extends GH11135MappedSuperclass
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\ORM\Internal;
|
||||
|
||||
use Doctrine\ORM\Internal\StronglyConnectedComponents;
|
||||
use Doctrine\Tests\OrmTestCase;
|
||||
|
||||
class StronglyConnectedComponentsTest extends OrmTestCase
|
||||
{
|
||||
/** @var array<string, Node> */
|
||||
private $nodes = [];
|
||||
|
||||
/** @var StronglyConnectedComponents */
|
||||
private $stronglyConnectedComponents;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->stronglyConnectedComponents = new StronglyConnectedComponents();
|
||||
}
|
||||
|
||||
public function testFindStronglyConnectedComponents(): void
|
||||
{
|
||||
// A -> B <-> C -> D <-> E
|
||||
$this->addNodes('A', 'B', 'C', 'D', 'E');
|
||||
|
||||
$this->addEdge('A', 'B');
|
||||
$this->addEdge('B', 'C');
|
||||
$this->addEdge('C', 'B');
|
||||
$this->addEdge('C', 'D');
|
||||
$this->addEdge('D', 'E');
|
||||
$this->addEdge('E', 'D');
|
||||
|
||||
$this->stronglyConnectedComponents->findStronglyConnectedComponents();
|
||||
|
||||
$this->assertNodesAreInSameComponent('B', 'C');
|
||||
$this->assertNodesAreInSameComponent('D', 'E');
|
||||
$this->assertNodesAreNotInSameComponent('A', 'B');
|
||||
$this->assertNodesAreNotInSameComponent('A', 'D');
|
||||
}
|
||||
|
||||
public function testFindStronglyConnectedComponents2(): void
|
||||
{
|
||||
// A -> B -> C -> D -> B
|
||||
$this->addNodes('A', 'B', 'C', 'D');
|
||||
|
||||
$this->addEdge('A', 'B');
|
||||
$this->addEdge('B', 'C');
|
||||
$this->addEdge('C', 'D');
|
||||
$this->addEdge('D', 'B');
|
||||
|
||||
$this->stronglyConnectedComponents->findStronglyConnectedComponents();
|
||||
|
||||
$this->assertNodesAreInSameComponent('B', 'C');
|
||||
$this->assertNodesAreInSameComponent('C', 'D');
|
||||
$this->assertNodesAreNotInSameComponent('A', 'B');
|
||||
}
|
||||
|
||||
public function testFindStronglyConnectedComponents3(): void
|
||||
{
|
||||
// v---------.
|
||||
// A -> B -> C -> D -> E
|
||||
// ^--------´
|
||||
|
||||
$this->addNodes('A', 'B', 'C', 'D', 'E');
|
||||
|
||||
$this->addEdge('A', 'B');
|
||||
$this->addEdge('B', 'C');
|
||||
$this->addEdge('C', 'D');
|
||||
$this->addEdge('D', 'E');
|
||||
$this->addEdge('E', 'C');
|
||||
$this->addEdge('D', 'B');
|
||||
|
||||
$this->stronglyConnectedComponents->findStronglyConnectedComponents();
|
||||
|
||||
$this->assertNodesAreInSameComponent('B', 'C');
|
||||
$this->assertNodesAreInSameComponent('C', 'D');
|
||||
$this->assertNodesAreInSameComponent('D', 'E');
|
||||
$this->assertNodesAreInSameComponent('E', 'B');
|
||||
$this->assertNodesAreNotInSameComponent('A', 'B');
|
||||
}
|
||||
|
||||
private function addNodes(string ...$names): void
|
||||
{
|
||||
foreach ($names as $name) {
|
||||
$node = new Node($name);
|
||||
$this->nodes[$name] = $node;
|
||||
$this->stronglyConnectedComponents->addNode($node);
|
||||
}
|
||||
}
|
||||
|
||||
private function addEdge(string $from, string $to, bool $optional = false): void
|
||||
{
|
||||
$this->stronglyConnectedComponents->addEdge($this->nodes[$from], $this->nodes[$to], $optional);
|
||||
}
|
||||
|
||||
private function assertNodesAreInSameComponent(string $first, string $second): void
|
||||
{
|
||||
self::assertSame(
|
||||
$this->stronglyConnectedComponents->getNodeRepresentingStronglyConnectedComponent($this->nodes[$first]),
|
||||
$this->stronglyConnectedComponents->getNodeRepresentingStronglyConnectedComponent($this->nodes[$second])
|
||||
);
|
||||
}
|
||||
|
||||
private function assertNodesAreNotInSameComponent(string $first, string $second): void
|
||||
{
|
||||
self::assertNotSame(
|
||||
$this->stronglyConnectedComponents->getNodeRepresentingStronglyConnectedComponent($this->nodes[$first]),
|
||||
$this->stronglyConnectedComponents->getNodeRepresentingStronglyConnectedComponent($this->nodes[$second])
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,7 @@ class TopologicalSortTest extends OrmTestCase
|
||||
$this->addEdge('E', 'A');
|
||||
|
||||
// There is only 1 valid ordering for this constellation
|
||||
self::assertSame(['E', 'A', 'B', 'C'], $this->computeResult());
|
||||
self::assertSame(['C', 'B', 'A', 'E'], $this->computeResult());
|
||||
}
|
||||
|
||||
public function testSkipOptionalEdgeToBreakCycle(): void
|
||||
@@ -44,7 +44,7 @@ class TopologicalSortTest extends OrmTestCase
|
||||
$this->addEdge('A', 'B', true);
|
||||
$this->addEdge('B', 'A', false);
|
||||
|
||||
self::assertSame(['B', 'A'], $this->computeResult());
|
||||
self::assertSame(['A', 'B'], $this->computeResult());
|
||||
}
|
||||
|
||||
public function testBreakCycleByBacktracking(): void
|
||||
@@ -57,7 +57,7 @@ class TopologicalSortTest extends OrmTestCase
|
||||
$this->addEdge('D', 'A'); // closes the cycle
|
||||
|
||||
// We can only break B -> C, so the result must be C -> D -> A -> B
|
||||
self::assertSame(['C', 'D', 'A', 'B'], $this->computeResult());
|
||||
self::assertSame(['B', 'A', 'D', 'C'], $this->computeResult());
|
||||
}
|
||||
|
||||
public function testCycleRemovedByEliminatingLastOptionalEdge(): void
|
||||
@@ -75,7 +75,7 @@ class TopologicalSortTest extends OrmTestCase
|
||||
$this->addEdge('B', 'D', true);
|
||||
$this->addEdge('D', 'A');
|
||||
|
||||
self::assertSame(['C', 'D', 'A', 'B'], $this->computeResult());
|
||||
self::assertSame(['B', 'A', 'C', 'D'], $this->computeResult());
|
||||
}
|
||||
|
||||
public function testGH7180Example(): void
|
||||
@@ -89,7 +89,7 @@ class TopologicalSortTest extends OrmTestCase
|
||||
$this->addEdge('F', 'E');
|
||||
$this->addEdge('E', 'D');
|
||||
|
||||
self::assertSame(['F', 'E', 'D', 'G'], $this->computeResult());
|
||||
self::assertSame(['G', 'D', 'E', 'F'], $this->computeResult());
|
||||
}
|
||||
|
||||
public function testCommitOrderingFromGH7259Test(): void
|
||||
@@ -106,9 +106,9 @@ class TopologicalSortTest extends OrmTestCase
|
||||
// the D -> A -> B ordering is important to break the cycle
|
||||
// on the nullable link.
|
||||
$correctOrders = [
|
||||
['D', 'A', 'B', 'C'],
|
||||
['D', 'A', 'C', 'B'],
|
||||
['D', 'C', 'A', 'B'],
|
||||
['C', 'B', 'A', 'D'],
|
||||
['B', 'C', 'A', 'D'],
|
||||
['B', 'A', 'C', 'D'],
|
||||
];
|
||||
|
||||
self::assertContains($this->computeResult(), $correctOrders);
|
||||
@@ -124,12 +124,12 @@ class TopologicalSortTest extends OrmTestCase
|
||||
$this->addEdge('B', 'C', true);
|
||||
$this->addEdge('C', 'D', true);
|
||||
|
||||
// Many orderings are possible here, but the bottom line is D must be before A (it's the only hard requirement).
|
||||
// Many orderings are possible here, but the bottom line is A must be before D (it's the only hard requirement).
|
||||
$result = $this->computeResult();
|
||||
|
||||
$indexA = array_search('A', $result, true);
|
||||
$indexD = array_search('D', $result, true);
|
||||
self::assertTrue($indexD < $indexA);
|
||||
self::assertTrue($indexD > $indexA);
|
||||
}
|
||||
|
||||
public function testCommitOrderingFromGH8349Case2Test(): void
|
||||
@@ -141,7 +141,7 @@ class TopologicalSortTest extends OrmTestCase
|
||||
$this->addEdge('A', 'B', true);
|
||||
|
||||
// The B -> A requirement determines the result here
|
||||
self::assertSame(['B', 'A'], $this->computeResult());
|
||||
self::assertSame(['A', 'B'], $this->computeResult());
|
||||
}
|
||||
|
||||
public function testNodesMaintainOrderWhenNoDepencency(): void
|
||||
@@ -153,6 +153,58 @@ class TopologicalSortTest extends OrmTestCase
|
||||
self::assertSame(['A', 'B', 'C'], $this->computeResult());
|
||||
}
|
||||
|
||||
public function testNodesReturnedInDepthFirstOrder(): void
|
||||
{
|
||||
$this->addNodes('A', 'B', 'C');
|
||||
$this->addEdge('A', 'B');
|
||||
$this->addEdge('A', 'C');
|
||||
|
||||
// We start on A and find that it has two dependencies on B and C,
|
||||
// added (as dependencies) in that order.
|
||||
// So, first we continue the DFS on B, because that edge was added first.
|
||||
// This gives the result order B, C, A.
|
||||
self::assertSame(['B', 'C', 'A'], $this->computeResult());
|
||||
}
|
||||
|
||||
public function testNodesReturnedInDepthFirstOrderWithEdgesInDifferentOrderThanNodes(): void
|
||||
{
|
||||
$this->addNodes('A', 'B', 'C');
|
||||
$this->addEdge('A', 'C');
|
||||
$this->addEdge('A', 'B');
|
||||
|
||||
// This is like testNodesReturnedInDepthFirstOrder, but it shows that for the two
|
||||
// nodes B and C that A depends upon, the result will follow the order in which
|
||||
// the edges were added.
|
||||
self::assertSame(['C', 'B', 'A'], $this->computeResult());
|
||||
}
|
||||
|
||||
public function testNodesReturnedInDepthFirstOrderWithDependingNodeLast(): void
|
||||
{
|
||||
$this->addNodes('B', 'C', 'A');
|
||||
$this->addEdge('A', 'B');
|
||||
$this->addEdge('A', 'C');
|
||||
|
||||
// This again is like testNodesReturnedInDepthFirstOrder, but this
|
||||
// time the node A that depends on B and C is added as the last node.
|
||||
// That means processing can go over B and C in the order they were given.
|
||||
// The order in which edges are added is not relevant (!), since at the time
|
||||
// the edges are evaluated, the nodes they point to have already been finished.
|
||||
self::assertSame(['B', 'C', 'A'], $this->computeResult());
|
||||
}
|
||||
|
||||
public function testNodesReturnedInDepthFirstOrderWithDependingNodeLastAndEdgeOrderInversed(): void
|
||||
{
|
||||
$this->addNodes('B', 'C', 'A');
|
||||
$this->addEdge('A', 'C');
|
||||
$this->addEdge('A', 'B');
|
||||
|
||||
// This again is like testNodesReturnedInDepthFirstOrderWithDependingNodeLast, but adds
|
||||
// the edges in the opposing order. Still, the result order is the same (!).
|
||||
// This may be surprising when comparing with testNodesReturnedInDepthFirstOrderWithEdgesInDifferentOrderThanNodes,
|
||||
// where the result order depends upon the _edge_ order.
|
||||
self::assertSame(['B', 'C', 'A'], $this->computeResult());
|
||||
}
|
||||
|
||||
public function testDetectSmallCycle(): void
|
||||
{
|
||||
$this->addNodes('A', 'B');
|
||||
@@ -205,7 +257,7 @@ class TopologicalSortTest extends OrmTestCase
|
||||
$this->computeResult();
|
||||
} catch (CycleDetectedException $exception) {
|
||||
self::assertEquals(
|
||||
[$this->nodes['D'], $this->nodes['B'], $this->nodes['C'], $this->nodes['D']],
|
||||
[$this->nodes['B'], $this->nodes['C'], $this->nodes['D'], $this->nodes['B']],
|
||||
$exception->getCycle()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -163,22 +163,7 @@ class BasicInheritanceMappingTest extends OrmTestCase
|
||||
/**
|
||||
* @group DDC-1156
|
||||
* @group DDC-1218
|
||||
*/
|
||||
public function testGeneratedValueFromMappedSuperclass(): void
|
||||
{
|
||||
$class = $this->cmf->getMetadataFor(SuperclassEntity::class);
|
||||
assert($class instanceof ClassMetadata);
|
||||
|
||||
self::assertInstanceOf(IdSequenceGenerator::class, $class->idGenerator);
|
||||
self::assertEquals(
|
||||
['allocationSize' => 1, 'initialValue' => 10, 'sequenceName' => 'foo'],
|
||||
$class->sequenceGeneratorDefinition
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @group DDC-1156
|
||||
* @group DDC-1218
|
||||
* @group GH-10927
|
||||
*/
|
||||
public function testSequenceDefinitionInHierarchyWithSandwichMappedSuperclass(): void
|
||||
{
|
||||
@@ -192,22 +177,6 @@ class BasicInheritanceMappingTest extends OrmTestCase
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @group DDC-1156
|
||||
* @group DDC-1218
|
||||
*/
|
||||
public function testMultipleMappedSuperclasses(): void
|
||||
{
|
||||
$class = $this->cmf->getMetadataFor(MediumSuperclassEntity::class);
|
||||
assert($class instanceof ClassMetadata);
|
||||
|
||||
self::assertInstanceOf(IdSequenceGenerator::class, $class->idGenerator);
|
||||
self::assertEquals(
|
||||
['allocationSize' => 1, 'initialValue' => 10, 'sequenceName' => 'foo'],
|
||||
$class->sequenceGeneratorDefinition
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure indexes are inherited from the mapped superclass.
|
||||
*
|
||||
|
||||
@@ -29,6 +29,8 @@ use Doctrine\Tests\Models\DDC117\DDC117ArticleDetails;
|
||||
use Doctrine\Tests\Models\DDC6412\DDC6412File;
|
||||
use Doctrine\Tests\Models\DDC964\DDC964Admin;
|
||||
use Doctrine\Tests\Models\DDC964\DDC964Guest;
|
||||
use Doctrine\Tests\Models\DirectoryTree\AbstractContentItem;
|
||||
use Doctrine\Tests\Models\DirectoryTree\Directory;
|
||||
use Doctrine\Tests\Models\Routing\RoutingLeg;
|
||||
use Doctrine\Tests\Models\TypedProperties;
|
||||
use Doctrine\Tests\ORM\Mapping\TypedFieldMapper\CustomIntAsStringTypedFieldMapper;
|
||||
@@ -1186,6 +1188,30 @@ class ClassMetadataTest extends OrmTestCase
|
||||
$cm->setAttributeOverride('name', ['type' => 'date']);
|
||||
}
|
||||
|
||||
public function testAttributeOverrideKeepsDeclaringClass(): void
|
||||
{
|
||||
$cm = new ClassMetadata(Directory::class);
|
||||
$cm->mapField(['fieldName' => 'id', 'type' => 'integer', 'declared' => AbstractContentItem::class]);
|
||||
$cm->setAttributeOverride('id', ['columnName' => 'new_id']);
|
||||
|
||||
$mapping = $cm->getFieldMapping('id');
|
||||
|
||||
self::assertArrayHasKey('declared', $mapping);
|
||||
self::assertSame(AbstractContentItem::class, $mapping['declared']);
|
||||
}
|
||||
|
||||
public function testAssociationOverrideKeepsDeclaringClass(): void
|
||||
{
|
||||
$cm = new ClassMetadata(Directory::class);
|
||||
$cm->mapManyToOne(['fieldName' => 'parentDirectory', 'targetEntity' => Directory::class, 'cascade' => ['remove'], 'declared' => Directory::class]);
|
||||
$cm->setAssociationOverride('parentDirectory', ['cascade' => '']);
|
||||
|
||||
$mapping = $cm->getAssociationMapping('parentDirectory');
|
||||
|
||||
self::assertArrayHasKey('declared', $mapping);
|
||||
self::assertSame(Directory::class, $mapping['declared']);
|
||||
}
|
||||
|
||||
/** @group DDC-1955 */
|
||||
public function testInvalidEntityListenerClassException(): void
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user