Básico do Eloquent ORM do Laravel
Eloquent é o "tradutor de banco de dados" do Laravel—você interage usando objetos PHP, e ele os traduz em consultas SQL para execução.
1. O Que Você Vai Aprender
- Criação de Modelo e Definição de Atributos: $fillable/$guarded/$casts/$attributes
- O Fluxo CRUD Completo: create/all/find/update/delete e Atribuição em Lote
- Query builders: where/orderBy/groupBy/subqueries
- Operações de coleção: filter/map/reduce/each processamento encadeado
- Soft Delete e Recuperação: trait SoftDeletes
2. Uma História Real de um Desenvolvedor Full-Stack
(1) Problema: Concatenação de strings SQL leva a injeção e perda de dados
No início, Bob escreveu o ShopMetrics em PHP nativo—todas as consultas SQL eram construídas usando concatenação de strings: "SELECT * FROM shops WHERE id = " . $_GET['id']. Um hacker injetou 1 OR 1=1 no ID da loja de Alice, causando um vazamento de dados em todo o site. Um problema mais comum foi quando Bob esqueceu de incluir uma cláusula WHERE em uma consulta UPDATE; um único comando zerou a receita de todas as lojas, e levou um dia inteiro para restaurar os dados.
(2) A Solução Eloquent ORM
O Eloquent usa objetos PHP para interagir com bancos de dados, fornece vinculação automática de parâmetros para prevenir ataques de injeção, protege campos sensíveis com atribuição em lote e usa soft delete para prevenir exclusão acidental.
// Seguro, legível, sem possibilidade de injeção SQL
$shop = Shop::create([
'name' => 'Alice Store',
'tenant_id' => 1,
]);
// Proteção de atribuição em lote — apenas campos $fillable permitidos
protected $fillable = ['name', 'slug', 'tenant_id'];
// revenue NÃO está em $fillable — não pode ser definido via create()
(3) Resultado
Depois que Bob começou a usar Eloquent, o risco de injeção SQL foi eliminado, e dados acidentalmente excluídos podiam ser restaurados com um único clique usando soft delete. A quantidade de código foi reduzida de 200 linhas de SQL para 30 linhas de PHP.
3. Definição do Modelo
(1) Criar um Modelo
php artisan make:model Shop
# Cria: app/Models/Shop.php
# Com migração
php artisan make:model Shop -m
# Cria: app/Models/Shop.php + database/migrations/create_shops_table.php
(2) Configuração de Propriedades do Modelo
// app/Models/Shop.php
class Shop extends Model
{
protected $fillable = [
'tenant_id', 'name', 'slug', 'description', 'status', 'revenue',
];
protected $guarded = ['id']; // Alternativa: bloquear campos específicos
protected $attributes = [
'status' => 'active',
'revenue' => 0,
];
protected $casts = [
'revenue' => 'decimal:2',
'is_active' => 'boolean',
'metadata' => 'json',
'launched_at' => 'datetime',
];
}
| Propriedade | Função | Método Recomendado |
|---|---|---|
$fillable |
Campos que permitem atribuição em lote | ✅ Lista Branca |
$guarded |
Campos onde atribuição em lote é proibida | ❌ Lista Negra |
$casts |
Conversão Automática de Tipo | Obrigatório |
$attributes |
Valor Padrão de Campo | Substitui Padrão do BD |
(3) Diagrama de Classes do Modelo Eloquent
classDiagram
class Model {
+save()
+delete()
+update(array data)
+fresh()
+refresh()
+toArray()
+toJson()
}
class Shop {
+array fillable
+array casts
+tenant()
+orders()
+products()
}
class SoftDeletes {
+forceDelete()
+restore()
+trashed()
+withTrashed()
+onlyTrashed()
}
Model <|-- Shop
Shop ..|> SoftDeletes : usa trait
(1) ▶ Exemplo: Modelo de Loja do ShopMetrics
// app/Models/Shop.php
class Shop extends Model
{
use SoftDeletes;
protected $fillable = [
'tenant_id', 'name', 'slug', 'description', 'status', 'revenue',
];
protected $casts = [
'revenue' => 'decimal:2',
'metadata' => 'array',
];
protected $attributes = [
'status' => 'active',
'revenue' => 0,
];
public function tenant(): BelongsTo
{
return $this->belongsTo(Tenant::class);
}
public function products(): HasMany
{
return $this->hasMany(Product::class);
}
public function scopeActive(Builder $query): Builder
{
return $query->where('status', 'active');
}
}
Saída:
// Execução bem-sucedida
4. Operações CRUD
(1) Criar
// Método 1: create() com atribuição em lote
$shop = Shop::create([
'tenant_id' => 1,
'name' => 'Alice Store',
'slug' => 'alice-store',
]);
// Método 2: new + save
$shop = new Shop();
$shop->tenant_id = 1;
$shop->name = 'Alice Store';
$shop->slug = 'alice-store';
$shop->save();
// Método 3: firstOrCreate — buscar ou criar
$shop = Shop::firstOrCreate(
['slug' => 'alice-store'], // critérios de busca
['name' => 'Alice Store', 'tenant_id' => 1], // valores se criar
);
// Método 4: updateOrCreate — atualizar ou criar
$shop = Shop::updateOrCreate(
['slug' => 'alice-store'],
['name' => 'Alice Store Updated', 'revenue' => 5000],
);
(2) Ler
// Buscar por chave primária
$shop = Shop::find(1);
$shop = Shop::findOrFail(1); // lança 404 se não encontrado
// Buscar por coluna
$shop = Shop::where('slug', 'alice-store')->first();
$shop = Shop::whereSlug('alice-store')->firstOrFail();
// Obter todos
$shops = Shop::all();
$shops = Shop::active()->get(); // usando scope
// Chunk para grandes conjuntos de dados
Shop::chunk(200, function ($shops) {
foreach ($shops as $shop) {
// Processar 200 lojas por vez
}
});
(3) Atualizar
// Atualizar modelo único
$shop->update(['name' => 'New Name']);
// Atualizar via consulta
Shop::where('status', 'suspended')->update(['status' => 'active']);
// Incrementar/Decrementar
$shop->increment('revenue', 1500);
Shop::whereId(1)->decrement('stock', 5);
(4) Excluir
// Soft delete (define deleted_at)
$shop->delete();
// Forçar exclusão (permanente)
$shop->forceDelete();
// Restaurar soft-deleted
$shop->restore();
// Consultar com lixeira
Shop::withTrashed()->where('id', 1)->first();
Shop::onlyTrashed()->get();
(1) ▶ Exemplo: Fluxo CRUD Completo do ShopMetrics
// Criar uma loja com produtos
$shop = Shop::create([
'tenant_id' => 1,
'name' => 'Bob Electronics',
'slug' => 'bob-electronics',
]);
$shop->products()->createMany([
['name' => 'Widget A', 'sku' => 'W-001', 'price' => 29.99],
['name' => 'Widget B', 'sku' => 'W-002', 'price' => 49.99],
]);
// Ler com eager loading
$shop = Shop::with('products')->whereSlug('bob-electronics')->firstOrFail();
// Atualizar loja e produto
$shop->update(['revenue' => 15000]);
$shop->products()->whereSku('W-001')->update(['price' => 34.99]);
// Soft delete e restaurar
$shop->delete();
Shop::withTrashed()->whereSlug('bob-electronics')->first()->restore();
Saída:
// Execução bem-sucedida
5. Query Builder
(1) Consulta Condicional
$shops = Shop::where('status', 'active')
->where('revenue', '>', 1000)
->orWhere(function ($query) {
$query->where('status', 'new')
->where('created_at', '>', now()->subDays(7));
})
->get();
// Where dinâmico
$shops = Shop::whereStatus('active')
->whereRevenueGreaterThan(1000)
->get();
(2) Ordenação, Agrupamento e Paginação
// OrderBy
$shops = Shop::orderBy('revenue', 'desc')->get();
// GroupBy com having
$revenueByStatus = Shop::select('status', DB::raw('SUM(revenue) as total'))
->groupBy('status')
->having('total', '>', 1000)
->get();
// Paginação
$shops = Shop::where('tenant_id', 1)->paginate(15);
$shops = Shop::where('tenant_id', 1)->simplePaginate(15);
$shops = Shop::where('tenant_id', 1)->cursorPaginate(15);
| Métodos de Paginação | Executar Consulta | Casos de Uso |
|---|---|---|
paginate() |
COUNT + SELECT | Número Total de Páginas Necessário |
simplePaginate() |
Apenas SELECT | Número total de páginas não necessário |
cursorPaginate() |
SELECT com WHERE apenas | Mais Eficiente para Grandes Conjuntos de Dados |
(3) Subconsultas
// Subconsulta no select
$shops = Shop::select('shops.*')
->selectSub(
Order::selectRaw('SUM(total)')
->whereColumn('shop_id', 'shops.id'),
'orders_total'
)
->get();
// Subconsulta no where
$latestOrders = Shop::where('created_at', function ($query) {
$query->selectRaw('MAX(created_at)')
->from('orders')
->whereColumn('shop_id', 'shops.id');
})->get();
(1) ▶ Exemplo: Consulta Complexa do ShopMetrics
// Top 10 lojas por receita no tenant atual, com contagem de pedidos
$topShops = Shop::select('shops.*')
->selectSub(
Order::selectRaw('COUNT(*)')
->whereColumn('shop_id', 'shops.id')
->where('created_at', '>=', now()->subDays(30)),
'recent_orders_count'
)
->where('tenant_id', tenant()->id)
->where('status', 'active')
->orderBy('revenue', 'desc')
->take(10)
->get();
Saída:
// Execução bem-sucedida
6. Operações de Coleção
O get() do Eloquent retorna um objeto Collection, que fornece métodos encadeados mais poderosos do que arrays.
| Método | Função | Equivalente SQL |
|---|---|---|
filter() |
Filtrar | WHERE |
map() |
Mapear Conversão | Conversão SELECT |
sortBy() |
Ordenar | ORDER BY |
groupBy() |
Agrupar | GROUP BY |
sum() |
Somar | SUM() |
count() |
Contar | COUNT() |
pluck() |
Extrair Coluna | SELECT uma coluna |
unique() |
Remover duplicatas | DISTINCT |
each() |
Iterar e executar | — |
reduce() |
Cálculo Cumulativo | — |
(1) ▶ Exemplo: Operações Encadeadas de Coleção do ShopMetrics
// Obter todas as lojas de um tenant, filtrar e transformar
$topShops = Shop::where('tenant_id', 1)
->with('products')
->get()
->filter(fn ($shop) => $shop->revenue > 1000)
->sortByDesc('revenue')
->map(fn ($shop) => [
'name' => $shop->name,
'revenue' => $shop->revenue,
'product_count' => $shop->products->count(),
])
->take(10);
// Agrupar lojas por status e contar
$shopsByStatus = Shop::where('tenant_id', 1)
->get()
->groupBy('status')
->map(fn ($group) => $group->count());
// ['active' => 15, 'suspended' => 2, 'closed' => 1]
// Extrair IDs para operação em lote
$shopIds = Shop::where('status', 'active')->pluck('id');
// [1, 2, 5, 8, 12]
Saída:
// Execução bem-sucedida
7. Soft Delete
(1) Habilitar soft delete
// Modelo
class Shop extends Model
{
use SoftDeletes;
protected $casts = [
'deleted_at' => 'datetime',
];
}
// Migração
$table->softDeletes(); // adiciona deleted_at TIMESTAMP NULL
(2) Operação de Soft Delete
// Excluir (soft — define deleted_at)
$shop->delete();
// Verificar se está na lixeira
$shop->trashed(); // true se soft-deleted
// Incluir registros na lixeira
Shop::withTrashed()->get();
// Apenas registros na lixeira
Shop::onlyTrashed()->get();
// Restaurar
$shop->restore();
// Exclusão permanente
$shop->forceDelete();
(1) ▶ Exemplo: Cenário de Recuperação com Soft Delete do ShopMetrics
// Alice acidentalmente excluiu uma loja
$shop = Shop::whereSlug('alice-store')->first();
$shop->delete();
// Bob ainda pode encontrá-la nos registros da lixeira
$trashed = Shop::onlyTrashed()->whereSlug('alice-store')->first();
// Restaurar a loja com todos os relacionamentos intactos
if ($trashed) {
$trashed->restore();
// $trashed->products ainda existem — não foram excluídos
}
Saída:
// Execução bem-sucedida
8. Exemplo Completo: Análise de Pedidos do ShopMetrics
// ============================================
// Completo: Análise de Pedidos do ShopMetrics
// Abrange: CRUD, consultas, coleções, soft delete, scopes
// ============================================
// app/Models/Order.php
class Order extends Model
{
use SoftDeletes;
protected $fillable = [
'tenant_id', 'shop_id', 'user_id', 'order_number',
'subtotal', 'discount', 'total', 'status', 'metadata',
];
protected $casts = [
'total' => 'decimal:2',
'metadata' => 'array',
'deleted_at' => 'datetime',
];
public function shop(): BelongsTo
{
return $this->belongsTo(Shop::class);
}
public function scopeCompleted(Builder $query): Builder
{
return $query->where('status', 'completed');
}
public function scopeThisMonth(Builder $query): Builder
{
return $query->whereBetween('created_at', [
now()->startOfMonth(), now()->endOfMonth(),
]);
}
}
// Consulta de análise — relatório de receita mensal
$monthlyReport = Order::where('tenant_id', tenant()->id)
->completed()
->thisMonth()
->with('shop')
->get()
->groupBy('shop.name')
->map(fn ($orders) => [
'shop' => $orders->first()->shop->name,
'order_count' => $orders->count(),
'revenue' => $orders->sum('total'),
'avg_order' => $orders->avg('total'),
])
->sortByDesc('revenue')
->values();
❓ Perguntas Frequentes
findOrFail?findOrFail quando precisar retornar uma página 404 se nenhum registro for encontrado. Se não encontrar registros for parte da lógica de negócio normal (como uma busca sem resultados), use find e verifique se é null.WHERE em SQL), enquanto Collections operam na memória (ex: filtragem com filter em PHP). Grandes conjuntos de dados devem ser filtrados no nível da consulta, enquanto pequenos conjuntos de resultados podem usar métodos de Collection.deleted_at; os dados associados permanecem no banco de dados. Uma vez que o modelo pai é restaurado, o relacionamento torna-se disponível imediatamente. Se você precisar realizar um soft delete em cascata, pode ouvir o evento deleting no método boot() do modelo.DB::table()) quando precisar apenas de resultados simples de consulta e não precisar de modelos. Eloquent é essencialmente um wrapper em torno do Query Builder.Shop::where(...)->update([...]) Executa uma única instrução SQL para atualizar todos os registros correspondentes; isso é altamente eficiente. $shops->each->update([...]) Executa instruções SQL uma a uma, acionando eventos do modelo. Use atualizações individuais quando precisar acionar eventos.📖 Resumo
- O Eloquent usa objetos PHP para interagir com bancos de dados, com vinculação automática de parâmetros para prevenir injeção SQL
- Listas brancas $fillable protegem contra atribuição em lote; $casts realiza conversão automática de tipo
- Os quatro passos do CRUD: create/read/update/delete;
findOrFailretorna 404 - O query builder suporta WHERE, ORDER BY, GROUP BY e subconsultas
- Collections oferecem operações encadeadas mais poderosas (filter/map/sortBy) do que arrays
- Soft delete marcado com
deleted_atem vez de exclusão permanente; suporta restauração
📝 Exercícios
-
Exercício Básico (⭐): Crie um modelo
Productpara o ShopMetrics, defina$fillablee$casts, implemente operações CRUD completas (criar, ler, atualizar, excluir), e use Tinker para validar cada operação. -
Problema Avançado (⭐⭐): Escreva uma consulta para recuperar as 5 lojas com maior receita sob o tenant atual, junto com suas contagens de pedidos. Use a subconsulta
selectSube o métodomapdaCollectionpara formatar a saída. -
Desafio (⭐⭐⭐): Implemente soft delete e restauração em cascata para o modelo Order: Quando um Order for excluído, seus OrderItems também são soft-deletados; quando o Order for restaurado, os OrderItems são restaurados junto. Implemente isso usando listeners de eventos do modelo.



