Um pouco mais sobre a camada de serviço em PHP

Na vida de todo desenvolvedor, chega um momento em que começa a faltar uma compreensão dos padrões e regras populares para escrever código limpo. Isso geralmente acontece quando um projeto é enviado ao fluxo que é mais complexo do que um site de catálogo típico. Ao criar tal projeto, é muito importante estabelecer a arquitetura correta (especialmente se o projeto for de longo prazo), que será capaz de se adaptar da maneira mais flexível e rápida aos novos requisitos de negócios.





- ( 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: E, claro, não é totalmente correto transferir dados extras para a camada de serviço na forma de uma solicitação inteira. Seria muito melhor encaminhar um DTO válido. Você também precisa retornar algo compreensível dos serviços. Esta abordagem faz sentido pelo menos no ecossistema Laravel.





Nesse ponto, o artigo chegou à sua conclusão lógica. Espero que ajude os desenvolvedores novatos e aqueles que não estão familiarizados com a camada de serviço a compreender totalmente a essência da abordagem e os problemas que ela resolve.





Obrigado a todos pela atenção!








All Articles