├── .gitignore ├── src ├── Exception │ ├── InvalidTokenException.php │ ├── NoOpenTransitionException.php │ ├── NoStartingNodeBuilderException.php │ ├── NotInitializedWorkflowException.php │ └── MoreThanOneOpenTransitionException.php ├── SpecificationInterface.php ├── ContextInterface.php ├── Event.php ├── NodeMap.php ├── Transition.php ├── Node.php ├── Builder.php └── Workflow.php ├── .travis.yml ├── composer.json ├── spec └── Alterway │ └── Component │ └── Workflow │ ├── NodeMapSpec.php │ ├── EventSpec.php │ ├── TransitionSpec.php │ ├── NodeSpec.php │ ├── BuilderSpec.php │ └── WorkflowSpec.php ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | .idea 3 | composer.lock 4 | build 5 | -------------------------------------------------------------------------------- /src/Exception/InvalidTokenException.php: -------------------------------------------------------------------------------- 1 | shouldHaveType('Alterway\Component\Workflow\NodeMap'); 13 | } 14 | 15 | function it_holds_nothing_by_default() 16 | { 17 | $this->has('A')->shouldReturn(false); 18 | } 19 | 20 | function it_creates_node_on_get_and_keep_it() 21 | { 22 | $this->get('B')->shouldReturnAnInstanceOf('Alterway\Component\Workflow\Node'); 23 | $this->has('B')->shouldReturn(true); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/ContextInterface.php: -------------------------------------------------------------------------------- 1 | context = $context; 25 | $this->token = $token; 26 | } 27 | 28 | public function getContext() 29 | { 30 | return $this->context; 31 | } 32 | 33 | public function getToken() 34 | { 35 | return $this->token; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /spec/Alterway/Component/Workflow/EventSpec.php: -------------------------------------------------------------------------------- 1 | beConstructedWith($ctx, Argument::any()); 15 | } 16 | 17 | function it_is_initializable() 18 | { 19 | $this->shouldHaveType('Alterway\Component\Workflow\Event'); 20 | } 21 | 22 | function it_has_a_context() 23 | { 24 | $this->getContext()->shouldHaveType('Alterway\Component\Workflow\ContextInterface'); 25 | } 26 | 27 | function it_has_a_token(Ctx $ctx) 28 | { 29 | $this->beConstructedWith($ctx, 'token'); 30 | $this->getToken()->shouldReturn('token'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/NodeMap.php: -------------------------------------------------------------------------------- 1 | items = array(); 17 | } 18 | 19 | /** 20 | * Gets a node by name. 21 | * 22 | * @param string $name 23 | * 24 | * @return Node 25 | */ 26 | public function get($name) 27 | { 28 | $name = (string)$name; 29 | 30 | if (!isset($this->items[$name])) { 31 | $this->items[$name] = new Node($name); 32 | } 33 | 34 | return $this->items[$name]; 35 | } 36 | 37 | /** 38 | * Checks if a node exists. 39 | * 40 | * @param string $name 41 | * 42 | * @return bool 43 | */ 44 | public function has($name) 45 | { 46 | $name = (string)$name; 47 | 48 | return isset($this->items[$name]); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Transition.php: -------------------------------------------------------------------------------- 1 | src = $src; 28 | $this->dst = $dst; 29 | $this->spec = $spec; 30 | } 31 | 32 | /** 33 | * Checks if the current transition satisfies the specifiation on the given context. 34 | * 35 | * @param ContextInterface $context 36 | * 37 | * @return bool 38 | */ 39 | public function isOpen(ContextInterface $context) 40 | { 41 | return $this->spec->isSatisfiedBy($context); 42 | } 43 | 44 | /** 45 | * Returns the destination of the current transition. 46 | * 47 | * @return Node 48 | */ 49 | public function getDestination() 50 | { 51 | return $this->dst; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 La Ruche Qui Dit Oui ! 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /spec/Alterway/Component/Workflow/TransitionSpec.php: -------------------------------------------------------------------------------- 1 | beConstructedWith($node, $node, $spec); 17 | } 18 | 19 | function it_is_initializable() 20 | { 21 | $this->shouldHaveType('Alterway\Component\Workflow\Transition'); 22 | } 23 | 24 | function it_has_a_destination() 25 | { 26 | $this->getDestination()->shouldHaveType('Alterway\Component\Workflow\Node'); 27 | } 28 | 29 | function it_knows_if_it_is_open_or_not(Node $node, Spec $spec, Ctx $ctx) 30 | { 31 | $this->let($node, $spec); 32 | 33 | $spec->isSatisfiedBy($ctx)->willReturn(true); 34 | $this->isOpen($ctx)->shouldReturn(true); 35 | 36 | $spec->isSatisfiedBy($ctx)->willReturn(false); 37 | $this->isOpen($ctx)->shouldReturn(false); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /spec/Alterway/Component/Workflow/NodeSpec.php: -------------------------------------------------------------------------------- 1 | beConstructedWith(Argument::any()); 17 | } 18 | 19 | function it_is_initializable() 20 | { 21 | $this->shouldHaveType('Alterway\Component\Workflow\Node'); 22 | } 23 | 24 | function it_has_a_name() 25 | { 26 | $this->beConstructedWith('name'); 27 | $this->getName()->shouldReturn('name'); 28 | } 29 | 30 | function it_creates_transitions(Node $node, Spec $spec) 31 | { 32 | $this->addTransition($node, $spec)->shouldReturn(null); 33 | } 34 | 35 | function it_finds_open_transition(Node $node, Spec $true, Spec $false, Ctx $ctx) 36 | { 37 | $true->isSatisfiedBy($ctx)->willReturn(true); 38 | $false->isSatisfiedBy($ctx)->willReturn(false); 39 | 40 | $this->addTransition($node, $true); 41 | $this->addTransition($node, $false); 42 | 43 | $transitions = $this->getOpenTransitions($ctx); 44 | 45 | $transitions->shouldBeArray(); 46 | $transitions->shouldHaveCount(1); 47 | 48 | $transitions[0]->shouldHaveType('Alterway\Component\Workflow\Transition'); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Node.php: -------------------------------------------------------------------------------- 1 | name = (string)$name; 23 | $this->transitions = array(); 24 | } 25 | 26 | /** 27 | * Returns the current node's name. 28 | * 29 | * @return string 30 | */ 31 | public function getName() 32 | { 33 | return $this->name; 34 | } 35 | 36 | /** 37 | * Adds a transition. 38 | * 39 | * @param Node $dst 40 | * @param SpecificationInterface $spec 41 | * 42 | * @return Node 43 | */ 44 | public function addTransition(Node $dst, SpecificationInterface $spec) 45 | { 46 | $this->transitions[] = new Transition($this, $dst, $spec); 47 | } 48 | 49 | /** 50 | * Returns the opened transitions. 51 | * 52 | * @param ContextInterface $context 53 | * 54 | * @return array 55 | */ 56 | public function getOpenTransitions(ContextInterface $context) 57 | { 58 | $transitions = array(); 59 | 60 | foreach ($this->transitions as $transition) { 61 | /** @var $transition Transition */ 62 | if ($transition->isOpen($context)) { 63 | $transitions[] = $transition; 64 | } 65 | } 66 | 67 | return $transitions; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /spec/Alterway/Component/Workflow/BuilderSpec.php: -------------------------------------------------------------------------------- 1 | beConstructedWith($dispatcher); 16 | } 17 | 18 | function it_is_initializable() 19 | { 20 | $this->shouldHaveType('Alterway\Component\Workflow\Builder'); 21 | } 22 | 23 | function it_can_be_opened(Spec $spec) 24 | { 25 | $this->open('start', $spec)->shouldHaveType('Alterway\Component\Workflow\Builder'); 26 | } 27 | 28 | function it_creates_links_between_nodes(Spec $spec) 29 | { 30 | $this->open('start', $spec); 31 | $this->link('start', 'end', $spec)->shouldHaveType('Alterway\Component\Workflow\Builder'); 32 | } 33 | 34 | function it_throws_exception_if_not_started_when_creating_links_between_nodes(Spec $spec) 35 | { 36 | $this->shouldThrow('Alterway\Component\Workflow\Exception\NoStartingNodeBuilderException')->duringLink('src', 'dst', $spec); 37 | } 38 | 39 | function it_builds_a_worflow_object(Spec $spec) 40 | { 41 | $this->open('start', $spec); 42 | $this->link('start', 'end', $spec); 43 | 44 | $this->getWorkflow()->shouldHaveType('Alterway\Component\Workflow\Workflow'); 45 | } 46 | 47 | function it_throws_exception_if_not_started_when_building_a_workflow_object(Spec $spec) 48 | { 49 | $this->shouldThrow('Alterway\Component\Workflow\Exception\NoStartingNodeBuilderException')->duringGetWorkflow(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Builder.php: -------------------------------------------------------------------------------- 1 | eventDispatcher = $eventDispatcher ?: new EventDispatcher(); 31 | $this->nodes = new NodeMap(); 32 | $this->start = null; 33 | } 34 | 35 | /** 36 | * Opens a workflow. 37 | * 38 | * @param string $src 39 | * @param SpecificationInterface $spec 40 | * 41 | * @return Builder 42 | */ 43 | public function open($src, SpecificationInterface $spec) 44 | { 45 | $this->start = $this->nodes->get(uniqid()); 46 | $this->start->addTransition($this->nodes->get($src), $spec); 47 | 48 | return $this; 49 | } 50 | 51 | /** 52 | * Adds a link to the workflow. 53 | * 54 | * @param string $src 55 | * @param string $dst 56 | * @param SpecificationInterface $spec 57 | * 58 | * @return Builder 59 | * 60 | * @throws Exception\NoStartingNodeBuilderException 61 | */ 62 | public function link($src, $dst, SpecificationInterface $spec) 63 | { 64 | if (null === $this->start) { 65 | throw new Exception\NoStartingNodeBuilderException(); 66 | }; 67 | 68 | $this->nodes->get($src)->addTransition($this->nodes->get($dst), $spec); 69 | 70 | return $this; 71 | } 72 | 73 | /** 74 | * Returns the workflow being built. 75 | * 76 | * @return Workflow 77 | * 78 | * @throws Exception\NoStartingNodeBuilderException 79 | */ 80 | public function getWorkflow() 81 | { 82 | if (null === $this->start) { 83 | throw new Exception\NoStartingNodeBuilderException(); 84 | }; 85 | 86 | return new Workflow($this->start, $this->nodes, $this->eventDispatcher); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Workflow.php: -------------------------------------------------------------------------------- 1 | start = $start; 35 | $this->nodes = $nodes; 36 | $this->eventDispatcher = $eventDispatcher; 37 | $this->current = null; 38 | } 39 | 40 | /** 41 | * Initializes the workflow with a given token. 42 | * 43 | * @param string $token 44 | * 45 | * @return Workflow 46 | * 47 | * @throws Exception\InvalidTokenException 48 | */ 49 | public function initialize($token = null) 50 | { 51 | if (null === $token) { 52 | $this->current = $this->start; 53 | } elseif ($this->nodes->has($token)) { 54 | $this->current = $this->nodes->get($token); 55 | } else { 56 | throw new Exception\InvalidTokenException(); 57 | } 58 | 59 | return $this; 60 | } 61 | 62 | /** 63 | * Moves the current token to the next node of the workflow. 64 | * 65 | * @param ContextInterface $context 66 | * 67 | * @return Workflow 68 | * 69 | * @throws Exception\NotInitializedWorkflowException 70 | * @throws Exception\NoOpenTransitionException 71 | * @throws Exception\MoreThanOneOpenTransitionException 72 | */ 73 | public function next(ContextInterface $context) 74 | { 75 | if (null === $this->current) { 76 | throw new Exception\NotInitializedWorkflowException(); 77 | } 78 | 79 | $transitions = $this->current->getOpenTransitions($context); 80 | 81 | if (0 === count($transitions)) { 82 | throw new Exception\NoOpenTransitionException(); 83 | } elseif (1 < count($transitions)) { 84 | throw new Exception\MoreThanOneOpenTransitionException(); 85 | } 86 | 87 | $transition = array_pop($transitions); 88 | $token = $transition->getDestination()->getName(); 89 | 90 | $this->initialize($token); 91 | $this->eventDispatcher->dispatch($token, new Event($context, $token)); 92 | 93 | return $this; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /spec/Alterway/Component/Workflow/WorkflowSpec.php: -------------------------------------------------------------------------------- 1 | getWrappedObject()); 18 | $builder 19 | ->open('A', $specA->getWrappedObject()) 20 | ->link('A', 'B', $specAB->getWrappedObject()) 21 | ->link('A', 'C', $specAC->getWrappedObject()); 22 | 23 | $this->beConstructedThrough(array($builder, 'getWorkflow')); 24 | } 25 | 26 | function it_is_initializable() 27 | { 28 | $this->shouldHaveType('Alterway\Component\Workflow\Workflow'); 29 | } 30 | 31 | function it_is_initializable_with_an_empty_token() 32 | { 33 | $this->initialize()->shouldHaveType('Alterway\Component\Workflow\Workflow'); 34 | } 35 | 36 | function it_is_initializable_with_a_known_token() 37 | { 38 | $this->initialize('A')->shouldHaveType('Alterway\Component\Workflow\Workflow'); 39 | } 40 | 41 | function it_throws_exception_if_initialized_with_an_unknown_token() 42 | { 43 | $this->shouldThrow('Alterway\Component\Workflow\Exception\InvalidTokenException')->duringInitialize('D'); 44 | } 45 | 46 | function it_throws_exception_when_advancing_if_not_initialized(Ctx $ctx) 47 | { 48 | $this->shouldThrow('Alterway\Component\Workflow\Exception\NotInitializedWorkflowException')->duringNext($ctx); 49 | } 50 | 51 | function it_advances_when_only_one_way_exists(EventDispatcherInterface $dispatcher, Spec $specA, Spec $specAB, Spec $specAC, Ctx $ctx) 52 | { 53 | $this->let($dispatcher, $specA, $specAB, $specAC); 54 | 55 | $dispatcher->dispatch('B', Argument::type('Alterway\Component\Workflow\Event'))->shouldBeCalled(); 56 | 57 | $specAB->isSatisfiedBy($ctx)->willReturn(true); 58 | $specAC->isSatisfiedBy($ctx)->willReturn(false); 59 | 60 | $this->initialize('A'); 61 | 62 | $this->next($ctx)->shouldHaveType('Alterway\Component\Workflow\Workflow'); 63 | } 64 | 65 | function it_throws_exception_when_no_way_exist(EventDispatcherInterface $dispatcher, Spec $specA, Spec $specAB, Spec $specAC, Ctx $ctx) 66 | { 67 | $this->let($dispatcher, $specA, $specAB, $specAC); 68 | 69 | $specAB->isSatisfiedBy($ctx)->willReturn(false); 70 | $specAC->isSatisfiedBy($ctx)->willReturn(false); 71 | 72 | $this->initialize('A'); 73 | 74 | $this->shouldThrow('Alterway\Component\Workflow\Exception\NoOpenTransitionException')->duringNext($ctx); 75 | } 76 | 77 | function it_throws_exception_when_more_than_one_way_exist(EventDispatcherInterface $dispatcher, Spec $specA, Spec $specAB, Spec $specAC, Ctx $ctx) 78 | { 79 | $this->let($dispatcher, $specA, $specAB, $specAC); 80 | 81 | $specAB->isSatisfiedBy($ctx)->willReturn(true); 82 | $specAC->isSatisfiedBy($ctx)->willReturn(true); 83 | 84 | $this->initialize('A'); 85 | 86 | $this->shouldThrow('Alterway\Component\Workflow\Exception\MoreThanOneOpenTransitionException')->duringNext($ctx); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Workflow Component [![Build Status](https://travis-ci.org/alterway/component-workflow.png?branch=master)](https://travis-ci.org/alterway/component-workflow) 2 | 3 | This component provides a workflow engine written as a PHP library. 4 | 5 | Instead of modeling a workflow as a Petri net or trying to enumerate workflow patterns, the library consider a workflow as a simple directed graph: vertices model nodes and edges model transitions. 6 | 7 | ### Nodes 8 | 9 | A node represents a point in a life cycle. 10 | The `Node` class implements the concept. 11 | A node is referenced by a unique name across the workflow. 12 | The constraint is the responsibility of `NodeMap` class. 13 | 14 | ### Transitions 15 | 16 | A transition is a link between nodes. 17 | The `Transition` class implements the concept. 18 | At creation, a transition is given a specification object implementing the `SpecificationInterface`. 19 | the specification is used as a business rule to decide where to advance in the workflow. 20 | 21 | ### Tokens 22 | 23 | A token is a simple string used to initialize the workflow in a particular node. 24 | The idea is to consider the token as a thing placed at the center of a node. 25 | When workflow engine is on, the token is moving from node to node. 26 | 27 | ### Events 28 | 29 | An event is an object created each time a token arrives at a node. 30 | The `Event` class implements the concept. 31 | This class extends the `Event` class from the Symfony EventDispatcher component. 32 | You can write listeners or subscribers to implement any business behaviour. 33 | 34 | ## Usage 35 | 36 | Let's say you are writing a blog engine in PHP and you want to implement the following workflow: 37 | * an article begins its existence as a draft 38 | * when ready, the article gets published 39 | * if controversial, the article is deleted 40 | * when too old, the article is archived 41 | 42 | First of all, you need to write classes implementing `SpecificationInterface` for every business rule: 43 | ```php 44 | namespace BlogEngine\Domain\Specification; 45 | 46 | use Alterway\Component\Workflow\ContextInterface; 47 | use Alterway\Component\Workflow\SpecificationInterface; 48 | 49 | class DraftableArticleSpecification implements SpecificationInterface 50 | { 51 | public function isSatisfiedBy(ContextInterface $context) 52 | { 53 | // an article can always be drafted 54 | return true; 55 | } 56 | } 57 | 58 | class PublishableArticleSpecification implements SpecificationInterface 59 | { 60 | public function isSatisfiedBy(ContextInterface $context) 61 | { 62 | // an article needs two reviews to be published 63 | return 1 < count($context->get('article')->getReviews()); 64 | } 65 | } 66 | 67 | class DeletableArticleSpecification implements SpecificationInterface 68 | { 69 | public function isSatisfiedBy(ContextInterface $context) 70 | { 71 | // an article can always be deleted if requested 72 | return 'delete' === $context->get('action'); 73 | } 74 | } 75 | 76 | class ArchivableArticleSpecification implements SpecificationInterface 77 | { 78 | public function isSatisfiedBy(ContextInterface $context) 79 | { 80 | // an article needs to be one month old to be archived 81 | $publishedAtPlusOneMonth = clone $context->get('publishedAt'); 82 | $publishedAtPlusOneMonth->modify('+1 month'); 83 | 84 | return 'archive' === $context->get('action') && $publishedAtPlusOneMonth < $context->get('now'); 85 | } 86 | } 87 | ``` 88 | 89 | Then, you can use the `Builder` class and the specifications to describe the workflow: 90 | ```php 91 | namespace BlogEngine\Domain\Service; 92 | 93 | use Alterway\Component\Workflow\Builder; 94 | use Alterway\Component\Workflow\ContextInterface; 95 | use BlogEngine\Domain\Event\ArticleSubscriber; 96 | use BlogEngine\Domain\Specification\DraftableArticleSpecification; 97 | use BlogEngine\Domain\Specification\PublishableArticleSpecification; 98 | use BlogEngine\Domain\Specification\DeletableArticleSpecification; 99 | use BlogEngine\Domain\Specification\ArchivableArticleSpecification; 100 | use BlogEngine\Util\Context; 101 | use Symfony\Component\EventDispatcher\EventDispatcherInterface; 102 | 103 | class ArticleService 104 | { 105 | private $workflow; 106 | 107 | public function __construct(EventDispatcherInterface $eventDispatcher) 108 | { 109 | $this->workflow = (new Builder($eventDispatcher)) 110 | ->open('article.draft', new DraftableArticleSpecification()) 111 | ->link('article.draft', 'article.published', new PublishableArticleSpecification()) 112 | ->link('article.published', 'article.deleted', new DeletableArticleSpecification()) 113 | ->link('article.published', 'article.archived', new ArchivableArticleSpecification()) 114 | ->getWorkflow(); 115 | 116 | $eventDispatcher->addSubscriber(new ArticleSubscriber()); 117 | } 118 | 119 | public function create(Article $article) 120 | { 121 | $this->advance($article, new Context()); 122 | } 123 | 124 | public function publish(Article $article) 125 | { 126 | $context = new Context(); 127 | $context->set('article', $article); 128 | 129 | $this->advance($article, $context); 130 | } 131 | 132 | public function delete(Article $article) 133 | { 134 | $context = new Context(); 135 | $context->set('action', 'delete'); 136 | 137 | $this->advance($article, $context); 138 | } 139 | 140 | public function archive(Article $article) 141 | { 142 | $context = new Context(); 143 | $context->set('action', 'archive'); 144 | $context->set('publishedAt', $article->getPublishedAt()); 145 | $context->set('now', new \DateTime()); 146 | 147 | $this->advance($article, $context); 148 | } 149 | 150 | private function advance($article, ContextInterface $context) 151 | { 152 | try { 153 | $this->workflow->initialize($article->getToken())->next($context); 154 | } catch (\LogicException $e) { 155 | // the workflow reports a problem 156 | } 157 | } 158 | } 159 | ``` 160 | 161 | Finally, you have to listen on events dispatched by the workflow to attach the business behavior: 162 | ```php 163 | namespace BlogEngine\Domain\Event; 164 | 165 | use Alterway\Component\Workflow\Event; 166 | use Symfony\Component\EventDispatcher\EventSubscriberInterface; 167 | 168 | class ArticleSubscriber implements EventSubscriberInterface 169 | { 170 | public static function getSubscribedEvents() 171 | { 172 | return array( 173 | 'article.draft' => array('onDraft', 0), 174 | 'article.published' => array('onPublished', 0), 175 | 'article.deleted' => array('onDeleted', 0), 176 | 'article.archived' => array('onArchived', 0), 177 | ); 178 | } 179 | 180 | public function onDraft(Event $event) { /* ... */ } 181 | 182 | public function onPublished(Event $event) { /* ... */ } 183 | 184 | public function onDeleted(Event $event) { /* ... */ } 185 | 186 | public function onArchived(Event $event) { /* ... */ } 187 | } 188 | ``` 189 | 190 | ## Contributing 191 | 192 | Pretty please, with sugar on top, phpspec specifications are provided and should be green when contributing code. 193 | 194 | ## References 195 | 196 | ### Theory 197 | 198 | * [Petri net](http://en.wikipedia.org/wiki/Petri_net) 199 | * [Workflow patterns](http://www.workflowpatterns.com/) 200 | * [Graph theory](http://en.wikipedia.org/wiki/Graph_theory) 201 | * [Specification pattern](http://en.wikipedia.org/wiki/Specification_pattern) 202 | 203 | ### PHP 204 | 205 | * [An activity based workflow engine](http://www.tonymarston.net/php-mysql/workflow.html) 206 | * [eZ Workflow component](http://www.ezcomponents.org/docs/api/latest/introduction_Workflow.html) 207 | * [Yii simpleWorkflow extension](http://www.yiiframework.com/extension/simpleworkflow/) 208 | * [Galaxia workflow engine](http://workflow.tikiwiki.org/tiki-index.php?page=homepage) 209 | * [State pattern by Sebastian Bergmann](https://github.com/sebastianbergmann/state) 210 | * [Petrinet Framework](https://github.com/florianv/petrinet) 211 | 212 | ## Licencing 213 | 214 | See the bundled LICENSE file for details. 215 | 216 | ## Sponsors 217 | 218 | * [Alter Way](http://www.alterway.fr) 219 | * [La Ruche Qui Dit Oui !](http://www.laruchequiditoui.fr) 220 | --------------------------------------------------------------------------------