Skip to main content

CRUD Simples

 

Visão geral da arquitetura em camadas

A API segue uma arquitetura em camadas, onde cada uma só conversa com a vizinha, composta por Model, Repository, Service e Controller. Cada uma dessas camadas tem uma classe base (BaseModel, BaseRepository, BaseService, BaseController) que já implementa o comportamento genérico de CRUD, para que cada entidade nova só precise herdar dela e informar suas particularidades.

Migration

O primeiro passo da arquitetura em camadas é escrever uma migration para criar a tabela no Banco de Dados. A migration define o nome da tabela, bem como os seus atributos e respectivos tipos. É importante destacar, por boa prática, as migrations devem ter: Timestamps, Soft Deletes e os campos created_by, updated_by e deleted_by, que armazenam a informação de qual usuário que realizou alterações nos dados daquela tabela.

É também na migration onde definimos em qual conexão de banco de dados será realizada às ações da migration. A conexão deve estar declarada em config/database.php.

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    protected $connection = 'pgsql_selecao_prppg';

    public function __construct()
    {
        if (app()->environment('testing')) {
            $this->connection = 'pgsql_testing';
        }
    }

    public function up(): void
    {
        if (! Schema::hasTable('programa')) {
            Schema::create('programa', function (Blueprint $table) {
                $table->comment('Entidade que armazena os programas de seleção da PRPPG.');
                $table->id()->comment('ID do programa');
                $table->bigInteger('coordenador_id')->unsigned()->comment('ID do coordenador');
                $table->string('codigo', 15)->unique()->comment('Codigo do programa');
                $table->string('nome', 100)->unique()->comment('Nome do programa');
                $table->timestamps();
                $table->softDeletes();
                $table->unsignedBigInteger('created_by')->nullable()
                    ->comment('Identificador do usuário que criou o programa.');
                $table->unsignedBigInteger('updated_by')->nullable()
                    ->comment('Identificador do usuário que fez a última alteração no programa.');
                $table->unsignedBigInteger('deleted_by')->nullable()
                    ->comment('Identificador do usuário que removeu o programa.');
            });
        }
    }

    public function down(): void
    {
        Schema::dropIfExists('programa');
    }
};

Model

O Model é a classe PHP que representa uma tabela do banco de dados dentro do código da aplicação. Cada linha da tabela vira um objeto; cada coluna vira uma propriedade (atributo) desse objeto. É a "tradução" entre o mundo do banco (linhas e colunas, em SQL) e o mundo da aplicação (objetos e propriedades, em PHP).

Uma analogia simples

Pense na tabela programa como uma planilha do Excel:

id codigo nome coordenador_id
1 PPGCC-2026 Mestrado em Ciência da Computação 1

O Model Programa é o que permite você escrever, em PHP, algo como:

$programa = Programa::find(1);
echo $programa->nome; // "Mestrado em Ciência da Computação"

Sem precisar escrever SELECT * FROM programa WHERE id = 1 manualmente, nem se preocupar em converter o resultado do banco (que vem como array/linha crua) em algo que o PHP consiga manipular como objeto. O Model faz essa ponte.

O model é a camada mais baixa, próxima do banco de dados. Isso significa que as camadas subsequentes nunca escrevem e manipulam SQL diretamente. Na verdade, elas conversam com o Model, isto é, com os objetos que representam as linhas do Banco de Dados. O Eloquent é o responsável por transformar as ações com objetos em consultas SQL reais.

Olhando o Programa que construímos:

class Programa extends BaseModel
{
    protected $connection = 'pgsql_selecao_prppg';
    protected $table = 'programa';
    protected $fillable = ['id', 'coordenador_id', 'codigo', 'nome', 'created_by', 'updated_by', 'deleted_by'];
    protected $hidden = [];
}

Cada linha responde a uma pergunta diferente que o Eloquent precisa saber para funcionar:

Propriedade Pergunta que responde
$connection "Em qual banco de dados essa tabela mora?" — no nosso caso, pgsql_selecao_prppg, não o banco padrão da aplicação.
$table "Qual é o nome exato da tabela?" — programa, porque o Eloquent, por padrão, tentaria adivinhar programas (plural), e erraria.
$fillable "Quais colunas podem ser preenchidas de uma vez, via array?" — é a proteção que impede alguém de mandar, por exemplo, deleted_by num POST de criação sem que isso devesse ser permitido.
$hidden "Quais colunas nunca devem aparecer quando esse objeto virar JSON?" — vazio no nosso caso, porque Programa não tem nenhum dado sensível a esconder

 

Base Model

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;
use Spatie\Activitylog\LogOptions;
use Spatie\Activitylog\Traits\LogsActivity;

class BaseModel extends Model
{
    use LogsActivity, SoftDeletes;

    public function __construct(array $attributes = [])
    {
        parent::__construct($attributes);

        if (app()->environment('testing')) {
            $this->setConnection('pgsql_testing');
        }
    }

    public function getActivitylogOptions(): LogOptions
    {
        return (new LogOptions)->logAll();
    }

    protected function casts(): array
    {
        return [
            'created_at' => 'datetime:d/m/Y H:i:s',
            'updated_at' => 'datetime:d/m/Y H:i:s',
            'deleted_at' => 'datetime:d/m/Y H:i:s',
        ];
    }

    public function createdBy(): BelongsTo
    {
        return $this->belongsTo(Usuario::class, 'created_by', 'id');
    }

    public function updatedBy(): BelongsTo
    {
        return $this->belongsTo(Usuario::class, 'updated_by', 'id');
    }

    public function deletedBy(): BelongsTo
    {
        return $this->belongsTo(Usuario::class, 'deleted_by', 'id');
    }

    public static function getModelQuery()
    {
        return self::query();
    }
}

Como Programa extends BaseModel, ele ganha de graça comportamentos que não estão escritos no arquivo Programa.php, mas que existem por herança:

  • Soft delete: $programa->delete() não apaga a linha — só marca deleted_at.
  • Log de atividades: toda alteração fica registrada automaticamente.
  • Formatação automática de datas: $programa->created_at já vem formatado como 07/07/2026 09:12:44, sem você precisar formatar manualmente.
  • Relacionamentos de auditoria prontos: $programa->createdBy te dá o objeto Usuario que criou o registro, sem escrever nenhum JOIN manual.

Repository Interface e Repository

O Repository é a classe responsável por conversar com o banco de dados através do Model, concentrando toda consulta (SELECT, INSERT, UPDATE, DELETE) num único lugar — para que nenhuma outra camada da aplicação precise saber como uma consulta é montada. 

Sem um Repository, seria comum ver código de consulta espalhado por todo lado — um pouco no Controller, um pouco no Service, cada desenvolvedor escrevendo a query do seu jeito. Isso cria dois problemas: (1) duplicação — a mesma consulta escrita de formas diferentes em lugares diferentes — e (2) acoplamento — se um dia a forma de buscar dados mudar, você precisaria caçar e alterar em vários arquivos.

O Repository fica entre o Service e o Model. Ele é a primeira camada que efetivamente monta código Eloquent — as camadas acima dele (Service, Controller) nunca fazem isso diretamente.

Cada Repository é associado a um model em sua criação, através de um construtor. Dessa forma, podemos direcionar sobre quais tabelas aquele Repository irá atuar. Além disso, cada Service é associado a um repository. Entretanto, referências ao Repository nas aplicações são feitas, na verdade, a sua abstração/interface.

Nesse sentido, todo Repository tem um Interface, que define os métodos que ele implementa. O Repository concreto deve então ser associado a um Model e implementar todos os métodos impostos pelo RepositoryInterface.

<?php

namespace App\Interfaces\Selecao_prppg;

use App\Interfaces\BaseRepositoryInterface;

interface ProgramaRepositoryInterface extends BaseRepositoryInterface
{
    // Sem métodos extras por enquanto — Programa é um CRUD simples.
}
class ProgramaRepository extends BaseRepository implements ProgramaRepositoryInterface
{
    public function __construct()
    {
        parent::__construct(new Programa);
    }
}

Base Repository e Base Repository Interface

Define as operações basica de um CRUD, como FindAll, FindById, Store, UpdateById e DeleteById. Base Repository Interface basicamente declara todos esses métodos, sendo apenas incrementadas dos novos métodos que serão implementados no Repository Concreto.

 

<?php

namespace App\Repositories;

use App\Interfaces\BaseRepositoryInterface;
use App\Interfaces\Contracts\PaginationInterface;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;

class BaseRepository implements BaseRepositoryInterface
{
    public function __construct(protected Model $model) {}

    protected function applyFilters($query, array $filter = [])
    {
        if (! empty($filter) && is_array($filter)) {
            foreach ($filter as $key => $value) {
                if ($this->isRelationKey($key)) {
                    [$relation, $column] = explode('.', $key);
                    $query->whereHas($relation, function ($q) use ($column, $value) {
                        $q->where($column, 'ilike', '%'.$value.'%');
                    });
                    continue;
                }

                is_array($value)
                    ? $query->whereIn($key, $value)
                    : $query->where($key, 'ilike', '%'.$value.'%');
            }
        }
    }

    protected function applyOrdering($query, array $order = [])
    {
        if (! empty($order) && is_array($order)) {
            foreach ($order as $key => $direction) {
                if ($this->isRelationKey($key)) {
                    [$relation, $column] = explode('.', $key);
                    $query->with($relation, function ($q) use ($column, $direction) {
                        $q->orderBy($column, $direction);
                    });
                    continue;
                }

                $query->orderBy($key, $direction);
            }
        }
    }

    protected function isRelationKey(string $key): bool
    {
        return strpos($key, '.') !== false;
    }

    public function paginate(int $page = 1, int $totalPerPage = 15, array $filter = [], array $order = []): PaginationInterface
    {
        $query = $this->model::getModelQuery();

        $this->applyFilters($query, $filter);
        $this->applyOrdering($query, $order);

        $result = $query->paginate($totalPerPage, ['*'], 'page', $page);

        return new PaginationRepository($result, $filter, $order);
    }

    public function all(array $filter = [], array $order = []): Collection
    {
        $query = $this->model::getModelQuery();
        $this->applyFilters($query, $filter);
        $this->applyOrdering($query, $order);
        return $query->get();
    }

    public function find(int $id): ?Model
    {
        return $this->model::findOrFail($id);
    }

    public function store(array $data): ?Model
    {
        return $this->model::create($data);
    }

    public function update(array $data, int $id): ?bool
    {
        return $this->model::findOrFail($id)->update($data);
    }

    public function delete(int $id): ?bool
    {
        return $this->model::findOrFail($id)->delete();
    }
}

O que cada método faz:

Método Visibilidade Função
__construct(Model $model) público Recebe qual Model esse repositório deve consultar. É por isso que ProgramaRepository só precisa fazer parent::__construct(new Programa) — todo o resto do BaseRepository já sabe usar esse $model internamente.
applyFilters() protegido Percorre o array de filtros vindo da query string (?filter[nome]=x) e monta WHERE campo ILIKE '%valor%' para cada um. Se a chave tiver um ponto (relacao.coluna), filtra dentro de um relacionamento via whereHas. Se o valor for um array, vira WHERE IN em vez de ILIKE.
applyOrdering() protegido Mesma lógica, mas para ORDER BY, incluindo ordenação dentro de relacionamentos.
isRelationKey() protegido Só verifica se a chave tem um . no meio (indicando relacao.coluna).
paginate() público Monta a query a partir de $model::getModelQuery() (herdado do BaseModel), aplica filtros/ordenação, pagina com o paginate() nativo do Eloquent, e embrulha o resultado numa PaginationRepository (classe própria da aplicação, não recebida ainda — ver Seção 8).
all() público Mesma coisa, mas sem paginar — devolve uma Collection completa.
find() público findOrFail($id) — lança ModelNotFoundException automaticamente se o id não existir (é isso que gera o 404 no Controller).
store() público Model::create($data) — só funciona para os campos que estiverem no $fillable do Model.
update() público Busca o registro com findOrFail e chama ->update($data) nele.
delete() público Busca o registro com findOrFail e chama ->delete() — que, por causa do SoftDeletes herdado via BaseModel, só marca deleted_at.

Service Provider

Como citado anteriormente, os Services possuem referência a um Repository, visto que ele precisar ter acessos a todos os seus métodos. O Service possui uma referência apenas a interface de um Repository, não ao Repository concreto. O Service Provider é o responsavel por "bindar" o RepositoryInterface ao Repository, garantindo que toda chamada a interface crie um Repository concreto.

<?php

namespace App\Providers\Selecao_prppg;

use App\Interfaces\Selecao_prppg\ProgramaRepositoryInterface;
use App\Repositories\Selecao_prppg\ProgramaRepository;
use Illuminate\Support\ServiceProvider;

class ProgramaServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->bind(ProgramaRepositoryInterface::class, ProgramaRepository::class);
    }

    public function boot(): void
    {
        //
    }
}

Service

O Service é a camada que concentra regras de negócio — principalmente aquelas que envolvem mais de uma operação no banco, mais de uma tabela, ou alguma decisão que vai além de um simples "buscar/salvar". É o lugar certo para orquestrar várias chamadas ao Repository (ou a outros Services) como se fossem uma única ação, do ponto de vista de quem consome.

class ProgramaService extends BaseService
{
    public function __construct()
    {
        parent::__construct(
            repository: app(ProgramaRepositoryInterface::class)
        );
    }
}

Base Service

O que muda em relação ao BaseRepository:

  • A classe é abstract — não pode ser instanciada diretamente, só através de uma subclasse concreta (ProgramaService).
  • O construtor recebe uma interface (BaseRepositoryInterface), não uma classe concreta — é aqui que a inversão de dependência (explicada anteriormente na conversa) se concretiza: quem decide qual implementação real entra aqui é o Service Provider.
  • Os nomes de método são levemente diferentes dos do Repository — por exemplo, create() no Service chama store() no Repository. Fora essa troca de nome, cada método do Service apenas repassa a chamada para o método equivalente do Repository (return $this->repository->algumMetodo(...)), sem adicionar nenhuma lógica própria.

 

<?php

namespace App\Services;

use App\Interfaces\BaseRepositoryInterface;
use App\Interfaces\Contracts\PaginationInterface;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;

abstract class BaseService
{
    public function __construct(protected BaseRepositoryInterface $repository) {}

    public function paginate(int $page = 1, int $totalPerPage = 15, array $filter = [], array $order = []): PaginationInterface
    {
        return $this->repository->paginate(page: $page, totalPerPage: $totalPerPage, filter: $filter, order: $order);
    }

    public function all(array $filter = [], array $order = []): Collection
    {
        return $this->repository->all($filter, $order);
    }

    public function find(int $id): ?Model
    {
        return $this->repository->find($id);
    }

    public function create(array $data): ?Model
    {
        return $this->repository->store($data);
    }

    public function update(array $data, int $id): ?bool
    {
        return $this->repository->update($data, $id);
    }

    public function delete(int $id): ?bool
    {
        return $this->repository->delete($id);
    }
}

Form Requests

Como visto anteriormente, a arquitetura em camadas finaliza no Controller, que é a camada de mais alto nível, que recebe as requestas e devolve as responses. Dessa maneira, é necessário estipular padrões de quais dados são aceitos para serem enviados pelas requests,

São necessários Form Request tanto para update quanto para create. Normalmente estes serão bem parecidos.

<?php
// app/Http/Requests/Selecao_prppg/Programa/ProgramaStoreRequest.php

namespace App\Http\Requests\Selecao_prppg\Programa;

use Illuminate\Foundation\Http\FormRequest;

class ProgramaStoreRequest extends FormRequest
{
    protected const USERS_RULES = 'sometimes|integer|exists:App\Models\Institucional\Usuario,id';

    public function authorize(): bool
    {
        return false;
    }

    public function rules(): array
    {
        return [
            'codigo' => 'required|string|max:15|unique:App\Models\Selecao_prppg\Programa,codigo',
            'nome' => 'required|string|max:100|unique:App\Models\Selecao_prppg\Programa,nome',
            'coordenador_id' => 'required|integer|exists:App\Models\Institucional\Usuario,id',
            'created_by' => self::USERS_RULES,
            'updated_by' => self::USERS_RULES,
            'deleted_by' => self::USERS_RULES,
        ];
    }
}
Tabela: propriedades e métodos de um Form Request

Baseado no ProgramaStoreRequest/ProgramaUpdateRequest que já construímos, aqui está o que cada parte faz:

Propriedade / Método Tipo Função
extends FormRequest Herança Dá à classe toda a infraestrutura de validação do Laravel — não é uma classe qualquer, é uma subclasse especializada que sabe como validar dados de uma requisição HTTP.
protected const USERS_RULES Constante de classe Não é um recurso nativo do FormRequest — é uma convenção do projeto para evitar repetir a mesma regra de validação três vezes (created_by, updated_by, deleted_by usam exatamente a mesma regra: sometimes|integer|exists:...Usuario,id). Reduz duplicação dentro da própria classe.
authorize(): bool Método (contrato do Laravel) Em um fluxo "padrão" do Laravel, decide se o usuário tem permissão para fazer essa requisição — retornar false bloquearia com HTTP 403 automaticamente. Neste projeto, esse método nunca é chamado de verdade (explicado no detalhe abaixo), por isso sempre retorna false sem quebrar nada.
rules(): array Método (contrato do Laravel) O método que realmente importa aqui: devolve um array associativo onde cada chave é o nome de um campo do payload, e o valor é a(s) regra(s) de validação daquele campo, separadas por |. É esse array que o BaseController usa em $this->validate($request, $this->storeFormRequest->rules()).
'campo' => 'required|...' Regra de validação required obriga o campo a estar presente; sometimes só valida o campo se ele vier no payload (permite omitir em updates parciais).
'campo' => '...|string|max:N' Regra de tipo/tamanho Garante o tipo do dado (string, integer) e limites de tamanho — sempre espelhando o que foi definido na migration (ex.: max:15 para uma coluna string('codigo', 15)).
'campo' => '...|unique:App\Models\...|Classe,coluna' Regra de unicidade Verifica se já existe outro registro com o mesmo valor naquela coluna, apontando para a classe do Model (não o nome da tabela) — o Laravel resolve a tabela sozinho a partir da classe.
'campo' => '...|exists:App\Models\...|Classe,coluna' Regra de existência Verifica se o valor informado corresponde a um registro que já existe em outra tabela — usado para coordenador_id/created_by/etc., garantindo que o id de usuário informado realmente existe.
Métodos extras que o FormRequest oferece (não usados no projeto, mas bom saber)
Método Função
messages(): array Personaliza as mensagens de erro de cada regra (em vez das mensagens genéricas em inglês do Laravel).
attributes(): array Personaliza o nome de exibição de um campo numa mensagem de erro (ex.: exibir "código do programa" em vez de "codigo").
prepareForValidation(): void Roda antes da validação — útil para normalizar dados (ex.: forçar codigo para maiúsculas antes de validar unique).
withValidator($validator): void Permite adicionar validação customizada, complexa demais para caber numa string de regra simples (ex.: "a soma de dois campos não pode passar de X").