Artigo 42 — Eloquent ORM com Laravel
Módulo 7 · Semana 42 · Nível: Intermediário
Introdução
Eloquent é o ORM do Laravel, e é um dos mais elegantes disponíveis em qualquer linguagem. Cada tabela do banco tem um Model correspondente. Cada linha da tabela é representada por uma instância do Model. Relacionamentos entre tabelas viram métodos fluentes. Queries complexas viram código que se lê em voz alta.
Já vimos Eloquent brevemente no artigo 22. Aqui aprofundamos no contexto do Laravel: migrations integradas, factories, relacionamentos avançados, e os recursos que fazem o Eloquent brilhar em projetos reais.
Migrations
Migrations são controle de versão para o banco de dados. Cada alteração no schema é um arquivo versionado que pode ser aplicado e revertido:
php artisan make:migration criar_tabela_produtos
<?php
// database/migrations/2024_01_15_000001_criar_tabela_produtos.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::create('categorias', function (Blueprint $table) {
$table->id();
$table->string('nome', 100);
$table->string('slug', 120)->unique();
$table->text('descricao')->nullable();
$table->boolean('ativo')->default(true);
$table->timestamps(); // created_at e updated_at automáticos
});
Schema::create('produtos', function (Blueprint $table) {
$table->id();
$table->foreignId('categoria_id')
->constrained() // referencia categorias.id
->cascadeOnDelete(); // deleta produtos ao deletar categoria
$table->string('nome', 200);
$table->string('slug', 220)->unique();
$table->text('descricao')->nullable();
$table->decimal('preco', 10, 2);
$table->unsignedInteger('estoque')->default(0);
$table->boolean('ativo')->default(true);
$table->json('atributos')->nullable(); // dados extras flexíveis
$table->softDeletes(); // deleted_at — soft delete
$table->timestamps();
$table->index(['ativo', 'categoria_id']); // índice composto
});
}
public function down(): void
{
Schema::dropIfExists('produtos');
Schema::dropIfExists('categorias');
}
};
php artisan migrate # Aplica migrations pendentes
php artisan migrate:rollback # Reverte a última batch
php artisan migrate:status # Mostra status de cada migration
Models e fillable
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Str;
class Produto extends Model
{
use HasFactory, SoftDeletes;
// Campos que podem ser preenchidos em massa
protected $fillable = [
'categoria_id',
'nome',
'slug',
'descricao',
'preco',
'estoque',
'ativo',
'atributos',
];
// Casting automático de tipos
protected $casts = [
'preco' => 'decimal:2',
'estoque' => 'integer',
'ativo' => 'boolean',
'atributos' => 'array', // JSON → array automaticamente
];
// Hook: executado antes de criar
protected static function booted(): void
{
static::creating(function (Produto $produto) {
if (empty($produto->slug)) {
$produto->slug = Str::slug($produto->nome);
}
});
}
// Accessor: transforma o valor ao ler
public function getPrecoFormatadoAttribute(): string
{
return 'R$ ' . number_format($this->preco, 2, ',', '.');
}
// Scope: query reutilizável
public function scopeAtivo($query)
{
return $query->where('ativo', true)->where('estoque', '>', 0);
}
// Relacionamentos
public function categoria(): BelongsTo
{
return $this->belongsTo(Categoria::class);
}
public function avaliacoes(): HasMany
{
return $this->hasMany(Avaliacao::class);
}
}
Relacionamentos completos
<?php
// Belongs To Many (muitos para muitos) — pedidos ↔ produtos
class Pedido extends Model
{
public function produtos(): BelongsToMany
{
return $this->belongsToMany(Produto::class, 'pedido_produto')
->withPivot(['quantidade', 'preco_unitario']) // colunas extras na pivot
->withTimestamps();
}
// Has Many Through — usuário → pedidos → produtos
public function produtosComprados(): HasManyThrough
{
return $this->hasManyThrough(Produto::class, PedidoProduto::class);
}
}
// Polymorphic — comentários em produtos E posts
class Comentario extends Model
{
public function comentavel(): MorphTo
{
return $this->morphTo();
}
}
class Produto extends Model
{
public function comentarios(): MorphMany
{
return $this->morphMany(Comentario::class, 'comentavel');
}
}
Queries com Eloquent
<?php
// Básico
$produtos = Produto::all();
$produto = Produto::find(1);
$produto = Produto::findOrFail(1); // lança 404 se não encontrar
// Com condições
$ativos = Produto::where('ativo', true)
->where('estoque', '>', 0)
->orderBy('nome')
->get();
// Usando o scope definido no model
$ativos = Produto::ativo()->orderBy('nome')->get();
// Eager loading — carrega relacionamentos em uma query (evita N+1)
$produtos = Produto::with(['categoria', 'avaliacoes'])
->where('ativo', true)
->get();
// Paginação
$produtos = Produto::ativo()
->with('categoria')
->paginate(15);
// Inserir
$produto = Produto::create([
'nome' => 'Monitor LG 27"',
'preco' => 2499.90,
'categoria_id' => 3,
'estoque' => 10,
]);
// Atualizar
$produto->update(['preco' => 2299.90, 'estoque' => 8]);
// Upsert
Produto::updateOrCreate(
['slug' => 'monitor-lg-27'], // condição
['nome' => 'Monitor LG 27"', 'preco' => 2299.90] // valores
);
// Deletar (soft delete com SoftDeletes)
$produto->delete(); // seta deleted_at, não remove do banco
$produto->restore(); // restaura
$produto->forceDelete(); // remove definitivamente
// Incluir deletados nas queries
Produto::withTrashed()->where('nome', 'like', '%lg%')->get();
Produto::onlyTrashed()->get();
// Usando pivot em many-to-many
$pedido->produtos()->attach($produtoId, [
'quantidade' => 2,
'preco_unitario' => 299.90,
]);
$pedido->produtos()->sync([1, 2, 3]); // sincroniza a lista completa
Factories e Seeders
php artisan make:factory ProdutoFactory
<?php
// database/factories/ProdutoFactory.php
namespace Database\Factories;
use App\Models\Categoria;
use Illuminate\Database\Eloquent\Factories\Factory;
class ProdutoFactory extends Factory
{
public function definition(): array
{
return [
'categoria_id' => Categoria::factory(),
'nome' => $this->faker->words(3, true),
'preco' => $this->faker->randomFloat(2, 9.90, 2999.90),
'estoque' => $this->faker->numberBetween(0, 100),
'ativo' => $this->faker->boolean(80), // 80% chance de true
'descricao' => $this->faker->paragraph(),
];
}
// States — variações da factory
public function esgotado(): static
{
return $this->state(['estoque' => 0, 'ativo' => false]);
}
public function premium(): static
{
return $this->state(['preco' => $this->faker->randomFloat(2, 1000, 9999)]);
}
}
// Uso nos testes:
$produto = Produto::factory()->create();
$produtos = Produto::factory()->count(20)->create();
$esgotado = Produto::factory()->esgotado()->create();
// Seeder:
class DatabaseSeeder extends Seeder
{
public function run(): void
{
Categoria::factory()->count(10)->create();
Produto::factory()->count(50)->create();
}
}
Resumo
| Recurso | Como funciona |
|---|---|
| Migrations | Schema versionado — up() aplica, down() reverte |
| Fillable | Define campos aceitos em mass assignment |
| Casts | Converte tipos automaticamente ao ler/escrever |
| Scopes | Queries reutilizáveis encapsuladas no Model |
| Eager loading | with() previne o problema N+1 |
| Soft deletes | deleted_at em vez de DELETE — dados recuperáveis |
| Factories | Geração de dados falsos para testes |
Exercício da semana
-
Crie migrations para:
usuarios,categorias,produtos,pedidos,pedido_produto(pivot). UseforeignId()->constrained(),softDeletes(),timestamps()e índices apropriados. -
Implemente o relacionamento many-to-many entre
PedidoeProdutocomwithPivot(['quantidade', 'preco_unitario']). Escreva código que cria um pedido, adiciona produtos viaattach(), e calcula o total iterando sobre$pedido->produtos. -
Crie factories para todos os models. Use states:
Produto::factory()->esgotado(),Pedido::factory()->cancelado(). Crie um seeder que gera dados realistas para o ambiente de desenvolvimento. -
Desafio: implemente o Observer Pattern com Eloquent Observers:
php artisan make:observer ProdutoObserver. Ao criar um produto, gere o slug automaticamente. Ao deletar (soft delete), desative todos os itens de pedidos pendentes que contêm esse produto.
Referências
- Eloquent ORM — Documentação Laravel: https://laravel.com/docs/eloquent
- Eloquent Relationships: https://laravel.com/docs/eloquent-relationships
- Database Migrations: https://laravel.com/docs/migrations
- Laravel Factories: https://laravel.com/docs/eloquent-factories