Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit fd7a14a

Browse files
authored
Merge pull request #10547 from mpdude/commit-order-entity-level
Compute the commit order (inserts/deletes) on the entity level
2 parents 3cc30c4 + 0d3ce5d commit fd7a14a

25 files changed

Lines changed: 2133 additions & 122 deletions

UPGRADE.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Upgrade to 2.16
22

3+
## Deprecated `\Doctrine\ORM\Internal\CommitOrderCalculator` and related classes
4+
5+
With changes made to the commit order computation, the internal classes
6+
`\Doctrine\ORM\Internal\CommitOrderCalculator`, `\Doctrine\ORM\Internal\CommitOrder\Edge`,
7+
`\Doctrine\ORM\Internal\CommitOrder\Vertex` and `\Doctrine\ORM\Internal\CommitOrder\VertexState`
8+
have been deprecated and will be removed in ORM 3.0.
9+
310
## Deprecated returning post insert IDs from `EntityPersister::executeInserts()`
411

512
Persisters implementing `\Doctrine\ORM\Persisters\Entity\EntityPersister` should no longer

docs/en/reference/events.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -707,8 +707,8 @@ not directly mapped by Doctrine.
707707
``UPDATE`` statement.
708708
- The ``postPersist`` event occurs for an entity after
709709
the entity has been made persistent. It will be invoked after the
710-
database insert operations. Generated primary key values are
711-
available in the postPersist event.
710+
database insert operation for that entity. A generated primary key value for
711+
the entity will be available in the postPersist event.
712712
- The ``postRemove`` event occurs for an entity after the
713713
entity has been deleted. It will be invoked after the database
714714
delete operations. It is not called for a DQL ``DELETE`` statement.

lib/Doctrine/ORM/Cache/Persister/Entity/AbstractEntityPersister.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use Doctrine\ORM\Persisters\Entity\EntityPersister;
2424
use Doctrine\ORM\UnitOfWork;
2525

26+
use function array_merge;
2627
use function assert;
2728
use function serialize;
2829
use function sha1;
@@ -314,7 +315,13 @@ public function getOwningTable($fieldName)
314315
*/
315316
public function executeInserts()
316317
{
317-
$this->queuedCache['insert'] = $this->persister->getInserts();
318+
// The commit order/foreign key relationships may make it necessary that multiple calls to executeInsert()
319+
// are performed, so collect all the new entities.
320+
$newInserts = $this->persister->getInserts();
321+
322+
if ($newInserts) {
323+
$this->queuedCache['insert'] = array_merge($this->queuedCache['insert'] ?? [], $newInserts);
324+
}
318325

319326
return $this->persister->executeInserts();
320327
}

lib/Doctrine/ORM/Internal/CommitOrder/Edge.php

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@
44

55
namespace Doctrine\ORM\Internal\CommitOrder;
66

7-
/** @internal */
7+
use Doctrine\Deprecations\Deprecation;
8+
9+
/**
10+
* @internal
11+
* @deprecated
12+
*/
813
final class Edge
914
{
1015
/**
@@ -27,6 +32,13 @@ final class Edge
2732

2833
public function __construct(string $from, string $to, int $weight)
2934
{
35+
Deprecation::triggerIfCalledFromOutside(
36+
'doctrine/orm',
37+
'https://github.com/doctrine/orm/pull/10547',
38+
'The %s class is deprecated and will be removed in ORM 3.0',
39+
self::class
40+
);
41+
3042
$this->from = $from;
3143
$this->to = $to;
3244
$this->weight = $weight;

lib/Doctrine/ORM/Internal/CommitOrder/Vertex.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,13 @@
44

55
namespace Doctrine\ORM\Internal\CommitOrder;
66

7+
use Doctrine\Deprecations\Deprecation;
78
use Doctrine\ORM\Mapping\ClassMetadata;
89

9-
/** @internal */
10+
/**
11+
* @internal
12+
* @deprecated
13+
*/
1014
final class Vertex
1115
{
1216
/**
@@ -32,6 +36,13 @@ final class Vertex
3236

3337
public function __construct(string $hash, ClassMetadata $value)
3438
{
39+
Deprecation::triggerIfCalledFromOutside(
40+
'doctrine/orm',
41+
'https://github.com/doctrine/orm/pull/10547',
42+
'The %s class is deprecated and will be removed in ORM 3.0',
43+
self::class
44+
);
45+
3546
$this->hash = $hash;
3647
$this->value = $value;
3748
}

lib/Doctrine/ORM/Internal/CommitOrder/VertexState.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@
44

55
namespace Doctrine\ORM\Internal\CommitOrder;
66

7-
/** @internal */
7+
use Doctrine\Deprecations\Deprecation;
8+
9+
/**
10+
* @internal
11+
* @deprecated
12+
*/
813
final class VertexState
914
{
1015
public const NOT_VISITED = 0;
@@ -13,5 +18,11 @@ final class VertexState
1318

1419
private function __construct()
1520
{
21+
Deprecation::triggerIfCalledFromOutside(
22+
'doctrine/orm',
23+
'https://github.com/doctrine/orm/pull/10547',
24+
'The %s class is deprecated and will be removed in ORM 3.0',
25+
self::class
26+
);
1627
}
1728
}

lib/Doctrine/ORM/Internal/CommitOrderCalculator.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Doctrine\ORM\Internal;
66

7+
use Doctrine\Deprecations\Deprecation;
78
use Doctrine\ORM\Internal\CommitOrder\Edge;
89
use Doctrine\ORM\Internal\CommitOrder\Vertex;
910
use Doctrine\ORM\Internal\CommitOrder\VertexState;
@@ -17,6 +18,8 @@
1718
* using a depth-first searching (DFS) to traverse the graph built in memory.
1819
* This algorithm have a linear running time based on nodes (V) and dependency
1920
* between the nodes (E), resulting in a computational complexity of O(V + E).
21+
*
22+
* @deprecated
2023
*/
2124
class CommitOrderCalculator
2225
{
@@ -45,6 +48,16 @@ class CommitOrderCalculator
4548
*/
4649
private $sortedNodeList = [];
4750

51+
public function __construct()
52+
{
53+
Deprecation::triggerIfCalledFromOutside(
54+
'doctrine/orm',
55+
'https://github.com/doctrine/orm/pull/10547',
56+
'The %s class is deprecated and will be removed in ORM 3.0',
57+
self::class
58+
);
59+
}
60+
4861
/**
4962
* Checks for node (vertex) existence in graph.
5063
*
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\ORM\Internal;
6+
7+
use Doctrine\ORM\Internal\TopologicalSort\CycleDetectedException;
8+
9+
use function array_keys;
10+
use function array_reverse;
11+
use function array_unshift;
12+
use function spl_object_id;
13+
14+
/**
15+
* TopologicalSort implements topological sorting, which is an ordering
16+
* algorithm for directed graphs (DG) using a depth-first searching (DFS)
17+
* to traverse the graph built in memory.
18+
* This algorithm has a linear running time based on nodes (V) and edges
19+
* between the nodes (E), resulting in a computational complexity of O(V + E).
20+
*
21+
* @internal
22+
*/
23+
final class TopologicalSort
24+
{
25+
private const NOT_VISITED = 1;
26+
private const IN_PROGRESS = 2;
27+
private const VISITED = 3;
28+
29+
/**
30+
* Array of all nodes, indexed by object ids.
31+
*
32+
* @var array<int, object>
33+
*/
34+
private $nodes = [];
35+
36+
/**
37+
* DFS state for the different nodes, indexed by node object id and using one of
38+
* this class' constants as value.
39+
*
40+
* @var array<int, self::*>
41+
*/
42+
private $states = [];
43+
44+
/**
45+
* Edges between the nodes. The first-level key is the object id of the outgoing
46+
* node; the second array maps the destination node by object id as key. The final
47+
* boolean value indicates whether the edge is optional or not.
48+
*
49+
* @var array<int, array<int, bool>>
50+
*/
51+
private $edges = [];
52+
53+
/**
54+
* Builds up the result during the DFS.
55+
*
56+
* @var list<object>
57+
*/
58+
private $sortResult = [];
59+
60+
/** @param object $node */
61+
public function addNode($node): void
62+
{
63+
$id = spl_object_id($node);
64+
$this->nodes[$id] = $node;
65+
$this->states[$id] = self::NOT_VISITED;
66+
$this->edges[$id] = [];
67+
}
68+
69+
/** @param object $node */
70+
public function hasNode($node): bool
71+
{
72+
return isset($this->nodes[spl_object_id($node)]);
73+
}
74+
75+
/**
76+
* Adds a new edge between two nodes to the graph
77+
*
78+
* @param object $from
79+
* @param object $to
80+
* @param bool $optional This indicates whether the edge may be ignored during the topological sort if it is necessary to break cycles.
81+
*/
82+
public function addEdge($from, $to, bool $optional): void
83+
{
84+
$fromId = spl_object_id($from);
85+
$toId = spl_object_id($to);
86+
87+
if (isset($this->edges[$fromId][$toId]) && $this->edges[$fromId][$toId] === false) {
88+
return; // we already know about this dependency, and it is not optional
89+
}
90+
91+
$this->edges[$fromId][$toId] = $optional;
92+
}
93+
94+
/**
95+
* Returns a topological sort of all nodes. When we have an edge A->B between two nodes
96+
* A and B, then A will be listed before B in the result.
97+
*
98+
* @return list<object>
99+
*/
100+
public function sort(): array
101+
{
102+
/*
103+
* When possible, keep objects in the result in the same order in which they were added as nodes.
104+
* Since nodes are unshifted into $this->>sortResult (see the visit() method), that means we
105+
* need to work them in array_reverse order here.
106+
*/
107+
foreach (array_reverse(array_keys($this->nodes)) as $oid) {
108+
if ($this->states[$oid] === self::NOT_VISITED) {
109+
$this->visit($oid);
110+
}
111+
}
112+
113+
return $this->sortResult;
114+
}
115+
116+
private function visit(int $oid): void
117+
{
118+
if ($this->states[$oid] === self::IN_PROGRESS) {
119+
// This node is already on the current DFS stack. We've found a cycle!
120+
throw new CycleDetectedException($this->nodes[$oid]);
121+
}
122+
123+
if ($this->states[$oid] === self::VISITED) {
124+
// We've reached a node that we've already seen, including all
125+
// other nodes that are reachable from here. We're done here, return.
126+
return;
127+
}
128+
129+
$this->states[$oid] = self::IN_PROGRESS;
130+
131+
// Continue the DFS downwards the edge list
132+
foreach ($this->edges[$oid] as $adjacentId => $optional) {
133+
try {
134+
$this->visit($adjacentId);
135+
} catch (CycleDetectedException $exception) {
136+
if ($exception->isCycleCollected()) {
137+
// There is a complete cycle downstream of the current node. We cannot
138+
// do anything about that anymore.
139+
throw $exception;
140+
}
141+
142+
if ($optional) {
143+
// The current edge is part of a cycle, but it is optional and the closest
144+
// such edge while backtracking. Break the cycle here by skipping the edge
145+
// and continuing with the next one.
146+
continue;
147+
}
148+
149+
// We have found a cycle and cannot break it at $edge. Best we can do
150+
// is to retreat from the current vertex, hoping that somewhere up the
151+
// stack this can be salvaged.
152+
$this->states[$oid] = self::NOT_VISITED;
153+
$exception->addToCycle($this->nodes[$oid]);
154+
155+
throw $exception;
156+
}
157+
}
158+
159+
// We have traversed all edges and visited all other nodes reachable from here.
160+
// So we're done with this vertex as well.
161+
162+
$this->states[$oid] = self::VISITED;
163+
array_unshift($this->sortResult, $this->nodes[$oid]);
164+
}
165+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\ORM\Internal\TopologicalSort;
6+
7+
use RuntimeException;
8+
9+
use function array_unshift;
10+
11+
class CycleDetectedException extends RuntimeException
12+
{
13+
/** @var list<object> */
14+
private $cycle;
15+
16+
/** @var object */
17+
private $startNode;
18+
19+
/**
20+
* Do we have the complete cycle collected?
21+
*
22+
* @var bool
23+
*/
24+
private $cycleCollected = false;
25+
26+
/** @param object $startNode */
27+
public function __construct($startNode)
28+
{
29+
parent::__construct('A cycle has been detected, so a topological sort is not possible. The getCycle() method provides the list of nodes that form the cycle.');
30+
31+
$this->startNode = $startNode;
32+
$this->cycle = [$startNode];
33+
}
34+
35+
/** @return list<object> */
36+
public function getCycle(): array
37+
{
38+
return $this->cycle;
39+
}
40+
41+
/** @param object $node */
42+
public function addToCycle($node): void
43+
{
44+
array_unshift($this->cycle, $node);
45+
46+
if ($node === $this->startNode) {
47+
$this->cycleCollected = true;
48+
}
49+
}
50+
51+
public function isCycleCollected(): bool
52+
{
53+
return $this->cycleCollected;
54+
}
55+
}

0 commit comments

Comments
 (0)