blob: c5bbbecf671cbe3bd3af6fd12d480a50c7e1c9f6 [file] [log] [blame]
<?php
/**
* File containing the ezcWorkflowDefinitionStorageXml 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
*/
/**
* XML workflow definition storage handler.
*
* The definitions are stored inside the directory specified to the constructor with the name:
* [workflowName]_[workflowVersion].xml where the name of the workflow has dots and spaces
* replaced by '_'.
*
* @todo DTD for the XML file.
* @package Workflow
* @version //autogen//
*/
class ezcWorkflowDefinitionStorageXml implements ezcWorkflowDefinitionStorage
{
/**
* The directory that holds the XML files.
*
* @var string
*/
protected $directory;
/**
* Constructs a new definition loader that loads definitions from $directory.
*
* $directory must contain the trailing '/'
*
* @param string $directory The directory that holds the XML files.
*/
public function __construct( $directory = '' )
{
$this->directory = $directory;
}
/**
* Load a workflow definition from a file.
*
* When the $workflowVersion argument is omitted,
* the most recent version is loaded.
*
* @param string $workflowName
* @param int $workflowVersion
* @return ezcWorkflow
* @throws ezcWorkflowDefinitionStorageException
*/
public function loadByName( $workflowName, $workflowVersion = 0 )
{
if ( $workflowVersion == 0 )
{
// Load the latest version of the workflow definition by default.
$workflowVersion = $this->getCurrentVersion( $workflowName );
}
$filename = $this->getFilename( $workflowName, $workflowVersion );
// Load the document.
$document = new DOMDocument;
if ( is_readable( $filename ) )
{
libxml_use_internal_errors( true );
$loaded = @$document->load( $filename );
if ( $loaded === false )
{
$message = '';
foreach ( libxml_get_errors() as $error )
{
$message .= $error->message;
}
throw new ezcWorkflowDefinitionStorageException(
sprintf(
'Could not load workflow "%s" (version %d) from "%s".%s',
$workflowName,
$workflowVersion,
$filename,
$message != '' ? "\n" . $message : ''
)
);
}
}
else
{
throw new ezcWorkflowDefinitionStorageException(
sprintf(
'Could not read file "%s".',
$filename
)
);
}
return $this->loadFromDocument( $document );
}
/**
* Load a workflow definition from a DOMDocument.
*
* @param DOMDocument $document
* @return ezcWorkflow
*/
public function loadFromDocument( DOMDocument $document )
{
$workflowName = $document->documentElement->getAttribute( 'name' );
$workflowVersion = (int) $document->documentElement->getAttribute( 'version' );
// Create node objects.
$nodes = array();
$xmlNodes = $document->getElementsByTagName( 'node' );
foreach ( $xmlNodes as $xmlNode )
{
$id = (int)$xmlNode->getAttribute( 'id' );
$className = 'ezcWorkflowNode' . $xmlNode->getAttribute( 'type' );
if ( class_exists( $className ) )
{
$configuration = call_user_func_array(
array( $className, 'configurationFromXML' ), array( $xmlNode )
);
if ( is_null( $configuration ) )
{
$configuration = ezcWorkflowUtil::getDefaultConfiguration( $className );
}
}
$node = new $className( $configuration );
$node->setId( $id );
if ( $node instanceof ezcWorkflowNodeFinally &&
!isset( $finallyNode ) )
{
$finallyNode = $node;
}
else if ( $node instanceof ezcWorkflowNodeEnd &&
!isset( $defaultEndNode ) )
{
$defaultEndNode = $node;
}
else if ( $node instanceof ezcWorkflowNodeStart )
{
$startNode = $node;
}
$nodes[$id] = $node;
}
if ( !isset( $startNode ) || !isset( $defaultEndNode ) )
{
throw new ezcWorkflowDefinitionStorageException(
'Could not load workflow definition.'
);
}
// Connect node objects.
foreach ( $xmlNodes as $xmlNode )
{
$id = (int)$xmlNode->getAttribute( 'id' );
$className = 'ezcWorkflowNode' . $xmlNode->getAttribute( 'type' );
foreach ( $xmlNode->getElementsByTagName( 'outNode' ) as $outNode )
{
$nodes[$id]->addOutNode( $nodes[(int)$outNode->getAttribute( 'id' )] );
}
if ( is_subclass_of( $className, 'ezcWorkflowNodeConditionalBranch' ) )
{
foreach ( ezcWorkflowUtil::getChildNodes( $xmlNode ) as $childNode )
{
if ( $childNode->tagName == 'condition' )
{
foreach ( $childNode->getElementsByTagName( 'else' ) as $elseNode )
{
foreach ( $elseNode->getElementsByTagName( 'outNode' ) as $outNode )
{
$elseId = (int)$outNode->getAttribute( 'id' );
}
}
$condition = self::xmlToCondition( $childNode );
$xpath = new DOMXPath( $childNode->ownerDocument );
foreach ( $xpath->query( 'outNode', $childNode ) as $outNode )
{
if ( !isset( $elseId ) )
{
$nodes[$id]->addConditionalOutNode(
$condition,
$nodes[(int)$outNode->getAttribute( 'id' )]
);
}
else
{
$nodes[$id]->addConditionalOutNode(
$condition,
$nodes[(int)$outNode->getAttribute( 'id' )],
$nodes[$elseId]
);
unset( $elseId );
}
}
}
}
}
}
if ( !isset( $finallyNode ) ||
count( $finallyNode->getInNodes() ) > 0 )
{
$finallyNode = null;
}
// Create workflow object and add the node objects to it.
$workflow = new ezcWorkflow( $workflowName, $startNode, $defaultEndNode, $finallyNode );
$workflow->definitionStorage = $this;
$workflow->version = $workflowVersion;
// Handle the variable handlers.
foreach ( $document->getElementsByTagName( 'variableHandler' ) as $variableHandler )
{
$workflow->addVariableHandler(
$variableHandler->getAttribute( 'variable' ),
$variableHandler->getAttribute( 'class' )
);
}
// Verify the loaded workflow.
$workflow->verify();
return $workflow;
}
/**
* Save a workflow definition to a file.
*
* @param ezcWorkflow $workflow
* @throws ezcWorkflowDefinitionStorageException
*/
public function save( ezcWorkflow $workflow )
{
$workflowVersion = $this->getCurrentVersion( $workflow->name ) + 1;
$filename = $this->getFilename( $workflow->name, $workflowVersion );
$document = $this->saveToDocument( $workflow, $workflowVersion );
file_put_contents( $filename, $document->saveXML() );
}
/**
* Save a workflow definition to a DOMDocument.
*
* @param ezcWorkflow $workflow
* @param int $workflowVersion
* @return DOMDocument
*/
public function saveToDocument( ezcWorkflow $workflow, $workflowVersion )
{
$document = new DOMDocument( '1.0', 'UTF-8' );
$document->formatOutput = true;
$root = $document->createElement( 'workflow' );
$document->appendChild( $root );
$root->setAttribute( 'name', $workflow->name );
$root->setAttribute( 'version', $workflowVersion );
$nodes = $workflow->nodes;
$numNodes = count( $nodes );
// Workaround for foreach() bug in PHP 5.2.1.
// http://bugs.php.net/bug.php?id=40608
$keys = array_keys( $nodes );
for ( $i = 0; $i < $numNodes; $i++ )
{
$id = $keys[$i];
$node = $nodes[$id];
$nodeClass = get_class( $node );
$xmlNode = $document->createElement( 'node' );
$xmlNode->setAttribute( 'id', $id );
$xmlNode->setAttribute(
'type',
str_replace( 'ezcWorkflowNode', '', get_class( $node ) )
);
$node->configurationtoXML( $xmlNode );
$root->appendChild( $xmlNode );
$outNodes = $node->getOutNodes();
$_keys = array_keys( $outNodes );
$numOutNodes = count( $_keys );
for ( $j = 0; $j < $numOutNodes; $j++ )
{
foreach ( $nodes as $outNodeId => $_node )
{
if ( $_node === $outNodes[$_keys[$j]] )
{
break;
}
}
$xmlOutNode = $document->createElement( 'outNode' );
$xmlOutNode->setAttribute( 'id', $outNodeId );
if ( is_subclass_of( $nodeClass, 'ezcWorkflowNodeConditionalBranch' ) &&
$condition = $node->getCondition( $outNodes[$_keys[$j]] ) )
{
if ( !$node->isElse( $outNodes[$_keys[$j]] ) )
{
$xmlCondition = self::conditionToXml(
$condition,
$document
);
$xmlCondition->appendChild( $xmlOutNode );
$xmlNode->appendChild( $xmlCondition );
}
else
{
$xmlElse = $xmlCondition->appendChild( $document->createElement( 'else' ) );
$xmlElse->appendChild( $xmlOutNode );
}
}
else
{
$xmlNode->appendChild( $xmlOutNode );
}
}
}
foreach ( $workflow->getVariableHandlers() as $variable => $class )
{
$variableHandler = $root->appendChild(
$document->createElement( 'variableHandler' )
);
$variableHandler->setAttribute( 'variable', $variable );
$variableHandler->setAttribute( 'class', $class );
}
return $document;
}
/**
* "Convert" an ezcWorkflowCondition object into an DOMElement object.
*
* @param ezcWorkflowCondition $condition
* @param DOMDocument $document
* @return DOMElement
*/
public static function conditionToXml( ezcWorkflowCondition $condition, DOMDocument $document )
{
$xmlCondition = $document->createElement( 'condition' );
$conditionClass = get_class( $condition );
$conditionType = str_replace( 'ezcWorkflowCondition', '', $conditionClass );
$xmlCondition->setAttribute( 'type', $conditionType );
switch ( $conditionClass )
{
case 'ezcWorkflowConditionVariable': {
$xmlCondition->setAttribute( 'name', $condition->getVariableName() );
$xmlCondition->appendChild(
self::conditionToXml( $condition->getCondition(), $document )
);
}
break;
case 'ezcWorkflowConditionVariables': {
list( $variableNameA, $variableNameB ) = $condition->getVariableNames();
$xmlCondition->setAttribute( 'a', $variableNameA );
$xmlCondition->setAttribute( 'b', $variableNameB );
$xmlCondition->appendChild(
self::conditionToXml( $condition->getCondition(), $document )
);
}
break;
case 'ezcWorkflowConditionAnd':
case 'ezcWorkflowConditionOr':
case 'ezcWorkflowConditionXor': {
foreach ( $condition->getConditions() as $childCondition )
{
$xmlCondition->appendChild(
self::conditionToXml( $childCondition, $document )
);
}
}
break;
case 'ezcWorkflowConditionNot': {
$xmlCondition->appendChild(
self::conditionToXml( $condition->getCondition(), $document )
);
}
break;
case 'ezcWorkflowConditionIsEqual':
case 'ezcWorkflowConditionIsEqualOrGreaterThan':
case 'ezcWorkflowConditionIsEqualOrLessThan':
case 'ezcWorkflowConditionIsGreaterThan':
case 'ezcWorkflowConditionIsLessThan':
case 'ezcWorkflowConditionIsNotEqual': {
$xmlCondition->setAttribute( 'value', $condition->getValue() );
}
break;
case 'ezcWorkflowConditionInArray': {
$xmlCondition->appendChild(
self::variableToXml( $condition->getValue(), $document )
);
}
break;
}
return $xmlCondition;
}
/**
* "Convert" an DOMElement object into an ezcWorkflowCondition object.
*
* @param DOMElement $element
* @return ezcWorkflowCondition
*/
public static function xmlToCondition( DOMElement $element )
{
$class = 'ezcWorkflowCondition' . $element->getAttribute( 'type' );
switch ( $class )
{
case 'ezcWorkflowConditionVariable': {
return new $class(
$element->getAttribute( 'name' ),
self::xmlToCondition( ezcWorkflowUtil::getChildNode( $element ) )
);
}
break;
case 'ezcWorkflowConditionVariables': {
return new $class(
$element->getAttribute( 'a' ),
$element->getAttribute( 'b' ),
self::xmlToCondition( ezcWorkflowUtil::getChildNode( $element ) )
);
}
break;
case 'ezcWorkflowConditionAnd':
case 'ezcWorkflowConditionOr':
case 'ezcWorkflowConditionXor': {
$conditions = array();
foreach ( ezcWorkflowUtil::getChildNodes( $element ) as $childNode )
{
if ( $childNode->tagName == 'condition' )
{
$conditions[] = self::xmlToCondition( $childNode );
}
}
return new $class( $conditions );
}
break;
case 'ezcWorkflowConditionNot': {
return new $class( self::xmlToCondition( ezcWorkflowUtil::getChildNode( $element ) ) );
}
break;
case 'ezcWorkflowConditionIsEqual':
case 'ezcWorkflowConditionIsEqualOrGreaterThan':
case 'ezcWorkflowConditionIsEqualOrLessThan':
case 'ezcWorkflowConditionIsGreaterThan':
case 'ezcWorkflowConditionIsLessThan':
case 'ezcWorkflowConditionIsNotEqual': {
return new $class( $element->getAttribute( 'value' ) );
}
break;
case 'ezcWorkflowConditionInArray': {
return new $class( self::xmlToVariable( ezcWorkflowUtil::getChildNode( $element ) ) );
}
break;
default: {
return new $class;
}
break;
}
}
/**
* "Convert" a PHP variable into an DOMElement object.
*
* @param mixed $variable
* @param DOMDocument $document
* @return DOMElement
*/
public static function variableToXml( $variable, DOMDocument $document )
{
if ( is_array( $variable ) )
{
$xmlResult = $document->createElement( 'array' );
foreach ( $variable as $key => $value )
{
$element = $document->createElement( 'element' );
$element->setAttribute( 'key', $key );
$element->appendChild( self::variableToXml( $value, $document ) );
$xmlResult->appendChild( $element );
}
}
if ( is_object( $variable ) )
{
$xmlResult = $document->createElement( 'object' );
$xmlResult->setAttribute( 'class', get_class( $variable ) );
}
if ( is_null( $variable ) )
{
$xmlResult = $document->createElement( 'null' );
}
if ( is_scalar( $variable ) )
{
$type = gettype( $variable );
if ( is_bool( $variable ) )
{
$variable = $variable === true ? 'true' : 'false';
}
$xmlResult = $document->createElement( $type, $variable );
}
return $xmlResult;
}
/**
* "Convert" an DOMElement object into a PHP variable.
*
* @param DOMElement $element
* @return mixed
*/
public static function xmlToVariable( DOMElement $element )
{
$variable = null;
switch ( $element->tagName )
{
case 'array': {
$variable = array();
foreach ( $element->getElementsByTagName( 'element' ) as $element )
{
$value = self::xmlToVariable( ezcWorkflowUtil::getChildNode( $element ) );
if ( $element->hasAttribute( 'key' ) )
{
$variable[ (string)$element->getAttribute( 'key' ) ] = $value;
}
else
{
$variable[] = $value;
}
}
}
break;
case 'object': {
$className = $element->getAttribute( 'class' );
if ( $element->hasChildNodes() )
{
$arguments = ezcWorkflowUtil::getChildNodes(
ezcWorkflowUtil::getChildNode( $element )
);
$constructorArgs = array();
foreach ( $arguments as $argument )
{
if ( $argument instanceof DOMElement )
{
$constructorArgs[] = self::xmlToVariable( $argument );
}
}
$class = new ReflectionClass( $className );
$variable = $class->newInstanceArgs( $constructorArgs );
}
else
{
$variable = new $className;
}
}
break;
case 'boolean': {
$variable = $element->nodeValue == 'true' ? true : false;
}
break;
case 'integer':
case 'double':
case 'string': {
$variable = $element->nodeValue;
settype( $variable, $element->tagName );
}
}
return $variable;
}
/**
* Returns the current version number for a given workflow name.
*
* @param string $workflowName
* @return integer
*/
protected function getCurrentVersion( $workflowName )
{
$workflowName = $this->getFilesystemWorkflowName( $workflowName );
$files = glob( $this->directory . $workflowName . '_*.xml' );
if ( !empty( $files ) )
{
return (int)str_replace(
array(
$this->directory . $workflowName . '_',
'.xml'
),
'',
$files[count( $files ) - 1]
);
}
else
{
return 0;
}
}
/**
* Returns the filename with path for given workflow name and version.
*
* The name of the workflow file is of the format [workFlowName]_[workFlowVersion].xml
*
* @param string $workflowName
* @param int $workflowVersion
* @return string
*/
protected function getFilename( $workflowName, $workflowVersion )
{
return sprintf(
'%s%s_%d.xml',
$this->directory,
$this->getFilesystemWorkflowName( $workflowName ),
$workflowVersion
);
}
/**
* Returns a safe filesystem name for a given workflow.
*
* This method replaces whitespace and '.' with '_'.
*
* @param string $workflowName
* @return string
*/
protected function getFilesystemWorkflowName( $workflowName )
{
return preg_replace( '#[^\w.]#', '_', $workflowName );
}
}
?>