Aplicação web com Laravel
Rotas, controladores, modelos e vistas para recursos REST apoiados na base de dados.
Um GET /sessoes precisa de escolher uma operação, consultar sessões e produzir uma resposta. No Laravel, a rota encaminha o pedido, o controlador coordena a operação, o modelo representa dados e relações e a vista produz HTML. Middleware, policies e serviços participam quando a operação exige autenticação, autorização ou regras de negócio.
O recurso Sessões de ponta a ponta
No percurso de um pedido, distingue o software pelo papel:
| Peça | Papel | Exemplos |
|---|---|---|
| Navegador | Pede recursos e apresenta a página | Firefox, Brave |
| Servidor HTTP | Recebe pedidos e devolve recursos ou encaminha a aplicação | Apache HTTP Server, Nginx |
| Aplicação | Executa regras e produz a resposta dinâmica | Uma aplicação Laravel executada em PHP |
| Base de dados | Guarda e consulta dados | PostgreSQL |
O servidor pode devolver um ficheiro estático ou obter uma resposta da aplicação. O navegador é o cliente deste percurso. O motor que interpreta e apresenta HTML no navegador não substitui o servidor HTTP.
Um recurso é algo identificável por um endereço, como uma sessão ou a lista de sessões. A aplicação define que operações permite sobre ele. As sessões da loja são um recurso. Estas rotas usam métodos HTTP com a semântica esperada; uma aplicação REST também tem outras condições, explicadas em HTTP:
Route::get('/sessoes', [SessaoController::class, 'index']);
Route::post('/sessoes', [SessaoController::class, 'store'])->middleware('auth');
O GET lista, o POST cria. O controlador recebe o pedido já encaminhado e devolve uma resposta, sem gerar HTML à mão:
// ler não muda nada: pode repetir-se e ficar na história do navegador
Route::get('/sessoes', [SessaoController::class, 'index']);O index lê as sessões futuras ordenadas e entrega-as à vista. Nada é escrito, por isso recarregar a página é seguro.
// escrever muda o estado: exige validação e redirecionamento
Route::post('/sessoes', [SessaoController::class, 'store'])->middleware('auth');O store verifica a permissão, valida os dados e só depois escreve. Em caso de erro volta ao formulário; em caso de sucesso redireciona, para recarregar não criar a sessão duas vezes.
// Excerto: Request, Gate e Sessao são importados no controlador.
class SessaoController extends Controller
{
public function index()
{
$sessoes = Sessao::where('data_hora', '>', now())
->orderBy('data_hora')
->get();
return view('sessoes.index', ['sessoes' => $sessoes]);
}
public function store(Request $request)
{
Gate::authorize('create', Sessao::class);
$dados = $request->validate([
'evento_id' => 'required|exists:eventos,id',
'data_hora' => 'required|date|after:now',
'preco' => 'required|numeric|min:0',
'lotacao' => 'required|integer|min:1',
]);
Sessao::create($dados);
return redirect('/sessoes', 303);
}
}
O método validate é o ponto a fixar: se os dados cumprirem as regras, devolve os campos validados. Não os sanitiza para todos os destinos. Se falharem num formulário web, interrompe e redireciona com os erros; um pedido que espera JSON recebe normalmente 422. A regra exists:eventos,id repete na aplicação a chave estrangeira da base de dados, para dar erro amigável antes do erro técnico. A regra after:now valida a criação de uma sessão futura. Sessões antigas continuam a existir no histórico.
Este exemplo pressupõe SessaoPolicy associada ao modelo: o seu método create recebe o utilizador autenticado e permite apenas os papéis que podem gerir sessões. Gate é a facade Illuminate\Support\Facades\Gate e recusa com 403 quando a policy não permite; o formulário usa @csrf e apresenta os erros com @error e os valores anteriores com old(...).
Migração e modelo
A tabela nasce de uma migração, código versionado que cria e altera o esquema. Este excerto cria as colunas e a FK da tabela do esquema relacional, com campos adicionais de criação e atualização:
Schema::create('sessoes', function (Blueprint $table) {
$table->id();
$table->foreignId('evento_id')->constrained('eventos');
$table->dateTime('data_hora');
$table->decimal('preco', 8, 2);
$table->integer('lotacao');
$table->timestamps();
});
O excerto de migração não declara os CHECK de preço e lotação. Mantém essas garantias no SQL de PostgreSQL, além da validação do formulário. Se usares o esquema sem created_at e updated_at, o modelo deve declarar public $timestamps = false.
O modelo liga a classe à tabela e declara o que se pode preencher em massa:
class Sessao extends Model
{
protected $table = 'sessoes';
protected $fillable = ['evento_id', 'data_hora', 'preco', 'lotacao'];
public function evento()
{
return $this->belongsTo(Evento::class);
}
}
O nome da tabela é explícito porque as convenções de pluralização não garantem os nomes portugueses do esquema. Os restantes modelos precisam do mesmo cuidado. O belongsTo espelha a associação do UML: cada sessão pertence a um evento. A partir daqui, $sessao->evento->titulo navega da sessão ao evento sem SQL escrito à mão.
Um POST que falha a validação
Com um utilizador autorizado a criar sessões, preenche o formulário de nova sessão com lotação 0 e guarda. O pedido POST /sessoes chega ao store, o validate rejeita lotacao por violar min:1, e o utilizador volta ao formulário com a mensagem de erro junto ao campo e os restantes valores preservados. Nenhuma linha tocou na base de dados, porque a validação correu antes de qualquer escrita.
Se a autorização e a validação passassem, o create inseria a sessão e o redirecionamento voltava à lista. Se dois compradores disputarem o último lugar, a validação anterior à compra pode estar desatualizada. O protocolo de concorrência decide qual compra pode confirmar.
Para isolar a condição «inteiro positivo», o bloco usa um inteiro PHP onde um formulário HTTP enviaria texto, que o validador Laravel também precisa de interpretar:
<?php
function validar($dados) {
$erros = [];
if (!isset($dados['lotacao']) || !is_int($dados['lotacao']) || $dados['lotacao'] < 1) {
$erros['lotacao'] = 'A lotação tem de ser um inteiro a partir de 1.';
}
return $erros;
}
$erros = validar(['evento_id' => 3, 'preco' => 10, 'lotacao' => 0]);
foreach ($erros as $campo => $mensagem) {
echo "$campo: $mensagem
";
}Executa o código para ver a saída e os gráficos.
Num formulário Laravel, o erro é apresentado junto ao campo. O bloco não reproduz a conversão das strings HTTP nem todas as regras do validador.
Na sequência da página da lista, a vista gera HTML no servidor; o navegador recebe esse HTML e decide quando enviar outro pedido.
Entrar e proteger rotas
A loja tem registo e login nos requisitos, e as compras exigem saber quem compra. A autenticação no Laravel segue o mesmo molde do recurso: formulário, validação e tentativa de entrada.
public function entrar(Request $request)
{
$dados = $request->validate([
'email' => 'required|email',
'palavra_passe' => 'required',
]);
if (!Auth::attempt([
'email' => $dados['email'],
'password' => $dados['palavra_passe'],
])) {
return back()->withErrors([
'email' => 'Credenciais inválidas.',
]);
}
$request->session()->regenerate();
return redirect('/sessoes');
}
Este fragmento pressupõe o modelo autenticável e a coluna password do starter Laravel. Para usar utilizadores.palavra_passe_hash do esquema anterior, configura o provider e o modelo para essa tabela e esse atributo de hash. A chave password das credenciais continua a receber a palavra-passe em claro para a API a verificar. Não passes o hash como credencial.
O Auth::attempt verifica a palavra-passe com hash e inicia a sessão. Se falhar, volta atrás com erro genérico de propósito: dizer “email certo, palavra-passe errada” ajuda quem ataca a adivinhar contas.
Com a entrada feita, protege as rotas que precisam de comprador autenticado com o middleware auth:
Route::middleware('auth')->group(function () {
Route::post('/compras', [CompraController::class, 'store']);
});
Quem não entrou é redirecionado para o login em vez de chegar ao controlador. A autorização sobre a operação e o recurso precisa de uma policy, gate ou verificação equivalente no servidor.
Ambiente e ferramentas
O ambiente do projeto usa PHP e Laravel para a aplicação, Composer para as dependências, PostgreSQL para os dados e Docker Compose para a infraestrutura local. Git e GitLab guardam versões e permitem rever as alterações do grupo. Usa o template e o guia da edição atual, ligados na apresentação da cadeira.
Uma imagem Docker descreve o sistema de ficheiros e a configuração que servem para criar contentores. Um contentor é uma instância em execução dessa imagem. Um volume conserva os dados fora da camada descartável do contentor. Parar ou recriar o contentor não é o mesmo que apagar o volume da base de dados.
Blade: gerar uma vista
view('sessoes.index', ['sessoes' => $sessoes]) passa os dados à vista resources/views/sessoes/index.blade.php. Blade compila o template para PHP:
<ul>
@foreach ($sessoes as $sessao)
<li>{{ $sessao->evento->titulo }}: {{ $sessao->data_hora }}</li>
@endforeach
</ul>
@foreach repete o elemento para cada sessão. {{ ... }} escreve texto escapado para HTML. O acesso a evento precisa da relação do modelo e pode provocar consultas adicionais, como veremos no caso N+1.
Ler o âmbito de uma variável em PHP
Uma variável global não fica automaticamente disponível numa função nomeada. Os parâmetros e variáveis locais pertencem à chamada. No programa abaixo, a $taxa local de calcular vale 1; a global continua a valer 4.
<?php
$taxa = 4;
function calcular($base) {
$taxa = 1;
return $base + $taxa;
}
function aumentarTaxa() {
global $taxa;
$taxa++;
}
echo calcular(10) . PHP_EOL;
echo $taxa . PHP_EOL;
aumentarTaxa();
echo $taxa . PHP_EOL;
$ajustar = fn($n) => $n + $taxa;
$taxa = 9;
echo $ajustar(10) . PHP_EOL;Executa o código para ver a saída e os gráficos.
global $taxa em aumentarTaxa liga o nome local à variável global e permite alterá-la. A função arrow captura o valor 5 ao ser criada; mudar depois a global para 9 não muda essa captura. Uma closure escrita com function precisa de use ($taxa) para essa captura por valor, ou use (&$taxa) para uma referência.
Ler uma variável local não definida gera um aviso no PHP atual; não soma silenciosamente a global do mesmo nome. Ao prever uma saída, começa por identificar onde cada nome foi definido. Para aplicações, passar dados explicitamente torna as dependências da função mais fáceis de seguir.
Separar responsabilidades no pedido
Uma rota escolhe a operação. Middleware pode verificar sessão e proteção CSRF antes do controlador. O controlador coordena o caso de uso, a policy decide a autorização, o modelo representa dados e relações, e a vista produz a representação. Nem todos os pedidos usam uma vista HTML: uma API pode devolver JSON, e uma operação pode redirecionar ou devolver 204.
Validação pergunta se os dados têm o formato e o domínio esperados. Autorização pergunta se esta pessoa pode executar esta operação sobre este objeto. Autenticação identifica a pessoa. As três são necessárias em contextos diferentes e nenhuma substitui as restantes.
public function atualizar(Request $request, Bilhete $bilhete)
{
Gate::authorize('update', $bilhete);
$dados = $request->validate(['estado' => 'required|in:reservado,pago,cancelado']);
// A transição de estado e a capacidade exigem ainda a regra de negócio.
return $this->servicoCompras->alterarEstado($bilhete, $dados['estado']);
}
Este fragmento usa a facade Gate e mostra a separação; pressupõe uma policy e um serviço com o protocolo de compra definido. Autorizar uma alteração não torna válida qualquer transição. Por exemplo, passar de cancelado a pago pode exigir voltar a reservar capacidade.
ORM e o problema N+1
Se carregares 100 sessões e, num ciclo, acederes a $sessao->evento, o carregamento preguiçoso pode fazer uma consulta inicial e mais 100 consultas. É o problema N+1. Carregamento antecipado, como Sessao::with('evento')->get(), pode carregar as relações em consultas agrupadas. Mede as consultas e os dados transferidos; antecipar todas as relações sem necessidade também custa recursos.
O mesmo cuidado aplica-se a verificações de autorização dentro de ciclos: um @can por linha numa lista pode reavaliar a policy para cada objeto e repetir consultas que a decisão usa. Quando a decisão é igual para todas as linhas, avalia-a uma vez fora do ciclo; quando depende de cada objeto, confirma que as relações usadas pela policy já vêm carregadas. Uma verificação barata em isolamento pode dominar o tempo da página quando é repetida cem vezes.
O modelo não é a página. Não coloques decisões de autorização só no template: um pedido direto pode contorná-lo. Não aceites campos privilegiados só porque estão no $fillable; esse atributo limita a atribuição em massa, não determina quem pode alterar cada valor.
Resposta e proteção de dados
Usa parâmetros para valores nas consultas; valida escolhas de coluna e direção de ordenação por uma lista explícita. Escapa dados na vista e usa proteção CSRF nas rotas de alteração que recebem cookies. Quando a base de dados recusar uma regra conhecida, traduz o resultado para uma resposta útil sem expor SQL, credenciais ou stack traces.
Uma atualização feita em vários modelos pode precisar de DB::transaction(...), mas só envolver código nessa função não resolve a corrida do último lugar. Aplica também a coordenação de transações.
Onde cada falha aparece no percurso
Os caminhos abaixo partem de POST /sessoes num formulário web. Cada recusa interrompe o percurso; o mesmo pedido não atravessa todas as falhas.
| Condição | Camada que recusa | Resultado |
|---|---|---|
| Sem login | Middleware auth | Redirecionamento para iniciar sessão. |
| Com login, sem permissão de criar | Policy chamada por Gate | 403, antes da validação e da escrita. |
| Autorizado, lotação 0 | Validação min:1 | Regresso ao formulário com erros e valores preservados. |
| Autorizado, evento eliminado após validar | FK na base de dados | Escrita recusada; a aplicação traduz a falha conhecida sem mostrar SQL. |
Num cliente que espera JSON, autenticação e validação podem devolver 401 e 422 em vez de redirecionar. O contrato da resposta precisa de declarar essa diferença.
Validar uma funcionalidade num protótipo vertical
Um protótipo vertical implementa um percurso através de todas as camadas. Uma lista desenhada com dados fixos pode validar a disposição da interface, mas não demonstra que a aplicação consulta, autoriza ou altera os dados. A8 serve para experimentar a arquitetura com funcionalidades do projeto; as histórias obrigatórias vêm dos requisitos e checklists da edição atual.
Para a história de cancelar uma reserva, liga os artefactos ao mesmo comportamento:
| Artefacto | Decisão que chega à implementação |
|---|---|
| A2 | O comprador cancela uma reserva sua e liberta um lugar uma única vez. |
| A3 | O bilhete mostra a ação; sucesso e recusa têm estados reconhecíveis. |
| A4 e A5 | Bilhete tem comprador, sessão e estado, com referências válidas. |
| A6 | Transição e capacidade são atómicas, com isolamento justificado. |
| A7 | Método, caminho, permissões, parâmetros e respostas estão especificados. |
| A8 | Interface, controlador, autorização e PostgreSQL executam esse percurso. |
Um contrato possível usa POST /bilhetes/{id}/cancelamento. Num formulário web, a ação processa e redireciona para uma página de resultado, preservando os dados necessários em caso de erro. Uma operação AJAX pode devolver JSON e códigos de falha, desde que isso conste do contrato e o cliente os interprete. Não devolvas um redirecionamento para HTML a um cliente que espera sempre o JSON de um bilhete.
Para verificar o percurso, começa com uma reserva de U7 e zero lugares livres. Entra como U7, cancela e confirma na interface e nos dados que o estado mudou e há um lugar livre. Recarrega: a alteração deve persistir. Repete: não deve libertar outro lugar. Com U8, um pedido direto ao mesmo endereço deve ser recusado, mesmo que o botão nem exista na sua página.
Depois verifica uma sessão esgotada, uma falha técnica e o retorno ao formulário. Uma mensagem de sucesso isolada não prova a escrita, e uma consulta bem-sucedida não prova autorização. Este é um percurso didático; não substitui as restantes histórias exigidas para o protótipo nem a verificação do produto no ambiente indicado pela cadeira.
Exercícios
Situar o servidor HTTP
Um pedido sai de Brave, chega a Nginx, executa a aplicação PHP e consulta PostgreSQL. Identifica o papel de cada peça.
Pista
Segue a direção do pedido.
Mais uma pista
Apresentar uma página e responder por HTTP são responsabilidades diferentes.
Solução
Brave é o navegador cliente. Nginx recebe o pedido HTTP. A aplicação PHP executa a lógica e produz a resposta dinâmica; PostgreSQL consulta os dados. Um servidor também pode devolver ficheiros sem executar essa lógica.
Confere a tua resposta:
- Coloco o navegador no cliente.
- Identifico Nginx como servidor HTTP.
- Distingo a aplicação PHP da base de dados.
Erros frequentes
Não confundas o motor de apresentação do navegador com o servidor web.
Ler uma captura por valor
Em PHP, executas
$taxa = 3; $f = fn($n) => $n + $taxa; $taxa = 8; echo $f(10);. Que número aparece?
Pista
A arrow function captura automaticamente por valor.
Mais uma pista
Observa o valor quando a função foi criada.
Solução
Aparece 13. A captura conservou 3. Alterar depois a variável exterior para 8 não altera essa captura.
Erros frequentes
Não confundas fn com uma closure que declara use (&$taxa).
Identificar a verificação em falta
Uma rota com middleware auth carrega qualquer bilhete por id. Que falta verificar antes de o mostrar?
Pista
Entrar e poder consultar são decisões distintas.
Mais uma pista
O recurso pode pertencer a outra pessoa.
Solução
É necessário verificar uma policy ou regra equivalente sobre o objeto. Autenticação identifica; autorização concede a operação sobre esse recurso.
Confere a tua resposta:
- Identifico o utilizador autenticado.
- Verifico autorização para aquele bilhete.
- Recuso leitura de recurso alheio sem permissão.
Erros frequentes
Não uses o facto de existir um id como permissão de acesso.
Contar consultas por relação
Uma consulta lê 40 sessões e cada acesso ao evento faz outra consulta. Quantas consultas há neste cenário?
Pista
Conta a consulta inicial mais uma por sessão.
Mais uma pista
A relação não está carregada nem reutilizada neste enunciado.
Solução
São 1+40=41. Eager loading pode agrupar o carregamento, segundo o ORM e a relação.
Erros frequentes
Não assumes que navegar num objeto é uma operação sem I/O.
Limitar campos não autoriza pessoas
Colocar estado em fillable prova que qualquer comprador pode alterar o estado de qualquer bilhete?
Pista
Pergunta que pessoa e que objeto a lista conhece.
Mais uma pista
Uma lista de atributos não expressa pertença.
Solução
Não. Usa autorização sobre o recurso e valida a transição, além de limitar atributos.
- Não. É uma regra de atribuição em massa.. Permissões e transições de negócio continuam a ser verificadas.
- Sim, a lista é a policy.. A lista não depende do proprietário do objeto.
- Sim, se os dados forem inteiros.. O tipo não prova autorização.
Erros frequentes
Não confundas filtragem de dados com autorização.
Definir o contrato de uma resposta
Uma página carrega lugares disponíveis com GET /api/sessoes/{id}. Propõe o contrato do recurso para que o JavaScript distinga uma sessão visível, uma sessão ausente e uma falha técnica. Basta documentar os parâmetros?
Pista
O cliente tem de decidir como ler cada resposta.
Mais uma pista
Inclui códigos HTTP, Content-Type, campos e permissões.
Solução
Não basta documentar parâmetros. Um contrato possível tem id no caminho, 200 com JSON de campos públicos da sessão visível, 404 para ausência ou existência não revelada e 500 sem detalhes internos para uma falha técnica. O cliente verifica o estado antes de usar json(). Pode haver outro contrato válido, desde que tipo, estrutura e tratamento de erros sejam explícitos. A disponibilidade indicada pode mudar antes da reserva.
Confere a tua resposta:
- Defino método, caminho e parâmetros.
- Declaro permissões e campos públicos devolvidos.
- Declaro código, tipo e estrutura do corpo em sucesso e falha.
- Escolho
json()apenas para o corpo JSON definido.
Erros frequentes
Responder 200 com uma página HTML de erro quebra um cliente que espera JSON de uma sessão. Documentar o recurso não dispensa autorização e coordenação no servidor.
Localizar a camada que recusa
Pista
Segue a ordem do pedido.
Mais uma pista
Auth, policy, validação e base.
Solução
Confere a tua resposta:
- Distingo auth, policy, validação e restrição.
- Prevejo código e resposta.
- Não exponho detalhes internos.
Erros frequentes
Não confies só na validação para concorrência.
Distinguir uma demonstração de interface de um protótipo vertical
O grupo mostra uma página de bilhetes com dados fixos. Clicar em Cancelar muda o texto para cancelado e mostra sucesso, mas não envia qualquer pedido. A história diz que o comprador cancela uma reserva sua e liberta um lugar uma única vez. Que parte da arquitetura foi demonstrada? Descreve o percurso e três verificações que faltam para demonstrar esta história num protótipo vertical.
Pista
Segue o efeito para além do DOM, até aos dados guardados.
Solução
A demonstração exercita um estado da interface, mas não o contrato HTTP, a autorização, a regra de negócio nem a persistência. Um percurso completo envia o pedido documentado, autentica, verifica o proprietário, executa a transição e a libertação de capacidade atomicamente em PostgreSQL e interpreta a resposta na interface.
- Após sucesso, recarregar e consultar o estado guardado confirma bilhete cancelado e um lugar libertado. Dados fixos voltariam ao estado inicial.
- Repetir o pedido conserva o cancelamento e não liberta outro lugar. Uma mensagem de sucesso não basta para demonstrar essa invariante.
- Um comprador diferente envia diretamente o mesmo pedido: o servidor recusa e os dados mantêm-se, independentemente da visibilidade do botão.
A2 fixa a história; A3 os estados da interface; A4–A6 os dados e garantias; A7 o contrato; A8 integra esse percurso. O protótipo tem de satisfazer também as restantes histórias exigidas na edição atual.
Confere a tua resposta:
- Mostro que dados fixos e mensagem de sucesso não provam persistência.
- Ligo história, estado da interface, contrato, autorização e transação.
- Verifico recarregamento, repetição e pedido direto de outra pessoa.