PHP

Artigo 42 — Eloquent ORM com Laravel Já leu

6 min de leitura

Artigo 42 — Eloquent ORM com Laravel
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

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

  1. Crie migrations para: usuarios, categorias, produtos, pedidos, pedido_produto (pivot). Use foreignId()->constrained(), softDeletes(), timestamps() e índices apropriados.

  2. Implemente o relacionamento many-to-many entre Pedido e Produto com withPivot(['quantidade', 'preco_unitario']). Escreva código que cria um pedido, adiciona produtos via attach(), e calcula o total iterando sobre $pedido->produtos.

  3. 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.

  4. 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

Comentários

Mais em PHP

Orientação a Objetos: Fundamentos
Orientação a Objetos: Fundamentos

Orienta&ccedil;&atilde;o a Objetos &eacute; o paradigma que organiza o c&oacu...

Artigo 40 — Boas Práticas e Código Limpo em PHP
Artigo 40 — Boas Práticas e Código Limpo em PHP

Boas práticas não são regras arbitrárias inventadas por puristas. São soluçõe...

PSR e Padrões de Código
PSR e Padrões de Código

PSR significa PHP Standards Recommendation — recomendações criadas pelo PHP-F...