Desenvolvimento do Sistema de Seleção da PRPPG - Guavira

Documentação do processo de desenvolvimento do Guavira, sistema de inscrição em processos seletivos da PRPPG

Preparação do Ambiente Virtual Conteinerizado

Tutorial de como realizar o deploy dos contêineres necessários para iniciar o desenvolvimento do Guavira.

Preparação do Ambiente Virtual Conteinerizado

Deploy dos contêineres

1. Estruturação Inicial e Banco de Dados

Como definido previamente, decidiu-se iniciar a implementação do Guavira pela estruturação e criação do seu banco de dados. Para isso, é possível consultar a documentação disponibilizada pela UFVJM, que apresenta um guia para a criação de um contêiner com o serviço de banco de dados PostgreSQL, responsável por hospedar a estrutura do sistema.

2. Acesso ao Repositório e Clonagem do Projeto

Após obter acesso ao GitLab por meio do LDAP, que é o sistema de login institucional, e realizar a criação de uma chave SSH para autorizar operações a partir do seu computador, é possível prosseguir para a próxima etapa. Nessa fase, será necessário implementar no projeto da API o contêiner responsável por hospedar o banco de dados do Guavira.

Para isso, deve-se realizar a clonagem do projeto para o ambiente local, permitindo sua manipulação de forma segura. É importante destacar que os projetos clonados do GitLab devem estar localizados no diretório home do usuário, pois alguns comandos e operações dependem dessa localização. Após a clonagem, recomenda-se executar o comando de configuração de codificação, a fim de evitar que diferenças de encoding sejam interpretadas como alterações pelo Git.

git clone git@git.dds.ufvjm.edu.br:conta-institucional/api.git api

# entrar na pasta baixada
cd api

# Instrução para o Git ignorar alterações de permissão de arquivo (as permissões não são versionadas)
git config core.fileMode false

3. Configuração do LDAP e Variáveis de Ambiente

Para que o projeto da API funcione corretamente, é necessário que o serviço de LDAP esteja ativo, visto que grande parte das funcionalidades depende desse mecanismo de autenticação. Para realizar o deploy dos contêineres, será preciso obter as credenciais do LDAP, que são confidenciais e devem ser solicitadas via RocketChat a um responsável do DSI, não sendo, portanto, incluídas nesta documentação.

Com as credenciais em mãos, deve-se criar um arquivo .env, contendo as variáveis de ambiente necessárias para o funcionamento dos contêineres. Esse arquivo pode ser gerado a partir de uma cópia do .env.example, substituindo-se os valores pelas credenciais corretas.

cp .env.example .env

4. Deploy dos Contêineres

O processo de deploy será realizado com o auxílio de um Makefile, que contém scripts responsáveis por executar comandos Docker. Inicialmente, é necessário clonar o projeto de automação, uma vez que ele contém o serviço de LDAP. Em seguida, deve-se realizar o login no HUB da UFVJM utilizando o comando apropriado.

docker login -u nome.sobrenome hub.dds.ufvjm.edu.br

Caso esteja utilizando o Docker Desktop, pode ocorrer um erro durante esse processo. Para solucioná-lo, é necessário acessar o arquivo ~/.docker/config.json e remover o par chave-valor "credsStore".

Após isso, pode-se prosseguir com a clonagem do projeto de automação e realizar o deploy apenas do contêiner de LDAP, sendo recomendado comentar os demais serviços para evitar consumo desnecessário de recursos da máquina.

# ir pra home do usuario
cd ~

# baixar o repositório
git clone git@git.dds.ufvjm.edu.br:dds/automacao.git

5. Execução dos Serviços

Por fim, com todas as configurações concluídas, pode-se utilizar o script start presente no Makefile para realizar o deploy dos contêineres da API. Paralelamente, deve-se utilizar o comando docker compose up para iniciar o contêiner de automação, garantindo o funcionamento completo do ambiente necessário para o desenvolvimento do sistema.

# cd ~/api
make start
# cd ~/automacao
docker compose up

Como etapa final de organização do ambiente de desenvolvimento, recomenda-se realizar o attach (ou abertura) dos dois projetos — API e automação — em uma IDE de sua preferência, como o PhpStorm. Isso facilita a navegação entre os arquivos, execução de comandos, edição de código e integração com ferramentas de versionamento. Em IDEs como o PhpStorm, é possível abrir ambos os projetos na mesma janela, utilizando a opção de Attach Project, ou simplesmente abrindo os diretórios simultaneamente no workspace.

Preparação do Ambiente Virtual Conteinerizado

Instalação do PHP, Composer e preparando ambiente Laravel no Linux

Tutorial: Preparação do Ambiente Laravel no Linux (Ubuntu)

Este tutorial descreve o processo de preparação de um ambiente Laravel em uma máquina Linux Ubuntu, considerando que:

  • O projeto já foi clonado do GitLab;
  • O arquivo .env já existe;
  • O objetivo é preparar o ambiente para executar migrations e desenvolver a aplicação.

1. Verificando a versão do PHP exigida pelo projeto

Antes de instalar ou atualizar o PHP, é importante verificar qual versão é exigida pelo projeto.

Na raiz do projeto, abra o arquivo composer.json e procure pela seção require:

{
    "require": {
        "php": "^8.2",
        "laravel/framework": "^12.0"
    }
}


O parâmetro "php" define a versão mínima necessária.

Exemplos:

Configuração Significado
"php": "^8.1" PHP 8.1 ou superior
"php": "^8.2" PHP 8.2 ou superior
"php": "^8.2|^8.3" PHP 8.2 ou 8.3

Também é possível verificar utilizando o terminal:

cat composer.json | grep php



2. Verificando a versão atual do PHP

Para verificar a versão instalada:

php -v


Exemplo de saída:

PHP 8.1.2 (cli)


Caso a versão instalada seja inferior à exigida pelo projeto, será necessário instalar uma versão mais recente.


3. Instalando o PHP 8.2 no Ubuntu

Primeiramente, atualize os repositórios:

sudo apt update


Caso o PHP 8.2 não esteja disponível nos repositórios padrão, adicione o repositório mantido por Sury.org:

curl -fsSL https://packages.sury.org/php/apt.gpg | sudo gpg --dearmor -o /usr/share/keyrings/deb.sury.org-php.gpg

echo "deb [signed-by=/usr/share/keyrings/deb.sury.org-php.gpg] https://packages.sury.org/php/ resolute main" | sudo tee /etc/apt/sources.list.d/php.list

Instale o PHP 8.2 juntamente com as extensões mais utilizadas pelo Laravel:

sudo apt update
sudo apt install php8.2 \
php8.2-cli \
php8.2-common \
php8.2-mysql \
php8.2-mbstring \
php8.2-xml \
php8.2-curl \
php8.2-zip \
php8.2-bcmath \
php8.2-intl



4. Definindo o PHP 8.2 como versão padrão

Após a instalação, o sistema pode continuar utilizando a versão antiga.

Liste as versões disponíveis:

sudo update-alternatives --config php


Exemplo:

There are 2 choices for the alternative php:

Selection    Path
-------------------------------------
0            /usr/bin/php8.2
1            /usr/bin/php8.1
2            /usr/bin/php8.2


Digite o número correspondente ao PHP 8.2.

Verifique novamente:

php -v


Saída esperada:

PHP 8.2.x



5. Instalando o Composer

O Composer é o gerenciador de dependências utilizado pelo Laravel.

Verifique se ele já está instalado:

composer --version


Caso o comando não seja encontrado, instale-o:

sudo apt install composer


Alternativamente, pode-se utilizar o instalador oficial disponível em:

Composer


6. Verificando a versão do Composer

Após a instalação:

composer --version


Exemplo:

Composer version 2.8.5



7. Instalando as dependências do projeto

Com o PHP e o Composer configurados, navegue até a raiz do projeto e execute:

composer install


Este comando:

  • Lê o arquivo composer.lock;
  • Baixa todas as dependências necessárias;
  • Cria a pasta vendor;
  • Gera o autoloader da aplicação.

Ao final, deverá existir a seguinte estrutura:

projeto/
├── app/
├── database/
├── routes/
├── vendor/
├── composer.json
├── composer.lock
└── .env



8. Verificando se as dependências foram instaladas corretamente

Após o término da instalação, execute:

php artisan


Se o ambiente estiver configurado corretamente, será exibida a lista de comandos do Artisan.

Exemplo:

Laravel Framework 12.x

Available commands:
  about
  migrate
  route:list
  serve
  ...




9. Verificando se o ambiente está pronto

Execute os seguintes comandos:

php -v
composer --version
php artisan


Se todos responderem corretamente, o ambiente está preparado para iniciar o desenvolvimento e executar as migrations.


Resumo dos comandos principais

# Verificar versão do PHP
php -v

# Instalar PHP 8.2
sudo add-apt-repository ppa:ondrej/php
sudo apt update

sudo apt install php8.2 \
php8.2-cli \
php8.2-common \
php8.2-mysql \
php8.2-mbstring \
php8.2-xml \
php8.2-curl \
php8.2-zip \
php8.2-bcmath \
php8.2-intl

# Definir PHP padrão
sudo update-alternatives --config php

# Verificar Composer
composer --version

# Instalar Composer
sudo apt install composer

# Instalar dependências do projeto
composer install

# Gerar APP_KEY
php artisan key:generate

# Verificar Laravel
php artisan


Após esses passos, o ambiente estará pronto para a criação e execução das migrations do projeto.

Criando as conexões no ambiente Laravel

Definindo a conexão com o banco de dados que se encontra no contêiner que fora configurado no capítulo passado.

Criando as conexões no ambiente Laravel

Criando e Estabelecendo conexão com o Banco de Dados do Guavira

6. Configuração da Conexão com o Banco de Dados

Após realizar o deploy do contêiner responsável pelo banco de dados, é necessário configurar uma conexão com ele na API Laravel. Esse passo é fundamental para garantir que as migrations e demais operações relacionadas ao banco de dados sejam executadas na base correta.

Essa configuração é especialmente importante porque a aplicação possui conexões com diversos bancos de dados institucionais, como os utilizados pelo RU, E-Campus, ambiente de testes e outros serviços. Portanto, antes de criar ou executar qualquer migration, é preciso garantir que a conexão do banco de dados da Seleção PRPPG esteja devidamente cadastrada.

Para realizar essa configuração, serão necessárias algumas variáveis de ambiente. Por motivos de segurança, os valores reais não serão apresentados nesta documentação, sendo substituídos por exemplos fictícios. As credenciais corretas devem ser solicitadas pelos canais oficiais da organização.

SELECAO_PRPPG_DB_CONNECTION=pgsql
SELECAO_PRPPG_DB_HOST=postgres
SELECAO_PRPPG_DB_PORT_FORWARD=5450
SELECAO_PRPPG_DB_PORT=5432
SELECAO_PRPPG_DB_USERNAME=admin
SELECAO_PRPPG_DB_PASSWORD=admin123
SELECAO_PRPPG_DB_DATABASE=prppg

7. Adicionando a Conexão no Laravel

Com as variáveis de ambiente configuradas, o próximo passo é acessar o arquivo:

api/config/database.php

Nesse arquivo encontra-se o array associativo connections, responsável por armazenar todas as conexões de banco de dados utilizadas pela aplicação. Cada conexão segue a estrutura:

nome_da_conexao => [
    // configurações
]

Dentro desse array, deve-se criar uma nova entrada correspondente ao banco de dados da Seleção PRPPG.

  env('SELECAO_PRPPG_DB_CONNECTION', 'pgsql_selecao_prppg') => [
            'driver' => 'pgsql',
            'url' => env('DB_URL'),
            'host' => env('SELECAO_PRPPG_DB_HOST', '127.0.0.1'),
            'port' => env('SELECAO_PRPPG_DB_PORT', '5432'),
            'database' => env('SELECAO_PRPPG_DB_DATABASE', 'laravel'),
            'username' => env('SELECAO_PRPPG_DB_USERNAME', 'root'),
            'password' => env('SELECAO_PRPPG_DB_PASSWORD', ''),
            'charset' => env('DB_CHARSET', 'utf8'),
            'prefix' => '',
            'prefix_indexes' => true,
            'search_path' => 'public',
            'sslmode' => 'prefer',
        ],

A configuração da conexão é composta por diversos parâmetros, cada um responsável por definir uma característica específica da comunicação com o banco de dados:

8. Utilizando a Conexão nas Migrations

Com a conexão devidamente cadastrada no Laravel, resta apenas informar às migrations qual conexão deverá ser utilizada durante sua execução.

Isso pode ser feito definindo o atributo $connection na classe da migration:

 

return new class extends Migration
{
    /**
     * The database connection used by the migration.
     *
     * @var string
     */
    protected $connection = 'pgsql_ru';

    /**
     * Class constructor.
     */
    public function __construct()
    {
        // if tests are running, then change the testing connection to pgsql_testing
        if (app()->environment('testing')) {
            $this->connection = 'pgsql_testing';
        }
    }

Ao especificar essa propriedade, o Laravel executará todas as operações daquela migration utilizando a conexão indicada, garantindo que as tabelas sejam criadas, alteradas ou removidas no banco de dados correto. Dessa forma, evita-se que alterações sejam aplicadas acidentalmente em outros bancos de dados configurados na aplicação.

Criando as Migrations

Capítulo ensinando como criar as primeiras migrations da aplicação.

Criando as Migrations

Como criar uma Migration

Tutorial: Criando uma Migration no Laravel

Este tutorial demonstra o processo completo de criação de uma migration no Laravel utilizando como exemplo a tabela usuario.

Ao final deste guia você será capaz de:


1. O que é uma Migration?

Uma migration é um arquivo PHP responsável por definir a estrutura do banco de dados.

Ela funciona como um controle de versão do banco.

Em vez de executar manualmente comandos SQL como:

CREATE TABLE usuarios (...);


o Laravel utiliza migrations:

Schema::create('usuarios', function (Blueprint $table) {
    ...
});


Isso permite que toda a equipe mantenha o banco sincronizado.


2. Criando uma Migration

Para criar uma migration:

php artisan make:migration create_usuarios_table


Será criado um arquivo semelhante a:

database/migrations/
└── 2026_06_10_144334_create_usuarios_table.php


Caso o projeto utilize uma pasta específica:

php artisan make:migration create_usuarios_table \
--path=database/migrations/selecao_prppg



3. Estrutura básica de uma Migration

Toda migration possui dois métodos:

public function up(): void
{
    // cria ou altera tabelas
}

public function down(): void
{
    // desfaz alterações
}



Método up()

Primeiro entramos no ouath-server:

docker exec -it oauth-server bash

Executado quando rodamos:

php artisan migrate --path=database/migrations/selecao_prppg/ --database=pgsql_selecao_prppg


Método down()

Executado quando queremos rever a última migration:

php artisan migrate:rollback --path=database/migrations/selecao_prppg/ --database=pgsql_selecao_prppg
Executado quando queremos reverter todas as migrations


php artisan migrate:fresh --path=database/migrations/selecao_prppg/ --database=pgsql_selecao_prppg



4. Definindo a conexão do banco

No projeto de vocês existe mais de uma conexão.

Por isso foi definido:

protected $connection = 'pgsql_selecao_prppg';


Isso indica que a migration será executada utilizando a conexão:

DB_CONNECTION=pgsql_selecao_prppg


configurada em:

config/database.php



5. Tratamento para ambiente de testes

No construtor:

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


Quando os testes automatizados forem executados:

php artisan test


a migration utilizará:

pgsql_testing


em vez de:

pgsql_selecao_prppg


evitando alterações no banco real.


6. Verificando se a tabela já existe

Antes da criação:

if (!Schema::hasTable('usuarios'))


O Laravel verifica:

SELECT ...
FROM information_schema.tables


Se a tabela já existir:

usuarios


ela não será criada novamente.


7. Criando a tabela

A criação da tabela ocorre através de:

Schema::create('usuarios', function (Blueprint $table) {
    ...
});


Equivalente ao SQL:

CREATE TABLE usuarios (...);



8. Criando a chave primária

Código:

$table->id()
      ->autoIncrement()
      ->primary()
      ->comment('Id do Usuario');


Resultado:

id BIGINT PRIMARY KEY AUTO_INCREMENT


Observação:

No Laravel moderno geralmente basta:

$table->id();


pois o framework já cria:

automaticamente.


9. Criando campos texto

Nome:

$table->string('nome', 100);


Resultado:

VARCHAR(100)



Email:

$table->string('email', 100);


Resultado:

VARCHAR(100)



CPF:

$table->string('cpf', 11);


Resultado:

VARCHAR(11)



10. Definindo valores únicos

Email:

$table->string('email', 100)->unique();


Resultado:

UNIQUE(email)


Não será possível cadastrar dois usuários com o mesmo email.


CPF:

$table->string('cpf', 11)->unique();


Resultado:

UNIQUE(cpf)



11. Trabalhando com datas

Campo:

$table->dateTime('data_cadastro');


Resultado:

DATETIME


ou

TIMESTAMP


dependendo do banco.

Exemplo:

2026-06-10 14:30:00



12. Trabalhando com Enum

Campo:

$table->enum('cargo', [
    'SECRETARIA',
    'COORDENADOR',
    'COMISSAO',
    'ADMINISTRADOR'
]);


Resultado:

ENUM(...)


Valores permitidos:

SECRETARIA
COORDENADOR
COMISSAO
ADMINISTRADOR


Qualquer outro valor será rejeitado.


13. Campo senha

Campo:

$table->string('senha', 60);


Resultado:

VARCHAR(60)


Normalmente armazenará um hash:

$2y$12$...


e não a senha em texto puro.


14. Timestamps automáticos

Código:

$table->timestamps();


Cria automaticamente:

created_at
updated_at


Exemplo:

Campo Função
created_at Data de criação
updated_at Última alteração

15. Executando a Migration

Após criar o arquivo:

php artisan migrate


O Laravel:

  1. Localiza migrations pendentes.
  2. Executa o método up().
  3. Registra a execução na tabela:
migrations



16. Verificando o status

php artisan migrate:status


Exemplo:

Migration name                     Ran?
-----------------------------------------
create_usuarios_table              Yes



17. Revertendo a Migration

Desfaz a última migration executada:

php artisan migrate:rollback


Executa:

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



18. Recriando tudo

Muito utilizado durante desenvolvimento:

php artisan migrate:fresh


Apaga todas as tabelas e executa novamente todas as migrations.


Comandos mais utilizados

Comando Função
php artisan make:migration nome Criar migration
php artisan migrate Executar migrations
php artisan migrate:status Ver status
php artisan migrate:rollback Desfazer última migration
php artisan migrate:fresh Recriar banco
php artisan migrate --path=... Executar pasta específica

Tipos de colunas mais utilizados

Sintaxe Tipo SQL
$table->id() BIGINT PK
$table->string('nome') VARCHAR(255)
$table->string('nome',100) VARCHAR(100)
$table->text('descricao') TEXT
$table->integer('idade') INT
$table->bigInteger('codigo') BIGINT
$table->decimal('valor',10,2) DECIMAL
$table->boolean('ativo') BOOLEAN
$table->date('inicio') DATE
$table->dateTime('inicio') DATETIME
$table->timestamp('inicio') TIMESTAMP
$table->enum('status',[...]) ENUM
$table->json('dados') JSON

Modificadores mais utilizados

Sintaxe Função
->nullable() Permite NULL
->default('X') Valor padrão
->unique() Valor único
->comment('texto') Comentário da coluna
->index() Cria índice
->primary() Define PK
->autoIncrement() Auto incremento

Relacionamentos (Foreign Keys)

Os mais utilizados no projeto Guavira serão:

Forma moderna

$table->foreignId('programa_id')
      ->constrained('programas');


Com cascade

$table->foreignId('programa_id')
      ->constrained('programas')
      ->cascadeOnDelete();


Forma explícita

$table->unsignedBigInteger('programa_id');

$table->foreign('programa_id')
      ->references('id')
      ->on('programas');



Métodos mais utilizados no projeto

Ao criar as tabelas programas, editais, linhas_pesquisa, inscricoes, resultados e demais entidades do sistema, você utilizará principalmente:

Método Frequência
$table->id() Muito alta
$table->string() Muito alta
$table->foreignId() Muito alta
->constrained() Muito alta
$table->date() Alta
$table->enum() Alta
$table->timestamps() Muito alta
->nullable() Alta
->unique() Média
->cascadeOnDelete() Média

Esses métodos são suficientes para construir praticamente todas as tabelas do banco do sistema de seleção PRPPG/Guavira.

Caso Especial: Migration realizada em banco de dados incorreto

Por padrão, as migrations do sistema são registradas no banco da api (oauth-postgres), visto que ele precisa centralizar todas as tabelas da aplicação. Por esse motivo, pode ocorrer situações em que as migrations não ocorrem da maneira esperada visto que elas "já estão criadas".

Caso você queira reverter as migrations do seu banco, deve fazer o seguinte:

Acessar o container do seu banco, como por exemplo o selecao_prppg, entrar no psql e apagar o banco de dados atual, em sequência, você deve criar novamente o banco de dados, que desta vez estará em branco e livre para suportar novas migrations.

Em sequência, você deve acessar o banco de dados da api e verificar as migrations a partir do seguinte comando:

SELECT * FROM migrations

Como resultado, você terá acesso a todas as migrations registradas naquele banco, seu nome e seus id's. A partir dessa informação, devemos realizar o delete das migrations que iremos refazer, utilizando o comando:

DELETE FROM migrations WHERE id = (condição)

Em sequência, podemos realizar as migrations do sistema, que dessa vez não terão nenhum empecilho as impedindo e poderão ser recriadas do zero.

Deploy de alterações do código via git

Deploy de alterações do código via git

Deploy de alterações do código via git

Fluxo de Desenvolvimento com GitLab


1. Criar uma Issue

Antes de iniciar qualquer desenvolvimento, deve ser criada uma Issue no GitLab descrevendo a atividade a ser realizada.

A Issue deve conter:

Após a criação, o GitLab atribuirá um identificador único (ID) à Issue, por exemplo:

#123

Esse ID será utilizado na nomenclatura da branch e nas mensagens de commit.


2. Criar uma Branch

A branch deve ser criada utilizando o ID da Issue.

Para novas funcionalidades

Padrão:

feature/<id-da-issue>-<descricao>


Exemplo:

feature/123-cadastro-de-usuarios


Para correções urgentes (Hotfix)

Padrão:

hotfix/<id-da-issue>-<descricao>


Exemplo:

hotfix/124-correcao-validacao-cpf



3. Atualizar as Referências Locais

Antes de trocar para a nova branch, atualize as referências do repositório:

git pull



4. Trocar para a Branch de Trabalho

Após a criação da branch:

git switch nome-da-branch


Exemplo:

git switch feature/123-cadastro-de-usuarios



5. Recuperar Alterações Temporariamente Armazenadas

Caso tenha utilizado stash anteriormente:

git stash pop



6. Verificar Arquivos Alterados

Antes de realizar o commit:

git status



7. Corrigir Padrão de Código

Caso existam inconsistências no padrão de escrita do código, execute:

make pint-fixer


Esse comando aplica automaticamente as regras de formatação definidas pelo projeto.


8. Adicionar Arquivos ao Commit

Após validar as alterações:

git add .



9. Verificar Novamente o Status

Confirme os arquivos que serão enviados:

git status



10. Realizar o Commit

A mensagem do commit deve obrigatoriamente referenciar a Issue correspondente.

Padrão:

git commit -m "#<id> descrição da alteração"


Exemplos:

git commit -m "#123 adiciona cadastro de usuários"


git commit -m "#124 corrige validação de CPF"



11. Enviar Alterações para o Repositório Remoto

git push


Caso seja o primeiro push da branch:

git push -u origin nome-da-branch


Exemplo:

git push -u origin feature/123-cadastro-de-usuarios



12. Criar Merge Request

Após concluir o desenvolvimento:

  1. Acesse o GitLab;
  2. Crie um Merge Request da branch criada para a branch principal do projeto;
  3. Vincule a Issue correspondente;
  4. Solicite revisão de código;
  5. Aguarde aprovação e merge.

Resumo do Fluxo

git pull
git switch feature/123-cadastro-de-usuarios
git stash pop
git status
make pint-fixer
git add .
git status
git commit -m "#123 adiciona cadastro de usuários"
git push

Criação de um CRUD

Passo a passo de como criar um CRUD simples para novas tabelas do Banco de Dados

Criação de um CRUD

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:

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:

<?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").

Resource

Assim como existe um arquivo para validar os dados enviados pela request, existe também um arquivo que define como a response será devolvida pela API. Neste projeto, a response é enviada em formato JSON.

<?php
// app/Http/Resources/Selecao_prppg/Programa/ProgramaResource.php

namespace App\Http\Resources\Selecao_prppg\Programa;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class ProgramaResource extends JsonResource
{
    public function __construct(protected $data)
    {
        parent::__construct($data);
    }

    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'codigo' => $this->codigo,
            'nome' => $this->nome,
            'coordenador_id' => $this->coordenador_id,
            'created_at' => $this->created_at ? date('d/m/Y H:i:s', strtotime($this->created_at)) : null,
            'updated_at' => $this->updated_at ? date('d/m/Y H:i:s', strtotime($this->updated_at)) : null,
            'deleted_at' => $this->deleted_at ? date('d/m/Y H:i:s', strtotime($this->deleted_at)) : null,
            'created_by' => $this->created_by,
            'updated_by' => $this->updated_by,
            'deleted_by' => $this->deleted_by,
        ];
    }
}
Tabela: propriedades e métodos de um Resource

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

Propriedade / Método Tipo Função
extends JsonResource Herança Dá à classe toda a infraestrutura de transformação de dados do Laravel — é o que permite tratar o Model recebido como se fosse um objeto comum ($this->id, $this->nome) dentro do toArray().
__construct(protected $data) Método (sobrescrito) Recebe o Model que será transformado e repassa para o construtor original (parent::__construct($data)), que guarda essa instância internamente. No nosso caso, essa sobrescrita não muda nenhum comportamento em relação ao construtor padrão do JsonResource — existe só por consistência de estilo com RestauranteResource.
toArray(Request $request): array Método (contrato do Laravel) O método que realmente importa: define exatamente quais campos aparecem na resposta JSON, em qual ordem, sob qual nome e em qual formato. É chamado pelo BaseController::find() via $this->resource::make($model)->toArray($request).
'campo' => $this->coluna Exposição direta Repassa o valor de uma coluna do Model sem alteração (ex.: 'id' => $this->id, 'codigo' => $this->codigo). O $this->coluna funciona porque JsonResource delega, por baixo dos panos, qualquer propriedade não encontrada na própria classe para o Model que está envolvendo.
'campo' => $this->coluna ? formata(...) : null Formatação condicional Reformata o valor antes de expor — no nosso caso, date('d/m/Y H:i:s', strtotime($this->created_at)) para as datas — com uma verificação prévia (? :) para não quebrar quando o valor for null (ex.: deleted_at de um registro que nunca foi removido).
$request (parâmetro de toArray) Parâmetro A requisição HTTP atual. Não é usado no nosso ProgramaResource hoje, mas está disponível caso a resposta precise variar conforme algo da requisição (ex.: mostrar campos extras só se um parâmetro de query específico estiver presente).

Controller

O Controller é a camada que recebe a requisição HTTP e devolve uma resposta HTTP — ele é o "recepcionista" da aplicação: pega o pedido que chegou pela rota, repassa para quem realmente sabe resolver (o Service), e traduz o resultado de volta para um formato que o cliente (Postman, o front-end React, etc.) entenda.

É a camada mais externa de todas — a única que efetivamente conversa com o protocolo HTTP (status codes, headers, corpo da requisição/resposta). Nenhuma outra camada do projeto sabe o que é um JsonResponse ou um código 404 — só o Controller.

class ProgramaController extends BaseController
{
    public function __construct()
    {
        parent::__construct(service: app(ProgramaService::class));

        $this->setResource(ProgramaResource::class);
        $this->storeFormRequest = new ProgramaStoreRequest;
        $this->updateFormRequest = new ProgramaUpdateRequest;
    }
}

Repare: não existe nenhum método index, store, find, update ou delete escrito aqui. O ProgramaController inteiro se resume a dizer, no construtor, quatro coisas — cada uma "encaixando uma peça" no comportamento genérico herdado:

O que é passado Para que o BaseController usa isso
service: app(ProgramaService::class) Guarda em $this->service — é para onde index(), store(), find(), update(), delete() repassam toda chamada de negócio.
$this->setResource(ProgramaResource::class) Diz qual classe formata a resposta de find().
$this->storeFormRequest = new ProgramaStoreRequest Diz de onde vêm as regras de validação usadas em store().
$this->updateFormRequest = new ProgramaUpdateRequest Diz de onde vêm as regras de validação usadas em update().

Base Controller

<?php

namespace App\Http\Controllers\Api;

use App\Adapter\ApiResponse;
use App\Http\Controllers\Controller;
use App\Http\Requests\ListGetRequest;
use App\Http\Resources\DefaultResource;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class BaseController extends Controller
{
    private $resource;
    protected FormRequest $storeFormRequest;
    protected FormRequest $updateFormRequest;

    public function __construct(protected $service)
    {
        $this->setResource(DefaultResource::class);
        $this->storeFormRequest = new FormRequest;
        $this->updateFormRequest = new FormRequest;
    }

    public function setResource($resource): void
    {
        $this->resource = $resource;
    }

    public function getResource(): mixed
    {
        return $this->resource;
    }

    public function index(ListGetRequest $request): JsonResponse
    {
        $page = $request->get('page', 1);
        if (! is_numeric($page) || $page < 1) {
            return ApiResponse::sendResponse(message: 'Invalid page number', statusCode: Response::HTTP_BAD_REQUEST);
        }

        $perPage = $request->get('per_page', 15);
        if (! is_numeric($perPage) || $perPage < 1) {
            return ApiResponse::sendResponse(message: 'Invalid per_page value', statusCode: Response::HTTP_BAD_REQUEST);
        }

        $filter = $request->get('filter', []);
        if (! is_array($filter)) {
            return ApiResponse::sendResponse(message: 'Filter must be an array', statusCode: Response::HTTP_BAD_REQUEST);
        }

        $order = $request->get('order_by', []);
        if (! is_array($order)) {
            return ApiResponse::sendResponse(message: 'Order must be an array', statusCode: Response::HTTP_BAD_REQUEST);
        }

        foreach ($order as $key => $value) {
            if (! in_array(strtolower($value), ['asc', 'desc'], true)) {
                return ApiResponse::sendResponse(
                    message: 'Invalid order value for '.$key.'. Use "asc" or "desc".',
                    statusCode: Response::HTTP_BAD_REQUEST
                );
            }
        }

        $data = $this->service->paginate(page: $page, totalPerPage: $perPage, filter: $filter, order: $order);

        return ApiResponse::sendListResponse(data: $data);
    }

    public function store(Request $request): JsonResponse
    {
        try {
            $validated = $this->validate($request, $this->storeFormRequest->rules());
            $data = $this->service->create($validated);
            if (! empty($data)) {
                return ApiResponse::sendResponse(message: 'item created successfully', statusCode: Response::HTTP_CREATED);
            }
            return ApiResponse::sendResponse(message: 'something went wrong', statusCode: Response::HTTP_BAD_REQUEST);
        } catch (\Exception $ex) {
            return ApiResponse::sendResponse(message: $ex->getMessage().' | something went wrong', statusCode: Response::HTTP_BAD_REQUEST);
        }
    }

    public function find(Request $request): JsonResponse
    {
        try {
            if (empty($request->id)) {
                return ApiResponse::sendResponse(message: 'parameter "id" missing', statusCode: Response::HTTP_NOT_FOUND);
            }

            $resourceData = $this->resource::make($this->service->find($request->id));

            return ApiResponse::sendResponse(data: $resourceData->toArray($request), statusCode: Response::HTTP_OK);
        } catch (\Exception $ex) {
            return ApiResponse::sendResponse(message: $ex->getMessage().' | model not found', statusCode: Response::HTTP_NOT_FOUND);
        }
    }

    public function update(Request $request, int $id): JsonResponse
    {
        try {
            if (empty($id)) {
                return ApiResponse::sendResponse(message: 'parameter "id" missing', statusCode: Response::HTTP_NOT_FOUND);
            }

            $validated = $this->validate($request, $this->updateFormRequest->rules());
            $updated = $this->service->update($validated, $id);
            if ($updated) {
                return ApiResponse::sendResponse(statusCode: Response::HTTP_NO_CONTENT);
            }
            return ApiResponse::sendResponse(message: 'something went wrong', statusCode: Response::HTTP_BAD_REQUEST);
        } catch (\Exception $ex) {
            return ApiResponse::sendResponse(message: $ex->getMessage().' | something went wrong', statusCode: Response::HTTP_BAD_REQUEST);
        }
    }

    public function delete(int $id): JsonResponse
    {
        try {
            if (empty($id)) {
                return ApiResponse::sendResponse(message: 'parameter "id" missing', statusCode: Response::HTTP_NOT_FOUND);
            }

            $deleted = $this->service->delete($id);
            if ($deleted) {
                return ApiResponse::sendResponse(statusCode: Response::HTTP_NO_CONTENT);
            }
            return ApiResponse::sendResponse(message: 'something went wrong', statusCode: Response::HTTP_NOT_FOUND);
        } catch (\Exception $ex) {
            return ApiResponse::sendResponse(message: $ex->getMessage().' | model not found', statusCode: Response::HTTP_NOT_FOUND);
        }
    }
}

Rotas

As rotas são o mapa que diz ao Laravel: "quando chegar uma requisição HTTP com tal método (GET/POST/PUT/DELETE) e tal endereço (URL), execute tal método de tal Controller". É a porta de entrada de toda a aplicação — antes das rotas, nada do que vimos até agora (Controller, Service, Repository, Model) é executado.

Como o projeto organiza os arquivos de rota

Ao invés de um único arquivo gigante com todas as rotas da aplicação inteira, o projeto divide por módulo. Tudo começa em routes/api.php:

Route::group(['namespace' => 'Api', 'prefix' => 'v1'], function () {
    require 'api/v1/usuario.php';
    require 'api/v1/pessoa.php';
    require 'api/v1/ru.php';
    require 'api/v1/selecao_prppg.php';
    // ...
});
Anatomia de uma rota, peça por peça

Vamos usar a rota de listagem de Programa como exemplo:

Route::get('/', [ProgramaController::class, 'index'])
    ->name('lista-programas')
    ->middleware('auth:api');
Peça Função
Route::get(...) Diz qual método HTTP essa rota responde. Existe um método do facade Route para cada verbo: get, post, put, delete, patch.
'/' O caminho (URI) dessa rota, relativo a tudo que veio antes dela nos grupos externos (prefix).
[ProgramaController::class, 'index'] Diz qual Controller e qual método deve tratar essa requisição — é a "ligação" entre a rota e o código que já detalhamos nas mensagens anteriores.
->name('lista-programas') Dá um apelido para essa rota, usado para gerar URLs a partir do nome (route('lista-programas')) em vez de escrever a URL na mão em outros lugares do código — evita que, se a URL mudar um dia, seja preciso caçar e trocar em todo lugar.
->middleware('auth:api') Adiciona uma camada de verificação que roda antes do Controller. auth:api exige um token Bearer válido — se não tiver, a requisição nem chega no ProgramaController::index, é barrada antes, com 401.
O arquivo completo de rotas de Programa, explicado por blocos
<?php

use App\Http\Controllers\Api\Selecao_prppg\ProgramaController;
use Illuminate\Support\Facades\Route;

Route::middleware(['auth:api'])->prefix('selecao-prppg')->group(function () {

    Route::middleware(['auth:api'])->prefix('programas')->group(function () {
        // ...as 5 rotas do CRUD aqui dentro
    });

});

Isso é um grupo dentro de outro grupo. Cada Route::...->group(function () { ... }) cria um "envelope" que aplica alguma configuração (prefixo, middleware) a tudo que estiver escrito dentro dele, sem precisar repetir em cada rota individual.

Grupo externoprefix('selecao-prppg'): toda rota dentro daqui vai começar com /api/v1/selecao-prppg/... (o /api/v1 já veio do routes/api.php).

Grupo internoprefix('programas'): dentro do grupo externo, mais um prefixo é somado: /api/v1/selecao-prppg/programas/....

middleware(['auth:api']) repetido nos dois níveis: tecnicamente bastaria declarar uma vez (no grupo mais externo já seria suficiente, porque o middleware "desce" para dentro de qualquer grupo aninhado). O projeto repete em todos os níveis por segurança/clareza extra — reforçando visualmente, em qualquer ponto do arquivo, que aquele trecho exige autenticação.