Criando um Projeto
composer create-project laravel/laravel meu-app # criar um projeto novo
cd meu-app
composer global require laravel/installer # ou instale o instalador uma vez
laravel new meu-app # e crie projetos com ele
composer install # instala as dependências PHP (vendor/ está no gitignore)
cp .env.example .env # cria o arquivo de ambiente local
php artisan key:generate # escreve a APP_KEY no .env
php artisan migrate # cria as tabelas no banco
composer run dev # servidor + worker de fila + Vite, tudo num terminal só
- Os quatro passos
composer install→migrateacima são o que um clone de um projeto existente precisa. - O
.envnão é commitado, por isso um projeto clonado não tem nenhum. - A
APP_KEYcriptografa sessões e cookies. Sem ela, nada sobe. composer run dev(ou sóphp artisan serve) é para desenvolvimento; produção roda atrás de Nginx/Apache ou FrankenPHP.- Um projeto novo já vem com SQLite: o
laravel newcria odatabase/database.sqlitee roda as migrations sozinho.
Starter Kits
O laravel new pergunta qual starter kit você quer: uma aplicação Laravel
normal que já vem com autenticação e um frontend montado, tudo dentro do seu
projeto para você editar.
- React — Inertia, React 19, TypeScript, Tailwind, shadcn/ui. Frontend em
resources/js/. - Vue — Inertia, Vue 3 Composition API, TypeScript, Tailwind, shadcn-vue. Frontend em
resources/js/. - Svelte — Inertia, Svelte 5, TypeScript, Tailwind, shadcn-svelte. Frontend em
resources/js/. - Livewire — Livewire, Tailwind, Flux UI. Sem framework JS, tudo em Blade e PHP. Frontend em
resources/views/. - Nenhum — o esqueleto puro, para aprender o framework ou construir uma API.
laravel new meu-app # pergunta qual kit
laravel new meu-app --using=vendor/starter-kit # kit da comunidade, via Packagist
- Os kits React, Vue e Svelte usam Inertia, mantendo o roteamento e os controllers no servidor, sem camada de API separada.
- Teams — uma opção no mesmo prompt. Adiciona telas de gerenciamento de
times e rotas escopadas por time (
/{current_team}/dashboard). - WorkOS AuthKit — outra opção. Troca a autenticação nativa por um
provedor hospedado, com login social, passkeys, magic link e SSO. Precisa
de
WORKOS_CLIENT_ID,WORKOS_API_KEYeWORKOS_REDIRECT_URLno.env.
Todo kit usa o Laravel Fortify para autenticação, configurado em
config/fortify.php:
use Laravel\Fortify\Features;
'features' => [
Features::registration(), // remova esta linha para fechar o cadastro público
Features::resetPasswords(),
Features::emailVerification(),
Features::twoFactorAuthentication(['confirm' => true]),
],
- A lógica de cadastro fica em
app/Actions/Fortify/CreateNewUser.php. - Nos kits com Inertia, desligar uma funcionalidade também exige remover as rotas dela usadas nos componentes, senão o build quebra.
- Breeze e Jetstream eram a geração anterior de starter kits; a documentação atual não os lista mais, então use os kits acima em projetos novos.
- Essa geração de kits chegou no Laravel 12; o kit Svelte e a autenticação via Fortify vieram no Laravel 13.
Ambiente Local: Herd e Sail
php artisan serve parte do princípio de que você já tem PHP, Composer e um
banco instalados e configurados. Herd e Sail eliminam esse trabalho de setup.
Herd (macOS / Windows)
herd park ~/Sites # serve todos os projetos dessa pasta
herd link meu-app # serve um projeto específico como meu-app.test
herd php -v # o binário PHP gerenciado pelo Herd
herd open # abre o projeto atual no navegador
- O Herd é um app nativo com PHP, Composer e
nginx. Projetos num diretório “parkeado” são servidos automaticamente em
<pasta>.test. - Troque a versão do PHP por projeto pela interface ou com
herd use php@8.3. - O Herd já traz os binários
php,composer,laravel,node,npmenvm; por isso onodepode não aparecer noPATHfora do seu shell. - A versão paga adiciona bancos de dados, captura de e-mails e visualização de logs.
Sail (Docker)
php artisan sail:install # adiciona o Sail a um projeto existente
./vendor/bin/sail up -d # sobe os containers
./vendor/bin/sail down # derruba os containers
php artisan serve → sail artisan serve
php artisan migrate → sail artisan migrate
composer install → sail composer install
npm run dev → sail npm run dev
php artisan tinker → sail artisan tinker
- Empacota PHP, MySQL, Redis e outros como containers via Docker Compose,
sem instalar nada na máquina. Prefixe todo comando com
sail. - Encurte com um alias no shell:
alias sail='[ -f sail ] && sh sail || sh vendor/bin/sail'. - Rodar
php artisan migratepuro na máquina é um erro comum; usa o PHP do host, não o banco do container. - No
.env,DB_HOSTé o nome do serviço (mysql), não127.0.0.1.
Herd e Sail mudam onde os comandos rodam, não o que eles fazem. Todo o resto deste cheat sheet é igual nos dois casos.
Arquitetura: MVC
O Laravel segue o padrão MVC. A requisição percorre a aplicação em uma direção:
Requisição → routes/web.php → Controller → Model (Eloquent) → Banco
↓
View (Blade) → Resposta
- Rota (
routes/web.php,api.php) — liga uma URL e um verbo HTTP a um código. - Controller (
app/Http/Controllers/) — recebe a requisição, orquestra, responde. - Model (
app/Models/) — representa uma tabela, guarda queries e relações. - View (
resources/views/) — templates Blade que renderizam o HTML.
app/Http/Middleware/ # código que roda antes/depois de cada requisição
bootstrap/app.php # bootstrap da app: rotas, middleware, exceções
config/ # arquivos de configuração, que leem do .env
database/migrations/ # mudanças de schema versionadas
database/seeders/ # dados iniciais/de exemplo
public/ # raiz web, ponto de entrada (index.php) e assets buildados
resources/js|css/ # código-fonte do frontend, compilado pelo Vite
routes/ # definição das rotas
storage/ # logs, cache, views compiladas, arquivos enviados
- Mantenha os controllers magros: valide, delegue, responda. Regra de negócio vive nos models ou em classes dedicadas, não no controller.
Comandos Artisan do Dia a Dia
php artisan serve # sobe o servidor de desenvolvimento
php artisan serve --port=8001 # em outra porta
php artisan tinker # REPL interativo com a aplicação carregada
php artisan route:list # todas as rotas registradas
php artisan route:list --path=user # filtrado
php artisan about # ambiente, versões, estado dos caches
artisané a CLI.php artisan listmostra tudo;php artisan help <comando>explica um.
Geradores
php artisan make:model Post -mcr # model + migration + controller + rotas resource
php artisan make:model Post -mfc # model + migration + factory + controller
php artisan make:model Post -m # model + migration
php artisan make:controller PostController --resource
php artisan make:migration create_posts_table
php artisan make:request StorePostRequest
php artisan make:middleware EnsureUserIsAdmin
php artisan make:seeder PostSeeder
php artisan make:factory PostFactory
php artisan make:command SyncPosts
Banco de Dados
php artisan migrate # roda as migrations pendentes
php artisan migrate --path=database/migrations/2024_01_01_000000_create_posts_table.php # roda uma migration específica
php artisan migrate:status # o que rodou e o que não rodou
php artisan migrate:rollback # desfaz o último lote
php artisan migrate:fresh --seed # dropa tudo, re-migra e roda os seeders
php artisan db:seed # roda só os seeders
migrate:freshapaga todos os dados. Nunca rode em produção.
Cache e Manutenção
php artisan optimize:clear # limpa cache de config, rotas, views e aplicação de uma vez
php artisan config:clear
php artisan cache:clear
php artisan view:clear
php artisan storage:link # cria o link public/storage → storage/app/public
php artisan queue:work # processa os jobs da fila
php artisan down # modo manutenção
php artisan up
- Em produção, cacheie:
php artisan config:cache route:cache view:cache. - Em desenvolvimento, mantenha limpo, já que config cacheada ignora mudanças no
.env.
Rotas
// routes/web.php
use App\Http\Controllers\PostController;
Route::get('/', fn () => view('welcome'));
Route::get('/posts', [PostController::class, 'index'])->name('posts.index');
Route::post('/posts', [PostController::class, 'store']);
Route::get('/posts/{post}', [PostController::class, 'show']);
Route::put('/posts/{post}', [PostController::class, 'update']);
Route::delete('/posts/{post}', [PostController::class, 'destroy']);
// As sete rotas de CRUD de uma vez
Route::resource('posts', PostController::class);
// Agrupamento
Route::middleware('auth')->prefix('admin')->group(function () {
Route::get('/dashboard', [DashboardController::class, 'index']);
});
- Tipar o model no método (
show(Post $post)) ativa o route model binding: o Laravel busca o registro e devolve 404 automaticamente se não existir. - Use rotas nomeadas e
route('posts.index')em vez de URLs escritas na mão. routes/web.phptem sessão e CSRF;routes/api.phpé stateless.
Controllers
namespace App\Http\Controllers;
use App\Models\Post;
use Illuminate\Http\Request;
class PostController extends Controller
{
public function index()
{
return view('posts.index', ['posts' => Post::latest()->paginate(15)]);
}
public function show(Post $post) // route model binding
{
return view('posts.show', compact('post'));
}
public function store(Request $request)
{
$data = $request->validate([
'title' => ['required', 'string', 'max:255'],
'body' => ['required'],
]);
$post = Post::create($data);
return redirect()->route('posts.show', $post)->with('status', 'Post criado!');
}
}
Eloquent
// Leitura
Post::all();
Post::find(1);
Post::findOrFail(1); // lança 404 se não existir
Post::where('published', true)->get();
Post::where('views', '>', 100)->orderBy('views', 'desc')->take(5)->get();
Post::first();
Post::count();
// Escrita
$post = Post::create(['title' => 'Oi', 'body' => '...']); // precisa de $fillable
$post->update(['title' => 'Atualizado']);
$post->delete();
// Relações
class Post extends Model
{
protected $fillable = ['title', 'body'];
public function user() { return $this->belongsTo(User::class); }
public function comments() { return $this->hasMany(Comment::class); }
}
$post->comments; // carregamento sob demanda
Post::with('comments')->get(); // eager loading, evita o problema N+1
create()eupdate()só atribuem campos listados em$fillable. Uma coluna que “sumiu” silenciosamente quase sempre é um$fillableincompleto.- O model
Postmapeia para a tabelapostspor convenção (plural, snake_case).
use Illuminate\Database\Eloquent\SoftDeletes;
class Post extends Model
{
use SoftDeletes; // precisa de uma coluna deleted_at anulável, veja Migrations
}
$post->delete(); // marca deleted_at, mantém a linha
$post->trashed(); // true se foi soft deleted
$post->restore(); // limpa o deleted_at
$post->forceDelete(); // remove a linha de verdade
Post::withTrashed()->get(); // inclui linhas soft deleted
Post::onlyTrashed()->get(); // só as linhas soft deleted
SoftDeletesmarca a linha como deletada em vez de removê-la. Toda query normal exclui as linhas deletadas automaticamente, então nada mais no app precisa filtrar isso na mão.- Adicione a coluna com
$table->softDeletes()numa migration.
Models Limpos
class Order extends Model
{
public function isPaid(): bool
{
return $this->status === 'paid';
}
}
if ($order->isPaid()) {
// ...
}
- Lógica usada em mais de um lugar, um controller, um job, uma view, pertence ao model como método, não copiada e colada em cada chamada.
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Support\Facades\Hash;
class User extends Model
{
protected function fullName(): Attribute
{
return Attribute::make(
get: fn () => "{$this->first_name} {$this->last_name}",
);
}
protected function password(): Attribute
{
return Attribute::make(
set: fn (string $value) => Hash::make($value),
);
}
}
$user->full_name; // acessado como uma coluna, sem parênteses
$user->password = 'secret'; // já sai com hash ao ser atribuído
- Acesse um accessor como
$user->full_name, não$user->fullName(). Quem chama não precisa saber se um atributo é uma coluna de verdade ou calculado; os dois devem parecer iguais. - O método é camelCase (
fullName); o Eloquent expõe como o atributo snake_case (full_name).
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
class Post extends Model
{
#[Scope]
protected function published(Builder $query): void
{
$query->where('published', true);
}
}
Post::published()->latest()->get();
Post::published()->where('user_id', $userId)->get();
- Um
whererepetido em vários controllers é sinal de que ele pertence a um scope, encadeável como qualquer outro método de query.
{{-- Evite --}}
@php
$total = 0;
foreach ($order->items as $item) {
$total += $item->price * $item->quantity;
}
@endphp
<p>Total: {{ $total }}</p>
{{-- Prefira --}}
<p>Total: {{ $order->total }}</p>
@php/@endphpnuma view geralmente é sinal de que a lógica pertence ao model, como um accessor, um scope, ou um método dedicado.
// app/Services/OrderService.php
class OrderService
{
public function checkout(User $user, array $items): Order
{
// cria o pedido, cobra o pagamento, envia o e-mail de confirmação...
}
}
// app/Http/Controllers/OrderController.php
class OrderController extends Controller
{
public function __construct(private OrderService $orderService) {}
public function store(Request $request)
{
$order = $this->orderService->checkout($request->user(), $request->validated());
return redirect()->route('orders.show', $order);
}
}
- Quando um método de controller começa a coordenar vários models, um pagamento, uma notificação, mova essa lógica pra uma Service injetada pelo construtor. O controller fica um coordenador magro: recebe a requisição, delega, responde.
- O Laravel não vem com um comando
make:service; uma service é só uma classe comum, por convenção dentro deapp/Services/.
Migrations
// database/migrations/xxxx_create_posts_table.php
public function up(): void
{
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->string('title');
$table->text('body');
$table->boolean('published')->default(false);
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('posts');
}
- Nunca edite uma migration que já rodou em produção; crie uma nova.
- Localmente,
php artisan migrate:freshé o jeito rápido de refazer tudo.
Templates Blade
{{-- resources/views/posts/index.blade.php --}}
@extends('layouts.app')
@section('content')
<h1>{{ $title }}</h1> {{-- saída escapada --}}
{!! $htmlConfiavel !!} {{-- saída crua, só para conteúdo confiável --}}
@if ($posts->isEmpty())
<p>Nenhum post ainda.</p>
@else
@foreach ($posts as $post)
<a href="{{ route('posts.show', $post) }}">{{ $post->title }}</a>
@endforeach
@endif
@auth
<p>Olá, {{ auth()->user()->name }}</p>
@endauth
<form method="POST" action="{{ route('posts.store') }}">
@csrf
<input name="title" value="{{ old('title') }}">
@error('title') <span>{{ $message }}</span> @enderror
<button>Salvar</button>
</form>
@endsection
- Todo formulário não-GET precisa de
@csrf, senão a requisição é rejeitada com 419. - Use
@method('PUT')/@method('DELETE')para verbos que o HTML não envia.
Validação
$request->validate([
'email' => ['required', 'email', 'unique:users,email'],
'password' => ['required', 'confirmed', 'min:8'],
'age' => ['nullable', 'integer', 'between:18,120'],
]);
php artisan make:request StorePostRequest
- Em caso de falha, o Laravel redireciona de volta automaticamente com os
erros e os dados antigos, sem
try/catch. - Para conjuntos maiores de regras, use um Form Request, gerado acima.
Vite e Assets
{{-- No <head> do seu layout --}}
@vite(['resources/css/app.css', 'resources/js/app.js'])
npm run dev # desenvolvimento: hot reload, precisa ficar rodando
npm run build # produção: gera public/build/ + manifest.json
- Em desenvolvimento, o
npm run devroda junto com ophp artisan serve. Ocomposer run devsobe os dois, mais o worker de fila, num terminal só. - Em produção, ou sempre que o servidor de dev não estiver de pé, você
precisa ter rodado
npm run buildpelo menos uma vez.
Testes
php artisan make:test PostTest # teste de feature, em tests/Feature
php artisan make:test PostTest --unit # teste unitário, em tests/Unit
php artisan test # roda toda a suíte
php artisan test --filter=PostTest # roda uma única classe de teste
use Illuminate\Foundation\Testing\RefreshDatabase;
class PostTest extends TestCase
{
use RefreshDatabase;
public function test_post_can_be_created(): void
{
$response = $this->post('/posts', ['title' => 'Hi', 'body' => '...']);
$response->assertRedirect();
$this->assertDatabaseHas('posts', ['title' => 'Hi']);
}
}
- O
RefreshDatabasereseta seu banco. Ele migra o schema antes da suíte rodar e envolve cada teste numa transação que é desfeita depois, então um teste não vê os dados do outro. - Um
phpunit.xmlrecém-criado aponta os testes paraDB_CONNECTION=sqlite,DB_DATABASE=:memory:, um banco isolado e descartado a cada execução. Se essas linhas estiverem faltando, comentadas, ou editadas para apontar pro seu banco de verdade, rodar os testes vai apagar seus dados locais. - Confira qual banco os testes realmente usam olhando o
phpunit.xml, ou rodephp artisan config:show database.default(depois dephp artisan config:clear) dentro do ambientetesting. - Testes unitários (
tests/Unit) não sobem a aplicação e não conseguem tocar no banco; testes de feature (tests/Feature) sobem, e é ali que a maioria dos seus testes deve morar.
Jobs, Commands e Scheduler
php artisan make:job ProcessPodcast
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
class ProcessPodcast implements ShouldQueue
{
use Queueable;
public function __construct(public Podcast $podcast) {}
public function handle(): void
{
// transcodifica o podcast, gera a thumbnail...
}
}
ProcessPodcast::dispatch($podcast); // empurrado pra fila
ProcessPodcast::dispatchSync($podcast); // roda na hora, sem fila
- Um job é um trabalho que não deveria travar a requisição: enviar um
e-mail, processar um arquivo, chamar uma API lenta.
dispatch()coloca na fila; um worker (php artisan queue:work) pega e roda ohandle(). - Precisa de uma conexão de fila no
.env(QUEUE_CONNECTION=database,redis, etc). Osync, padrão num projeto novo, roda os jobs na hora sem fila de verdade, útil em desenvolvimento local.
php artisan make:command SendEmails
namespace App\Console\Commands;
use Illuminate\Console\Command;
class SendEmails extends Command
{
protected $signature = 'emails:send {user}';
protected $description = 'Send a batch of emails to a user';
public function handle(): void
{
$this->info("Sending emails to user #{$this->argument('user')}...");
}
}
php artisan emails:send 1
- Um command é qualquer coisa que você quer rodar pela CLI: um script
avulso, uma tarefa de manutenção, algo que você também vai querer
agendar. Commands em
app/Console/Commandssão descobertos automaticamente.
// routes/console.php
use Illuminate\Support\Facades\Schedule;
Schedule::command('emails:send 1')->daily();
Schedule::job(new ProcessPodcast($podcast))->everyFiveMinutes();
Schedule::call(fn () => Log::info('Heartbeat'))->everyMinute();
- O scheduler substitui uma entrada de cron por tarefa por código,
definido em
routes/console.php. O servidor só precisa de uma linha de cron, rodando a cada minuto:
* * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1
- Localmente, pule a entrada de cron e rode
php artisan schedule:workno lugar; ele fica em loop no terminal e dispara as tarefas devidas a cada minuto. php artisan schedule:listmostra toda tarefa agendada e a próxima hora que ela vai rodar.
Erros Comuns
No application encryption key has been specified—APP_KEYvazia ou.envfaltando. Correção:cp .env.example .env && php artisan key:generate.- Página em branco ou 500 genérico — o erro real está escondido. Ponha
APP_DEBUG=trueno.env, ou leiastorage/logs/laravel.log. SQLSTATE[HY000] [2002] Connection refused— o banco não está rodando, ouDB_HOST/DB_PORTno.envestão errados.SQLSTATE[HY000] [1045] Access denied for user—DB_USERNAME/DB_PASSWORDerrados. Ajuste e rodephp artisan config:clear.SQLSTATE[42S02] Base table or view not found— as migrations nunca rodaram. Correção:php artisan migrate.Unable to open database file(SQLite) — o arquivo não existe. Correção:touch database/database.sqlite.Vite manifest not found at public/build/manifest.json— assets nunca foram buildados e o dev server está desligado. Correção:npm run build, ou rodenpm run dev.- Página carrega mas sem CSS/JS — o
npm run devparou. Reinicie, ou rodenpm run build. - 419 Page Expired — falta
@csrfou a sessão expirou. Adicione@csrfno formulário, ou limpe os cookies. - 404 numa rota que você acabou de criar — rotas cacheadas, ou verbo
errado. Rode
php artisan route:clear, depois confiraphp artisan route:list. Target class [FooController] does not exist— namespace errado ou typo na rota. Importe o controller e use[FooController::class, 'metodo'].Class "App\Models\Post" not found— autoloader desatualizado. Correção:composer dump-autoload.- Mudança no
.envnão surte efeito — config está cacheada. Correção:php artisan config:clear(ouoptimize:clear). The stream or file .../laravel.log could not be opened— sem permissão de escrita. Correção:chmod -R 775 storage bootstrap/cache.- Imagens enviadas dão 404 em
/storage/...— falta o symlink. Correção:php artisan storage:link. Add [title] to fillable property— mass assignment bloqueado. Adicione a coluna ao$fillabledo model.Attempt to read property on null— uma query retornounull. UsefindOrFail(), ou proteja com@if/?->.composer installfalha emext-...— uma extensão do PHP está faltando. Instale (ex.:php-mbstring,php-xml,php-curl).- Usando Sail,
Connection refusedem127.0.0.1— o comando rodou no host, ouDB_HOSTestá errado. Prefixe comsail; useDB_HOST=mysql.
A Ordem de Depuração
tail -f storage/logs/laravel.log # 1. leia o erro de verdade
php artisan optimize:clear # 2. limpe todos os caches
php artisan migrate:status # 3. o schema está atualizado?
php artisan route:list # 4. a rota existe como você imagina?
php artisan about # 5. ambiente, conexão com o banco, caches
- A maioria dos 500 de iniciante é uma de três coisas: falta de
APP_KEY, falta de conexão com o banco, ou migrations que nunca rodaram.