How to use actions to organize your logic in an amazing way

How to use actions to organize your logic in an amazing way

We’ve all been there, you need to create some new functionality for your application, but you dont know where to put it. I used to create several service classes based on the entities in my application. For example I usually create a UserService which contains all logic related to my User entity. But what if I need to implement functionality that spans multiple entities? Where do I put that functionality? After seeing a talk by Luke Downing at Laracon Online Winter ’22 called ‘Actions are a dev’s best friend’ I’ve found my solution: the action paradigm.

Organise logic by functionality

When you use the action paradigm you’ll organise your logic by functionality instead of your entities. For each action a user of your application can do you’ll create an action in your code base. For example when a user enters their data into a form to register an account, that is an action. When a user clicks on the account activation link, that’s another action. When a user creates a product, another action. I think you get the gist of it.

One of the main benefits of using actions is that your classes will be smaller and easier to test. An action will contain exactly one piece of functionality, therefore you only have to create tests for that functionality. You can also add your validation rules for that action inside it, so your validation is coupled to the functionality provided by the action. This ensures the data processed is always validated correctly for the action to work properly.

Another great benefit is you can reuse the actions. For example you can use the RegisterUser action from a controller where a user enters their information. But you can call the same action from a command to register a user from the command line. Because the validation logic is inside the action there is no duplicate code.

Useful library for using actions

In my first SaaS, Data Blade, I’ve organised my logic using the action paradigm. Data Blade is written using the Laravel Framework and because of this I’ve been using the Laravel Actions library. This library makes it really easy to write and use your actions in different places in your application. When you want to run an action you can do this using just one line of code and without the need of injecting the action in your controller. You can just use $user = RegisterUser::run($data); to register your user. The library will automatically call the handle method of the action.

Another great feature of the Laravel Actions library is that you can directly use the actions as a controller or command by implementing the asController or asCommand method.

Using actions in a real world example

In the previous paragraphs I’ve talked about a RegisterUser action. So let me show the action to you.

use App\Models\Enums\Roles;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Lorisleiva\Actions\Concerns\AsAction;

class RegisterUser
{
    use AsAction;

    /**
     * @param  array  $data
     * @return User
     */
    public function handle(array $data): User
    {
        $data['role'] = Roles::USER;
        $data['password'] = Hash::make($data['password']);
        $user = User::create($data);

        return $user;
    }
}
Code language: PHP (php)

This class registers a new user in the database. It’s a really basic action and that’s it’s power. A sharp eye might have noticed it’s missing any form of validation, so let’s add the validation rules to the class.

use App\Models\Enums\Roles;
use App\Models\User;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Hash;
use Lorisleiva\Actions\Concerns\AsAction;

class RegisterUser
{
    use AsAction;

    /**
     * @param  array  $data
     * @return User
     */
    public function handle(array $data): User
    {
        $this->validate($data);
        $data['role'] = Roles::USER;
        $data['password'] = Hash::make($data['password']);
        $user = User::create($data);

        return $user;
    }

    /**
     * @param  array  $data
     * @return bool
     */
    private function validate(array $data): bool
    {
        \Validator::validate($data, [
            'given_name' => ['required'],
            'family_name' => ['required'],
            'email' => ['required', 'email', 'unique:users'],
            'password' => ['required'],
        ]);

        return true;
    }
}
Code language: PHP (php)

Now I’ve added a validate method which I will call in our handle method. I use the Validator functionality to validate our data and make sure everything is in order before we register our user. Because I use Filament to build and render my pages I don’t have to register this action as a controller since I can just call the action from the Filament page.

But I’d like to be able to register a user from the commandline manually, so let’s add that functionality to the action.

use App\Models\Enums\Roles;
use App\Models\User;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Hash;
use Lorisleiva\Actions\Concerns\AsAction;

class RegisterUser
{
    use AsAction;

    public string $commandSignature = 'app:users:register {given_name?} {family_name?} {email?} {password?}';

    public string $commandDescription = 'Registers a new user in the application';

    /**
     * @param  array  $data
     * @return User
     */
    public function handle(array $data): User
    {
        $this->validate($data);
        $data['role'] = Roles::USER;
        $data['password'] = Hash::make($data['password']);
        $user = User::create($data);

        return $user;
    }

    /**
     * @param  Command  $command
     * @return void
     */
    public function asCommand(Command $command): void
    {
        try {
            $user = $this->handle([
                'given_name' => $command->argument('given_name') ?? $command->ask('Whats the users given name?'),
                'family_name' => $command->argument('family_name') ?? $command->ask('Whats the users family name?'),
                'email' => $command->argument('email') ?? $command->ask('Whats the users emailaddress?'),
                'password' => $command->argument('password') ?? $command->ask('Whats the users password?'),
            ]);
            $command->info(sprintf('User %s %s (%s) has been registered with id "%s"', $user->given_name, $user->family_name, $user->email, $user->id));
        } catch (\Exception $e) {
            $command->error(sprintf('The following error has occurred: %s', $e->getMessage()));
        }
    }

    /**
     * @param  array  $data
     * @return bool
     */
    private function validate(array $data): bool
    {
        \Validator::validate($data, [
            'given_name' => ['required'],
            'family_name' => ['required'],
            'email' => ['required', 'email', 'unique:users'],
            'password' => ['required'],
        ]);

        return true;
    }
}
Code language: PHP (php)

To make this action a command I need to tell the Laravel Actions library what command I’d like to type to run this action. I do this by adding a public property called $commandSignature. In this case I can run php artisan app:users:register to call this action. The public property $commandDescription contains the description of the command. This description is shown to the user when you just list all available commands using php artisan.

Now for the fun part, processing the command input and calling our handle method. To do this I’ve added the public method asCommand. This method is executed when you execute the command specified in the signature. As a parameter you’ll receive an instance of the Command class. This instance is used to fetch the arguments given when you’ve executed the command. For this action all of the arguments are optional, as denoted by the questionmarks in the signature. So when I fetch the nessecary data from the command I’ll check if there is any for the argument, or I ask the user to enter the data.

Using this data I build the array for the handle method and I call the handle method. After everything checked out, remember, the data is validated in the handle method, the user is created and a success message is shown to the user. When an error occurred I simply show the exception to the user.

Final thoughts

As I’ve shown you, actions are a really nice and easy way to organize your application logic by functionality. What do you think of this method? Do you like it, or do you have any suggestions for other methods of ordering your application logic? Let me know in the comments below or on Twitter.

Leave a Reply

Your email address will not be published. Required fields are marked *