Chapter Query Scopes
mengajari kita membungkus kondisi query ke method scopePopular di model. Semakin banyak scope tumbuh, model berubah menjadi kumpulan query berjalan — dan sebagian scope seperti published(), recent(), byUser() sebenarnya adalah bahasa domain aplikasi, bukan detail model. Custom Eloquent builder memindahkan seluruh kosakata query itu ke satu kelas PostBuilder tersendiri, sehingga Post::popular()->recent()->get() menjadi API resmi aplikasi. Kita juga akan melihat bahwa builder kustom otomatis dipakai di semua pintu — query langsung, relasi, sampai eager loading
.
Class Builder Kustom #
Builder kustom adalah subclass dari Illuminate\Database\Eloquent\Builder:
<?php
namespace App\Builders;
use App\Models\User;
use Illuminate\Database\Eloquent\Builder;
/**
* @template TModel of \App\Models\Post
*
* @extends Builder<TModel>
*/
class PostBuilder extends Builder
{
public function popular(int $minViews = 100): static
{
return $this->where('views', '>=', $minViews)->orderByDesc('views');
}
public function recent(): static
{
return $this->whereNotNull('published_at')->orderByDesc('published_at');
}
public function byUser(User|int $user): static
{
return $this->where('user_id', $user instanceof User ? $user->id : $user);
}
}Dua hal membuat kelas ini enak dipakai. Setiap method mengembalikan $this bertipe static, sehingga rantai pemanggilan tidak pernah kehilangan tipe PostBuilder — autocomplete editor tetap menawarkan popular() setelah recent(). Docblock @extends Builder<TModel> menjaga generic type sehingga ->get() tetap dikenal mengembalikan koleksi Post, bukan Model serba bisa.
Menghubungkan Builder ke Model #
Model menunjuk builder-nya lewat method newEloquentBuilder():
<?php
// app/Models/Post.php — bagian inti
public function newEloquentBuilder($query): PostBuilder
{
return new PostBuilder($query);
}Laravel 13 menambahkan cara deklaratif: attribute #[UseEloquentBuilder] di atas kelas model — keduanya setara:
<?php
// app/Models/Post.php — alternatif di Laravel 13
#[UseEloquentBuilder(PostBuilder::class)]
class Post extends Model
{
// ...
}Attribute ini butuh use Illuminate\Database\Eloquent\Attributes\UseEloquentBuilder; di bagian import. Model dengan builder kustom tetap kompatibel penuh dengan seluruh fitur Eloquent — yang berubah hanya kelas di balik Post::query().
Bahasa Query Domain #
Dengan koneksi di atas, pemanggilan query berubah menjadi kalimat domain. Mengingat data kita — Budi punya tiga post dan Siti dua post di tabel posts:
<?php
foreach (Post::popular()->recent()->get() as $post) {
echo "{$post->title} ({$post->views} views)".PHP_EOL;
}Output yang Dihasilkan:
Belajar Laravel 13 (320 views)
Rahasia Eloquent (145 views)Filter per penulis menerima objek User maupun id:
<?php
foreach (Post::byUser($budi)->get() as $post) {
echo $post->title.PHP_EOL;
}Output yang Dihasilkan:
Belajar Laravel 13
Rahasia Eloquent
Migration Tanpa DramaBuilder kustom juga otomatis mengalir ke relasi, karena HasMany membangun query lewat builder model yang sama:
<?php
echo 'builder query: '.get_class($siti->posts()->getQuery()).PHP_EOL;
echo 'posts milik Siti yang popular: '.$siti->posts()->popular()->count().PHP_EOL;Output yang Dihasilkan:
builder query: App\Builders\PostBuilder
posts milik Siti yang popular: 0Untuk kondisi opsional — misalnya filter hanya aktif bila request membawa parameter — builder mewarisi when() dan unless() dari trait Conditionable, sama seperti query biasa:
<?php
$minViews = 100;
$posts = Post::query()
->when($minViews >= 100, fn ($q) => $q->popular($minViews))
->unless($minViews >= 500, fn ($q) => $q->where('views', '<=', 400))
->orderByDesc('views')
->get();Method when() menjalankan closure pertama bila kondisi benar; unless() sebaliknya, menjalankan closure saat kondisi salah — keduanya menghilangkan if bertingkat di controller.
Builder atau Scope #
Keduanya memecahkan masalah yang sama dengan biaya berbeda:
| Aspek | Query scope | Custom builder |
|---|---|---|
| Tempat kode | Method di model (scopePopular) | Class terpisah (PostBuilder) |
| Penamaan pemanggilan | Post::popular() (prefix scope disingkirkan) | Post::popular() — sama |
| Ukuran wajar | 3–10 method | 10+ method atau satu domain query |
| Type di editor | Hilang setelah scope pertama | Tetap PostBuilder di sepanjang rantai |
| Dipakai lintas model | Tidak, per model | Ya, bila builder memang dimaksudkan lintas model |
Praktik yang sehat: mulai dengan scope untuk satu-dua kondisi, lalu angkat seluruhnya menjadi builder ketika daftar scope melewati sepuluh atau method-nya butuh tipe kembalian yang kuat.
Macro untuk Satu Method Saja #
Kadang kita hanya butuh satu method ekstra pada semua query — misalnya pencarian LIKE untuk kolom apa pun. Alih-alih membuat builder kustom untuk satu method, daftarkan macro pada Illuminate\Database\Eloquent\Builder di method boot() milik AppServiceProvider:
<?php
Builder::macro('whereLike', function (string $column, string $keyword): Builder {
/** @var Builder $this */
return $this->where($column, 'LIKE', "%{$keyword}%");
});Macro langsung tersedia di semua model, tanpa subclass:
<?php
echo Post::whereLike('title', 'Laravel')->toRawSql().PHP_EOL;Output yang Dihasilkan:
select * from "posts" where "title" LIKE '%Laravel%'Mekanisme macro ini akan kita bedah tuntas di chapter Macros .
Menguji Builder dalam Isolasi #
Test builder tidak membutuhkan baris data sama sekali — method toRawSql() merakit string SQL final tanpa menyentuh database. Gabungkan dengan test berdata untuk perilaku nyata:
Output yang Dihasilkan:
PASS Tests\Feature\PostBuilderTest
✓ sql builder bisa diuji tanpa database 0.13s
✓ popular menyaring dan mengurutkan postingan 0.02s
✓ by user menerima model atau id 0.01s
✓ where like macro dari provider 0.01s
Tests: 4 passed (6 assertions)
Duration: 0.25sTest pertama cukup mengandalkan assertStringContainsString('"views" >= 150', Post::popular(150)->recent()->toRawSql()) — assertion pada SQL mentah, tanpa migrasi dan tanpa factory. Test berikutnya menyemai data lewat factory lalu memastikan hasil get() dan pluck() sesuai. Kombinasi ini membuat kegagalan mudah dilacak: test SQL menangkap kesalahan kondisi, test data menangkap kesalahan logika.
PostBuilder kita kini menjadi kamus resmi untuk semua query post. Macro whereLike di atas memberi petunjuk mekanisme yang lebih umum — kemampuan menambah method ke class framework mana pun pada saat runtime. Chapter berikutnya membuka kap mesinnya.