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
- Criando as conexões no ambiente Laravel
- Criando as Migrations
- Deploy de alterações do código via git
- Criação de um CRUD
Preparação do Ambiente Virtual Conteinerizado
Tutorial de como realizar o deploy dos contêineres necessários para iniciar o desenvolvimento do Guavira.
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.
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:
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:
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:
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:
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 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:
- O nome da conexão e o valor padrão utilizado caso a variável de ambiente correspondente não seja encontrada;
- O driver de banco de dados utilizado pela conexão;
- A URL de conexão, utilizada como configuração genérica dentro do projeto;
- O host do banco de dados e seu valor padrão;
- A porta em que o serviço está escutando e seu valor padrão;
- O nome do banco de dados e seu valor padrão;
- O usuário utilizado para autenticação e seu valor padrão;
- A senha da conexão e seu valor padrão;
- O conjunto de caracteres (charset) utilizado pelo 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.
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:
- Criar uma migration;
- Definir a conexão do banco utilizada;
- Criar tabelas e colunas;
- Definir chaves primárias;
- Definir restrições de unicidade;
- Utilizar enums;
- Executar migrations;
- Reverter migrations;
- Compreender os tipos de colunas mais utilizados.
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
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:
- auto increment
- primary key
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:
- Localiza migrations pendentes.
- Executa o método
up(). - 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
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:
- Título claro e objetivo;
- Descrição detalhada da demanda;
- Critérios de aceitação, quando aplicável;
- Labels apropriadas (feature, bug, hotfix, etc.).
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:
- Acesse o GitLab;
- Crie um Merge Request da branch criada para a branch principal do projeto;
- Vincule a Issue correspondente;
- Solicite revisão de código;
- 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
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ó marcadeleted_at. - Log de atividades: toda alteração fica registrada automaticamente.
- Formatação automática de datas:
$programa->created_atjá vem formatado como07/07/2026 09:12:44, sem você precisar formatar manualmente. - Relacionamentos de auditoria prontos:
$programa->createdByte dá o objetoUsuarioque 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 chamastore()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"). |
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 externo — prefix('selecao-prppg'): toda rota dentro daqui vai começar com /api/v1/selecao-prppg/... (o /api/v1 já veio do routes/api.php).
Grupo interno — prefix('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.