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
- Service Layer Pattern - Martin Fowler — Definição clássica do padrão Service Layer por Martin Fowler, com exemplos e contexto arquitetural
- Laravel: Service Layer Best Practices — Artigo do Laravel News sobre implementação prática de Service Layer em aplicações Laravel
- PHP The Right Way: Design Patterns — Guia oficial da comunidade PHP sobre padrões de design, incluindo Service Layer
- Symfony: Service Container Documentation — Documentação oficial do Symfony sobre injeção de dependências e container de serviços
- Domain-Driven Design in PHP — Livro sobre DDD em PHP com capítulos dedicados a Service Layer e separação de responsabilidades
- PHP: Single Responsibility Principle — Documentação oficial do PHP sobre princípios de orientação a objetos, base para SRP
- Refactoring Guru: Service Layer Pattern — Explicação visual e exemplos práticos do padrão Service Layer em PHP