The order cycle state machine

Let's assume you're developing an e-commerce application for the ACME corporation. They sell one product - a tablet. Now the application has to track the steps an orders of such a tablet goes through. A logical way is using a state machine, by implementing one you can move an order back and forward or even reset it completely without difficulty and without losing the history of the product.

Let's work on the model Order first.

<?php

namespace Acme\ECommerce;

use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;

/**
 * @property int $id
 * @property int $user_id
 * @property int $total_ex_vat
 * @property int $total_inc_vat
 * @property string $state
 * @property Carbon $ordered_at
 * @property Carbon $paid_at
 * @property Carbon $delivered_at
 * @property string $return_reason
 * @property Carbon $returned_at
 */
class Order extends Model
{
    // ... relation and other code ..
}

📘

Please note the above sample has a string state column.

The state column is required to get the state machine package working.

The next step is to write down the mapping, which simply lists all transitions and states the order can go through. Important in the state machine is to always have at least one initial state. This is where an order is placed in, if nothing is specified.

📘

If you're having a hard time getting a list of everything up, simply start with the states first.

<?php

namespace Acme\ECommerce\Order\Statemachine;

use Acme\ECommerce\Order;
use Acme\ECommerce\Statemachine\States;
use Acme\ECommerce\Statemachine\Transitions;
use Hyn\Statemachine\Contracts\MachineDefinitionContract;

class Definition implements MachineDefinitionContract
{
    /**
     * Models assigned to this statemachine definition.
     *
     * @return array
     */
    public function models() : array
    {
        return [
            Order::class
        ];
    }

    /**
     * The flow from states to transitions the state machine has to respect.
     *
     * @see https://state-machine.readme.io/v1.0/docs/the-definition
     *
     * @return array
     */
    public function mapping() : array
    {
        return [
            'states' => [
                States\Ordered::class,
                States\Paid::class,
                States\Delivered::class,
                States\Returned::class,
            ],
            'transitions' => [
                Transitions\PaymentProcessed::class,
                Transitions\PaymentCancelled::class,
                Transitions\InTransit::class,
                Transitions\Received::class,
            ]
        ];
    }
}

As you can see the provided example is quite straightforward, let's start with the initial state Ordered.

  • The Ordered state is the state where all orders are initially placed in.
  • From this state, the state machine can try to go through the PaymentProcessed or PaymentCancelled transition. Either of those can be run as an event generated by customer interaction.
  • Assuming the customer payed and you had the state machine move the order through the PaymentProcessed using the below code. The next state it enters is Paid.
<?php

// ..

$machine = new Statemachine($order, $definition);
$machine->moveThrough(new PaymentProcessed($order));

// Or if you don't want to do anything else with the state machine:

(new Statemachine($order, $definition))
	->moveThrough(new PaymentProcessed($order));