Aplikasi yang tumbuh akan menumbuhkan query yang mirip di banyak tempat: daftar post populer di halaman depan, widget sidebar, sampai laporan mingguan semuanya membutuhkan where('views', '>=', 100). Ketika ambang popularitas berubah dari 100 menjadi 200, kita harus memburu setiap angka itu di banyak controller — dan pasti ada yang terlewat. Query scope menyelesaikan masalah ini dengan membungkus kondisi query ke dalam method di model, lalu memberinya nama yang bisa dipanggil berantai seperti method Eloquent lain. Chapter ini membahas local scope, dynamic scope berparameter, dan konvensi penamaannya. Contoh tetap memakai tabel posts kita: dua post milik Budi dan satu post milik Siti.

Local Scope: scopePopular #

Local scope adalah method di model yang namanya diawali scope, menerima instance Builder, lalu mengembalikan Builder yang sudah disisipi kondisi. Berikut dua scope untuk model Post kita:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    protected $fillable = ['user_id', 'judul', 'slug', 'isi', 'views', 'harga', 'published', 'meta'];

    public function scopePopular(Builder $query): Builder
    {
        return $query->where('views', '>=', 100);
    }

    public function scopePublished(Builder $query): Builder
    {
        return $query->where('published', true);
    }
}

Mari kita bedah anatomi kedua method tersebut:

  1. Prefiks scope — Eloquent memakai prefiks ini untuk mengenali method sebagai scope; sisanya menjadi nama panggilannya. scopePopular() dipanggil sebagai Post::popular().
  2. Parameter Builder $query — saat popular() dipanggil, Laravel otomatis menyuntikkan instance query builder yang sedang berjalan sebagai argumen pertama; kita tidak pernah mengirimnya secara manual.
  3. return $query->where(...) — scope hanya menambahkan kondisi lalu mengembalikan builder, sehingga hasilnya tetap bisa dirantai dengan orderBy(), where(), maupun scope lain.

Pemakaiannya terasa seperti method Eloquent bawaan:

php
foreach (Post::popular()->orderBy('views')->get() as $post) {
    echo ' ', $post->judul, ' (views=', $post->views, ")\n";
}

Output yang Dihasilkan:

text
 Belajar Eloquent (views=150)
 Tips Query Builder (views=320)

Hanya post dengan views minimal 100 yang tampil — draft Latihan Migration (20 views) tersaring meski milik Budi yang sama. Sekarang bila ambang popularitas berubah, kita menyunting satu angka di satu tempat.

SQL yang Benar-Benar Dibentuk #

Scope bukan sihir — ia cuma pengganti penulisan. Method toRawSql() memperlihatkan SQL final yang dibentuk, lengkap dengan nilai binding yang sudah dimasukkan:

php
echo Post::popular()->toRawSql(), "\n";

Output yang Dihasilkan:

text
select * from "posts" where "views" >= 100

Persis query yang akan kita tulis manual. Perbedaannya murni soal pengorganisasian kode, bukan performa.

Merantai Beberapa Scope #

Kekuatan scope baru terasa saat dirantai. Kita bisa menggabungkan published() dan popular() untuk mendapatkan post yang terbit sekaligus ramai dibaca:

php
foreach (Post::published()->popular()->orderByDesc('views')->get() as $post) {
    echo ' ', $post->judul, ' | published=', $post->published ? 'true' : 'false',
        ' | views=', $post->views, "\n";
}

Output yang Dihasilkan:

text
 Tips Query Builder | published=true | views=320
 Belajar Eloquent | published=true | views=150

Urutan perantai bebas — published()->popular() dan popular()->published() menghasilkan SQL yang sama. Kondisi where dari kedua scope digabungkan dengan AND, lalu orderByDesc('views') mengurutkan hasil akhirnya. Dari sini terlihat pola komposisinya: setiap scope menambahkan satu fragmen query, dan controller tinggal merangkai fragmen yang dibutuhkan layaknya blok bangunan.

Dynamic Scope dengan Parameter #

Local scope di atas selalu memakai ambang tetap. Dynamic scope menambahkan parameter setelah $query — nilai yang bisa dikirim pemanggil setiap kali scope dipakai. Scope berikut menyaring post milik satu penulis:

php
public function scopeOfUser(Builder $query, User $user): Builder
{
    return $query->where('user_id', $user->id);
}

Argumen kedua dan setelahnya diteruskan Laravel apa adanya, jadi pemanggilan ofUser($budi) menghasilkan where('user_id', 1):

php
$budi = User::where('email', '[email protected]')->first();
$siti = User::where('email', '[email protected]')->first();

echo 'Post milik Budi: ', Post::ofUser($budi)->count(), "\n";
echo 'Post milik Siti: ', Post::ofUser($siti)->count(), "\n";

foreach (Post::ofUser($budi)->published()->get() as $post) {
    echo ' Budi yang published: ', $post->judul, "\n";
}

Output yang Dihasilkan:

text
Post milik Budi: 2
Post milik Siti: 1
 Budi yang published: Belajar Eloquent

Dari dua post milik Budi, hanya Belajar Eloquent yang lolos rantai ofUser($budi)->published() — draft Latihan Migration tersaring. Dokumentasi Eloquent memakai pola yang sama dengan scopeOfType($query, $type) untuk kolom kategori; bentuknya selalu scope + nama + $query di depan + parameter sesudahnya. Nama scope yang diawali “of” (ofUser, ofType) adalah konvensi yang jamak untuk scope berparameter karena terbaca alami: “post of user ini”.

Local Scope vs Global Scope #

Satu istilah lain yang akan kita temui adalah global scope — kondisi yang otomatis menempel pada setiap query model tanpa perlu dipanggil, didaftarkan lewat method booted() di model. Perbedaan keduanya ringkas:

AspekLocal scopeGlobal scope
Cara aktifdipanggil eksplisit: Post::popular()otomatis di semua query
Cocok untukkondisi opsional yang dirangkai sesuai konteksaturan yang tak boleh dilanggar (mis. filter data per tenant)
Pembatalantidak perlu dibatalkanPost::withoutGlobalScope(...) bila harus dilepas

Dengan scope, logika query menemukan rumahnya di model. Ada satu kelompok kebutuhan lain yang juga milik model: mengubah nilai saat dibaca atau disimpan — misalnya mengubah judul menjadi kapital otomatis atau men-generate slug. Chapter berikutnya membahas accessor dan mutator.