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
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:
- Prefiks
scope— Eloquent memakai prefiks ini untuk mengenali method sebagai scope; sisanya menjadi nama panggilannya.scopePopular()dipanggil sebagaiPost::popular(). - Parameter
Builder $query— saatpopular()dipanggil, Laravel otomatis menyuntikkan instance query builder yang sedang berjalan sebagai argumen pertama; kita tidak pernah mengirimnya secara manual. return $query->where(...)— scope hanya menambahkan kondisi lalu mengembalikan builder, sehingga hasilnya tetap bisa dirantai denganorderBy(),where(), maupun scope lain.
Pemakaiannya terasa seperti method Eloquent bawaan:
foreach (Post::popular()->orderBy('views')->get() as $post) {
echo ' ', $post->judul, ' (views=', $post->views, ")\n";
}Output yang Dihasilkan:
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:
echo Post::popular()->toRawSql(), "\n";Output yang Dihasilkan:
select * from "posts" where "views" >= 100Persis 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:
foreach (Post::published()->popular()->orderByDesc('views')->get() as $post) {
echo ' ', $post->judul, ' | published=', $post->published ? 'true' : 'false',
' | views=', $post->views, "\n";
}Output yang Dihasilkan:
Tips Query Builder | published=true | views=320
Belajar Eloquent | published=true | views=150Urutan 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:
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):
$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:
Post milik Budi: 2
Post milik Siti: 1
Budi yang published: Belajar EloquentDari 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:
| Aspek | Local scope | Global scope |
|---|---|---|
| Cara aktif | dipanggil eksplisit: Post::popular() | otomatis di semua query |
| Cocok untuk | kondisi opsional yang dirangkai sesuai konteks | aturan yang tak boleh dilanggar (mis. filter data per tenant) |
| Pembatalan | tidak perlu dibatalkan | Post::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.