[2.0] Started to simplify commit order calculation.
This commit is contained in:
parent
56a708843d
commit
2807a83d5d
4 changed files with 104 additions and 266 deletions
|
@ -30,88 +30,14 @@ namespace Doctrine\ORM\Internal;
|
|||
*/
|
||||
class CommitOrderCalculator
|
||||
{
|
||||
private $_currentTime;
|
||||
const NOT_VISITED = 1;
|
||||
const IN_PROGRESS = 2;
|
||||
const VISITED = 3;
|
||||
|
||||
/**
|
||||
* The node list used for sorting.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $_nodes = array();
|
||||
|
||||
/**
|
||||
* The topologically sorted list of items. Note that these are not nodes
|
||||
* but the wrapped items.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $_sorted;
|
||||
|
||||
/**
|
||||
* Orders the given list of CommitOrderNodes based on their dependencies.
|
||||
*
|
||||
* Uses a depth-first search (DFS) to traverse the graph.
|
||||
* The desired topological sorting is the reverse postorder of these searches.
|
||||
*
|
||||
* @param array $nodes The list of (unordered) CommitOrderNodes.
|
||||
* @return array The list of ordered items. These are the items wrapped in the nodes.
|
||||
*/
|
||||
public function getCommitOrder()
|
||||
{
|
||||
// Check whether we need to do anything. 0 or 1 node is easy.
|
||||
$nodeCount = count($this->_nodes);
|
||||
if ($nodeCount == 0) {
|
||||
return array();
|
||||
} else if ($nodeCount == 1) {
|
||||
$node = array_pop($this->_nodes);
|
||||
return array($node->getClass());
|
||||
}
|
||||
|
||||
$this->_sorted = array();
|
||||
|
||||
// Init
|
||||
foreach ($this->_nodes as $node) {
|
||||
$node->markNotVisited();
|
||||
$node->setPredecessor(null);
|
||||
}
|
||||
|
||||
$this->_currentTime = 0;
|
||||
|
||||
// Go
|
||||
foreach ($this->_nodes as $node) {
|
||||
if ($node->isNotVisited()) {
|
||||
$node->visit();
|
||||
}
|
||||
}
|
||||
|
||||
return $this->_sorted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a node to consider when ordering.
|
||||
*
|
||||
* @param mixed $key Somme arbitrary key for the node (must be unique!).
|
||||
* @param unknown_type $node
|
||||
*/
|
||||
public function addNode($key, $node)
|
||||
{
|
||||
$this->_nodes[$key] = $node;
|
||||
}
|
||||
|
||||
public function addNodeWithItem($key, $item)
|
||||
{
|
||||
$this->_nodes[$key] = new CommitOrderNode($item, $this);
|
||||
}
|
||||
|
||||
public function getNodeForKey($key)
|
||||
{
|
||||
return $this->_nodes[$key];
|
||||
}
|
||||
|
||||
public function hasNodeWithKey($key)
|
||||
{
|
||||
return isset($this->_nodes[$key]);
|
||||
}
|
||||
private $_nodeStates = array();
|
||||
private $_classes = array(); // The nodes to sort
|
||||
private $_relatedClasses = array();
|
||||
private $_sorted = array();
|
||||
|
||||
/**
|
||||
* Clears the current graph and the last result.
|
||||
|
@ -124,13 +50,74 @@ class CommitOrderCalculator
|
|||
$this->_sorted = array();
|
||||
}
|
||||
|
||||
public function getNextTime()
|
||||
/**
|
||||
* Gets a valid commit order for all current nodes.
|
||||
*
|
||||
* Uses a depth-first search (DFS) to traverse the graph.
|
||||
* The desired topological sorting is the reverse postorder of these searches.
|
||||
*
|
||||
* @return array The list of ordered items. These are the items wrapped in the nodes.
|
||||
*/
|
||||
public function getCommitOrder()
|
||||
{
|
||||
return ++$this->_currentTime;
|
||||
// Check whether we need to do anything. 0 or 1 node is easy.
|
||||
$nodeCount = count($this->_classes);
|
||||
if ($nodeCount == 0) {
|
||||
return array();
|
||||
} else if ($nodeCount == 1) {
|
||||
return $this->_classes;
|
||||
}
|
||||
|
||||
// Init
|
||||
$this->_sorted = array();
|
||||
$this->_nodeStates = array();
|
||||
foreach ($this->_classes as $node) {
|
||||
$this->_nodeStates[$node->name] = self::NOT_VISITED;
|
||||
}
|
||||
|
||||
// Go
|
||||
foreach ($this->_classes as $node) {
|
||||
if ($this->_nodeStates[$node->name] == self::NOT_VISITED) {
|
||||
$this->_visitNode($node);
|
||||
}
|
||||
}
|
||||
|
||||
return array_reverse($this->_sorted);
|
||||
}
|
||||
|
||||
public function prependNode($node)
|
||||
private function _visitNode($node)
|
||||
{
|
||||
array_unshift($this->_sorted, $node->getClass());
|
||||
$this->_nodeStates[$node->name] = self::IN_PROGRESS;
|
||||
|
||||
if (isset($this->_relatedClasses[$node->name])) {
|
||||
foreach ($this->_relatedClasses[$node->name] as $relatedNode) {
|
||||
if ($this->_nodeStates[$relatedNode->name] == self::NOT_VISITED) {
|
||||
$this->_visitNode($relatedNode);
|
||||
}
|
||||
if ($this->_nodeStates[$relatedNode->name] == self::IN_PROGRESS) {
|
||||
// back edge => cycle
|
||||
//TODO: anything to do here?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->_nodeStates[$node->name] = self::VISITED;
|
||||
|
||||
$this->_sorted[] = $node;
|
||||
}
|
||||
|
||||
public function addDependency($fromClass, $toClass)
|
||||
{
|
||||
$this->_relatedClasses[$fromClass->name][] = $toClass;
|
||||
}
|
||||
|
||||
public function hasClass($className)
|
||||
{
|
||||
return isset($this->_classes[$className]);
|
||||
}
|
||||
|
||||
public function addClass($class)
|
||||
{
|
||||
$this->_classes[$class->name] = $class;
|
||||
}
|
||||
}
|
|
@ -1,144 +0,0 @@
|
|||
<?php
|
||||
/*
|
||||
* $Id$
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* This software consists of voluntary contributions made by many individuals
|
||||
* and is licensed under the LGPL. For more information, see
|
||||
* <http://www.doctrine-project.org>.
|
||||
*/
|
||||
|
||||
namespace Doctrine\ORM\Internal;
|
||||
|
||||
/**
|
||||
* A CommitOrderNode is a temporary wrapper around ClassMetadata instances
|
||||
* that is used to sort the order of commits in a UnitOfWork.
|
||||
*
|
||||
* @since 2.0
|
||||
* @author Roman Borschel <roman@code-factory.org>
|
||||
*/
|
||||
class CommitOrderNode
|
||||
{
|
||||
const NOT_VISITED = 1;
|
||||
const IN_PROGRESS = 2;
|
||||
const VISITED = 3;
|
||||
|
||||
private $_traversalState;
|
||||
private $_predecessor;
|
||||
private $_status;
|
||||
private $_calculator;
|
||||
private $_relatedNodes = array();
|
||||
|
||||
/* The "time" when this node was first discovered during traversal */
|
||||
public $discoveryTime;
|
||||
/* The "time" when this node was finished during traversal */
|
||||
public $finishingTime;
|
||||
|
||||
/* The wrapped object */
|
||||
private $_wrappedObj;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* Creates a new node.
|
||||
*
|
||||
* @param mixed $wrappedObj The object to wrap.
|
||||
* @param Doctrine\ORM\Internal\CommitOrderCalculator $calc The calculator.
|
||||
*/
|
||||
public function __construct($wrappedObj, CommitOrderCalculator $calc)
|
||||
{
|
||||
$this->_wrappedObj = $wrappedObj;
|
||||
$this->_calculator = $calc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the wrapped object.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getClass()
|
||||
{
|
||||
return $this->_wrappedObj;
|
||||
}
|
||||
|
||||
public function setPredecessor($node)
|
||||
{
|
||||
$this->_predecessor = $node;
|
||||
}
|
||||
|
||||
public function getPredecessor()
|
||||
{
|
||||
return $this->_predecessor;
|
||||
}
|
||||
|
||||
public function markNotVisited()
|
||||
{
|
||||
$this->_traversalState = self::NOT_VISITED;
|
||||
}
|
||||
|
||||
public function markInProgress()
|
||||
{
|
||||
$this->_traversalState = self::IN_PROGRESS;
|
||||
}
|
||||
|
||||
public function markVisited()
|
||||
{
|
||||
$this->_traversalState = self::VISITED;
|
||||
}
|
||||
|
||||
public function isNotVisited()
|
||||
{
|
||||
return $this->_traversalState == self::NOT_VISITED;
|
||||
}
|
||||
|
||||
public function isInProgress()
|
||||
{
|
||||
return $this->_traversalState == self::IN_PROGRESS;
|
||||
}
|
||||
|
||||
public function visit()
|
||||
{
|
||||
$this->markInProgress();
|
||||
$this->discoveryTime = $this->_calculator->getNextTime();
|
||||
|
||||
foreach ($this->getRelatedNodes() as $node) {
|
||||
if ($node->isNotVisited()) {
|
||||
$node->setPredecessor($this);
|
||||
$node->visit();
|
||||
}
|
||||
if ($node->isInProgress()) {
|
||||
// back edge => cycle
|
||||
//TODO: anything to do here?
|
||||
}
|
||||
}
|
||||
|
||||
$this->markVisited();
|
||||
$this->_calculator->prependNode($this);
|
||||
$this->finishingTime = $this->_calculator->getNextTime();
|
||||
}
|
||||
|
||||
public function getRelatedNodes()
|
||||
{
|
||||
return $this->_relatedNodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a directed dependency (an edge on the graph). "$this -before-> $other".
|
||||
*
|
||||
* @param Doctrine\ORM\Internal\CommitOrderNode $node
|
||||
*/
|
||||
public function before(CommitOrderNode $node)
|
||||
{
|
||||
$this->_relatedNodes[] = $node;
|
||||
}
|
||||
}
|
|
@ -798,33 +798,24 @@ class UnitOfWork implements PropertyChangedListener
|
|||
$newNodes = array();
|
||||
foreach ($entityChangeSet as $entity) {
|
||||
$className = get_class($entity);
|
||||
if ( ! $this->_commitOrderCalculator->hasNodeWithKey($className)) {
|
||||
$this->_commitOrderCalculator->addNodeWithItem(
|
||||
$className, // index/key
|
||||
$this->_em->getClassMetadata($className) // item
|
||||
);
|
||||
$newNodes[] = $this->_commitOrderCalculator->getNodeForKey($className);
|
||||
if ( ! $this->_commitOrderCalculator->hasClass($className)) {
|
||||
$class = $this->_em->getClassMetadata($className);
|
||||
$this->_commitOrderCalculator->addClass($class);
|
||||
$newNodes[] = $class;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate dependencies for new nodes
|
||||
foreach ($newNodes as $node) {
|
||||
$class = $node->getClass();
|
||||
foreach ($newNodes as $class) {
|
||||
foreach ($class->associationMappings as $assocMapping) {
|
||||
//TODO: should skip target classes that are not in the changeset.
|
||||
if ($assocMapping->isOwningSide) {
|
||||
$targetClass = $this->_em->getClassMetadata($assocMapping->targetEntityName);
|
||||
$targetClassName = $targetClass->name;
|
||||
// If the target class does not yet have a node, create it
|
||||
if ( ! $this->_commitOrderCalculator->hasNodeWithKey($targetClassName)) {
|
||||
$this->_commitOrderCalculator->addNodeWithItem(
|
||||
$targetClassName, // index/key
|
||||
$targetClass // item
|
||||
);
|
||||
if ( ! $this->_commitOrderCalculator->hasClass($targetClass->name)) {
|
||||
$this->_commitOrderCalculator->addClass($targetClass);
|
||||
}
|
||||
// add dependency
|
||||
$otherNode = $this->_commitOrderCalculator->getNodeForKey($targetClassName);
|
||||
$otherNode->before($node);
|
||||
$this->_commitOrderCalculator->addDependency($targetClass, $class);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
namespace Doctrine\Tests\ORM;
|
||||
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
|
||||
require_once __DIR__ . '/../TestInit.php';
|
||||
|
||||
/**
|
||||
|
@ -19,34 +21,36 @@ class CommitOrderCalculatorTest extends \Doctrine\Tests\OrmTestCase
|
|||
{
|
||||
$this->_calc = new \Doctrine\ORM\Internal\CommitOrderCalculator();
|
||||
}
|
||||
|
||||
/** Helper to create an array of nodes */
|
||||
private function _createNodes(array $names)
|
||||
{
|
||||
$nodes = array();
|
||||
foreach ($names as $name) {
|
||||
$node = new \Doctrine\ORM\Internal\CommitOrderNode($name, $this->_calc);
|
||||
$nodes[$name] = $node;
|
||||
$this->_calc->addNode($node->getClass(), $node);
|
||||
}
|
||||
return $nodes;
|
||||
}
|
||||
|
||||
public function testCommitOrdering1()
|
||||
{
|
||||
$nodes = $this->_createNodes(array("node1", "node2", "node3", "node4", "node5"));
|
||||
$class1 = new ClassMetadata(__NAMESPACE__ . '\NodeClass1');
|
||||
$class2 = new ClassMetadata(__NAMESPACE__ . '\NodeClass2');
|
||||
$class3 = new ClassMetadata(__NAMESPACE__ . '\NodeClass3');
|
||||
$class4 = new ClassMetadata(__NAMESPACE__ . '\NodeClass4');
|
||||
$class5 = new ClassMetadata(__NAMESPACE__ . '\NodeClass5');
|
||||
|
||||
$nodes['node1']->before($nodes['node2']);
|
||||
$nodes['node2']->before($nodes['node3']);
|
||||
$nodes['node3']->before($nodes['node4']);
|
||||
$nodes['node5']->before($nodes['node1']);
|
||||
$this->_calc->addClass($class1);
|
||||
$this->_calc->addClass($class2);
|
||||
$this->_calc->addClass($class3);
|
||||
$this->_calc->addClass($class4);
|
||||
$this->_calc->addClass($class5);
|
||||
|
||||
shuffle($nodes); // some randomness
|
||||
$this->_calc->addDependency($class1, $class2);
|
||||
$this->_calc->addDependency($class2, $class3);
|
||||
$this->_calc->addDependency($class3, $class4);
|
||||
$this->_calc->addDependency($class5, $class1);
|
||||
|
||||
$sorted = $this->_calc->getCommitOrder();
|
||||
|
||||
// There is only 1 valid ordering for this constellation
|
||||
$correctOrder = array("node5", "node1", "node2", "node3", "node4");
|
||||
$correctOrder = array($class5, $class1, $class2, $class3, $class4);
|
||||
$this->assertSame($correctOrder, $sorted);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class NodeClass1 {}
|
||||
class NodeClass2 {}
|
||||
class NodeClass3 {}
|
||||
class NodeClass4 {}
|
||||
class NodeClass5 {}
|
Loading…
Add table
Reference in a new issue