Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

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
Benjamin Eberlei authored January 06, 2011
4  README.markdown
Source Rendered
@@ -115,6 +115,10 @@ after the save method was called.
115 115
 > The reason for this is simple and powerful: Workflows can be so complex
116 116
 > that changing the inner workings of one could easily break already
117 117
 > existing execution cycles of this workfow.
  118
+>
  119
+> This is unless you only change node configurations, variable handlers or do
  120
+> not add more than one new node (and don't remove any nodes). In this case
  121
+> it is possible to update an existing workflow instead of creating a new one.
118 122
 
119 123
 You can load a workflow by querying for its Workflow Id:
120 124
 
110  lib/DoctrineExtensions/Workflow/DefinitionStorage.php
@@ -28,9 +28,18 @@ class DefinitionStorage implements \ezcWorkflowDefinitionStorage
28 28
     private $options;
29 29
 
30 30
     /**
  31
+     * Map of nodes to their database id for later saving of non-breaking new workflows.
  32
+     * 
31 33
      * @var array
32 34
      */
33  
-    private $identityMap = array();
  35
+    private $nodeMap = array();
  36
+
  37
+    /**
  38
+     * A map of all storage node ids found while loading a workflow.
  39
+     *
  40
+     * @var array
  41
+     */
  42
+    private $workflowNodeIds = array();
34 43
 
35 44
     /**
36 45
      * @param Connection $conn
@@ -126,9 +135,6 @@ public function loadById( $workflowId )
126 135
     protected function loadWorkflow($workflowId, $workflowName, $workflowVersion)
127 136
     {
128 137
         $workflowId = (int)$workflowId;
129  
-        if (isset($this->identityMap[$workflowId])) {
130  
-            return $this->identityMap[$workflowId];
131  
-        }
132 138
 
133 139
         $sql = "SELECT node_id, node_class, node_configuration FROM " . $this->options->nodeTable() . " WHERE workflow_id = ?";
134 140
         $stmt = $this->conn->prepare($sql);
@@ -148,6 +154,10 @@ protected function loadWorkflow($workflowId, $workflowName, $workflowVersion)
148 154
             }
149 155
 
150 156
             $nodes[$node['node_id']] = $this->options->getWorkflowFactory()->createNode($node['node_class'], $configuration);
  157
+            $nodes[$node['node_id']]->setId($node['node_id']);
  158
+
  159
+            $this->nodeMap[spl_object_hash($nodes[$node['node_id']])] = $node['node_id'];
  160
+            $this->workflowNodeIds[$workflowId][] = $node['node_id'];
151 161
 
152 162
             if ($nodes[$node['node_id']] instanceof \ezcWorkflowNodeFinally &&
153 163
                     !isset( $finallyNode ) ) {
@@ -195,8 +205,6 @@ protected function loadWorkflow($workflowId, $workflowName, $workflowVersion)
195 205
         $workflow->id = (int)$workflowId;
196 206
         $workflow->version = (int)$workflowVersion;
197 207
 
198  
-        $this->identityMap[$workflow->id] = $workflow;
199  
-
200 208
         $sql = "SELECT variable, class FROM " . $this->options->variableHandlerTable() . " WHERE workflow_id = ?";
201 209
         $stmt = $this->conn->prepare($sql);
202 210
         $stmt->bindParam(1, $workflowId);
@@ -239,6 +247,31 @@ public function save( \ezcWorkflow $workflow )
239 247
 
240 248
         $platform = $this->conn->getDatabasePlatform();
241 249
 
  250
+        // what mode of saving should it be? Update or Re-Generate?
  251
+        //
  252
+        // Conditions that an update sufficies:
  253
+        // 1. No node has been deleted
  254
+        // 2. No node has changed its meaning (action class or type)
  255
+        // 3. For simplicitly only zero or one new nodes will be created.
  256
+
  257
+        $hasExistingNodeIds = array();
  258
+        $newNodes = 0;
  259
+        foreach ( $workflow->nodes as $node ) {
  260
+            $oid = spl_object_hash($node);
  261
+            if (!isset($this->nodeMap[$oid])) {
  262
+                $newNodes++;
  263
+            } else {
  264
+                $hasExistingNodeIds[] = $this->nodeMap[$oid];
  265
+            }
  266
+        }
  267
+
  268
+        $canBeUpdate = false;
  269
+        if ($newNodes < 2 && count(array_diff($hasExistingNodeIds, $this->workflowNodeIds[$workflow->id])) == 0 && $workflow->id) {
  270
+            $canBeUpdate = true;
  271
+        }
  272
+
  273
+        $this->workflowNodeIds[$workflow->id] = array();
  274
+
242 275
         try {
243 276
             $this->conn->beginTransaction();
244 277
             
@@ -251,31 +284,58 @@ public function save( \ezcWorkflow $workflow )
251 284
             );
252 285
 
253 286
             $date = new \DateTime("now");
254  
-            $this->conn->insert($this->options->workflowTable(), array(
255  
-                'workflow_name' => $workflow->name,
256  
-                'workflow_version' => $workflowVersion,
257  
-                'workflow_created' => $date->format($platform->getDateTimeFormatString()),
258  
-                'workflow_outdated' => 0,
259  
-            ));
260  
-            $workflow->id = (int)$this->conn->lastInsertId();
261  
-            $workflow->definitionStorage = $this;
262 287
 
263  
-            $this->identityMap[$workflow->id] = $workflow;
  288
+            if ($canBeUpdate) {
  289
+                $this->conn->update($this->options->workflowTable(), array(
  290
+                        'workflow_version' => $workflowVersion,
  291
+                        'workflow_created' => $date->format($platform->getDateTimeFormatString()),
  292
+                    ), array('workflow_id' => $workflow->id)
  293
+                );
  294
+            } else {
  295
+                $this->conn->insert($this->options->workflowTable(), array(
  296
+                    'workflow_name' => $workflow->name,
  297
+                    'workflow_version' => $workflowVersion,
  298
+                    'workflow_created' => $date->format($platform->getDateTimeFormatString()),
  299
+                    'workflow_outdated' => 0,
  300
+                ));
  301
+                $workflow->id = (int)$this->conn->lastInsertId();
  302
+                $workflow->definitionStorage = $this;
  303
+            }
264 304
 
265 305
             // Write node table rows.
266 306
             $nodeMap = array();
267 307
 
268 308
             foreach ( $workflow->nodes as $node ) {
269 309
                 /* @var $node \ezcWorkflowNode */
  310
+                $oid = spl_object_hash($node);
  311
+
  312
+                if ($canBeUpdate && isset($this->nodeMap[$oid])) {
  313
+                    $nodeId = (int)$this->nodeMap[$oid];
  314
+
  315
+                    $this->conn->update($this->options->nodeTable(), array(
  316
+                        'node_configuration' => $this->options->getSerializer()->serialize( $node->getConfiguration() ),
  317
+                    ), array('node_id' => $nodeId));
  318
+                } else {
  319
+                    $this->conn->insert($this->options->nodeTable(), array(
  320
+                        'workflow_id' => (int)$workflow->id,
  321
+                        'node_class' => get_class($node),
  322
+                        'node_configuration' => $this->options->getSerializer()->serialize( $node->getConfiguration() ),
  323
+                    ));
270 324
 
271  
-                $this->conn->insert($this->options->nodeTable(), array(
272  
-                    'workflow_id' => (int)$workflow->id,
273  
-                    'node_class' => get_class($node),
274  
-                    'node_configuration' => $this->options->getSerializer()->serialize( $node->getConfiguration() ),
275  
-                ));
276  
-
277  
-                $nodeId = $this->conn->lastInsertId();
  325
+                    $nodeId = (int)$this->conn->lastInsertId();
  326
+                }
278 327
                 $nodeMap[$nodeId] = $node;
  328
+
  329
+                $this->workflowNodeIds[$workflow->id][] = $nodeId;
  330
+                $this->nodeMap[$oid] = $nodeId;
  331
+            }
  332
+
  333
+            if ($canBeUpdate) {
  334
+                // Delete all the node connections, NodeMap Keys are casted to (int) so usage here is safe.
  335
+                $query = "DELETE FROM " . $this->options->nodeConnectionTable() . " " .
  336
+                         "WHERE incoming_node_id IN (" . implode(",", array_keys($nodeMap)) . ") OR " .
  337
+                         "outgoing_node_id IN (" . implode(",", array_keys($nodeMap)) . ")";
  338
+                $this->conn->executeUpdate($query);
279 339
             }
280 340
 
281 341
             foreach ($workflow->nodes AS $node) {
@@ -305,6 +365,10 @@ public function save( \ezcWorkflow $workflow )
305 365
             }
306 366
             unset($nodeMap);
307 367
 
  368
+            if ($canBeUpdate) {
  369
+                $this->conn->delete($this->options->variableHandlerTable(), array('workflow_id' => (int)$workflow->id));
  370
+            }
  371
+
308 372
             foreach ($workflow->getVariableHandlers() AS $variable => $class) {
309 373
                 $this->conn->insert($this->options->variableHandlerTable(), array(
310 374
                     'workflow_id' => (int)$workflow->id,
@@ -316,7 +380,7 @@ public function save( \ezcWorkflow $workflow )
316 380
             $this->conn->commit();
317 381
         } catch(\Exception $e) {
318 382
             $this->conn->rollBack();
319  
-            throw new \ezcWorkflowDefinitionStorageException("Error while persistint workflow: " . $e->getMessage());
  383
+            throw new \ezcWorkflowDefinitionStorageException("Error while persisting workflow: " . $e->getMessage());
320 384
         }
321 385
     }
322 386
 
20  lib/DoctrineExtensions/Workflow/SchemaBuilder.php
@@ -46,10 +46,11 @@ public function getWorkflowSchema(WorkflowOptions $options)
46 46
         $schema = new \Doctrine\DBAL\Schema\Schema();
47 47
 
48 48
         $workflowTable = $schema->createTable($options->workflowTable());
  49
+        $columnOptions = array();
49 50
         if ($this->conn->getDatabasePlatform()->prefersIdentityColumns()) {
50  
-            $workflowTable->setIdGeneratorType(Table::ID_IDENTITY);
  51
+            $columnOptions = array('autoincrement' => true);
51 52
         }
52  
-        $workflowTable->addColumn('workflow_id', 'integer');
  53
+        $workflowTable->addColumn('workflow_id', 'integer', $columnOptions);
53 54
         $workflowTable->addColumn('workflow_name', 'string');
54 55
         $workflowTable->addColumn('workflow_version', 'integer');
55 56
         $workflowTable->addColumn('workflow_outdated', 'integer');
@@ -58,10 +59,11 @@ public function getWorkflowSchema(WorkflowOptions $options)
58 59
         $workflowTable->addUniqueIndex(array('workflow_name', 'workflow_version'));
59 60
 
60 61
         $nodeTable = $schema->createTable($options->nodeTable());
  62
+        $columnOptions = array();
61 63
         if ($this->conn->getDatabasePlatform()->prefersIdentityColumns()) {
62  
-            $nodeTable->setIdGeneratorType(Table::ID_IDENTITY);
  64
+            $columnOptions = array('autoincrement' => true);
63 65
         }
64  
-        $nodeTable->addColumn('node_id', 'integer');
  66
+        $nodeTable->addColumn('node_id', 'integer', $columnOptions);
65 67
         $nodeTable->addColumn('workflow_id', 'integer');
66 68
         $nodeTable->addColumn('node_class', 'string');
67 69
         $nodeTable->addColumn('node_configuration', 'text', array('notnull' => false, "length" => null));
@@ -70,10 +72,11 @@ public function getWorkflowSchema(WorkflowOptions $options)
70 72
         $nodeTable->addForeignKeyConstraint($options->workflowTable(), array('workflow_id'), array('workflow_id'), array('onDelete' => 'CASCADE'));
71 73
 
72 74
         $connectionTable = $schema->createTable($options->nodeConnectionTable());
  75
+        $columnOptions = array();
73 76
         if ($this->conn->getDatabasePlatform()->prefersIdentityColumns()) {
74  
-            $connectionTable->setIdGeneratorType(Table::ID_IDENTITY);
  77
+            $columnOptions = array('autoincrement' => true);
75 78
         }
76  
-        $connectionTable->addColumn('id', 'integer');
  79
+        $connectionTable->addColumn('id', 'integer', $columnOptions);
77 80
         $connectionTable->addColumn('incoming_node_id', 'integer');
78 81
         $connectionTable->addColumn('outgoing_node_id', 'integer');
79 82
         $connectionTable->setPrimaryKey(array('id'));
@@ -88,10 +91,11 @@ public function getWorkflowSchema(WorkflowOptions $options)
88 91
         $variableHandlerTable->addForeignKeyconstraint($options->workflowTable(), array('workflow_id'), array('workflow_id'));
89 92
 
90 93
         $executionTable = $schema->createTable($options->executionTable());
  94
+        $columnOptions = array();
91 95
         if ($this->conn->getDatabasePlatform()->prefersIdentityColumns()) {
92  
-            $executionTable->setIdGeneratorType(Table::ID_IDENTITY);
  96
+            $columnOptions = array('autoincrement' => true);
93 97
         }
94  
-        $executionTable->addColumn('execution_id', 'integer');
  98
+        $executionTable->addColumn('execution_id', 'integer', $columnOptions);
95 99
         $executionTable->addColumn('workflow_id', 'integer');
96 100
         $executionTable->addColumn('execution_parent', 'integer', array('notnull' => false));
97 101
         $executionTable->addColumn('execution_started', 'datetime');
127  tests/DoctrineExtensions/Workflow/DefinitionStorageTest.php
@@ -50,29 +50,16 @@ public function testSaveFinallyNodes()
50 50
 
51 51
     public function testSaveVariableHandlers()
52 52
     {
53  
-        $variableHandler = $this->getMock('ezcWorkflowVariableHandler');
54 53
 
55 54
         $workflow = new \ezcWorkflow('Test');
56 55
         $workflow->startNode->addOutNode($workflow->endNode);
  56
+
  57
+        $variableHandler = $this->getMock('ezcWorkflowVariableHandler');
57 58
         $workflow->addVariableHandler('foo', get_class($variableHandler));
58 59
 
59 60
         $this->assertWorkflowPersistance($workflow);
60 61
     }
61 62
 
62  
-    public function testWorkflowsAreNeverUpdated()
63  
-    {
64  
-        $workflow = new \ezcWorkflow('Test');
65  
-        $workflow->startNode->addOutNode($workflow->endNode);
66  
-
67  
-        $manager = new WorkflowManager($this->conn, $this->options);
68  
-        $manager->save($workflow);
69  
-        $workflowId1 = $workflow->id;
70  
-        $manager->save($workflow);
71  
-        $workflowId2 = $workflow->id;
72  
-
73  
-        $this->assertEquals($workflowId1 + 1, $workflowId2);
74  
-    }
75  
-
76 63
     public function assertWorkflowPersistance(\ezcWorkflow $workflow)
77 64
     {
78 65
         $manager = new WorkflowManager($this->conn, $this->options);
@@ -84,6 +71,8 @@ public function assertWorkflowPersistance(\ezcWorkflow $workflow)
84 71
 
85 72
     public function testWorkflowIdentityMap()
86 73
     {
  74
+        $this->markTestSkipped('No Identity Map anymore, workflows have state that i dont fully grasp yet.');
  75
+
87 76
         $workflow = new \ezcWorkflow('IdentityTest');
88 77
         $workflow->startNode->addOutNode($workflow->endNode);
89 78
         
@@ -109,6 +98,114 @@ public function testDeleteWorkflow()
109 98
         $this->setExpectedException('ezcWorkflowDefinitionStorageException', 'Could not load workflow definition.');
110 99
         $manager->loadWorkflowById($workflow->id);
111 100
     }
  101
+
  102
+    public function testUpdateWorkflowWithNoChangesKeepsWorkflowId()
  103
+    {
  104
+        $workflow = new \ezcWorkflow('UpdateTest');
  105
+        $workflow->startNode->addOutNode($workflow->endNode);
  106
+
  107
+        $manager = new WorkflowManager($this->conn, $this->options);
  108
+        $manager->save($workflow);
  109
+
  110
+        $workflowId = $workflow->id;
  111
+
  112
+        $manager->save($workflow);
  113
+
  114
+        $this->assertEquals($workflowId, $workflow->id);
  115
+    }
  116
+
  117
+    public function testUpdateWorkflowWithOneNewNode()
  118
+    {
  119
+        $workflow = new \ezcWorkflow('UpdateTest2');
  120
+        $workflow->startNode->addOutNode($workflow->endNode);
  121
+
  122
+        $manager = new WorkflowManager($this->conn, $this->options);
  123
+        $manager->save($workflow);
  124
+
  125
+        $workflowId = $workflow->id;
  126
+
  127
+        // add new node
  128
+        $printAction1 = new \ezcWorkflowNodeAction(array('class' => 'DoctrinExtensions\Workflow\MyPrintAction', 'arguments' => array('Foo')));
  129
+        $workflow->startNode->removeOutNode($workflow->endNode);
  130
+        $workflow->startNode->addOutNode($printAction1);
  131
+        $printAction1->addOutNode($workflow->endNode);
  132
+
  133
+        // add variable handler
  134
+        $variableHandler = $this->getMock('ezcWorkflowVariableHandler');
  135
+        $workflow->addVariableHandler('foo', get_class($variableHandler));
  136
+
  137
+        $manager->save($workflow);
  138
+        $this->assertEquals($workflowId, $workflow->id);
  139
+
  140
+        $loadedWorkflow = $manager->loadWorkflowById($workflow->id);
  141
+
  142
+        $startOutNodes = $loadedWorkflow->startNode->getOutNodes();
  143
+        $this->assertInstanceOf('ezcWorkflowNodeAction', $startOutNodes[0]);
  144
+
  145
+        $actionOutNodes = $startOutNodes[0]->getOutNodes();
  146
+        $this->assertInstanceOf('ezcWorkflowNodeEnd', $actionOutNodes[0]);
  147
+
  148
+        $this->assertEquals(array('foo' => get_class($variableHandler)), $workflow->getVariableHandlers());
  149
+    }
  150
+
  151
+    public function testUpdateWorkflowWithOneNewNodeVariableHandler()
  152
+    {
  153
+        $workflow = new \ezcWorkflow('UpdateTest3');
  154
+        $workflow->startNode->addOutNode($workflow->endNode);
  155
+
  156
+        $manager = new WorkflowManager($this->conn, $this->options);
  157
+        $manager->save($workflow);
  158
+
  159
+        $workflowId = $workflow->id;
  160
+
  161
+        // add variable handler
  162
+        $variableHandler = $this->getMock('ezcWorkflowVariableHandler');
  163
+        $workflow->addVariableHandler('foo', get_class($variableHandler));
  164
+
  165
+        $manager->save($workflow);
  166
+        $this->assertEquals($workflowId, $workflow->id);
  167
+
  168
+        $this->assertEquals(array('foo' => get_class($variableHandler)), $workflow->getVariableHandlers());
  169
+    }
  170
+
  171
+    public function testUpdateWorkflowNodeConfiguration()
  172
+    {
  173
+        $workflow = new \ezcWorkflow('UpdateTest4');
  174
+
  175
+        $printAction1 = new \ezcWorkflowNodeAction(array('class' => 'DoctrinExtensions\Workflow\MyPrintAction', 'arguments' => array('Foo')));
  176
+        $workflow->startNode->addOutNode($printAction1);
  177
+        $printAction1->addOutNode($workflow->endNode);
  178
+
  179
+        $manager = new WorkflowManager($this->conn, $this->options);
  180
+        $manager->save($workflow);
  181
+
  182
+        $workflowId = $workflow->id;
  183
+
  184
+        $reflField = new \ReflectionProperty('ezcWorkflowNodeAction', 'configuration');
  185
+        $reflField->setAccessible(true);
  186
+
  187
+        $this->assertEquals(
  188
+            array('class' => 'DoctrinExtensions\Workflow\MyPrintAction', 'arguments' => array('Foo')),
  189
+            $reflField->getValue($printAction1)
  190
+        );
  191
+
  192
+        $reflField->setValue($printAction1, array('class' => 'DoctrinExtensions\Workflow\MyPrintAction', 'arguments' => array('bar')));
  193
+
  194
+        $manager->save($workflow);
  195
+
  196
+        $this->assertEquals($workflowId, $workflow->id);
  197
+
  198
+        $loadedWorkflow = $manager->loadWorkflowById($workflow->id);
  199
+
  200
+        $startOutNodes = $loadedWorkflow->startNode->getOutNodes();
  201
+        $this->assertInstanceOf('ezcWorkflowNodeAction', $startOutNodes[0]);
  202
+        $printAction1 = $startOutNodes[0];
  203
+
  204
+        $this->assertEquals(
  205
+            array('class' => 'DoctrinExtensions\Workflow\MyPrintAction', 'arguments' => array('bar')),
  206
+            $reflField->getValue($printAction1)
  207
+        );
  208
+    }
112 209
 }
113 210
 
114 211
 class MyPrintAction implements \ezcWorkflowServiceObject
2  tests/DoctrineExtensions/Workflow/SchemaBuilderTest.php
@@ -12,7 +12,7 @@ public function testSchemaCreate()
12 12
         $builder = new SchemaBuilder($conn);
13 13
         $schema = $builder->getWorkflowSchema($options);
14 14
 
15  
-        $this->assertType('Doctrine\DBAL\Schema\Schema', $schema);
  15
+        $this->assertInstanceOf('Doctrine\DBAL\Schema\Schema', $schema);
16 16
 
17 17
         $this->assertFalse($schema->hasTable('workflow'));
18 18
         $this->assertTrue($schema->hasTable('myprefix_workflow'));

0 notes on commit 0ef7c7a

Please sign in to comment.
Something went wrong with that request. Please try again.