Service, Model and Repository: where should logic, validations and possible external communication be?

Asked

Viewed 717 times

1

I’m doing a small project for learning purposes using Laravel, this system revolves around two "models", Conta and Transacao.

one Conta has name and balance, a Transacao has count_source, count_destination, value and details.

I created repositories and services for the two "Models" to encapsulate communication with Eloquent and Controller.

Sample structure of Transacao

class TransactionController extends Controller
{
   
    public function transfer(Request $request, TransactionService $transactionService)
    {
      
      try {

            $transactionService->transfer(
                $request->from, $request->to, $request->amount, $request->details
            );

            return response()->json(['success' => 'true']);

        } catch (\Throwable $error) {

            return response()->json([
                'success' => 'false',
                'errors' => $error
            ], Response::BAD_REQUEST);
        }
    }

}
class TransactionService
    {
    
        public function findById(int $id, TransactionRepositoryContract $TransactionRepository)
        {
            return $TransactionRepository->findById($id);
        }
    
        public function transfer($from, $to, $amount, $details)
        {
            $TransactionEloquentRepository = new TransactionEloquentRepository;
            $AccountEloquentRepository = new AccountEloquentRepository;
    
            $fromAccount = $AccountEloquentRepository->findById($to);
    
            if($fromAccount->getAmount() < $amount) {
                throw new Exception('source account does not have enough balance');
            }
    
            //db transaction
    
            $TransactionEloquentRepository->create([
                'from' => $from
                'to' => $to,
                'amount' => $amount,
                'details' => $details
            ]);
    
            $AccountEloquentRepository->update($from, [
                'balance' => $fromAccount->balance - $amount
            ]);
    
            //db end transaction
        }
    }

The $TransactionEloquentRepository basically just encapsulates a few calls to Eloquent.

The doubts are as follows:

1 - Is this implementation correct? the service that should handle these validations (check if an account is valid, if it has sufficient balance, create balance, decrease balance etc) and can communicate with external repositories?

1.1 - The Laravel has the validation with Formrequest, but this validation must also be in the service?

2 - In case of failure at any stage of this process, who should launch exceptions and treat exceptions? only the controller should handle?

3 - Is it wrong to use Eloquent transactions in Repositories? since considering abstraction, not all banks have the concept of transactions

1 answer

2

I’ll give you some of my experience!

I don’t believe that there are totally right or wrong approaches, there are different ways to implement and each one will have its pros and cons.

First point that I would change, would be the Repository layer, because you are instantiating it directly in the transfer method, and you are calling directly the Repositoryeloquent, for me, the ideal is that you put the Repository in the Service constructor and do not use it directly but an interface, because you would have the flexibility in the future to implement with other situations, such as an API call to a third-party system, for example, hence you would treat the dependency injection Bind on a Repositoryserviceprovider for example, informing that Transactionrepositoryinterface will use Transactionrepositoryeloquent, another important point you gain by using dependency injection is that when you create the unit tests, it will be simpler for you to create a Repositories MOCK so you can test only the Transactionservice rule.

As for validations, I usually separate in validations of Data Inputs and Business Rules validations.

The validations of data inputs, I create a class of Validator, example Transactionvalidator and inject it into the service and call the validation as soon as I enter the transfer method, Remembering to follow the same principle of using Interface in case you need to implement other types of validations, it will only be change the dependency injection.

As for business rule validations, it depends on the project, sometimes stay in the Validator class that is separate or within Transactionservice itself, it will depend on what I think makes more sense for the understanding at that time, if it’s something I don’t believe can be modified, it usually stays within the Service itself, if it’s something I think can be changed in the future, having to put in the external Validator class.

I don’t usually use Laravel Formrequest because I usually use Services in other contexts, such as CLI calls, Integrations via Websocket, Crons, etc, where an HTTP request doesn’t happen, so I think the Formrequest validation limits you to this, so I create the separate Validator class and inject it into the Service, which by both the Controller, CLI, Cron, etc., in which the Service is called, it will perform the validation.

As for the exception part, I usually launch when I have some validation problem and I let the Controller, CLI, Cron, etc validate this, because I can have a Controller that calls several Services, or a Service that calls more other Services, then let the border handle the Exceptions to know what will return, it ends up being better, because it can return a different HTTP CODE status, or send a log message in the CLI, etc.

The same principle I follow for processing transaction in the database, I always leave to the edge to do this processing, ie Controller, Command, Job, Listener, etc. PS. In the case of Job and Listener, I only let them handle, when they are asynchronous, otherwise, as they are not the application’s edge, I will not use them, I will use in Controller, Command, etc

And as for using bank transactions in Repository, I don’t usually use, due to the above explanations of how I use transaction.

I hope I helped! A hug!

  • Thank you very much for the answer, a question, in the case of transfer, would I inject the two repository interfaces into the Construct? because not all methods will use the Accountrepository. Or should I create a specific Service pro Transfer, and inject the two repository interfaces into it? Being: Transactioncontroller.transfer -> Transferservice -> Transaction and Account Repository in __Construct, and within Transferservice, another method, makeTransfer, which would handle these rules and release exceptions, good approach?

Browser other questions tagged

You are not signed in. Login or sign up in order to post.