Single Responsibility Principle (SRP) with Laravel

The SRP Principle (One Responsibility Principle) is one of the fundamental principles for writing supported code. In this article, I will show how to apply this principle using the example of the PHP language and the Laravel framework.

Often, when describing the MVC development model, unreasonably large tasks are assigned to the controller. Getting parameters, business logic, authorization and response.

Of course, in articles and books this is described as an example, but is often perceived as a call to action in work projects. Such an approach will inevitably lead to uncontrolled class growth and greatly complicate code support.

Principle of shared responsibility


For an example we will take rather rough, but often met type of the "thick" controller.

class OrderController extends Controller
{
    public function create(Request $request)
    {
        $rules = [
            'product_id' => 'required|max:10',
            'count' => 'required|max:10',
        ];
        $validator = Validator::make($request->all(), $rules);
        if ($validator->fails()) {
            return response()->json($validator->errors(), 400);
        }

         //  
        $order = Order::create($request->all());

        //   

        //  sms    

        return response()->json($order, 201);
    }
}

In this example, it is clear that the controller knows too much about “placing an order”, it is also entrusted with the task of notifying the buyer and reserving the goods.

We will strive to ensure that the controller only has to control the entire process and no business logic.

To begin with, we will validate the parameters in a separate Request class.

class OrderRequest extends Request
{
    public function rules(): array
    {
         return [
             'product_id' => 'required|max:10',
             'count' => 'required|max:10',
         ];    
    }
}

And move all business logic to the OrderService class

public class OrderService
{
    public function create(array $params)
    {
        //  

        //  

        //     sms
        $sms = new RuSms($appId);

        //  ,    
        $sms->send($number, $text);
     }
}

Using the Service Container we will implement our service in the controller.

As a result, we have a "thin" controller.

class OrderController extends Controller
{
    protected $service;
	
    public function __construct(OrderService $service)
    {
	$this->service = $service;	
    }

    public function create(OrderRequest $request)
    {
        $this->service->create($request->all());

        return response()->noContent(201);
    }
}

Already better. All business logic has been moved to service. The controller only knows about the OrderRequest and OrderService classes - the minimum set of information necessary to do its job.

But now our service also needs refactoring. Let's take out the logic of sending sms to a separate class.

class SmsRu
{
    protected $appId;

    public function __constructor(string $appId)
    {
        $this->appId = $appId;
    }

    public function send($number, $text): void
    {
          //     
    }
}    

And implement it through the constructor

class OrderService
{
    private $sms;

    public function __construct()
    {
	$this->sms = new SmsRu($appId);	
    }

    public function create(array $params): void
    {
          //  

          //  

          //  ,    
          $this->sms->send($nubmer, $text);
    }
}    

Already better, but the OrderService class still knows too much about sending messages. We may need to replace the messaging provider in the future or add unit tests. We continue refactoring by adding the SmsSender interface , and specify SmsRu through the SmsServiceProvider provider .

interface SmsSenderInterface
{
    public function send($number, $text): void;
}

class SmsServiceProvider implements ServiceProviderInterface
{
    public function register(): void
    {
        $this->app->singleton(SmsSenderInterface::class, function ($app) {
            return new SmsRu($params['app_id']);
        });
    }
}

Now the service is also freed from unnecessary details

class OrderService
{
    private $sms;

    public function __construct(SmsSenderInterface $sms)
    {
	$this->sms = $sms;	
    }

    public function create(): void
    {
          //  

          //  

          //  ,    
          $this->sms->send($nubmer, $text);
    }
}    

Event Driven Architecture


Sending messages is not part of the basic order creation process. The order will be created regardless of the message sent, it is also possible in the future the option of canceling sms notifications will be added. In order not to overload the OrderService with unnecessary notification details, you can use Laravel Observers . A class that will track events with a certain behavior of our Order model and assign to it all the logic of customer notification.

class OrderObserver
{
    private $sms;

    public function __construct(SmsSenderInterface $sms)
    {
	$this->sms = $sms;	
    }

    /**
     * Handle the Order "created" event.
     */
    public function created(Order $order)
    {
        $this->sms->send($nubmer, $text);
    }

Do not forget to register OrderObserver in AppServiceProvider .

Conclusion


I understand that I am describing rather banal things in this post, but I wanted to show how this principle is implemented on the Laravel framework.

The SPR principle is one of the most important and paves the way for the rest. It is required to use, especially since the Laravel framework provides all the necessary tools.

All Articles