Traits
Are an alternative approach to inheritance that solves some limitations of single class inheritance, which PHP uses. Since all Eloquent models extends Model it can not extend another class. This is commonly used to share similar logic across objects. Let's imagine a couple of models have a Company relationship.
trait HasCompany {
public function company() {
return $this->belongsTo(Company::class);
}
}
Now the user can easily share code from the trait, by the keyword using. This is an example and most often a more complex use case would be needed for it to make sense.
class User extends Model {
use HasCompany;
}
Repositories
Repositories is a design pattern to abstract data layers from the application. Your logic should not care about how you store data, so if you wanted to change from Mysql to Mongodb, you would only swap out the repository and not have to change business logic.
Very opinionated here, but this is not a fitting design pattern for Laravel. Laravel has Eloquent and the database layer is therefor already abstracted. Repositories is sometimes used for Laravel applications, but rather an outlier, than a common sight. One of the main reasons for repositories, is to be data implementation agnostic, which already exists and you can swap between SQL servers flawlessly. Also Eloquents features like ::find(), scopes etc. feels like a replacement for Repositories and is quirky to use at the same time.
If you use Doctrine as the ORM, which you can in Laravel, it is the core of their architecture and repositories should be used.
It sometimes occurs that the repository pattern is used to encapsulate business logic, with a similar approach as used with services or action pattern.
Services
Is commonly used for a place to store business logic or the building blocks of your actions in your application. In traditional MVC design, Controllers should only handle input. Normally you would put your logic in Models, but they get "fat" very quickly, when this happens services is a common place to put business logic. Sometimes action or command design pattern are used, which is similar but still different.
One of the core things it solves, is to make your business logic reusable. Imaging filtering all users by an active flag, when you retrieve it in its controller.
public function all() {
return User::where('active', true)->get();
}
Now you have your business logic, that enforces that you only work on active users, later you want to notify all active users with an email, by notifications using a command.
class NotifyUsers extends Command {
public function handle() {
foreach (User::where('active', true)->get() as $user) {
$user->notify();
}
}
}
Now you manually have to keep business logic up to date. Next time you add a second condition or change the logic, you have to change the code in two places. In a big application where this code block is used often, it can make it quite hard to maintain the conditions without forgetting one of the places. If you make a service with this logic, you can easily utilize the same business logic across the application. While one have one place to change the code, if this logic had to change.
class UserService {
public function all() {
return User::where('active', true)->get();
}
}
Everywhere you want to use this business logic getting active users, you can use the service. Therefor only having one place to maintain the logic. A call can be as simply as resolve(UserService::class)->all(). Example of the updated logic with services would be.
// controller
public function all(UserService $userService) {
return $userService->all();
}
// command
class NotifyUsers extends Command {
public function handle(UserService $userService) {
$userService->all()->each->notify();
}
}
Conclusion
The world is not black and white, you have to figure out your own approach. My advice is, do not spend time on Repositories, Laravel has a lot of features to handle data related operations scopes, getters setters etc. that conflicts with the Repository design pattern. See if a service like design approach suits you and you can utilize em. Traits is not as much an architectural design pattern, as it is a Class inheritance alternative, simply to share logic between classes.
php - When to use Repository vs Service vs Trait in Laravel? - Stack Overflow
php - Laravel: Service/Repository Pattern and duplicating code - Stack Overflow
Yet another repository pattern post... Developers that don't use repository pattern and think it's redundant and over-engineering, where do you leave the complex queries of your project at?
Is using the repository pattern best practise?
Videos
I think if you break this down into smaller steps you can achieve DRY architecture. The steps I'm seeing are:
- Create Client
- Create User
- Associate (via pivot table, junction table etc)
To avoid having the dreaded duplicate code you'd create a method around each of these in your service class or classes. You'd then create an action encapsulating all of the steps involved based around these methods.
Don't be scared to implement things outside of your service class - this doesn't mean it is outside of your service layer.
I see registering client interest as an action. You follow synchronous steps to achieve your desired action. So based on methods like creating a user, client etc we can build an action to register client interest, like so:
<?php
class ClientService {
public function addAction(IAction $action)
{
return $action->process();
}
public function createUser() {} // business logic for creating a user.
public function createClient() {} // business logic for creating a client.
public function createAssociation() {} // business logic for creating an association.
}
interface IAction {
public function process();
}
class RegisterClientInterestAction implements IAction {
protected $client;
public function __construct(ClientService $client)
{
$this->client = $client;
}
public function process()
{
$this->createUser()->createClient()->createAssociation();
}
private function createUser() {} // interact with your client service to call the method $client->createUser()
private function createClient() {} // interact with your client service to call the method $client->createClient()
private function createAssociation() {} // interact with your client service to call the method $client->createAssociation()
}
//USAGE
$service = new ClientService;
$results = $service->addAction(new RegisterClientInterestAction($service));
?>
By doing it this way you are able to utilise the createUser etc methods in a new action but without duplicating the code. By having the addAction on the service class you are still executing the business logic inside of your service layer.
If two or more services are required, I'd take a slightly different approach by moving where I would execute the action.
In terms of handling more than one service you can use DI within the constructor of your action.
Like this:
<?php
class Service {
public function addAction(IAction $action)
{
return $action->process();
}
// Other stuff for a base service...
}
class UserService extends Service {
public function createUser() {} // business logic for creating a user.
}
class ClientService extends Service {
public function createClient() {} // business logic for creating a client.
public function createAssociation() {} // business logic for creating an association.
}
interface IAction {
public function process();
}
class RegisterClientInterestAction implements IAction {
protected $client;
protected $service;
public function __construct(ClientService $client, UserService $user)
{
$this->user = $user;
$this->client = $client;
}
public function process()
{
$this->createUser()->createClient()->createAssociation();
}
private function createUser() {} // interact with your user service to call the method $client->createUser()
private function createClient() {} // interact with your client service to call the method $client->createClient()
private function createAssociation() {} // interact with your client service to call the method $client->createAssociation()
}
//USAGE
$service = new Service;
$results = $service->addAction(new RegisterClientInterestAction(new ClientService, new UserService));
?>
What feels best for me is your proposed solution of #2.
What I like to do is build out the two service classes and see what the duplication there is, then refactor/extract any duplication to another class. This way all classes are very testable and you have the least chance of breaking any SOLID principles.