Collection

Operasi map, filter, dan groupBy pada hasil query Eloquent.

Daftar isi

Hasil query Eloquent yang mengembalikan banyak baris tidak berupa array biasa, melainkan objek collection dengan puluhan metode pemrosesan yang siap digunakan. Topik ini membahas metode map, filter, reject, groupBy, sum, pluck, dan contains pada hasil query model Note, beserta perbedaan singkat antara Collection dasar dan Eloquent Collection. Seluruh contoh dijalankan melalui Tinker dengan dua kategori dan lima catatan deterministik, sehingga setiap keluaran dapat direproduksi ulang secara persis.

Collection sebagai hasil query#

Metode get() dan all() pada Eloquent mengembalikan objek Illuminate\Database\Eloquent\Collection. Kelas ini turunan dari Illuminate\Support\Collection, sehingga seluruh metode pemrosesan collection dasar tersedia, ditambah beberapa metode khusus model. Objeknya dapat diiterasi dengan foreach seperti array, dihitung dengan count(), dan metode-metodenya dapat dirantai dalam satu pernyataan. Meskipun perilakunya mirip array, isinya tetap objek PHP, sehingga fungsi bawaan array tidak menerima collection sebagai argumennya dan pemrosesannya menggunakan metode collection.

bash
# menyiapkan data uji lalu memeriksa kelas hasil get()
cd ~/aplikasi-catatan
php artisan tinker --execute='
use App\Models\Category;
use App\Models\Note;
$musik = Category::firstOrCreate(["nama" => "Musik"]);
$rumah = Category::firstOrCreate(["nama" => "Rumah"]);
Note::where("category_id", $musik->id)->delete();
Note::where("category_id", $rumah->id)->delete();
foreach (["Daftar lagu untuk latihan gitar", "Ide playlist pagi", "Catatan latihan piano"] as $j) {
    Note::create(["judul" => $j, "isi" => "Isi catatan: " . $j . ".", "category_id" => $musik->id]);
}
foreach (["Daftar perlengkapan dapur", "Jadwal merawat tanaman"] as $j) {
    Note::create(["judul" => $j, "isi" => "Isi catatan: " . $j . ".", "category_id" => $rumah->id]);
}
$catatan = Note::whereIn("category_id", [$musik->id, $rumah->id])->orderBy("judul")->get();
echo get_class($catatan) . " (" . $catatan->count() . " model)" . PHP_EOL;
'
text Keluaran
Illuminate\Database\Eloquent\Collection (5 model)

Keluaran menunjukkan kelas hasil get() beserta jumlah modelnya. Data uji dibangun di awal blok dengan pola hapus lalu buat ulang, dan urutannya mengikuti orderBy("judul"). Pada bagian yang mengelompokkan data, nama kategori dipetakan dari category_id melalui array sederhana agar keluaran tidak bergantung pada nilai id yang berubah setiap eksekusi.

map, filter, dan reject#

Metode map() menjalankan fungsi pada setiap elemen lalu mengembalikan collection baru berisi hasilnya, cocok untuk membentuk tampilan ringkas dari sekumpulan model. Metode filter() menyaring elemen yang lolos uji fungsi yang diberikan, sedangkan reject() adalah kebalikannya, yaitu menyimpan elemen yang justru ditolak oleh fungsi tersebut. Ketiganya tidak mengubah collection aslinya, sehingga hasilnya ditampung ke variabel untuk digunakan pada langkah berikutnya.

bash
# mengubah, menyaring, dan menolak elemen collection
cd ~/aplikasi-catatan
php artisan tinker --execute='
use App\Models\Category;
use App\Models\Note;
$musik = Category::firstOrCreate(["nama" => "Musik"]);
$rumah = Category::firstOrCreate(["nama" => "Rumah"]);
Note::where("category_id", $musik->id)->delete();
Note::where("category_id", $rumah->id)->delete();
foreach (["Daftar lagu untuk latihan gitar", "Ide playlist pagi", "Catatan latihan piano"] as $j) {
    Note::create(["judul" => $j, "isi" => "Isi catatan: " . $j . ".", "category_id" => $musik->id]);
}
foreach (["Daftar perlengkapan dapur", "Jadwal merawat tanaman"] as $j) {
    Note::create(["judul" => $j, "isi" => "Isi catatan: " . $j . ".", "category_id" => $rumah->id]);
}
$catatan = Note::whereIn("category_id", [$musik->id, $rumah->id])->orderBy("judul")->get();
$nama = [$musik->id => "Musik", $rumah->id => "Rumah"];
$ringkas = $catatan->map(fn ($n) => $nama[$n->category_id] . ": " . $n->judul);
echo $ringkas->implode(" | ") . PHP_EOL;
$awalD = $catatan->filter(fn ($n) => str_starts_with($n->judul, "Daftar"));
echo $awalD->pluck("judul")->implode(" | ") . PHP_EOL;
$selainD = $catatan->reject(fn ($n) => str_starts_with($n->judul, "Daftar"));
echo $selainD->pluck("judul")->implode(" | ") . PHP_EOL;
'
text Keluaran
Musik: Catatan latihan piano | Musik: Daftar lagu untuk latihan gitar | Rumah: Daftar perlengkapan dapur | Musik: Ide playlist pagi | Rumah: Jadwal merawat tanaman
Daftar lagu untuk latihan gitar | Daftar perlengkapan dapur
Catatan latihan piano | Ide playlist pagi | Jadwal merawat tanaman

Baris pertama adalah hasil map() yang menggabungkan nama kategori dan judul, sedangkan dua baris berikutnya berpasangan: filter() menyisakan dua judul berawalan “Daftar”, dan reject() menyisakan tiga judul sisanya. Collection asli $catatan tetap berisi lima model setelah ketiga metode itu dijalankan, karena setiap metode mengembalikan collection baru.

groupBy dan sum#

Metode groupBy() mengelompokkan elemen menurut kunci yang ditentukan, baik berupa nama atribut maupun hasil fungsi. Keluarannya adalah collection baru yang tiap elemennya berupa collection berisi anggota grupnya, sehingga perhitungan lanjutan seperti count() dijalankan pada tiap grup. Metode sum() menjumlahkan nilai seluruh elemen dan sering dipadukan dengan map(): model diubah menjadi angka terlebih dahulu, misalnya jumlah kata pada setiap judul, sebelum dijumlahkan.

bash
# mengelompokkan catatan per kategori lalu menjumlahkan kata judul
cd ~/aplikasi-catatan
php artisan tinker --execute='
use App\Models\Category;
use App\Models\Note;
$musik = Category::firstOrCreate(["nama" => "Musik"]);
$rumah = Category::firstOrCreate(["nama" => "Rumah"]);
Note::where("category_id", $musik->id)->delete();
Note::where("category_id", $rumah->id)->delete();
foreach (["Daftar lagu untuk latihan gitar", "Ide playlist pagi", "Catatan latihan piano"] as $j) {
    Note::create(["judul" => $j, "isi" => "Isi catatan: " . $j . ".", "category_id" => $musik->id]);
}
foreach (["Daftar perlengkapan dapur", "Jadwal merawat tanaman"] as $j) {
    Note::create(["judul" => $j, "isi" => "Isi catatan: " . $j . ".", "category_id" => $rumah->id]);
}
$catatan = Note::whereIn("category_id", [$musik->id, $rumah->id])->orderBy("judul")->get();
$nama = [$musik->id => "Musik", $rumah->id => "Rumah"];
foreach ($catatan->groupBy(fn ($n) => $nama[$n->category_id]) as $namaGrup => $grup) {
    echo $namaGrup . ": " . $grup->count() . " catatan" . PHP_EOL;
}
echo "Total kata judul: " . $catatan->map(fn ($n) => str_word_count($n->judul))->sum() . PHP_EOL;
'
text Keluaran
Musik: 3 catatan
Rumah: 2 catatan
Total kata judul: 17

Dua baris pertama memperlihatkan isi groupBy(), yaitu dua grup yang masing-masing dihitung dengan count(). Baris ketiga menunjukkan rantai map() dan sum(): setiap judul diubah menjadi jumlah katanya dengan str_word_count(), lalu kelima angka itu dijumlahkan menjadi 17.

pluck dan contains#

Metode pluck("kolom") mengambil satu atribut dari setiap model dan mengembalikannya sebagai collection nilai sederhana, mirip perannya pada query builder tetapi bekerja pada model yang sudah berada di memori. Metode contains() memeriksa keberadaan data dan mengembalikan nilai boolean. Bentuk dua argumen seperti contains("judul", nilai) memeriksa kesetaraan atribut, sedangkan bentuk satu argumen pada Eloquent Collection dibaca sebagai pencarian primary key, sehingga bentuk itu tidak tepat untuk atribut biasa.

bash
# menarik satu kolom lalu memeriksa keberadaan data
cd ~/aplikasi-catatan
php artisan tinker --execute='
use App\Models\Category;
use App\Models\Note;
$musik = Category::firstOrCreate(["nama" => "Musik"]);
$rumah = Category::firstOrCreate(["nama" => "Rumah"]);
Note::where("category_id", $musik->id)->delete();
Note::where("category_id", $rumah->id)->delete();
foreach (["Daftar lagu untuk latihan gitar", "Ide playlist pagi", "Catatan latihan piano"] as $j) {
    Note::create(["judul" => $j, "isi" => "Isi catatan: " . $j . ".", "category_id" => $musik->id]);
}
foreach (["Daftar perlengkapan dapur", "Jadwal merawat tanaman"] as $j) {
    Note::create(["judul" => $j, "isi" => "Isi catatan: " . $j . ".", "category_id" => $rumah->id]);
}
$catatan = Note::whereIn("category_id", [$musik->id, $rumah->id])->orderBy("judul")->get();
echo $catatan->pluck("judul")->implode(" | ") . PHP_EOL;
var_dump($catatan->contains("judul", "Ide playlist pagi"));
var_dump($catatan->contains("judul", "Resep kue basah"));
'
text Keluaran
Catatan latihan piano | Daftar lagu untuk latihan gitar | Daftar perlengkapan dapur | Ide playlist pagi | Jadwal merawat tanaman
bool(true)
bool(false)

pluck() mengembalikan lima judul dalam urutan yang sama dengan collection aslinya. Dua baris terakhir memperlihatkan contains() dengan var_dump(): judul yang ada menghasilkan bool(true), sedangkan judul yang tidak ada menghasilkan bool(false).

Perbedaan Collection dan Eloquent Collection#

Eloquent Collection adalah turunan langsung dari Collection dasar, sehingga keduanya dapat dibedakan melalui get_class() dan diperiksa keberadaan metodenya. Kelas Eloquent menambahkan metode khusus model, misalnya modelKeys() yang mengembalikan daftar primary key seluruh model, serta versi find() yang mencari model menurut primary key, sedangkan find() pada Collection dasar mencari nilai biasa. Metode transformasi umum seperti map() justru mengembalikan Collection dasar karena hasilnya sudah bukan sekumpulan model.

bash
# membandingkan kelas dan metode kedua jenis collection
cd ~/aplikasi-catatan
php artisan tinker --execute='
use App\Models\Category;
use App\Models\Note;
use Illuminate\Support\Collection;
$musik = Category::firstOrCreate(["nama" => "Musik"]);
$rumah = Category::firstOrCreate(["nama" => "Rumah"]);
Note::where("category_id", $musik->id)->delete();
Note::where("category_id", $rumah->id)->delete();
foreach (["Daftar lagu untuk latihan gitar", "Ide playlist pagi", "Catatan latihan piano"] as $j) {
    Note::create(["judul" => $j, "isi" => "Isi catatan: " . $j . ".", "category_id" => $musik->id]);
}
foreach (["Daftar perlengkapan dapur", "Jadwal merawat tanaman"] as $j) {
    Note::create(["judul" => $j, "isi" => "Isi catatan: " . $j . ".", "category_id" => $rumah->id]);
}
$catatan = Note::whereIn("category_id", [$musik->id, $rumah->id])->orderBy("judul")->get();
$peta = $catatan->map(fn ($n) => $n->judul);
echo get_class($catatan) . PHP_EOL;
echo get_class($peta) . PHP_EOL;
echo (method_exists($catatan, "modelKeys") ? "modelKeys ada" : "modelKeys tidak ada") . PHP_EOL;
echo (method_exists($peta, "modelKeys") ? "modelKeys ada" : "modelKeys tidak ada") . PHP_EOL;
$kunci = $catatan->first()->id;
echo $catatan->find($kunci)->judul . PHP_EOL;
'
text Keluaran
Illuminate\Database\Eloquent\Collection
Illuminate\Support\Collection
modelKeys ada
modelKeys tidak ada
Catatan latihan piano

Keluaran memperlihatkan dua kelas yang berbeda dari satu rantai metode: hasil get() tetap Eloquent Collection, sedangkan hasil map() menjadi Collection dasar. Metode modelKeys() hanya ada pada kelas pertama, dan find() pada baris terakhir mengembalikan model yang primary key-nya sama dengan $kunci.

Kesalahan umum#

Kesalahan pertama adalah memberikan collection kepada fungsi array bawaan PHP, misalnya array_map(fn ($n) => $n->judul, $catatan). PHP menampilkan error “array_map(): Argument #2 ($array) must be of type array, Illuminate\Database\Eloquent\Collection given” karena collection bukan array. Pemrosesannya dituliskan sebagai metode, yaitu $catatan->map(...), dan hasilnya tetap sama.

Kesalahan kedua adalah mengira map(), filter(), dan reject() mengubah collection aslinya. Ketiganya mengembalikan collection baru, sehingga pemanggilan $catatan->filter(...) tanpa menampung hasilnya membuat hasil itu hilang. Kebiasaan yang tepat adalah menampung hasilnya ke variabel baru, seperti $awalD pada contoh sebelumnya.

Kesalahan ketiga adalah memanggil contains() dengan satu argumen untuk memeriksa atribut, misalnya contains("judul"). Pada Eloquent Collection, satu argumen non-callable dibaca sebagai nilai primary key, sehingga pemanggilan itu membandingkan id setiap model dengan teks “judul” dan mengembalikan false meskipun atribut judul ada pada model-modelnya. Pemeriksaan atribut memerlukan dua argumen, yaitu contains("judul", nilai).

Ringkasan#

  • get() dan all() mengembalikan Illuminate\Database\Eloquent\Collection, yaitu turunan Collection dasar yang diiterasi seperti array dan diproses dengan metode collection.
  • map(), filter(), dan reject() membentuk collection baru tanpa mengubah yang asli, sedangkan groupBy() menghasilkan collection berisi grup dan sum() menjumlahkan nilai.
  • pluck() menarik satu atribut menjadi collection nilai, sedangkan contains() memeriksa keberadaan data dengan bentuk dua argumen untuk atribut dan satu argumen untuk primary key.
  • Eloquent Collection menambahkan metode khusus model seperti modelKeys() dan versi find() yang mencari menurut primary key, sedangkan metode transformasi seperti map() mengembalikan Collection dasar.

Referensi#