├── tests ├── phpunit.dist.xml └── DoctrineExtensions │ └── Workflow │ ├── SchemaBuilderTest.php │ ├── Util │ └── SerializerTest.php │ ├── EzcDefinitionTest.php │ ├── DoctrineExecutionTest.php │ ├── TestHelper.php │ ├── DefinitionStorageTest.php │ └── EzcExecutionTest.php ├── lib └── DoctrineExtensions │ └── Workflow │ ├── Util │ └── Serialize │ │ ├── ZetaSerializer.php │ │ ├── Serializer.php │ │ └── WddxSerializer.php │ ├── WorkflowFactory.php │ ├── IWorkflowManager.php │ ├── VariableHandler │ └── EntityManagerHandler.php │ ├── WorkflowOptions.php │ ├── WorkflowManager.php │ ├── SchemaBuilder.php │ ├── DoctrineExecution.php │ └── DefinitionStorage.php └── README.markdown /tests/phpunit.dist.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/DoctrineExtensions/Workflow/SchemaBuilderTest.php: -------------------------------------------------------------------------------- 1 | getWorkflowSchema($options); 14 | 15 | $this->assertInstanceOf('Doctrine\DBAL\Schema\Schema', $schema); 16 | 17 | $this->assertFalse($schema->hasTable('workflow')); 18 | $this->assertTrue($schema->hasTable('myprefix_workflow')); 19 | } 20 | } -------------------------------------------------------------------------------- /lib/DoctrineExtensions/Workflow/Util/Serialize/ZetaSerializer.php: -------------------------------------------------------------------------------- 1 | 'bar'), array('foo' => 'bar'), array()), 18 | array(array('foo' => 'bar'), array('foo' => 'bar'), null), 19 | array(array('c' => "\xc9\x80"), array('c' => "\xc9\x80"), null), 20 | // empty object instances exist in ezcWorkflow!! 21 | array(array('c' => new \stdClass()), array('c' => new \stdClass()), null), 22 | ); 23 | } 24 | 25 | /** 26 | * @dataProvider dataSerialize 27 | */ 28 | public function testZetaSerializer($value, $expectedValue, $defaultValue) 29 | { 30 | $z = new ZetaSerializer(); 31 | $this->assertEquals($expectedValue, $z->unserialize($z->serialize($value), $defaultValue)); 32 | } 33 | 34 | /** 35 | * @dataProvider dataSerialize 36 | */ 37 | public function testWbbxSerializer($value, $expectedValue, $defaultValue) 38 | { 39 | if (!extension_loaded('wddx')) { 40 | $this->markTestSkipped('WDDX PHP Extension is required for this tests to run.'); 41 | } 42 | 43 | $z = new WddxSerializer(); 44 | $this->assertEquals($expectedValue, $z->unserialize($z->serialize($value), $defaultValue)); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/DoctrineExtensions/Workflow/WorkflowFactory.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | 21 | namespace DoctrineExtensions\Workflow; 22 | 23 | /** 24 | * Factory that creates the node, variable handler and service objects instances required by a specific workflow. 25 | * 26 | * You can hook into this class to add nodes that require certain dependencies, 27 | * for example database services, webservices or other services. 28 | */ 29 | class WorkflowFactory 30 | { 31 | private $entityManager; 32 | 33 | public function __construct($entityManager = null) 34 | { 35 | $this->em = $entityManager; 36 | } 37 | 38 | /** 39 | * @param string $className 40 | * @param array $configuration 41 | * @return \ezcWorkflowNode 42 | */ 43 | public function createNode($className, $configuration) 44 | { 45 | return new $className($configuration); 46 | } 47 | 48 | /** 49 | * @param string $className 50 | * @return \ezcWorkflowVariableHandler 51 | */ 52 | public function createVariableHandler($className) 53 | { 54 | if ($className == "DoctrineExtensions\Workflow\VariableHandler\EntityManagerHandler") { 55 | if (!($this->entityManager instanceof \Doctrine\ORM\EntityManager)) { 56 | throw new \ezcWorkflowException("EntityManagerHandler requires an EntityManager to be passed to the WorkflowFactory."); 57 | } 58 | 59 | return new $className($this->entityManager); 60 | } else { 61 | return new $className; 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /tests/DoctrineExtensions/Workflow/EzcDefinitionTest.php: -------------------------------------------------------------------------------- 1 | conn = \DoctrineExtensions\Workflow\TestHelper::getConnection(); 16 | $this->options = new WorkflowOptions('test_'); 17 | TestHelper::createSchema($this->options); 18 | 19 | $this->dbStorage = new DefinitionStorage($this->conn, $this->options); 20 | } 21 | 22 | /** 23 | * @dataProvider workflowNameProvider 24 | */ 25 | public function testSaveAndLoadWorkflow( $workflowName ) 26 | { 27 | $xmlWorkflow = $this->xmlStorage->loadByName( $workflowName ); 28 | 29 | $this->dbStorage->save( $xmlWorkflow ); 30 | $dbWorkflow = $this->dbStorage->loadByName( $workflowName ); 31 | 32 | $this->assertEquals( $xmlWorkflow, $dbWorkflow ); 33 | } 34 | 35 | public function testExceptionWhenLoadingNotExistingWorkflow() 36 | { 37 | try { 38 | $this->dbStorage->loadById( 1 ); 39 | } catch ( \ezcWorkflowDefinitionStorageException $e ) { 40 | $this->assertEquals( 'Could not load workflow definition.', $e->getMessage() ); 41 | return; 42 | } 43 | 44 | $this->fail( 'Expected an ezcWorkflowDefinitionStorageException to be thrown.' ); 45 | } 46 | 47 | public function testExceptionWhenLoadingNotExistingWorkflow2() 48 | { 49 | try { 50 | $this->dbStorage->loadByName( 'NotExisting' ); 51 | } catch ( \ezcWorkflowDefinitionStorageException $e ) { 52 | $this->assertEquals( 'Could not load workflow definition.', $e->getMessage() ); 53 | return; 54 | } 55 | 56 | $this->fail( 'Expected an ezcWorkflowDefinitionStorageException to be thrown.' ); 57 | } 58 | 59 | public function testExceptionWhenLoadingNotExistingWorkflowVersion() 60 | { 61 | $this->setUpStartEnd(); 62 | $this->dbStorage->save( $this->workflow ); 63 | 64 | try { 65 | $workflow = $this->dbStorage->loadByName( 'StartEnd', 2 ); 66 | } catch ( \ezcWorkflowDefinitionStorageException $e ) { 67 | $this->assertEquals( 'Could not load workflow definition.', $e->getMessage() ); 68 | return; 69 | } 70 | 71 | $this->fail( 'Expected an ezcWorkflowDefinitionStorageException to be thrown.' ); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /lib/DoctrineExtensions/Workflow/IWorkflowManager.php: -------------------------------------------------------------------------------- 1 | conn = \DoctrineExtensions\Workflow\TestHelper::getConnection(); 40 | $this->options = new WorkflowOptions('test_', null, null, $this->createSerializer()); 41 | TestHelper::createSchema($this->options); 42 | $this->manager = new WorkflowManager($this->conn, $this->options); 43 | } 44 | 45 | public function testStartToEndExecution() 46 | { 47 | $workflow = new \ezcWorkflow('Test'); 48 | $workflow->startNode->addOutNode($workflow->endNode); 49 | 50 | $this->manager->save($workflow); 51 | 52 | $execution = $this->manager->createExecution($workflow); 53 | $execution->workflow = $workflow; 54 | $execution->setVariable('foo', 'bar'); 55 | $execution->setVariable('bar', 'baz'); 56 | $execution->start(); 57 | 58 | $this->assertEquals(0, count($this->conn->fetchAll('SELECT * FROM '.$this->options->executionTable()))); 59 | $this->assertEquals(0, count($this->conn->fetchAll('SELECT * FROM '.$this->options->executionStateTable()))); 60 | } 61 | 62 | public function testStartSuspendResume() 63 | { 64 | $workflow = new \ezcWorkflow('Test'); 65 | $input = new \ezcWorkflowNodeInput(array( 'choice' => new \ezcWorkflowConditionIsBool )); 66 | $workflow->startNode->addOutNode($input); 67 | $input->addOutNode($workflow->endNode); 68 | 69 | $this->manager->save($workflow); 70 | 71 | $execution = $this->manager->createExecution($workflow); 72 | $execution->workflow = $workflow; 73 | $executionId = $execution->start(); 74 | 75 | $execution = $this->manager->loadExecution($executionId); 76 | $execution->resume(array('choice' => true)); 77 | } 78 | } -------------------------------------------------------------------------------- /lib/DoctrineExtensions/Workflow/VariableHandler/EntityManagerHandler.php: -------------------------------------------------------------------------------- 1 | em = $em; 42 | } 43 | 44 | /** 45 | * Reconstruct an entity for the given variable name checking for details in a transformation. 46 | * 47 | * @param ezcWorkflowExecution $execution 48 | * @param string $variableName 49 | */ 50 | public function load(ezcWorkflowExecution $execution, $variableName) 51 | { 52 | $entityDetailsVariable = $variableName . "_dc2entity"; 53 | if ($execution->hasVariable($entityDetailsVariable)) { 54 | $entityData = $execution->getVariable($entityDetailsVariable); 55 | if (isset($entityData[0]) && isset($entityData[1])) { 56 | return $this->em->find($entityData[0], $entityData[1]); 57 | } 58 | $execution->setVariable($entityDetailsVariable, null); 59 | } 60 | return null; 61 | } 62 | 63 | /** 64 | * Savely persist Doctrine 2 Entity information 65 | * 66 | * @param ezcWorkflowExecution $execution 67 | * @param string $variableName 68 | * @param $value 69 | */ 70 | public function save(ezcWorkflowExecution $execution, $variableName, $value) 71 | { 72 | if (!is_object($value)) { 73 | return null; 74 | } 75 | 76 | if (!$this->em->contains($value)) { 77 | throw new \ezcWorkflowExecutionException("Entity '".get_class($value)."' at variable " . $variableName . " has to be managed by the EntityManager."); 78 | } 79 | 80 | $entityData = array(get_class($value), $this->em->getUnitOfWork()->getEntityIdentifier($value)); 81 | $entityDetailsVariable = $variableName . "_dc2entity"; 82 | $execution->setVariable($entityDetailsVariable, $entityData); 83 | $execution->setVariable($variableName, null); 84 | } 85 | } -------------------------------------------------------------------------------- /lib/DoctrineExtensions/Workflow/WorkflowOptions.php: -------------------------------------------------------------------------------- 1 | prefix = $prefix; 48 | $this->workflowClass = ($workflowClassName) ?: 'ezcWorkflow'; 49 | $this->workflowFactory = ($workflowFactory) ?: new WorkflowFactory(); 50 | $this->serializer = ($serializer) ?: new Util\Serialize\ZetaSerializer(); 51 | } 52 | 53 | public function getTablePrefix() 54 | { 55 | return $this->prefix; 56 | } 57 | 58 | public function workflowTable() 59 | { 60 | return $this->prefix . 'workflow'; 61 | } 62 | 63 | public function workflowSequence( ) 64 | { 65 | return $this->workflowTable() . '_workflow_id_seq'; 66 | } 67 | 68 | public function nodeTable() 69 | { 70 | return $this->prefix . 'node'; 71 | } 72 | 73 | public function nodeSequence( ) 74 | { 75 | return $this->nodeTable() . '_node_id_seq'; 76 | } 77 | 78 | public function nodeConnectionTable() 79 | { 80 | return $this->prefix . 'node_connection'; 81 | } 82 | 83 | public function nodeConnectionSequence() 84 | { 85 | return $this->nodeConnectionTable() . '_id_seq'; 86 | } 87 | 88 | public function variableHandlerTable() 89 | { 90 | return $this->prefix . 'variable_handler'; 91 | } 92 | 93 | public function executionTable() 94 | { 95 | return $this->prefix . 'execution'; 96 | } 97 | 98 | public function executionSequence() 99 | { 100 | return $this->executionTable() . '_execution_id_seq'; 101 | } 102 | 103 | public function executionStateTable() 104 | { 105 | return $this->prefix . 'execution_state'; 106 | } 107 | 108 | public function workflowClassName() 109 | { 110 | return $this->workflowClass; 111 | } 112 | 113 | /** 114 | * @return WorkflowFactory 115 | */ 116 | public function getWorkflowFactory() 117 | { 118 | return $this->workflowFactory; 119 | } 120 | 121 | /** 122 | * @return Serializer 123 | */ 124 | public function getSerializer() 125 | { 126 | return $this->serializer; 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /tests/DoctrineExtensions/Workflow/TestHelper.php: -------------------------------------------------------------------------------- 1 | getTablePrefix()])) { 20 | $schemaBuilder = new SchemaBuilder($conn); 21 | try { 22 | $schemaBuilder->dropWorkflowSchema($options); 23 | } catch(\PDOException $e) { 24 | 25 | } 26 | $schemaBuilder->createWorkflowSchema($options); 27 | 28 | self::$schema[$options->getTablePrefix()] = true; 29 | } 30 | 31 | $platform = $conn->getDatabasePlatform(); 32 | $tables = array( 33 | $options->executionStateTable(), 34 | $options->executionTable(), 35 | $options->variableHandlerTable(), 36 | $options->nodeConnectionTable(), 37 | $options->nodeTable(), 38 | $options->workflowTable(), 39 | ); 40 | foreach ($tables AS $table) { 41 | $conn->executeUpdate($platform->getTruncateTableSQL($table)); 42 | } 43 | } 44 | 45 | static public function getConnection() 46 | { 47 | if (self::$conn == null) { 48 | self::$conn = \Doctrine\DBAL\DriverManager::getConnection(array( 49 | 'driver' => $GLOBALS['DC2_DRIVER'], 50 | 'dbname' => $GLOBALS['DC2_DBNAME'], 51 | 'user' => $GLOBALS['DC2_USER'], 52 | 'password' => $GLOBALS['DC2_PASSWORD'], 53 | 'memory' => true, // for sqlite 54 | )); 55 | } 56 | return self::$conn; 57 | } 58 | } 59 | 60 | if (!isset($GLOBALS['doctrine2-dbal-path']) || !isset($GLOBALS['doctrine2-common-path'])) { 61 | throw new \InvalidArgumentException('Global variables "doctrine2-common-path" and "doctrine2-dbal-path" have to be set in phpunit.xml'); 62 | } 63 | 64 | $loaderfile = $GLOBALS['doctrine2-common-path']."/Doctrine/Common/ClassLoader.php"; 65 | if (!file_exists($loaderfile)) { 66 | throw new \InvalidArgumentException('Could not include Doctrine\Common\ClassLoader from "doctrine2-common-path".'); 67 | } 68 | require_once($loaderfile); 69 | 70 | $loader = new \Doctrine\Common\ClassLoader("Doctrine\Common", $GLOBALS['doctrine2-common-path']); 71 | $loader->register(); 72 | 73 | $loader = new \Doctrine\Common\ClassLoader("Doctrine\DBAL", $GLOBALS['doctrine2-dbal-path']); 74 | $loader->register(); 75 | 76 | $loader = new \Doctrine\Common\ClassLoader("DoctrineExtensions\Workflow", __DIR__."/../../../lib"); 77 | $loader->register(); 78 | 79 | if (!isset($GLOBALS['ezc-base-file']) || !file_exists($GLOBALS['ezc-base-file'])) { 80 | throw new \InvalidArgumentException('No path to the ezzBase class file given or file does not exist!'); 81 | } 82 | 83 | require_once $GLOBALS['ezc-base-file']; 84 | spl_autoload_register(array('ezcBase', 'autoload')); 85 | 86 | if (!isset($GLOBALS['ezc-workflow-tests-dir']) || !file_exists($GLOBALS['ezc-workflow-tests-dir'])) { 87 | throw new \InvalidArgumentException('No path to the ezzWorkflow tests directory given or directory does not exist!'); 88 | } 89 | 90 | require_once $GLOBALS['ezc-workflow-tests-dir'] . "/case.php"; -------------------------------------------------------------------------------- /lib/DoctrineExtensions/Workflow/WorkflowManager.php: -------------------------------------------------------------------------------- 1 | conn = $conn; 42 | $this->options = $options; 43 | $this->definitionStorage = new DefinitionStorage($conn, $options); 44 | } 45 | 46 | /** 47 | * @return \ezcWorkflowDefinitionStorage 48 | */ 49 | public function getDefinitionStorage() 50 | { 51 | return $this->definitionStorage; 52 | } 53 | 54 | /** 55 | * Load an execution given by a specifiy Id 56 | * 57 | * @param int $executionId 58 | * @return DoctrineExecution 59 | */ 60 | public function loadExecution($executionId) 61 | { 62 | return new DoctrineExecution($this->conn, $this->definitionStorage, $executionId); 63 | } 64 | 65 | /** 66 | * @param \ezcWorkflow $workflow 67 | * @return DoctrineExceution 68 | */ 69 | public function createExecution(\ezcWorkflow $workflow) 70 | { 71 | $execution = new DoctrineExecution($this->conn, $this->definitionStorage); 72 | $execution->workflow = $workflow; 73 | return $execution; 74 | } 75 | 76 | /** 77 | * @param int $workflowId 78 | * @return DoctrineExecution 79 | */ 80 | public function createExecutionByWorkflowId($workflowId) 81 | { 82 | return $this->createExecution($this->definitionStorage->loadById($workflowId)); 83 | } 84 | 85 | /** 86 | * Poll across the complete execution table to find suspended workflows ready for execution. 87 | * 88 | * @param int $limit 89 | * @param int $offset 90 | * @return array 91 | */ 92 | public function pollSuspendedExecutionIds($limit = null, $offset = null) 93 | { 94 | $platform = $this->conn->getDatabasePlatform(); 95 | 96 | $query = 'SELECT execution_id FROM ' . $this->options->executionTable() . ' ' . 97 | 'WHERE execution_next_poll_date < ?'; 98 | if ($limit) { 99 | $query = $platform->modifyLimitQuery($query, $limit, $offset); 100 | } 101 | 102 | $stmt = $this->conn->prepare($query); 103 | $stmt->bindParam(1, date_create('now')->format($platform->getDateTimeFormatString())); 104 | $stmt->execute(); 105 | 106 | $executionIds = array(); 107 | while ($executionId = $stmt->fetchColumn()) { 108 | $executionIds[] = $executionId; 109 | } 110 | return $executionIds; 111 | } 112 | 113 | public function deleteWorkflow($workflowId) 114 | { 115 | return $this->definitionStorage->delete($workflowId); 116 | } 117 | 118 | public function getUnusedWorkflowIds() 119 | { 120 | $sql = 'SELECT w.workflow_id FROM ' . $this->options->workflowTable() . ' w ' . 121 | 'WHERE w.workflow_id NOT IN ( SELECT DISTINCT e.workflow_id FROM ' . $this->options->executionTable() . ') ' . 122 | ' AND w.workflow_outdated = 1'; 123 | $stmt = $this->conn->query(); 124 | 125 | $workflowIds = array(); 126 | while ($workflowId = $stmt->fetchColumn()) { 127 | $workflowIds[] = $workflowId; 128 | } 129 | return $workflowIds; 130 | } 131 | 132 | public function loadWorkflowById($workflowId) 133 | { 134 | return $this->definitionStorage->loadById($workflowId); 135 | } 136 | 137 | public function save(\ezcWorkflow $workflow) 138 | { 139 | $this->definitionStorage->save($workflow); 140 | } 141 | } -------------------------------------------------------------------------------- /lib/DoctrineExtensions/Workflow/SchemaBuilder.php: -------------------------------------------------------------------------------- 1 | conn = $connection; 16 | } 17 | 18 | public function createWorkflowSchema(WorkflowOptions $options) 19 | { 20 | $sqlCol = new \Doctrine\DBAL\Schema\Visitor\CreateSchemaSqlCollector($this->conn->getDatabasePlatform()); 21 | $schema = $this->getWorkflowSchema($options); 22 | $schema->visit($sqlCol); 23 | 24 | foreach ($sqlCol->getQueries() AS $query) { 25 | $this->conn->exec($query); 26 | } 27 | } 28 | 29 | public function dropWorkflowSchema(WorkflowOptions $options) 30 | { 31 | $sqlCol = new \Doctrine\DBAL\Schema\Visitor\DropSchemaSqlCollector($this->conn->getDatabasePlatform()); 32 | $schema = $this->getWorkflowSchema($options); 33 | $schema->visit($sqlCol); 34 | 35 | $queries = $sqlCol->getQueries(); 36 | foreach ($queries AS $query) { 37 | try { 38 | $this->conn->exec($query); 39 | } catch(\Exception $e) { 40 | 41 | } 42 | } 43 | } 44 | 45 | public function getWorkflowSchema(WorkflowOptions $options) 46 | { 47 | $schema = new \Doctrine\DBAL\Schema\Schema(); 48 | 49 | $workflowTable = $schema->createTable($options->workflowTable()); 50 | $columnOptions = $this->_handlePrimaryKey($schema, $options->workflowTable(), $options->workflowSequence() ); 51 | $workflowTable->addColumn('workflow_id', 'integer', $columnOptions); 52 | $workflowTable->addColumn('workflow_name', 'string'); 53 | $workflowTable->addColumn('workflow_version', 'integer'); 54 | $workflowTable->addColumn('workflow_outdated', 'integer'); 55 | $workflowTable->addColumn('workflow_created', 'datetime'); 56 | $workflowTable->setPrimaryKey(array('workflow_id')); 57 | $workflowTable->addUniqueIndex(array('workflow_name', 'workflow_version')); 58 | 59 | $nodeTable = $schema->createTable($options->nodeTable()); 60 | $columnOptions = $this->_handlePrimaryKey($schema, $options->nodeTable(), $options->nodeSequence() ); 61 | $nodeTable->addColumn('node_id', 'integer', $columnOptions); 62 | $nodeTable->addColumn('workflow_id', 'integer'); 63 | $nodeTable->addColumn('node_class', 'string'); 64 | $nodeTable->addColumn('node_configuration', 'text', array('notnull' => false, "length" => null)); 65 | $nodeTable->setPrimaryKey(array('node_id')); 66 | $nodeTable->addIndex(array('workflow_id')); 67 | $nodeTable->addForeignKeyConstraint($options->workflowTable(), array('workflow_id'), array('workflow_id'), array('onDelete' => 'CASCADE')); 68 | 69 | $connectionTable = $schema->createTable($options->nodeConnectionTable()); 70 | $columnOptions = $this->_handlePrimaryKey($schema, $options->nodeConnectionTable(), $options->nodeConnectionSequence() ); 71 | $connectionTable->addColumn('id', 'integer', $columnOptions); 72 | $connectionTable->addColumn('incoming_node_id', 'integer'); 73 | $connectionTable->addColumn('outgoing_node_id', 'integer'); 74 | $connectionTable->setPrimaryKey(array('id')); 75 | $connectionTable->addForeignKeyConstraint($options->nodeTable(), array('incoming_node_id'), array('node_id'), array('onDelete' => 'CASCADE')); 76 | $connectionTable->addForeignKeyConstraint($options->nodeTable(), array('outgoing_node_id'), array('node_id'), array('onDelete' => 'CASCADE')); 77 | 78 | $variableHandlerTable = $schema->createTable($options->variableHandlerTable()); 79 | $variableHandlerTable->addColumn('workflow_id', 'integer'); 80 | $variableHandlerTable->addColumn('variable', 'string'); 81 | $variableHandlerTable->addColumn('class', 'string'); 82 | $variableHandlerTable->setPrimaryKey(array('workflow_id', 'variable')); 83 | $variableHandlerTable->addForeignKeyconstraint($options->workflowTable(), array('workflow_id'), array('workflow_id')); 84 | 85 | $executionTable = $schema->createTable($options->executionTable()); 86 | $columnOptions = $this->_handlePrimaryKey($schema, $options->executionTable(), $options->executionSequence() ); 87 | $executionTable->addColumn('execution_id', 'integer', $columnOptions); 88 | $executionTable->addColumn('workflow_id', 'integer'); 89 | $executionTable->addColumn('execution_parent', 'integer', array('notnull' => false)); 90 | $executionTable->addColumn('execution_started', 'datetime'); 91 | $executionTable->addColumn('execution_suspended', 'datetime', array('notnull' => false)); 92 | $executionTable->addColumn('execution_variables', 'text', array('notnull' => false, "length" => null)); 93 | $executionTable->addColumn('execution_waiting_for', 'text', array('notnull' => false, "length" => null)); 94 | $executionTable->addColumn('execution_threads', 'text', array('notnull' => false, "length" => null)); 95 | $executionTable->addColumn('execution_next_thread_id', 'integer'); 96 | $executionTable->addColumn('execution_next_poll_date', 'datetime', array('notnull' => false)); 97 | $executionTable->addIndex(array('execution_next_poll_date')); 98 | 99 | $executionTable->setPrimaryKey(array('execution_id')); 100 | $executionTable->addIndex(array('execution_parent')); 101 | $executionTable->addForeignKeyConstraint($options->workflowTable(), array('workflow_id'), array('workflow_id')); 102 | $executionTable->addForeignKeyConstraint($options->executionTable(), array('execution_parent'), array('execution_id')); 103 | 104 | $executionStateTable = $schema->createTable($options->executionStateTable()); 105 | $executionStateTable->addColumn('execution_id', 'integer'); 106 | $executionStateTable->addColumn('node_id', 'integer'); 107 | $executionStateTable->addColumn('node_state', 'text', array('notnull' => false, "length" => null)); 108 | $executionStateTable->addColumn('node_activated_from', 'text', array('notnull' => false, "length" => null)); 109 | $executionStateTable->addColumn('node_thread_id', 'integer'); 110 | $executionStateTable->setPrimaryKey(array('execution_id', 'node_id')); 111 | $executionStateTable->addForeignKeyConstraint($options->executionTable(), array('execution_id'), array('execution_id')); 112 | $executionStateTable->addForeignKeyConstraint($options->nodeTable(), array('node_id'), array('node_id')); 113 | 114 | return $schema; 115 | } 116 | 117 | protected function _handlePrimaryKey(Schema $schema, $tableName, $sequenceName = null) 118 | { 119 | $columnOptions = array(); 120 | if ($this->conn->getDatabasePlatform()->prefersIdentityColumns()) { 121 | $columnOptions = array('autoincrement' => true); 122 | } elseif ($this->conn->getDatabasePlatform( )->prefersSequences()) { 123 | $sequence = $schema->createSequence($sequenceName); 124 | // Doens't work because of the ordering used by Doctrine in dropping tables. 125 | //$columnOptions = array( 'default' => "nextval('" . $sequenceName . "')" ); 126 | } 127 | return $columnOptions; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /tests/DoctrineExtensions/Workflow/DefinitionStorageTest.php: -------------------------------------------------------------------------------- 1 | conn = \DoctrineExtensions\Workflow\TestHelper::getConnection(); 21 | $this->options = new WorkflowOptions('test_', null, null, $this->createSerializer()); 22 | TestHelper::createSchema($this->options); 23 | } 24 | 25 | public function testSaveNodes() 26 | { 27 | $workflow = new \ezcWorkflow('Test'); 28 | 29 | $printAction1 = new \ezcWorkflowNodeAction(array('class' => 'DoctrinExtensions\Workflow\MyPrintAction', 'arguments' => array('Foo'))); 30 | $printAction2 = new \ezcWorkflowNodeAction(array('class' => 'DoctrinExtensions\Workflow\MyPrintAction', 'arguments' => array('Bar'))); 31 | $printAction3 = new \ezcWorkflowNodeAction(array('class' => 'DoctrinExtensions\Workflow\MyPrintAction', 'arguments' => array('Baz'))); 32 | 33 | $workflow->startNode->addOutNode($printAction1); 34 | $printAction2->addInNode($printAction1); 35 | $printAction3->addInNode($printAction2); 36 | $workflow->endNode->addInNode($printAction3); 37 | 38 | $this->assertWorkflowPersistance($workflow); 39 | } 40 | 41 | public function testSaveFinallyNodes() 42 | { 43 | $finallyAction = new \ezcWorkflowNodeFinally(); 44 | $workflow = new \ezcWorkflow('Test', null, null, $finallyAction); 45 | 46 | $workflow->startNode->addOutNode($workflow->endNode); 47 | 48 | $this->assertWorkflowPersistance($workflow); 49 | } 50 | 51 | public function testSaveVariableHandlers() 52 | { 53 | 54 | $workflow = new \ezcWorkflow('Test'); 55 | $workflow->startNode->addOutNode($workflow->endNode); 56 | 57 | $variableHandler = $this->getMock('ezcWorkflowVariableHandler'); 58 | $workflow->addVariableHandler('foo', get_class($variableHandler)); 59 | 60 | $this->assertWorkflowPersistance($workflow); 61 | } 62 | 63 | public function assertWorkflowPersistance(\ezcWorkflow $workflow) 64 | { 65 | $manager = new WorkflowManager($this->conn, $this->options); 66 | $manager->save($workflow); 67 | 68 | $persistedWorkflow = $manager->loadWorkflowById($workflow->id); 69 | $this->assertEquals($workflow, $persistedWorkflow, "The persisted workflow has to be exactly equal to the orignal one after loading."); 70 | } 71 | 72 | public function testWorkflowIdentityMap() 73 | { 74 | $this->markTestSkipped('No Identity Map anymore, workflows have state that i dont fully grasp yet.'); 75 | 76 | $workflow = new \ezcWorkflow('IdentityTest'); 77 | $workflow->startNode->addOutNode($workflow->endNode); 78 | 79 | $manager = new WorkflowManager($this->conn, $this->options); 80 | $manager->save($workflow); 81 | 82 | $this->assertSame($workflow, $manager->loadWorkflowById($workflow->id)); 83 | $this->assertSame($manager->loadWorkflowById($workflow->id), $manager->loadWorkflowById($workflow->id)); 84 | } 85 | 86 | public function testDeleteWorkflow() 87 | { 88 | $variableHandler = $this->getMock('ezcWorkflowVariableHandler'); 89 | $workflow = new \ezcWorkflow('IdentityTest'); 90 | $workflow->startNode->addOutNode($workflow->endNode); 91 | $workflow->addVariableHandler('foo', get_class($variableHandler)); 92 | 93 | $manager = new WorkflowManager($this->conn, $this->options); 94 | $manager->save($workflow); 95 | 96 | $manager->deleteWorkflow($workflow->id); 97 | 98 | $this->setExpectedException('ezcWorkflowDefinitionStorageException', 'Could not load workflow definition.'); 99 | $manager->loadWorkflowById($workflow->id); 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 | } 209 | } 210 | 211 | class MyPrintAction implements \ezcWorkflowServiceObject 212 | { 213 | private $whatToSay; 214 | 215 | public function __construct($whatToSay) { 216 | $this->whatToSay = $whatToSay; 217 | } 218 | 219 | public function __toString() { 220 | return 'myPrint'; 221 | } 222 | public function execute(\ezcWorkflowExecution $execution) { 223 | echo $this->whatToSay."\n"; 224 | } 225 | } -------------------------------------------------------------------------------- /lib/DoctrineExtensions/Workflow/DoctrineExecution.php: -------------------------------------------------------------------------------- 1 | conn = $conn; 38 | $this->definitionStorage = $storage; 39 | $this->options = $storage->getOptions(); 40 | 41 | if ($executionId !== null) { 42 | $this->loadExecution($executionId); 43 | } 44 | } 45 | 46 | protected function doStart($parentId) 47 | { 48 | $this->conn->beginTransaction(); 49 | 50 | $platform = $this->conn->getDatabasePlatform(); 51 | 52 | $variables = $this->variables; 53 | 54 | $executionNextPollDate = null; 55 | if (isset($variables['batchWaitInterval'])) { 56 | if (!($variables['batchWaitInterval'] instanceof \DateInterval)) { 57 | throw new \ezcWorkflowExecutionException("Specified batch waiting interval has to be instance of DateInterval!"); 58 | } 59 | 60 | $executionNextPollDate = new \DateTime("now"); 61 | $executionNextPollDate->add($variables['batchWaitInterval']); 62 | $executionNextPollDate = $executionNextPollDate->format($platform->getDateTimeFormatString()); 63 | } 64 | 65 | $serializer = $this->options->getSerializer(); 66 | 67 | $now = new \DateTime("now"); 68 | $data = array( 69 | 'workflow_id' => (int)$this->workflow->id, 70 | 'execution_parent' => $parentId, 71 | 'execution_started' => $now->format($platform->getDateTimeFormatString()), 72 | 'execution_variables' => $serializer->serialize($variables), 73 | 'execution_waiting_for' => $serializer->serialize($this->waitingFor), 74 | 'execution_threads' => $serializer->serialize($this->threads), 75 | 'execution_next_thread_id' => (int)$this->nextThreadId, 76 | 'execution_next_poll_date' => $executionNextPollDate, 77 | ); 78 | if ( $platform->prefersSequences( ) ) { 79 | $data['execution_id'] = (int) $this->conn->fetchColumn($platform->getSequenceNextValSQL($this->options->executionSequence())); 80 | $this->id = $data['execution_id']; 81 | } 82 | $this->conn->insert($this->options->executionTable(), $data); 83 | 84 | // execution_id 85 | if ( !$platform->prefersSequences( ) ) { 86 | $this->id = (int)$this->conn->lastInsertId(); 87 | } 88 | } 89 | 90 | protected function doResume() 91 | { 92 | $this->conn->beginTransaction(); 93 | } 94 | 95 | protected function doEnd() 96 | { 97 | $this->cleanUpExecutionStateTable(); 98 | $this->cleanUpExecutionTable(); 99 | 100 | if ( !$this->isCancelled() ) { 101 | $this->conn->commit(); 102 | } else { 103 | $this->conn->rollBack(); // ? 104 | } 105 | } 106 | 107 | protected function cleanUpExecutionTable() 108 | { 109 | $sql = 'DELETE FROM ' . $this->options->executionTable() . ' WHERE execution_id = ? OR execution_parent = ?'; 110 | $stmt = $this->conn->prepare($sql); 111 | $stmt->bindParam(1, $this->id); 112 | $stmt->bindParam(2, $this->id); 113 | $stmt->execute(); 114 | } 115 | 116 | protected function cleanUpExecutionStateTable() 117 | { 118 | $sql = 'DELETE FROM ' . $this->options->executionStateTable() . ' WHERE execution_id = ?'; 119 | $stmt = $this->conn->prepare($sql); 120 | $stmt->bindParam(1, $this->id); 121 | $stmt->execute(); 122 | } 123 | 124 | protected function doGetSubExecution($id = null) 125 | { 126 | if (is_numeric($id)) { 127 | $id = (int)$id; 128 | } 129 | 130 | return new self( $this->conn, $this->definitionStorage, $id ); 131 | } 132 | 133 | protected function doSuspend() 134 | { 135 | $platform = $this->conn->getDatabasePlatform(); 136 | 137 | $variables = $this->variables; 138 | $executionNextPollDate = null; 139 | if (isset($variables['batchWaitInterval'])) { 140 | if (!($variables['batchWaitInterval'] instanceof \DateInterval)) { 141 | throw new \ezcWorkflowExecutionException("Specified batch waiting interval has to be instance of DateInterval!"); 142 | } 143 | 144 | $executionNextPollDate = new \DateTime("now"); 145 | $executionNextPollDate->add($variables['batchWaitInterval']); 146 | $executionNextPollDate = $executionNextPollDate->format($platform->getDateTimeFormatString()); 147 | } 148 | 149 | $serializer = $this->options->getSerializer(); 150 | 151 | $now = new \DateTime("now"); 152 | $data = array( 153 | 'execution_suspended' => $now->format($platform->getDateTimeFormatString()), 154 | 'execution_variables' => $serializer->serialize($variables), 155 | 'execution_waiting_for' => $serializer->serialize($this->waitingFor), 156 | 'execution_threads' => $serializer->serialize($this->threads), 157 | 'execution_next_thread_id' => (int)$this->nextThreadId, 158 | 'execution_next_poll_date' => $executionNextPollDate, 159 | ); 160 | 161 | $this->cleanUpExecutionStateTable(); 162 | $this->conn->update($this->options->executionTable(), $data, array('execution_id' => (int)$this->id)); 163 | 164 | foreach ($this->activatedNodes AS $node) { 165 | $data = array( 166 | 'execution_id' => (int)$this->id, 167 | 'node_id' => (int)$node->getId(), 168 | 'node_state' => $serializer->serialize( $node->getState() ), 169 | 'node_activated_from' => $serializer->serialize( $node->getActivatedFrom() ), 170 | 'node_thread_id' => $node->getThreadId(), 171 | ); 172 | $this->conn->insert($this->options->executionStateTable(), $data); 173 | } 174 | 175 | $this->conn->commit(); 176 | } 177 | 178 | protected function loadExecution($executionId) 179 | { 180 | if (!is_int($executionId)) { 181 | throw new \ezcWorkflowExecutionException("Execution-Id has to be an integer (strictly-typed)."); 182 | } 183 | 184 | $sql = 'SELECT * FROM ' . $this->options->executionTable() . ' WHERE execution_id = ?'; 185 | $stmt = $this->conn->prepare($sql); 186 | $stmt->bindParam(1, $executionId); 187 | $stmt->execute(); 188 | 189 | $result = $stmt->fetchAll( \PDO::FETCH_ASSOC ); 190 | 191 | if ( $result === false || empty( $result ) ) { 192 | throw new \ezcWorkflowExecutionException('Could not load execution state for ID ' . ((int)$executionId)); 193 | } 194 | 195 | $execution = array_change_key_case($result[0], \CASE_LOWER); 196 | 197 | $this->id = (int)$execution['execution_id']; 198 | $this->nextThreadId = $execution['execution_next_thread_id']; 199 | 200 | $serializer = $this->options->getSerializer(); 201 | 202 | $this->variables = $serializer->unserialize($execution['execution_variables']); 203 | $this->waitingFor = $serializer->unserialize($execution['execution_waiting_for']); 204 | $this->threads = $serializer->unserialize($execution['execution_threads']); 205 | 206 | $this->workflow = $this->definitionStorage->loadById($execution['workflow_id']); 207 | 208 | $sql = 'SELECT * FROM ' . $this->options->executionStateTable() . ' WHERE execution_id = ?'; 209 | $stmt = $this->conn->prepare($sql); 210 | $stmt->bindParam(1, $executionId); 211 | $stmt->execute(); 212 | 213 | $result = $stmt->fetchAll(\PDO::FETCH_ASSOC); 214 | $active = array(); 215 | 216 | foreach ( $result as $row ) { 217 | $row = array_change_key_case($row, \CASE_LOWER); 218 | 219 | $active[$row['node_id']] = array( 220 | 'activated_from' => $serializer->unserialize($row['node_activated_from']), 221 | 'state' => $serializer->unserialize($row['node_state'], null), 222 | 'thread_id' => $row['node_thread_id'], 223 | ); 224 | } 225 | 226 | foreach ( $this->workflow->nodes as $node ) { 227 | $nodeId = $node->getId(); 228 | 229 | if ( isset( $active[$nodeId] ) ) { 230 | $node->setActivationState( \ezcWorkflowNode::WAITING_FOR_EXECUTION ); 231 | $node->setThreadId( $active[$nodeId]['thread_id'] ); 232 | $node->setState( $active[$nodeId]['state'], null ); 233 | $node->setActivatedFrom( $active[$nodeId]['activated_from'] ); 234 | 235 | $this->activate( $node, false ); 236 | } 237 | } 238 | 239 | $this->cancelled = false; 240 | $this->ended = false; 241 | $this->loaded = true; 242 | $this->resumed = false; 243 | $this->suspended = true; 244 | } 245 | 246 | /** 247 | * Loads data from variable handlers and 248 | * merge it with the current execution data. 249 | */ 250 | protected function loadFromVariableHandlers() 251 | { 252 | foreach ($this->workflow->getVariableHandlers() as $variableName => $className) { 253 | $object = $this->options->getWorkflowFactory()->createVariableHandler($className); 254 | $this->setVariable($variableName, $object->load($this, $variableName)); 255 | } 256 | } 257 | 258 | /** 259 | * Saves data to execution data handlers. 260 | */ 261 | protected function saveToVariableHandlers() 262 | { 263 | foreach ($this->workflow->getVariableHandlers() as $variableName => $className) { 264 | if (isset($this->variables[$variableName])) { 265 | $object = $this->options->getWorkflowFactory()->createVariableHandler($className); 266 | $object->save($this, $variableName, $this->variables[$variableName]); 267 | } 268 | } 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /tests/DoctrineExtensions/Workflow/EzcExecutionTest.php: -------------------------------------------------------------------------------- 1 | conn = \DoctrineExtensions\Workflow\TestHelper::getConnection(); 16 | $this->options = new WorkflowOptions('test_'); 17 | TestHelper::createSchema($this->options); 18 | 19 | $this->dbStorage = new DefinitionStorage($this->conn, $this->options); 20 | } 21 | 22 | public function testStartInputEnd() 23 | { 24 | $this->setUpStartInputEnd(); 25 | $this->dbStorage->save( $this->workflow ); 26 | 27 | $execution = new DoctrineExecution($this->conn, $this->dbStorage); 28 | $execution->workflow = $this->workflow; 29 | 30 | $id = $execution->start(); 31 | 32 | $this->assertNotNull( $id ); 33 | $this->assertFalse( $execution->hasEnded() ); 34 | $this->assertFalse( $execution->isCancelled() ); 35 | $this->assertFalse( $execution->isResumed() ); 36 | $this->assertTrue( $execution->isSuspended() ); 37 | 38 | $execution = new DoctrineExecution($this->conn, $this->dbStorage, $id); 39 | 40 | $this->assertFalse( $execution->hasEnded() ); 41 | $this->assertFalse( $execution->isCancelled() ); 42 | $this->assertFalse( $execution->isResumed() ); 43 | $this->assertTrue( $execution->isSuspended() ); 44 | 45 | $execution->resume( array( 'variable' => 'value' ) ); 46 | 47 | $this->assertTrue( $execution->hasEnded() ); 48 | $this->assertFalse( $execution->isCancelled() ); 49 | $this->assertFalse( $execution->isResumed() ); 50 | $this->assertFalse( $execution->isSuspended() ); 51 | } 52 | 53 | public function testStartInputEndReset() 54 | { 55 | $this->setUpStartInputEnd(); 56 | $this->dbStorage->save( $this->workflow ); 57 | 58 | $execution = new DoctrineExecution($this->conn, $this->dbStorage); 59 | $execution->workflow = $this->workflow; 60 | 61 | $id = $execution->start(); 62 | 63 | $this->assertNotNull( $id ); 64 | $this->assertFalse( $execution->hasEnded() ); 65 | $this->assertFalse( $execution->isCancelled() ); 66 | $this->assertFalse( $execution->isResumed() ); 67 | $this->assertTrue( $execution->isSuspended() ); 68 | 69 | $execution = new DoctrineExecution($this->conn, $this->dbStorage, $id); 70 | 71 | $this->assertFalse( $execution->hasEnded() ); 72 | $this->assertFalse( $execution->isCancelled() ); 73 | $this->assertFalse( $execution->isResumed() ); 74 | $this->assertTrue( $execution->isSuspended() ); 75 | 76 | $execution->resume( array( 'variable' => 'value' ) ); 77 | 78 | $this->assertTrue( $execution->hasEnded() ); 79 | $this->assertFalse( $execution->isCancelled() ); 80 | $this->assertFalse( $execution->isResumed() ); 81 | $this->assertFalse( $execution->isSuspended() ); 82 | 83 | $execution = new DoctrineExecution($this->conn, $this->dbStorage); 84 | $execution->workflow = $this->workflow; 85 | $execution->workflow->reset(); 86 | 87 | $id = $execution->start(); 88 | 89 | $this->assertNotNull( $id ); 90 | $this->assertFalse( $execution->hasEnded() ); 91 | $this->assertFalse( $execution->isCancelled() ); 92 | $this->assertFalse( $execution->isResumed() ); 93 | $this->assertTrue( $execution->isSuspended() ); 94 | 95 | $execution = new DoctrineExecution($this->conn, $this->dbStorage, $id); 96 | 97 | $this->assertFalse( $execution->hasEnded() ); 98 | $this->assertFalse( $execution->isCancelled() ); 99 | $this->assertFalse( $execution->isResumed() ); 100 | $this->assertTrue( $execution->isSuspended() ); 101 | 102 | $execution->resume( array( 'variable' => 'value' ) ); 103 | 104 | $this->assertTrue( $execution->hasEnded() ); 105 | $this->assertFalse( $execution->isCancelled() ); 106 | $this->assertFalse( $execution->isResumed() ); 107 | $this->assertFalse( $execution->isSuspended() ); 108 | } 109 | 110 | public function testParallelSplitSynchronization() 111 | { 112 | $this->setUpParallelSplitSynchronization2(); 113 | $this->dbStorage->save( $this->workflow ); 114 | 115 | $execution = new DoctrineExecution($this->conn, $this->dbStorage); 116 | $execution->workflow = $this->workflow; 117 | 118 | $id = $execution->start(); 119 | 120 | $this->assertNotNull( $id ); 121 | $this->assertFalse( $execution->hasEnded() ); 122 | $this->assertFalse( $execution->isCancelled() ); 123 | $this->assertFalse( $execution->isResumed() ); 124 | $this->assertTrue( $execution->isSuspended() ); 125 | 126 | $execution = new DoctrineExecution($this->conn, $this->dbStorage, $id); 127 | 128 | $this->assertFalse( $execution->hasEnded() ); 129 | $this->assertFalse( $execution->isCancelled() ); 130 | $this->assertFalse( $execution->isResumed() ); 131 | $this->assertTrue( $execution->isSuspended() ); 132 | 133 | $execution->resume( array( 'foo' => 'bar' ) ); 134 | 135 | $this->assertFalse( $execution->hasEnded() ); 136 | $this->assertFalse( $execution->isCancelled() ); 137 | $this->assertFalse( $execution->isResumed() ); 138 | $this->assertTrue( $execution->isSuspended() ); 139 | 140 | $execution = new DoctrineExecution($this->conn, $this->dbStorage, $id); 141 | 142 | $this->assertFalse( $execution->hasEnded() ); 143 | $this->assertFalse( $execution->isCancelled() ); 144 | $this->assertFalse( $execution->isResumed() ); 145 | $this->assertTrue( $execution->isSuspended() ); 146 | 147 | $execution->resume( array( 'bar' => 'foo' ) ); 148 | 149 | $this->assertTrue( $execution->hasEnded() ); 150 | $this->assertFalse( $execution->isCancelled() ); 151 | $this->assertFalse( $execution->isResumed() ); 152 | $this->assertFalse( $execution->isSuspended() ); 153 | } 154 | 155 | public function testNonInteractiveSubWorkflow() 156 | { 157 | $this->setUpStartEnd(); 158 | $this->dbStorage->save( $this->workflow ); 159 | 160 | $this->setUpWorkflowWithSubWorkflow( 'StartEnd' ); 161 | $this->dbStorage->save( $this->workflow ); 162 | 163 | $execution = new DoctrineExecution($this->conn, $this->dbStorage); 164 | $execution->workflow = $this->workflow; 165 | 166 | $id = $execution->start(); 167 | 168 | $this->assertNull( $id ); 169 | $this->assertTrue( $execution->hasEnded() ); 170 | $this->assertFalse( $execution->isCancelled() ); 171 | $this->assertFalse( $execution->isResumed() ); 172 | $this->assertFalse( $execution->isSuspended() ); 173 | } 174 | 175 | public function testInteractiveSubWorkflow() 176 | { 177 | $this->setUpStartInputEnd(); 178 | $this->dbStorage->save( $this->workflow ); 179 | 180 | $this->setUpWorkflowWithSubWorkflow( 'StartInputEnd' ); 181 | $this->dbStorage->save( $this->workflow ); 182 | 183 | $execution = new DoctrineExecution($this->conn, $this->dbStorage); 184 | $execution->workflow = $this->workflow; 185 | 186 | $id = $execution->start(); 187 | 188 | $this->assertNotNull( $id ); 189 | $this->assertFalse( $execution->hasEnded() ); 190 | $this->assertFalse( $execution->isCancelled() ); 191 | $this->assertFalse( $execution->isResumed() ); 192 | $this->assertTrue( $execution->isSuspended() ); 193 | 194 | $execution = new DoctrineExecution($this->conn, $this->dbStorage, $id); 195 | 196 | $this->assertFalse( $execution->hasEnded() ); 197 | $this->assertFalse( $execution->isCancelled() ); 198 | $this->assertFalse( $execution->isResumed() ); 199 | $this->assertTrue( $execution->isSuspended() ); 200 | 201 | $execution->resume( array( 'variable' => 'value' ) ); 202 | 203 | $this->assertTrue( $execution->hasEnded() ); 204 | $this->assertFalse( $execution->isCancelled() ); 205 | $this->assertFalse( $execution->isResumed() ); 206 | $this->assertFalse( $execution->isSuspended() ); 207 | } 208 | 209 | public function testInvalidExecutionIdThrowsException() 210 | { 211 | try { 212 | $execution = new DoctrineExecution($this->conn, $this->dbStorage, '1'); 213 | } 214 | catch ( \ezcWorkflowExecutionException $e ) { 215 | $this->assertEquals( 'Execution-Id has to be an integer (strictly-typed).', $e->getMessage() ); 216 | return; 217 | } 218 | 219 | $this->fail( 'Expected an ezcWorkflowExecutionException to be thrown.' ); 220 | } 221 | 222 | public function testNotExistingExecutionThrowsException() 223 | { 224 | try { 225 | $execution = new DoctrineExecution($this->conn, $this->dbStorage, 1); 226 | } 227 | catch ( \ezcWorkflowExecutionException $e ) { 228 | $this->assertEquals( 'Could not load execution state for ID 1', $e->getMessage() ); 229 | return; 230 | } 231 | 232 | $this->fail( 'Expected an ezcWorkflowExecutionException to be thrown.' ); 233 | } 234 | 235 | public function testPropertiesInvalidWorkflowInstance_ThrowsException() 236 | { 237 | $execution = new DoctrineExecution($this->conn, $this->dbStorage); 238 | 239 | try { 240 | $execution->workflow = new \StdClass; 241 | } 242 | catch ( \ezcBaseValueException $e ) { 243 | $this->assertEquals( 'The value \'O:8:"stdClass":0:{}\' that you were trying to assign to setting \'workflow\' is invalid. Allowed values are: ezcWorkflow.', $e->getMessage() ); 244 | return; 245 | } 246 | 247 | $this->fail( 'Expected an ezcBaseValueException to be thrown.' ); 248 | } 249 | 250 | public function testProperties3() 251 | { 252 | $execution = new DoctrineExecution($this->conn, $this->dbStorage); 253 | 254 | try { 255 | $foo = $execution->foo; 256 | } 257 | catch ( \ezcBasePropertyNotFoundException $e ) { 258 | $this->assertEquals( 'No such property name \'foo\'.', $e->getMessage() ); 259 | return; 260 | } 261 | 262 | $this->fail( 'Expected an ezcBasePropertyNotFoundException to be thrown.' ); 263 | } 264 | 265 | public function testProperties4() 266 | { 267 | $this->setUpStartEnd(); 268 | 269 | $execution = new DoctrineExecution($this->conn, $this->dbStorage); 270 | 271 | try { 272 | $execution->foo = null; 273 | } 274 | catch ( \ezcBasePropertyNotFoundException $e ) { 275 | $this->assertEquals( 'No such property name \'foo\'.', $e->getMessage() ); 276 | return; 277 | } 278 | 279 | $this->fail( 'Expected an ezcBasePropertyNotFoundException to be thrown.' ); 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # Doctrine 2 Persistence for ezcWorkflow 2 | 3 | This Doctrine 2 Extension offers a persistence mechanism for workflows and workflow executions exactly like ezcWorkflowDatabaseTiein does for the ezcDatabase component. 4 | 5 | * [Doctrine 2](http://www.doctrine-project.org) 6 | * [Zeta Components Workflow](http://www.ezcomponents.org/docs/api/trunk/introduction_Workflow.html) 7 | 8 | This extension uses the `Doctrine\DBAL` component and is not built on top of the ORM by default. In a later section this 9 | document also describes how you can integrate Workflow with the Doctrine 2 ORM. 10 | 11 | ## Configuration 12 | 13 | The Public API of Doctrine Workflow is implemented by the `DotrineExtensions\Workflow\WorkflowManager` class. 14 | It accepts a `Doctrine\DBAL\Connection` and a `DoctrineExtensions\Workflow\WorkflowOptions` instance as 15 | its constructor arguments: 16 | 17 | use DotrineExtensions\Workflow\WorkflowManager; 18 | 19 | $manager = new WorkflowManager($conn, $options); 20 | 21 | The `WorkflowManager` implements an interface `IWorkflowManager` that can be used for decoupling 22 | your domain model from the Doctrine based Workflow Engine for testability. 23 | 24 | Doctrine Workflow is configured with a `WorkflowOptions` instance. It receives the following arguments 25 | in the constructor: 26 | 27 | $options = new WorkflowOptions($prefix, $workflowClass, $nodeFactory, $serializer); 28 | 29 | ### $prefix - Table Prefix 30 | 31 | Defines the prefix for the 6 required workflow persistence tables. 32 | 33 | ### $workflowClass 34 | 35 | Defines the class workflows should be instantiated with, defaults to `ezcWorkflow`. 36 | 37 | ### $workflowFactory - Dependency Injection 38 | 39 | By default Doctrine uses a the `DoctrineExtensions\Workflow\WorkflowFactory` instance to create 40 | Node and VariableHandler instances. However there are often cases when you want to inject node classes that delegate work to other 41 | more powerful services. You can extend the WorkflowFactory to support this: 42 | 43 | $myFactory = new MyWorkflowFactory($myDependenyInjectionContainer); 44 | $options = new WorkflowOptions('', null, $myFactory); 45 | 46 | $manager = new WorkflowManager($conn, $options); 47 | 48 | ### $serializer - Serialization of Arrays 49 | 50 | Both the Workflow Definitions and Executions contain variables that are arrays. 51 | Arrays are always ugly to serialize into the database and ezcWorkflows DatabaseTiein 52 | does so by using PHP internal methods `serialize` and `unserialize`. 53 | 54 | By default the Doctrine Workflow also uses this methods however it offers 55 | an alternative using [WDDX](http://en.wikipedia.org/wiki/WDDX) and 56 | the related [PHP Extension](http://php.net/manual/en/book.wddx.php). This 57 | allows to save the data in a human-readable (and editable) format. 58 | 59 | use \DoctrineExtensions\Workflow\WorkflowOptions; 60 | use \DoctrineExtensions\Workflow\Util\Serialize\WddxSerializer; 61 | 62 | $serializer = new WddxSerializer(); 63 | $options = new WorkflowOptions('prefix_', null, null, $serializer); 64 | 65 | Don't bother to implement JSON as a serializer, it won't work since 66 | objects have to be serialized and deserialized. JSON cannot handle this. 67 | 68 | > **WARNING** 69 | > 70 | > You cannot easily change the serializer down the road unless you 71 | > implement the `Serializer` interface with some kind of decorator 72 | > that detects the serializing format before delegating to the 73 | > real serializer. 74 | 75 | ### Setup the Database Schema 76 | 77 | you can setup the database schema for the Persistence using the `DoctrineExtensions\Workflow\SchemaBuilder` 78 | class: 79 | 80 | use DoctrineExtensions\Workflow\WorkflowOptions; 81 | use DoctrineExtensions\Workflow\SchemaBuilder; 82 | 83 | $conn = \Doctrine\DBAL\DriverManager::getConnection($params); 84 | $options = new WorkflowOptions($prefix = 'test_'); 85 | $schemaBuilder = new SchemaBuilder(conn); 86 | $schemaBuilder->dropWorkflowSchema($options); 87 | $schemaBuilder->createWorkflowSchema($options); 88 | 89 | This way you can use `ezcWorkflow` reliably against all supported Doctrine 2 drivers. You 90 | should make sure to (re-)use the same `WorkflowOptions` instance, because it defines 91 | the table prefix to be used. 92 | 93 | ## Saving, Loading and Removing Workflows 94 | 95 | The API resembles the one of the ezcWorkflowDatabaseTiein, see [the tutorial for more information](http://www.ezcomponents.org/docs/api/trunk/introduction_WorkflowDatabaseTiein.html). 96 | 97 | Saving a new workflow is very simple: 98 | 99 | use DoctrineExtensions\Workflow\WorkflowManager; 100 | use DoctrineExtensions\Workflow\WorkflowOptions; 101 | 102 | $options = new WorkflowOptions(...); 103 | $conn = \Doctrine\DBAL\DriverManager::getConnection(...); 104 | 105 | $manager = new WorkflowManager($conn, $options); 106 | $manager->save($workflow); 107 | 108 | You can access the saved workflows ID by accessing the `$workflow->id` property 109 | after the save method was called. 110 | 111 | > **BEWARE** 112 | > 113 | > When you save a workflow retrieved from the database the existing workflow 114 | > is not updated but a completely new workflow is saved into the database. 115 | > The reason for this is simple and powerful: Workflows can be so complex 116 | > that changing the inner workings of one could easily break already 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. 122 | 123 | You can load a workflow by querying for its Workflow Id: 124 | 125 | $workflow = $manager->loadWorkflowById($id); 126 | 127 | Removing a workflow from the database is very simple also, you can do it by ID: 128 | 129 | $manager->deleteWorkflow($workflow->id); 130 | 131 | ### Cleaning up unused Workflows 132 | 133 | As described in the previous sections, workflows are never updated but 134 | new rows are inserted into the database. Depending on the number of workflows 135 | you may get into trouble with the number of rows in the workflow related tables. 136 | 137 | There are methods that allow you to clean up unused workflows. A workflow 138 | is unused, if its marked as outdated (i.e. not the current version of the workflow 139 | as defined by the workflow-name + version unique key) and no execution still 140 | works with that workflow. 141 | 142 | use DoctrineExtensions\Workflow\WorkflowManager; 143 | 144 | $manager = new WorkflowManager($conn, $options); 145 | foreach ($manager->getUnusedWorkflowIds() AS $workflowId) { 146 | $manager->deleteWorkflow($workflowId); 147 | } 148 | 149 | ## Executing Workflows 150 | 151 | Start a workflow and retrieve the execution id when it gets suspended: 152 | 153 | use DoctrineExtensions\Workflow\WorkflowManager; 154 | 155 | $manager = new WorkflowManager($conn, $options); 156 | $execution = $manager->createExecution($workflow); 157 | // or 158 | $execution = $manager->createExecutionByWorkflowId($workflowId); 159 | 160 | $executionId = $execution->start(); 161 | 162 | Resume an operation for a given Execution Id. 163 | 164 | use DoctrineExtensions\Workflow\WorkflowManager; 165 | 166 | $manager = new WorkflowManager($conn, $options); 167 | $execution = $manager->loadExecution($executionId); 168 | $execution->resume(array('choice' => true)); 169 | 170 | ### Batch-Jobs for Resuming Execution 171 | 172 | Whenever Workflow Operations are suspended there are two options to resume 173 | them: 174 | 175 | 1. By a user that knows the Workflow Id 176 | 2. By a batch-job 177 | 178 | A batch job would naturally process ALL the supsended workflows, which 179 | can obviously cause considerable performance issues. 180 | 181 | That is why Doctrine Workflow listens to a special execution variable called 182 | `batchWaitInterval`. This variable has to be an instance of 183 | [`DateInterval`](http://de.php.net/DateInterval). Whenever the execution 184 | of a workflow is suspended and this variable exists, the interval 185 | is applied against the current date and saved into the database. 186 | 187 | You can then poll for the suspended executions: 188 | 189 | $manager = new WorkflowManager($conn, $options); 190 | $exIds = $manager->pollSuspendedExecutionIds($limit = 50, $offset = 0); 191 | 192 | foreach ($exIds AS $executionId) { 193 | $execution = $manager->loadExecution($executionId); 194 | $execution->resume(array()); 195 | } 196 | 197 | This API is really only for batch jobs as its querying very broad for 198 | the suspended execution ids across all workflows. If you need something 199 | more specific or optimized you have to implement it yourself. 200 | 201 | ## Integrating Workflow with Doctrine 2 ORM 202 | 203 | Using the Workflow Engine in isolation is a very academic endeavor, this 204 | section describes how you integrate it into your application using 205 | a Doctrine 2 domain model. 206 | 207 | As an example I use the classic Content-Management-System Article Publishing 208 | cycle. A workflow has to be processed before any article is published. 209 | 210 | The details of workflow generation through a GUI are very complex. Therefore lets 211 | say that our application defines 3 different workflows for article publishing 212 | beforehand. Whenever a new Article is created, it directly gets assigned 213 | a workflow based on some business logic (Depending on User, Category, Whatever). 214 | 215 | Because Doctrine 2 Entities can contain very deep object-graphs (due to lazy-loading) 216 | all the entity variables used have to be handled by the `EntityManagerHandler` Variable Handler. 217 | A Variable Handler in `ezcWorkflow` is a serialize/unserialize transformation applied to a certain variable 218 | upon suspend and resume operations. 219 | 220 | In our case the workflow uses the `CmsArticle` variable as instance of the Article, so 221 | we call: 222 | 223 | $workflow->addVariableHandler('CmsArticle', 'DoctrineExtensions\Workflow\VariableHandler\EntityManagerHandler'); 224 | 225 | One restriction exists with the EntityManagerHandler: You are only allowed to use entities that already have an ID assigned. 226 | 227 | This handler needs access to the `EntityManager` that manages the CmsArticle instance. You can 228 | configure that by passing it to the `WorkflowFactory` constructor: 229 | 230 | $em = EntityManager::create($params); 231 | $factory = new WorkflowFactory($em); 232 | $options = new WorkflowOptions($prefix, null, $factory); 233 | 234 | $workflowManager = new WorkflowManager($em->getConnection(), $options); 235 | 236 | The author can then start the publishing workflow, `CmsArticle::startPublishingWorkflow()` 237 | is called. 238 | 239 | The CMS Article class looks like: 240 | 241 | /** 242 | * @Entity 243 | */ 244 | class CmsArticle 245 | { 246 | /** @Id @GeneratedValue @Column(type="integer") */ 247 | private $id; 248 | 249 | /** @Column(type="integer") */ 250 | private $publishingWorkflowId = 1; 251 | 252 | /** @Column(type="integer", nullable=true) */ 253 | private $publishingExecutionId = null; 254 | 255 | public function getPublishingWorkflowId() 256 | { 257 | return $this->publishingWorkflowId; 258 | } 259 | 260 | public function getPublishingExecutionId() 261 | { 262 | return $this->publishingExecutionId; 263 | } 264 | 265 | public function startPublishingWorkflow($executionId) 266 | { 267 | $this->publishingExecutionId = $executionId; 268 | } 269 | 270 | public function publishingWorkflowHasStarted() 271 | { 272 | return ($this->publishingExecutionId != null); 273 | } 274 | } 275 | 276 | An intermediary CmsPublishingWorkflow class (domain object but not an entity) now controls the workflow: 277 | 278 | /** 279 | * Not an Entity 280 | */ 281 | class CmsPublishingWorkflow 282 | { 283 | private $manager; 284 | 285 | public function __construct(IWorkflowManager $manager) 286 | { 287 | $this->manager = $manager; 288 | } 289 | 290 | public function startPublishingWorkflow(CmsArticle $article) 291 | { 292 | if ($article->publishingWorkflowHasStarted()) { 293 | throw new Exception("A workflow execution was already started!"); 294 | } 295 | 296 | $this->execution = $this->manager->createExecutionByWorkflowId($article->getPublishingWorkflowId()); 297 | $this->execution->setVariable('CmsArticle', $article); 298 | $article->startPublishingWorkflow($this->execution->start()); 299 | } 300 | 301 | public function resumePublishingWorkflow(CmsArticle $article) 302 | { 303 | $this->execution = $this->manager->loadExecution($article->getPublishingExecutionId()); 304 | } 305 | 306 | public function needsPublisherOk() 307 | { 308 | $variables = $this->execution->getWaitingFor(); 309 | return isset($variables['publisherOk']); 310 | } 311 | // more logic! 312 | } 313 | 314 | See how `$article` is passed as a variable to the execution context in `startPublishingWorkflow()`. This variable 315 | is handled by the EntityManagerHandler as described above. 316 | 317 | > **NOTICE** 318 | > 319 | > The `EntityManagerHandler` variable handler does *NOT* execute the flush-operation on the Entity Manager. 320 | > When a workflow is suspended that potentially changed the used entities you have to call `EntityManager::flush()` 321 | > yourself. 322 | 323 | An example of how to use this now: 324 | 325 | $article = $em->find('CmsArticle', $articleId); 326 | // do stuff with the article 327 | $publishingWorkflow = new CmsPublishingWorkflow($manager); 328 | $publishingWorkflow->startPublishingWorkflow($article); 329 | -------------------------------------------------------------------------------- /lib/DoctrineExtensions/Workflow/DefinitionStorage.php: -------------------------------------------------------------------------------- 1 | conn = $conn; 51 | $this->options = $options; 52 | } 53 | 54 | /** 55 | * @return WorkflowOptions 56 | */ 57 | public function getOptions() 58 | { 59 | return $this->options; 60 | } 61 | 62 | /** 63 | * Load a workflow definition by name. 64 | * 65 | * @param string $workflowName 66 | * @param int $workflowVersion 67 | * @return ezcWorkflow 68 | * @throws ezcWorkflowDefinitionStorageException 69 | */ 70 | public function loadByName( $workflowName, $workflowVersion = 0 ) 71 | { 72 | if ($workflowVersion == 0) { 73 | $workflowVersion = $this->getCurrentVersion($workflowName); 74 | } 75 | 76 | $sql = 'SELECT workflow_id FROM ' . $this->options->workflowTable() . ' WHERE workflow_name = ? AND workflow_version = ?'; 77 | $stmt = $this->conn->prepare($sql); 78 | $stmt->bindParam(1, $workflowName); 79 | $stmt->bindParam(2, $workflowVersion); 80 | $stmt->execute(); 81 | 82 | $result = $stmt->fetchAll( \PDO::FETCH_ASSOC ); 83 | 84 | if ( $result !== false && isset( $result[0] ) ) { 85 | $result[0] = array_change_key_case($result[0], \CASE_LOWER); 86 | $workflowId = $result[0]['workflow_id']; 87 | } else { 88 | throw new \ezcWorkflowDefinitionStorageException('Could not load workflow definition.'); 89 | } 90 | 91 | return $this->loadWorkflow($workflowId, $workflowName, $workflowVersion); 92 | } 93 | 94 | /** 95 | * Load a workflow definition by ID. 96 | * 97 | * Providing the name of the workflow that is to be loaded as the 98 | * optional second parameter saves a database query. 99 | * 100 | * @param int $workflowId 101 | * @return ezcWorkflow 102 | * @throws ezcWorkflowDefinitionStorageException 103 | * @throws ezcDbException 104 | */ 105 | public function loadById( $workflowId ) 106 | { 107 | $platform = $this->conn->getDatabasePlatform(); 108 | 109 | $sql = "SELECT workflow_name, workflow_version FROM " . $this->options->workflowTable() . " WHERE workflow_id = ?"; 110 | $stmt = $this->conn->prepare($sql); 111 | $stmt->bindParam(1, $workflowId); 112 | $stmt->execute(); 113 | 114 | $result = $stmt->fetchAll(\PDO::FETCH_ASSOC); 115 | 116 | if (count($result) == 1) { 117 | $result[0] = array_change_key_case($result[0], \CASE_LOWER); 118 | 119 | $workflowName = $result[0]['workflow_name']; 120 | $workflowVersion = $result[0]['workflow_version']; 121 | } else { 122 | throw new \ezcWorkflowDefinitionStorageException('Could not load workflow definition.'); 123 | } 124 | 125 | return $this->loadWorkflow($workflowId, $workflowName, $workflowVersion); 126 | } 127 | 128 | /** 129 | * 130 | * @param int $workflowId 131 | * @param string $workflowName 132 | * @param int $workflowVersion 133 | * @return \ezcWorkflow 134 | */ 135 | protected function loadWorkflow($workflowId, $workflowName, $workflowVersion) 136 | { 137 | $workflowId = (int)$workflowId; 138 | 139 | $sql = "SELECT node_id, node_class, node_configuration FROM " . $this->options->nodeTable() . " WHERE workflow_id = ?"; 140 | $stmt = $this->conn->prepare($sql); 141 | $stmt->bindParam(1, $workflowId); 142 | $stmt->execute(); 143 | 144 | $result = $stmt->fetchAll(\PDO::FETCH_ASSOC); 145 | 146 | $this->workflowNodeIds[$workflowId] = array(); 147 | 148 | // Create node objects. 149 | foreach ( $result as $node ) { 150 | $node = array_change_key_case($node, \CASE_LOWER); 151 | 152 | $configuration = $this->options->getSerializer()->unserialize($node['node_configuration'], null); 153 | 154 | if ( is_null( $configuration ) ) { 155 | $configuration = \ezcWorkflowUtil::getDefaultConfiguration( $node['node_class'] ); 156 | } 157 | 158 | $nodes[$node['node_id']] = $this->options->getWorkflowFactory()->createNode($node['node_class'], $configuration); 159 | $nodes[$node['node_id']]->setId($node['node_id']); 160 | 161 | $this->nodeMap[spl_object_hash($nodes[$node['node_id']])] = $node['node_id']; 162 | $this->workflowNodeIds[$workflowId][] = $node['node_id']; 163 | 164 | if ($nodes[$node['node_id']] instanceof \ezcWorkflowNodeFinally && 165 | !isset( $finallyNode ) ) { 166 | $finallyNode = $nodes[$node['node_id']]; 167 | } 168 | 169 | else if ($nodes[$node['node_id']] instanceof \ezcWorkflowNodeEnd && 170 | !isset( $defaultEndNode ) ) { 171 | $defaultEndNode = $nodes[$node['node_id']]; 172 | } 173 | 174 | else if ($nodes[$node['node_id']] instanceof \ezcWorkflowNodeStart && 175 | !isset( $startNode ) ) { 176 | $startNode = $nodes[$node['node_id']]; 177 | } 178 | } 179 | 180 | if ( !isset( $startNode ) || !isset( $defaultEndNode ) ) { 181 | throw new \ezcWorkflowDefinitionStorageException( 182 | 'Could not load workflow definition.' 183 | ); 184 | } 185 | 186 | $sql = "SELECT nc.outgoing_node_id, nc.incoming_node_id FROM " . $this->options->nodeConnectionTable() ." nc ". 187 | "INNER JOIN " . $this->options->nodeTable() . " n ON n.node_id = nc.incoming_node_id WHERE n.workflow_id = ?"; 188 | $stmt = $this->conn->prepare($sql); 189 | $stmt->bindParam(1, $workflowId); 190 | $stmt->execute(); 191 | 192 | $connections = $stmt->fetchAll( \PDO::FETCH_ASSOC ); 193 | 194 | foreach ( $connections as $connection ) { 195 | $connection = array_change_key_case($connection, \CASE_LOWER); 196 | $nodes[$connection['incoming_node_id']]->addOutNode($nodes[$connection['outgoing_node_id']]); 197 | } 198 | 199 | if ( !isset( $finallyNode ) || count( $finallyNode->getInNodes() ) > 0 ) { 200 | $finallyNode = null; 201 | } 202 | 203 | // Create workflow object and add the node objects to it. 204 | $workflowClassName = $this->options->workflowClassName(); 205 | $workflow = new $workflowClassName( $workflowName, $startNode, $defaultEndNode, $finallyNode ); 206 | $workflow->definitionStorage = $this; 207 | $workflow->id = (int)$workflowId; 208 | $workflow->version = (int)$workflowVersion; 209 | 210 | $sql = "SELECT variable, class FROM " . $this->options->variableHandlerTable() . " WHERE workflow_id = ?"; 211 | $stmt = $this->conn->prepare($sql); 212 | $stmt->bindParam(1, $workflowId); 213 | $stmt->execute(); 214 | 215 | $result = $stmt->fetchAll( \PDO::FETCH_ASSOC ); 216 | $nodes = array(); 217 | 218 | if ( $result !== false ) 219 | { 220 | foreach ( $result as $variableHandler ) 221 | { 222 | $workflow->addVariableHandler( 223 | $variableHandler['variable'], 224 | $variableHandler['class'] 225 | ); 226 | } 227 | } 228 | 229 | // Verify the loaded workflow. 230 | $workflow->verify(); 231 | 232 | return $workflow; 233 | } 234 | 235 | /** 236 | * Save a workflow definition to the database. 237 | * 238 | * @param ezcWorkflow $workflow 239 | * @throws ezcWorkflowDefinitionStorageException 240 | */ 241 | public function save( \ezcWorkflow $workflow ) 242 | { 243 | // Verify the workflow. 244 | $workflow->verify(); 245 | 246 | if (strlen($workflow->name) == 0) { 247 | throw new \ezcWorkflowDefinitionStorageException(); 248 | } 249 | 250 | $platform = $this->conn->getDatabasePlatform(); 251 | 252 | // what mode of saving should it be? Update or Re-Generate? 253 | // 254 | // Conditions that an update sufficies: 255 | // 1. No node has been deleted 256 | // 2. No node has changed its meaning (action class or type) 257 | // 3. For simplicitly only zero or one new nodes will be created. 258 | 259 | $hasExistingNodeIds = array(); 260 | $newNodes = 0; 261 | foreach ( $workflow->nodes as $node ) { 262 | $oid = spl_object_hash($node); 263 | if (!isset($this->nodeMap[$oid])) { 264 | $newNodes++; 265 | } else { 266 | $hasExistingNodeIds[] = $this->nodeMap[$oid]; 267 | } 268 | } 269 | 270 | $canBeUpdate = false; 271 | if ($newNodes < 2 && count(array_diff($hasExistingNodeIds, $this->workflowNodeIds[$workflow->id])) == 0 && $workflow->id) { 272 | $canBeUpdate = true; 273 | } 274 | 275 | $this->workflowNodeIds[$workflow->id] = array(); 276 | 277 | try { 278 | $this->conn->beginTransaction(); 279 | 280 | $workflowVersion = $this->getCurrentVersion($workflow->name) + 1; 281 | 282 | $this->conn->update( 283 | $this->options->workflowTable(), 284 | array('workflow_outdated' => 1), 285 | array('workflow_name' => $workflow->name) 286 | ); 287 | 288 | $date = new \DateTime("now"); 289 | 290 | if ($canBeUpdate) { 291 | $this->conn->update($this->options->workflowTable(), array( 292 | 'workflow_version' => $workflowVersion, 293 | 'workflow_created' => $date->format($platform->getDateTimeFormatString()), 294 | ), array('workflow_id' => $workflow->id) 295 | ); 296 | } else { 297 | $data = array( 298 | 'workflow_name' => $workflow->name, 299 | 'workflow_version' => $workflowVersion, 300 | 'workflow_created' => $date->format($platform->getDateTimeFormatString()), 301 | 'workflow_outdated' => 0, 302 | ); 303 | // For sequences: get id before insert 304 | if ( $platform->prefersSequences( ) ) { 305 | $id = (int) $this->conn->fetchColumn($platform->getSequenceNextValSQL($this->options->workflowSequence())); 306 | $data['workflow_id'] = $id; 307 | $workflow->id = $id; 308 | } 309 | 310 | $this->conn->insert($this->options->workflowTable(), $data); 311 | 312 | if ( $platform->prefersIdentityColumns( ) ) { 313 | $workflow->id = (int)$this->conn->lastInsertId(); 314 | } 315 | 316 | 317 | $workflow->definitionStorage = $this; 318 | } 319 | 320 | // Write node table rows. 321 | $nodeMap = array(); 322 | 323 | foreach ( $workflow->nodes as $node ) { 324 | /* @var $node \ezcWorkflowNode */ 325 | $oid = spl_object_hash($node); 326 | 327 | if ($canBeUpdate && isset($this->nodeMap[$oid])) { 328 | $nodeId = (int)$this->nodeMap[$oid]; 329 | 330 | $this->conn->update($this->options->nodeTable(), array( 331 | 'node_configuration' => $this->options->getSerializer()->serialize( $node->getConfiguration() ), 332 | ), array('node_id' => $nodeId)); 333 | } else { 334 | $data = array( 335 | 'workflow_id' => (int)$workflow->id, 336 | 'node_class' => get_class($node), 337 | 'node_configuration' => $this->options->getSerializer()->serialize( $node->getConfiguration() ), 338 | ); 339 | 340 | if ( $platform->prefersSequences( ) ) { 341 | $nodeId = (int) $this->conn->fetchColumn($platform->getSequenceNextValSQL($this->options->nodeSequence())); 342 | $data['node_id'] = $nodeId; 343 | } 344 | 345 | $this->conn->insert($this->options->nodeTable(), $data); 346 | 347 | if ( $platform->prefersIdentityColumns( ) ) { 348 | $nodeId = (int)$this->conn->lastInsertId(); 349 | } 350 | 351 | } 352 | $nodeMap[$nodeId] = $node; 353 | 354 | $this->workflowNodeIds[$workflow->id][] = $nodeId; 355 | $this->nodeMap[$oid] = $nodeId; 356 | } 357 | 358 | if ($canBeUpdate) { 359 | // Delete all the node connections, NodeMap Keys are casted to (int) so usage here is safe. 360 | $query = "DELETE FROM " . $this->options->nodeConnectionTable() . " " . 361 | "WHERE incoming_node_id IN (" . implode(",", array_keys($nodeMap)) . ") OR " . 362 | "outgoing_node_id IN (" . implode(",", array_keys($nodeMap)) . ")"; 363 | $this->conn->executeUpdate($query); 364 | } 365 | 366 | foreach ($workflow->nodes AS $node) { 367 | foreach ( $node->getOutNodes() as $outNode ) { 368 | $incomingNodeId = null; 369 | $outgoingNodeId = null; 370 | 371 | foreach ( $nodeMap as $_id => $_node ) { 372 | if ( $_node === $node ) { 373 | $incomingNodeId = $_id; 374 | } 375 | 376 | else if ( $_node === $outNode ) { 377 | $outgoingNodeId = $_id; 378 | } 379 | 380 | if ( $incomingNodeId !== NULL && $outgoingNodeId !== NULL ) { 381 | break; 382 | } 383 | } 384 | 385 | $data = array( 386 | 'incoming_node_id' => $incomingNodeId, 387 | 'outgoing_node_id' => $outgoingNodeId, 388 | ); 389 | 390 | if ( $platform->prefersSequences( ) ) { 391 | $id = (int) $this->conn->fetchColumn($platform->getSequenceNextValSQL($this->options->nodeConnectionSequence())); 392 | $data['id'] = $id; 393 | } 394 | 395 | $this->conn->insert($this->options->nodeConnectionTable(), $data ); 396 | 397 | } 398 | } 399 | unset($nodeMap); 400 | 401 | if ($canBeUpdate) { 402 | $this->conn->delete($this->options->variableHandlerTable(), array('workflow_id' => (int)$workflow->id)); 403 | } 404 | 405 | foreach ($workflow->getVariableHandlers() AS $variable => $class) { 406 | $this->conn->insert($this->options->variableHandlerTable(), array( 407 | 'workflow_id' => (int)$workflow->id, 408 | 'variable' => $variable, 409 | 'class' => $class, 410 | )); 411 | } 412 | 413 | $this->conn->commit(); 414 | } catch(\Exception $e) { 415 | $this->conn->rollBack(); 416 | throw new \ezcWorkflowDefinitionStorageException("Error while persisting workflow: " . $e->getMessage()); 417 | } 418 | } 419 | 420 | protected function getCurrentVersion($name) 421 | { 422 | $platform = $this->conn->getDatabasePlatform(); 423 | 424 | $sql = "SELECT workflow_version AS version FROM " . $this->options->workflowTable() . " ". 425 | "WHERE workflow_name = ? " . 426 | " AND workflow_version = ( SELECT MAX(workflow_version) FROM " . $this->options->workflowTable( ) . " WHERE workflow_name = ? )" . 427 | $platform->getForUpdateSQL(); 428 | $stmt = $this->conn->prepare($sql); 429 | $stmt->bindParam(1, $name); 430 | $stmt->bindParam(2, $name); 431 | $stmt->execute(); 432 | 433 | $result = $stmt->fetchAll(\PDO::FETCH_ASSOC); 434 | 435 | if ( $result !== false && isset( $result[0]['version'] ) && $result[0]['version'] !== null ) { 436 | $result[0] = array_change_key_case($result[0], \CASE_LOWER); 437 | return $result[0]['version']; 438 | } else { 439 | return 0; 440 | } 441 | } 442 | 443 | /** 444 | * Delete a workflow by its ID 445 | * 446 | * @param int $workflowId 447 | * @return void 448 | */ 449 | public function delete($workflowId) 450 | { 451 | $this->conn->beginTransaction(); 452 | try { 453 | // delete only those two, the rest should be deleted automatically through cascading foreign keys 454 | $this->conn->delete($this->options->variableHandlerTable(), array('workflow_id' => $workflowId)); 455 | $this->conn->delete($this->options->workflowTable(), array('workflow_id' => $workflowId)); 456 | 457 | $this->conn->commit(); 458 | } catch(\Exception $e) { 459 | $this->conn->rollback(); 460 | } 461 | } 462 | } 463 | --------------------------------------------------------------------------------