diff --git a/.scrutinizer.yml b/.scrutinizer.yml
index 03a2d3f..aee36ca 100644
--- a/.scrutinizer.yml
+++ b/.scrutinizer.yml
@@ -2,6 +2,18 @@
# need to install vendors, for example via composer.
before_commands:
- composer install --dev
+checks:
+ php:
+ code_rating: true
+ duplication: true
+build:
+ tests:
+ override:
+ -
+ command: 'phpunit --coverage-clover=some-file'
+ coverage:
+ file: 'some-file'
+ format: 'php-clover'
tools:
php_cs_fixer:
extensions:
diff --git a/App/Command/RenderWorkflowSpecificationFromWorkflowServiceCommand.php b/App/Command/RenderWorkflowSpecificationFromWorkflowServiceCommand.php
new file mode 100644
index 0000000..5834a28
--- /dev/null
+++ b/App/Command/RenderWorkflowSpecificationFromWorkflowServiceCommand.php
@@ -0,0 +1,43 @@
+
+ * @see Command Design Pattern
+ * SpecificationGeneration Bounded Context available behaviour
+ */
+class RenderWorkflowSpecificationFromWorkflowServiceCommand
+{
+ /** @var string */
+ private $workFlowServiceId;
+
+ /** @var string */
+ private $outputFileName;
+
+ /**
+ * @param string $workFlowServiceId
+ * @param string $outputFileName
+ */
+ public function __construct($workFlowServiceId, $outputFileName)
+ {
+ $this->workFlowServiceId = $workFlowServiceId;
+ $this->outputFileName = $outputFileName;
+ }
+
+ /**
+ * @return string
+ */
+ public function getWorkFlowServiceId()
+ {
+ return $this->workFlowServiceId;
+ }
+
+ /**
+ * @return string
+ */
+ public function getOutputFileName()
+ {
+ return $this->outputFileName;
+ }
+}
diff --git a/App/Resources/config/services.xml b/App/Resources/config/services.xml
new file mode 100644
index 0000000..c7f132c
--- /dev/null
+++ b/App/Resources/config/services.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/App/SpecificationService.php b/App/SpecificationService.php
new file mode 100644
index 0000000..f854c86
--- /dev/null
+++ b/App/SpecificationService.php
@@ -0,0 +1,78 @@
+
+ * SpecificationGeneration Bounded Context entry point
+ */
+class SpecificationService
+{
+ /** @var WorkflowContainer */
+ private $workflowContainer;
+
+ /** @var SpecificationRepresentationGeneratorInterface */
+ private $specificationRepresentationGenerator;
+
+ /** @var SpecificationWriterInterface */
+ private $specificationWriter;
+
+ /**
+ * @param WorkflowContainer $workflowContainer
+ * @param SpecificationRepresentationGeneratorInterface $specificationRepresentationGenerator
+ * @param SpecificationWriterInterface $specificationWriter
+ */
+ public function __construct(WorkflowContainer $workflowContainer, SpecificationRepresentationGeneratorInterface $specificationRepresentationGenerator, SpecificationWriterInterface $specificationWriter)
+ {
+ $this->workflowContainer = $workflowContainer;
+ $this->specificationRepresentationGenerator = $specificationRepresentationGenerator;
+ $this->specificationWriter = $specificationWriter;
+ }
+
+ /**
+ * Render specification for the given StateWorkflow
+ * @api
+ * @param RenderWorkflowSpecificationFromWorkflowServiceCommand $command
+ *
+ * @throws WorkflowServiceNotFoundException
+ */
+ public function renderSpecification(RenderWorkflowSpecificationFromWorkflowServiceCommand $command)
+ {
+ $stateWorkflow = $this->workflowContainer->get(
+ $command->getWorkFlowServiceId()
+ );
+
+ $htmlSpecificationRepresentation = $this->specificationRepresentationGenerator->createSpecification(
+ $stateWorkflow
+ );
+
+ $this->specificationWriter->write(
+ $htmlSpecificationRepresentation,
+ $command->getOutputFileName()
+ );
+ }
+
+ /**
+ * Get all available StateWorkflow service
+ * @api
+ *
+ * @return string[]
+ */
+ public function getAvailableWorkflowIds()
+ {
+ $availableWorkflows = $this->workflowContainer->all();
+
+ $availableWorkflowsIds = array();
+ foreach ($availableWorkflows as $availableWorkflow) {
+ $availableWorkflowsIds[] = $availableWorkflow->getServiceId();
+ }
+
+ return $availableWorkflowsIds;
+ }
+}
diff --git a/DependencyInjection/RegisterStateWorkflowCompilerPass.php b/DependencyInjection/RegisterStateWorkflowCompilerPass.php
new file mode 100644
index 0000000..7619b25
--- /dev/null
+++ b/DependencyInjection/RegisterStateWorkflowCompilerPass.php
@@ -0,0 +1,33 @@
+
+ */
+class RegisterStateWorkflowCompilerPass implements CompilerPassInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function process(ContainerBuilder $container)
+ {
+ if (false === $container->hasDefinition('spec_gen.state_workflow.workflow.container')) {
+ throw new InvalidConfigurationException('Cant find "spec_gen.state_workflow.workflow.container" service');
+ }
+
+ $definition = $container->getDefinition('spec_gen.state_workflow.workflow.container');
+
+ $services = $container->findTaggedServiceIds('gmorel.state_workflow_bundle.workflow');
+
+ foreach ($services as $id => $attributes) {
+ $definition->addMethodCall('addWorkflow', array(new Reference($id)));
+ }
+ }
+}
diff --git a/DependencyInjection/SpecGenStateWorkflowSpecGenBundleExtension.php b/DependencyInjection/SpecGenStateWorkflowSpecGenBundleExtension.php
new file mode 100644
index 0000000..13b4cb2
--- /dev/null
+++ b/DependencyInjection/SpecGenStateWorkflowSpecGenBundleExtension.php
@@ -0,0 +1,31 @@
+
+ */
+class SpecGenStateWorkflowSpecGenBundleExtension extends Extension
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function load(array $configs, ContainerBuilder $container)
+ {
+ $loader = new XmlFileLoader($container, new FileLocator(__DIR__ . '/../App/Resources/config'));
+ $loader->load('services.xml');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getAlias()
+ {
+ return 'spec_gen_state_workflow';
+ }
+}
diff --git a/Domain/Exception/UnableToWriteSpecificationException.php b/Domain/Exception/UnableToWriteSpecificationException.php
new file mode 100644
index 0000000..91b440d
--- /dev/null
+++ b/Domain/Exception/UnableToWriteSpecificationException.php
@@ -0,0 +1,11 @@
+
+ */
+class UnableToWriteSpecificationException extends \Exception
+{
+
+}
diff --git a/Domain/Exception/WorkflowServiceNotFoundException.php b/Domain/Exception/WorkflowServiceNotFoundException.php
new file mode 100644
index 0000000..dde14af
--- /dev/null
+++ b/Domain/Exception/WorkflowServiceNotFoundException.php
@@ -0,0 +1,11 @@
+
+ */
+class WorkflowServiceNotFoundException extends \DomainException
+{
+
+}
diff --git a/Domain/IntrospectedState.php b/Domain/IntrospectedState.php
new file mode 100644
index 0000000..a959a72
--- /dev/null
+++ b/Domain/IntrospectedState.php
@@ -0,0 +1,91 @@
+
+ */
+class IntrospectedState
+{
+ const IS_ROOT = true;
+ const IS_NOT_ROOT = false;
+
+ const IS_LEAF = true;
+ const IS_NOT_LEAF = false;
+
+ /** @var string */
+ private $key;
+
+ /** @var string */
+ private $name;
+
+ /** @var bool */
+ private $isRoot;
+
+ /** @var bool */
+ private $isLeaf;
+
+ /**
+ * @param string $stateKey
+ * @param string $stateName
+ */
+ public function __construct($stateKey, $stateName)
+ {
+ $this->key = $stateKey;
+ $this->name = $stateName;
+ $this->isRoot = self::IS_NOT_ROOT;
+ $this->isLeaf = self::IS_NOT_LEAF;
+ }
+
+ /**
+ * @return string
+ */
+ public function getKey()
+ {
+ return $this->key;
+ }
+
+ /**
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * @return boolean
+ */
+ public function isRoot()
+ {
+ return $this->isRoot;
+ }
+
+ /**
+ * @return boolean
+ */
+ public function isLeaf()
+ {
+ return $this->isLeaf;
+ }
+
+ /**
+ * @return $this
+ */
+ public function setIsRoot()
+ {
+ $this->isRoot = true;
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ public function setIsLeaf()
+ {
+ $this->isLeaf = true;
+
+ return $this;
+ }
+}
diff --git a/Domain/IntrospectedTransition.php b/Domain/IntrospectedTransition.php
new file mode 100644
index 0000000..a6c87ff
--- /dev/null
+++ b/Domain/IntrospectedTransition.php
@@ -0,0 +1,74 @@
+
+ */
+class IntrospectedTransition
+{
+ /** @var string */
+ private $name;
+
+ /** @var IntrospectedState */
+ private $fromIntrospectedState;
+
+ /** @var IntrospectedState */
+ private $toIntrospectedState;
+
+ /**
+ * @param string $transitionName
+ * @param IntrospectedState $fromIntrospectedState
+ * @param IntrospectedState $toIntrospectedState
+ */
+ public function __construct($transitionName, IntrospectedState $fromIntrospectedState, IntrospectedState $toIntrospectedState)
+ {
+ $this->guardAgainstEmptyName($transitionName, $fromIntrospectedState, $toIntrospectedState);
+
+ $this->name = $transitionName;
+ $this->fromIntrospectedState = $fromIntrospectedState;
+ $this->toIntrospectedState = $toIntrospectedState;
+ }
+
+ /**
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * @return IntrospectedState
+ */
+ public function getFromIntrospectedState()
+ {
+ return $this->fromIntrospectedState;
+ }
+
+ /**
+ * @return IntrospectedState
+ */
+ public function getToIntrospectedState()
+ {
+ return $this->toIntrospectedState;
+ }
+
+ /**
+ * @param $name
+ * @param IntrospectedState $fromIntrospectedState
+ * @param IntrospectedState $toIntrospectedState
+ */
+ private function guardAgainstEmptyName($name, IntrospectedState $fromIntrospectedState, IntrospectedState $toIntrospectedState)
+ {
+ if (empty($name)) {
+ throw new \LogicException(
+ sprintf(
+ 'IntrospectedTransition from %s to %s must have a name.',
+ $fromIntrospectedState->getKey(),
+ $toIntrospectedState->getKey()
+ )
+ );
+ }
+ }
+}
diff --git a/Domain/IntrospectedWorkflow.php b/Domain/IntrospectedWorkflow.php
new file mode 100644
index 0000000..bdeb37d
--- /dev/null
+++ b/Domain/IntrospectedWorkflow.php
@@ -0,0 +1,246 @@
+
+ */
+class IntrospectedWorkflow
+{
+ /** @var string */
+ private $workflowName;
+
+ /** @var IntrospectedState[] */
+ private $introspectedStates = array();
+
+ /** @var IntrospectedTransition[] */
+ private $introspectedTransitions = array();
+
+ /**
+ * @param \Gmorel\StateWorkflowBundle\StateEngine\StateWorkflow $stateWorkflow
+ */
+ public function __construct(StateWorkflow $stateWorkflow)
+ {
+ $this->workflowName = $stateWorkflow->getName();
+ $availableStates = $stateWorkflow->getAvailableStates();
+
+ if (empty($availableStates)) {
+ throw new EmptyWorkflowException(
+ sprintf(
+ 'Workflow "%s" has no State defined.',
+ $stateWorkflow->getName()
+ )
+ );
+ }
+
+ $this->createIntrospectedStates($availableStates);
+
+ $this->createIntrospectedTransitions($availableStates);
+ }
+
+ /**
+ * @return string
+ */
+ public function getWorkflowName()
+ {
+ return $this->workflowName;
+ }
+
+ /**
+ * @return IntrospectedState[]
+ */
+ public function getIntrospectedStates()
+ {
+ return $this->introspectedStates;
+ }
+
+ /**
+ * @return IntrospectedTransition[]
+ */
+ public function getIntrospectedTransitions()
+ {
+ return $this->introspectedTransitions;
+ }
+
+ /**
+ * @param string $methodName
+ * @param StateInterface $fromState
+ *
+ * @return IntrospectedTransition
+ * @throws StateNotImplementedException
+ */
+ private function createIntrospectedTransition($methodName, StateInterface $fromState)
+ {
+ $toState = $this->getToState($fromState, $methodName);
+
+ return new IntrospectedTransition(
+ $methodName,
+ $this->introspectedStates[$fromState->getKey()],
+ $this->introspectedStates[$toState->getKey()]
+ );
+ }
+
+ /**
+ * @param StateInterface $state
+ *
+ * @return IntrospectedState
+ */
+ private function createIntrospectedState(StateInterface $state)
+ {
+ return new IntrospectedState(
+ $state->getKey(),
+ $state->getName()
+ );
+ }
+
+ /**
+ * @param StateInterface $state
+ *
+ * @return string[]
+ */
+ private function extractAvailableStateMethodNames(StateInterface $state)
+ {
+ $methodNames = array();
+ $reflection = new \ReflectionClass(get_class($state));
+
+ $publicMethods = $reflection->getMethods(\ReflectionMethod::IS_PUBLIC);
+
+ foreach ($publicMethods as $method) {
+ if ($this->isTransitionMethod($method->getName())
+ && $method->getDeclaringClass()->getName() === $reflection->getName()) {
+ $methodNames[] = $method->getName();
+ }
+ }
+
+ return $methodNames;
+ }
+
+ /**
+ * @param StateInterface[] $availableStates
+ */
+ private function createIntrospectedStates(array $availableStates)
+ {
+ foreach ($availableStates as $availableState) {
+ $this->introspectedStates[$availableState->getKey()] = $this->createIntrospectedState($availableState);
+ }
+ }
+
+ /**
+ * @param StateInterface[] $availableStates
+ */
+ private function createIntrospectedTransitions(array $availableStates)
+ {
+ $methodNames = $this->extractDistinctAvailableStateMethodNames($availableStates);
+
+ foreach ($availableStates as $availableToState) {
+ foreach ($methodNames as $methodName) {
+ try {
+ $transitionName = $methodName . '_from_' . $availableToState->getKey();
+ $this->introspectedTransitions[$transitionName] = $this->createIntrospectedTransition(
+ $methodName, $availableToState
+ );
+ } catch (UnsupportedStateTransitionException $e) {
+ // Do nothing
+ }
+ }
+ }
+
+ $this->guessIsIntrospectedStateRootOrLeaf();
+ }
+
+ /**
+ * @param StateInterface $fromState
+ * @param string $methodName
+ *
+ * @return StateInterface
+ * @throws StateNotImplementedException
+ */
+ private function getToState(StateInterface $fromState, $methodName)
+ {
+ if (!method_exists($fromState, $methodName)) {
+ throw new UnsupportedStateTransitionException(
+ sprintf('State %s has no method %s.', $fromState->getKey(), $methodName)
+ );
+ }
+
+ return call_user_func(array($fromState, $methodName), new StubHasState());
+ }
+
+ /**
+ * @param string $methodName
+ *
+ * @return bool
+ */
+ private function isTransitionMethod($methodName)
+ {
+ return !in_array($methodName, array('getKey', 'getName', 'setWorkflow', 'initialize'));
+ }
+
+ /**
+ * Update introspectedState isLeaf|isRoot on the fly
+ */
+ private function guessIsIntrospectedStateRootOrLeaf()
+ {
+ foreach ($this->introspectedStates as $introspectedState) {
+ $this->guessIsIntrospectedStateRoot($introspectedState);
+ $this->guessIsIntrospectedStateLeaf($introspectedState);
+ }
+ }
+
+ /**
+ * @param IntrospectedState $introspectedState
+ */
+ private function guessIsIntrospectedStateRoot(IntrospectedState $introspectedState)
+ {
+ $isRoot = true;
+ foreach ($this->introspectedTransitions as $introspectedTransition) {
+ if ($introspectedTransition->getToIntrospectedState()->getKey() === $introspectedState->getKey()) {
+ $isRoot = false;
+ }
+ }
+
+ if ($isRoot) {
+ $introspectedState->setIsRoot();
+ }
+ }
+
+ /**
+ * @param IntrospectedState $introspectedState
+ */
+ private function guessIsIntrospectedStateLeaf(IntrospectedState $introspectedState)
+ {
+ $isLeaf = true;
+ foreach ($this->introspectedTransitions as $introspectedTransition) {
+ if ($introspectedTransition->getFromIntrospectedState()->getKey() === $introspectedState->getKey()) {
+ $isLeaf = false;
+ }
+ }
+
+ if ($isLeaf) {
+ $introspectedState->setIsLeaf();
+ }
+ }
+
+ /**
+ * @param StateInterface[] $availableStates
+ *
+ * @return string[]
+ */
+ private function extractDistinctAvailableStateMethodNames(array $availableStates)
+ {
+ $methodNames = array();
+ foreach ($availableStates as $availableToState) {
+ $methodNames = array_unique(array_merge(
+ $methodNames, $this->extractAvailableStateMethodNames($availableToState)
+ ));
+ }
+
+ return $methodNames;
+ }
+}
diff --git a/Domain/Representation/SpecificationRepresentationInterface.php b/Domain/Representation/SpecificationRepresentationInterface.php
new file mode 100644
index 0000000..eee667f
--- /dev/null
+++ b/Domain/Representation/SpecificationRepresentationInterface.php
@@ -0,0 +1,16 @@
+
+ */
+interface SpecificationRepresentationInterface
+{
+ /**
+ * Render human readable workflow specification page
+ *
+ * @return string
+ */
+ public function render();
+}
diff --git a/Domain/Representation/WorkflowRepresentationInterface.php b/Domain/Representation/WorkflowRepresentationInterface.php
new file mode 100644
index 0000000..4c7b105
--- /dev/null
+++ b/Domain/Representation/WorkflowRepresentationInterface.php
@@ -0,0 +1,23 @@
+
+ */
+interface WorkflowRepresentationInterface
+{
+ /**
+ * Get human readable workflow name
+ *
+ * @return string
+ */
+ public function getWorkflowName();
+
+ /**
+ * Serialize Workflow into a ready to be processed data (JSON/XML/..)
+ *
+ * @return string
+ */
+ public function serialize();
+}
diff --git a/Domain/SpecificationRepresentationGeneratorInterface.php b/Domain/SpecificationRepresentationGeneratorInterface.php
new file mode 100644
index 0000000..e94750c
--- /dev/null
+++ b/Domain/SpecificationRepresentationGeneratorInterface.php
@@ -0,0 +1,18 @@
+
+ */
+interface SpecificationRepresentationGeneratorInterface
+{
+ /**
+ * @param StateWorkflow $stateWorkflow
+ * @return HtmlSpecificationRepresentation
+ */
+ public function createSpecification(StateWorkflow $stateWorkflow);
+}
diff --git a/Domain/SpecificationWriterInterface.php b/Domain/SpecificationWriterInterface.php
new file mode 100644
index 0000000..675bdec
--- /dev/null
+++ b/Domain/SpecificationWriterInterface.php
@@ -0,0 +1,21 @@
+
+ */
+interface SpecificationWriterInterface
+{
+ /**
+ * Write specification on a target
+ * @param SpecificationRepresentationInterface $specificationRepresentation
+ * @param string $target
+ *
+ * @throws UnableToWriteSpecificationException
+ */
+ public function write(SpecificationRepresentationInterface $specificationRepresentation, $target);
+}
diff --git a/Domain/StubHasState.php b/Domain/StubHasState.php
new file mode 100644
index 0000000..1f9c0e7
--- /dev/null
+++ b/Domain/StubHasState.php
@@ -0,0 +1,31 @@
+
+ */
+class StubHasState implements HasStateInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function changeState(StateWorkflow $stateContext, StateInterface $newState)
+ {
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getState(StateWorkflow $stateContext)
+ {
+ return null;
+ }
+
+}
diff --git a/Domain/WorkflowContainer.php b/Domain/WorkflowContainer.php
new file mode 100644
index 0000000..977e6c6
--- /dev/null
+++ b/Domain/WorkflowContainer.php
@@ -0,0 +1,48 @@
+
+ */
+class WorkflowContainer
+{
+ /** @var StateWorkflow[] */
+ private $workflows = array();
+
+ /**
+ * Used by DIC during compiler pass
+ * @param StateWorkflow $stateWorkflow
+ */
+ public function addWorkflow(StateWorkflow $stateWorkflow)
+ {
+ $this->workflows[$stateWorkflow->getServiceId()] = $stateWorkflow;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function get($id)
+ {
+ if (!isset($this->workflows[$id])) {
+ throw new WorkflowServiceNotFoundException(
+ sprintf('Workflow service "%s" not found in Sf2 DIC.', $id)
+ );
+ }
+
+ $workflow = $this->workflows[$id];
+
+ return $workflow;
+ }
+
+ /**
+ * @return \Gmorel\StateWorkflowBundle\StateEngine\StateWorkflow[]
+ */
+ public function all()
+ {
+ return $this->workflows;
+ }
+}
diff --git a/Infra/CytoscapeSpecificationRepresentationGenerator.php b/Infra/CytoscapeSpecificationRepresentationGenerator.php
new file mode 100644
index 0000000..54d5654
--- /dev/null
+++ b/Infra/CytoscapeSpecificationRepresentationGenerator.php
@@ -0,0 +1,33 @@
+
+ */
+class CytoscapeSpecificationRepresentationGenerator implements SpecificationRepresentationGeneratorInterface
+{
+ const TEMPLATE_FILE_PATH = 'UI/Resource/workflow-template.html';
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createSpecification(StateWorkflow $stateWorkflow)
+ {
+ $introspectedWorkflow = new IntrospectedWorkflow($stateWorkflow);
+
+ $templateFilePath = realpath(dirname( __FILE__ ) . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . self::TEMPLATE_FILE_PATH);
+
+ return new HtmlSpecificationRepresentation(
+ new CytoscapeWorkflowRepresentation($introspectedWorkflow),
+ $templateFilePath
+ );
+ }
+}
diff --git a/Infra/FileSystemSpecificationWriter.php b/Infra/FileSystemSpecificationWriter.php
new file mode 100644
index 0000000..0da4afd
--- /dev/null
+++ b/Infra/FileSystemSpecificationWriter.php
@@ -0,0 +1,99 @@
+
+ */
+class FileSystemSpecificationWriter implements SpecificationWriterInterface
+{
+ /**
+ * {@inheritdoc}
+ * @param string $target Directory
+ *
+ * @throws UnableToWriteSpecificationException
+ */
+ public function write(SpecificationRepresentationInterface $specificationRepresentation, $target)
+ {
+ $this->guardAgainstEmptyTarget($target);
+
+ $renderedSpecification = $specificationRepresentation->render();
+
+ $this->guardAgainstEmptyRenderedSpecification($target, $renderedSpecification);
+
+ $this->createDirectoryIfNotExist($target);
+
+ $result = file_put_contents(
+ $target,
+ $specificationRepresentation->render()
+ );
+
+ $this->guardAgainstNotWrittenSpecification($target, $result);
+ }
+
+ /**
+ * @param string $directory
+ */
+ private function createDirectoryIfNotExist($directory)
+ {
+ if (!is_dir(dirname($directory))) {
+ mkdir(dirname($directory), 0777, true);
+ }
+ }
+
+ /**
+ * @param string $target
+ * @param string $renderedSpecification
+ *
+ * @throws UnableToWriteSpecificationException
+ */
+ private function guardAgainstEmptyRenderedSpecification($target, $renderedSpecification)
+ {
+ if (empty($renderedSpecification)) {
+ throw new UnableToWriteSpecificationException(
+ sprintf(
+ 'Unable to write the specification on "%s" because specification was rendered as an empty string.',
+ $target
+ )
+ );
+ }
+ }
+
+ /**
+ * @param string $target
+ * @param bool|int $result
+ */
+ private function guardAgainstNotWrittenSpecification($target, $result)
+ {
+ if (false === $result) {
+ throw new UnableToWriteSpecificationException(
+ sprintf(
+ 'Unable to write the specification on "%s".',
+ $target
+ )
+ );
+ }
+ }
+
+ /**
+ * @param string $target
+ *
+ * @throws UnableToWriteSpecificationException
+ */
+ private function guardAgainstEmptyTarget($target)
+ {
+ if (empty($target)) {
+ throw new UnableToWriteSpecificationException(
+ sprintf(
+ 'Unable to write the specification because target path is empty.',
+ $target
+ )
+ );
+ }
+ }
+
+}
diff --git a/README.md b/README.md
index 6fb7778..cb648fe 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,126 @@
-# SpecGen-StateWorkflow
-Specification generator for StateWorkflowBundle
+SpecGen - State Workflow Bundle
+===============================
+
+[![Build Status](https://travis-ci.org/spec-gen/state-workflow-spec-gen-bundle.svg?branch=master)](https://travis-ci.org/spec-gen/state-workflow-spec-gen-bundle)
+[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/spec-gen/state-workflow-spec-gen-bundle/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/spec-gen/state-workflow-spec-gen-bundle/?branch=master)
+[![Code Coverage](https://scrutinizer-ci.com/g/spec-gen/state-workflow-spec-gen-bundle/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/spec-gen/state-workflow-spec-gen-bundle/?branch=master)
+[![Dependency Status](https://www.versioneye.com/user/projects/55460de92405490d1f000002/badge.svg?style=flat)](https://www.versioneye.com/user/projects/55460de92405490d1f000002)
+[![Latest Stable Version](https://poser.pugx.org/spec-gen/state-workflow-spec-gen-bundle/v/stable.svg)](https://packagist.org/packages/spec-gen/state-workflow-spec-gen-bundle)
+[![License](https://poser.pugx.org/gmorel/state-workflow-bundle/license)](https://packagist.org/packages/gmorel/state-workflow-bundle)
+
+
+Ease complex workflow readability by generating its specification from your code base
+---------------------------------------------
+
+Keywords : Workflow, Finite State Machine, Symfony2, Specification Generation
+
+**Specification Generator** for [StateWorkflowBundle](https://github.com/gmorel/StateWorkflowBundle).
+
+> The worst specifications are **not updated** specifications..
+
+
+Aim is to have your `Workflow Specification` (Available states and transitions) always up to date in order to ease your Domain readability.
+Hence **avoiding misunderstandings** and allow new comers to assist you **quicker** in your project.
+**Saving you valuable time** since you no more have to make sure your specs are up to date.
+
+Generated specification for simple workflow
+![Demo Booking Workflow simple](https://raw.githubusercontent.com/spec-gen/state-workflow-spec-gen-bundle/master/doc/demo-booking-workflow.png "Demo Booking Workflow simple")
+
+
+
+
+
+Generated specification for more complex workflow
+![Demo Quote Workflow complex](https://raw.githubusercontent.com/spec-gen/state-workflow-spec-gen-bundle/master/doc/demo-quote-workflow-complex.png "Demo Quote Workflow complex")
+
+
+Usage
+=====
+
+From a Symfony2 project
+
+```cli
+php app/console.php spec-gen:state-workflow:generate-specifications
+```
+
+Workflow specification files will be generated in `{PROJECT ROOT}/specification/workflow/`
+
+Example : {PROJECT ROOT}/specification/workflow/demo.booking_engine.state_workflow.html
+
+
+Installation
+============
+
+Step 1: Download the Bundle
+---------------------------
+
+Open a command console, enter your project directory and execute the
+following command to download the latest stable version of this bundle:
+
+```bash
+$ composer require spec-gen/state-workflow-spec-gen-bundle "~1"
+```
+
+This command requires you to have Composer installed globally, as explained
+in the [installation chapter](https://getcomposer.org/doc/00-intro.md)
+of the Composer documentation.
+
+Step 2: Enable the Bundle
+-------------------------
+
+Then, enable the bundle by adding the following line in the `app/AppKernel.php`
+file of your project:
+
+```php
+getEnvironment(), array('dev', 'test'))) {
+ // ...
+ $bundles[] = new SpecGen\StateWorkflowSpecGenBundle\SpecGenStateWorkflowSpecGenBundle();
+ }
+
+ // ...
+ }
+
+ // ...
+}
+```
+
+Step 3: Implement your workflow
+-------------------------------
+
+Using [StateWorkflowBundle](https://github.com/gmorel/StateWorkflowBundle).
+
+Credits
+=======
+
+- [Cytoscape](http://www.cytoscape.org) Javascript Engine used to generate workflow specs.
+
+Licence
+=======
+
+MIT License (MIT)
+
+Contributing
+============
+
+Wanting to ease understanding of your projects from yourself and team members ?
+
+Wanting to contribute finding new ways of auto generating specifications from other SF2 project aspects ?
+- Enhancing Micro service interactions readability ?
+- DDD - Bounded Context - UML generation from application service ?
+- Ubiquitous Language dictionary generator ?
+- Other ideas ?
+
+Join https://github.com/spec-gen
diff --git a/SpecGenStateWorkflowSpecGenBundle.php b/SpecGenStateWorkflowSpecGenBundle.php
new file mode 100644
index 0000000..f0568ab
--- /dev/null
+++ b/SpecGenStateWorkflowSpecGenBundle.php
@@ -0,0 +1,32 @@
+
+ */
+class SpecGenStateWorkflowSpecGenBundle extends Bundle
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function build(ContainerBuilder $container)
+ {
+ parent::build($container);
+
+ $container->addCompilerPass(new RegisterStateWorkflowCompilerPass());
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getContainerExtension()
+ {
+ return new SpecGenStateWorkflowSpecGenBundleExtension();
+ }
+}
diff --git a/Test/App/SpecificationServiceTest.php b/Test/App/SpecificationServiceTest.php
new file mode 100644
index 0000000..29a1000
--- /dev/null
+++ b/Test/App/SpecificationServiceTest.php
@@ -0,0 +1,90 @@
+
+ */
+class SpecificationServiceTest extends \PHPUnit_Framework_TestCase
+{
+ public function test_it_should_introspect_workflow_states()
+ {
+ // Given
+ $stateWorkflow = $this->createValidStateWorkflow();
+ $outputFileName = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid() . '.html';
+
+ $command = new RenderWorkflowSpecificationFromWorkflowServiceCommand(
+ $stateWorkflow->getServiceId(),
+ $outputFileName
+ );
+ $workflowContainer = new WorkflowContainer();
+ $workflowContainer->addWorkflow($stateWorkflow);
+
+ $specificationWriter = new FileSystemSpecificationWriter();
+
+ $introspectedWorkflow = new SUT(
+ $workflowContainer,
+ new CytoscapeSpecificationRepresentationGenerator(),
+ $specificationWriter
+ );
+
+ $expected = '
+
+
+
+
+ Booking Workflow Specification
+
+
+
+
+
+
+
+
+
+
+';
+
+ // When
+ $introspectedWorkflow->renderSpecification($command);
+ $actual = file_get_contents($outputFileName);
+
+ // Then
+ $this->assertEquals($expected, $actual, 'Workflow Specification is not well rendered anymore.');
+ }
+
+ /**
+ * @return StateWorkflow
+ */
+ /**
+ * @return StateWorkflow
+ */
+ private function createValidStateWorkflow()
+ {
+ $stateA = new StateA();
+ $stateB = new StateB();
+ $stateC = new StateC();
+
+ $stateWorkflow = new StateWorkflow('Booking Workflow', 'key');
+ $stateWorkflow->addAvailableState($stateA);
+ $stateWorkflow->addAvailableState($stateB);
+ $stateWorkflow->addAvailableState($stateC);
+
+ $stateWorkflow->setStateAsDefault($stateA->getKey());
+
+ return $stateWorkflow;
+ }
+}
diff --git a/Test/Domain/IntrospectedWorkflowTest.php b/Test/Domain/IntrospectedWorkflowTest.php
new file mode 100644
index 0000000..b739dfa
--- /dev/null
+++ b/Test/Domain/IntrospectedWorkflowTest.php
@@ -0,0 +1,96 @@
+
+ */
+class IntrospectedWorkflowTest extends \PHPUnit_Framework_TestCase
+{
+ public function test_it_should_introspect_workflow_states()
+ {
+ // Given
+ $stateWorkflow = $this->createValidStateWorkflow();
+ $expected = $this->createExpectedStates();
+
+ $expected['a']->setIsRoot();
+ $expected['c']->setIsLeaf();
+
+ // When
+ $introspectedWorkflow = new SUT($stateWorkflow);
+ $actual = $introspectedWorkflow->getIntrospectedStates();
+
+ // Then
+ $this->assertEquals($expected, $actual, 'State are not well introspected anymore.');
+ }
+
+ public function test_it_should_introspect_workflow_transitions()
+ {
+ // Given
+ $stateWorkflow = $this->createValidStateWorkflow();
+
+ $expectedStates = $this->createExpectedStates();
+
+ $expectedStates['a']->setIsRoot();
+ $expectedStates['c']->setIsLeaf();
+
+ $expectedTransitions = array(
+ 'setToB_from_a' => new IntrospectedTransition(
+ 'setToB',
+ $expectedStates['a'],
+ $expectedStates['b']
+ ),
+ 'setToC_from_b' => new IntrospectedTransition(
+ 'setToC',
+ $expectedStates['b'],
+ $expectedStates['c']
+ ),
+ );
+
+ // When
+ $introspectedWorkflow = new SUT($stateWorkflow);
+ $actual = $introspectedWorkflow->getIntrospectedTransitions();
+
+ // Then
+ $this->assertEquals($expectedTransitions, $actual, 'Transitions are not well introspected anymore.');
+ }
+
+ /**
+ * @return StateWorkflow
+ */
+ private function createValidStateWorkflow()
+ {
+ $stateA = new StateA();
+ $stateB = new StateB();
+ $stateC = new StateC();
+
+ $stateWorkflow = new StateWorkflow('Booking Workflow', 'key');
+ $stateWorkflow->addAvailableState($stateA);
+ $stateWorkflow->addAvailableState($stateB);
+ $stateWorkflow->addAvailableState($stateC);
+
+ $stateWorkflow->setStateAsDefault($stateA->getKey());
+
+ return $stateWorkflow;
+ }
+
+ /**
+ * @return IntrospectedState[]
+ */
+ private function createExpectedStates()
+ {
+ return array(
+ 'a' => new IntrospectedState('a', 'A'),
+ 'b' => new IntrospectedState('b', 'B'),
+ 'c' => new IntrospectedState('c', 'C'),
+ );
+ }
+}
diff --git a/Test/Stub/Booking.php b/Test/Stub/Booking.php
new file mode 100644
index 0000000..24afaac
--- /dev/null
+++ b/Test/Stub/Booking.php
@@ -0,0 +1,62 @@
+
+ */
+class Booking implements HasStateInterface
+{
+ /** @var string */
+ protected $stateKey;
+
+ public function __construct(StateWorkflow $stateWorkflow)
+ {
+ $stateWorkflow->getDefaultState()->initialize($this);
+ }
+
+ /**
+ * @param string $stateKey
+ *
+ * @return $this
+ */
+ public function setStateKey($stateKey)
+ {
+ $this->stateKey = $stateKey;
+
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getStateKey()
+ {
+ return $this->stateKey;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function changeState(StateWorkflow $stateWorkflow, StateInterface $newState)
+ {
+ $stateWorkflow->guardExistingState($newState->getKey());
+ $this->setStateKey($newState->getKey());
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ * @return StateInterface
+ */
+ public function getState(StateWorkflow $stateWorkflow)
+ {
+ return $stateWorkflow->getStateFromKey($this->stateKey);
+ }
+}
diff --git a/Test/Stub/StateA.php b/Test/Stub/StateA.php
new file mode 100644
index 0000000..2c1b3ca
--- /dev/null
+++ b/Test/Stub/StateA.php
@@ -0,0 +1,63 @@
+
+ */
+class StateA extends AbstractState implements StubStateInterface
+{
+ /** Stored in database, easily indexed */
+ const KEY = 'a';
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getKey()
+ {
+ return self::KEY;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName()
+ {
+ return 'A';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function initialize(HasStateInterface $entity)
+ {
+ $entity->changeState($this->getStateWorkflow(), $this);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setToB(HasStateInterface $entity)
+ {
+ $newState = $this->getStateFromStateId(StateB::KEY, __METHOD__, $entity);
+ if ($newState) {
+ $entity->changeState($this->getStateWorkflow(), $newState);
+
+ // Implement necessary relevant transition here
+ }
+
+ return $newState;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setToC(HasStateInterface $entity)
+ {
+ throw $this->buildUnsupportedTransitionException(__METHOD__, $entity);
+ }
+}
diff --git a/Test/Stub/StateB.php b/Test/Stub/StateB.php
new file mode 100644
index 0000000..2bddbf8
--- /dev/null
+++ b/Test/Stub/StateB.php
@@ -0,0 +1,70 @@
+
+ */
+class StateB extends AbstractState implements StubStateInterface
+{
+ /** Stored in database, easily indexed */
+ const KEY = 'b';
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getKey()
+ {
+ return self::KEY;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName()
+ {
+ return 'B';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function initialize(HasStateInterface $entity)
+ {
+ $newState = $this->getStateFromStateId(StateA::KEY, __METHOD__, $entity);
+ if ($newState) {
+ $entity->changeState($this->getStateWorkflow(), $newState);
+
+ // Implement necessary relevant transition here
+ }
+
+ return $newState;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setToB(HasStateInterface $entity)
+ {
+ throw $this->buildUnsupportedTransitionException(__METHOD__, $entity);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setToC(HasStateInterface $entity)
+ {
+ $newState = $this->getStateFromStateId(StateC::KEY, __METHOD__, $entity);
+ if ($newState) {
+ $entity->changeState($this->getStateWorkflow(), $newState);
+
+ // Implement necessary relevant transition here
+ }
+
+ return $newState;
+ }
+}
diff --git a/Test/Stub/StateC.php b/Test/Stub/StateC.php
new file mode 100644
index 0000000..9851eaa
--- /dev/null
+++ b/Test/Stub/StateC.php
@@ -0,0 +1,56 @@
+
+ */
+class StateC extends AbstractState implements StubStateInterface
+{
+ /** Stored in database, easily indexed */
+ const KEY = 'c';
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getKey()
+ {
+ return self::KEY;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName()
+ {
+ return 'C';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function initialize(HasStateInterface $entity)
+ {
+ throw $this->buildUnsupportedTransitionException(__METHOD__, $entity);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setToB(HasStateInterface $entity)
+ {
+ throw $this->buildUnsupportedTransitionException(__METHOD__, $entity);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setToC(HasStateInterface $entity)
+ {
+ throw $this->buildUnsupportedTransitionException(__METHOD__, $entity);
+ }
+}
diff --git a/Test/Stub/StubStateInterface.php b/Test/Stub/StubStateInterface.php
new file mode 100644
index 0000000..1f4e66e
--- /dev/null
+++ b/Test/Stub/StubStateInterface.php
@@ -0,0 +1,35 @@
+
+ */
+interface StubStateInterface extends StateInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function initialize(HasStateInterface $entity);
+
+ /**
+ * To B
+ * @param \Gmorel\StateWorkflowBundle\StateEngine\HasStateInterface $entity
+ *
+ * @return StateB
+ */
+ public function setToB(HasStateInterface $entity);
+
+ /**
+ * To C
+ * @param \Gmorel\StateWorkflowBundle\StateEngine\HasStateInterface $entity
+ *
+ * @return StateC
+ */
+ public function setToC(HasStateInterface $entity);
+}
diff --git a/Test/UI/Cli/GenerateWorkflowSpecificationsCommandTest.php b/Test/UI/Cli/GenerateWorkflowSpecificationsCommandTest.php
new file mode 100644
index 0000000..e7f9d56
--- /dev/null
+++ b/Test/UI/Cli/GenerateWorkflowSpecificationsCommandTest.php
@@ -0,0 +1,41 @@
+
+ */
+class GenerateWorkflowSpecificationsCommandTest extends \PHPUnit_Framework_TestCase
+{
+ public function testExecute()
+ {
+ $mockSpecificationService = $this->mockSpecificationService();
+
+ $application = new Application();
+ $application->add(new GenerateWorkflowSpecificationsCommand($mockSpecificationService));
+
+ $command = $application->find('spec-gen:state-workflow:generate-specifications');
+ $commandTester = new CommandTester($command);
+ $commandTester->execute(array('command' => $command->getName()));
+ }
+
+ /**
+ * @return \SpecGen\StateWorkflowSpecGenBundle\App\SpecificationService
+ */
+ private function mockSpecificationService()
+ {
+ $mock = $this->getMockBuilder('SpecGen\StateWorkflowSpecGenBundle\App\SpecificationService')
+ ->disableOriginalConstructor()
+ ->getMock();
+ $mock->method('renderSpecification')
+ ->willReturn(null);
+ $mock->method('getAvailableWorkflowIds')
+ ->willReturn(array('booking'));
+
+ return $mock;
+ }
+}
diff --git a/Test/UI/Representation/CytoscapeWorkflowRepresentationTest.php b/Test/UI/Representation/CytoscapeWorkflowRepresentationTest.php
new file mode 100644
index 0000000..682e254
--- /dev/null
+++ b/Test/UI/Representation/CytoscapeWorkflowRepresentationTest.php
@@ -0,0 +1,54 @@
+
+ */
+class CytoscapeWorkflowRepresentationTest extends \PHPUnit_Framework_TestCase
+{
+ public function test_it_should_represent_itself_in_json()
+ {
+ // Given
+ $stateWorkflow = $this->createValidStateWorkflow();
+ $introspectedWorkflow = new IntrospectedWorkflow($stateWorkflow);
+
+ $expected = '{"nodes":[{"data":{"id":"a","name":"A","weight":50,"faveColor":"#999999","faveShape":"triangle"}},{"data":{"id":"b","name":"B","weight":50,"faveColor":"#FFFFFF","faveShape":"rectangle"}},{"data":{"id":"c","name":"C","weight":50,"faveColor":"#FFFFFF","faveShape":"ellipse"}}],"edges":[{"data":{"source":"a","target":"b","faveColor":"#999999","strength":20}},{"data":{"source":"b","target":"c","faveColor":"#FFFFFF","strength":20}}]}';
+
+ // When
+ $representation = new SUT($introspectedWorkflow);
+ $actual = $representation->serialize();
+
+ // Then
+ $this->assertEquals($expected, $actual, 'State Workflow are not well represented in JSON Cytoscape anymore.');
+ }
+
+ /**
+ * @return StateWorkflow
+ */
+ /**
+ * @return StateWorkflow
+ */
+ private function createValidStateWorkflow()
+ {
+ $stateA = new StateA();
+ $stateB = new StateB();
+ $stateC = new StateC();
+
+ $stateWorkflow = new StateWorkflow('Booking Workflow', 'key');
+ $stateWorkflow->addAvailableState($stateA);
+ $stateWorkflow->addAvailableState($stateB);
+ $stateWorkflow->addAvailableState($stateC);
+
+ $stateWorkflow->setStateAsDefault($stateA->getKey());
+
+ return $stateWorkflow;
+ }
+}
diff --git a/UI/Cli/GenerateWorkflowSpecificationsCommand.php b/UI/Cli/GenerateWorkflowSpecificationsCommand.php
new file mode 100644
index 0000000..2d2bcac
--- /dev/null
+++ b/UI/Cli/GenerateWorkflowSpecificationsCommand.php
@@ -0,0 +1,73 @@
+
+ */
+class GenerateWorkflowSpecificationsCommand extends Command
+{
+ /** @var SpecificationService */
+ private $specificationService;
+
+ public function __construct(SpecificationService $specificationService)
+ {
+ $this->specificationService = $specificationService;
+ parent::__construct();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function configure()
+ {
+ $this
+ ->setName('spec-gen:state-workflow:generate-specifications')
+ ->setDescription('Generate workflow specifications')
+ ->addOption(
+ '--target-path',
+ null,
+ InputOption::VALUE_REQUIRED,
+ 'Generated Workflow specification path directory.',
+ $this->getDefaultSpecificationDirectory()
+ )
+ ;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ foreach ($this->specificationService->getAvailableWorkflowIds() as $workflowId) {
+ $specificationFileName = $input->getOption('target-path') . DIRECTORY_SEPARATOR . $workflowId . '.html';
+ $command = new RenderWorkflowSpecificationFromWorkflowServiceCommand(
+ $workflowId,
+ $specificationFileName
+ );
+
+ $output->writeln(sprintf('Generating "%s" workflow specification.', $command->getWorkFlowServiceId()));
+ $this->specificationService->renderSpecification($command);
+ $output->writeln(sprintf('Workflow specification generated in "%s".', $specificationFileName));
+ }
+ }
+
+ /**
+ * @return string
+ */
+ private function getDefaultSpecificationDirectory()
+ {
+ return str_replace(
+ '/',
+ DIRECTORY_SEPARATOR,
+ realpath(__DIR__ . '/../../../../..')
+ ) . DIRECTORY_SEPARATOR . 'specification/workflow';
+ }
+}
diff --git a/UI/Representation/CytoscapeWorkflowRepresentation.php b/UI/Representation/CytoscapeWorkflowRepresentation.php
new file mode 100644
index 0000000..a5a0ad0
--- /dev/null
+++ b/UI/Representation/CytoscapeWorkflowRepresentation.php
@@ -0,0 +1,216 @@
+
+ */
+class CytoscapeWorkflowRepresentation implements WorkflowRepresentationInterface
+{
+ const KEY_STATE = 'nodes';
+ const KEY_TRANSITION = 'edges';
+
+ const DEFAULT_STATE_WEIGHT = 50;
+ const DEFAULT_TRANSITION_STRENGTH = 20;
+
+ const STATE_SHAPE_ROOT = 'triangle';
+ const STATE_SHAPE_NORMAL = 'rectangle';
+ const STATE_SHAPE_LEAF = 'ellipse';
+
+ /** @var string */
+ private $workflowName;
+
+ /** @var array */
+ private $jsonableStates;
+
+ /** @var array */
+ private $jsonableTransitions;
+
+ public function __construct(IntrospectedWorkflow $instrospectedWorkflow)
+ {
+ $this->workflowName = $instrospectedWorkflow->getWorkflowName();
+ $colors = $this->assignUniqueColorToStates(
+ $instrospectedWorkflow->getIntrospectedStates()
+ );
+
+ $this->jsonableStates = $this->createStatesRepresentation(
+ $instrospectedWorkflow->getIntrospectedStates(),
+ $colors
+ );
+
+ $this->jsonableTransitions = $this->createTransitionsRepresentation(
+ $instrospectedWorkflow->getIntrospectedTransitions(),
+ $colors
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getWorkflowName()
+ {
+ return $this->workflowName;
+ }
+
+ /**
+ * {@inheritdoc}
+ * @return string JSON
+ */
+ public function serialize()
+ {
+ return json_encode(
+ array(
+ self::KEY_STATE => $this->jsonableStates,
+ self::KEY_TRANSITION => $this->jsonableTransitions
+ )
+ );
+ }
+
+ /**
+ * @param IntrospectedState[] $introspectedStates
+ * @param string[] $colors
+ *
+ * @return array
+ */
+ private function createStatesRepresentation(array $introspectedStates, array $colors)
+ {
+ $jsonableStates = array();
+ foreach ($introspectedStates as $introspectedState) {
+ $jsonableStates[] = array(
+ 'data' => array(
+ 'id' => $introspectedState->getKey(),
+ 'name' => $introspectedState->getName(),
+ 'weight' => self::DEFAULT_STATE_WEIGHT,
+ 'faveColor' => $colors[$introspectedState->getKey()],
+ 'faveShape' => $this->getStateShape($introspectedState)
+ )
+ );
+ }
+
+ return $jsonableStates;
+ }
+
+ /**
+ * @param IntrospectedTransition[] $introspectedTransitions
+ * @param string[] $colors
+ *
+ * @return array
+ */
+ private function createTransitionsRepresentation(array $introspectedTransitions, array $colors)
+ {
+ $jsonableTransitions = array();
+ foreach ($introspectedTransitions as $introspectedTransition) {
+ $jsonableTransitions[] = array(
+ 'data' => array(
+ 'source' => $introspectedTransition->getFromIntrospectedState()->getKey(),
+ 'target' => $introspectedTransition->getToIntrospectedState()->getKey(),
+ 'faveColor' => $colors[$introspectedTransition->getFromIntrospectedState()->getKey()],
+ 'strength' => self::DEFAULT_TRANSITION_STRENGTH
+ )
+ );
+ }
+
+ return $jsonableTransitions;
+ }
+
+ /**
+ * @param IntrospectedState[] $introspectedStates
+ *
+ * @return string[]
+ */
+ private function assignUniqueColorToStates(array $introspectedStates)
+ {
+ $colors = array();
+ foreach ($introspectedStates as $introspectedState) {
+ $colors[$introspectedState->getKey()] = $this->createUniqueColor($introspectedState->getKey());
+ }
+
+ return $colors;
+ }
+
+ /**
+ * @param IntrospectedState $introspectedState
+ *
+ * @return string
+ */
+ private function getStateShape(IntrospectedState $introspectedState)
+ {
+ if ($introspectedState->isRoot()) {
+ return self::STATE_SHAPE_ROOT;
+ }
+
+ if ($introspectedState->isLeaf()) {
+ return self::STATE_SHAPE_LEAF;
+ }
+
+ return self::STATE_SHAPE_NORMAL;
+ }
+
+ /**
+ * @param int $number
+ *
+ * @return string
+ */
+ private function getColorChar($number)
+ {
+ $modulo = $number % 22;
+
+ if ($modulo < 10) {
+ return strrev((string) $modulo);
+ }
+
+ $numberMap = array(
+ 10 => 'A',
+ 11 => 'B',
+ 12 => 'C',
+ 13 => 'D',
+ 14 => 'E'
+ );
+
+ if (isset($numberMap[$number])) {
+ return strrev($numberMap[$number]);
+ }
+
+ $moduloMap = array(
+ 21 => 'A',
+ 20 => 'B',
+ 19 => 'C',
+ 18 => 'D',
+ 17 => 'E'
+ );
+
+ if (isset($moduloMap[$modulo])) {
+ return strrev($moduloMap[$modulo]);
+ }
+
+ return strrev('F');
+ }
+
+ /**
+ * @param string $initializationVector
+ *
+ * @return string #FF0000
+ * @credits https://github.com/baykall/Php-Unique-HTML-Color-Generator-From-String/blob/master/color.php
+ */
+ private function createUniqueColor($initializationVector)
+ {
+ if(empty($initializationVector)) {
+ return '#FF0000';
+ }
+
+ $length = strlen($initializationVector);
+ $color = '';
+ for ($i = 1; $i <= 6; $i++) {
+
+ $charNumber = ($i - 1) % $length;
+ $color = $color . $this->getColorChar(ord($initializationVector{$charNumber}));
+ }
+
+ return '#' . $color;
+ }
+}
diff --git a/UI/Representation/HtmlSpecificationRepresentation.php b/UI/Representation/HtmlSpecificationRepresentation.php
new file mode 100644
index 0000000..bdca3ba
--- /dev/null
+++ b/UI/Representation/HtmlSpecificationRepresentation.php
@@ -0,0 +1,84 @@
+
+ * We avoid to use Twig as not every projects are using it.
+ * No need to force them to install this dependency.
+ */
+class HtmlSpecificationRepresentation implements SpecificationRepresentationInterface
+{
+ const TEMPLATE_VARIABLE_WORKFLOW_NAME = '__WORKFLOW_NAME__';
+ const TEMPLATE_VARIABLE_JSONED_WORKFLOW = '__JSON__';
+
+ /** @var string HTML */
+ private $renderedHtml;
+
+ public function __construct(CytoscapeWorkflowRepresentation $workflowRepresentation, $htmlTemplatePath)
+ {
+ $this->guardAgainstHtmlTemplatePathNotExisting($htmlTemplatePath);
+
+ $htmlTemplate = file_get_contents($htmlTemplatePath);
+
+ $htmlTemplate = $this->fillTemplateWithJsonedWorkflow($workflowRepresentation, $htmlTemplate);
+
+ $renderedHtml = $this->fillTemplateWithWorkflowName($workflowRepresentation, $htmlTemplate);
+
+ $this->renderedHtml = $renderedHtml;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function render()
+ {
+ return $this->renderedHtml;
+ }
+
+ /**
+ * @param $htmlTemplatePath
+ */
+ private function guardAgainstHtmlTemplatePathNotExisting($htmlTemplatePath)
+ {
+ if (!file_exists($htmlTemplatePath)) {
+ throw new \LogicException(
+ sprintf(
+ 'Template file "%s" was not found.',
+ $htmlTemplatePath
+ )
+ );
+ }
+ }
+
+ /**
+ * @param CytoscapeWorkflowRepresentation $workflowRepresentation
+ * @param string $htmlTemplate
+ *
+ * @return string HTML
+ */
+ private function fillTemplateWithJsonedWorkflow(CytoscapeWorkflowRepresentation $workflowRepresentation, $htmlTemplate)
+ {
+ return str_replace(
+ self::TEMPLATE_VARIABLE_JSONED_WORKFLOW,
+ $workflowRepresentation->serialize(),
+ $htmlTemplate
+ );
+ }
+
+ /**
+ * @param CytoscapeWorkflowRepresentation $workflowRepresentation
+ * @param $htmlTemplate
+ * @return mixed
+ */
+ private function fillTemplateWithWorkflowName(CytoscapeWorkflowRepresentation $workflowRepresentation, $htmlTemplate)
+ {
+ return str_replace(
+ self::TEMPLATE_VARIABLE_WORKFLOW_NAME,
+ $workflowRepresentation->getWorkflowName(),
+ $htmlTemplate
+ );
+ }
+}
diff --git a/UI/Resource/code.js b/UI/Resource/code.js
new file mode 100644
index 0000000..f912d22
--- /dev/null
+++ b/UI/Resource/code.js
@@ -0,0 +1,59 @@
+/**
+ * Created by gmorel on 27/03/15.
+ */
+$(function(){ // on dom ready
+
+ $('#cy').cytoscape({
+ layout: {
+ name: 'cose',
+ padding: 50
+ },
+
+ style: cytoscape.stylesheet()
+ .selector('node')
+ .css({
+ 'shape': 'data(faveShape)',
+ 'width': 'mapData(weight, 40, 80, 20, 60)',
+ 'content': 'data(name)',
+ 'text-valign': 'center',
+ 'text-outline-width': 2,
+ 'text-outline-color': 'data(faveColor)',
+ 'background-color': 'data(faveColor)',
+ 'color': '#fff'
+ })
+ .selector(':selected')
+ .css({
+ 'border-width': 3,
+ 'border-color': '#333'
+ })
+ .selector('edge')
+ .css({
+ 'opacity': 0.666,
+ 'width': 'mapData(strength, 70, 100, 2, 6)',
+ 'target-arrow-shape': 'triangle',
+ 'source-arrow-shape': 'circle',
+ 'line-color': 'data(faveColor)',
+ 'source-arrow-color': 'data(faveColor)',
+ 'target-arrow-color': 'data(faveColor)',
+ 'curve-style': 'bezier'
+ })
+ .selector('edge.questionable')
+ .css({
+ 'line-style': 'dotted',
+ 'target-arrow-shape': 'diamond'
+ })
+ .selector('.faded')
+ .css({
+ 'opacity': 0.25,
+ 'text-opacity': 0
+ }),
+
+ elements: dataWorkflow,
+
+ ready: function(){
+ window.cy = this;
+ }
+ });
+
+}); // on dom ready
+
diff --git a/UI/Resource/style.css b/UI/Resource/style.css
new file mode 100644
index 0000000..2cafc56
--- /dev/null
+++ b/UI/Resource/style.css
@@ -0,0 +1,11 @@
+body {
+ font: 14px helvetica neue, helvetica, arial, sans-serif;
+}
+
+#cy {
+ height: 100%;
+ width: 100%;
+ position: absolute;
+ left: 0;
+ top: 0;
+}
diff --git a/UI/Resource/workflow-template.html b/UI/Resource/workflow-template.html
new file mode 100644
index 0000000..0e3aedd
--- /dev/null
+++ b/UI/Resource/workflow-template.html
@@ -0,0 +1,18 @@
+
+
+
+
+
+ __WORKFLOW_NAME__ Specification
+
+
+
+
+
+
+
+
+
+
diff --git a/composer.json b/composer.json
index 67ae3e0..e7569ff 100644
--- a/composer.json
+++ b/composer.json
@@ -1,5 +1,5 @@
{
- "name": "gmorel/SpecGen-StateWorkflow",
+ "name": "spec-gen/state-workflow-spec-gen-bundle",
"license": "MIT",
"authors": [
{
@@ -9,8 +9,8 @@
],
"type": "library",
"autoload": {
- "psr-0": {
- "Gmorel\\SpecGenStateWorkflowBundle": ""
+ "psr-4": {
+ "SpecGen\\StateWorkflowSpecGenBundle\\": ""
}
},
"require": {
@@ -23,22 +23,10 @@
"require-dev": {
"composer/composer": "1.0.*@dev",
"phpunit/phpunit": "~4",
- "gmorel/state-workflow-bundle": "dev-develop",
- "gmorel/state-workflow-demo": "dev-master"
+ "gmorel/state-workflow-bundle": "dev-master"
},
- "repositories": [
- {
- "type": "vcs",
- "url": "https://github.com/gmorel/StateWorkflowBundle.git"
- },
- {
- "type": "vcs",
- "url": "https://github.com/gmorel/StateWorkflowDemo.git"
- }
- ],
"scripts": {
},
- "target-dir": "Gmorel/SpecGenStateWorkflowBundle",
"minimum-stability": "dev",
"config": {
"bin-dir": "bin"
diff --git a/doc/demo-booking-workflow.png b/doc/demo-booking-workflow.png
new file mode 100644
index 0000000..ce08742
Binary files /dev/null and b/doc/demo-booking-workflow.png differ
diff --git a/doc/demo-quote-workflow-complex.png b/doc/demo-quote-workflow-complex.png
new file mode 100644
index 0000000..28461c4
Binary files /dev/null and b/doc/demo-quote-workflow-complex.png differ
diff --git a/doc/symfony.png b/doc/symfony.png
new file mode 100644
index 0000000..c19a942
Binary files /dev/null and b/doc/symfony.png differ
diff --git a/phpunit.xml b/phpunit.xml
index a8d3fcc..b2ed20b 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -9,7 +9,7 @@
>
- test/*
+ Test/*