Skip to content

Repository files navigation

Workflow Component Build Status

This component provides a workflow engine written as a PHP library.

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.

Nodes

A node represents a point in a life cycle. The Node class implements the concept. A node is referenced by a unique name across the workflow. The constraint is the responsibility of NodeMap class.

Transitions

A transition is a link between nodes. The Transition class implements the concept. At creation, a transition is given a specification object implementing the SpecificationInterface. the specification is used as a business rule to decide where to advance in the workflow.

Tokens

A token is a simple string used to initialize the workflow in a particular node. The idea is to consider the token as a thing placed at the center of a node. When workflow engine is on, the token is moving from node to node.

Events

An event is an object created each time a token arrives at a node. The Event class implements the concept. This class extends the Event class from the Symfony EventDispatcher component. You can write listeners or subscribers to implement any business behaviour.

Usage

Let's say you are writing a blog engine in PHP and you want to implement the following workflow:

  • an article begins its existence as a draft
  • when ready, the article gets published
  • if controversial, the article is deleted
  • when too old, the article is archived

First of all, you need to write classes implementing SpecificationInterface for every business rule:

namespaceBlogEngine\Domain\Specification;
useAlterway\Component\Workflow\ContextInterface;
useAlterway\Component\Workflow\SpecificationInterface;
class DraftableArticleSpecification implements SpecificationInterface
{
publicfunctionisSatisfiedBy(ContextInterface$context)
{
// an article can always be draftedreturntrue;
}
}
class PublishableArticleSpecification implements SpecificationInterface
{
publicfunctionisSatisfiedBy(ContextInterface$context)
{
// an article needs two reviews to be publishedreturn1 < count($context->get('article')->getReviews());
}
}
class DeletableArticleSpecification implements SpecificationInterface
{
publicfunctionisSatisfiedBy(ContextInterface$context)
{
// an article can always be deleted if requestedreturn'delete' === $context->get('action');
}
}
class ArchivableArticleSpecification implements SpecificationInterface
{
publicfunctionisSatisfiedBy(ContextInterface$context)
{
// an article needs to be one month old to be archived$publishedAtPlusOneMonth = clone$context->get('publishedAt');
$publishedAtPlusOneMonth->modify('+1 month');
return'archive' === $context->get('action') && $publishedAtPlusOneMonth < $context->get('now');
}
}

Then, you can use the Builder class and the specifications to describe the workflow:

namespaceBlogEngine\Domain\Service;
useAlterway\Component\Workflow\Builder;
useAlterway\Component\Workflow\ContextInterface;
useBlogEngine\Domain\Event\ArticleSubscriber;
useBlogEngine\Domain\Specification\DraftableArticleSpecification;
useBlogEngine\Domain\Specification\PublishableArticleSpecification;
useBlogEngine\Domain\Specification\DeletableArticleSpecification;
useBlogEngine\Domain\Specification\ArchivableArticleSpecification;
useBlogEngine\Util\Context;
useSymfony\Component\EventDispatcher\EventDispatcherInterface;
class ArticleService
{
private$workflow;
publicfunction__construct(EventDispatcherInterface$eventDispatcher)
{
$this->workflow = (newBuilder($eventDispatcher))
->open('article.draft', newDraftableArticleSpecification())
->link('article.draft', 'article.published', newPublishableArticleSpecification())
->link('article.published', 'article.deleted', newDeletableArticleSpecification())
->link('article.published', 'article.archived', newArchivableArticleSpecification())
->getWorkflow();
$eventDispatcher->addSubscriber(newArticleSubscriber());
}
publicfunctioncreate(Article$article)
{
$this->advance($article, newContext());
}
publicfunctionpublish(Article$article)
{
$context = newContext();
$context->set('article', $article);
$this->advance($article, $context);
}
publicfunctiondelete(Article$article)
{
$context = newContext();
$context->set('action', 'delete');
$this->advance($article, $context);
}
publicfunctionarchive(Article$article)
{
$context = newContext();
$context->set('action', 'archive');
$context->set('publishedAt', $article->getPublishedAt());
$context->set('now', new \DateTime());
$this->advance($article, $context);
}
privatefunctionadvance($article, ContextInterface$context)
{
try {
$this->workflow->initialize($article->getToken())->next($context);
} catch (\LogicException$e) {
// the workflow reports a problem
}
}
}

Finally, you have to listen on events dispatched by the workflow to attach the business behavior:

namespaceBlogEngine\Domain\Event;
useAlterway\Component\Workflow\Event;
useSymfony\Component\EventDispatcher\EventSubscriberInterface;
class ArticleSubscriber implements EventSubscriberInterface
{
publicstaticfunctiongetSubscribedEvents()
{
returnarray(
'article.draft' => array('onDraft', 0),
'article.published' => array('onPublished', 0),
'article.deleted' => array('onDeleted', 0),
'article.archived' => array('onArchived', 0),
);
}
publicfunctiononDraft(Event$event) { /* ... */ }
publicfunctiononPublished(Event$event) { /* ... */ }
publicfunctiononDeleted(Event$event) { /* ... */ }
publicfunctiononArchived(Event$event) { /* ... */ }
}

Contributing

Pretty please, with sugar on top, phpspec specifications are provided and should be green when contributing code.

References

Theory

PHP

Licencing

See the bundled LICENSE file for details.

Sponsors

About

Workflow component heavily used by Alter Way

Resources

Stars

65 stars

Watchers

14 watching

Forks

Releases

Packages

Used by

Contributors

Languages