Capítulo 17: Service Pattern, Repositories e Injeção de Dependências
Especialização em Backend com PHP & Laravel • Laravel 11 & PHP 8.3+ • Eloquent ORM, Sanctum, Filas, Horizon e APIs Corporativas
🗺️ Mapa Conceitual do Tópico
flowchart TD
A["Controller (Skinny)"] --> B["Service / Action"]
B --> C["Repository Interface"]
C --> D["Eloquent Repository (implementação concreta)"]
D --> E["Banco de Dados"]
subgraph DECORATOR["Decorator opcional"]
F["CachingRepository"]
end
B -.-> DECORATOR
DECORATOR -.-> D
style A fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
style B fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
style C fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
style D fill:#fce4ec,stroke:#e91e63,stroke-width:2px
style E fill:#f3e5f5,stroke:#9c27b0,stroke-width:2px
style F fill:#fff3e0,stroke:#ff9800,stroke-width:2px
🏛️ 1. Por que Sair do “Fat Controller”
Um Controller Laravel “gordo” mistura validação, regra de negócio e persistência no mesmo método, tornando-o difícil de testar isoladamente e impossível de reaproveitar fora do contexto HTTP (por exemplo, ao processar o mesmo fluxo dentro de um Job de fila ou de um comando Artisan). O padrão Service Layer resolve isso extraindo a orquestração de negócio para uma classe dedicada, injetada via constructor promotion:
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Services\PedidoService;
use Illuminate\Http\Request;
class PedidoController extends Controller
{
public function __construct(private readonly PedidoService $service) {}
public function store(Request $request)
{
$pedido = $this->service->processarPedido($request->validated());
return response()->json($pedido, 201);
}
}
O Controller passa a ter uma única responsabilidade: traduzir HTTP em chamada de domínio e traduzir o retorno de volta em resposta HTTP.
🧱 2. Repository: Abstraindo a Camada de Persistência
O padrão Repository introduz uma interface entre a lógica de negócio e o Eloquent, permitindo trocar a implementação (Eloquent, uma API externa, um cache) sem alterar quem consome o repositório:
<?php
namespace App\Repositories;
use App\Models\Produto;
use Illuminate\Database\Eloquent\Collection;
interface ProdutoRepositoryInterface
{
public function all(): Collection;
public function findById(int $id): ?Produto;
public function create(array $data): Produto;
}
A implementação concreta é registrada no Service Container (tipicamente em um Service Provider), e qualquer classe que declare o tipo da interface no construtor recebe a implementação automaticamente — sem new explícito e sem acoplamento ao Eloquent.
Trade-off importante: para modelos Eloquent simples, um Repository que só espelha Model::query() é overengineering — o próprio Eloquent já é uma implementação do padrão Active Record e serve como abstração razoável. O Repository paga seu custo quando há lógica de consulta complexa reaproveitada em múltiplos lugares, múltiplas fontes de dados, ou necessidade real de trocar a implementação em testes.
🎁 3. Decorator: Cache Transparente sobre o Repository
Como o Repository é definido por interface, é possível empilhar um Decorator que adiciona cache sem que o código cliente saiba disso:
<?php
namespace App\Repositories;
use Illuminate\Support\Facades\Cache;
use Illuminate\Database\Eloquent\Collection;
use App\Models\Produto;
class CachingProdutoRepository implements ProdutoRepositoryInterface
{
public function __construct(private readonly ProdutoRepositoryInterface $next) {}
public function all(): Collection
{
return Cache::remember('produtos:all', 3600, fn () => $this->next->all());
}
public function findById(int $id): ?Produto
{
return $this->next->findById($id);
}
public function create(array $data): Produto
{
Cache::forget('produtos:all');
return $this->next->create($data);
}
}
No AppServiceProvider, o binding encadeia as duas implementações:
$this->app->bind(ProdutoRepositoryInterface::class, function ($app) {
return new CachingProdutoRepository(new EloquentProdutoRepository());
});
⚙️ 4. Actions e DTOs
Uma alternativa (ou complemento) ao Service genérico é a Single Action Class: uma classe com um único método de execução (__invoke() ou execute()), nomeada como um verbo de negócio (CriarNovoPedidoAction). Isso força granularidade fina e facilita testes unitários diretos, sem precisar montar uma requisição HTTP.
Para transportar dados entre camadas sem arrays associativos frágeis ($dados['preco'], sujeitos a erro de digitação e sem checagem estática), usa-se DTOs com classes readonly do PHP 8.2+:
final readonly class CriarProdutoDTO
{
public function __construct(
public string $nome,
public float $preco,
) {}
}
O IDE valida os tipos em tempo de desenvolvimento e o PHP garante imutabilidade em runtime — qualquer tentativa de reatribuir uma propriedade readonly lança Error.