Sitemap
Alessandro D'Orazio

Alessandro D'Orazio

Crafting web solutions with passion and motivation | Laravel | Vue.JS | Python | Master Degree in Computer Science https://alessandrodorazio.it

Laravel
PHP
Coding
Development
Domain Driven Design

How to use actions in Laravel in 10 mins

4 min readFeb 28, 2023

--

When working on software, you have to deal with some business logic. For example, if you are working on a money management app, each user should be able to create a new transaction.

I will write code in PHP (in particular Laravel), but the concept can be applied in almost every language.

I saw many times functionalities added inside a controller, for example, like the following:

<?php

namespace App\Http\Controllers;

use App\Models\Transaction;
use Illuminate\Http\Request;

class TransactionController extends Controller {

public function store(Request $request) {
// get all data from the request
$data = $request->all();
// create the transaction -> business logic
$newTransaction = auth()->user()->transactions()->create($data);
return to_route('transactions.edit', ['id' => $newTransaction->id]);
}

}

This is a very simple logic, but we have some main problems:

  1. If you want to create transactions in another point (for example using another controller or when dispatching a job), you have to copy/paste the code, so it will be less maintainable;
  2. Your controller is managing your business logic, but a controller should be responsible only to control logic by calling models and manage the response to the client (for example through a view or JSON). Adding business logic to the controller can lead to making your code less clear because you are adding responsibilities to the controller.

And what happens if the business logic changes? For example, if each transaction can be associated with a category, every piece of code where you are creating a transaction will change to:

<?php

namespace App\Http\Controllers;

use App\Models\Transaction;
use App\Models\TransactionCategory;
use Illuminate\Http\Request;

class TransactionController extends Controller {

public function store(Request $request) {
// client passes the category as an id
// but we want to use relationships to associate a transaction to a category
$data = $request->except('category');
// getting the category
$categoryId = $request->input('category');
// finding the category
$category = TransactionCategory::findOrFail($categoryId);
// we cannot store immediately the transaction because we have to add the category
$newTransaction = auth()->user()->transactions()->make($data);
// we associate the category with the model
$newTransaction->category()->associate($category);
// we save the model into our database
$newTransaction->save();
return to_route('transactions.edit', ['id' => $newTransaction->id]);
}

}

Our code had a little change, but the data and logic flow changed drastically, and we have to change every code where we can create a transaction.

❌ This clearly is not optimal.

How to deal with business logic?

I think there exist two kinds of approaches:

  1. Adding business logic into your models (e.g. a function storeTransaction into Transaction model);
  2. Creating a class responsible for a single business logic functionality (e.g. StoreTransaction).

Both are good solutions, but the first approach can lead to having models with thousands of lines because they should contain every logic about them.

Get Alessandro D'Orazio’s stories in your inbox

Join Medium for free to get updates from this writer.

Instead, I prefer to create a class for every functionality. This class is an action, so a functionality that does something, no matter what. In our case, it will register a new transaction into the database. I will not talk about SOLID principles, but this concept helps to solve the Single-responsibility principle.

Creating actions in PHP

Basically, you can create a class RegisterNewTransaction that takes data from the controller (or job, command, etc.) and it is responsible only for this logic.

In PHP, you can create a class like this:

<?php

class RegisterNewTransaction {
public static function handle(array $data) {
// code to get the $data array and add it into the database
}
}

and calling the action where you want by using:

<?php
$data = [...];
RegisterNewTransaction::handle($data);

Creating an action in Laravel

There is a really good package called Laravel Actions that helps you to organize the business logic.

In fact, you can create a new class (where you want, but I prefer to add them into app/Actions or app/Domain{DomainName}/Actions when using Domain Driven Design) and use the trait asAction to handle a specific task.

This package is not only capable to manage easily the action concept, but it provides methods to use these actions in various ways, for example using an action as a job, as a single action controller, and so on. I suggest reading the documentation.

But, talking about how to use these actions, the RegisterNewTransaction action will look like this:

<?php

namespace App\Actions;

use App\Models\User;
use App\Models\Transaction;
use App\Models\TransactionCategory;
use Lorisleiva\Actions\Concerns\AsAction;

class RegisterNewTransaction {
use AsAction; // adding the trait
// defining an handle function
public function handle(array $data, int $categoryId, User $user): Transaction {
$category = TransactionCategory::findOrFail($categoryId);
$newTransaction = $user->transactions()->make($data);
$newTransaction->category()->associate($category);
$newTransaction->save();
return $newTransaction;
}
}

You can think we have only moved our code. In fact, this can seem true, but now you can call this class from everywhere. This class is responsible only for one single business logic. The previous controller will be:

<?php

namespace App\Http\Controllers;

use App\Actions\RegisterNewTransaction;
use App\Models\Transaction;
use App\Models\TransactionCategory;
use Illuminate\Http\Request;

class TransactionController extends Controller {

public function store(Request $request) {
$data = $request->except('category');
$categoryId = $request->input('category');
// we use "run" instead of "handle" because with laravel actions
// this function instantiates a new "RegisterNewTransaction" object and
// executes the handle method
$newTransaction = RegisterNewTransaction::run($data, $categoryId, auth()->user);
return to_route('transactions.edit', ['id' => $newTransaction->id]);
}

}

In this way, if your logic changes, you have only to update the action and not each piece of code where you create a new transaction. Furthermore, the code is more clear because you read exactly what’s happening (i.e. registering a new transaction) instead of what your code is technically doing (i.e. finding a category, getting the auth user, and so on).

You learned how to use actions. I think this is a must-have on each application, especially in the case in which a business logic performs multiple actions (forming thus a pipeline of actions).

If you think this article was helpful, you can tip me through this link.

Press enter or click to view image in full size
Laravel
PHP
Coding
Development
Domain Driven Design

--

--

Alessandro D'Orazio
Alessandro D'Orazio

Crafting web solutions with passion and motivation | Laravel | Vue.JS | Python | Master Degree in Computer Science https://alessandrodorazio.it