blob: 4d81cfe37bee48f4800f9091a159d2cca7342d98 [file] [log] [blame]
<?php
/**
* File containing the ezcWorkflowExecution class.
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*
* @package Workflow
* @version //autogen//
* @copyright Copyright (C) 2005-2010 eZ Systems AS. All rights reserved.
* @license http://www.apache.org/licenses/LICENSE-2.0 Apache License, Version 2.0
*/
/**
* Abstract base class for workflow execution engines.
*
* ezcWorkflowExecution provides all functionality necessary to execute
* a workflow. However, it does not provide functionality to make the
* execution of a workflow persistent and hence usuable over more than
* one PHP run.
*
* Implementations must implement the do* methods and provide the means
* to store the execution data to a persistent medium.
*
* @property ezcWorkflowDefinitonStorage $definitionStorage
* The definition handler used to fetch subworkflows if needed.
* @property ezcWorkflow $workflow
* The workflow being executed.
*
* @package Workflow
* @version //autogen//
* @mainclass
*/
abstract class ezcWorkflowExecution
{
/**
* Container to hold the properties
*
* @var array(string=>mixed)
*/
protected $properties = array(
'definitionStorage' => null,
'workflow' => null
);
/**
* Execution ID.
*
* @var integer
*/
protected $id;
/**
* Nodes of the workflow being executed that are activated.
*
* @var ezcWorkflowNode[]
*/
protected $activatedNodes = array();
/**
* Number of activated nodes.
*
* @var integer
*/
protected $numActivatedNodes = 0;
/**
* Number of activated end nodes.
*
* @var integer
*/
protected $numActivatedEndNodes = 0;
/**
* Nodes of the workflow that started a new thread of execution.
*
* @var array
*/
protected $threads = array();
/**
* Sequence for thread ids.
*
* @var integer
*/
protected $nextThreadId = 0;
/**
* Flag that indicates whether or not this execution has been cancelled.
*
* @var bool
*/
protected $cancelled;
/**
* Flag that indicates whether or not this execution has ended.
*
* @var bool
*/
protected $ended;
/**
* Flag that indicates whether or not this execution has been resumed.
*
* @var bool
*/
protected $resumed;
/**
* Flag that indicates whether or not this execution has been suspended.
*
* @var bool
*/
protected $suspended;
/**
* Plugins registered for this execution.
*
* @var array
*/
protected $plugins = array();
/**
* Workflow variables.
*
* @var array
*/
protected $variables = array();
/**
* Workflow variables the execution is waiting for.
*
* @var array
*/
protected $waitingFor = array();
/**
* Property read access.
*
* @throws ezcBasePropertyNotFoundException
* If the the desired property is not found.
*
* @param string $propertyName Name of the property.
* @return mixed Value of the property or null.
* @ignore
*/
public function __get( $propertyName )
{
switch ( $propertyName )
{
case 'definitionStorage':
case 'workflow':
return $this->properties[$propertyName];
}
throw new ezcBasePropertyNotFoundException( $propertyName );
}
/**
* Property write access.
*
* @param string $propertyName Name of the property.
* @param mixed $val The value for the property.
*
* @throws ezcBaseValueException
* If a the value for the property definitionStorage is not an
* instance of ezcWorkflowDefinitionStorage.
* @throws ezcBaseValueException
* If a the value for the property workflow is not an instance of
* ezcWorkflow.
* @ignore
*/
public function __set( $propertyName, $val )
{
switch ( $propertyName )
{
case 'definitionStorage':
if ( !( $val instanceof ezcWorkflowDefinitionStorage ) )
{
throw new ezcBaseValueException( $propertyName, $val, 'ezcWorkflowDefinitionStorage' );
}
$this->properties['definitionStorage'] = $val;
return;
case 'workflow':
if ( !( $val instanceof ezcWorkflow ) )
{
throw new ezcBaseValueException( $propertyName, $val, 'ezcWorkflow' );
}
$this->properties['workflow'] = $val;
return;
}
throw new ezcBasePropertyNotFoundException( $propertyName );
}
/**
* Property isset access.
*
* @param string $propertyName Name of the property.
* @return bool True is the property is set, otherwise false.
* @ignore
*/
public function __isset( $propertyName )
{
switch ( $propertyName )
{
case 'definitionStorage':
case 'workflow':
return true;
}
return false;
}
/**
* Starts the execution of the workflow and returns the execution id.
*
* $parentId is used to specify the execution id of the parent workflow
* when executing subworkflows. It should not be used when manually
* starting workflows.
*
* Calls doStart() right before the first node is activated.
*
* @param int $parentId
* @return mixed Execution ID if the workflow has been suspended,
* null otherwise.
* @throws ezcWorkflowExecutionException
* If no workflow has been set up for execution.
*/
public function start( $parentId = null )
{
if ( $this->workflow === null )
{
throw new ezcWorkflowExecutionException(
'No workflow has been set up for execution.'
);
}
$this->cancelled = false;
$this->ended = false;
$this->resumed = false;
$this->suspended = false;
$this->doStart( $parentId );
$this->loadFromVariableHandlers();
foreach ( $this->plugins as $plugin )
{
$plugin->afterExecutionStarted( $this );
}
// Start workflow execution by activating the start node.
$this->workflow->startNode->activate( $this );
// Continue workflow execution until there are no more
// activated nodes.
$this->execute();
// Return execution ID if the workflow has been suspended.
if ( $this->isSuspended() )
{
return (int)$this->id;
}
}
/**
* Suspends workflow execution.
*
* This method is usually called by the execution environment when there are no more
* more activated nodes that can be executed. This is commonly the case with input
* nodes waiting for input.
*
* This method calls doSuspend() before calling saveToVariableHandlers() allowing
* reimplementations to save variable and node information.
*
* @ignore
*/
public function suspend()
{
$this->cancelled = false;
$this->ended = false;
$this->resumed = false;
$this->suspended = true;
$this->saveToVariableHandlers();
$keys = array_keys( $this->variables );
$count = count( $keys );
$handlers = $this->workflow->getVariableHandlers();
for ( $i = 0; $i < $count; $i++ )
{
if ( isset( $handlers[$keys[$i]] ) )
{
unset( $this->variables[$keys[$i]] );
}
}
$this->doSuspend();
foreach ( $this->plugins as $plugin )
{
$plugin->afterExecutionSuspended( $this );
}
}
/**
* Resumes workflow execution of a suspended workflow.
*
* $executionId is the id of the execution to resume. $inputData is an
* associative array of the format array( 'variable name' => value ) that should
* contain new workflow variable data required to resume execution.
*
* Calls do doResume() before the variables are loaded using the variable handlers.
*
* @param array $inputData The new input data.
* @throws ezcWorkflowInvalidInputException if the input given does not match the expected data.
* @throws ezcWorkflowExecutionException if there is no prior ID for this execution.
*/
public function resume( array $inputData = array() )
{
if ( $this->id === null )
{
throw new ezcWorkflowExecutionException(
'No execution id given.'
);
}
$this->cancelled = false;
$this->ended = false;
$this->resumed = true;
$this->suspended = false;
$this->doResume();
$this->loadFromVariableHandlers();
$errors = array();
foreach ( $inputData as $variableName => $value )
{
if ( isset( $this->waitingFor[$variableName] ) )
{
if ( $this->waitingFor[$variableName]['condition']->evaluate( $value ) )
{
$this->setVariable( $variableName, $value );
unset( $this->waitingFor[$variableName] );
}
else
{
$errors[$variableName] = (string)$this->waitingFor[$variableName]['condition'];
}
}
}
if ( !empty( $errors ) )
{
throw new ezcWorkflowInvalidInputException( $errors );
}
foreach ( $this->plugins as $plugin )
{
$plugin->afterExecutionResumed( $this );
}
$this->execute();
// Return execution ID if the workflow has been suspended.
if ( $this->isSuspended() )
{
// @codeCoverageIgnoreStart
return $this->id;
// @codeCoverageIgnoreEnd
}
}
/**
* Cancels workflow execution with the node $endNode.
*
* @param ezcWorkflowNode $node
*/
public function cancel( ezcWorkflowNode $node = null )
{
if ( $node !== null )
{
foreach ( $this->plugins as $plugin )
{
$plugin->afterNodeExecuted( $this, $node );
}
}
$this->activatedNodes = array();
$this->numActivatedNodes = 0;
$this->waitingFor = array();
if ( count( $this->workflow->finallyNode->getOutNodes() ) > 0 )
{
$this->workflow->finallyNode->activate( $this );
$this->execute();
}
$this->cancelled = true;
$this->ended = false;
$this->end( $node );
$this->doEnd();
}
/**
* Ends workflow execution with the node $endNode.
*
* End nodes must call this method to end the execution.
*
* @param ezcWorkflowNode $node
* @ignore
*/
public function end( ezcWorkflowNode $node = null )
{
if ( !$this->cancelled )
{
if ( $node !== null )
{
foreach ( $this->plugins as $plugin )
{
$plugin->afterNodeExecuted( $this, $node );
}
}
$this->ended = true;
$this->resumed = false;
$this->suspended = false;
$this->doEnd();
$this->saveToVariableHandlers();
if ( $node !== null )
{
$this->endThread( $node->getThreadId() );
foreach ( $this->plugins as $plugin )
{
$plugin->afterExecutionEnded( $this );
}
}
}
else
{
foreach ( $this->plugins as $plugin )
{
$plugin->afterExecutionCancelled( $this );
}
}
}
/**
* The workflow engine's main execution loop. It is started by start() and
* resume().
*
* @ignore
*/
protected function execute()
{
// Try to execute nodes while there are executable nodes on the stack.
do
{
// Flag that indicates whether a node has been executed during the
// current iteration of the loop.
$executed = false;
// Iterate the stack of activated nodes.
foreach ( $this->activatedNodes as $key => $node )
{
// Only try to execute a node if the execution of the
// workflow instance has not ended yet.
if ( $this->cancelled && $this->ended )
{
// @codeCoverageIgnoreStart
break;
// @codeCoverageIgnoreEnd
}
// The current node is an end node but there are still
// activated nodes on the stack.
if ( $node instanceof ezcWorkflowNodeEnd &&
!$node instanceof ezcWorkflowNodeCancel &&
$this->numActivatedNodes != $this->numActivatedEndNodes )
{
continue;
}
// Execute the current node and check whether it finished
// executing.
if ( $node->execute( $this ) )
{
// Remove current node from the stack of activated
// nodes.
unset( $this->activatedNodes[$key] );
$this->numActivatedNodes--;
// Notify plugins that the node has been executed.
if ( !$this->cancelled && !$this->ended )
{
foreach ( $this->plugins as $plugin )
{
$plugin->afterNodeExecuted( $this, $node );
}
}
// Toggle flag (see above).
$executed = true;
}
}
}
while ( !empty( $this->activatedNodes ) && $executed );
// The stack of activated nodes is not empty but at the moment none of
// its nodes can be executed.
if ( !$this->cancelled && !$this->ended )
{
$this->suspend();
}
}
/**
* Activates a node and returns true if it was activated, false if not.
*
* The node will only be activated if the node is executable.
* See {@link ezcWorkflowNode::isExecutable()}.
*
* @param ezcWorkflowNode $node
* @param bool $notifyPlugins
* @return bool
* @ignore
*/
public function activate( ezcWorkflowNode $node, $notifyPlugins = true )
{
// Only activate the node when
// - the execution of the workflow has not been cancelled,
// - the node is ready to be activated,
// - and the node is not already activated.
if ( $this->cancelled ||
!$node->isExecutable() ||
ezcWorkflowUtil::findObject( $this->activatedNodes, $node ) !== false )
{
return false;
}
$activateNode = true;
foreach ( $this->plugins as $plugin )
{
$activateNode = $plugin->beforeNodeActivated( $this, $node );
if ( !$activateNode )
{
// @codeCoverageIgnoreStart
break;
// @codeCoverageIgnoreEnd
}
}
if ( $activateNode )
{
// Add node to list of activated nodes.
$this->activatedNodes[] = $node;
$this->numActivatedNodes++;
if ( $node instanceof ezcWorkflowNodeEnd )
{
$this->numActivatedEndNodes++;
}
if ( $notifyPlugins )
{
foreach ( $this->plugins as $plugin )
{
$plugin->afterNodeActivated( $this, $node );
}
}
return true;
}
else
{
// @codeCoverageIgnoreStart
return false;
// @codeCoverageIgnoreEnd
}
}
/**
* Adds a variable that an (input) node is waiting for.
*
* @param ezcWorkflowNode $node
* @param string $variableName
* @param ezcWorkflowCondition $condition
* @ignore
*/
public function addWaitingFor( ezcWorkflowNode $node, $variableName, ezcWorkflowCondition $condition )
{
if ( !isset( $this->waitingFor[$variableName] ) )
{
$this->waitingFor[$variableName] = array(
'node' => $node->getId(),
'condition' => $condition
);
}
}
/**
* Returns the variables that (input) nodes are waiting for.
*
* @return array
* @ignore
*/
public function getWaitingFor()
{
return $this->waitingFor;
}
/**
* Start a new thread and returns the id of the new thread.
*
* @param int $parentId The id of the parent thread.
* @param int $numSiblings The number of threads that are started by the same node.
* @return int
* @ignore
*/
public function startThread( $parentId = null, $numSiblings = 1 )
{
if ( !$this->cancelled )
{
$this->threads[$this->nextThreadId] = array(
'parentId' => $parentId,
'numSiblings' => $numSiblings
);
foreach ( $this->plugins as $plugin )
{
$plugin->afterThreadStarted( $this, $this->nextThreadId, $parentId, $numSiblings );
}
return $this->nextThreadId++;
}
return false;
}
/**
* Ends the thread with id $threadId
*
* @param integer $threadId
* @ignore
*/
public function endThread( $threadId )
{
if ( isset( $this->threads[$threadId] ) )
{
unset( $this->threads[$threadId] );
foreach ( $this->plugins as $plugin )
{
$plugin->afterThreadEnded( $this, $threadId );
}
}
else
{
throw new ezcWorkflowExecutionException(
sprintf(
'There is no thread with id #%d.',
$threadId
)
);
}
}
/**
* Returns a new execution object for a sub workflow.
*
* If this method is used to resume a subworkflow you must provide
* the execution id through $id.
*
* If $interactive is false an ezcWorkflowExecutionNonInteractive
* will be returned.
*
* This method can be used by nodes implementing sub-workflows
* to get a new execution environment for the subworkflow.
*
* @param int $id
* @param bool $interactive
* @return ezcWorkflowExecution
* @ignore
*/
public function getSubExecution( $id = null, $interactive = true )
{
if ( $interactive )
{
$execution = $this->doGetSubExecution( $id );
}
else
{
$execution = new ezcWorkflowExecutionNonInteractive;
}
foreach ( $this->plugins as $plugin )
{
$execution->addPlugin( $plugin );
}
return $execution;
}
/**
* Returns the number of siblings for a given thread.
*
* @param int $threadId The id of the thread for which to return the number of siblings.
* @return int
* @ignore
*/
public function getNumSiblingThreads( $threadId )
{
if ( isset( $this->threads[$threadId] ) )
{
return $this->threads[$threadId]['numSiblings'];
}
else
{
return false;
}
}
/**
* Returns the id of the parent thread for a given thread.
*
* @param int $threadId The id of the thread for which to return the parent thread id.
* @return int
* @ignore
*/
public function getParentThreadId( $threadId )
{
if ( isset( $this->threads[$threadId] ) )
{
return $this->threads[$threadId]['parentId'];
}
else
{
return false;
}
}
/**
* Adds a plugin to this execution.
*
* @param ezcWorkflowExecutionPlugin $plugin
* @return bool true when the plugin was added, false otherwise.
*/
public function addPlugin( ezcWorkflowExecutionPlugin $plugin )
{
$pluginClass = get_class( $plugin );
if ( !isset( $this->plugins[$pluginClass] ) )
{
$this->plugins[$pluginClass] = $plugin;
return true;
}
else
{
return false;
}
}
/**
* Removes a plugin from this execution.
*
* @param ezcWorkflowExecutionPlugin $plugin
* @return bool true when the plugin was removed, false otherwise.
*/
public function removePlugin( ezcWorkflowExecutionPlugin $plugin )
{
$pluginClass = get_class( $plugin );
if ( isset( $this->plugins[$pluginClass] ) )
{
unset( $this->plugins[$pluginClass] );
return true;
}
else
{
return false;
}
}
/**
* Adds a listener to this execution.
*
* @param ezcWorkflowExecutionListener $listener
* @return bool true when the listener was added, false otherwise.
*/
public function addListener( ezcWorkflowExecutionListener $listener )
{
if ( !isset( $this->plugins['ezcWorkflowExecutionListenerPlugin'] ) )
{
$this->addPlugin( new ezcWorkflowExecutionListenerPlugin );
}
return $this->plugins['ezcWorkflowExecutionListenerPlugin']->addListener( $listener );
}
/**
* Removes a listener from this execution.
*
* @param ezcWorkflowExecutionListener $listener
* @return bool true when the listener was removed, false otherwise.
*/
public function removeListener( ezcWorkflowExecutionListener $listener )
{
if ( isset( $this->plugins['ezcWorkflowExecutionListenerPlugin'] ) )
{
return $this->plugins['ezcWorkflowExecutionListenerPlugin']->removeListener( $listener );
}
return false;
}
/**
* Returns the activated nodes.
*
* @return array
* @ignore
*/
public function getActivatedNodes()
{
return $this->activatedNodes;
}
/**
* Returns the execution ID.
*
* @return int
* @ignore
*/
public function getId()
{
return $this->id;
}
/**
* Returns a variable.
*
* @param string $variableName
* @ignore
*/
public function getVariable( $variableName )
{
if ( array_key_exists( $variableName, $this->variables ) )
{
return $this->variables[$variableName];
}
else
{
throw new ezcWorkflowExecutionException(
sprintf(
'Variable "%s" does not exist.',
$variableName
)
);
}
}
/**
* Returns the variables.
*
* @return array
* @ignore
*/
public function getVariables()
{
return $this->variables;
}
/**
* Checks whether or not a workflow variable has been set.
*
* @param string $variableName
* @return bool true when the variable exists and false otherwise.
* @ignore
*/
public function hasVariable( $variableName )
{
return array_key_exists( $variableName, $this->variables );
}
/**
* Sets a variable.
*
* @param string $variableName
* @param mixed $value
* @return mixed the value that the variable has been set to
* @ignore
*/
public function setVariable( $variableName, $value )
{
foreach ( $this->plugins as $plugin )
{
$value = $plugin->beforeVariableSet( $this, $variableName, $value );
}
$this->variables[$variableName] = $value;
foreach ( $this->plugins as $plugin )
{
$plugin->afterVariableSet( $this, $variableName, $value );
}
return $value;
}
/**
* Sets the variables.
*
* @param array $variables
* @ignore
*/
public function setVariables( array $variables )
{
$this->variables = array();
foreach ( $variables as $variableName => $value )
{
$this->setVariable( $variableName, $value );
}
}
/**
* Unsets a variable.
*
* @param string $variableName
* @return true, when the variable has been unset, false otherwise
* @ignore
*/
public function unsetVariable( $variableName )
{
$unsetVariable = true;
if ( array_key_exists( $variableName, $this->variables ) )
{
foreach ( $this->plugins as $plugin )
{
$unsetVariable = $plugin->beforeVariableUnset( $this, $variableName );
if ( !$unsetVariable )
{
break;
}
}
if ( $unsetVariable )
{
unset( $this->variables[$variableName] );
foreach ( $this->plugins as $plugin )
{
$plugin->afterVariableUnset( $this, $variableName );
}
}
}
return $unsetVariable;
}
/**
* Returns true when the workflow execution has been cancelled.
*
* @return bool
*/
public function isCancelled()
{
return $this->cancelled;
}
/**
* Returns true when the workflow execution has ended.
*
* @return bool
*/
public function hasEnded()
{
return $this->ended;
}
/**
* Returns true when the workflow execution has been resumed.
*
* @return bool
* @ignore
*/
public function isResumed()
{
return $this->resumed;
}
/**
* Returns true when the workflow execution has been suspended.
*
* @return bool
*/
public function isSuspended()
{
return $this->suspended;
}
/**
* Loads data from variable handlers and
* merge it with the current execution data.
*/
protected function loadFromVariableHandlers()
{
foreach ( $this->workflow->getVariableHandlers() as $variableName => $className )
{
$object = new $className;
$this->setVariable( $variableName, $object->load( $this, $variableName ) );
}
}
/**
* Saves data to execution data handlers.
*/
protected function saveToVariableHandlers()
{
foreach ( $this->workflow->getVariableHandlers() as $variableName => $className )
{
if ( isset( $this->variables[$variableName] ) )
{
$object = new $className;
$object->save( $this, $variableName, $this->variables[$variableName] );
}
}
}
/**
* Called by start() when workflow execution is initiated.
*
* Reimplementations can use this method to store workflow information
* to a persistent medium when execution is started.
*
* @param integer $parentId
*/
abstract protected function doStart( $parentId );
/**
* Called by suspend() when workflow execution is suspended.
*
* Reimplementations can use this method to variable and node information
* to a persistent medium.
*/
abstract protected function doSuspend();
/**
* Called by resume() when workflow execution is resumed.
*
* Reimplementations can use this method to fetch execution
* data if necessary..
*/
abstract protected function doResume();
/**
* Called by end() when workflow execution is ended.
*
* Reimplementations can use this method to remove execution
* data from the persistent medium.
*/
abstract protected function doEnd();
/**
* Returns a new execution object for a sub workflow.
*
* Called by getSubExecution to get a new execution
* environment for the new execution thread.
*
* Reimplementations must return a new execution
* environment similar to themselves.
*
* @param int $id
* @return ezcWorkflowExecution
*/
abstract protected function doGetSubExecution( $id = null );
}
?>