diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/WorkflowDumpCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/WorkflowDumpCommand.php index ddfb987ff1937..40604e3d809db 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/WorkflowDumpCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/WorkflowDumpCommand.php @@ -15,6 +15,7 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Workflow\Dumper\GraphvizDumper; +use Symfony\Component\Workflow\Dumper\StateMachineGraphvizDumper; use Symfony\Component\Workflow\Marking; use Symfony\Component\Workflow\Workflow; @@ -60,13 +61,14 @@ protected function execute(InputInterface $input, OutputInterface $output) $serviceId = $input->getArgument('name'); if ($container->has('workflow.'.$serviceId)) { $workflow = $container->get('workflow.'.$serviceId); + $dumper = new GraphvizDumper(); } elseif ($container->has('state_machine.'.$serviceId)) { $workflow = $container->get('state_machine.'.$serviceId); + $dumper = new StateMachineGraphvizDumper(); } else { throw new \InvalidArgumentException(sprintf('No service found for "workflow.%1$s" nor "state_machine.%1$s".', $serviceId)); } - $dumper = new GraphvizDumper(); $marking = new Marking(); foreach ($input->getArgument('marking') as $place) { diff --git a/src/Symfony/Component/Workflow/Dumper/GraphvizDumper.php b/src/Symfony/Component/Workflow/Dumper/GraphvizDumper.php index 916c4bd460622..3681b6f1391a8 100644 --- a/src/Symfony/Component/Workflow/Dumper/GraphvizDumper.php +++ b/src/Symfony/Component/Workflow/Dumper/GraphvizDumper.php @@ -26,7 +26,7 @@ */ class GraphvizDumper implements DumperInterface { - private static $defaultOptions = array( + protected static $defaultOptions = array( 'graph' => array('ratio' => 'compress', 'rankdir' => 'LR'), 'node' => array('fontsize' => 9, 'fontname' => 'Arial', 'color' => '#333333', 'fillcolor' => 'lightblue', 'fixedsize' => true, 'width' => 1), 'edge' => array('fontsize' => 9, 'fontname' => 'Arial', 'color' => '#333333', 'arrowhead' => 'normal', 'arrowsize' => 0.5), @@ -58,7 +58,10 @@ public function dump(Definition $definition, Marking $marking = null, array $opt .$this->endDot(); } - private function findPlaces(Definition $definition, Marking $marking = null) + /** + * @internal + */ + protected function findPlaces(Definition $definition, Marking $marking = null) { $places = array(); @@ -79,7 +82,10 @@ private function findPlaces(Definition $definition, Marking $marking = null) return $places; } - private function findTransitions(Definition $definition) + /** + * @internal + */ + protected function findTransitions(Definition $definition) { $transitions = array(); @@ -93,37 +99,38 @@ private function findTransitions(Definition $definition) return $transitions; } - private function addPlaces(array $places) + /** + * @internal + */ + protected function addPlaces(array $places) { $code = ''; foreach ($places as $id => $place) { - $code .= sprintf(" place_%s [label=\"%s\", shape=circle%s];\n", - $this->dotize($id), - $id, - $this->addAttributes($place['attributes']) - ); + $code .= sprintf(" place_%s [label=\"%s\", shape=circle%s];\n", $this->dotize($id), $id, $this->addAttributes($place['attributes'])); } return $code; } - private function addTransitions(array $transitions) + /** + * @internal + */ + protected function addTransitions(array $transitions) { $code = ''; foreach ($transitions as $place) { - $code .= sprintf(" transition_%s [label=\"%s\", shape=box%s];\n", - $this->dotize($place['name']), - $place['name'], - $this->addAttributes($place['attributes']) - ); + $code .= sprintf(" transition_%s [label=\"%s\", shape=box%s];\n", $this->dotize($place['name']), $place['name'], $this->addAttributes($place['attributes'])); } return $code; } - private function findEdges(Definition $definition) + /** + * @internal + */ + protected function findEdges(Definition $definition) { $dotEdges = array(); @@ -147,7 +154,10 @@ private function findEdges(Definition $definition) return $dotEdges; } - private function addEdges($edges) + /** + * @internal + */ + protected function addEdges(array $edges) { $code = ''; @@ -163,7 +173,10 @@ private function addEdges($edges) return $code; } - private function startDot(array $options) + /** + * @internal + */ + protected function startDot(array $options) { return sprintf("digraph workflow {\n %s\n node [%s];\n edge [%s];\n\n", $this->addOptions($options['graph']), @@ -172,12 +185,23 @@ private function startDot(array $options) ); } - private function endDot() + /** + * @internal + */ + protected function endDot() { return "}\n"; } - private function addAttributes($attributes) + /** + * @internal + */ + protected function dotize($id) + { + return strtolower(preg_replace('/[^\w]/i', '_', $id)); + } + + private function addAttributes(array $attributes) { $code = array(); @@ -188,7 +212,7 @@ private function addAttributes($attributes) return $code ? ', '.implode(', ', $code) : ''; } - private function addOptions($options) + private function addOptions(array $options) { $code = array(); @@ -198,9 +222,4 @@ private function addOptions($options) return implode(' ', $code); } - - private function dotize($id) - { - return strtolower(preg_replace('/[^\w]/i', '_', $id)); - } } diff --git a/src/Symfony/Component/Workflow/Dumper/StateMachineGraphvizDumper.php b/src/Symfony/Component/Workflow/Dumper/StateMachineGraphvizDumper.php new file mode 100644 index 0000000000000..9f68e1daf72f3 --- /dev/null +++ b/src/Symfony/Component/Workflow/Dumper/StateMachineGraphvizDumper.php @@ -0,0 +1,80 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Workflow\Dumper; + +use Symfony\Component\Workflow\Definition; +use Symfony\Component\Workflow\Marking; + +class StateMachineGraphvizDumper extends GraphvizDumper +{ + /** + * {@inheritdoc} + * + * Dumps the workflow as a graphviz graph. + * + * Available options: + * + * * graph: The default options for the whole graph + * * node: The default options for nodes (places) + * * edge: The default options for edges + */ + public function dump(Definition $definition, Marking $marking = null, array $options = array()) + { + $places = $this->findPlaces($definition, $marking); + $edges = $this->findEdges($definition); + + $options = array_replace_recursive(self::$defaultOptions, $options); + + return $this->startDot($options) + .$this->addPlaces($places) + .$this->addEdges($edges) + .$this->endDot() + ; + } + + /** + * @internal + */ + protected function findEdges(Definition $definition) + { + $edges = array(); + + foreach ($definition->getTransitions() as $transition) { + foreach ($transition->getFroms() as $from) { + foreach ($transition->getTos() as $to) { + $edges[$from][] = array( + 'name' => $transition->getName(), + 'to' => $to, + ); + } + } + } + + return $edges; + } + + /** + * @internal + */ + protected function addEdges(array $edges) + { + $code = ''; + + foreach ($edges as $id => $edges) { + foreach ($edges as $edge) { + $code .= sprintf(" place_%s -> place_%s [label=\"%s\" style=\"%s\"];\n", $this->dotize($id), $this->dotize($edge['to']), $edge['name'], 'solid'); + } + } + + return $code; + } +} diff --git a/src/Symfony/Component/Workflow/Tests/Dumper/GraphvizDumperTest.php b/src/Symfony/Component/Workflow/Tests/Dumper/GraphvizDumperTest.php index 2b90ea8d5f197..01927b209c2ff 100644 --- a/src/Symfony/Component/Workflow/Tests/Dumper/GraphvizDumperTest.php +++ b/src/Symfony/Component/Workflow/Tests/Dumper/GraphvizDumperTest.php @@ -20,7 +20,7 @@ public function setUp() /** * @dataProvider provideWorkflowDefinitionWithoutMarking */ - public function testGraphvizDumperWithoutMarking($definition, $expected) + public function testDumpWithoutMarking($definition, $expected) { $dump = $this->dumper->dump($definition); @@ -30,7 +30,7 @@ public function testGraphvizDumperWithoutMarking($definition, $expected) /** * @dataProvider provideWorkflowDefinitionWithMarking */ - public function testWorkflowWithMarking($definition, $marking, $expected) + public function testDumpWithMarking($definition, $marking, $expected) { $dump = $this->dumper->dump($definition, $marking); @@ -40,9 +40,9 @@ public function testWorkflowWithMarking($definition, $marking, $expected) public function provideWorkflowDefinitionWithMarking() { yield array( - $this->createComplexWorkflow(), + $this->createComplexWorkflowDefinition(), new Marking(array('b' => 1)), - $this->createComplexWorkflowDumpWithMarking(), + $this->createComplexWorkflowDefinitionDumpWithMarking(), ); yield array( @@ -54,11 +54,11 @@ public function provideWorkflowDefinitionWithMarking() public function provideWorkflowDefinitionWithoutMarking() { - yield array($this->createComplexWorkflow(), $this->provideComplexWorkflowDumpWithoutMarking()); + yield array($this->createComplexWorkflowDefinition(), $this->provideComplexWorkflowDumpWithoutMarking()); yield array($this->createSimpleWorkflowDefinition(), $this->provideSimpleWorkflowDumpWithoutMarking()); } - public function createComplexWorkflowDumpWithMarking() + public function createComplexWorkflowDefinitionDumpWithMarking() { return 'digraph workflow { ratio="compress" rankdir="LR" diff --git a/src/Symfony/Component/Workflow/Tests/Dumper/StateMachineGraphvizDumperTest.php b/src/Symfony/Component/Workflow/Tests/Dumper/StateMachineGraphvizDumperTest.php new file mode 100644 index 0000000000000..c9a49b36f71e1 --- /dev/null +++ b/src/Symfony/Component/Workflow/Tests/Dumper/StateMachineGraphvizDumperTest.php @@ -0,0 +1,74 @@ +dumper = new StateMachineGraphvizDumper(); + } + + public function testDumpWithoutMarking() + { + $definition = $this->createComplexStateMachineDefinition(); + + $dump = $this->dumper->dump($definition); + + $expected = <<<'EOGRAPH' +digraph workflow { + ratio="compress" rankdir="LR" + node [fontsize="9" fontname="Arial" color="#333333" fillcolor="lightblue" fixedsize="1" width="1"]; + edge [fontsize="9" fontname="Arial" color="#333333" arrowhead="normal" arrowsize="0.5"]; + + place_a [label="a", shape=circle, style="filled"]; + place_b [label="b", shape=circle]; + place_c [label="c", shape=circle]; + place_d [label="d", shape=circle]; + place_a -> place_b [label="t1" style="solid"]; + place_d -> place_b [label="t1" style="solid"]; + place_b -> place_c [label="t2" style="solid"]; + place_b -> place_d [label="t3" style="solid"]; +} + +EOGRAPH; + + $this->assertEquals($expected, $dump); + } + + public function testDumpWithMarking() + { + $definition = $this->createComplexStateMachineDefinition(); + $marking = new Marking(array('b' => 1)); + + $expected = <<<'EOGRAPH' +digraph workflow { + ratio="compress" rankdir="LR" + node [fontsize="9" fontname="Arial" color="#333333" fillcolor="lightblue" fixedsize="1" width="1"]; + edge [fontsize="9" fontname="Arial" color="#333333" arrowhead="normal" arrowsize="0.5"]; + + place_a [label="a", shape=circle, style="filled"]; + place_b [label="b", shape=circle, color="#FF0000", shape="doublecircle"]; + place_c [label="c", shape=circle]; + place_d [label="d", shape=circle]; + place_a -> place_b [label="t1" style="solid"]; + place_d -> place_b [label="t1" style="solid"]; + place_b -> place_c [label="t2" style="solid"]; + place_b -> place_d [label="t3" style="solid"]; +} + +EOGRAPH; + + $dump = $this->dumper->dump($definition, $marking); + + $this->assertEquals($expected, $dump); + } +} diff --git a/src/Symfony/Component/Workflow/Tests/StateMachineTest.php b/src/Symfony/Component/Workflow/Tests/StateMachineTest.php index 6aa3c60fc1cc6..7dca9a824d7c1 100644 --- a/src/Symfony/Component/Workflow/Tests/StateMachineTest.php +++ b/src/Symfony/Component/Workflow/Tests/StateMachineTest.php @@ -2,21 +2,16 @@ namespace Symfony\Component\Workflow\Tests; -use Symfony\Component\Workflow\Definition; use Symfony\Component\Workflow\Marking; use Symfony\Component\Workflow\StateMachine; -use Symfony\Component\Workflow\Transition; class StateMachineTest extends \PHPUnit_Framework_TestCase { + use WorkflowBuilderTrait; + public function testCan() { - $places = array('a', 'b', 'c', 'd'); - $transitions[] = new Transition('t1', 'a', 'b'); - $transitions[] = new Transition('t1', 'd', 'b'); - $transitions[] = new Transition('t2', 'b', 'c'); - $transitions[] = new Transition('t3', 'b', 'd'); - $definition = new Definition($places, $transitions); + $definition = $this->createComplexStateMachineDefinition(); $net = new StateMachine($definition); $subject = new \stdClass(); @@ -29,47 +24,18 @@ public function testCan() $subject->marking = 'b'; $this->assertFalse($net->can($subject, 't1')); - - // The graph looks like: - // - // +-------------------------------+ - // v | - // +---+ +----+ +----+ +----+ +---+ +----+ - // | a | --> | t1 | --> | b | --> | t3 | --> | d | --> | t1 | - // +---+ +----+ +----+ +----+ +---+ +----+ - // | - // | - // v - // +----+ +----+ - // | t2 | --> | c | - // +----+ +----+ } public function testCanWithMultipleTransition() { - $places = array('a', 'b', 'c'); - $transitions[] = new Transition('t1', 'a', 'b'); - $transitions[] = new Transition('t2', 'a', 'c'); - $definition = new Definition($places, $transitions); + $definition = $this->createComplexStateMachineDefinition(); $net = new StateMachine($definition); $subject = new \stdClass(); - // If you are in place "a" you should be able to apply "t1" and "t2" - $subject->marking = 'a'; - $this->assertTrue($net->can($subject, 't1')); + // If you are in place "b" you should be able to apply "t1" and "t2" + $subject->marking = 'b'; $this->assertTrue($net->can($subject, 't2')); - - // The graph looks like: - // - // +----+ +----+ +---+ - // | a | --> | t1 | --> | b | - // +----+ +----+ +---+ - // | - // | - // v - // +----+ +----+ - // | t2 | --> | c | - // +----+ +----+ + $this->assertTrue($net->can($subject, 't3')); } } diff --git a/src/Symfony/Component/Workflow/Tests/Validator/WorkflowValidatorTest.php b/src/Symfony/Component/Workflow/Tests/Validator/WorkflowValidatorTest.php index 1b5fa67fdb083..30d2551fa1b0f 100644 --- a/src/Symfony/Component/Workflow/Tests/Validator/WorkflowValidatorTest.php +++ b/src/Symfony/Component/Workflow/Tests/Validator/WorkflowValidatorTest.php @@ -15,7 +15,7 @@ class WorkflowValidatorTest extends \PHPUnit_Framework_TestCase */ public function testSinglePlaceWorkflowValidatorAndComplexWorkflow() { - $definition = $this->createComplexWorkflow(); + $definition = $this->createComplexWorkflowDefinition(); (new WorkflowValidator(true))->validate($definition, 'foo'); } diff --git a/src/Symfony/Component/Workflow/Tests/WorkflowBuilderTrait.php b/src/Symfony/Component/Workflow/Tests/WorkflowBuilderTrait.php index d7b8de530445c..5e8db29061295 100644 --- a/src/Symfony/Component/Workflow/Tests/WorkflowBuilderTrait.php +++ b/src/Symfony/Component/Workflow/Tests/WorkflowBuilderTrait.php @@ -7,7 +7,7 @@ trait WorkflowBuilderTrait { - private function createComplexWorkflow() + private function createComplexWorkflowDefinition() { $places = range('a', 'g'); @@ -33,7 +33,7 @@ private function createComplexWorkflow() // +----+ +----+ +----+ +----+ } - public function createSimpleWorkflowDefinition() + private function createSimpleWorkflowDefinition() { $places = range('a', 'c'); @@ -42,5 +42,38 @@ public function createSimpleWorkflowDefinition() $transitions[] = new Transition('t2', 'b', 'c'); return new Definition($places, $transitions); + + // The graph looks like: + // +---+ +----+ +---+ +----+ +---+ + // | a | --> | t1 | --> | b | --> | t2 | --> | c | + // +---+ +----+ +---+ +----+ +---+ + } + + private function createComplexStateMachineDefinition() + { + $places = array('a', 'b', 'c', 'd'); + + $transitions[] = new Transition('t1', 'a', 'b'); + $transitions[] = new Transition('t1', 'd', 'b'); + $transitions[] = new Transition('t2', 'b', 'c'); + $transitions[] = new Transition('t3', 'b', 'd'); + + $definition = new Definition($places, $transitions); + + return $definition; + + // The graph looks like: + // t1 + // +------------------+ + // v | + // +---+ t1 +-----+ t2 +---+ | + // | a | ----> | b | ----> | c | | + // +---+ +-----+ +---+ | + // | | + // | t3 | + // v | + // +-----+ | + // | d | -------------+ + // +-----+ } } diff --git a/src/Symfony/Component/Workflow/Tests/WorkflowTest.php b/src/Symfony/Component/Workflow/Tests/WorkflowTest.php index 0a886752d5992..cb5256f412933 100644 --- a/src/Symfony/Component/Workflow/Tests/WorkflowTest.php +++ b/src/Symfony/Component/Workflow/Tests/WorkflowTest.php @@ -56,7 +56,7 @@ public function testGetMarkingWithImpossiblePlace() public function testGetMarkingWithEmptyInitialMarking() { - $definition = $this->createComplexWorkflow(); + $definition = $this->createComplexWorkflowDefinition(); $subject = new \stdClass(); $subject->marking = null; $workflow = new Workflow($definition, new MultipleStateMarkingStore()); @@ -70,7 +70,7 @@ public function testGetMarkingWithEmptyInitialMarking() public function testGetMarkingWithExistingMarking() { - $definition = $this->createComplexWorkflow(); + $definition = $this->createComplexWorkflowDefinition(); $subject = new \stdClass(); $subject->marking = null; $subject->marking = array('b' => 1, 'c' => 1); @@ -89,7 +89,7 @@ public function testGetMarkingWithExistingMarking() */ public function testCanWithUnexistingTransition() { - $definition = $this->createComplexWorkflow(); + $definition = $this->createComplexWorkflowDefinition(); $subject = new \stdClass(); $subject->marking = null; $workflow = new Workflow($definition, new MultipleStateMarkingStore()); @@ -99,7 +99,7 @@ public function testCanWithUnexistingTransition() public function testCan() { - $definition = $this->createComplexWorkflow(); + $definition = $this->createComplexWorkflowDefinition(); $subject = new \stdClass(); $subject->marking = null; $workflow = new Workflow($definition, new MultipleStateMarkingStore()); @@ -110,7 +110,7 @@ public function testCan() public function testCanWithGuard() { - $definition = $this->createComplexWorkflow(); + $definition = $this->createComplexWorkflowDefinition(); $subject = new \stdClass(); $subject->marking = null; $eventDispatcher = new EventDispatcher(); @@ -128,7 +128,7 @@ public function testCanWithGuard() */ public function testApplyWithImpossibleTransition() { - $definition = $this->createComplexWorkflow(); + $definition = $this->createComplexWorkflowDefinition(); $subject = new \stdClass(); $subject->marking = null; $workflow = new Workflow($definition, new MultipleStateMarkingStore()); @@ -138,7 +138,7 @@ public function testApplyWithImpossibleTransition() public function testApply() { - $definition = $this->createComplexWorkflow(); + $definition = $this->createComplexWorkflowDefinition(); $subject = new \stdClass(); $subject->marking = null; $workflow = new Workflow($definition, new MultipleStateMarkingStore()); @@ -153,7 +153,7 @@ public function testApply() public function testApplyWithEventDispatcher() { - $definition = $this->createComplexWorkflow(); + $definition = $this->createComplexWorkflowDefinition(); $subject = new \stdClass(); $subject->marking = null; $eventDispatcher = new EventDispatcherMock(); @@ -187,7 +187,7 @@ public function testApplyWithEventDispatcher() public function testGetEnabledTransitions() { - $definition = $this->createComplexWorkflow(); + $definition = $this->createComplexWorkflowDefinition(); $subject = new \stdClass(); $subject->marking = null; $eventDispatcher = new EventDispatcher();