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.