Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Implement a way to update a workflow instead of creating a new one, w…

…hen certain conditions are met.
  • Loading branch information...
commit 0ef7c7a9b3b8d268149245da856a1f915ca64922 1 parent a9618b7
@beberlei authored
View
4 README.markdown
@@ -115,6 +115,10 @@ after the save method was called.
> The reason for this is simple and powerful: Workflows can be so complex
> that changing the inner workings of one could easily break already
> existing execution cycles of this workfow.
+>
+> This is unless you only change node configurations, variable handlers or do
+> not add more than one new node (and don't remove any nodes). In this case
+> it is possible to update an existing workflow instead of creating a new one.
You can load a workflow by querying for its Workflow Id:
View
110 lib/DoctrineExtensions/Workflow/DefinitionStorage.php
@@ -28,9 +28,18 @@ class DefinitionStorage implements \ezcWorkflowDefinitionStorage
private $options;
/**
+ * Map of nodes to their database id for later saving of non-breaking new workflows.
+ *
* @var array
*/
- private $identityMap = array();
+ private $nodeMap = array();
+
+ /**
+ * A map of all storage node ids found while loading a workflow.
+ *
+ * @var array
+ */
+ private $workflowNodeIds = array();
/**
* @param Connection $conn
@@ -126,9 +135,6 @@ public function loadById( $workflowId )
protected function loadWorkflow($workflowId, $workflowName, $workflowVersion)
{
$workflowId = (int)$workflowId;
- if (isset($this->identityMap[$workflowId])) {
- return $this->identityMap[$workflowId];
- }
$sql = "SELECT node_id, node_class, node_configuration FROM " . $this->options->nodeTable() . " WHERE workflow_id = ?";
$stmt = $this->conn->prepare($sql);
@@ -148,6 +154,10 @@ protected function loadWorkflow($workflowId, $workflowName, $workflowVersion)
}
$nodes[$node['node_id']] = $this->options->getWorkflowFactory()->createNode($node['node_class'], $configuration);
+ $nodes[$node['node_id']]->setId($node['node_id']);
+
+ $this->nodeMap[spl_object_hash($nodes[$node['node_id']])] = $node['node_id'];
+ $this->workflowNodeIds[$workflowId][] = $node['node_id'];
if ($nodes[$node['node_id']] instanceof \ezcWorkflowNodeFinally &&
!isset( $finallyNode ) ) {
@@ -195,8 +205,6 @@ protected function loadWorkflow($workflowId, $workflowName, $workflowVersion)
$workflow->id = (int)$workflowId;
$workflow->version = (int)$workflowVersion;
- $this->identityMap[$workflow->id] = $workflow;
-
$sql = "SELECT variable, class FROM " . $this->options->variableHandlerTable() . " WHERE workflow_id = ?";
$stmt = $this->conn->prepare($sql);
$stmt->bindParam(1, $workflowId);
@@ -239,6 +247,31 @@ public function save( \ezcWorkflow $workflow )
$platform = $this->conn->getDatabasePlatform();
+ // what mode of saving should it be? Update or Re-Generate?
+ //
+ // Conditions that an update sufficies:
+ // 1. No node has been deleted
+ // 2. No node has changed its meaning (action class or type)
+ // 3. For simplicitly only zero or one new nodes will be created.
+
+ $hasExistingNodeIds = array();
+ $newNodes = 0;
+ foreach ( $workflow->nodes as $node ) {
+ $oid = spl_object_hash($node);
+ if (!isset($this->nodeMap[$oid])) {
+ $newNodes++;
+ } else {
+ $hasExistingNodeIds[] = $this->nodeMap[$oid];
+ }
+ }
+
+ $canBeUpdate = false;
+ if ($newNodes < 2 && count(array_diff($hasExistingNodeIds, $this->workflowNodeIds[$workflow->id])) == 0 && $workflow->id) {
+ $canBeUpdate = true;
+ }
+
+ $this->workflowNodeIds[$workflow->id] = array();
+
try {
$this->conn->beginTransaction();
@@ -251,31 +284,58 @@ public function save( \ezcWorkflow $workflow )
);
$date = new \DateTime("now");
- $this->conn->insert($this->options->workflowTable(), array(
- 'workflow_name' => $workflow->name,
- 'workflow_version' => $workflowVersion,
- 'workflow_created' => $date->format($platform->getDateTimeFormatString()),
- 'workflow_outdated' => 0,
- ));
- $workflow->id = (int)$this->conn->lastInsertId();
- $workflow->definitionStorage = $this;
- $this->identityMap[$workflow->id] = $workflow;
+ if ($canBeUpdate) {
+ $this->conn->update($this->options->workflowTable(), array(
+ 'workflow_version' => $workflowVersion,
+ 'workflow_created' => $date->format($platform->getDateTimeFormatString()),
+ ), array('workflow_id' => $workflow->id)
+ );
+ } else {
+ $this->conn->insert($this->options->workflowTable(), array(
+ 'workflow_name' => $workflow->name,
+ 'workflow_version' => $workflowVersion,
+ 'workflow_created' => $date->format($platform->getDateTimeFormatString()),
+ 'workflow_outdated' => 0,
+ ));
+ $workflow->id = (int)$this->conn->lastInsertId();
+ $workflow->definitionStorage = $this;
+ }
// Write node table rows.
$nodeMap = array();
foreach ( $workflow->nodes as $node ) {
/* @var $node \ezcWorkflowNode */
+ $oid = spl_object_hash($node);
+
+ if ($canBeUpdate && isset($this->nodeMap[$oid])) {
+ $nodeId = (int)$this->nodeMap[$oid];
+
+ $this->conn->update($this->options->nodeTable(), array(
+ 'node_configuration' => $this->options->getSerializer()->serialize( $node->getConfiguration() ),
+ ), array('node_id' => $nodeId));
+ } else {
+ $this->conn->insert($this->options->nodeTable(), array(
+ 'workflow_id' => (int)$workflow->id,
+ 'node_class' => get_class($node),
+ 'node_configuration' => $this->options->getSerializer()->serialize( $node->getConfiguration() ),
+ ));
- $this->conn->insert($this->options->nodeTable(), array(
- 'workflow_id' => (int)$workflow->id,
- 'node_class' => get_class($node),
- 'node_configuration' => $this->options->getSerializer()->serialize( $node->getConfiguration() ),
- ));
-
- $nodeId = $this->conn->lastInsertId();
+ $nodeId = (int)$this->conn->lastInsertId();
+ }
$nodeMap[$nodeId] = $node;
+
+ $this->workflowNodeIds[$workflow->id][] = $nodeId;
+ $this->nodeMap[$oid] = $nodeId;
+ }
+
+ if ($canBeUpdate) {
+ // Delete all the node connections, NodeMap Keys are casted to (int) so usage here is safe.
+ $query = "DELETE FROM " . $this->options->nodeConnectionTable() . " " .
+ "WHERE incoming_node_id IN (" . implode(",", array_keys($nodeMap)) . ") OR " .
+ "outgoing_node_id IN (" . implode(",", array_keys($nodeMap)) . ")";
+ $this->conn->executeUpdate($query);
}
foreach ($workflow->nodes AS $node) {
@@ -305,6 +365,10 @@ public function save( \ezcWorkflow $workflow )
}
unset($nodeMap);
+ if ($canBeUpdate) {
+ $this->conn->delete($this->options->variableHandlerTable(), array('workflow_id' => (int)$workflow->id));
+ }
+
foreach ($workflow->getVariableHandlers() AS $variable => $class) {
$this->conn->insert($this->options->variableHandlerTable(), array(
'workflow_id' => (int)$workflow->id,
@@ -316,7 +380,7 @@ public function save( \ezcWorkflow $workflow )
$this->conn->commit();
} catch(\Exception $e) {
$this->conn->rollBack();
- throw new \ezcWorkflowDefinitionStorageException("Error while persistint workflow: " . $e->getMessage());
+ throw new \ezcWorkflowDefinitionStorageException("Error while persisting workflow: " . $e->getMessage());
}
}
View
20 lib/DoctrineExtensions/Workflow/SchemaBuilder.php
@@ -46,10 +46,11 @@ public function getWorkflowSchema(WorkflowOptions $options)
$schema = new \Doctrine\DBAL\Schema\Schema();
$workflowTable = $schema->createTable($options->workflowTable());
+ $columnOptions = array();
if ($this->conn->getDatabasePlatform()->prefersIdentityColumns()) {
- $workflowTable->setIdGeneratorType(Table::ID_IDENTITY);
+ $columnOptions = array('autoincrement' => true);
}
- $workflowTable->addColumn('workflow_id', 'integer');
+ $workflowTable->addColumn('workflow_id', 'integer', $columnOptions);
$workflowTable->addColumn('workflow_name', 'string');
$workflowTable->addColumn('workflow_version', 'integer');
$workflowTable->addColumn('workflow_outdated', 'integer');
@@ -58,10 +59,11 @@ public function getWorkflowSchema(WorkflowOptions $options)
$workflowTable->addUniqueIndex(array('workflow_name', 'workflow_version'));
$nodeTable = $schema->createTable($options->nodeTable());
+ $columnOptions = array();
if ($this->conn->getDatabasePlatform()->prefersIdentityColumns()) {
- $nodeTable->setIdGeneratorType(Table::ID_IDENTITY);
+ $columnOptions = array('autoincrement' => true);
}
- $nodeTable->addColumn('node_id', 'integer');
+ $nodeTable->addColumn('node_id', 'integer', $columnOptions);
$nodeTable->addColumn('workflow_id', 'integer');
$nodeTable->addColumn('node_class', 'string');
$nodeTable->addColumn('node_configuration', 'text', array('notnull' => false, "length" => null));
@@ -70,10 +72,11 @@ public function getWorkflowSchema(WorkflowOptions $options)
$nodeTable->addForeignKeyConstraint($options->workflowTable(), array('workflow_id'), array('workflow_id'), array('onDelete' => 'CASCADE'));
$connectionTable = $schema->createTable($options->nodeConnectionTable());
+ $columnOptions = array();
if ($this->conn->getDatabasePlatform()->prefersIdentityColumns()) {
- $connectionTable->setIdGeneratorType(Table::ID_IDENTITY);
+ $columnOptions = array('autoincrement' => true);
}
- $connectionTable->addColumn('id', 'integer');
+ $connectionTable->addColumn('id', 'integer', $columnOptions);
$connectionTable->addColumn('incoming_node_id', 'integer');
$connectionTable->addColumn('outgoing_node_id', 'integer');
$connectionTable->setPrimaryKey(array('id'));
@@ -88,10 +91,11 @@ public function getWorkflowSchema(WorkflowOptions $options)
$variableHandlerTable->addForeignKeyconstraint($options->workflowTable(), array('workflow_id'), array('workflow_id'));
$executionTable = $schema->createTable($options->executionTable());
+ $columnOptions = array();
if ($this->conn->getDatabasePlatform()->prefersIdentityColumns()) {
- $executionTable->setIdGeneratorType(Table::ID_IDENTITY);
+ $columnOptions = array('autoincrement' => true);
}
- $executionTable->addColumn('execution_id', 'integer');
+ $executionTable->addColumn('execution_id', 'integer', $columnOptions);
$executionTable->addColumn('workflow_id', 'integer');
$executionTable->addColumn('execution_parent', 'integer', array('notnull' => false));
$executionTable->addColumn('execution_started', 'datetime');
View
127 tests/DoctrineExtensions/Workflow/DefinitionStorageTest.php
@@ -50,29 +50,16 @@ public function testSaveFinallyNodes()
public function testSaveVariableHandlers()
{
- $variableHandler = $this->getMock('ezcWorkflowVariableHandler');
$workflow = new \ezcWorkflow('Test');
$workflow->startNode->addOutNode($workflow->endNode);
+
+ $variableHandler = $this->getMock('ezcWorkflowVariableHandler');
$workflow->addVariableHandler('foo', get_class($variableHandler));
$this->assertWorkflowPersistance($workflow);
}
- public function testWorkflowsAreNeverUpdated()
- {
- $workflow = new \ezcWorkflow('Test');
- $workflow->startNode->addOutNode($workflow->endNode);
-
- $manager = new WorkflowManager($this->conn, $this->options);
- $manager->save($workflow);
- $workflowId1 = $workflow->id;
- $manager->save($workflow);
- $workflowId2 = $workflow->id;
-
- $this->assertEquals($workflowId1 + 1, $workflowId2);
- }
-
public function assertWorkflowPersistance(\ezcWorkflow $workflow)
{
$manager = new WorkflowManager($this->conn, $this->options);
@@ -84,6 +71,8 @@ public function assertWorkflowPersistance(\ezcWorkflow $workflow)
public function testWorkflowIdentityMap()
{
+ $this->markTestSkipped('No Identity Map anymore, workflows have state that i dont fully grasp yet.');
+
$workflow = new \ezcWorkflow('IdentityTest');
$workflow->startNode->addOutNode($workflow->endNode);
@@ -109,6 +98,114 @@ public function testDeleteWorkflow()
$this->setExpectedException('ezcWorkflowDefinitionStorageException', 'Could not load workflow definition.');
$manager->loadWorkflowById($workflow->id);
}
+
+ public function testUpdateWorkflowWithNoChangesKeepsWorkflowId()
+ {
+ $workflow = new \ezcWorkflow('UpdateTest');
+ $workflow->startNode->addOutNode($workflow->endNode);
+
+ $manager = new WorkflowManager($this->conn, $this->options);
+ $manager->save($workflow);
+
+ $workflowId = $workflow->id;
+
+ $manager->save($workflow);
+
+ $this->assertEquals($workflowId, $workflow->id);
+ }
+
+ public function testUpdateWorkflowWithOneNewNode()
+ {
+ $workflow = new \ezcWorkflow('UpdateTest2');
+ $workflow->startNode->addOutNode($workflow->endNode);
+
+ $manager = new WorkflowManager($this->conn, $this->options);
+ $manager->save($workflow);
+
+ $workflowId = $workflow->id;
+
+ // add new node
+ $printAction1 = new \ezcWorkflowNodeAction(array('class' => 'DoctrinExtensions\Workflow\MyPrintAction', 'arguments' => array('Foo')));
+ $workflow->startNode->removeOutNode($workflow->endNode);
+ $workflow->startNode->addOutNode($printAction1);
+ $printAction1->addOutNode($workflow->endNode);
+
+ // add variable handler
+ $variableHandler = $this->getMock('ezcWorkflowVariableHandler');
+ $workflow->addVariableHandler('foo', get_class($variableHandler));
+
+ $manager->save($workflow);
+ $this->assertEquals($workflowId, $workflow->id);
+
+ $loadedWorkflow = $manager->loadWorkflowById($workflow->id);
+
+ $startOutNodes = $loadedWorkflow->startNode->getOutNodes();
+ $this->assertInstanceOf('ezcWorkflowNodeAction', $startOutNodes[0]);
+
+ $actionOutNodes = $startOutNodes[0]->getOutNodes();
+ $this->assertInstanceOf('ezcWorkflowNodeEnd', $actionOutNodes[0]);
+
+ $this->assertEquals(array('foo' => get_class($variableHandler)), $workflow->getVariableHandlers());
+ }
+
+ public function testUpdateWorkflowWithOneNewNodeVariableHandler()
+ {
+ $workflow = new \ezcWorkflow('UpdateTest3');
+ $workflow->startNode->addOutNode($workflow->endNode);
+
+ $manager = new WorkflowManager($this->conn, $this->options);
+ $manager->save($workflow);
+
+ $workflowId = $workflow->id;
+
+ // add variable handler
+ $variableHandler = $this->getMock('ezcWorkflowVariableHandler');
+ $workflow->addVariableHandler('foo', get_class($variableHandler));
+
+ $manager->save($workflow);
+ $this->assertEquals($workflowId, $workflow->id);
+
+ $this->assertEquals(array('foo' => get_class($variableHandler)), $workflow->getVariableHandlers());
+ }
+
+ public function testUpdateWorkflowNodeConfiguration()
+ {
+ $workflow = new \ezcWorkflow('UpdateTest4');
+
+ $printAction1 = new \ezcWorkflowNodeAction(array('class' => 'DoctrinExtensions\Workflow\MyPrintAction', 'arguments' => array('Foo')));
+ $workflow->startNode->addOutNode($printAction1);
+ $printAction1->addOutNode($workflow->endNode);
+
+ $manager = new WorkflowManager($this->conn, $this->options);
+ $manager->save($workflow);
+
+ $workflowId = $workflow->id;
+
+ $reflField = new \ReflectionProperty('ezcWorkflowNodeAction', 'configuration');
+ $reflField->setAccessible(true);
+
+ $this->assertEquals(
+ array('class' => 'DoctrinExtensions\Workflow\MyPrintAction', 'arguments' => array('Foo')),
+ $reflField->getValue($printAction1)
+ );
+
+ $reflField->setValue($printAction1, array('class' => 'DoctrinExtensions\Workflow\MyPrintAction', 'arguments' => array('bar')));
+
+ $manager->save($workflow);
+
+ $this->assertEquals($workflowId, $workflow->id);
+
+ $loadedWorkflow = $manager->loadWorkflowById($workflow->id);
+
+ $startOutNodes = $loadedWorkflow->startNode->getOutNodes();
+ $this->assertInstanceOf('ezcWorkflowNodeAction', $startOutNodes[0]);
+ $printAction1 = $startOutNodes[0];
+
+ $this->assertEquals(
+ array('class' => 'DoctrinExtensions\Workflow\MyPrintAction', 'arguments' => array('bar')),
+ $reflField->getValue($printAction1)
+ );
+ }
}
class MyPrintAction implements \ezcWorkflowServiceObject
View
2  tests/DoctrineExtensions/Workflow/SchemaBuilderTest.php
@@ -12,7 +12,7 @@ public function testSchemaCreate()
$builder = new SchemaBuilder($conn);
$schema = $builder->getWorkflowSchema($options);
- $this->assertType('Doctrine\DBAL\Schema\Schema', $schema);
+ $this->assertInstanceOf('Doctrine\DBAL\Schema\Schema', $schema);
$this->assertFalse($schema->hasTable('workflow'));
$this->assertTrue($schema->hasTable('myprefix_workflow'));
Please sign in to comment.
Something went wrong with that request. Please try again.