Commit 2807a83d authored by romanb's avatar romanb

[2.0] Started to simplify commit order calculation.

parent 56a70884
......@@ -30,107 +30,94 @@ 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();
private $_nodeStates = array();
private $_classes = array(); // The nodes to sort
private $_relatedClasses = array();
private $_sorted = array();
/**
* The topologically sorted list of items. Note that these are not nodes
* but the wrapped items.
* Clears the current graph and the last result.
*
* @var array
* @return void
*/
private $_sorted;
public function clear()
{
$this->_nodes = array();
$this->_sorted = array();
}
/**
* Orders the given list of CommitOrderNodes based on their dependencies.
* 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.
*
* @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);
$nodeCount = count($this->_classes);
if ($nodeCount == 0) {
return array();
} else if ($nodeCount == 1) {
$node = array_pop($this->_nodes);
return array($node->getClass());
return $this->_classes;
}
$this->_sorted = array();
// Init
foreach ($this->_nodes as $node) {
$node->markNotVisited();
$node->setPredecessor(null);
$this->_sorted = array();
$this->_nodeStates = array();
foreach ($this->_classes as $node) {
$this->_nodeStates[$node->name] = self::NOT_VISITED;
}
$this->_currentTime = 0;
// Go
foreach ($this->_nodes as $node) {
if ($node->isNotVisited()) {
$node->visit();
foreach ($this->_classes as $node) {
if ($this->_nodeStates[$node->name] == self::NOT_VISITED) {
$this->_visitNode($node);
}
}
return $this->_sorted;
return array_reverse($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)
private function _visitNode($node)
{
$this->_nodes[$key] = $node;
}
public function addNodeWithItem($key, $item)
{
$this->_nodes[$key] = new CommitOrderNode($item, $this);
}
$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;
public function getNodeForKey($key)
{
return $this->_nodes[$key];
}
public function hasNodeWithKey($key)
{
return isset($this->_nodes[$key]);
$this->_sorted[] = $node;
}
/**
* Clears the current graph and the last result.
*
* @return void
*/
public function clear()
public function addDependency($fromClass, $toClass)
{
$this->_nodes = array();
$this->_sorted = array();
$this->_relatedClasses[$fromClass->name][] = $toClass;
}
public function getNextTime()
public function hasClass($className)
{
return ++$this->_currentTime;
return isset($this->_classes[$className]);
}
public function prependNode($node)
public function addClass($class)
{
array_unshift($this->_sorted, $node->getClass());
$this->_classes[$class->name] = $class;
}
}
\ No newline at end of file
<?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;
}
}
\ No newline at end of file
......@@ -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);
}
}
\ No newline at end of file
}
class NodeClass1 {}
class NodeClass2 {}
class NodeClass3 {}
class NodeClass4 {}
class NodeClass5 {}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment