Ipeleng Molete
The Home of Ipelatech's Blog

The Home of Ipelatech's Blog

Ipela-Marshal

Ipela-Marshal

A simple State Machine

Ipeleng Molete's photo
Ipeleng Molete
·Nov 23, 2021·

7 min read

Table of contents

  • Introduction
  • First steps
  • The Promised (Code)Land
  • The Library
  • Installation
  • Usage/Examples
  • Conclusion

Introduction

Marshal is a simple state machine for managing workflows. It embodies the spirit of our Open Source philosophy by providing simple, extendable solutions for intended use-cases. With Marshal, you can create 2 types of State Machine - Deterministic and Non-Deterministic. You can download it here.

The pain

Managing complex processes isn't a trivial task in development however there are many good solutions to the problem. The State Machine and related State Pattern are very good for these types of problems, and there are a some good libraries out there (for PHP but I'm sure for other languages as well) to help the aspiring developer out of a very annyogin hole to fall into.

However, as the owner of possibly the Simplest Brain in the World, a lot of these fall short of helping me, either because they're relatively annoying to set up - I'm allergic to configs of third-party solutions that have more that 5 fields to change! Of course, I'll make exceptions but in most part, once a package passes this threshold I move right along! - or they have so many features that I won't use (this is actually part of a larger problem I'll speak about in another article, where devs try to make solutions that cover not just the use-case but almost every other possible edge case as well).

Unable to find a happy medium, I decided to create one.

First steps

The solution was Marshal. The first version was designed to be a hierarchical State Machine, one with a State that had children States. The reasoning was I was building an Auditing project where users could answer questions in a survey-like fashion and on completion, the other ancestor States had to be contacted. I, very unwisely in hindsight, chose a tree to be the main data structure powering this State Machine.

I technically don't want talk about this version because the code to create it came out so ugly, looking at it could constitute self-harm and a possible need to contact Interpol and the UN about human rights abuses!

It did help, however, teach me the importance about Data Structures and Design Patterns and that you need to pick the right tool for the job. I meandered about for a while without really making much progress and eventually left the project abandonded until a recent breakthrough gave it new life. Another concept I picked up here is contraint. The original machine had only one transition (so no tables needed) - this would come in handy later.

The Promised (Code)Land

You'll come across this a lot in my writing - simplicity - and I reached this golden milestone with the project when I connected it with another concept I use/love in Shepherd - the Array!

In my opinion, it's probably one of the most powerful data structures because you can do so much with it, especially in a dynamic language like PHP. The second you need to group objects (especially in a list or table) or look up something, your base data structure is an array or array-like data structure (list, dictionary, hash table, etc). PHP makes all these a dawdle to work with by using the associative array (if you don't know what that is - it's a dictionary).

And that's all a State Machine is - a group of States you can look up with a transition table. You can make State Machines without transition tables, they'll have implicit transitions stored inside them. In fact, this was what I was initially going to do for this, however, I had the self-imposed requirement that a user should be able to see the entire structure of the table in one place.

I started thinking about all the times I needed something like this and saw the pattern that sometimes I needed to branch and sometimes I didn't and that those two needs needed similar but very different approaches. But turns out the array can handle both just fine.

The Library

Features

Ability to create:

  • Deterministic, and
  • Non-Deterministic State Machines

Deterministic State Machines

Deterministic machines in this context, are those defined with an array with integer keys (i.e. a regular array) and the values are the States. States are executed one after the other and there's no branching. There is one transition, a default method called execute, which is configurable. What this means is that in your State class, you define a method called on_execute for the Runner to call. When you define a transition in the Process Definition, you define it without the "on_" prefix, the Runner prefixes the "on_". The reason being, there's a class to get transitions from the Process Definition; if you're to pass those to the front-end, say you want to give a user two transitions, say, "Go" and "Stay", you don't want on_go and on_stay but as a dev, I like the method names to be like on_go because it feels like it communicates intent better.

Non-Deterministic State Machines

Non-Deterministic machines are defined by an associative array, where the State is the key and the transitions and the new states they return are defined as an array. You don't define the branching logic inside the State Machine itself, you just pass in the current state and desired transition, like you would with most of the libraries I've used.

Installation

Install with composer

  composer require ipelatech/ipela-marshal

Concepts

  • Runner The class that runs the State Machine defined in the Process Definition. Internally, it checks whether the Process Definition is an associative array, then picks the correct specific Runner between the two below.

    • Methods:
      • public static function run(RunnerArgs $args)
  • RunnerDeterministic

    The Runner that runs Deterministic State Machines.

    • Methods:
      • public static function run(RunnerArgs $args)
  • RunnerNonDeterministic

    The Runner that runs NonDeterministic State Machines.

    • Methods:
      • public static function run(RunnerArgs $args)
  • Process Definition

    A class that implements IProcessDefinition and contains your Process Definition. There's only one method to implement, get_definition which returns an array.

  • State

A class that extends IState and contains all the transition methods. Deterministic States will have one method - by default, execute, Non-Deterministic will have as many as you want.

Convenience Classes

  • Associative Array Checker

Checks whether an array is associative or not.

- Methods:
    - check(array $array) : bool
  • StateTransitionGetter

Gets the valid transitions from a State in a Non-Deterministic State Machine.

- Methods
    - get(IProcessDefinition $process_definition,
    string $current_state = null) : string or array

Note, for Deterministic State Machines, you do not pass a current state.

  • ValidStateChecker

Checks that the full qualified class name you pass in implements IState.

- Methods: 
    - check(string $class) : bool

Usage/Examples

The generalised workflow for working with State Machines is:

  1. Create a Process Definition that implements IProcessDefinition. Define your machine in get_definition as an array and return it
  2. Create State/s that extend IState. Each state will contain the Transition Name/s you wish to use.
  3. Call Runner::run() and pass in the arguments. The returned value will be the next state class, or false if there are no more states.

Remember the one transition concept from earlier? That approach is how I deal with the Deterministic State Machine in the library. The transition method name is implicit, so you never have to worry about it in the Definition. In your code, define the State Machine like so:

The rest is in the documentation.

Conclusion

There is a roadmap for this project, I don't feel it's feature complete quite yet. For one, it'd be nice to have a way to visualise State Machinces. The next would be to have before and after lifecycle hooks. I think after that, I'll call it feature-complete and just work on updating dependencies as the need arises.

With this simple tool, I've been able to build any kind of workflow I need to in my projects. It's deeply satisfying to come up with simple solutions to complex problems and share them with the world. I hope this library is as handy to you as is it to me.

Thanks for dropping by. Le saleng hanthle!

Cover image

 
Share this