Service layer e separação de responsabilidades

1. Introdução ao Service Layer em PHP

O padrão Service Layer (Camada de Serviço) é uma abordagem arquitetural que introduz uma camada intermediária entre os controllers e os repositórios/modelos de domínio. Seu objetivo principal é encapsular a lógica de negócios da aplicação, mantendo os controllers enxutos e as regras de negócio centralizadas.

Problemas resolvidos pelo Service Layer

Em aplicações PHP tradicionais, é comum encontrar controllers "gordos" que acumulam responsabilidades como validação, persistência, envio de e-mails e cálculos complexos. Esse anti-padrão torna o código difícil de testar, manter e reutilizar.

Antes do Service Layer:

class UserController
{
    public function register(Request $request)
    {
        // Validação
        if (strlen($request->password) < 8) {
            return response()->json(['error' => 'Senha muito curta'], 400);
        }

        // Hash da senha
        $hashedPassword = password_hash($request->password, PASSWORD_BCRYPT);

        // Persistência
        $user = new User();
        $user->name = $request->name;
        $user->email = $request->email;
        $user->password = $hashedPassword;
        $user->save();

        // Envio de e-mail
        Mail::to($user->email)->send(new WelcomeMail($user));

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

Com Service Layer:

class UserController
{
    public function __construct(
        private UserRegistrationService $registrationService
    ) {}

    public function register(RegisterUserRequest $request)
    {
        $result = $this->registrationService->register(
            $request->validated()
        );

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

Diferença entre Service Layer, Repository e Domain Model

  • Repository: Responsável pelo acesso e persistência de dados (camada de infraestrutura)
  • Domain Model: Representa entidades de negócio com seu comportamento inerente
  • Service Layer: Orquestra operações de negócio que envolvem múltiplas entidades ou repositórios

2. Princípios de Separação de Responsabilidades (SRP)

O Single Responsibility Principle (Princípio da Responsabilidade Única) é fundamental para construir serviços coesos. Cada classe de serviço deve ter uma única razão para mudar.

Coesão: um serviço, uma responsabilidade

// ❌ Ruim: Serviço com múltiplas responsabilidades
class UserService
{
    public function register(array $data): User { /* ... */ }
    public function sendPasswordReset(string $email): void { /* ... */ }
    public function generateReport(): array { /* ... */ }
    public function updateProfile(int $userId, array $data): void { /* ... */ }
}

// ✅ Bom: Serviços focados
class UserRegistrationService
{
    public function register(array $data): User { /* ... */ }
}

class PasswordResetService
{
    public function sendResetLink(string $email): void { /* ... */ }
}

class ReportGenerationService
{
    public function generateUserReport(): array { /* ... */ }
}

Acoplamento baixo entre serviços e outras camadas

Serviços devem depender de abstrações (interfaces), não de implementações concretas. Isso permite trocar implementações sem modificar o serviço.

3. Estrutura Básica de um Service Layer

Nomenclatura e organização de diretórios

app/
├── Services/
│   ├── User/
│   │   ├── UserRegistrationService.php
│   │   └── UserProfileService.php
│   ├── Order/
│   │   ├── OrderCreationService.php
│   │   └── OrderCancellationService.php
│   └── Payment/
│       └── PaymentProcessingService.php
├── Repositories/
├── Models/
└── Http/
    └── Controllers/

Exemplo de um serviço simples

namespace App\Services\User;

use App\Repositories\UserRepository;
use App\Notifications\WelcomeNotification;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Notification;

class UserRegistrationService
{
    public function __construct(
        private UserRepository $userRepository,
        private WelcomeNotification $welcomeNotification
    ) {}

    public function register(array $data): User
    {
        // Regra de negócio: validar e-mail único
        if ($this->userRepository->findByEmail($data['email'])) {
            throw new UserAlreadyExistsException('E-mail já cadastrado');
        }

        // Regra de negócio: hash da senha
        $data['password'] = Hash::make($data['password']);

        // Persistência
        $user = $this->userRepository->create($data);

        // Notificação
        Notification::send($user, $this->welcomeNotification);

        return $user;
    }
}

Métodos públicos vs. privados

  • Públicos: Operações de negócio que o controller pode chamar
  • Privados: Lógica auxiliar interna (validações, cálculos intermediários)
class OrderService
{
    public function placeOrder(array $items, int $userId): Order
    {
        $this->validateStock($items);
        $order = $this->createOrder($items, $userId);
        $this->processPayment($order);
        return $order;
    }

    private function validateStock(array $items): void
    {
        // Lógica interna de validação de estoque
    }

    private function createOrder(array $items, int $userId): Order
    {
        // Criação da ordem no banco
    }

    private function processPayment(Order $order): void
    {
        // Processamento de pagamento
    }
}

4. Serviços Especializados vs. Serviços Genéricos

Serviços focados em uma entidade

class PaymentService
{
    public function processPayment(Order $order, PaymentMethod $method): PaymentResult
    {
        // Lógica específica de pagamento
    }

    public function refundPayment(Payment $payment): void
    {
        // Lógica específica de reembolso
    }
}

Serviços de orquestração

class CheckoutService
{
    public function __construct(
        private OrderCreationService $orderService,
        private PaymentService $paymentService,
        private InventoryService $inventoryService,
        private NotificationService $notificationService
    ) {}

    public function checkout(array $cartData, PaymentMethod $paymentMethod): CheckoutResult
    {
        // Orquestração
        $this->inventoryService->reserveItems($cartData['items']);
        $order = $this->orderService->createOrder($cartData);
        $paymentResult = $this->paymentService->processPayment($order, $paymentMethod);
        $this->notificationService->sendOrderConfirmation($order);

        return new CheckoutResult($order, $paymentResult);
    }
}

Quando criar um novo serviço vs. adicionar um método

Crie um novo serviço quando:
- A funcionalidade envolve um conjunto diferente de dependências
- A lógica pode ser reutilizada em diferentes contextos
- O serviço atual ultrapassa 200-300 linhas

5. Injeção de Dependências em Serviços

Uso de interfaces para desacoplamento

// Interface para o repositório
interface UserRepositoryInterface
{
    public function findByEmail(string $email): ?User;
    public function create(array $data): User;
}

// Implementação concreta
class EloquentUserRepository implements UserRepositoryInterface
{
    public function findByEmail(string $email): ?User
    {
        return User::where('email', $email)->first();
    }

    public function create(array $data): User
    {
        return User::create($data);
    }
}

// Serviço dependendo da abstração
class UserRegistrationService
{
    public function __construct(
        private UserRepositoryInterface $userRepository
    ) {}
}

Exemplo prático com construtor e DI container (Laravel)

// Service Provider
class AppServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->bind(UserRepositoryInterface::class, EloquentUserRepository::class);
        $this->app->bind(EmailServiceInterface::class, SendGridEmailService::class);
    }
}

// Controller usando o serviço
class AuthController extends Controller
{
    public function __construct(
        private UserRegistrationService $registrationService
    ) {}

    public function register(RegisterRequest $request)
    {
        $user = $this->registrationService->register($request->validated());
        return response()->json($user, 201);
    }
}

6. Tratamento de Erros e Exceções

Exceções específicas do domínio

namespace App\Exceptions\Domain;

class InsufficientStockException extends \DomainException
{
    public function __construct(string $productName, int $available)
    {
        parent::__construct(
            "Estoque insuficiente para o produto '{$productName}'. Disponível: {$available}"
        );
    }
}

class PaymentDeclinedException extends \DomainException
{
    public function __construct(string $reason)
    {
        parent::__construct("Pagamento recusado: {$reason}");
    }
}

Retorno de resultados (DTOs)

class ServiceResult
{
    public function __construct(
        public readonly bool $success,
        public readonly mixed $data = null,
        public readonly ?string $error = null,
        public readonly array $errors = []
    ) {}

    public static function success(mixed $data = null): self
    {
        return new self(true, $data);
    }

    public static function failure(string $error, array $errors = []): self
    {
        return new self(false, null, $error, $errors);
    }
}

// Uso no serviço
class OrderService
{
    public function placeOrder(array $data): ServiceResult
    {
        try {
            // Lógica de criação do pedido
            return ServiceResult::success($order);
        } catch (InsufficientStockException $e) {
            Log::warning('Estoque insuficiente', ['error' => $e->getMessage()]);
            return ServiceResult::failure($e->getMessage());
        }
    }
}

7. Boas Práticas e Armadilhas Comuns

Evitar "God Services"

// ❌ God Service - faz tudo
class GodService
{
    public function handleUser(): void { /* ... */ }
    public function handleOrder(): void { /* ... */ }
    public function handlePayment(): void { /* ... */ }
    public function handleReport(): void { /* ... */ }
    public function handleNotification(): void { /* ... */ }
}

// ✅ Serviços pequenos e focados
class UserService { /* ... */ }
class OrderService { /* ... */ }
class PaymentService { /* ... */ }

Serviços sem estado (stateless)

Serviços devem ser stateless — não armazenar estado entre chamadas de método. Isso facilita testes e reutilização.

// ❌ Com estado
class ShoppingCartService
{
    private array $items = [];

    public function addItem(Product $product): void
    {
        $this->items[] = $product; // Estado interno!
    }
}

// ✅ Sem estado
class ShoppingCartService
{
    public function addItem(array $cart, Product $product): array
    {
        $cart[] = $product;
        return $cart; // Retorna o estado modificado
    }
}

Testabilidade

Serviços facilitam testes unitários porque suas dependências podem ser facilmente mockadas:

class UserRegistrationServiceTest extends TestCase
{
    public function test_register_creates_user(): void
    {
        $repository = $this->createMock(UserRepositoryInterface::class);
        $repository->expects($this->once())
            ->method('create')
            ->willReturn(new User(['name' => 'John']));

        $service = new UserRegistrationService($repository);
        $result = $service->register(['name' => 'John', 'email' => 'john@test.com']);

        $this->assertInstanceOf(User::class, $result);
    }
}

Quando o Service Layer não é a melhor escolha

Para operações CRUD simples sem lógica de negócios complexa, o Service Layer pode ser excessivo:

// CRUD simples - pode usar controller diretamente
class ProductController extends Controller
{
    public function index(): Collection
    {
        return Product::all();
    }

    public function store(StoreProductRequest $request): Product
    {
        return Product::create($request->validated());
    }
}

Nesses casos, o custo de criar um serviço adicional não compensa. O Service Layer brilha quando há regras de negócio complexas, orquestração de múltiplas operações ou lógica que precisa ser reutilizada em diferentes pontos da aplicação.

Referências