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) +Spec Gen logo + +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.. + +Symfony 2 +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/*