Laravel 13

O projeto que você vai construir

Ao longo das aulas você vai montar, peça por peça, um painel de usuários simples. Não precisa adiantar nada agora — cada recurso abaixo vai aparecer exatamente na aula em que o conceito correspondente for ensinado. Isso aqui é só o "mapa" do destino final:

AuthGateEvent/ListenerMailHTTP ClientJobs/ScheduleSeederDeploy

Básico

Aulas 1–5

O que é o Laravel, rapidamente

Laravel é um framework (conjunto de ferramentas e convenções prontas que aceleram o desenvolvimento, em vez de você escrever tudo do zero) para a linguagem PHP. Ele organiza o código seguindo o padrão MVC (Model-View-Controller): o Model cuida dos dados e das regras de negócio, a View cuida da tela que o usuário vê, e o Controller é a "cola" que recebe o pedido, conversa com o Model e decide qual View mostrar.

Conceito de PHP — função e closureVocê vai ver bastante código PHP entre function () { ... }. Isso é uma função anônima (também chamada de closure) — um bloco de código sem nome, que pode ser guardado numa variável ou passado como argumento pra outra função, e executado depois. É exatamente assim que o Laravel deixa você "anexar" um comportamento a uma rota: você não dá nome pra essa função, só entrega ela pronta pro Laravel chamar quando alguém acessar aquela URL.

Instalação

terminal
composer create-project laravel/laravel:^13.0 painel-usuarios
cd painel-usuarios
php artisan serve

composer é o gerenciador de dependências (pacotes/bibliotecas prontas de outras pessoas que seu projeto pode usar) do PHP — equivalente ao npm do Node.js, se você já ouviu falar. artisan é a CLI (command-line interface, "interface de linha de comando" — um programa que você controla digitando comandos no terminal, sem tela gráfica) própria do Laravel. Depois do php artisan serve, seu projeto fica disponível em http://localhost:8000.

Estrutura de pastas essencial

estrutura
app/
  Http/
    Controllers/   → classes que recebem a requisição e decidem o que fazer
  Models/          → classes que representam tabelas do banco
routes/
  web.php          → rotas para páginas (navegador)
database/
  migrations/      → "receitas" versionadas de como o banco deve ser
resources/
  views/           → arquivos Blade (templates HTML)
public/            → único diretório exposto ao navegador

A pasta public/ — a única porta de entrada

Quando você configura um servidor de verdade (Apache, Nginx) pra rodar seu projeto Laravel em produção, o document root (a pasta que o servidor HTTP aponta como raiz pública) deve ser sempre a pasta public/ — nunca a raiz do projeto inteiro. Dentro dela fica o index.php, o único ponto de entrada de toda a aplicação.

O motivo é segurança: tudo fora de public/ — sua pasta app/, o arquivo .env com senhas e chaves de API — fica inacessível diretamente pelo navegador. Em desenvolvimento local, php artisan serve já cuida disso sozinho; na hora de subir pra produção, configurar isso certo é obrigatório.

Roteamento

Rota é a definição de "quando alguém acessar essa URL com esse método HTTP, execute isso". Fica em routes/web.php. Vamos criar as três rotas que vão sustentar o projeto inteiro.

routes/web.php
use Illuminate\Support\Facades\Route;

Route::get('/usuarios', function () {
    return 'Aqui vai a lista de usuários';
});

Se você acessar http://localhost:8000/usuarios, verá esse texto. Isso confirma que a rota está funcionando — próximas aulas vão trocar esse texto solto por um Controller de verdade.

Verbos HTTP — cada ação tem um verbo próprio:

routes/web.php
Route::get('/usuarios', ...);         // ver a lista
Route::get('/usuarios/create', ...);   // ver o formulário de cadastro
Route::post('/usuarios', ...);        // salvar um usuário novo

GET é usado quando o navegador só está pedindo pra ver algo (uma página, uma lista). POST é usado quando o navegador está enviando dados pro servidor processar (como o conteúdo de um formulário).

Mão na massa

Instale o projeto e crie, em routes/web.php, as três rotas acima: GET /usuarios, GET /usuarios/create e POST /usuarios. Pra cada uma, retorne por enquanto só um texto simples identificando qual rota é (ex: return 'Formulário de cadastro';). Acesse cada URL no navegador (a POST você só consegue testar mais pra frente, quando tiver um formulário de verdade) pra confirmar que todas respondem.

Dica Se aparecer uma página de erro 419 ou 405 na rota POST, não se preocupe — isso é esperado por enquanto, porque ainda não existe formulário nenhum mandando dados pra ela. Vamos resolver isso na Aula 5.
Conceito de PHP — classe, objeto e métodoUma classe é um "molde" que descreve um tipo de coisa — o que ela sabe (suas propriedades) e o que ela sabe fazer (seus métodos, que são funções que pertencem à classe). Um objeto é uma instância real, criada a partir desse molde (new NomeDaClasse()). Um método é só o nome que damos a uma função quando ela está dentro de uma classe. No Laravel, um Controller é uma classe, e cada uma das suas ações (listar, criar, salvar) é um método dessa classe.

O que é um Controller

Controller (controlador) é uma classe que agrupa a lógica que responde a uma requisição (o pedido que o navegador faz ao servidor). Em vez de escrever a lógica direto dentro da rota (como fizemos na Aula 1, só pra testar), organizamos por responsabilidade — o UsuarioController vai cuidar de tudo relacionado a usuários.

Criando o Controller

terminal
php artisan make:controller UsuarioController

Isso cria o arquivo app/Http/Controllers/UsuarioController.php, já com a estrutura básica de uma classe vazia.

Estrutura básica

app/Http/Controllers/UsuarioController.php
namespace App\Http\Controllers;

class UsuarioController extends Controller
{
    public function index()
    {
        return 'Lista de usuários';
    }

    public function create()
    {
        return 'Formulário de cadastro';
    }

    public function store()
    {
        return 'Usuário salvo';
    }
}

extends Controller significa que UsuarioController "herda" características da classe base Controller do Laravel — é assim que toda classe Controller do seu projeto ganha, de graça, alguns recursos internos do framework.

Repare que os três métodos (index, create, store) já batem exatamente com as três ações do projeto: index lista, create mostra o formulário, store salva. Esses nomes não são obrigatórios, mas são a convenção que o próprio Laravel usa — vamos ver isso com mais detalhe daqui a pouco.

Um atalho: gerando o Controller já com os métodos prontos

Em vez de criar a classe vazia e digitar os métodos na mão, dá pra pedir pro Artisan já gerar a estrutura completa de um CRUD de uma vez, com a flag --resource:

terminal
php artisan make:controller UsuarioController --resource

Isso cria o arquivo já com os 7 métodos convencionais de um CRUD completo (index, create, store, show, edit, update, destroy), cada um com o corpo vazio, prontos pra você preencher. Como nosso projeto por enquanto só precisa de três desses, você pode apagar os outros quatro ou só deixá-los vazios, guardados pra um dia que decidir adicionar edição/exclusão de usuário. Mostramos a versão manual acima de propósito, pra você ver exatamente cada método sendo escrito — mas, a partir daqui, sinta-se à vontade pra usar --resource sempre que for criar um Controller que vai virar um CRUD completo; vamos usar mais comandos Artisan como esse ao longo da trilha, pra ganhar tempo com o que já é repetitivo.

Apontando as rotas pro Controller

routes/web.php
use App\Http\Controllers\UsuarioController;

Route::get('/usuarios', [UsuarioController::class, 'index']);
Route::get('/usuarios/create', [UsuarioController::class, 'create']);
Route::post('/usuarios', [UsuarioController::class, 'store']);

[UsuarioController::class, 'index'] diz: "quando bater nessa rota, instancie o UsuarioController e chame o método index". UsuarioController::class é só um jeito seguro de escrever o nome completo da classe (com o namespace todo) sem digitar errado.

Repare que a URL é /usuarios/create, em inglês — não /usuarios/criar. Isso é proposital, e vale a pena fixar desde já: mais adiante (Aula 10), vamos trocar essas rotas escritas na mão por Route::resource('usuarios', UsuarioController::class), um atalho que gera as rotas convencionais sozinho. Só que esse atalho gera a URL de criação sempre como /create — em inglês, fixo — não importa que o nome do recurso (usuarios) esteja em português. Se a gente tivesse usado /usuarios/criar agora, o link "Cadastrar novo usuário" quebraria silenciosamente quando o Route::resource entrasse em cena — a URL antiga simplesmente deixaria de existir. Escrever /create desde já evita esse retrabalho.

Controller Resource — o atalho pra CRUD

CRUD é a sigla pra Create, Read, Update, Delete — as 4 operações básicas sobre um dado. Como nosso Controller já segue a convenção de nomes do Laravel, dá pra trocar as três linhas de rota acima por uma única linha:

routes/web.php
Route::resource('usuarios', UsuarioController::class);

Isso cria automaticamente 7 rotas convencionais (incluindo edit, update e destroy, que vamos usar mais pra frente) — todas seguindo o padrão REST (um estilo de organizar rotas em torno de "recursos", como "usuários", combinado com os verbos HTTP), e todas esperando métodos com nomes em inglês específicos no Controller (veremos a lista completa na Aula 10). Por enquanto, pode usar as três rotas explícitas da Aula 1 mesmo, pra deixar mais claro o que está acontecendo; trocamos pelo Route::resource mais adiante.

Request — os dados que chegam num formulário

Request é o objeto que carrega tudo que o navegador enviou: campos de formulário, arquivos, etc. Vamos usar isso de verdade a partir da Aula 5, mas o formato básico é:

exemplo
use Illuminate\Http\Request;

public function store(Request $request)
{
    $nome = $request->input('nome'); // ou forma mais curta: $request->nome
}

Mão na massa

Crie o UsuarioController com os três métodos index, create e store, cada um retornando um texto identificador (como no exemplo). Troque as rotas da Aula 1 pra apontar pro Controller, exatamente como no bloco "Apontando as rotas pro Controller". Confirme no navegador que /usuarios e /usuarios/create continuam respondendo, agora vindo do Controller.

O que é uma View

View (visão) é o arquivo que gera o HTML que o usuário vê. Fica em resources/views/. No Laravel, views normalmente são escritas com Blade, o motor de templates (uma linguagem que mistura HTML com pedacinhos de PHP, com sintaxe mais limpa que PHP puro).

Retornando uma view

app/Http/Controllers/UsuarioController.php
public function index()
{
    return view('usuarios.index');
}

public function create()
{
    return view('usuarios.criar');
}

O ponto em 'usuarios.index' representa uma subpasta: isso busca o arquivo resources/views/usuarios/index.blade.php.

Criando os arquivos de view pelo terminal

Em vez de criar a pasta usuarios/ e cada arquivo .blade.php na mão, o Artisan tem um comando pra isso — make:view, que aceita a mesma dot notation que acabamos de ver:

terminal
php artisan make:view layouts.app
php artisan make:view usuarios.index
php artisan make:view usuarios.criar

Cada comando já cria a subpasta necessária (se ela ainda não existir) e o arquivo .blade.php vazio no lugar certo — usuarios.index vira resources/views/usuarios/index.blade.php, seguindo exatamente a mesma convenção de ponto-vira-subpasta que o view('usuarios.index') usa pra encontrar o arquivo. Os arquivos criados vêm vazios — o conteúdo de cada um você escreve a seguir, copiando os exemplos abaixo.

Layout base — pra não repetir HTML em toda página

resources/views/layouts/app.blade.php
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>@yield('titulo') — Painel de Usuários</title>
</head>
<body>
    <nav>Painel de Usuários</nav>

    <main>
        @yield('conteudo')
    </main>
</body>
</html>

@yield('nome') é um "espaço reservado" que cada página vai preencher com seu próprio conteúdo.

A tela de lista

resources/views/usuarios/index.blade.php
@extends('layouts.app')

@section('titulo', 'Usuários')

@section('conteudo')
    <h1>Usuários cadastrados</h1>

    <ul>
    @foreach ($usuarios as $usuario)
        <li>{{ $usuario->name }} — nascido em {{ $usuario->data_nascimento }}</li>
    @endforeach
    </ul>
@endsection

@extends('layouts.app') diz "esta view usa aquele layout como base". {{ $usuario->name }} exibe o valor da propriedade (com escape automático — proteção contra XSS, um ataque onde código malicioso é injetado numa página). @foreach repete o bloco de HTML uma vez pra cada item da lista de usuários. Ainda não temos $usuarios vindo de lugar nenhum — isso vem do banco de dados, que só vamos construir na Aula 4.

O formulário de cadastro

resources/views/usuarios/criar.blade.php
@extends('layouts.app')

@section('titulo', 'Cadastrar usuário')

@section('conteudo')
    <h1>Cadastrar novo usuário</h1>

    <form method="POST" action="/usuarios">
        @csrf

        <label>Nome</label>
        <input type="text" name="name">

        <label>E-mail</label>
        <input type="email" name="email">

        <label>Senha</label>
        <input type="password" name="password">

        <label>Confirmar senha</label>
        <input type="password" name="password_confirmation">

        <label>Data de nascimento</label>
        <input type="date" name="data_nascimento">

        <label>Papel</label>
        <select name="papel">
            <option value="comum">Comum</option>
            <option value="gerente">Gerente</option>
            <option value="admin">Admin</option>
        </select>

        <button type="submit">Cadastrar</button>
    </form>
@endsection

@csrf vamos explicar em detalhe na Aula 5 — por enquanto, saiba que sem essa linha o formulário simplesmente não funciona, então não esqueça dela. O campo password_confirmation também só vai ganhar sentido na Aula 5, quando a regra de validação que o exige entrar em cena — por ora, só deixe ele no formulário.

Mão na massa

Use os três comandos make:view pra criar o layout e as duas views, depois preencha cada arquivo exatamente como nos exemplos. Ajuste o Controller pra retornar view('usuarios.criar') no método create. Acesse /usuarios/create no navegador — o formulário deve aparecer (sem estilo bonito ainda, isso é só HTML cru). O método index ainda vai dar erro, porque falta o $usuarios — vamos resolver isso já na próxima aula.

Conceito de PHP — arrayUm array é uma "caixa" que guarda vários valores organizados. Pode ser uma lista simples (['a', 'b', 'c']) ou um array associativo, onde cada valor tem um nome/chave (['nome' => 'Ana', 'idade' => 30]). Você vai ver arrays associativos o tempo todo no Laravel — por exemplo, pra listar quais campos um formulário pode preencher.

O que é ORM

ORM (Object-Relational Mapping, mapeamento objeto-relacional) é uma técnica onde você manipula tabelas do banco de dados como se fossem classes e objetos do PHP, sem escrever SQL manualmente na maioria dos casos. No Laravel, esse ORM se chama Eloquent.

Boa notícia: o model de usuário já existe

Todo projeto Laravel novo já vem, por padrão, com um model User (em app/Models/User.php) e uma migration criando a tabela users — porque autenticação de usuário é algo tão comum que o framework já vem preparado. Em vez de criar um model separado do zero, vamos estender esse que já existe, adicionando as colunas que nosso projeto precisa: papel, data_nascimento e criador_id (pra saber quem cadastrou quem).

Migrations (migrações)

Migration é um arquivo PHP que descreve, de forma versionada, como o banco deve ser — como um "controle de versão" (git) só que pra estrutura do banco. A migration da tabela users já existe (database/migrations/..._create_users_table.php); vamos criar uma nova migration que só adiciona colunas nela.

terminal
php artisan make:migration add_papel_e_nascimento_to_users_table --table=users
database/migrations/..._add_papel_e_nascimento_to_users_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::table('users', function (Blueprint $table) {
            $table->string('papel')->default('comum');     // 'admin', 'gerente' ou 'comum'
            $table->date('data_nascimento')->nullable();
            $table->foreignId('criador_id')->nullable()->constrained('users');
        });
    }

    public function down(): void
    {
        Schema::table('users', function (Blueprint $table) {
            $table->dropColumn(['papel', 'data_nascimento', 'criador_id']);
        });
    }
};

Repare a diferença de Schema::create (cria uma tabela nova do zero) pra Schema::table (abre uma tabela que já existe pra adicionar/remover colunas). nullable() permite que o campo fique vazio (nem todo usuário precisa ter um "criador" — o primeiro admin, por exemplo, não foi criado por ninguém). foreignId('criador_id')->constrained('users') cria uma chave estrangeira que aponta pra própria tabela users — um usuário referenciando outro usuário.

O que acontece quando o "criador" é excluído

Toda chave estrangeira levanta a mesma pergunta, mais cedo ou mais tarde: se o registro referenciado (aqui, o usuário-criador) for excluído, o que acontece com quem aponta pra ele? O Laravel deixa você escolher, encadeando um método depois de constrained('users') — vale conhecer as opções agora, mesmo sem usar todas ainda, porque é uma decisão que fica “gravada” no banco e é mais chata de mudar depois do que de acertar de início:

EncadeamentoComportamento
(nenhum — o que fizemos)Bloqueia a exclusão do "pai" enquanto existir algum "filho" apontando pra ele
->restrictOnDelete()Mesma coisa que "nenhum", só que explícito — deixa a intenção clara no código em vez de depender do padrão do banco
->cascadeOnDelete()Exclui os "filhos" junto, automaticamente, quando o "pai" é excluído
->nullOnDelete()Zera a coluna nos "filhos" (vira NULL) quando o "pai" é excluído, sem apagar nem bloquear nada

Pra criador_id, deixamos sem nenhum modificador de propósito — e é a escolha certa aqui: cascadeOnDelete() seria perigoso demais nesse caso específico, porque excluir um gerente apagaria, em cascata, todos os usuários que ele cadastrou, sem aviso nenhum — ótimo pra outros tipos de relacionamento (excluir um pedido e seus itens junto, por exemplo), péssimo pra "quem cadastrou quem". nullOnDelete() é uma alternativa razoável se, mais pra frente, você decidir que excluir um criador não deveria ficar bloqueado — vamos ver exatamente como aplicar essa mudança quando o assunto de exclusão de usuário aparecer (Aula 11).

Existe o mesmo conjunto de opções pra quando a chave do "pai" muda em vez de ser excluída (->cascadeOnUpdate(), ->restrictOnUpdate()) — bem mais raro de precisar na prática, já que uma chave primária praticamente nunca muda depois de criada, mas vale saber que existe caso um dia você troque a estratégia de IDs do projeto.

Rodando a migration (aplica a mudança no banco de fato):

terminal
php artisan migrate

Ajustando o Model User

app/Models/User.php
protected $fillable = [
    'name',
    'email',
    'password',
    'papel',
    'data_nascimento',
    'criador_id',
];

$fillable é a lista de campos que podem ser preenchidos em massa (mass assignment: atribuir vários campos de uma vez, tipo User::create($dados)) — é uma proteção de segurança, sem isso o Eloquent recusa preencher os campos por padrão.

Operações básicas (CRUD com Eloquent)

exemplo
// Criar
$usuario = User::create([
    'name' => 'Ana Souza',
    'email' => 'ana@exemplo.com',
    'password' => bcrypt('senha123'), // nunca salve senha em texto puro!
    'papel' => 'comum',
    'data_nascimento' => '1998-03-14',
]);

// Ler
$todos = User::all();
$um = User::find(1);
$primeiro = User::where('email', 'ana@exemplo.com')->first();

bcrypt() é uma função do Laravel que transforma a senha num hash (uma "impressão digital" de tamanho fixo, impossível de reverter pra descobrir a senha original) — é assim que o Laravel guarda senhas com segurança, nunca em texto puro.

Preenchendo a lista de usuários

Agora dá pra completar o método index que deixamos incompleto na Aula 3:

app/Http/Controllers/UsuarioController.php
use App\Models\User;

public function index()
{
    $usuarios = User::all();
    return view('usuarios.index', compact('usuarios'));
}

compact('usuarios') é uma função do PHP que empacota a variável $usuarios num array associativo (['usuarios' => $usuarios]) automaticamente, usando o nome da variável como chave — assim a view recebe uma variável chamada $usuarios.

Mão na massa

1) Crie e rode a migration que adiciona papel, data_nascimento e criador_id na tabela users. 2) Atualize o $fillable do model User. 3) Complete o método index do Controller pra buscar todos os usuários e passar pra view. 4) Pra testar sem precisar do formulário ainda, use o Tinker (console interativo do Laravel) pra criar um usuário na mão:

terminal
php artisan tinker
dentro do tinker
User::create(['name' => 'Ana Souza', 'email' => 'ana@exemplo.com', 'password' => bcrypt('123456'), 'data_nascimento' => '1998-03-14']);

Acesse /usuarios e confirme que a Ana aparece na lista.

Dica Tinker é um "terminal PHP" que já carrega seu projeto Laravel inteiro — dá pra rodar qualquer código PHP/Eloquent ali direto, ótimo pra testar coisas rápido sem precisar de tela.

CSRF (proteção contra falsificação de requisição)

CSRF (Cross-Site Request Forgery) é um tipo de ataque onde um site malicioso induz o navegador de uma vítima já logada em outro site a enviar uma requisição indesejada. O Laravel se protege exigindo um token CSRF (um código único por sessão) em todo formulário que envia dados via POST — é por isso que a linha @csrf no nosso formulário da Aula 3 é obrigatória: sem ela, o Laravel recusa a requisição com erro 419.

Validação no Controller

Agora vamos completar o método store de verdade: receber os dados do formulário, validar, e salvar.

app/Http/Controllers/UsuarioController.php
use Illuminate\Http\Request;
use App\Models\User;

public function store(Request $request)
{
    $validado = $request->validate([
        'name' => 'required|string|max:255',
        'email' => 'required|email|unique:users,email',
        'password' => 'required|string|min:6|confirmed',
        'data_nascimento' => 'required|date',
        'papel' => 'required|in:admin,gerente,comum',
    ]);

    $validado['password'] = bcrypt($validado['password']);

    User::create($validado);

    return redirect('/usuarios')->with('sucesso', 'Usuário cadastrado com sucesso!');
}

Vamos entender cada regra: required obriga o campo a vir preenchido. email valida o formato de e-mail. unique:users,email garante que não existe outro usuário já cadastrado com esse e-mail (consulta a tabela users, coluna email). min:6 exige pelo menos 6 caracteres na senha. confirmed (Aula VAL1) exige um campo password_confirmation no formulário, com o mesmo valor de password — é assim que a pessoa confirma que não digitou a senha errado sem perceber, já que os dois campos vêm mascarados (type="password") e ela não consegue ler o que digitou. in:admin,gerente,comum só aceita esses três valores exatos pro papel — protege contra alguém mandar um papel inventado pelo formulário.

Avisando a pessoa que deu certo

Repare no final do store: ->with('sucesso', 'Usuário cadastrado com sucesso!'). Isso guarda essa mensagem na sessão (aquele mesmo mecanismo que lembra quem está logado entre requisições, Aula 9) — só que por uma única requisição: assim que a próxima página lê e mostra ela, o Laravel apaga sozinho. É por isso que esse tipo de dado é chamado de flash message ("mensagem relâmpago") — ela pisca uma vez e some, exatamente como um aviso de "salvo com sucesso" deveria funcionar.

Guardar essa mensagem não obriga nada a acontecer — o redirecionamento funcionaria perfeitamente sem essa linha, o usuário só não veria confirmação nenhuma. É uma melhoria de experiência, não um requisito técnico. Pra ela realmente aparecer na tela, o layout precisa ler esse dado — e é aqui que fechamos o ciclo: o mesmo layout que você criou na Aula 3 ganha um pequeno acréscimo:

resources/views/layouts/app.blade.php (trecho a adicionar, dentro do <main>, antes do @yield)
@if (session('sucesso'))
    <p style="color: green;">{{ session('sucesso') }}</p>
@endif

session('sucesso') lê exatamente o que ->with('sucesso', '...') guardou — os dois são a mesma chave, um escreve e o outro lê. Como esse bloco fica no layout (não numa view específica de uma tela só), qualquer página do sistema que redirecionar com ->with('sucesso', '...') já mostra a mensagem automaticamente dali em diante, sem precisar repetir esse trecho em cada view nova — inclusive em Controllers que você ainda vai escrever mais adiante na trilha.

Por que o criador_id fica de fora, por enquantoRepare que $validado não inclui criador_id — e é proposital: nesse ponto da trilha ainda não existe login (isso só chega na Aula 9), então não há "usuário logado" nenhum pra apontar como criador. Como a coluna é nullable (Aula 4), o Eloquent simplesmente salva NULL ali, sem erro nenhum. Assim que o Auth existir, a Aula 9 já atualiza esse mesmo método pra preencher esse campo de verdade — fique de olho.

Se qualquer regra falhar, o Laravel automaticamente redireciona de volta pro formulário, mantendo os dados já digitados e mostrando as mensagens de erro — você não escreve esse comportamento, ele já vem pronto.

Exibindo erros no formulário

resources/views/usuarios/criar.blade.php
<label>Nome</label>
<input type="text" name="name" value="{{ old('name') }}">
@error('name')
    <p style="color: red;">{{ $message }}</p>
@enderror

<label>E-mail</label>
<input type="email" name="email" value="{{ old('email') }}">
@error('email')
    <p style="color: red;">{{ $message }}</p>
@enderror

<label>Senha</label>
<input type="password" name="password">
@error('password')
    <p style="color: red;">{{ $message }}</p>
@enderror

<label>Confirmar senha</label>
<input type="password" name="password_confirmation">

old('name') recupera o valor digitado antes do erro, pra não obrigar a pessoa a preencher tudo de novo. @error('campo') ... @enderror mostra a mensagem só se aquele campo específico falhou — o Laravel guarda os erros automaticamente numa variável $errors, disponível em qualquer view sem você precisar passar manualmente. Repare que os campos de senha não usam old('password') — por segurança, o Laravel nunca guarda o valor de campos de senha no old(), mesmo que você tente; a pessoa precisa redigitar a senha se algum outro campo falhar na validação.

Mão na massa

Complete o método store exatamente como no exemplo, incluindo o ->with('sucesso', ...). Adicione o bloco @if (session('sucesso')) no layout. Adicione value="{{ old('...') }}" e @error em todos os campos do formulário de cadastro. Teste três cenários no navegador: (1) enviar o formulário vazio — deve voltar mostrando os erros; (2) enviar com um e-mail que já existe — deve acusar duplicado; (3) preencher tudo certo — deve criar o usuário, te levar de volta pra /usuarios com a mensagem verde de sucesso aparecendo, e o usuário já na lista.

Intermediário

Aulas 6–9

O que são relacionamentos

Relacionamento é a forma do Eloquent expressar, em PHP, como as tabelas se conectam entre si (chaves estrangeiras) — sem você escrever JOIN manualmente. Nosso caso é interessante porque é uma tabela se relacionando com ela mesma: um usuário (o criador) pode ter cadastrado vários outros usuários.

belongsTo — "este usuário pertence a um criador"

Lembra da coluna criador_id que criamos na Aula 4? Ela guarda o id de quem cadastrou aquele usuário. Um relacionamento belongsTo (pertence a) expressa isso: "este registro aponta pra um único registro relacionado".

app/Models/User.php
public function criador()
{
    return $this->belongsTo(User::class, 'criador_id');
}

O segundo parâmetro ('criador_id') é necessário aqui porque o Eloquent, por padrão, procuraria uma coluna chamada user_id (baseado no nome da classe relacionada) — como nossa coluna se chama diferente, precisamos avisar explicitamente.

hasMany — "este usuário criou vários outros"

O lado inverso: um hasMany (tem muitos) expressa "este registro tem vários outros registros apontando pra ele".

app/Models/User.php
public function usuariosCriados()
{
    return $this->hasMany(User::class, 'criador_id');
}

Usando os dois relacionamentos

exemplo
$usuario = User::find(3);

$usuario->criador;         // objeto User de quem cadastrou esse usuário (ou null)
$usuario->usuariosCriados; // Collection (coleção) de todos que ESSE usuário cadastrou

Repare: sem parênteses ($usuario->criador), o Eloquent já executa a consulta e devolve o resultado pronto — é a "mágica" das propriedades dinâmicas do Eloquent, baseada no nome do método que você criou.

Mostrando "quem cadastrou" na lista de usuários

resources/views/usuarios/index.blade.php
@foreach ($usuarios as $usuario)
    <li>
        {{ $usuario->name }} — nascido em {{ $usuario->data_nascimento }}
        @if ($usuario->criador)
            (cadastrado por {{ $usuario->criador->name }})
        @endif
    </li>
@endforeach

O @if evita erro quando criador é null (caso do primeiro admin, que não foi cadastrado por ninguém).

Vale entender pra que esse @if está preparado, não só que ele funciona. Lembra das opções de exclusão da chave estrangeira que vimos na Aula 4? Deixamos criador_id sem cascadeOnDelete()/nullOnDelete() — ou seja, no comportamento padrão de bloquear a exclusão. Isso significa que, hoje, um criador que já existe nunca desaparece sozinho: a constraint no banco impede excluir quem ainda tem gente apontando pra ele. Mas se um dia você trocar essa constraint pra nullOnDelete() (a Aula 11 mostra como e quando isso faria sentido), o cenário muda: um usuário que hoje mostra "cadastrado por Fulano" pode, depois que Fulano for excluído, passar a não mostrar mais nada — o criador_id dele silenciosamente virou NULL com o tempo, não só no momento em que foi criado. O @if que já escrevemos aqui já cobre os dois casos sem precisar de nenhuma mudança — mas é bom saber que ele está protegendo contra algo que pode acontecer mais tarde, não só contra o caso do admin original.

Mão na massa

Adicione os métodos criador() e usuariosCriados() no model User. Ajuste a view de lista pra mostrar quem cadastrou cada usuário, quando existir. Pelo Tinker, crie um segundo usuário passando 'criador_id' => 1 (assumindo que a Ana da aula passada tem id 1) e confirme na tela que aparece "cadastrado por Ana Souza".

Consultas com filtro (WHERE)

exemplos
User::where('papel', 'gerente')->get();
User::where('papel', '!=', 'comum')->get();
User::whereMonth('data_nascimento', 3)->get(); // todos que nasceram em março

whereMonth é um atalho do Eloquent pra filtrar só pelo mês de uma coluna de data, ignorando o dia e o ano — vamos usar exatamente esse tipo de filtro na Aula 14, pra achar os aniversariantes do dia.

Query Scopes — empacotando um filtro comum

Scope é um método no Model que encapsula uma condição usada com frequência, evitando repetir o mesmo where em vários lugares do sistema.

app/Models/User.php
public function scopeGerentes($query)
{
    return $query->where('papel', 'gerente');
}

public function scopeAdmins($query)
{
    return $query->where('papel', 'admin');
}

public function scopePodeCadastrar($query)
{
    return $query->whereIn('papel', ['admin', 'gerente']);
}

Repare na convenção: o método começa com scope + nome em PascalCase, mas na hora de usar você chama sem o prefixo e em camelCase:

uso
$gerentes = User::gerentes()->get();
$quemPodeCadastrar = User::podeCadastrar()->get();

Accessor — calculando a idade sem guardar ela no banco

Accessor é um método que transforma um valor na hora de ler o atributo, sem alterar o que está salvo no banco. Idade é um ótimo exemplo: não faz sentido guardar "32 anos" numa coluna, porque isso muda todo ano — melhor calcular na hora, a partir da data_nascimento.

app/Models/User.php
use Illuminate\Database\Eloquent\Casts\Attribute;
use Carbon\Carbon;

protected function idade(): Attribute
{
    return Attribute::make(
        get: fn () => $this->data_nascimento
            ? Carbon::parse($this->data_nascimento)->age
            : null,
    );
}

Carbon é a biblioteca de datas que o Laravel usa por baixo dos panos — Carbon::parse(...)->age já calcula a idade atual a partir de uma data de nascimento, sem você escrever a matemática. Repare que o método se chama idade() (camelCase), mas você acessa como $usuario->idade — o Eloquent converte automaticamente.

Blade
<li>{{ $usuario->name }} — {{ $usuario->idade }} anos</li>

Mão na massa

Adicione os três scopes (scopeGerentes, scopeAdmins, scopePodeCadastrar) e o accessor idade no model User. Mostre a idade calculada na view de lista de usuários, ao lado da data de nascimento.

O que é Middleware

Middleware é uma camada de código que intercepta a requisição antes dela chegar ao Controller. Pense nele como um "segurança na porta": ele decide se deixa a requisição passar, ou barra ali mesmo.

O middleware auth, já pronto no Laravel

Pra exigir que a pessoa esteja logada antes de acessar as telas de usuários, não precisamos criar um middleware do zero — o Laravel já vem com um pronto, chamado auth. Vamos aplicá-lo nas nossas rotas:

routes/web.php
Route::middleware('auth')->group(function () {
    Route::get('/usuarios', [UsuarioController::class, 'index']);
    Route::get('/usuarios/create', [UsuarioController::class, 'create']);
    Route::post('/usuarios', [UsuarioController::class, 'store']);
});

Route::middleware('auth')->group(function () { ... }) aplica o middleware auth em todas as rotas dentro daquela closure — assim você não repete ->middleware('auth') em cada linha. Se alguém não logado tentar acessar qualquer uma dessas URLs, o Laravel redireciona automaticamente pra tela de login (que ainda vamos configurar na próxima aula).

Fluxo visual, pra fixar

fluxo
Requisição → Middleware "auth" → (logado?) → Controller → Resposta
                              ↳ (não logado?) → Redireciona pro login

Criando seu próprio middleware (pra referência futura)

Ainda não precisamos disso — a Aula 9 vai resolver a regra "só admin/gerente pode cadastrar" com Gate, uma ferramenta mais adequada pra esse tipo de checagem. Mas vale saber como um middleware customizado se pareceria:

terminal
php artisan make:middleware VerificaPapel
exemplo
use Closure;
use Illuminate\Http\Request;

class VerificaPapel
{
    public function handle(Request $request, Closure $next)
    {
        if ($request->user()->papel === 'comum') {
            abort(403, 'Você não tem permissão pra acessar isso.');
        }

        return $next($request);
    }
}

$next($request) é o que deixa a requisição continuar pro Controller; não chamar isso (como no abort acima) interrompe o fluxo ali mesmo.

Mão na massa

Envolva as três rotas de usuários (index, create, store) num grupo com Route::middleware('auth'). Como você ainda não tem sistema de login pronto (isso vem na próxima aula), tente acessar /usuarios agora — o Laravel vai reclamar que a rota de login não existe. É esperado! Vamos resolver isso já na Aula 9.

Diferença entre Autenticação e Autorização

Autenticação responde "quem é você?" (login). Autorização responde "você pode fazer isso?" (permissão, depois que já se sabe quem é a pessoa). São conceitos separados, mas sempre andam juntos — e é exatamente essa dupla que o nosso projeto precisa: primeiro saber quem está logado (Auth), depois decidir se essa pessoa pode cadastrar outra (Gate).

Rotas e tela de login

routes/web.php
use App\Http\Controllers\AuthController;

Route::get('/login', [AuthController::class, 'mostrarFormulario'])->name('login');
Route::post('/login', [AuthController::class, 'login']);
Route::post('/logout', [AuthController::class, 'logout'])->name('logout');

->name('login') não é opcional aqui, mesmo que pareça só um detalhe estético — e o detalhe importante é por que ele é necessário. Ter uma rota que responde em /login não é a mesma coisa que ter uma rota nomeada login. São duas mecânicas diferentes: acessar /login pelo navegador usa o caminho da URL; o middleware auth (aplicado nas rotas de usuário desde a Aula 8), quando precisa redirecionar alguém não logado, não usa o caminho — ele chama internamente route('login'), que procura pelo nome. Por isso é fácil essa falha passar despercebida: a página de login pode carregar perfeitamente se você digitar /login na barra de endereço, e mesmo assim o sistema quebra com Route [login] not defined assim que alguém desloga (ou nunca logou) e tenta acessar algo protegido — porque esse erro só aparece nesse caminho específico do middleware, não no acesso direto à rota. Nomear logout não é estritamente exigido pelo framework, mas é boa prática deixar consistente, já que outras partes do sistema podem querer usar route('logout') em vez do caminho fixo.

terminal
php artisan make:controller AuthController
app/Http/Controllers/AuthController.php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class AuthController extends Controller
{
    public function mostrarFormulario()
    {
        return view('auth.login');
    }

    public function login(Request $request)
    {
        $credenciais = $request->validate([
            'email' => 'required|email',
            'password' => 'required',
        ]);

        if (Auth::attempt($credenciais)) {
            $request->session()->regenerate(); // proteção contra "roubo" de sessão antiga
            return redirect('/usuarios');
        }

        return back()->withErrors(['email' => 'Credenciais inválidas.']);
    }

    public function logout(Request $request)
    {
        Auth::logout();
        $request->session()->invalidate();
        $request->session()->regenerateToken();
        return redirect('/login');
    }
}

Auth::attempt() verifica o e-mail e a senha contra a tabela users — ele compara o hash automaticamente, você nunca lida com a senha em texto puro. Se bater, a pessoa fica "logada" (o Laravel guarda isso numa sessão, que é um jeito do servidor lembrar quem é você entre uma requisição e outra).

No logout(), repare nas três linhas em sequência: session()->invalidate() troca o ID da sessão e apaga os dados guardados nela — mas isso sozinho não gera um token CSRF novo. session()->regenerateToken() é quem faz isso, garantindo que o token antigo (que ainda poderia estar guardado em algum formulário aberto numa aba, por exemplo) pare de ser aceito. Pular essa linha é um descuido comum — sem ela, o logout invalida a sessão, mas deixa uma brecha pequena onde o token CSRF anterior ainda circula por aí.

terminal
php artisan make:view auth.login
resources/views/auth/login.blade.php
@extends('layouts.app')
@section('conteudo')
    <form method="POST" action="/login">
        @csrf
        <input type="email" name="email" placeholder="E-mail">
        <input type="password" name="password" placeholder="Senha">
        <button type="submit">Entrar</button>
    </form>
@endsection

Gate — a regra "só admin/gerente cadastra"

Gate é a forma mais simples de autorização: uma função que responde true/false pra uma ação específica. Usamos Gate (em vez de Policy) porque nossa regra é única e simples, não amarrada a um Model específico com várias ações diferentes.

app/Providers/AppServiceProvider.php
use Illuminate\Support\Facades\Gate;

public function boot(): void
{
    Gate::define('cadastrar-usuarios', function ($user) {
        return in_array($user->papel, ['admin', 'gerente']);
    });
}

in_array() é uma função nativa do PHP que verifica se um valor existe dentro de um array — aqui, se $user->papel é 'admin' ou 'gerente'.

Usando o Gate pra proteger as rotas de cadastro

app/Http/Controllers/UsuarioController.php
use Illuminate\Support\Facades\Auth;

public function create()
{
    Gate::authorize('cadastrar-usuarios'); // aborta com 403 automaticamente se negar

    return view('usuarios.criar');
}

public function store(Request $request)
{
    Gate::authorize('cadastrar-usuarios');

    $validado = $request->validate([ /* ... como na Aula 5 */ ]);
    $validado['password'] = bcrypt($validado['password']);
    $validado['criador_id'] = Auth::id();

    User::create($validado);

    return redirect('/usuarios')->with('sucesso', 'Usuário cadastrado com sucesso!');
}

Gate::authorize() checa a regra e, se ela retornar false, já interrompe a execução e devolve um erro 403 (proibido) — você não precisa escrever o if manualmente. E agora que existe alguém logado de verdade, dá pra completar aquele criador_id que ficou de fora na Aula 5: Auth::id() retorna o id do usuário autenticado no momento — exatamente quem está preenchendo esse formulário agora.

Escondendo o link de cadastro na tela, pra quem não pode ver

resources/views/usuarios/index.blade.php
@can('cadastrar-usuarios')
    <a href="/usuarios/create">Cadastrar novo usuário</a>
@endcan

Isso não substitui a checagem no Controller (alguém ainda poderia digitar a URL na mão) — é só uma questão de boa experiência, escondendo um botão que a pessoa não pode usar mesmo.

Mão na massa

Crie o AuthController, as rotas de login/logout e a view auth/login.blade.php. Antes de logar, acesse /usuarios direto pela URL (a rota já está protegida pelo middleware auth desde a Aula 8) — o Laravel deve te redirecionar sozinho pra /login, sem erro nenhum. Se aparecer Route [login] not defined mesmo com a rota já criada, é sinal de que o ->name('login') ficou faltando. Defina o Gate cadastrar-usuarios no AppServiceProvider e proteja create/store com Gate::authorize(). Esconda o link de cadastro com @can. Teste logando com a Ana (que criamos com papel 'comum' por padrão) e tentando acessar /usuarios/create direto pela URL — deve dar 403. Depois, pelo Tinker, mude o papel dela pra 'admin' (User::find(1)->update(['papel' => 'admin'])) e teste de novo — agora deve funcionar.

Editando e Excluindo Usuários

Aulas 10–11

Trocando as rotas soltas pelo Route::resource

Lá na Aula 2, quando o Route::resource apareceu pela primeira vez, ficou combinado de continuar com as três rotas explícitas "pra deixar mais claro", e trocar mais adiante quando precisássemos de edit/update/destroy. Chegou a hora — mas antes, vale ver a convenção completa, porque ela cobra um preço: o Laravel só liga cada rota ao método certo se o Controller usar exatamente esses nomes, em inglês.

Verbo + URLMétodo esperadoO que faz
GET /usuariosindex()Lista (Aula 3)
GET /usuarios/createcreate()Formulário de cadastro (Aula 3)
POST /usuariosstore()Salva um novo (Aula 5)
GET /usuarios/{usuario}show()Mostra um único registro, sozinho
GET /usuarios/{usuario}/editedit()Formulário de edição (agora)
PUT /usuarios/{usuario}update()Salva a edição (agora)
DELETE /usuarios/{usuario}destroy()Exclui (Aula 11)

Repare no show(): é o único dos 7 que esse projeto não precisa. Nossa tela de lista (Aula 3) já mostra tudo que faz sentido ver de cada usuário — não existe uma página separada "só esse usuário, em detalhe". Se você registrar Route::resource sem mais nada, a rota GET /usuarios/{usuario} passa a existir e o Laravel tenta chamar show() nela — como o método não existe no Controller, isso quebra com um erro assim que alguém (ou você, sem querer) acessar essa URL. A solução é dizer explicitamente que não queremos essa rota:

routes/web.php
Route::middleware('auth')->group(function () {
    Route::resource('usuarios', UsuarioController::class)->except('show');
});

->except('show') registra as outras 6 rotas normalmente e simplesmente não cria a de show — se alguém tentar /usuarios/{id} agora, recebe um 404 (rota não existe) em vez de um erro de método faltando, o que é um comportamento muito mais correto e previsível. O oposto também existe, ->only(['index', 'create', 'store']), pra quando você quer só algumas das 7 rotas em vez de excluir uma de todas.

Essa única linha substitui as três rotas de antes e já cria as 6 rotas convencionais que o projeto realmente usa — incluindo GET /usuarios/{usuario}/edit e PUT /usuarios/{usuario}, que vamos usar agora.

Um Gate que recebe mais do que só o usuário logado

O Gate cadastrar-usuarios (Aula 9) só precisava saber quem está logado. Agora a pergunta é diferente: "essa pessoa pode editar aquele usuário específico?" — a resposta depende de dois usuários, não só um. Isso não exige nada novo: um Gate::define() aceita parâmetros extras além do usuário logado, você só passa o valor a mais na hora de checar.

app/Providers/AppServiceProvider.php (adicionando ao boot() que já existe)
Gate::define('editar-usuario', function (User $user, User $usuario) {
    // admin/gerente podem editar qualquer um; qualquer pessoa pode editar a si mesma
    return in_array($user->papel, ['admin', 'gerente']) || $user->id === $usuario->id;
});

O primeiro parâmetro ($user) o Laravel sempre injeta sozinho, com quem está logado — igual já acontecia no cadastrar-usuarios. O segundo ($usuario) é nosso: representa o registro específico sendo afetado, e é ele que permite a comparação $user->id === $usuario->id — checar se a pessoa está editando a si mesma.

Os métodos edit() e update()

app/Http/Controllers/UsuarioController.php
use Illuminate\Support\Facades\Gate;

public function edit(User $usuario)
{
    Gate::authorize('editar-usuario', $usuario);

    return view('usuarios.editar', compact('usuario'));
}

public function update(Request $request, User $usuario)
{
    Gate::authorize('editar-usuario', $usuario);

    $regras = [
        'name' => 'required|string|max:255',
        'email' => 'required|email|unique:users,email,' . $usuario->id,
        'data_nascimento' => 'required|date',
    ];

    // só valida/exige senha se o campo veio preenchido — editar não deve forçar troca de senha
    if ($request->filled('password')) {
        $regras['password'] = 'required|string|min:6|confirmed';
    }

    // só quem já pode cadastrar usuário pode mudar o papel de alguém
    if (Gate::allows('cadastrar-usuarios')) {
        $regras['papel'] = 'required|in:admin,gerente,comum';
    }

    $validado = $request->validate($regras);

    if (isset($validado['password'])) {
        $validado['password'] = bcrypt($validado['password']);
    }

    $usuario->update($validado);

    return redirect('/usuarios')->with('sucesso', 'Usuário atualizado!');
}

Gate::authorize('editar-usuario', $usuario) passa $usuario como o segundo argumento da closure que você definiu — o Laravel casa isso automaticamente. 'unique:users,email,' . $usuario->id é um detalhe importante: sem o terceiro parâmetro, a regra unique rejeitaria o próprio e-mail atual do usuário (porque ele "já existe" — nele mesmo!); passando o id, a regra ignora esse registro específico na hora de checar duplicidade.

$request->filled('password') é diferente de $request->has('password'): has() só confere se o campo veio na requisição, mesmo que vazio; filled() confere se veio e não está vazio — exatamente o que queremos aqui, já que um campo de senha deixado em branco não deve contar como "o usuário quer trocar a senha". Repare que a regra de senha só entra em $regras dentro desse if — dava pra simplesmente sempre incluir 'password' => 'nullable|string|min:6|confirmed' e deixar o nullable pular a validação sozinho quando vazio, e funcionaria também. Mas construir a regra condicionalmente deixa a intenção explícita no código: "só validamos senha quando o usuário realmente está tentando trocar ela" — mais fácil de ler seis meses depois, principalmente num campo sensível como senha.

isset($validado['password']) funciona porque validate() só devolve as chaves que tinham regra definida — se o if acima não rodou, 'password' nem existe dentro de $validado. E é só por isso que $usuario->update($validado) já "sabe" pra não mexer na senha: o Eloquent só atualiza as colunas presentes no array que você passa — se password não está lá, ele simplesmente não toca nessa coluna, sem você precisar dizer isso explicitamente.

Repare no if (Gate::allows('cadastrar-usuarios')) envolvendo o campo papel: isso é uma proteção de segurança, não só uma conveniência. Se um usuário comum editando a si mesmo mandasse papel=admin escondido no formulário (fácil de forjar, já que é só um campo <select> no HTML), essa regra nem entraria em $regras, então papel nunca chega em $validado, e o update() ignora esse campo — sem essa checagem, seria uma brecha real de privilégio.

A view de edição

terminal
php artisan make:view usuarios.editar
resources/views/usuarios/editar.blade.php
@extends('layouts.app')

@section('titulo', 'Editar usuário')

@section('conteudo')
    <h1>Editar usuário</h1>

    <form method="POST" action="/usuarios/{{ $usuario->id }}">
        @csrf
        @method('PUT')

        <label>Nome</label>
        <input type="text" name="name" value="{{ old('name', $usuario->name) }}">

        <label>E-mail</label>
        <input type="email" name="email" value="{{ old('email', $usuario->email) }}">

        <label>Data de nascimento</label>
        <input type="date" name="data_nascimento" value="{{ old('data_nascimento', $usuario->data_nascimento) }}">

        <label>Nova senha (deixe em branco pra manter a atual)</label>
        <input type="password" name="password">
        @error('password')
            <p style="color: red;">{{ $message }}</p>
        @enderror

        <label>Confirmar nova senha</label>
        <input type="password" name="password_confirmation">

        @can('cadastrar-usuarios')
            <label>Papel</label>
            <select name="papel">
                <option value="comum" @selected($usuario->papel === 'comum')>Comum</option>
                <option value="gerente" @selected($usuario->papel === 'gerente')>Gerente</option>
                <option value="admin" @selected($usuario->papel === 'admin')>Admin</option>
            </select>
        @endcan

        <button type="submit">Salvar</button>
    </form>
@endsection

@method('PUT') é novo — HTML puro só sabe mandar formulário via GET ou POST, então o Laravel usa um campo escondido pra "disfarçar" a requisição como PUT por baixo dos panos; a rota Route::resource criada mais cedo já espera exatamente isso. old('name', $usuario->name) é uma variação do old() que você já conhece (Aula 5): usa o valor antigo se veio de um erro de validação, e cai pro segundo parâmetro (o valor atual do usuário) se for a primeira vez abrindo a tela. Os campos de senha ficam em branco de propósito (diferente dos outros, eles não usam old() com o valor atual — a senha já vem criptografada no banco, não haveria como pré-preencher ela mesmo se quisesse); o rótulo "deixe em branco pra manter a atual" é tão importante quanto o código, porque sem esse aviso é fácil a pessoa achar que precisa preencher de novo, ou ter medo de mexer no formulário. O campo password_confirmation existe porque a regra confirmed (Aula VAL1) exige um campo com esse nome exato, contendo o mesmo valor — sem ele no formulário, qualquer tentativa de trocar a senha falharia na validação. @selected($condição) marca a <option> certa como selecionada automaticamente, sem você escrever selected="selected" na mão. E repare que o campo papel nem aparece na tela pra quem não tem a permissão de cadastrar usuários — a mesma checagem @can que você já usa desde a Aula 9.

Um link pra chegar até aqui

resources/views/usuarios/index.blade.php (trecho)
@can('editar-usuario', $usuario)
    <a href="/usuarios/{{ $usuario->id }}/edit">Editar</a>
@endcan

Mão na massa

Troque as três rotas soltas por Route::resource(...)->except('show'). Adicione o Gate editar-usuario no AppServiceProvider, junto do cadastrar-usuarios que já existe. Crie os métodos edit()/update(), a view de edição (já com os campos de senha) e o link na lista. Teste quatro cenários: (1) logado como comum, edite seu próprio nome — deve funcionar, e o campo papel nem deve aparecer; (2) tente editar outro usuário direto pela URL — deve dar 403; (3) logado como admin, edite qualquer usuário, incluindo o papel dele; (4) edite um usuário deixando os campos de senha em branco — confirme, pelo Tinker, que a senha antiga continua funcionando pra login; depois edite de novo preenchendo uma senha nova e confirme que a antiga para de funcionar.

A regra: só admin exclui, e nunca a própria conta

Excluir merece uma regra mais restrita que editar — faz sentido um gerente editar outros usuários, mas normalmente só o admin deveria poder removê-los de vez. E ninguém deveria conseguir excluir a própria conta (imagina o problema de um admin sozinho no sistema fazendo isso sem querer). Mais um Gate, seguindo o mesmo padrão do editar-usuario:

app/Providers/AppServiceProvider.php (adicionando ao boot() que já existe)
Gate::define('excluir-usuario', function (User $user, User $usuario) {
    return $user->papel === 'admin' && $user->id !== $usuario->id;
});

Uma tela de confirmação, em vez de um alerta do navegador

É comum ver excluir algo disparando um confirm() do JavaScript — mas esse projeto continua sem nenhuma linha de JS (Aula 28), então a confirmação vira uma tela de verdade, renderizada pelo servidor como qualquer outra. Precisa de uma rota a mais, que o Route::resource não cria sozinho:

routes/web.php
Route::middleware('auth')->group(function () {
    Route::resource('usuarios', UsuarioController::class);
    Route::get('/usuarios/{usuario}/confirmar-exclusao', [UsuarioController::class, 'confirmarExclusao']);
});
app/Http/Controllers/UsuarioController.php
public function confirmarExclusao(User $usuario)
{
    Gate::authorize('excluir-usuario', $usuario);

    return view('usuarios.confirmar-exclusao', compact('usuario'));
}

public function destroy(User $usuario)
{
    Gate::authorize('excluir-usuario', $usuario);

    $usuario->delete();

    return redirect('/usuarios')->with('sucesso', 'Usuário excluído.');
}

Lembrando o que já decidimos na Aula 4

Vale recapitular antes de seguir: se você tentar excluir um usuário que já cadastrou outros, o que acontece depende exatamente da constraint que definimos lá na Aula 4 — no comportamento que escolhemos (sem cascadeOnDelete() nem nullOnDelete()), o banco bloqueia essa exclusão. Se esse for o comportamento que você quer pro seu projeto, não precisa fazer nada a mais aqui. Se quiser permitir a exclusão mesmo assim, a própria Aula 4 já mostra a troca pra nullOnDelete() — vale voltar lá pra revisar as opções antes de decidir.

$usuario->delete() é só uma das formas de excluir

Usamos $usuario->delete() porque, graças ao Route Model Binding (Aula 2), já chegamos no método com o model carregado — faz sentido excluir a partir dele. Mas o Eloquent tem mais de um jeito de apagar um registro, e vale conhecer todos pra escolher o certo em cada situação:

as várias formas
// A que usamos: a partir de uma instância já carregada
$usuario = User::find(1);
$usuario->delete(); // devolve true ou false

// Por ID, sem precisar carregar o model primeiro
User::destroy(1);
User::destroy([1, 2, 3]);   // vários, como array
User::destroy(1, 2, 3);     // vários, como argumentos separados

// Por condição, direto no Query Builder — sem carregar model nenhum
User::where('papel', 'comum')->where('ultimo_acesso', '<', now()->subYear())->delete();

User::destroy(...) é um atalho conveniente quando você só tem o(s) ID(s) em mãos — tipo numa lista de checkboxes marcados num painel admin — e não quer escrever o find() manualmente. Por baixo dos panos, ele carrega cada model e chama delete() em cada um, então funciona exatamente como a forma que usamos: dispara os Model Events do Eloquent normalmente — eventos internos como deleting/deleted, que o próprio model dispara sozinho em cada etapa do ciclo de vida dele (criar, atualizar, excluir), sem relação com o Event/Listener customizado que você construiu na Aula 15. Não chegamos a usar Model Events nesse projeto, mas é uma peça comum o suficiente pra valer saber que ela existe.

User::where(...)->delete() é diferente de verdade, não só na sintaxe: ele manda um DELETE direto pro banco, pra todas as linhas que baterem com a condição, sem nunca instanciar um model individual pra cada uma. Isso é bem mais rápido pra apagar muitos registros de uma vez (não precisa carregar nada em memória) — mas, como consequência, não dispara nenhum Model Event. Se algum dia esse projeto ganhasse uma limpeza extra amarrada à exclusão de um usuário (apagar a foto de perfil do Storage, por exemplo — Aula 25), um where(...)->delete() passaria batido por essa lógica, enquanto $usuario->delete() ou User::destroy(...) não.

Regra práticaTem um usuário específico (veio de uma rota, de um formulário)? Use a instância: $usuario->delete(). Tem só o ID, ou uma lista de IDs? User::destroy(...). Quer apagar em massa por uma condição, e não se importa em pular os eventos do model? where(...)->delete() — mais rápido, mas ciente do que está abrindo mão.
terminal
php artisan make:view usuarios.confirmar-exclusao
resources/views/usuarios/confirmar-exclusao.blade.php
@extends('layouts.app')

@section('titulo', 'Confirmar exclusão')

@section('conteudo')
    <h1>Excluir {{ $usuario->name }}?</h1>
    <p>Essa ação não pode ser desfeita.</p>

    <form method="POST" action="/usuarios/{{ $usuario->id }}">
        @csrf
        @method('DELETE')
        <button type="submit">Sim, excluir</button>
    </form>

    <a href="/usuarios">Cancelar</a>
@endsection

Igual na edição, @method('DELETE') disfarça esse POST como DELETE pra bater com a rota que o Route::resource já criou. E o link na lista de usuários aponta pra essa tela de confirmação, não direto pro destroy():

resources/views/usuarios/index.blade.php (trecho)
@can('excluir-usuario', $usuario)
    <a href="/usuarios/{{ $usuario->id }}/confirmar-exclusao">Excluir</a>
@endcan

Mão na massa

Adicione o Gate excluir-usuario, a rota de confirmação, os métodos confirmarExclusao()/destroy() e a view. Teste: logado como gerente, o link de excluir não deve nem aparecer (o Gate nega). Logado como admin, tente excluir a si mesmo direto pela URL — deve dar 403. Exclua um usuário comum qualquer — deve funcionar, com a mensagem verde de sucesso aparecendo. Depois, tente excluir um usuário que já cadastrou outro (como a Ana, se ela criou alguém lá na Aula 6) — com a constraint que definimos na Aula 4, isso deve dar erro; observe o que acontece na tela (vamos tratar isso com cuidado na Aula 26).

Avançado

Aulas 12–13, 14–17, 18–19

O problema que isso resolve

Daqui a pouco (Aula 15 e 24) vamos precisar mandar uma mensagem de WhatsApp de boas-vindas pro usuário recém-cadastrado, usando um serviço externo chamado evolution-api. Se toda vez que precisarmos disso fizermos new EvolutionApiService(...) na mão, repetimos código e fica difícil trocar a configuração depois. O Service Container resolve isso.

O que é o Service Container

É o "gerente" central do Laravel que sabe como construir qualquer classe da sua aplicação, incluindo suas dependências. Em vez de instanciar manualmente, você pede ao container e ele monta o objeto pra você.

Criando o serviço

Laravel não tem um comando make:service pronto (Services não são um conceito "oficial" do framework, é só uma convenção de organização) — mas existe um gerador genérico, make:class, que serve exatamente pra esse tipo de situação:

terminal
php artisan make:class Services/EvolutionApiService

Isso cria o arquivo já no lugar certo (app/Services/EvolutionApiService.php) com o namespace App\Services preenchido sozinho — só falta você completar com o construtor e os métodos:

app/Services/EvolutionApiService.php
namespace App\Services;

class EvolutionApiService
{
    public function __construct(
        protected string $url,
        protected string $chave,
    ) {}

    public function enviarMensagem(string $numero, string $texto): void
    {
        // a implementação real vem na Aula 24, com HTTP Client
    }
}

public function __construct(protected string $url, protected string $chave) é uma forma resumida do PHP de declarar E já guardar duas propriedades da classe ao mesmo tempo — sem essa abreviação, você teria que escrever $this->url = $url; manualmente dentro do construtor.

Injeção de dependência — pedindo o serviço pronto

Se você pedir EvolutionApiService em qualquer construtor ou método do seu projeto, o Laravel tenta montar ele sozinho. Mas como o construtor espera $url e $chave (duas strings), o Laravel não sabe de onde tirar esses valores — precisamos ensinar ele, e é isso que a próxima aula (Service Providers) resolve.

exemplo de uso, depois de configurado
class UsuarioController extends Controller
{
    public function __construct(protected EvolutionApiService $evolutionApi) {}

    // agora $this->evolutionApi já vem pronto, configurado, em qualquer método
}

Mão na massa

Crie o arquivo app/Services/EvolutionApiService.php exatamente como no exemplo (o método enviarMensagem pode ficar vazio por enquanto). Não precisa usar ele ainda — isso vem na Aula 13 e na Aula 24.

O que é um Service Provider

É o lugar central onde você "liga" as coisas: registra bindings (as instruções de como construir uma classe) no Service Container, configura serviços, registra eventos. Todo projeto novo já vem com um, o AppServiceProvider — foi ali mesmo que definimos o Gate na Aula 9.

Ensinando o container a montar o EvolutionApiService

app/Providers/AppServiceProvider.php
use App\Services\EvolutionApiService;

public function register(): void
{
    $this->app->singleton(EvolutionApiService::class, function ($app) {
        return new EvolutionApiService(
            url: config('services.evolution_api.url'),
            chave: config('services.evolution_api.chave'),
        );
    });
}

register() é o método certo pra isso — ele roda bem cedo, antes de qualquer outra coisa do sistema, e serve exclusivamente pra ensinar o container a construir classes. Usamos singleton (em vez de bind) porque não faz sentido criar uma nova conexão com a API do WhatsApp toda vez que alguém pede o serviço — uma instância única, reaproveitada durante toda a requisição, já resolve.

A partir daqui, qualquer lugar do seu projeto que pedir EvolutionApiService $evolutionApi num construtor recebe ele já pronto, com url e chave preenchidos — sem repetir essa configuração em lugar nenhum.

De onde vêm essas configs (preview da Aula 21)

Repare que usamos config('services.evolution_api.url') em vez de pegar direto do .env. Isso é proposital — vamos explicar exatamente por quê na Aula 21. Por enquanto, adicione isso no arquivo de config que já existe:

config/services.php
'evolution_api' => [
    'url' => env('EVOLUTION_API_URL'),
    'chave' => env('EVOLUTION_API_KEY'),
],
.env
EVOLUTION_API_URL=http://localhost:8080
EVOLUTION_API_KEY=sua-chave-aqui

Mão na massa

Adicione o bloco evolution_api em config/services.php e as duas variáveis no .env (pode deixar valores de mentira por enquanto, já que ainda não estamos chamando a API de verdade). Registre o singleton do EvolutionApiService no AppServiceProvider.

O problema que isso resolve

Precisamos de algo que rode sozinho, todo dia, sem ninguém precisar clicar em nada — verificando quem faz aniversário hoje e mandando um e-mail de parabéns. Isso não pode depender de uma requisição do navegador (ninguém vai acessar o site especificamente pra "disparar" essa checagem). É exatamente pra isso que existem Jobs combinados com o agendador (scheduler) do Laravel.

O que é um Job

Job é uma classe que representa "uma tarefa que pode ser executada depois, de forma assíncrona (não bloqueando quem a disparou)" — normalmente processada por um worker, um processo separado que fica escutando a fila e executando os jobs conforme chegam.

terminal
php artisan make:job VerificarAniversariantesJob
app/Jobs/VerificarAniversariantesJob.php
namespace App\Jobs;

use App\Mail\FelizAniversarioMail;
use App\Models\User;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Facades\Mail;

class VerificarAniversariantesJob implements ShouldQueue
{
    use Queueable;

    public function handle(): void
    {
        $aniversariantes = User::whereMonth('data_nascimento', now()->month)
            ->whereDay('data_nascimento', now()->day)
            ->get();

        foreach ($aniversariantes as $usuario) {
            Mail::to($usuario->email)->send(new FelizAniversarioMail($usuario));
        }
    }
}

implements ShouldQueue é o que diz ao Laravel "isso pode rodar em segundo plano, não precisa ser na hora". now()->month e now()->day pegam o mês e o dia de hoje. whereMonth/whereDay comparam só essa parte da data, ignorando o ano — assim pegamos todo mundo que faz aniversário hoje, não importa em que ano nasceu. O Mail::to()->send() vamos entender em detalhe na Aula 22; por enquanto, saiba que ele manda um e-mail usando o "molde" FelizAniversarioMail, que também vamos criar lá.

Uma trait só, e o que ela faz

use Queueable;, importada de Illuminate\Foundation\Queue\Queueable, é o padrão atual de Job no Laravel — uma única trait reunindo tudo que antes vinha espalhado em quatro (Dispatchable, InteractsWithQueue, a Queueable antiga de Illuminate\Bus, e SerializesModels). Se você encontrar tutoriais ou projetos mais antigos mostrando as quatro juntas, é esse o padrão anterior — ainda funciona, mas não é mais o que o make:job gera. Essa trait única entrega:

  • O método estático ::dispatch() (e variações como dispatchIf(), dispatchSync()) — sem ela, não existiria a sintaxe VerificarAniversariantesJob::dispatch() que você vai usar; precisaria instanciar a classe e empurrar pra fila manualmente, na mão.
  • Métodos que controlam o processamento na própria fila, de dentro do handle(): $this->release(30) devolve o job pra fila pra tentar de novo em 30 segundos, $this->delete() remove o job antes da hora, $this->attempts() conta quantas vezes já foi tentado. Nosso Job não usa nada disso ainda, mas o método fica disponível caso precise.
  • As configurações de como o job deve ser enfileirado: em qual fila nomeada (->onQueue('baixa-prioridade')), em qual conexão (->onConnection('redis')), com que atraso (->delay(now()->addMinutes(5))) — métodos encadeáveis na hora de disparar.
  • Serialização de qualquer model do Eloquent recebido no construtor (não é o nosso caso aqui, mas apareceu nos Listeners da Aula 15) — e vai além do que a trait antiga fazia: além de guardar só a classe e o ID pra buscar de novo no banco quando o worker for rodar, ela também preserva os relacionamentos já carregados (with(...)) do model, algo que a versão anterior não fazia sozinha.

Outras peças que existem, e quando usar

ShouldQueue, repare, não é uma trait — é uma interface, e é ela (não a trait Queueable) quem decide se o Job realmente vai pra fila ou roda na hora, bloqueando quem chamou. Um Job sem implements ShouldQueue ainda pode ter a trait (ela não atrapalha em nada rodando síncrono), mas executaria imediatamente — a trait só faz sentido "de verdade" combinada com essa interface.

Uma trait a mais que vale conhecer: Illuminate\Bus\Batchable — essa continua separada, não foi absorvida pela Queueable nova. Ela entra quando você precisa disparar vários Jobs relacionados de uma vez e acompanhar o progresso do grupo inteiro (via Bus::batch([...])) — dá acesso a $this->batch() de dentro do handle(), pra saber em qual lote esse job está, quantos do grupo já terminaram, ou até cancelar o lote inteiro se algo der muito errado no meio do caminho. Usa-se junto: use Batchable, Queueable;. Não usamos isso nesse projeto (só temos um Job rodando sozinho), mas é comum em cenários tipo "processar 10 mil registros em paralelo e avisar quando tudo terminar".

Agendando o Job pra rodar todo dia

No Laravel 13, o agendamento fica em routes/console.php. Isso não executa o Job agora — só ensina o Laravel a rodar ele sozinho, todo dia, num horário fixo.

routes/console.php
use App\Jobs\VerificarAniversariantesJob;
use Illuminate\Support\Facades\Schedule;

Schedule::job(new VerificarAniversariantesJob)->dailyAt('08:00');

Pra isso funcionar de verdade num servidor de produção, é preciso configurar uma única entrada de cron (o agendador nativo do Linux) rodando a cada minuto — o Laravel decide sozinho, internamente, o que precisa executar naquele momento com base no que você agendou.

crontab do servidor (produção)
* * * * * cd /caminho/do/projeto && php artisan schedule:run >> /dev/null 2>&1

Em desenvolvimento, você não precisa configurar cron nenhum pra testar — dá pra disparar o Job manualmente.

Isso não é o mesmo problema que os Listeners em filaEssa entrada de crontab resolve só o agendamento — "rode isso uma vez por dia, num horário fixo". Ela não tem relação com os Listeners em fila que você vai construir na Aula 15 (EnviarBoasVindasEmailListener e os outros, todos com ShouldQueue) — esses precisam de um processo php artisan queue:work rodando continuamente, não uma vez por minuto, pra ir processando a fila assim que algo cai nela. São dois mecanismos complementares, não alternativos: o crontab aciona o agendador, e é o Supervisor quem mantém o queue:work de pé — se ele cair (memória, reinício do servidor), ninguém reinicia sozinho sem essa peça. Vamos configurar isso na Aula D1, junto do resto do deploy.

Testando sem esperar até amanhã

terminal (tinker)
php artisan tinker
dentro do tinker
(new App\Jobs\VerificarAniversariantesJob)->handle();

Isso chama o método handle() direto, sem passar pela fila — útil só pra testar rapidamente que a lógica funciona.

Mão na massa

Crie o VerificarAniversariantesJob (o FelizAniversarioMail ainda não existe — vamos criar na Aula 22, então por enquanto troque o corpo do foreach por um simples logger()->info("Feliz aniversário, {$usuario->name}!");, só pra confirmar que a busca funciona). Agende ele em routes/console.php. Pelo Tinker, atualize a data de nascimento de um usuário pra hoje (User::find(1)->update(['data_nascimento' => now()])) e rode o Job manualmente — confira em storage/logs/laravel.log se a mensagem apareceu.

O problema que isso resolve

A regra do nosso projeto é: sempre que um usuário com papel gerente (não o admin) cadastra alguém novo, três coisas precisam acontecer — o admin recebe um e-mail avisando, o novo usuário recebe um e-mail de boas-vindas, e o novo usuário recebe uma mensagem de WhatsApp. Se colocarmos essas três ações direto dentro do método store() do Controller, ele vira um método gigante, fazendo coisas demais. Event (evento) e Listener (ouvinte) resolvem isso: o Controller só anuncia "um usuário foi cadastrado", e cada Listener decide, por conta própria, se e como reagir.

Criando o Event

terminal
php artisan make:event UsuarioCadastrado
app/Events/UsuarioCadastrado.php
namespace App\Events;

use App\Models\User;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class UsuarioCadastrado
{
    use Dispatchable, SerializesModels;

    public function __construct(
        public User $novoUsuario,
        public User $criador,
    ) {}
}

O Event é basicamente um "envelope" de dados, sem lógica nenhuma — só carrega o usuário recém-criado e quem foi o criador, pra quem for reagir ter essa informação disponível.

Disparando o Event no Controller

app/Http/Controllers/UsuarioController.php
use App\Events\UsuarioCadastrado;
use Illuminate\Support\Facades\Auth;

public function store(Request $request)
{
    Gate::authorize('cadastrar-usuarios');

    $validado = $request->validate([ /* ... como na Aula 5 */ ]);
    $validado['password'] = bcrypt($validado['password']);
    $validado['criador_id'] = Auth::id(); // já preenchido desde a Aula 9

    $novoUsuario = User::create($validado);

    UsuarioCadastrado::dispatch($novoUsuario, Auth::user());

    return redirect('/usuarios')->with('sucesso', 'Usuário cadastrado com sucesso!');
}

A única linha nova aqui é o UsuarioCadastrado::dispatch($novoUsuario, Auth::user()) no final — o resto do método é o mesmo que você já tem desde a Aula 9. dispatch(...) dispara o evento, passando o usuário recém-criado e quem estava logado no momento (o "criador") — o Controller não sabe (nem precisa saber) o que vai acontecer depois disso.

Criando os três Listeners

Cada Listener cuida de uma reação. Todos vão checar se quem criou é um gerente — se foi o próprio admin que cadastrou, ninguém precisa ser notificado.

terminal
php artisan make:listener NotificarAdminListener --event=UsuarioCadastrado
php artisan make:listener EnviarBoasVindasEmailListener --event=UsuarioCadastrado
php artisan make:listener EnviarBoasVindasWhatsappListener --event=UsuarioCadastrado
app/Listeners/NotificarAdminListener.php
namespace App\Listeners;

use App\Events\UsuarioCadastrado;
use App\Mail\NovoUsuarioCriadoMail;
use App\Models\User;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Mail;

class NotificarAdminListener implements ShouldQueue
{
    public function handle(UsuarioCadastrado $event): void
    {
        if ($event->criador->papel !== 'gerente') {
            return; // só notifica quando quem cadastrou é gerente
        }

        $admins = User::where('papel', 'admin')->get();

        foreach ($admins as $admin) {
            Mail::to($admin->email)->send(new NovoUsuarioCriadoMail($event->novoUsuario, $event->criador));
        }
    }
}

O return; sozinho, sem valor nenhum, é uma forma de "sair do método imediatamente" — se a condição não bater, o resto do código simplesmente não roda.

app/Listeners/EnviarBoasVindasEmailListener.php
namespace App\Listeners;

use App\Events\UsuarioCadastrado;
use App\Mail\BoasVindasMail;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Mail;

class EnviarBoasVindasEmailListener implements ShouldQueue
{
    public function handle(UsuarioCadastrado $event): void
    {
        if ($event->criador->papel !== 'gerente') {
            return;
        }

        Mail::to($event->novoUsuario->email)->send(new BoasVindasMail($event->novoUsuario));
    }
}
app/Listeners/EnviarBoasVindasWhatsappListener.php
namespace App\Listeners;

use App\Events\UsuarioCadastrado;
use App\Services\EvolutionApiService;
use Illuminate\Contracts\Queue\ShouldQueue;

class EnviarBoasVindasWhatsappListener implements ShouldQueue
{
    public function handle(UsuarioCadastrado $event): void
    {
        if ($event->criador->papel !== 'gerente') {
            return;
        }

        app(EvolutionApiService::class)->enviarMensagem(
            numero: $event->novoUsuario->telefone ?? '',
            texto: "Olá {$event->novoUsuario->name}! Seja bem-vindo(a) ao painel."
        );
    }
}

app(EvolutionApiService::class) pede o serviço direto do container (você viu esse padrão na Aula 12) — dentro de um Listener, que não é uma classe onde normalmente injetamos pelo construtor com tanta frequência, esse é o jeito mais direto de pegar o serviço já configurado.

Implementar ShouldQueue nos três Listeners faz cada um rodar em fila, separadamente — se o WhatsApp cair, isso não atrasa nem trava o e-mail de boas-vindas, e vice-versa.

Por que não usamos a classe Notification aquiO Laravel tem uma ferramenta chamada Notification (você vai ver na Aula 23) que também lida com múltiplos canais numa única classe. Optamos por Event + Listener aqui de propósito: como cada canal tem uma regra ligeiramente diferente e pode crescer separadamente no futuro, ter um Listener por canal deixa mais fácil adicionar um quarto canal (ex: Slack) sem tocar nos outros três.

Mão na massa

Crie o Event UsuarioCadastrado e os três Listeners. Ajuste o método store() pra preencher criador_id e disparar o evento (os Mails ainda não existem — pra não quebrar, troque temporariamente o corpo de cada listener por um logger()->info(...) descrevendo o que ele faria; vamos trocar pelos Mails de verdade na Aula 22). Logado como um usuário gerente (crie um pelo Tinker com 'papel' => 'gerente'), cadastre um novo usuário pelo formulário e confira em storage/logs/laravel.log se as três mensagens apareceram. Depois, logado como admin, cadastre outro usuário e confirme que nenhuma mensagem aparece — por causa do return condicional em cada Listener.

O problema que isso resolve

Imagine um pequeno painel mostrando "3 admins, 5 gerentes, 40 usuários comuns" no topo da tela de lista. Se essa contagem for feita a cada visita à página, e a página for visitada centenas de vezes por hora, você está refazendo o mesmo cálculo repetidamente à toa — o número de admins/gerentes muda raramente. Cache é guardar esse resultado num lugar de acesso rápido, pra reaproveitar em vez de recalcular.

remember() — a ferramenta certa pra isso

app/Http/Controllers/UsuarioController.php
use Illuminate\Support\Facades\Cache;

public function index()
{
    $usuarios = User::all();

    $contagemPorPapel = Cache::remember('contagem_usuarios_por_papel', now()->addMinutes(10), function () {
        return [
            'admin' => User::admins()->count(),
            'gerente' => User::gerentes()->count(),
            'comum' => User::where('papel', 'comum')->count(),
        ];
    });

    return view('usuarios.index', compact('usuarios', 'contagemPorPapel'));
}

Cache::remember('chave', tempo, function) funciona assim: "se já existe algo guardado com essa chave, devolve direto; se não existe (ou já expirou), executa a closure, guarda o resultado por 10 minutos, e devolve". Na próxima visita, dentro desses 10 minutos, o Laravel nem toca no banco — devolve o valor já calculado.

Invalidando quando um usuário novo é cadastrado

Se alguém cadastrar um usuário novo, essa contagem guardada fica desatualizada até os 10 minutos passarem. Pra corrigir na hora, invalidamos o cache assim que um novo usuário é criado — dá pra fazer isso dentro do próprio Listener que já criamos:

app/Listeners/NotificarAdminListener.php (adicionando uma linha)
public function handle(UsuarioCadastrado $event): void
{
    Cache::forget('contagem_usuarios_por_papel');

    if ($event->criador->papel !== 'gerente') {
        return;
    }

    // ... resto igual
}

Repare que colocamos o Cache::forget() antes do return condicional — porque a contagem deve ser invalidada sempre que um usuário for criado, não só quando o criador é gerente.

Mão na massa

Adicione o Cache::remember no método index e mostre a contagem no topo da view de lista (ex: <p>Admins: {{ $contagemPorPapel['admin'] }}</p>). Adicione o Cache::forget no Listener. Cadastre um usuário novo e confirme que a contagem na tela atualiza na hora, sem esperar os 10 minutos.

O problema que isso resolve

A regra do projeto diz: usuários comuns só podem ver nome e data de nascimento de cada usuário — não o e-mail, nem o papel de cada um. Se algum dia você construir uma versão em JSON dessa lista (pra consumir via JavaScript, por exemplo — o que vamos fazer na Aula 28), retornar o Model inteiro exporia campos demais. API Resource é a camada que decide exatamente o que sai no JSON, com base em quem está pedindo.

Criando o Resource

terminal
php artisan make:resource UsuarioResource
app/Http/Resources/UsuarioResource.php
namespace App\Http\Resources;

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

class UsuarioResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'name' => $this->name,
            'data_nascimento' => $this->data_nascimento,
            'idade' => $this->idade,

            // só aparece se quem está vendo puder cadastrar usuários
            'email' => $this->when(
                $request->user()->can('cadastrar-usuarios'),
                $this->email
            ),
            'papel' => $this->when(
                $request->user()->can('cadastrar-usuarios'),
                $this->papel
            ),
        ];
    }
}

$request->user()->can('cadastrar-usuarios') reaproveita o mesmo Gate que criamos na Aula 9 — não escrevemos a regra de novo, só perguntamos "esse usuário passa nessa regra?". $this->when(condição, valor) só inclui o campo no JSON se a condição for verdadeira; se for falsa, o campo simplesmente não aparece (diferente de aparecer como null).

Usando o Resource num Controller de API

exemplo (endpoint JSON)
public function indexJson()
{
    $usuarios = User::all();
    return UsuarioResource::collection($usuarios);
}

UsuarioResource::collection($usuarios) aplica a mesma transformação pra cada item de uma lista, devolvendo tudo já em JSON — sem você chamar json_encode manualmente.

Mão na massa

Crie o UsuarioResource exatamente como no exemplo. Não precisa criar uma rota JSON agora (isso volta na Aula 28, quando o front-end de verdade precisar consumir esses dados) — só deixe a classe pronta.

O problema que isso resolve

Até agora, promover alguém a gerente exigia abrir o Tinker e rodar código na mão. Isso funciona, mas não é prático pro dia a dia. Um Artisan Command customizado transforma isso num comando de terminal de verdade, com nome e parâmetros — algo que qualquer pessoa da equipe pode rodar sem saber PHP.

Criando o Command

terminal
php artisan make:command PromoverUsuarioCommand
app/Console/Commands/PromoverUsuarioCommand.php
namespace App\Console\Commands;

use App\Models\User;
use Illuminate\Console\Command;

class PromoverUsuarioCommand extends Command
{
    protected $signature = 'usuarios:promover {email} {--papel=gerente}';

    protected $description = 'Promove um usuário existente pra outro papel (gerente ou admin)';

    public function handle(): int
    {
        $usuario = User::where('email', $this->argument('email'))->first();

        if (!$usuario) {
            $this->error('Nenhum usuário encontrado com esse e-mail.');
            return self::FAILURE;
        }

        $novoPapel = $this->option('papel');

        $usuario->update(['papel' => $novoPapel]);

        $this->info("{$usuario->name} agora é {$novoPapel}.");

        return self::SUCCESS;
    }
}

{email} no $signature é um argumento obrigatório (um valor que você digita na ordem, sem prefixo). {--papel=gerente} é uma opção com valor padrão — se você não informar --papel, assume gerente automaticamente. $this->error() e $this->info() imprimem mensagens coloridas no terminal (vermelho pra erro, verde/normal pra sucesso).

Rodando o comando

terminal
php artisan usuarios:promover ana@exemplo.com
php artisan usuarios:promover joao@exemplo.com --papel=admin

Mão na massa

Crie o PromoverUsuarioCommand exatamente como no exemplo. Rode php artisan usuarios:promover passando o e-mail de um dos usuários que você já cadastrou, sem passar --papel (deve virar gerente por padrão). Confirme no Tinker (User::where('email', '...')->first()->papel) que o papel mudou.

O problema: o método store() está fazendo coisa demais

Olhando o store() que construímos até aqui, ele já: valida, transforma a senha, preenche o criador, cria o usuário, e dispara o evento. Se um dia você precisar cadastrar um usuário também por um comando de terminal (tipo um script de importação em lote), teria que copiar essa lógica toda de novo. Uma Action resolve isso: uma classe com uma única responsabilidade, chamável de qualquer lugar.

Criando a Action

Assim como os Services (Aula 12), Actions não têm um gerador dedicado — usamos make:class de novo:

terminal
php artisan make:class Actions/CadastrarUsuarioAction
app/Actions/CadastrarUsuarioAction.php
namespace App\Actions;

use App\Events\UsuarioCadastrado;
use App\Models\User;

class CadastrarUsuarioAction
{
    public function executar(array $dados, User $criador): User
    {
        $dados['password'] = bcrypt($dados['password']);
        $dados['criador_id'] = $criador->id;

        $novoUsuario = User::create($dados);

        UsuarioCadastrado::dispatch($novoUsuario, $criador);

        return $novoUsuario;
    }
}

O Controller fica enxuto

app/Http/Controllers/UsuarioController.php
use App\Actions\CadastrarUsuarioAction;

public function store(Request $request, CadastrarUsuarioAction $action)
{
    Gate::authorize('cadastrar-usuarios');

    $validado = $request->validate([
        'name' => 'required|string|max:255',
        'email' => 'required|email|unique:users,email',
        'password' => 'required|string|min:6|confirmed',
        'data_nascimento' => 'required|date',
        'papel' => 'required|in:admin,gerente,comum',
    ]);

    $action->executar($validado, Auth::user());

    return redirect('/usuarios')->with('sucesso', 'Usuário cadastrado com sucesso!');
}

Repare que CadastrarUsuarioAction $action aparece como parâmetro do método — é injeção de dependência de novo (Aula 12), o Laravel monta a Action sozinho. O Controller agora só cuida do que é responsabilidade dele: receber a requisição, validar, e devolver uma resposta. Toda a regra de negócio de "o que significa cadastrar um usuário" mora na Action, reaproveitável por qualquer outro lugar que precisar dela no futuro (o comando da Aula 18, por exemplo, poderia usar essa mesma Action em vez de fazer update() direto).

Por que só agora?Repare que, até aqui, deixamos a lógica direto no Controller de propósito — pra você primeiro ver o problema crescendo (Aula 5 → 9 → 15, o método store() foi inchando aula após aula) antes de aprender a solução. Essa é exatamente a lição da Aula 19 original: comece simples, e só introduza uma Action quando sentir a dor de verdade.

Mão na massa

Crie a CadastrarUsuarioAction movendo pra lá a lógica que hoje está no store() (transformar senha, preencher criador, criar usuário, disparar evento). Atualize o Controller pra só validar e chamar a Action. Cadastre um usuário pelo formulário de novo e confirme que continua funcionando exatamente igual — o comportamento não muda, só a organização do código.

Ferramentas do dia a dia

Aulas 20–28

O problema que isso resolve

Até aqui, criamos o primeiro usuário na mão, pelo Tinker. Isso é péssimo pra um sistema de verdade: cada vez que alguém instalar o projeto do zero (você numa máquina nova, um colega, o servidor de produção), teria que lembrar de criar o admin manualmente. Seeder resolve isso — é código versionado que popula o banco de forma automática e repetível.

Criando o Seeder do admin

terminal
php artisan make:seeder AdminSeeder
database/seeders/AdminSeeder.php
namespace Database\Seeders;

use App\Models\User;
use Illuminate\Database\Seeder;

class AdminSeeder extends Seeder
{
    public function run(): void
    {
        User::create([
            'name' => 'Administrador',
            'email' => 'admin@painel.com',
            'password' => bcrypt('admin123'),
            'papel' => 'admin',
            'data_nascimento' => '1990-01-01',
        ]);
    }
}

Registrando no DatabaseSeeder

Existe um Seeder "orquestrador", chamado quando você roda só php artisan db:seed sem especificar classe:

database/seeders/DatabaseSeeder.php
namespace Database\Seeders;

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    public function run(): void
    {
        $this->call([
            AdminSeeder::class,
        ]);
    }
}

Rodando

terminal
php artisan db:seed

Ou, se você quiser recomeçar do zero (apaga o banco, recria todas as tabelas, e já popula com os Seeders — útil em desenvolvimento):

terminal
php artisan migrate:fresh --seed
Cuidado com duplicarSe você rodar db:seed duas vezes sem recriar o banco, vai tentar criar dois admins com o mesmo e-mail e vai dar erro (lembra da regra unique na validação? o banco também tem essa restrição, se você configurou a coluna email como única). Uma forma mais segura, pra rodar em produção sem risco, é usar firstOrCreate em vez de create: User::firstOrCreate(['email' => 'admin@painel.com'], [...resto dos dados...]); — isso só cria se ainda não existir ninguém com aquele e-mail.

Mão na massa

Crie o AdminSeeder e registre ele no DatabaseSeeder. Rode php artisan migrate:fresh --seed pra recomeçar o banco do zero, já com o admin criado automaticamente. Faça login com admin@painel.com / admin123 e confirme que dá pra acessar a tela de cadastro.

O problema que isso resolve

Na Aula 13, usamos config('services.evolution_api.url') em vez de env('EVOLUTION_API_URL') direto no Service Provider. Existe um motivo técnico real pra isso, não é só estilo: em produção, o Laravel costuma rodar com config:cache ativado (uma otimização de performance) — e esse cache só lê os arquivos dentro de config/. Uma chamada a env() feita fora de config/ (direto no Service Provider, num Controller, em qualquer lugar) pode simplesmente devolver null quando esse cache está ativo — um bug sutil que só aparece em produção, nunca no seu ambiente local.

A regra de ouro

resumo
.env                  → guarda o VALOR (varia por ambiente: dev, produção)
config/algo.php       → guarda a CHAVE de acesso a esse valor, com env() (só aqui!)
resto do código        → usa config('algo.chave'), nunca env() direto

Já fizemos isso — revisando

config/services.php
'evolution_api' => [
    'url' => env('EVOLUTION_API_URL'),
    'chave' => env('EVOLUTION_API_KEY'),
],

Vamos aproveitar essa aula pra criar mais um arquivo de config — dessa vez pra uma regra de negócio do próprio sistema, não uma credencial externa:

config/usuarios.php
<?php

return [
    'papeis_disponiveis' => ['admin', 'gerente', 'comum'],
    'papeis_que_podem_cadastrar' => ['admin', 'gerente'],
];

Isso NÃO vem do .env (não é segredo nem varia por ambiente) — é só um lugar central pra não espalhar a lista ['admin', 'gerente', 'comum'] em vários arquivos diferentes (na validação, no Gate, no formulário). Se um dia você quiser adicionar um quarto papel, muda num lugar só.

reaproveitando na validação (Aula 5)
'papel' => 'required|in:' . implode(',', config('usuarios.papeis_disponiveis')),

implode(',', [...]) é uma função nativa do PHP que junta os itens de um array numa única string, separados pelo caractere que você escolher — aqui, transforma ['admin', 'gerente', 'comum'] em "admin,gerente,comum", exatamente o formato que a regra in: espera.

Mão na massa

Crie config/usuarios.php com os dois itens acima. Atualize a regra de validação do campo papel no store() (ou na Action, se você já fez o exercício da Aula 19) pra usar config('usuarios.papeis_disponiveis') em vez da lista escrita na mão.

O que é o sistema de Mail do Laravel

É a camada responsável por compor e enviar e-mails, com suporte a templates Blade e fila — os mesmos conceitos de View que você já sabe (Aula 3) se aplicam aqui, o corpo do e-mail também é uma view Blade normal.

Configurando o envio em desenvolvimento

.env
MAIL_MAILER=log

Com MAIL_MAILER=log, em vez de tentar enviar de verdade, o Laravel só grava o conteúdo do e-mail no arquivo storage/logs/laravel.log — perfeito pra testar sem precisar configurar um servidor de e-mail de verdade.

Criando as três Mailables

terminal
php artisan make:mail BoasVindasMail
php artisan make:mail NovoUsuarioCriadoMail
php artisan make:mail FelizAniversarioMail
app/Mail/BoasVindasMail.php
namespace App\Mail;

use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;

class BoasVindasMail extends Mailable
{
    use Queueable, SerializesModels;

    public function __construct(public User $usuario) {}

    public function envelope(): Envelope
    {
        return new Envelope(subject: 'Bem-vindo(a) ao Painel de Usuários!');
    }

    public function content(): Content
    {
        return new Content(view: 'emails.boas-vindas');
    }
}
terminal
php artisan make:view emails.boas-vindas
resources/views/emails/boas-vindas.blade.php
<h1>Olá, {{ $usuario->name }}!</h1>
<p>Sua conta foi criada com sucesso no Painel de Usuários.</p>
<p>Seu e-mail de acesso é: {{ $usuario->email }}</p>

Dentro da view, $usuario está disponível automaticamente — o Laravel pega qualquer propriedade pública do construtor da Mailable e entrega pra view, sem você passar manualmente.

As outras duas seguem o mesmo padrão — crie as views delas do mesmo jeito (php artisan make:view emails.novo-usuario-criado e php artisan make:view emails.feliz-aniversario) e preencha com um HTML simples equivalente:

app/Mail/NovoUsuarioCriadoMail.php
class NovoUsuarioCriadoMail extends Mailable
{
    use Queueable, SerializesModels;

    public function __construct(public User $novoUsuario, public User $criador) {}

    public function envelope(): Envelope
    {
        return new Envelope(subject: "Novo usuário cadastrado por {$this->criador->name}");
    }

    public function content(): Content
    {
        return new Content(view: 'emails.novo-usuario-criado');
    }
}
app/Mail/FelizAniversarioMail.php
class FelizAniversarioMail extends Mailable
{
    use Queueable, SerializesModels;

    public function __construct(public User $usuario) {}

    public function envelope(): Envelope
    {
        return new Envelope(subject: 'Feliz Aniversário! 🎉');
    }

    public function content(): Content
    {
        return new Content(view: 'emails.feliz-aniversario');
    }
}

Ligando de volta nos Listeners e no Job

Agora sim dá pra trocar os logger()->info(...) que deixamos temporariamente nas Aulas 14 e 15 pelos e-mails de verdade:

app/Listeners/EnviarBoasVindasEmailListener.php
Mail::to($event->novoUsuario->email)->send(new BoasVindasMail($event->novoUsuario));
app/Jobs/VerificarAniversariantesJob.php
Mail::to($usuario->email)->send(new FelizAniversarioMail($usuario));

Mão na massa

Crie as três Mailables e suas views. Substitua os logger()->info(...) temporários (das Aulas 14 e 15) pelos envios de e-mail de verdade. Cadastre um usuário logado como gerente e confira, em storage/logs/laravel.log, se os dois e-mails (admin + novo usuário) aparecem registrados. Rode o Job de aniversário manualmente de novo e confira o terceiro.

O que são Notifications e por que existem, se já temos Mail

Notification é uma camada mais abstrata que Mail: em vez de "vou mandar um e-mail", você pensa "vou notificar alguém sobre algo" — e escolhe, na mesma classe, por quais canais isso sai: e-mail, banco de dados (o "sino" de notificação que você vê em painéis administrativos), entre outros. Vamos usar essa ferramenta pra dar ao admin, além do e-mail que já criamos na Aula 22, também um aviso visual dentro do próprio sistema.

Criando a Notification

terminal
php artisan make:notification NovoUsuarioNotification
php artisan notifications:table
php artisan migrate

O comando notifications:table cria a migration da tabela onde as notificações de banco de dados ficam guardadas — é esse comando que faz o canal database ter onde salvar.

app/Notifications/NovoUsuarioNotification.php
namespace App\Notifications;

use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
use Illuminate\Notifications\Messages\MailMessage;

class NovoUsuarioNotification extends Notification
{
    use Queueable;

    public function __construct(public User $novoUsuario, public User $criador) {}

    public function via(object $notifiable): array
    {
        return ['database'];
    }

    public function toArray(object $notifiable): array
    {
        return [
            'mensagem' => "{$this->criador->name} cadastrou {$this->novoUsuario->name}",
        ];
    }
}

via() define os canais — aqui só 'database', já que o e-mail nós mantemos vindo do Listener da Aula 15, pra não duplicar o envio. toArray() define o que fica guardado na tabela de notificações.

Disparando junto com o resto

app/Listeners/NotificarAdminListener.php
use App\Notifications\NovoUsuarioNotification;

public function handle(UsuarioCadastrado $event): void
{
    Cache::forget('contagem_usuarios_por_papel');

    if ($event->criador->papel !== 'gerente') {
        return;
    }

    $admins = User::where('papel', 'admin')->get();

    foreach ($admins as $admin) {
        Mail::to($admin->email)->send(new NovoUsuarioCriadoMail($event->novoUsuario, $event->criador));
        $admin->notify(new NovoUsuarioNotification($event->novoUsuario, $event->criador));
    }
}

$admin->notify(...) funciona porque o model User já vem, por padrão, com a trait Notifiable — é ela quem dá esse método notify() pra qualquer usuário.

Mostrando o "sino" na tela

resources/views/layouts/app.blade.php
@auth
    <span>🔔 {{ auth()->user()->unreadNotifications->count() }}</span>
    @foreach (auth()->user()->unreadNotifications as $notificacao)
        <p>{{ $notificacao->data['mensagem'] }}</p>
    @endforeach
@endauth

unreadNotifications é uma propriedade que já vem pronta, também da trait Notifiable — retorna só as notificações que ainda não foram marcadas como lidas.

Mão na massa

Crie a NovoUsuarioNotification e rode a migration do canal database. Adicione o $admin->notify(...) no Listener, junto do e-mail. Adicione o sininho no layout. Cadastre outro usuário como gerente e confira que, ao logar como admin, o número ao lado do 🔔 aumenta.

O que é

O HTTP Client do Laravel permite fazer requisições a APIs externas com uma sintaxe fluente (métodos encadeados, fáceis de ler), sem precisar configurar nada complicado. É exatamente o que falta pro nosso EvolutionApiService, criado vazio lá na Aula 12, mandar mensagem de verdade.

Implementando o envio

app/Services/EvolutionApiService.php
namespace App\Services;

use Illuminate\Support\Facades\Http;

class EvolutionApiService
{
    public function __construct(
        protected string $url,
        protected string $chave,
    ) {}

    public function enviarMensagem(string $numero, string $texto): void
    {
        if (empty($numero)) {
            return; // usuário sem telefone cadastrado — não tem pra onde mandar
        }

        $response = Http::withHeaders(['apikey' => $this->chave])
            ->timeout(10)
            ->retry(2, 300) // tenta mais uma vez se a primeira falhar
            ->post("{$this->url}/message/sendText", [
                'number' => $numero,
                'text' => $texto,
            ]);

        if ($response->failed()) {
            logger()->error('Falha ao enviar WhatsApp', [
                'numero' => $numero,
                'status' => $response->status(),
            ]);
        }
    }
}

empty($numero) é uma função nativa do PHP que verifica se uma variável está vazia (string vazia, null, zero, etc.) — uma proteção simples pra não tentar mandar mensagem pra ninguém. Http::withHeaders([...]) monta os cabeçalhos da requisição (aqui, a chave de autenticação que a evolution-api exige). ->retry(2, 300) tenta de novo até 2 vezes, esperando 300 milissegundos entre tentativas, se a primeira falhar — útil porque APIs externas às vezes engasgam por um instante. Importante: por padrão, o HTTP Client do Laravel não lança um erro automaticamente quando a API responde com falha — por isso checamos $response->failed() manualmente.

Adicionando telefone ao cadastro

Pra isso funcionar de verdade, precisamos de um campo telefone no usuário — vamos adicionar rapidinho, do mesmo jeito que já fizemos na Aula 4:

terminal
php artisan make:migration add_telefone_to_users_table --table=users
migration
Schema::table('users', function (Blueprint $table) {
    $table->string('telefone')->nullable();
});

Não esqueça de adicionar 'telefone' no $fillable do model, no formulário de cadastro, e na regra de validação ('telefone' => 'nullable|string').

Mão na massa

Complete o EvolutionApiService como no exemplo. Adicione a coluna telefone, o campo no formulário e no $fillable. Como você provavelmente não tem uma instância real da evolution-api rodando, use Http::fake(['*' => Http::response(['status' => 'ok'], 200)]) no Tinker antes de testar, pra simular uma resposta de sucesso sem chamar nada de verdade.

O sistema de Storage do Laravel

O Storage é a camada de abstração do Laravel pra ler/gravar arquivos, funcionando igual não importa se o arquivo está salvo localmente ou na nuvem. Vamos usar isso pra deixar cada usuário com uma foto de perfil.

terminal
php artisan storage:link
php artisan make:migration add_foto_to_users_table --table=users
migration
Schema::table('users', function (Blueprint $table) {
    $table->string('foto')->nullable();
});

Ajustando o formulário

resources/views/usuarios/criar.blade.php
<form method="POST" action="/usuarios" enctype="multipart/form-data">
    @csrf
    {{-- ...campos existentes... --}}
    <label>Foto de perfil</label>
    <input type="file" name="foto">
    <button type="submit">Cadastrar</button>
</form>

enctype="multipart/form-data" é obrigatório sempre que um formulário HTML precisa enviar arquivo — sem isso, o navegador nem tenta mandar o conteúdo do arquivo, só o nome dele.

Salvando o arquivo no Controller/Action

app/Actions/CadastrarUsuarioAction.php
public function executar(array $dados, User $criador, ?\Illuminate\Http\UploadedFile $foto = null): User
{
    $dados['password'] = bcrypt($dados['password']);
    $dados['criador_id'] = $criador->id;

    if ($foto) {
        $dados['foto'] = $foto->store('fotos-usuarios', 'public');
    }

    $novoUsuario = User::create($dados);

    UsuarioCadastrado::dispatch($novoUsuario, $criador);

    return $novoUsuario;
}

?\Illuminate\Http\UploadedFile $foto = null declara um parâmetro opcional (o ? antes do tipo e o = null depois do nome dizem "isso pode não vir preenchido"). $foto->store('fotos-usuarios', 'public') gera um nome de arquivo aleatório, salva dentro de storage/app/public/fotos-usuarios/, e devolve o caminho relativo pra guardar no banco.

Controller — passando o arquivo pra Action
$action->executar($validado, Auth::user(), $request->file('foto'));

Exibindo a foto

Blade
@if ($usuario->foto)
    <img src="{{ Storage::url($usuario->foto) }}" width="40">
@endif

Mão na massa

Adicione a coluna foto, o campo de upload no formulário, e ajuste a Action (ou o Controller, se você não fez o exercício da Aula 19) pra salvar o arquivo. Mostre a foto na lista de usuários, quando existir. Cadastre um usuário com foto e confirme que ela aparece.

O que é

DB é o facade (uma forma estática de acessar uma classe do container de serviços) que dá acesso direto à camada de conexão com o banco — a mesma sobre a qual o Eloquent é construído. Usamos quando precisamos de algo que o Eloquent não expressa tão bem, como um agrupamento estatístico.

Quantos usuários nascem em cada mês

exemplo
use Illuminate\Support\Facades\DB;

$porMes = DB::table('users')
    ->selectRaw('MONTH(data_nascimento) as mes, COUNT(*) as total')
    ->groupBy('mes')
    ->orderBy('mes')
    ->get();

selectRaw() permite escrever uma expressão SQL crua quando o Query Builder não tem um método pronto pra ela — aqui, MONTH(data_nascimento) extrai só o número do mês de cada data. groupBy('mes') agrupa as linhas por esse mês, e COUNT(*) conta quantas linhas caem em cada grupo. O resultado é uma lista tipo "mês 3 → 5 pessoas, mês 7 → 2 pessoas".

Transação — protegendo o cadastro em massa

Se um dia você importar vários usuários de uma planilha de uma vez, quer que ou todos sejam criados, ou nenhum — nunca "só metade". Imagina o contrário: a linha 480 de 500 tem um e-mail inválido, a validação estoura, e as 479 anteriores já foram salvas — agora você tem um banco pela metade, e vai ter que descobrir na mão o que já entrou e o que não entrou. O nome técnico pra "tudo ou nada" é atomicidade: ou todas as operações são gravadas de vez (o commit), ou qualquer uma que falhar desfaz tudo, como se nada tivesse acontecido (o rollback) — o mesmo princípio de uma transferência bancária, onde não existe "saiu da minha conta mas não chegou na sua".

A forma com closure — a que você vai usar quase sempre

exemplo
DB::transaction(function () use ($listaDeUsuarios) {
    foreach ($listaDeUsuarios as $dados) {
        User::create($dados);
    }
    // se qualquer create() falhar no meio, TUDO é desfeito automaticamente
});

Repare que não tem try/catch, commit nem rollBack escritos em lugar nenhum — a closure faz esse trabalho sozinha. Se qualquer linha lá dentro lançar uma exception, o Laravel desfaz tudo automaticamente; se rodar até o fim sem erro, ele confirma tudo sozinho também. De bônus, o que a closure devolver vira o retorno do próprio DB::transaction().

A forma manual — pra quando você precisa decidir na mão

Às vezes a closure não dá conta: você precisa de alguma lógica pra decidir quando confirmar ou desfazer, não só "deu erro ou não deu". Aí entra a versão manual, com try/catch de verdade:

exemplo
DB::beginTransaction();

try {
    foreach ($listaDeUsuarios as $dados) {
        User::create($dados);
    }

    DB::commit();
} catch (\Throwable $e) {
    DB::rollBack();

    report($e); // loga o erro pra você investigar depois
    throw $e;   // relança — não engula o problema em silêncio
}

DB::beginTransaction() abre a transação; DB::commit() confirma tudo se chegou até ali sem problema; DB::rollBack(), no catch, desfaz tudo se algo lançou exception. O detalhe que mais gente erra: depois do rollBack(), não é pra simplesmente seguir a vida como se nada tivesse acontecido — report($e) registra o erro nos seus logs (Aula DBG1) pra alguém investigar depois, e throw $e relança a exception, deixando claro pra quem chamou esse código que algo falhou. Engolir o erro silenciosamente esconde o bug, não resolve ele. Na dúvida, prefira a versão com closure — menos código, menos chance de esquecer um rollBack.

A pegadinha: transação é só pra banco de dados

O erro clássico é meter, dentro da transação, coisa que não é operação de banco — achando que também vai ser "desfeita" se der rollback. Lembra do UsuarioCadastrado::dispatch(...) que disparamos dentro do store() (Aula 15)? Se alguém reescrevesse aquele método assim:

exemplo — NÃO faça isso
DB::transaction(function () use ($validado, $criador) {
    $novoUsuario = User::create($validado);

    UsuarioCadastrado::dispatch($novoUsuario, $criador); // ⚠️ perigo
});

...e a transação desse rollback depois do dispatch() (por algum outro erro logo em seguida, por exemplo), o usuário some do banco — mas os Listeners (Aula 15) já podem ter processado e mandado o e-mail e o WhatsApp de boas-vindas pra alguém que, no fim das contas, nunca existiu de verdade no seu sistema. Rollback desfaz linha de banco, não desfaz e-mail já enviado.

A regra de ouro: dentro da transação, só operação de banco. Envio de e-mail, chamada de API externa (evolution-api, Aula 24), disparo de evento — deixa pra depois do commit. Você já está protegido disso de um jeito, sem perceber: os Listeners da Aula 15 implementam ShouldQueue, então eles vão pra fila em vez de rodar na hora — mas pra ter certeza de que só disparam depois que a transação realmente confirmou (evitando um worker processar a fila mais rápido que o commit acontece), vale adicionar public $afterCommit = true; na Listener, uma propriedade que diz ao Laravel "não me processe até o commit sair de fato".

app/Listeners/EnviarBoasVindasEmailListener.php (reforço)
class EnviarBoasVindasEmailListener implements ShouldQueue
{
    public $afterCommit = true;

    // ...resto igual
}

try/catch sozinho, ou dentro de uma transação — qual usar

Depois de ver os dois, fica a pergunta: quando vale um try/catch simples, sem transação nenhuma, e quando precisa mesmo de DB::transaction()? A resposta está em quantas escritas relacionadas existem. Se é uma única escrita, não tem nada pra "desfazer em conjunto" — o try/catch sozinho só protege contra o erro virar uma tela de 500, sem precisar de rollback nenhum, porque não existe uma segunda escrita pra manter sincronizada com a primeira.

É exatamente o caso do destroy() que você construiu na Aula 11 — voltando a ele agora, com esse cuidado a mais:

app/Http/Controllers/UsuarioController.php (revisão do destroy())
public function destroy(User $usuario)
{
    Gate::authorize('excluir-usuario', $usuario);

    try {
        $usuario->delete();
    } catch (\Exception $e) {
        report($e); // registra o erro real no log, mesmo mostrando uma mensagem genérica pro usuário

        return redirect('/usuarios')->with('erro', 'Não foi possível excluir esse usuário.');
    }

    return redirect('/usuarios')->with('sucesso', 'Usuário excluído.');
}

Isso resolve exatamente o cenário da Aula 4: se o usuário já cadastrou outros, a constraint que definimos lá recusa o DELETE, e sem esse try/catch, isso vira uma tela de erro 500. Com ele, a pessoa volta pra lista com uma mensagem explicando que algo não deu certo. catch (\Exception $e) aqui é genérico de propósito — pega qualquer falha, não só a de chave estrangeira — e o report($e) garante que você não perde o motivo real no log (Aula DBG1), mesmo escondendo o detalhe técnico da pessoa usando o sistema. Repare que não tem DB::transaction() nenhum aqui — é uma única operação, não faria sentido envolver ela numa transação.

Pra essa mensagem aparecer, o layout precisa do mesmo tipo de bloco que já mostra 'sucesso' desde a Aula 5:

resources/views/layouts/app.blade.php (trecho a adicionar, logo abaixo do bloco de sucesso)
@if (session('erro'))
    <p style="color: red;">{{ session('erro') }}</p>
@endif

Já quando várias escritas precisam acontecer juntas — o cadastro em massa que vimos mais cedo nessa mesma aula, ou o checkout com pedido, itens e pagamento — só o try/catch sozinho não resolve: ele captura o erro depois que metade das escritas já aconteceu, mas não desfaz nada sozinho. Aí sim entra o DB::transaction(). Resumindo:

SituaçãoFerramenta
Uma escrita só, só quer evitar a tela de 500try/catch simples
Várias escritas relacionadas, precisam ser tudo-ou-nadaDB::transaction() com closure
Várias escritas + precisa de lógica pra decidir quando confirmarDB::beginTransaction() manual, com try/catch

Mão na massa

Adicione a consulta porMes num novo método do Controller (ex: relatorioAniversarios()) e crie uma rota/view simples mostrando o resultado numa lista (ex: "Março: 2 pessoas"). Depois, volte no destroy() do UsuarioController e adicione o try/catch mostrado acima, e o bloco @if (session('erro')) no layout. Teste excluindo um usuário que já cadastrou outro (como a Ana, se ela criou alguém na Aula 6) — confirme que agora aparece a mensagem vermelha em vez de uma tela de erro 500.

O problema que isso resolve

O sino que construímos na Aula 23 só atualiza quando a página recarrega. Se o admin estiver com a tela aberta e um gerente cadastrar alguém em outra aba, o número não muda sozinho. Broadcasting transmite eventos do servidor pro navegador em tempo real, via WebSocket (uma conexão que fica aberta entre navegador e servidor).

Tornando o evento transmissível

terminal
php artisan install:broadcasting
php artisan reverb:start
app/Events/UsuarioCadastrado.php
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;

class UsuarioCadastrado implements ShouldBroadcast
{
    use Dispatchable, SerializesModels;

    public function __construct(public User $novoUsuario, public User $criador) {}

    public function broadcastOn(): array
    {
        return [new PrivateChannel('admins')];
    }

    public function broadcastWith(): array
    {
        return ['mensagem' => "{$this->criador->name} cadastrou {$this->novoUsuario->name}"];
    }
}

Só adicionar implements ShouldBroadcast não muda nada do que já fizemos — o Event continua dando origem aos mesmos três Listeners de sempre. Ele só ganha um destino a mais: o navegador.

routes/channels.php
use Illuminate\Support\Facades\Broadcast;

Broadcast::channel('admins', function ($user) {
    return $user->papel === 'admin';
});

Essa closure decide quem pode "ouvir" o canal — só usuários com papel admin recebem essas transmissões.

Escutando no navegador

resources/js/app.js
Echo.private('admins')
    .listen('UsuarioCadastrado', (e) => {
        alert(e.mensagem); // versão simples — dá pra trocar por algo mais bonito depois
    });

Mão na massa

Adicione ShouldBroadcast ao Event, configure o canal em routes/channels.php, e escute ele no JavaScript. Abra duas abas logadas como admin — cadastre um usuário como gerente numa aba e veja o alerta aparecer sozinho na outra, sem recarregar.

Esse projeto foi pensado pra você dominar bem o "tripé" clássico do desenvolvimento web com Laravel — PHP, Blade/HTML e CSS (com a ajuda do Tailwind pra estilo) — antes de qualquer coisa em JavaScript. Por isso, a lista de usuários continua sendo uma tabela HTML normal, renderizada pelo servidor com @foreach (do jeito que você já sabe desde a Aula 3), só que agora com classes do Tailwind pra ficar com cara de sistema de verdade. Sem biblioteca de JavaScript nenhuma.

O que é Tailwind — um jeito diferente de pensar CSS

Tailwind é um framework de CSS, mas funciona de um jeito bem diferente de coisas como Bootstrap. Em vez de te dar classes já "prontas" com nome de componente (tipo card, btn-primary, navbar — onde uma classe só já resolve um monte de estilo escondido nela), o Tailwind te dá centenas de classes bem pequenas e diretas, cada uma fazendo uma única coisa (utility-first CSS, "CSS orientado a utilitários") — bg-white só põe fundo branco, p-4 só adiciona espaçamento interno, rounded-lg só arredonda os cantos. Você monta o visual combinando várias dessas classes direto na tag HTML.

Isso significa que sua tag vai acumular bastante classe (o que estranha no começo!), mas em troca você tem controle total sem precisar "brigar" com CSS escondido em outro arquivo tentando descobrir de onde veio aquele estilo — está tudo ali, na própria linha do HTML.

Adicionando o Tailwind ao layout

Pra estudar, o jeito mais simples é carregar o Tailwind via CDN (um link que carrega o arquivo direto de um servidor público, sem precisar instalar nada nem configurar build). Esse modo "Play CDN" gera as classes sob demanda, direto no navegador — ótimo pra aprender e prototipar; projetos maiores em produção normalmente compilam o Tailwind com o Vite (a mesma ferramenta que citamos lá na Aula 27), mas isso foge do nosso escopo aqui.

resources/views/layouts/app.blade.php
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>@yield('titulo') — Painel de Usuários</title>
    <script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-50">
    <nav class="bg-gray-900 text-white px-6 py-4 flex justify-between items-center">
        <span class="font-bold text-lg">Painel de Usuários</span>
        @auth
            <span>🔔 {{ auth()->user()->unreadNotifications->count() }}</span>
        @endauth
    </nav>

    <main class="max-w-4xl mx-auto mt-6 px-4">
        @if (session('sucesso'))
            <p style="color: green;">{{ session('sucesso') }}</p>
        @endif

        @if (session('erro'))
            <p style="color: red;">{{ session('erro') }}</p>
        @endif

        @yield('conteudo')
    </main>
</body>
</html>

Esse é o layout completo, reunindo tudo que foi adicionado ao longo da trilha: o sino de notificações (Aula 22) e os blocos de mensagem de sucesso (Aula 5) e erro (Aula 26) — cada um daqueles trechos foi mostrado isoladamente na hora certa, mas é aqui, na reescrita final com Tailwind, que tudo precisa estar junto de verdade. Se você for copiando esse arquivo aula a aula, vale conferir agora se nada ficou de fora.

A partir daqui, qualquer classe do Tailwind que você usar em qualquer view (inclusive nas de aulas anteriores, se quiser voltar e arrumar) já vai funcionar, sem mais nenhuma configuração.

O vocabulário básico — as categorias que você mais vai usar

Diferente do Bootstrap (que tem poucas cores "de tema", tipo primary/secondary), o Tailwind te dá uma escala numérica de 50 até 900 pra cada cor — quanto maior o número, mais escuro o tom (gray-50 é quase branco, gray-900 é quase preto). Essa tabela cobre o essencial pra você já conseguir ler o resto do código dessa aula:

CategoriaExemplosO que faz
Espaçamentop-4, px-4, py-2, mt-6p=padding, m=margin; x=horizontal, y=vertical; o número segue uma escala fixa (4 = 1rem)
Coresbg-gray-900, text-whitebg=fundo, text=cor do texto, border=cor da borda
Tipografiatext-sm, text-lg, font-boldTamanho e peso (grossura) da fonte
Layoutflex, w-full, max-w-4xl, mx-autoComo os elementos se organizam e ocupam espaço na tela (mx-auto centraliza horizontalmente)
Bordas e cantosrounded-lg, rounded-full, divide-yCantos arredondados e linhas divisórias automáticas entre elementos filhos
Estadoshover:bg-gray-100, even:bg-gray-50A classe depois dos dois-pontos só se aplica quando aquela condição acontece (mouse em cima, item de posição par, campo focado)

A tabela, sem nenhuma linha de JavaScript

Trocamos a lista <ul>/<li> da Aula 3 por uma <table> de verdade. A regra de "usuário comum só vê nome e nascimento" continua sendo resolvida do mesmo jeito que já vimos na Aula 9 — com @can, checando o Gate direto no Blade, sem precisar de Controller nem endpoint novo:

resources/views/usuarios/index.blade.php
@extends('layouts.app')

@section('titulo', 'Usuários')

@section('conteudo')
    <h1 class="text-2xl font-bold text-gray-800 mb-6">Usuários cadastrados</h1>

    <table class="w-full border-collapse bg-white shadow-sm rounded-lg overflow-hidden">
        <thead class="bg-gray-100 text-left text-sm font-semibold text-gray-600">
            <tr>
                <th class="px-4 py-3">Nome</th>
                <th class="px-4 py-3">Nascimento</th>
                @can('cadastrar-usuarios')
                    <th class="px-4 py-3">E-mail</th>
                    <th class="px-4 py-3">Papel</th>
                @endcan
            </tr>
        </thead>
        <tbody class="divide-y divide-gray-100">
            @foreach ($usuarios as $usuario)
                <tr class="even:bg-gray-50 hover:bg-gray-100">
                    <td class="px-4 py-3">{{ $usuario->name }}</td>
                    <td class="px-4 py-3">{{ $usuario->data_nascimento }}</td>
                    @can('cadastrar-usuarios')
                        <td class="px-4 py-3">{{ $usuario->email }}</td>
                        <td class="px-4 py-3">
                            <span class="inline-block bg-indigo-100 text-indigo-700 text-xs font-medium px-2.5 py-1 rounded-full">
                                {{ $usuario->papel }}
                            </span>
                        </td>
                    @endcan
                </tr>
            @endforeach
        </tbody>
    </table>
@endsection

Repare que, diferente do Bootstrap (que te dá table-striped e table-hover já prontos), no Tailwind você monta esses efeitos você mesmo, combinando utilitários: divide-y divide-gray-100 desenha uma linha fina entre cada <tr> automaticamente (sem precisar bordar célula por célula), even:bg-gray-50 pinta só as linhas de posição par (o efeito "zebrado"), e hover:bg-gray-100 destaca a linha quando o mouse passa em cima. O "selinho" colorido do papel também é construído do zero: rounded-full deixa os cantos totalmente arredondados (formato de pílula), e o par bg-indigo-100/text-indigo-700 usa um tom bem claro de fundo com um tom bem mais escuro da mesma cor no texto, pra manter contraste e legibilidade.

Repare também que quem decide se a coluna de e-mail/papel aparece continua sendo o @can, avaliado no servidor antes do HTML ser enviado — um usuário comum nem recebe esse HTML no navegador, diferente de "esconder com CSS", que poderia ser burlado inspecionando a página.

Por que não usamos JavaScript aquiDá pra construir sistemas administrativos inteiros e funcionais só com Blade renderizando HTML no servidor — é o jeito mais simples de manter, e é exatamente esse o foco desse projeto: Laravel, PHP, HTML e CSS bem dominados primeiro. JavaScript entra depois, quando você sentir uma necessidade real de interatividade (uma tela que atualiza sem recarregar, por exemplo) — e não porque "é assim que se faz".

Mão na massa

Adicione o script do Tailwind no layout. Troque a lista <ul> da view usuarios/index.blade.php pela tabela acima. Teste logado como comum (deve ver só nome e nascimento) e depois como admin (deve ver todas as colunas). Se quiser, aproveite e estilize o formulário de cadastro (Aula 3/5) com Tailwind — nos inputs, algo como class="border border-gray-300 rounded px-3 py-2 w-full", e no botão, class="bg-indigo-600 text-white px-4 py-2 rounded hover:bg-indigo-700" — não muda nenhuma lógica, só o visual.

Essa aula é só uma referência, pra você conhecer. Nada aqui faz parte do painel de usuários que você construiu — seu projeto continua 100% com formulários tradicionais (aqueles que recarregam a página inteira no POST, como fizemos desde a Aula 5). Mas é bem comum, em outros projetos Laravel, ver formulários sendo enviados via AJAX (uma requisição feita pelo JavaScript, em segundo plano, sem recarregar a página) — vale saber como isso se pareceria.

O token CSRF, agora vindo por um header

Lembra do @csrf que todo formulário Blade usa (Aula 5)? Uma requisição AJAX não passa por um <form> de verdade, então esse token precisa ser mandado manualmente, no cabeçalho da requisição:

layout — meta tag
<meta name="csrf-token" content="{{ csrf_token() }}">
exemplo de referência
$.ajaxSetup({
    headers: { 'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content') }
});

Enviando o formulário sem recarregar a página

exemplo de referência
$('#form-cadastro').on('submit', function (e) {
    e.preventDefault(); // impede o comportamento padrão (recarregar a página)

    $.ajax({
        url: '/usuarios',
        method: 'POST',
        data: $(this).serialize(), // empacota todos os campos do form automaticamente
        success: function (resposta) {
            alert('Usuário cadastrado com sucesso!');
        },
        error: function (xhr) {
            if (xhr.status === 422) {
                console.log(xhr.responseJSON.errors); // erros de validação, um por campo
            }
        }
    });
});

.serialize() pega todos os campos do <form> e monta o corpo da requisição sozinho. Pra esse exemplo funcionar de ponta a ponta, o método store() do Controller também precisaria devolver uma resposta em JSON (response()->json([...])) em vez do redirect() que usamos no projeto real — é mais uma peça que muda quando se troca de abordagem, e é justamente por isso que decidimos não misturar as duas no mesmo projeto.

Debug e Diagnóstico

Aulas DBG1–DBG2 · conteúdo novo

dd() — "dump and die"

Antes de qualquer ferramenta sofisticada, a primeira pergunta que você faz quando algo não funciona é sempre a mesma: "o que exatamente tem dentro dessa variável, nesse ponto do código?" dd() (abreviação de dump and die, "despeja e morre") resolve isso — imprime o conteúdo de uma variável de forma legível, formatada, e interrompe a execução ali mesmo, sem rodar mais nada depois.

app/Http/Controllers/UsuarioController.php
public function store(Request $request)
{
    $validado = $request->validate([ /* ... */ ]);

    dd($validado); // a execução para bem aqui — o resto do método nem roda

    $novoUsuario = $action->executar($validado, Auth::user());
    return redirect('/usuarios');
}

Acessando a rota no navegador, em vez da tela normal, você vê uma página formatada mostrando exatamente o array $validado — cada chave, cada valor, com cores e identação. Muito mais legível que a saída crua do var_dump() nativo do PHP (o dd() usa por baixo dos panos o componente VarDumper, do Symfony, que o Laravel já traz pronto).

dd() aceita quantas variáveis você quiser de uma vez, e funciona com qualquer coisa — array, model do Eloquent, Collection, string, objeto:

exemplo
dd($usuario, $usuario->papel, $request->all());

dump() — a versão que não interrompe nada

Às vezes você quer só espiar um valor no meio do caminho, sem parar o resto da execução — por exemplo, dentro de um foreach, pra ver o que acontece em cada volta, sem que a primeira já pare tudo. É pra isso que serve dump(): imprime a variável, exatamente com a mesma formatação bonita do dd(), mas deixa o código continuar rodando depois.

exemplo
foreach ($usuarios as $usuario) {
    dump($usuario->name, $usuario->idade); // imprime e segue pro próximo do loop
}

Dentro de uma view Blade, existe o equivalente @dump($variavel), útil quando você quer conferir o que está chegando na tela sem sair do arquivo .blade.php.

logger() e o arquivo de log

Nem todo problema você quer ver "ao vivo" na tela — às vezes o comportamento só acontece em produção, ou dentro de um Job rodando em fila (onde não tem tela nenhuma pra mostrar um dd()). Pra esses casos, você já usou logger()->info(...) algumas vezes ao longo da trilha, como placeholder temporário nos Listeners (Aula 15) — chegou a hora de entender essa ferramenta de verdade.

exemplo
logger()->info('Usuário cadastrado', ['id' => $novoUsuario->id, 'papel' => $novoUsuario->papel]);
logger()->warning('Estoque baixo, atenção');
logger()->error('Falha ao chamar a evolution-api', ['numero' => $numero]);

logger() é um atalho pra Log:: facade — os dois fazem a mesma coisa. O segundo parâmetro (opcional) é um array de "contexto": dados extras que ficam anexados àquela linha do log, sem precisar concatenar tudo numa string só. Cada chamada vai parar em storage/logs/laravel.log, um arquivo de texto simples que cresce a cada entrada.

Os níveis (info, warning, error, entre outros: debug, notice, critical, alert, emergency) indicam a gravidade — não mudam onde a mensagem vai parar, mas ajudam a filtrar depois (a maioria das ferramentas de monitoramento de log deixa você ver só os error pra cima, por exemplo, ignorando o ruído de info).

terminal — acompanhando o log em tempo real
tail -f storage/logs/laravel.log

tail -f é um comando do Linux/Mac que mostra as últimas linhas de um arquivo e continua "grudado" nele, imprimindo linhas novas assim que aparecem — deixe esse comando rodando num terminal enquanto testa o sistema no navegador, numa outra janela, e você vê cada log surgir na hora.

dd()/dump() são só pra desenvolvimentoNunca deixe um dd() esquecido em código que vai pra produção — ele trava a requisição inteira ali, então qualquer usuário real que passar por aquele trecho vê a tela de debug em vez do sistema funcionando. É fácil esquecer um depois de uma sessão de investigação — vale o hábito de dar um Ctrl+F por "dd(" antes de cada commit.

Mão na massa

No método store() do UsuarioController, adicione temporariamente dd($validado) logo depois da validação, e confirme que a página para ali, mostrando o array formatado. Remova o dd() e troque por logger()->info('Usuário cadastrado', ['email' => $validado['email']]);. Abra um terminal com tail -f storage/logs/laravel.log rodando, cadastre um usuário pelo formulário, e veja a linha aparecer ao vivo.

Vendo cada query que o Eloquent dispara

Às vezes o problema não é "o que tem numa variável", é "quantas consultas o Eloquent está fazendo no banco, e quais são elas". O facade DB (Aula 26) tem duas ferramentas pra isso.

Registrando tudo, de forma global — útil pra deixar ligado durante uma sessão inteira de investigação:

app/Providers/AppServiceProvider.php
use Illuminate\Support\Facades\DB;

public function boot(): void
{
    DB::listen(function ($query) {
        logger()->info($query->sql, [
            'bindings' => $query->bindings,
            'tempo_ms' => $query->time,
        ]);
    });
}

DB::listen() registra uma função que roda a cada query executada, em qualquer lugar do sistema — cada uma vai parar no storage/logs/laravel.log, junto com o tempo que levou. Deixe isso só em ambiente local (nunca em produção — gera log demais e deixa tudo mais lento).

Inspecionando um trecho pontual — quando você já sabe onde olhar:

exemplo
DB::enableQueryLog();

$usuarios = User::with('criador')->get();

dd(DB::getQueryLog()); // mostra as queries rodadas até aqui, com tempo, e já para a execução

Detectando N+1 automaticamente

Lembra do problema N+1 da Aula 6 — uma query pra buscar os usuários, mais uma pra cada relacionamento acessado sem eager loading? O Eloquent tem um interruptor que transforma esse problema num erro imediato em desenvolvimento, em vez de deixar passar silenciosamente até virar lentidão em produção:

app/Providers/AppServiceProvider.php
use Illuminate\Database\Eloquent\Model;

public function boot(): void
{
    Model::preventLazyLoading(! $this->app->isProduction());
}

preventLazyLoading() faz o Eloquent lançar uma exceção (LazyLoadingViolationException) assim que algum código acessar um relacionamento sem ele ter sido carregado antes via with() — em vez de simplesmente disparar a query extra calada, como faria por padrão. ! $this->app->isProduction() garante que essa trava só fica ativa fora de produção — você quer descobrir o problema enquanto desenvolve, não quebrar o sistema pros seus usuários reais se algum caso passar despercebido.

Laravel Telescope — um painel pra tudo isso de uma vez

Ao invés de espalhar dd() e conferir o arquivo de log manualmente, o Telescope (pacote oficial, mantido pelo próprio time do Laravel) dá um painel visual completo: toda requisição, toda query com seu tempo, todo Job disparado, todo e-mail enviado, toda exceção — tudo navegável pelo navegador.

terminal
composer require laravel/telescope --dev
php artisan telescope:install
php artisan migrate

--dev marca o pacote como dependência só de desenvolvimento (lembra do --no-dev na Aula D1? é exatamente esse mecanismo — o Telescope nunca deveria ir pra produção). telescope:install publica os arquivos de configuração e assets; migrate cria as tabelas onde ele guarda o que captura. Depois disso, acesse /telescope no navegador — cada aba (Requests, Queries, Jobs, Mail, Exceptions...) mostra exatamente o que aconteceu, em ordem cronológica, sem você precisar instrumentar nada manualmente no seu próprio código.

Existe também o Laravel Debugbar (pacote de terceiros, não oficial, mas extremamente popular), que mostra uma barra fixa no rodapé de cada página com um resumo rápido — queries, tempo de resposta, memória usada. É mais leve que o Telescope pra uma checada rápida; o Telescope é melhor quando você precisa investigar algo que aconteceu em background (um Job, um e-mail), fora do ciclo normal de tela.

A tela de erro em desenvolvimento também é uma ferramenta

Lembra do aviso na Aula D1 sobre nunca deixar APP_DEBUG=true em produção? Em desenvolvimento, é exatamente o oposto — deixe sempre ligado. Com ele ativo, qualquer exceção não tratada mostra uma página detalhada (o Laravel usa o pacote Ignition pra isso) com o trecho de código exato onde quebrou, a pilha de chamadas completa, e os dados da requisição — geralmente é a primeira coisa que aparece quando algo dá errado, antes mesmo de você precisar sair caçando com dd().

Mão na massa

Adicione o DB::listen() no AppServiceProvider e acesse /usuarios — confira no log quantas queries rodaram. Ative o Model::preventLazyLoading() e, em algum ponto do código, force um acesso a um relacionamento sem eager loading (ex: tire o with('criador') do index() por um instante) — confirme que aparece o erro em vez da query silenciosa. Instale o Telescope, acesse /telescope, cadastre um usuário pelo formulário, e explore as abas Requests e Queries pra ver tudo que aconteceu por trás daquele clique.

Validação Avançada

Aulas VAL1–VAL2 · conteúdo novo

Desde a Aula 5 você usa regras como required, email e unique — mas o Laravel tem bem mais de 90 regras prontas. Não vale a pena decorar todas; essa aula é uma referência pra consultar quando precisar de algo que ainda não apareceu na trilha. A lista completa e sempre atualizada mora na documentação oficial (laravel.com/docs/validation); aqui vai a fatia que resolve a grande maioria dos casos do dia a dia.

Presença e obrigatoriedade

RegraO que faz
requiredObriga o campo a vir preenchido (visto na Aula 5)
nullablePermite o campo vir vazio, sem falhar por isso
sometimesSó aplica o resto das regras se o campo vier na requisição — se ele nem existir, ignora
required_if:outro_campo,valorSó obrigatório se outro campo tiver aquele valor específico
required_unless:outro_campo,valorObrigatório, exceto quando outro campo tiver aquele valor
required_with:outro_campoObrigatório só se outro campo também estiver presente
prohibitedO campo não pode vir preenchido de jeito nenhum

Tipo de dado

RegraO que faz
stringPrecisa ser texto
integerPrecisa ser um número inteiro
numericPrecisa ser numérico (aceita decimal)
booleanAceita true/false/1/0
arrayPrecisa ser um array
datePrecisa ser uma data válida (visto na Aula 5)
emailFormato de e-mail válido (Aula 5)
urlFormato de URL válida
uuidFormato de UUID válido
jsonPrecisa ser um JSON válido

Tamanho e quantidade

RegraO que faz
min:6Tamanho mínimo (Aula 5) — pra número, é o valor mínimo, não o tamanho da string
max:255Tamanho/valor máximo (Aula 5)
size:14Tamanho exato — nem mais, nem menos
between:1,100Precisa estar entre dois valores/tamanhos
digits:14Precisa ter exatamente esse número de dígitos

Comparando com outro campo

RegraO que faz
confirmedExige um campo _confirmation junto (ex: password + password_confirmation) e verifica se são iguais
same:outro_campoPrecisa ser idêntico ao valor de outro campo
different:outro_campoPrecisa ser diferente do valor de outro campo
gt:outro_campoPrecisa ser maior que o valor de outro campo (também tem gte, lt, lte)

Formato específico

RegraO que faz
alphaSó letras
alpha_numSó letras e números
alpha_dashLetras, números, hífen e underline
regex:/padrão/Precisa bater com uma expressão regular customizada
date_format:Y-m-dData num formato específico
ipFormato de endereço IP válido

Banco de dados e listas

RegraO que faz
unique:users,emailNão pode já existir na tabela/coluna (Aula 5)
exists:categorias,idO valor precisa existir de verdade naquela tabela/coluna — o oposto do unique
in:admin,gerente,comumSó aceita esses valores exatos (Aula 5)
not_in:valor1,valor2Rejeita esses valores específicos

Arquivos (já vistas na Aula 25)

RegraO que faz
filePrecisa ser um upload de arquivo válido
imagePrecisa ser uma imagem (jpg, png, etc.)
mimes:pdf,docxSó aceita esses tipos de arquivo pela extensão/conteúdo real

Todas essas regras podem ser combinadas livremente, com | ou em array — exatamente como você já faz desde a Aula 5. Mas nem tudo cabe numa regra pronta: o próximo passo é aprender a criar a sua própria.

Quando as regras prontas não bastam

Nenhuma das regras da Aula VAL1 sabe validar um CNPJ de verdade — não é só conferir tamanho ou formato, é calcular dois dígitos verificadores a partir de um algoritmo específico. Esse é exatamente o tipo de situação onde vale criar sua própria regra, reutilizável em qualquer formulário do sistema.

Vamos usar um exemplo bem atual: desde julho de 2026, a Receita Federal passou a emitir CNPJs alfanuméricos (misturando letras e números nas 12 primeiras posições, mantendo só os 2 dígitos verificadores finais numéricos) — uma mudança real (Instrução Normativa RFB nº 2.229/2024), pensada pra não esgotar a quantidade de CNPJs possíveis só com números. Qualquer sistema que valide CNPJ precisou se adaptar.

Opção rápida: uma closure inline

Pra uma validação simples e usada uma vez só, dá pra passar uma função direto no array de regras, sem criar arquivo nenhum:

exemplo
'codigo' => [
    'required',
    function (string $attribute, mixed $value, \Closure $fail) {
        if (strtoupper($value) !== $value) {
            $fail('O :attribute precisa estar em maiúsculas.');
        }
    },
],

$fail(...) é como você avisa que a validação falhou — chamar ela marca o campo como inválido e usa o texto passado como mensagem de erro (:attribute é substituído automaticamente pelo nome do campo). Se $fail nunca for chamado, a validação passa.

Isso funciona, mas fica difícil de reaproveitar em outro formulário, e complicado de testar isoladamente. Pra uma regra de verdade — como validar CNPJ, algo que provavelmente vai aparecer em vários formulários do sistema — vale criar uma classe própria.

Gerando a classe da regra

terminal
php artisan make:rule Cnpj

Isso cria app/Rules/Cnpj.php, já implementando a interface ValidationRule — a forma atual do Laravel de definir regras customizadas (versões mais antigas usavam uma interface diferente, chamada só Rule, com métodos separados passes()/message(); isso está obsoleto, então se você encontrar exemplos assim por aí, são de um Laravel mais antigo).

O algoritmo do CNPJ alfanumérico

Cada caractere (dígito ou letra) vira um número através do código ASCII menos 48 — os dígitos 0-9 continuam valendo 0-9 (compatível com o CNPJ antigo, só numérico), e as letras A-Z passam a valer de 17 a 42. Os dois dígitos verificadores continuam sendo calculados por módulo 11, com pesos fixos — o mesmo princípio do CNPJ tradicional, só que agora alimentado por esses valores alfanuméricos.

app/Rules/Cnpj.php
namespace App\Rules;

use Closure;
use Illuminate\Contracts\Validation\ValidationRule;

class Cnpj implements ValidationRule
{
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        $cnpj = strtoupper(preg_replace('/[^A-Z0-9]/i', '', $value));

        if (! preg_match('/^[A-Z0-9]{12}[0-9]{2}$/', $cnpj)) {
            $fail('O :attribute tem um formato inválido.');
            return;
        }

        if (! $this->digitosValidos($cnpj)) {
            $fail('O :attribute informado não é um CNPJ válido.');
        }
    }

    private function digitosValidos(string $cnpj): bool
    {
        $pesosPrimeiroDigito = [5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2];
        $pesosSegundoDigito = [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2];

        $base = substr($cnpj, 0, 12);
        $primeiroDigito = $this->calcularDigito($base, $pesosPrimeiroDigito);
        $segundoDigito = $this->calcularDigito($base . $primeiroDigito, $pesosSegundoDigito);

        return $cnpj === $base . $primeiroDigito . $segundoDigito;
    }

    private function calcularDigito(string $base, array $pesos): int
    {
        $soma = 0;

        foreach (str_split($base) as $indice => $caractere) {
            $valor = ord($caractere) - 48; // dígitos: 0-9 | letras A-Z: 17-42
            $soma += $valor * $pesos[$indice];
        }

        $resto = $soma % 11;

        return $resto < 2 ? 0 : 11 - $resto;
    }
}

Vamos por partes: preg_replace('/[^A-Z0-9]/i', '', $value) remove qualquer máscara (pontos, barra, hífen) que o usuário tenha digitado, deixando só os 14 caracteres que importam. O primeiro preg_match confere o formato básico — 12 caracteres alfanuméricos seguidos de 2 dígitos numéricos — antes de gastar processamento calculando dígito verificador de algo obviamente errado. digitosValidos() calcula os dois dígitos esperados e compara com os que realmente vieram no valor. ord($caractere) - 48 é a conversão ASCII descrita acima — repare que essa única linha já resolve tanto dígito quanto letra, sem precisar de um if separado pra cada caso. $resto < 2 ? 0 : 11 - $resto é a regra especial do módulo 11: se o resto da divisão for 0 ou 1, o dígito verificador é 0; caso contrário, é 11 menos o resto.

Usando a regra

exemplo
use App\Rules\Cnpj;

$request->validate([
    'cnpj_empresa' => ['required', new Cnpj],
]);

new Cnpj dentro do array de regras funciona exatamente como 'required' ou 'email' — só que agora é a sua própria classe controlando a lógica, testável isoladamente e reaproveitável em quantos formulários precisar, sem copiar e colar esse cálculo em cada um.

CNPJs antigos continuam válidosA Receita Federal deixou claro que CNPJs já existentes (só numéricos) continuam válidos indefinidamente — os dois formatos convivem. Repare que o algoritmo acima já cobre isso automaticamente: como ord('0') - 48 = 0 até ord('9') - 48 = 9, um CNPJ 100% numérico passa pelo mesmo cálculo e dá o resultado certo, sem precisar de nenhum caminho especial pro formato antigo.

Mão na massa

Esse exemplo é standalone — não faz parte do Painel de Usuários (que não tem campo de CNPJ), é só pra você ter a técnica pronta pro dia que precisar. Crie a regra Cnpj como no exemplo. Pelo Tinker, teste ela isolada: app(Illuminate\Validation\Factory::class)->make(['x' => '12ABC34501DE35'], ['x' => [new App\Rules\Cnpj]])->fails() — ajuste o valor de teste e confirme que CNPJs com dígito verificador errado falham, e um com o cálculo certo passa.

Deploy e Produção

Aula D1 · conteúdo novo

Por que "só dar git clone" não é suficiente

Se você tentar rodar o projeto logo depois de um git clone, ele não vai funcionar — e isso é esperado, não um erro seu. O motivo é o .gitignore: um arquivo que já vem configurado em todo projeto Laravel, listando pastas e arquivos que nunca devem ir pro controle de versão (Git). Repare no dele:

.gitignore (trecho relevante)
/vendor
/node_modules
.env
/public/storage
/storage/*.log
/storage/framework/cache/*
/storage/framework/sessions/*
/storage/framework/views/*

Cada linha tem um motivo: /vendor (as bibliotecas PHP instaladas pelo Composer) e /node_modules não precisam ir pro Git porque são reconstruíveis — dá pra recriar exatamente as mesmas versões a partir de dois arquivos que são versionados: composer.json/composer.lock. .env guarda senhas de banco, chaves de API, tokens — coisas que, se fossem parar num repositório Git (principalmente um público), estariam expostas pra qualquer um que visse o histórico. Os arquivos dentro de storage/ são gerados em tempo de execução (logs, cache, sessões) — não fazem sentido "congelados" no controle de versão.

Isso significa que, toda vez que o projeto for parar numa máquina nova (a sua, um colega, ou o servidor de produção), essas peças precisam ser reconstruídas manualmente. É exatamente esse processo que essa aula cobre, passo a passo.

Passo 1 — clonar e instalar as dependências PHP

terminal
git clone https://github.com/sua-conta/painel-usuarios.git
cd painel-usuarios
composer install --optimize-autoloader --no-dev

composer install lê o composer.lock (que trava as versões exatas de cada dependência, garantindo que todo mundo instale exatamente a mesma coisa) e recria a pasta vendor/ do zero. --no-dev pula pacotes que só servem pra desenvolvimento (ferramentas de debug, por exemplo) — use essa flag em produção; num ambiente de desenvolvimento novo, rode só composer install sem ela. --optimize-autoloader monta um mapa de classes mais eficiente, deixando o carregamento um pouco mais rápido — vale a pena em produção.

Painel de hospedagem tipo aaPanel? Pode precisar do caminho completo do PHPSe você instalar o projeto num servidor gerenciado por um painel — aaPanel, cPanel, Plesk — é comum ele manter várias versões de PHP instaladas lado a lado, sem deixar o comando php do terminal apontando pra versão certa (às vezes nem aponta pra nenhuma). Rodar php artisan migrate nesse cenário pode usar uma versão diferente da que seu projeto precisa, ou simplesmente devolver "command not found". A solução é chamar o binário do PHP pelo caminho completo, em vez de confiar no php genérico. No aaPanel, o padrão é /www/server/php/{versão}/bin/php — o número da versão sem o ponto (83 = PHP 8.3, 82 = PHP 8.2). Então, em vez de php artisan migrate, você roda:
terminal (aaPanel ou similar)
/www/server/php/83/bin/php artisan migrate
Isso vale pra qualquer comando php artisan mostrado daqui em diante nessa aula — troque php pelo caminho completo sempre que estiver num painel assim. Pra descobrir o caminho certo no seu, veja a seção de configuração de PHP dele (no aaPanel, "Software" → "PHP" já mostra onde cada versão fica instalada), ou rode which php no terminal do servidor pra conferir se já existe algum atalho configurado.
"detected dubious ownership in repository" — outro clássico de painelSe ao rodar um comando git no servidor (um git pull, por exemplo) aparecer um erro citando dubious ownership, é uma proteção do próprio Git: ele recusa operar numa pasta cujo dono não é o mesmo usuário rodando o comando. É comum em painéis, onde os arquivos do projeto pertencem ao usuário do servidor web (www-data, ou um usuário específico do painel) mas você está rodando git como root ou outro usuário via SSH. A solução é marcar a pasta como seguros explicitamente, uma única vez por servidor:
terminal (no servidor)
git config --global --add safe.directory /www/wwwroot/mapa.beepsttore.com
Troque o caminho pelo caminho real do seu projeto — depois disso, o Git para de reclamar dessa pasta especificamente.

Passo 2 — recriando o .env

Como o .env não veio no clone (está no .gitignore), você precisa criar o seu próprio, a partir de um arquivo de exemplo que esse sim é versionado — o .env.example, que lista todas as variáveis que existem, sem os valores reais/secretos:

terminal
cp .env.example .env

Agora edite o .env recém-criado com os valores reais desse ambiente: credenciais do banco de dados (DB_DATABASE, DB_USERNAME, DB_PASSWORD), APP_URL, as chaves da evolution-api (Aula 13), configuração de e-mail (Aula 22) — tudo que você já viu na Aula 21 sobre config personalizada.

APP_ENV e APP_DEBUG — atenção redobrada em produçãoDuas variáveis do .env merecem cuidado especial: APP_ENV=production (identifica o ambiente) e, principalmente, APP_DEBUG=false. Com APP_DEBUG=true, qualquer erro no sistema mostra pro visitante uma página cheia de detalhes internos — caminho de arquivos do servidor, trechos de código, variáveis de ambiente, às vezes até credenciais de banco. Isso é ótimo pra você depurando localmente, e um risco de segurança real se ficar ligado em produção. Nunca esqueça de conferir essa linha antes de publicar.

Passo 3 — gerando a chave da aplicação

terminal
php artisan key:generate

APP_KEY é usada pelo Laravel pra criptografar dados sensíveis — sessões de usuário, cookies, e qualquer coisa que você criptografar manualmente via Crypt::encrypt(). Cada ambiente (seu computador, o servidor de produção) deve ter a sua própria chave, gerada nesse passo — nunca copie a chave de um ambiente pro outro, e nunca a exponha num repositório público.

Um detalhe importante: rode esse comando só na primeira vez que o ambiente é configurado. Se você rodar de novo depois que o sistema já estiver em uso, com sessões ativas ou dados criptografados salvos com a chave antiga, esses dados ficam ilegíveis — a chave nova não consegue descriptografar o que foi criptografado com a antiga.

Passo 4 — banco de dados

terminal
php artisan migrate --force

Normalmente, quando o Laravel detecta APP_ENV=production, o comando migrate pergunta uma confirmação antes de rodar (é uma operação que pode alterar dados de verdade, então o framework é cauteloso por padrão). A flag --force pula essa pergunta — necessária em scripts de deploy automatizados, que não têm ninguém ali pra digitar "sim".

Se for a primeira vez que esse banco existe (produção nova, do zero), você também quer o AdminSeeder da Aula 20 rodando junto:

terminal (só na primeira vez)
php artisan migrate --force --seed

Passo 5 — o link de storage e as permissões de arquivo

terminal
php artisan storage:link

Esse é o mesmo comando da Aula 25 — como ele cria um link simbólico (um "atalho" no sistema de arquivos), e links simbólicos não são replicados pelo Git, precisa ser refeito em cada ambiente novo.

Em servidores Linux (o caso mais comum com Apache), o usuário que roda o servidor web (geralmente chamado www-data em distribuições baseadas em Debian/Ubuntu) precisa ter permissão de escrita em duas pastas específicas — sem isso, você vai ver erros de "permission denied" ao tentar gravar logs, cache, ou arquivos enviados por upload:

terminal (no servidor)
chmod -R 775 storage bootstrap/cache
chown -R www-data:www-data storage bootstrap/cache

Passo 6 — cache de otimização (só em produção)

Antes de gerar cache novo, vale limpar qualquer cache antigo que possa ter sobrado de um deploy anterior — evita ficar "cache em cima de cache" com valores desatualizados:

terminal
php artisan optimize:clear

optimize:clear limpa de uma vez todos os caches que o Laravel mantém — config, rotas, views, e mais alguns que ainda não vimos. É o oposto do que vamos rodar a seguir, e um bom primeiro passo sempre que algo "não está atualizando" depois de um deploy, mesmo fora desse checklist.

Esses três comandos "congelam" partes do sistema num formato mais rápido de carregar — vale a pena em produção, onde performance importa; em desenvolvimento local, geralmente é melhor não usar, porque eles atrapalham a atualização automática quando você edita um arquivo:

terminal
php artisan config:cache
php artisan route:cache
php artisan view:cache

config:cache junta todos os arquivos de config/ (incluindo os valores já lidos do .env, lembra da Aula 21?) num único arquivo compilado. route:cache faz o mesmo com as rotas. view:cache pré-compila os arquivos Blade pra HTML/PHP puro, evitando recompilar a cada requisição.

A pegadinha do config:cacheDepois de rodar config:cache, o Laravel para de ler o .env diretamente — ele passa a usar só o que foi congelado no cache. Se você editar o .env depois disso (pra trocar uma senha, por exemplo) e a mudança não parecer ter efeito nenhum, é isso: rode php artisan config:clear (ou config:cache de novo) pra recriar o cache com os valores atualizados.

Como o Tailwind desse projeto vem via CDN (Aula 28, um <script> direto no layout), não existe nenhum passo de build de JavaScript aqui — o projeto é 100% PHP/Blade servido direto pelo Apache, sem npm run build nenhum.

Passo 7 — mantendo os Listeners em fila de pé (Supervisor)

Falta uma peça que os passos anteriores não cobrem: os Listeners com ShouldQueue (Aula 15) — os que mandam e-mail e WhatsApp de boas-vindas — não rodam sozinhos quando algo cai na fila. Eles ficam esperando até um processo php artisan queue:work vir processá-los. Rodar esse comando manualmente uma vez não é suficiente pra produção: se ele cair (reinício do servidor, erro de memória, deploy novo), ninguém volta a processar a fila até alguém perceber e religar na mão.

É pra isso que serve o Supervisor: um gerenciador de processos do Linux que mantém o queue:work de pé, reiniciando ele sozinho sempre que cair. Repare que isso não é a mesma coisa que o crontab da Aula 14 — o crontab aciona o agendador (schedule:run, uma vez por minuto); o Supervisor mantém o worker da fila rodando continuamente. São duas peças diferentes, cuidando de duas responsabilidades diferentes — a maioria dos projetos com fila usa as duas juntas, não uma no lugar da outra.

terminal (no servidor)
sudo apt install supervisor
/etc/supervisor/conf.d/painel-usuarios-worker.conf
[program:painel-usuarios-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /caminho/do/projeto/artisan queue:work --sleep=3 --tries=3
autostart=true
autorestart=true
numprocs=1
user=www-data
stdout_logfile=/caminho/do/projeto/storage/logs/worker.log

command é o processo que o Supervisor mantém vivo — o mesmo queue:work que você rodaria manualmente, só que agora vigiado. autorestart=true é o coração disso tudo: se o processo morrer por qualquer motivo, o Supervisor sobe outro na hora. --tries=3 é do próprio Laravel, não do Supervisor — quantas vezes tentar reprocessar um job que falhou antes de desistir dele. numprocs=1 controla quantas cópias rodam em paralelo; mais de uma processa a fila mais rápido, à custa de mais memória — pra um projeto desse tamanho, uma já basta.

terminal
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start painel-usuarios-worker:*

reread + update fazem o Supervisor descobrir essa configuração nova; start liga o processo pela primeira vez. Dali em diante, ele fica de pé sozinho, sobrevivendo a queda de processo e (se o Supervisor estiver configurado pra iniciar no boot, o padrão na maioria das instalações) até a reinícios do servidor inteiro.

Trocou o código do Listener? Reinicie o workerO queue:work carrega o código da aplicação uma única vez, quando inicia — se você fizer deploy de uma mudança nos Listeners, o worker antigo continua rodando a versão velha até ser reiniciado. Rode php artisan queue:restart depois de cada deploy (o Supervisor sobe um processo novo automaticamente, já com o código atualizado) — vale adicionar esse comando no fim do seu checklist de atualização, na tabela abaixo.

Quando o git pull reclama de mudanças locais

Às vezes um arquivo que é versionado (não está no .gitignore) acaba sendo modificado direto no servidor — de propósito (um ajuste específico daquele ambiente) ou sem querer. Quando isso acontece, git pull recusa continuar, com medo de sobrescrever uma mudança que ninguém commitou. Duas saídas, dependendo do que você quer fazer:

Descartar tudo que foi modificado localmente, sem exceção:

terminal
git reset --hard HEAD
git pull origin main

reset --hard HEAD volta cada arquivo rastreado exatamente pro estado do último commit, jogando fora qualquer mudança local — é destrutivo e sem confirmação, então só use tendo certeza de que não tem nada ali que precise guardar.

Ignorar mudanças só de um arquivo específico, mantendo o resto normal:

Isso é diferente de colocar o arquivo no .gitignore (que só funciona pra arquivos que o Git ainda não rastreia). Pra um arquivo que já está no repositório, mas que você quer que o Git pare de notar mudanças locais nele:

terminal
git update-index --skip-worktree caminho/do/arquivo

Dali em diante, o Git ignora qualquer modificação local nesse arquivo específico em operações como pull/status/checkout — útil pra um arquivo de configuração com um ajuste só daquele servidor, sem precisar mexer no .gitignore nem arriscar perder esse ajuste no próximo pull. Pra reverter e voltar a rastrear normalmente: git update-index --no-skip-worktree caminho/do/arquivo.

Passo 8 — garantindo HTTPS nos links gerados

Dependendo de como o servidor/painel está configurado, o Laravel às vezes não detecta sozinho que a conexão é HTTPS — e aí passa a gerar links e URLs de asset como http:// mesmo numa página carregada com certificado válido, o que o navegador trata como "conteúdo misto" e pode bloquear. A forma mais direta de evitar isso é forçar o esquema HTTPS explicitamente em produção:

app/Providers/AppServiceProvider.php (adicionando ao boot() que já existe)
use Illuminate\Support\Facades\URL;

public function boot(): void
{
    // Enforce HTTPS in production environments
    if (app()->isProduction()) {
        URL::forceHttps();
    }

    // ...resto do boot() (Gates, etc.)
}

app()->isProduction() é um atalho pra app()->environment('production') — confere o APP_ENV do .env. URL::forceHttps() faz o Laravel gerar toda URL (rotas, asset(), os links do @vite se você estiver na trilha de Vue) sempre com https://, independente do que ele tenha detectado na requisição — sem precisar mexer em configuração de proxy nenhuma. Restringir ao ambiente de produção evita que isso atrapalhe o php artisan serve local, que normalmente roda em HTTP mesmo. Não esqueça também de conferir o APP_URL no .env de produção, já com o esquema certo: APP_URL=https://seu-dominio.com.

Resumo: primeiro deploy vs. atualização do dia a dia

O checklist inteiro acima só é necessário na primeira vez que o projeto chega num ambiente novo. Pra publicar uma atualização num servidor que já está rodando, o processo é bem mais curto:

Primeiro deploy (ambiente novo)Atualização (servidor já rodando)
git clonegit pull origin main
composer installcomposer install (só se o composer.lock mudou)
cp .env.example .env + editarnão repete — o .env já existe e fica intacto
php artisan key:generatenão repete — geraria uma chave nova e quebraria dados já criptografados
migrate --force --seedmigrate --force (sem --seed, pra não duplicar o admin)
storage:link + permissõesnão repete, a menos que a pasta storage/ tenha sido recriada
Configurar e iniciar o Supervisornão repete — já fica de pé sozinho
Adicionar o URL::forceHttps() no AppServiceProvidernão repete — fica no código, versionado
optimize:clear + config:cache / route:cache / view:cacherodar de novo sempre, pra pegar o código atualizado
php artisan queue:restart, pra o worker pegar o código novo

Mão na massa

Simule um "primeiro deploy" na sua própria máquina: faça uma cópia de segurança do seu .env atual (renomeie pra .env.backup, por exemplo), depois apague as pastas vendor/ e o arquivo .env. Siga o checklist inteiro, do Passo 1 ao Passo 6 (pule os comandos de cache, que atrapalham o desenvolvimento local), usando os mesmos dados de banco que você já tinha. No final, confirme que o sistema volta a funcionar exatamente como antes — login, cadastro de usuário, tudo. Se tiver acesso a um servidor Linux de teste, configure o Supervisor do Passo 7 de verdade: cadastre um usuário como gerente, mate o processo do queue:work na mão (kill no PID dele) e confirme que o Supervisor sobe outro sozinho em poucos segundos.

Referência Rápida

Aula 29 · resumo

Esse é o "cheat sheet" — pra quando você esquecer se era singular ou plural, camelCase ou snake_case. Uma distinção importa em toda essa lista: algumas convenções são mecânicas (o framework literalmente usa o nome pra montar a query ou resolver o arquivo — fugir delas quebra o funcionamento, a menos que você configure manualmente); outras são só estilo de comunidade (o Laravel funciona do mesmo jeito não importa o nome, mas seguir a convenção deixa o código previsível pra qualquer dev que abrir o projeto).

A regra-mãe: camelCase no PHP, snake_case no banco

Em código PHP (métodos, variáveis), o padrão é camelCase ($dataNascimento, calcularIdade()). Em banco de dados (colunas, tabelas), o padrão é snake_case (data_nascimento, criador_id). O Eloquent converte automaticamente entre os dois nos accessors — é por isso que um método idade() vira $usuario->idade na hora de usar.

Models e Tabelas (mecânico)

O quêConvençãoExemplo do nosso projeto
Nome do ModelSingular, PascalCaseUser
Nome da tabelaPlural, snake_caseusers
Chave primáriaid, auto-incremento$table->id();
Fugir da convençãoSobrescrever explicitamenteprotected $table = 'nome';

Cuidado com plurais irregulares do português (papelpapéis, não papels) — a pluralização automática do Laravel segue regras de inglês.

Chaves Estrangeiras (mecânico)

O quêConvençãoExemplo
Chave estrangeira{model_singular}_idcriador_id (aponta pra outro User)
Tabela pivot (N:N)Os dois models no singular, ordem alfabéticacategoria_produto

Relacionamentos (nome do método) (estilo de comunidade)

TipoConvençãoExemplo do projeto
hasOne / belongsTo (1-para-1)Singular, camelCasecriador()
hasMany / belongsToMany (vários)Plural, camelCaseusuariosCriados()

Controllers, Rotas e Migrations (estilo de comunidade)

O quêConvençãoExemplo
Nome do ControllerPascalCase + sufixo ControllerUsuarioController
Nome de rota (dot notation)Gerado por Route::resource()usuarios.index
Arquivo de migrationVerbo + tabela, snake_caseadd_papel_to_users_table

Classes auxiliares (Policy é mecânico; o resto é estilo)

O quêConvençãoExemplo
Policy{Model}Policy, em app/Policies/ — descoberta automáticaUserPolicy
Form Request{Ação}{Model}RequestStoreUsuarioRequest
API Resource{Model}ResourceUsuarioResource
JobVerbo + assunto, PascalCaseVerificarAniversariantesJob
EventFato no particípio passadoUsuarioCadastrado
ListenerAção + sufixo ListenerEnviarBoasVindasEmailListener
MailableAssunto + sufixo MailBoasVindasMail
ActionVerbo + assunto + sufixo ActionCadastrarUsuarioAction
Scope localMétodo scopeNome(), chamado sem o prefixoscopeGerentes()User::gerentes()
AccessorMétodo camelCase, acesso snake_caseidade()$u->idade

Blade (mecânico)

O quêConvençãoExemplo
ViewDot notation aponta pra subpastaview('usuarios.index')usuarios/index.blade.php
Componente (tag)kebab-case, prefixo x-<x-alerta-erro>

Config e Ambiente (mecânico)

O quêConvençãoExemplo
Variável de ambienteSCREAMING_SNAKE_CASEEVOLUTION_API_URL
Chave de configsnake_case, dot notationconfig('usuarios.papeis_disponiveis')

Regra prática pra decidir se algo é mecânico ou estilo: se o Laravel precisa adivinhar um nome pra fazer alguma coisa funcionar sozinha, é mecânico. Se é só o nome de uma classe que você mesmo vai referenciar em todo lugar, é estilo — funciona do jeito que você chamar, mas seguir o padrão poupa confusão.

O sistema completo, de relance

Se você fez todos os exercícios, seu projeto agora tem: rotas e Controller de usuários protegidos por autenticação e Gate; um model User estendido com papel, data de nascimento, foto e telefone; um relacionamento de auto-referência (quem criou quem); scopes e accessor de idade; um Event disparando três Listeners em fila (e-mail pro admin, e-mail e WhatsApp pro novo usuário); uma Notification com sino em tempo real via Broadcasting; um Job agendado verificando aniversariantes todo dia; um Seeder criando o admin original; e uma listagem em tabela HTML com Tailwind respeitando quem pode ver o quê. Esse é o "mini-ERP de usuários" completo — e cada peça dele você escreveu com a própria mão, aula por aula, sem depender de JavaScript nenhum.

nenhuma aula encontrada