A little more about the service layer in PHP

In the life of every developer, there comes a time when one understanding of popular patterns and rules for writing clean code begins to be lacking. This usually happens when a project is submitted to the stream that is more complex than a typical catalog site. When creating such a project, it is very important to lay down the correct architecture (especially if the project is long-term), which will be able to adapt as flexibly and quickly to new business requirements.





- ( service layer), ,   . MVC Laravel.





, , , .   , , -, , , .





, Service layer - . , .





, :





(Service layer) — , .





, . ,   ( ) -, . , S SOLID.





  , Eloquent , .. , , . , -, , . , .





Email

, - - , . .





namespace App\Http\Controllers;

use App\Http\Requests\CreateOrderRequest;
use Illuminate\Support\Facades\Mail;

class OrderController
{
    public function createOrder(CreateOrderRequest $request)
    {
        //   ...

        Mail::send('mail.order_created', [
            'order' => $order
        ], function ($message) use ($order) {
            $message->to($order->email)
                ->subject(trans('mail/order_created.mail_title'));
        });
    }
}
      
      



, . Laravel . , , .





public function editOrder(EditOrderRequest $request)
{
    //    ...

    Mail::send('mail.order_updated', [
        'order' => $order
    ], function ($message) use ($order) {
        $message->to($order->email)
            ->subject(trans('mail/order_updated.mail_title'));
    });
}
      
      



, , .





public function registerCustomer(RegisterCustomerRequest $request)
{
    //   ...

    Mail::send('mail.customer_register', [
        'customer' => $customer
    ], function ($message) use ($customer) {
        $message->to($customer->email)
            ->subject(trans('mail/customer_register.mail_title'));
    });
}
      
      



, Mail , , - .





, email . , , , .. - email , . .





, email , , - . , Mail . ( )? , . , , . , .





, NotificationService.





namespace App\Services;

use Illuminate\Support\Facades\Mail;
use App\Mail\Events\MailEventInterface;
use App\Mail\Events\OrderCreatedEvent;
use App\Mail\Events\OrderUpdatedEvent;
use App\Mail\Events\CustomerRegisterEvent;

class NotificationService
{
    public function notify(string $event, array $data)
    {
        $event = $this->makeNotificationEvent($event, $data);

        Mail::send($event->getView(), $event->getData(), function ($message) use ($event) {
            $message->to($event->getEmail())
                ->subject($event->getMailSubject());
        });
    }

    private function makeNotificationEvent(string $event, array $data) : MailEventInterface
    {
        switch ($event) {
            case 'order_created':
                return new OrderCreatedEvent($data);
            case 'order_updated':
                return new OrderUpdatedEvent($data);
            case 'customer_register':
                return new CustomerRegisterEvent($data);
            default:
                throw new \InvalidArgumentException("Undefined event $event");
        }
    }
}
      
      



,  MailEventInterface.





namespace App\Mail\Events;

interface MailEventInterface
{
    public function getView() : string;
    public function getData() : array;
    public function getEmail() : string;
    public function getMailSubject() : string;
}
      
      



, ,  OrderCreatedEvent ( ).





namespace App\Mail\Events;

class OrderCreatedEvent implements MailEventInterface
{
    private $order;

    public function __construct(array $data)
    {
        //   ( )

        $this->order = $data['order'];
    }

    public function getView(): string
    {
        return 'mail.order_created';
    }

    public function getData(): array
    {
        return [
            'order' => $this->order
        ];
    }

    public function getEmail(): string
    {
        return $this->order->email;
    }

    public function getMailSubject(): string
    {
        return trans('mail/order_created.mail_title');
    }
}
      
      



, .





namespace App\Http\Controllers;

use App\Http\Requests\CreateOrderRequest;
use App\Services\NotificationService;

class OrderController
{
    private $notificationService;
    
    public function __construct(NotificationService $notificationService)
    {
        $this->notificationService = $notificationService;
    }

    public function createOrder(CreateOrderRequest $request)
    {
        //   ...
        
        $this->notificationService->notify('order_created', [
            'order' => $order
        ]);
    }
}
      
      



? , . , , . ( -), , . , , " " . .





?

. . . , .   ?  , NotificationServiceInterface , -. - .





$this->app->when(OrderController::class)
    ->needs(NotificationServiceInterface::class)
    ->give(function () {
        return new ESputnikNotificationService();
    });

$this->app->when(OrderUpdateController::class)
    ->needs(NotificationServiceInterface::class)
    ->give(function () {
        return new MailNotificationService();
    });
      
      



, 95% , - .





?

, single responsibility , , , .





.





1. . , , try/catch.





class OrderController
{
    public function saveOrder(
        SaveOrderRequest $request, 
        OrderService $orderService, 
        NotificationService $notificationService
    ) {
        try {
            $order = $orderService->createOrderFromRequest($request);
            $notificationService->notify('order_created', [
                'order' => $order
            ]);

            return response()->json([
                'success' => true,
                'data' => [
                    'order' => $order
                ]
            ]);
        }
        catch (OrderServiceException|NotificationServiceException $e) {
            return response()->json([
                'success' => false,
                'exception' => $e->getMessage()
            ]);
        }
    }
}
      
      



2. , . , Operation (CreateOrderOperation). try/catch, OperationResult, . .





class OrderController
{
    public function saveOrder(
        SaveOrderRequest $request,
        CreateOrderOperation $createOrderOperation
    ) {
        //         ..
        $result = $createOrderOperation->createOrderFromRequest($request);

        //    ,  OperationResult
        //   JsonSerializable

        return response()->json($result);
    }
}
      
      



UPD: , , . , , ..Service.





UPD: And of course, it is not entirely correct to transfer extra data to the service layer in the form of a whole request. It would be much better to forward a valid DTO. You also need to return something understandable from services. This approach makes sense at least in the Laravel ecosystem.





At this point, the article came to its logical conclusion. I hope it will help novice developers and those who are little familiar with the service layer to fully understand the essence of the approach and the problems it solves.





Thank you all for your attention!








All Articles