"Action Design Pattern" in Laravel

Jul 2022

"Action Design Pattern" is a simple but clean and powerful way to reuse business logic in Laravel.

Let's start with a server management application. A user has servers, and one day, the user wants to archive a server. A controller to do that might be:

namespace App\Http\Controllers;

use App\Models\Server;

class ArchivedServersController extends Controller
{
    public function store(Server $server)
    {
        $this->authorize('update', $server);

        $server->markAsArchived();

        $server->owner->notify(new ServerArchivedNotification($server));

        return $server;
    }
}

This is a typical Laravel way. The business logic is hidden in the model like this:

class Server extends Model
{
    public function markAsArchived()
    {
        return tap($this)->update([
            'archived_at' => now(),
        ]);
    }
}

I love this way. Simple and clean, isn't it?

Now let's say the application wants to scan and archive servers that have been inactive for a long time. A command to do that would be:

class ArchiveInactiveServers extends Command
{
    protected $signature = 'archive-inactive-servers';

    public function handle()
    {
        Server::inactiveForALongTime()
            ->get()
            ->each(function ($server) {
                $server->markAsArchived();

                $server->owner->notify(new ServerArchivedNotification($server));
            });
    }
}

Honestly, from my perspective, it's still good enough to leave it as above.

Now, the business grows, and for some reasons, the application needs to do a couple of things to prepare and finalize before/after the server is marked as archived.

$server->prepareToArchieve();

$server->markAsArchived();

$server->doSomethingToFinalise();

$server->owner->notify(new ServerArchivedNotification($server));

Now with each upcoming update, you need to repeat it twice: in the controller and in the command. It's not mandatory, but you might consider extracting the business logic.

Let's move the logic to a dedicated class called ArchiveServer:

namespace App\Actions;

use App\Models\Server;
use App\Notifications\ServerArchivedNotification;

class ArchiveServer
{
    public function archive(Server $server)
    {
        $server->prepareToArchieve();

        $server->markAsArchived();

        $server->doSomethingToFinalise();

        $server->owner->notify(new ServerArchivedNotification($server));
    }
}

We call these classes "actions". An action is a simple class with only one public method which handles the business logic. You can name it whatever you want: the verb of the action, handle, or even __invoke to call it directly as a function. It depends on your taste.

Now you can execute the action via the app helper:

class ArchivedServersController extends Controller
{
    public function store(Server $server)
    {
        $this->authorize('update', $server);

        app(ArchieveServer::class)->archive($server);

        return $server;
    }
}
class ArchiveInactiveServers extends Command
{
    protected $signature = 'archive-inactive-servers';

    public function handle()
    {
        Server::inactiveForALongTime()
            ->get()
            ->each(fn ($server) => app(ArchieveServer::class)->archive($server));
    }
}

Or use dependency injection if you'd like:

class ArchivedServersController extends Controller
{
    public function store(ArchieveServer $archiveServer, Server $server)
    {
        $this->authorize('update', $server);

        $archiveServer->archive($server);

        return $server;
    }
}

Even shorter if you use __invoke function for the action class:

class ArchivedServersController extends Controller
{
    public function store(ArchieveServer $archiveServer, Server $server)
    {
        $this->authorize('update', $server);

        $archiveServer($server);

        return $server;
    }
}

In my opinion, all look good! Which way you choose to use depends on your taste.

Some questions you might be concerned about:

Should the action class need a contract interface? - Unless you are developing a public package, no. Even then, consider abstracting things carefully. Remember two of the most important principles, in my opinion: KISS (keep it simple, stupid) and YAGNI (You Ain't Gonna Need It).

Seems there are some packages available for actions, should I use them? - No. Why? See the two principles above. Even with this action design pattern, only consider using it when you know you need it.

My Newsletter

I send out an email every so often about cool stuff I'm working on or launching. If you dig, go ahead and sign up!

    Follow the RSS Feed.