"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.