AJAX ile WordPress Arka Plan İstek Yönetimi

WordPress eklenti geliştirirken en can sıkıcı anlardan biri şudur: Kullanıcı bir butona tıklar, sayfa donar, birkaç saniye bekler ve sonuç gelir. Bu deneyim 2008’de belki kabul edilebilirdi, ama artık değil. AJAX kullanarak bu sorunu çözmek hem kullanıcı deneyimini kökten değiştirir hem de arka plan işlemlerini çok daha esnek bir şekilde yönetmenizi sağlar. Bu yazıda WordPress’in kendi AJAX altyapısını kullanarak nasıl sağlam, güvenli ve performanslı arka plan istek sistemi kurulacağını adım adım ele alacağız.

WordPress AJAX’ın Çalışma Mantığı

WordPress’in AJAX sistemi admin-ajax.php dosyası üzerine kuruludur. Tüm AJAX istekleri bu dosyaya gönderilir ve WordPress, gelen action parametresine göre hangi PHP fonksiyonunun çalışacağını belirler. Bu mekanizma hem yetkili (logged-in) hem de yetkisiz (nopriv) kullanıcılar için ayrı hook’larla çalışır.

Pek çok geliştirici REST API’yi tercih etmeye başlamış olsa da, wp-ajax sistemi hâlâ bazı senaryolarda çok daha pratiktir. Özellikle admin paneli işlemleri, basit veri doğrulama ve küçük ölçekli arka plan görevleri için admin-ajax.php yeterli ve yönetimi kolaydır.

Temel akış şu şekilde işler:

  • Kullanıcı tarafında JavaScript bir istek gönderir
  • İstek admin-ajax.php‘ye ulaşır
  • WordPress ilgili wp_ajax_ hook’unu tetikler
  • PHP fonksiyonu çalışır ve wp_send_json() veya echo ile yanıt döner
  • JavaScript yanıtı işler

Temel Yapı: İlk AJAX İsteğinizi Kurun

Önce eklentinizin temel iskelesini oluşturalım. Basit ama gerçek dünyada kullanılabilir bir örnek üzerinden gidelim: Kullanıcının admin panelinde bir butona tıkladığında uzak bir API’den veri çekip, bunu veritabanına kaydeden bir sistem.

<?php
/**
 * Plugin Name: Örnek AJAX Yöneticisi
 * Description: AJAX ile arka plan istek yönetimi örneği
 * Version: 1.0.0
 */

if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

class Ornek_Ajax_Yoneticisi {

    public function __construct() {
        add_action( 'wp_enqueue_scripts', [ $this, 'scriptleri_yukle' ] );
        add_action( 'admin_enqueue_scripts', [ $this, 'admin_scriptleri_yukle' ] );

        // Yetkili kullanıcılar için
        add_action( 'wp_ajax_veri_guncelle', [ $this, 'veri_guncelle_isle' ] );

        // Yetkisiz kullanıcılar için (misafir ziyaretçiler)
        add_action( 'wp_ajax_nopriv_veri_guncelle', [ $this, 'veri_guncelle_isle' ] );
    }

    public function admin_scriptleri_yukle( $hook ) {
        // Sadece ilgili sayfada yükle
        if ( 'toplevel_page_ornek-ajax' !== $hook ) {
            return;
        }

        wp_enqueue_script(
            'ornek-ajax-js',
            plugin_dir_url( __FILE__ ) . 'assets/js/ajax-handler.js',
            [ 'jquery' ],
            '1.0.0',
            true
        );

        wp_localize_script(
            'ornek-ajax-js',
            'ornekAjax',
            [
                'ajaxurl' => admin_url( 'admin-ajax.php' ),
                'nonce'   => wp_create_nonce( 'veri_guncelle_nonce' ),
                'mesajlar' => [
                    'yukleniyor' => __( 'İşleniyor...', 'ornek-ajax' ),
                    'basarili'   => __( 'Güncelleme tamamlandı!', 'ornek-ajax' ),
                    'hata'       => __( 'Bir hata oluştu.', 'ornek-ajax' ),
                ],
            ]
        );
    }
}

new Ornek_Ajax_Yoneticisi();

Burada dikkat edilmesi gereken iki kritik nokta var. Birincisi, wp_localize_script() ile JavaScript’e PHP değişkenlerini güvenli bir şekilde aktarıyoruz. İkincisi, $hook parametresiyle script’i sadece gerekli sayfada yüklüyoruz. Her sayfaya script yüklemek gereksiz HTTP isteği demektir.

Nonce Doğrulaması: Güvenliği Atlamamak

Bu konuda çok ciddi davranmak gerekiyor. Nonce doğrulaması olmayan bir AJAX endpoint’i açık bir kapı bırakmak gibidir. CSRF saldırılarına karşı en temel savunma mekanizmasıdır.

public function veri_guncelle_isle() {
    // Nonce kontrolü - bu satırı asla atlama
    if ( ! check_ajax_referer( 'veri_guncelle_nonce', 'nonce', false ) ) {
        wp_send_json_error(
            [ 'mesaj' => 'Güvenlik doğrulaması başarısız.' ],
            403
        );
    }

    // Yetki kontrolü
    if ( ! current_user_can( 'manage_options' ) ) {
        wp_send_json_error(
            [ 'mesaj' => 'Bu işlem için yetkiniz yok.' ],
            403
        );
    }

    // Gelen veriyi doğrula ve temizle
    $urun_id = isset( $_POST['urun_id'] ) ? absint( $_POST['urun_id'] ) : 0;

    if ( ! $urun_id ) {
        wp_send_json_error( [ 'mesaj' => 'Geçersiz ürün ID.' ] );
    }

    // İşlemi gerçekleştir
    $sonuc = $this->urun_verisini_guncelle( $urun_id );

    if ( is_wp_error( $sonuc ) ) {
        wp_send_json_error( [ 'mesaj' => $sonuc->get_error_message() ] );
    }

    wp_send_json_success( [
        'mesaj'   => 'Ürün başarıyla güncellendi.',
        'urun_id' => $urun_id,
        'zaman'   => current_time( 'mysql' ),
    ] );
}

check_ajax_referer() fonksiyonunun üçüncü parametresi olan false değeri önemlidir. Bu parametre true olsaydı, doğrulama başarısız olduğunda WordPress otomatik olarak die() çağırır ve hata yanıtı gönderemezdiniz. false ile kontrolü kendiniz yapıp düzgün bir JSON hata yanıtı döndürebilirsiniz.

JavaScript Tarafı: Temiz ve Yönetilebilir Kod

Frontend kısmını modüler yazmak, uzun vadede bakımı kolaylaştırır. jQuery kullanacağız çünkü WordPress zaten onu paketliyor, ama Vanilla JS ile de aynı mantığı uygulayabilirsiniz.

(function ($) {
    'use strict';

    const AjaxYoneticisi = {
        
        init: function () {
            this.olaylariDinle();
        },

        olaylariDinle: function () {
            $(document).on('click', '#veri-guncelle-btn', this.veriGuncelle.bind(this));
            $(document).on('click', '#toplu-islem-btn', this.topluIslem.bind(this));
        },

        veriGuncelle: function (e) {
            e.preventDefault();

            const $buton = $(e.currentTarget);
            const urunId = $buton.data('urun-id');

            if (!urunId) {
                this.bildirimGoster('error', 'Ürün ID bulunamadı.');
                return;
            }

            this.butonuDevre($buton, true);

            $.ajax({
                url: ornekAjax.ajaxurl,
                type: 'POST',
                data: {
                    action: 'veri_guncelle',
                    nonce: ornekAjax.nonce,
                    urun_id: urunId,
                },
                success: function (yanit) {
                    if (yanit.success) {
                        AjaxYoneticisi.bildirimGoster('success', yanit.data.mesaj);
                    } else {
                        AjaxYoneticisi.bildirimGoster('error', yanit.data.mesaj);
                    }
                },
                error: function (xhr, status, hata) {
                    AjaxYoneticisi.bildirimGoster(
                        'error',
                        ornekAjax.mesajlar.hata + ' (' + hata + ')'
                    );
                },
                complete: function () {
                    AjaxYoneticisi.butonuDevre($buton, false);
                },
            });
        },

        butonuDevre: function ($buton, devre) {
            $buton.prop('disabled', devre);
            $buton.text(devre ? ornekAjax.mesajlar.yukleniyor : $buton.data('orijinal-metin'));
        },

        bildirimGoster: function (tur, mesaj) {
            const $bildirim = $('<div>')
                .addClass('notice notice-' + (tur === 'success' ? 'success' : 'error'))
                .addClass('is-dismissible')
                .html('<p>' + mesaj + '</p>');

            $('.wrap h1').after($bildirim);

            setTimeout(function () {
                $bildirim.fadeOut(400, function () {
                    $(this).remove();
                });
            }, 5000);
        },
    };

    $(document).ready(function () {
        AjaxYoneticisi.init();
    });

})(jQuery);

Toplu İşlemler: Büyük Veri Setlerini Yönetmek

Gerçek projelerde sıkça karşılaştığım senaryo şudur: 5000 ürünü tek seferde işlemek zorundasınız ama bu istek zaman aşımına uğruyor. Çözüm, işi parçalara bölüp sıralı AJAX istekleriyle yönetmektir.

// PHP tarafı - toplu işlem handler'ı
public function toplu_islem_isle() {
    check_ajax_referer( 'toplu_islem_nonce', 'nonce' );

    if ( ! current_user_can( 'manage_options' ) ) {
        wp_send_json_error( [ 'mesaj' => 'Yetersiz yetki.' ], 403 );
    }

    $offset    = isset( $_POST['offset'] ) ? absint( $_POST['offset'] ) : 0;
    $limit     = 50; // Her seferinde 50 kayıt
    $toplam    = $this->toplam_kayit_say();

    $kayitlar = $this->kayitlari_getir( $offset, $limit );

    foreach ( $kayitlar as $kayit ) {
        $this->kayit_isle( $kayit );
    }

    $islenen    = $offset + count( $kayitlar );
    $devam_ediyor = $islenen < $toplam;

    wp_send_json_success( [
        'islenen'        => $islenen,
        'toplam'         => $toplam,
        'devam_ediyor'   => $devam_ediyor,
        'yuzde'          => round( ( $islenen / $toplam ) * 100 ),
        'sonraki_offset' => $devam_ediyor ? $islenen : null,
    ] );
}
// JavaScript tarafı - ilerleme çubuğuyla toplu işlem
topluIslem: function (e) {
    e.preventDefault();
    this.topluIslemiBaslat(0);
},

topluIslemiBaslat: function (offset) {
    $.ajax({
        url: ornekAjax.ajaxurl,
        type: 'POST',
        data: {
            action: 'toplu_islem',
            nonce: ornekAjax.nonce,
            offset: offset,
        },
        success: function (yanit) {
            if (!yanit.success) {
                AjaxYoneticisi.bildirimGoster('error', yanit.data.mesaj);
                return;
            }

            const veri = yanit.data;

            // İlerleme çubuğunu güncelle
            $('#ilerleme-cubugu').css('width', veri.yuzde + '%');
            $('#ilerleme-metin').text(
                veri.islenen + ' / ' + veri.toplam + ' kayıt işlendi'
            );

            if (veri.devam_ediyor) {
                // Kısa bir bekleme sonrası devam et
                setTimeout(function () {
                    AjaxYoneticisi.topluIslemiBaslat(veri.sonraki_offset);
                }, 200);
            } else {
                AjaxYoneticisi.bildirimGoster('success', 'Tüm kayıtlar işlendi!');
            }
        },
        error: function () {
            AjaxYoneticisi.bildirimGoster('error', 'İşlem sırasında hata oluştu.');
        },
    });
},

Bu yaklaşımın güzel yanı, sunucu zaman aşımı limitlerini aşmaması ve kullanıcıya gerçek zamanlı geri bildirim vermesidir. 200ms’lik bekleme, sunucunun nefes almasına izin verir.

WooCommerce ile Entegrasyon: Ürün Stok Kontrolü

WooCommerce projelerinde sık kullanılan bir senaryo: Kullanıcı sepete ürün eklemeden önce gerçek zamanlı stok kontrolü. Bu örnek hem frontend hem admin için çalışır.

add_action( 'wp_ajax_stok_kontrol', [ $this, 'stok_kontrol_isle' ] );
add_action( 'wp_ajax_nopriv_stok_kontrol', [ $this, 'stok_kontrol_isle' ] );

public function stok_kontrol_isle() {
    check_ajax_referer( 'stok_kontrol_nonce', 'nonce' );

    $urun_id  = isset( $_POST['urun_id'] ) ? absint( $_POST['urun_id'] ) : 0;
    $miktar   = isset( $_POST['miktar'] ) ? absint( $_POST['miktar'] ) : 1;

    if ( ! $urun_id ) {
        wp_send_json_error( [ 'mesaj' => 'Geçersiz ürün.' ] );
    }

    $urun = wc_get_product( $urun_id );

    if ( ! $urun || ! $urun->exists() ) {
        wp_send_json_error( [ 'mesaj' => 'Ürün bulunamadı.' ] );
    }

    $stok_miktari  = $urun->get_stock_quantity();
    $stok_durumu   = $urun->get_stock_status();
    $stok_yonetimi = $urun->managing_stock();

    $yeterli_stok = true;
    $stok_mesaji  = '';

    if ( $stok_yonetimi && $stok_miktari !== null ) {
        if ( $miktar > $stok_miktari ) {
            $yeterli_stok = false;
            $stok_mesaji  = sprintf(
                'Stokta sadece %d adet kaldı.',
                $stok_miktari
            );
        }
    } elseif ( 'outofstock' === $stok_durumu ) {
        $yeterli_stok = false;
        $stok_mesaji  = 'Bu ürün stokta yok.';
    }

    // Önbelleğe alma ile performansı artır
    $cache_key  = 'stok_kontrol_' . $urun_id;
    $cache_sure = 300; // 5 dakika

    wp_cache_set( $cache_key, [
        'yeterli'  => $yeterli_stok,
        'miktar'   => $stok_miktari,
        'durum'    => $stok_durumu,
    ], 'stok_kontrol', $cache_sure );

    wp_send_json_success( [
        'yeterli_stok' => $yeterli_stok,
        'mesaj'        => $stok_mesaji,
        'stok_miktari' => $stok_miktari,
    ] );
}

Hata Yönetimi ve Loglama

Production ortamında hata ayıklamak kabusa dönebilir. Özellikle AJAX isteklerinde, hangi isteğin nerede başarısız olduğunu takip etmek kritiktir. Basit ama etkili bir loglama sistemi ekleyelim:

private function ajax_log( $seviye, $mesaj, $context = [] ) {
    if ( ! defined( 'WP_DEBUG' ) || ! WP_DEBUG ) {
        return;
    }

    $log_girdisi = sprintf(
        '[%s] [%s] %s | Kullanıcı: %d | IP: %s | Action: %s',
        current_time( 'Y-m-d H:i:s' ),
        strtoupper( $seviye ),
        $mesaj,
        get_current_user_id(),
        $_SERVER['REMOTE_ADDR'] ?? 'bilinmiyor',
        $_POST['action'] ?? 'bilinmiyor'
    );

    if ( ! empty( $context ) ) {
        $log_girdisi .= ' | Veri: ' . wp_json_encode( $context );
    }

    error_log( $log_girdisi );

    // Kritik hatalar için veritabanına da kaydet
    if ( 'critical' === $seviye ) {
        $this->kritik_hata_kaydet( $mesaj, $context );
    }
}

private function kritik_hata_kaydet( $mesaj, $context ) {
    global $wpdb;

    $wpdb->insert(
        $wpdb->prefix . 'ajax_hata_log',
        [
            'mesaj'         => sanitize_text_field( $mesaj ),
            'context'       => wp_json_encode( $context ),
            'kullanici_id'  => get_current_user_id(),
            'olusturma_tar' => current_time( 'mysql' ),
        ],
        [ '%s', '%s', '%d', '%s' ]
    );
}

Performans: Önbellek ve Rate Limiting

Özellikle yoğun trafikli sitelerde, aynı AJAX isteğinin sürekli tekrarlanması sunucuyu yorabilir. İki katmanlı bir savunma mekanizması işe yarar: transient önbelleği ve rate limiting.

public function veri_guncelle_isle() {
    check_ajax_referer( 'veri_guncelle_nonce', 'nonce' );

    $kullanici_id = get_current_user_id();
    $urun_id      = absint( $_POST['urun_id'] ?? 0 );

    // Rate limiting: Kullanıcı başına dakikada 10 istek
    $rate_key   = 'ajax_rate_' . $kullanici_id;
    $istek_sayisi = (int) get_transient( $rate_key );

    if ( $istek_sayisi >= 10 ) {
        wp_send_json_error(
            [ 'mesaj' => 'Çok fazla istek gönderdiniz. Lütfen bir dakika bekleyin.' ],
            429
        );
    }

    set_transient( $rate_key, $istek_sayisi + 1, 60 );

    // Önbellekten kontrol et
    $cache_key    = 'urun_veri_' . $urun_id;
    $onbellekten  = get_transient( $cache_key );

    if ( false !== $onbellekten ) {
        wp_send_json_success( array_merge( $onbellekten, [ 'onbellekten' => true ] ) );
    }

    // Gerçek işlemi yap
    $sonuc = $this->urun_verisini_guncelle( $urun_id );

    if ( is_wp_error( $sonuc ) ) {
        $this->ajax_log( 'error', $sonuc->get_error_message(), [ 'urun_id' => $urun_id ] );
        wp_send_json_error( [ 'mesaj' => $sonuc->get_error_message() ] );
    }

    // Sonucu önbelleğe al (5 dakika)
    set_transient( $cache_key, $sonuc, 300 );

    wp_send_json_success( $sonuc );
}

Gerçek Dünya Senaryosu: Asenkron Rapor Oluşturma

Bir müşteri projesinde karşılaştığım durum: 50.000 satırlık satış raporu oluşturmak. Sayfa zaman aşımına uğruyordu. Çözüm, işi arka plana taşımak ve kullanıcıya hazır olduğunda bildirmekti.

// Rapor oluşturmayı başlat, hemen yanıt dön
public function rapor_olustur_baslat() {
    check_ajax_referer( 'rapor_nonce', 'nonce' );

    $rapor_id  = uniqid( 'rapor_', true );
    $baslangic = isset( $_POST['baslangic'] ) ? sanitize_text_field( $_POST['baslangic'] ) : '';
    $bitis     = isset( $_POST['bitis'] ) ? sanitize_text_field( $_POST['bitis'] ) : '';

    // İşi kuyruğa al
    update_option( 'rapor_' . $rapor_id, [
        'durum'     => 'bekliyor',
        'baslangic' => $baslangic,
        'bitis'     => $bitis,
        'olusturma' => time(),
    ] );

    // Arka plan işlemini schedule et
    wp_schedule_single_event( time(), 'rapor_olustur_arka_plan', [ $rapor_id ] );

    wp_send_json_success( [
        'rapor_id' => $rapor_id,
        'mesaj'    => 'Rapor oluşturma başlatıldı. Hazır olduğunda bildirim alacaksınız.',
    ] );
}

// Durum sorgulama endpoint'i
public function rapor_durumu_kontrol() {
    check_ajax_referer( 'rapor_nonce', 'nonce' );

    $rapor_id = sanitize_text_field( $_POST['rapor_id'] ?? '' );
    $rapor    = get_option( 'rapor_' . $rapor_id, false );

    if ( ! $rapor ) {
        wp_send_json_error( [ 'mesaj' => 'Rapor bulunamadı.' ] );
    }

    wp_send_json_success( $rapor );
}

Bu yaklaşımla kullanıcı anında yanıt alır, arka planda işlem devam eder. JavaScript tarafında basit bir polling mekanizmasıyla (her 3 saniyede bir rapor_durumu_kontrol çağırarak) kullanıcıya gerçek zamanlı ilerleme gösterebilirsiniz.

Sonuç

WordPress AJAX sistemi, doğru kullanıldığında hem güçlü hem de yönetilebilir bir altyapı sunar. Bu yazıda ele aldığımız yaklaşımları özetlersek:

  • Güvenlik birinci: Nonce doğrulaması ve yetki kontrolü asla atlanmamalı
  • Veri doğrulama: Gelen her parametre sanitize ve validate edilmeli
  • Hata yönetimi: wp_send_json_error() ve wp_send_json_success() tutarlı kullanılmalı
  • Performans: Rate limiting ve transient önbelleği yoğun sitelerde zorunlu
  • Toplu işlemler: Büyük veri setleri parçalara bölünmeli, tek istekte işlenmeye çalışılmamalı
  • Loglama: Production hatalarını takip edebilmek için iyi bir loglama mekanizması şart

En önemli tavsiyem şu: AJAX endpoint’lerinizi her zaman sanki dışarıdan doğrudan erişiliyormuş gibi yazın. Çünkü birileri mutlaka deneyecektir. Güvenlik katmanlarını oluşturduktan sonra geri kalanı zaten yerine oturur. Kod yazmak kolay, güvenli kod yazmak alışkanlık meselesi.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir