Simplify your complex workflows in your projects developed with Laravel with the `caner/state-machine` package

Caner Ergez
8 min readJan 24, 2022

--

Today we will see how to make your projects with complex workflows developed with Laravel simpler and more controllable. Although there are many ways to do this, the method we will use is to understand the State Machine approach and see how much the caner/state-machine package will make our job easier.

What is State Machine?

In simple terms, state machines are a model of behavior that consists of a finite number of states, transitions between states, and combinations of actions. In other words, it is an approach that allows us to switch between these states in conditions where we have more than one, but a certain number of states.

We call each state transition process a Transition. For example, when we want to do a transition from ‘AState’ to ‘BState’, it would make sense to run a Transition with the name ‘AToBTransition’ here. The image below is a visual representation of this.

A State to B State chage process.

Transition structure can be formatted according to your needs. In the package that we will work on, the transition structure consists of 3 parts;

1-) Guards: Classes that run before the state transition takes place and check whether the appropriate state exists for the state transition.
2-) Action: The function where the state transition and simultaneous tasks are done.
3-) AfterActions: Classes with jobs that need to run after the state transition is over

When the Transitions start to run, they first run the defined Guards respectively. If a positive result is returned, each Guard assumes that the state can be run and runs to the ‘Action’ operation. If any guard returns a negative result, the process is stopped and any changes made are rolled back.

Action is the main part of the state change. After this function successfully runs, Transition starts executing the ‘AfterActions’ that should run. If there is a problem with the ‘Action’ sequence, existing operations are stopped and any changes made are rolled back.

AfterActions run sequentially just like guards. But there is an important difference.

In some cases, the actions taken in AfterAction can be quite important. For example, sending an after-sales invoice to your customer. In this case, if there is a problem creating the invoice, you may need to roll back all the changes. You should use the synchronous working method in your transactions for this purpose. So, in case of throw an error in the operation, the system will stop working and all changes made in ‘Guards’ and ‘Action’ will be rolled back.

But in some cases, ‘AfterAction’ giving an error may not be a situation that will interrupt the progress of the process for you. For example, sending a notification for the system. In this case, you should run your ‘AfterActions’ with an asynchronous working method. In this case, even if an error occurs in asynchronous running processes, your migration will be completed without any problems.

The working scheme of the transitions is shown in the image below.

Transitions working scheme

Now let’s continue our article with our sample project. First, we need to install Laravel. You should use PHP 8 and above, Laravel 8 and above. You can click here for the installation guide.

Our second process will be to install the caner/state-machine package, which is the package that provides the State Machine structure and facilitates our operations. For this, you must enter your project directory and run the following command.

composer require caner/state-machine

With this command, the most up-to-date and suitable version of the package will be installed.

Our third action should be to add the config file of the package to our home directory. The command to run for this is as follows:

php artisan vendor:publish --tag=caner-state-machine-config

Laravel 8 will identify the ‘provider’ file of the package with its auto discovery capability. However, if auto discovery does not work in possible cases, you will need to add the code below to the providersarray in the config/app.php file.

\Caner\StateMachine\StateMachineServiceProvider::class,

You have successfully completed the installation of the package. We can now prepare our system for an example.

For example, let’s try we’re writing a ‘blog’ system. We can start by creating the necessary migration, model and controller for this.

php artisan make:model Post -mc

This command will automatically create the ‘PostModel, Migration and Controller.

Let’s assume that our migration file is as follows and we migrate it.

Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->longText('content');
$table->integer('status')->default(0);
$table->timestamps();
});

Let’s keep the states that will exist in the ‘status’ version of our post model in a class as Constants. Let’s have our App/Enums/PostStatus.php file as follows;

class PostStatus
{
const DRAFT = 0;
const NEED_CHANGES = 1;
const APPROVED = 2;
}

In the next step, we need to write the necessary codes for our State Machine. For this, you may need to review the links below. The rest of the article will proceed assuming that you have read the links and configured your State Machine system accordingly. You can review the sample project here to see the places you do not understand and wonder how to do it.

You can create your State Machine suitable for the conditions you want.

We will examine how we can create a model record with the codes on the sample project and a simple State Machine and how we can make a simple transition.

You can also access the demo project here, review the sample codes and make them suitable for your own project.

In the Laravel API standard, the store and update functions are used to create and update data. In order not to break this standard, we can add the following to our web.php file.

Route::resource('post', PostController::class)->only(['store', 'update']);

Now let’s write our store function.

public function store(Request $request)
{
$postStateMachine = new PostStateMachine();
//$postStateMachine->getPossibleTransitions();
$postStateMachine->transitionTo(DraftState::class, $request);
return response()->json(['success' => true]);
}

This code will simply generate a new PostStateMachineclass when a store request arrives.

While your State Machine is in any state, you can see the list of transitions you can use with the getPossibleTransitions() method.

You can start the State change process with the transitionTo() method on your State Machine. In this case, State Machine will find the appropriate Transition and initiate the migration. Transition suitable for this process in the demo project is shown in the picture below.

UnCreatedToDraftTransition will work in this process. You can view the code inside this class here. Also, in the sample project, a Guard, a SyncAfterAction and an ASyncAfterAction operation are included in this Transition for your better understanding.

After the state change is complete, your record will now appear as a DraftState. You can see a screenshot of the sample request below.

Now let’s do an example State transition;

We can do the update method of PostController as follows.

public function update(Post $post, Request $request)
{
$exampleData = ['foo' => 'bar'];
$post->state(PostStateMachine::class, 'status')
->transitionTo($request->target, $request, $exampleData);
}

Let’s say we made your update request with the Model we just created. In order to change the state after receiving the data of the post model, we must first get the state in which this model is. For this, after adding the HasState trait that comes in the package to our model, we can learn which state our model is in with the state() method.

$model->state(YourStateMachine::class, 'your_attribute');

The important point here is that you define the correct State Machine in the first parameter of the state() method and enter the correct model attribute to return the state value in the second parameter. In the given example, the first parameter is PostStateMachine::class and the model attribute is status.

The state() method will return us a DraftStatein this example. Then you can start a change to the desired State with the transitionTo() method.

$state->transitionTo(TargetState::class, $request, $data);

Now let’s examine the request we prepared in the sample project for the state transition.

We can use the targetvalue given in this request as the 1st parameter in the transitionTo() method. This is the same as writing ApproveState::class. In the example project, it is shown that state transition can be made with different requests without the need to change the update() function.

The above request will find and run the Transition needed to migrate your Model from DraftStateto ApprovedState.

Your Model will transition to ApprovedState when this request is submitted. You can find Transition working during this transition here.

Now let’s try the same operation in a State change operation that is not possible to pass. For example, when we want to transition our model from ApprovedStateto NeedChangesState, it will throw a TransitionNotFoundExceptionbecause such a transition is not defined in our State Machine.

As you can see in the image above, a transition from ApprovedStateto NeedChangesStateis not defined. In this case, you will get a response like the one below.

Note that the demo project has a simple logic. You can use this package for more complex operations, send us the problems you find in the Github Issues section and contribute to the development process of the package.

You can click here to reach the Github page of the package.

In this article, we briefly examined the State Machine concept and learned how to do this easily with the caner/state-machine package.

--

--