Halaman single di sebuah custom theme yang saya bangun selalu terpotong di tengah jalan. Output-nya berhenti mendadak — markup yang belum tertutup, footer hilang, separuh konten lenyap. Yang bikin saya langsung curiga ke memori: ukuran byte respons-nya stabil persis sama di setiap load. Bukan acak, bukan kadang-kadang. Selalu mentok di titik yang sama. Itu pola klasik PHP yang kehabisan napas di tengah render, jadi saya buang waktu berjam-jam ngutak-ngatik memory_limit.
Di template itu saya menjalankan loop kedua untuk related posts, dan WP_Query-nya saya simpan di variabel $more. Di beberapa tempat lain saya juga pakai $page dan $id. Kira-kira begini bentuk pemicunya:
$more = new WP_Query( array(
'post_type' => 'listing',
'posts_per_page' => 4,
'post__not_in' => array( get_the_ID() ),
) );
while ( $more->have_posts() ) {
$more->the_post();
// render kartu related listing...
}
wp_reset_postdata();
// ... jauh di bawah, masih di template yang sama:
if ( $more->have_posts() ) {
// tampilkan "lihat semua"
}Baris kedua $more->have_posts() itulah yang mematikan halaman. Pesan fatal-nya:
Fatal error: Uncaught Error: Call to a member function have_posts() on intTapi pesan itu tidak pernah muncul di layar. Yang saya lihat cuma halaman yang terpotong rapi.
Kenapa ini terjadi
$more bukan nama variabel biasa. Itu global WordPress. WordPress memakainya untuk menandai apakah sebuah post sedang ditampilkan dalam mode "lihat selengkapnya" (terkait tag <!--more-->). Begitu loop saya memanggil the_post(), di baliknya WordPress menjalankan setup_postdata(), dan fungsi itu menulis ulang sederet global berdasarkan post yang sedang aktif — termasuk $more, yang di-set ke integer 0 atau 1.
Jadi inilah urutan kejadiannya: saya menaruh objek WP_Query di $more, loop berjalan normal, lalu di iterasi pertama setup_postdata() menimpa $more saya menjadi sebuah integer. Objek WP_Query-nya hilang ditelan global WordPress. Saat eksekusi sampai ke $more->have_posts() yang kedua, $more sudah bukan objek lagi — dia int. Memanggil method pada integer itulah yang memicu fatal "on int".
Lalu kenapa kelihatannya seperti out-of-memory? Karena fatal error PHP menghentikan eksekusi tepat di titik yang sama setiap kali. Render berjalan deterministik sampai ketemu baris beracun itu, lalu mati. Output ter-flush sampai posisi itu identik di tiap request, sehingga ukuran byte-nya stabil. Itu justru petunjuk terkuat bahwa ini fatal, bukan OOM — kehabisan memori cenderung mentok di ambang yang bergerak-gerak tergantung beban, bukan di byte yang sama persis berulang kali.
Perbaikannya
Diagnostik yang akhirnya membongkar kasus ini adalah register_shutdown_function() yang saya pasang dengan gerbang, untuk mencetak error_get_last() saat shutdown:
// Taruh sementara, digerbang supaya tidak bocor ke produksi/publik.
register_shutdown_function( function () {
if ( ! defined( 'WP_DEBUG' ) || ! WP_DEBUG ) {
return;
}
$err = error_get_last();
if ( $err && in_array( $err['type'], array( E_ERROR, E_PARSE, E_COMPILE_ERROR ), true ) ) {
error_log( sprintf(
'SHUTDOWN FATAL: %s in %s:%d',
$err['message'], $err['file'], $err['line']
) );
}
});Begitu shutdown handler ini menyala, ia langsung menyodorkan file:line asli plus pesan "Call to a member function have_posts() on int" yang sesungguhnya. Saat itu juga teori memori runtuh — ini fatal bertipe, bukan kehabisan memori.
Perbaikannya sederhana setelah penyebabnya kelihatan: jangan pernah memakai ulang nama global loop yang sudah dipesan WordPress. Saya ganti $more jadi $related_query (dan di tempat lain pakai $secondary_query):
$related_query = new WP_Query( array(
'post_type' => 'listing',
'posts_per_page' => 4,
'post__not_in' => array( get_the_ID() ),
) );
while ( $related_query->have_posts() ) {
$related_query->the_post();
// render kartu related listing...
}
wp_reset_postdata();
if ( $related_query->have_posts() ) {
// tampilkan "lihat semua"
}Karena $related_query bukan global yang disentuh setup_postdata(), objeknya tetap utuh sepanjang template. Tidak ada lagi integer menyamar jadi WP_Query, tidak ada lagi fatal, halaman ter-render penuh.
Daftar nama yang harus dihindari di dalam template — karena WordPress akan menulis ulang atau membayanginya lewat setup_postdata() — termasuk: $post, $more, $page, $pages, $numpages, $multipage, $id, dan $authordata. Untuk WP_Query sekunder, beri nama yang jelas dan tidak bertabrakan: $related_query, $secondary_query, $featured_query.
Pelajaran
Dua hal yang saya bawa pulang dari kasus ini. Pertama, truncation dengan ukuran stabil itu ciri fatal, bukan OOM. Kalau halaman selalu terpotong di byte yang sama persis tiap load, jangan langsung lari ke memory_limit. Pasang register_shutdown_function() yang digerbang dan baca error_get_last() — ia akan menunjukkan file, baris, dan pesan asli, sesuatu yang tidak akan Anda dapat dari layar yang cuma diam terpotong.
Kedua, global cadangan WordPress adalah bom waktu yang senyap di dalam template. Nama seperti $more, $page, dan $id terlihat tak berdosa dan generik, justru karena itu gampang dipakai tanpa pikir panjang. Tapi setup_postdata() akan menimpanya di tengah loop, dan variabel Anda berubah tipe diam-diam tanpa satu pun warning. Beri nama yang spesifik dan unik untuk query kedua Anda, maka seluruh kelas bug ini hilang sebelum sempat lahir.
