@elenktik thanks for the quote, that book is in my read-list but I didn't get to it yet.
I think this is more a matter of concepts definition.
My notion of repositories to read and commands (I prefer calling them actions) to write comes from the CQRS architecture pattern:
https://en.wikipedia.org/wiki/Command%E2%80%93query_separation#Command_query_responsibility_segregation
I grew to adopt this pattern as I found it easier to compose actions/commands and test them in isolation.
For example, this is an excerpt from a project I worked on previously:
<?php
namespace App\Actions\Users;
use App\DTO\SystemUserDTO;
use App\Models\User;
use Illuminate\Database\ConnectionInterface;
class CreateSystemUser
{
private ConnectionInterface $connection;
private CreateUser $userCreator;
private UpdateSystemUser $userUpdater;
private GrantSystemAccess $accessGranter;
public function __construct(
ConnectionInterface $connection,
CreateUser $userCreator, // another action
UpdateSystemUser $userUpdater, // another action
GrantSystemAccess $accessGranter // another action
) {
$this->connection = $connection;
$this->userCreator = $userCreator;
$this->userUpdater = $userUpdater;
$this->accessGranter = $accessGranter;
}
public function perform(SystemUserDTO $dto): User
{
$user = User::query()->firstWhere('email', $dto->email);
if ($user) {
// defer to more specialized action
return $this->userUpdater->perform($user, $dto);
}
return $this->connection->transaction(function () use ($user, $dto) {
$user = $this->userCreator->perform($dto);
return $this->accessGranter->perform($dto->role, $user);
});
}
}
You can see here I am composing different actions to build a more specific one.
In this code, for example, the GrantSystemAccess class is responsible for sending a welcome email if it is a new user or if the user was granted a new role.
When testing, I can test the actions that actually interact with the database separately, and test this is one using mocks from the others and asserting the expected methods were called.
When composing actions like this, the constructor dependencies are resolved from the container. So it is easy to swap implementation of dependencies.
In a controller I call this action like this:
public function store(SystemUserRequest $request, CreateSystemUser $action): RedirectResponse
{
$user = $action->perform($request->dto());
// removed flash message and simplified redirect details
// to preserve client's info
return redirect()->back();
}
Also some actions that are composed here are used directly in other parts of the system, for example from an API webhook that is only called for existing users and calls GrantSystemAccess (and its counter-part RevokeSystemAccess) directly.
Another example is from a CSV import ran from an artisan command, which calls a different composition of actions.
On the other hand, I have Repository classes that only care about data retrieval and adding constraints to the SELECT queries (where clauses, group by's, having clauses, etc.)
Of course I could have all these actions on a single "Repository" (quoted to differentiate the concept) class. But, in my opinion, having the commands separated allows for more composable code.
In the end of the day, it is a matter on finding the architecture that "clicks" best for you, and allows you to understand and maintain the code in the best way.
Hope this helps somehow.
And I apologize for not knowing before hand the same name was used for a different concept, and implying your concept might be wrong. My intention was the best.
Have a nice day =)