Options API ile WordPress Eklenti Verisi Saklama

Eklenti geliştirmeye yeni başlayanların yaptığı en yaygın hatalardan biri, küçük bir ayarı saklamak için gereksiz yere özel bir veritabanı tablosu oluşturmaktır. WordPress’in wp_options tablosu ve bunu yöneten Options API, zaten bu iş için tasarlanmış, olgun ve güvenilir bir mekanizma sunar. Ama “basit” görünen bu API’nin derinliklerine indiğinizde, performans, güvenlik ve veri tutarlılığı açısından dikkat etmeniz gereken pek çok nüans olduğunu görürsünüz.

Bu yazıda Options API’yi hem temel hem de ileri düzeyde ele alacağız. Gerçek eklenti geliştirme senaryoları üzerinden gideceğiz; ne zaman autoload kullanmalı, ne zaman kullanmamalı, transient ile options farkı nedir, çok siteli WordPress kurulumlarında ne olur gibi soruları yanıtlayacağız.

Options API Nedir ve wp_options Tablosu Nasıl Çalışır?

WordPress her ayarı wp_options tablosunda üç temel sütunla saklar: option_name, option_value ve autoload. Bu yapı son derece esnektir; string, integer, array, hatta nesne bile saklayabilirsiniz. WordPress bunları PHP’nin serialize() ve unserialize() fonksiyonları aracılığıyla işler.

autoload sütunu ise çoğu geliştiricinin gözden kaçırdığı kritik bir noktadır. yes olarak işaretlenen tüm seçenekler, WordPress her sayfa yüklenişinde tek bir SQL sorgusuyla belleğe alınır. Bu bir optimizasyondur, ancak yanlış kullanıldığında tersine döner. Eğer eklentiniz büyük veri bloklarını autoload=yes ile saklıyorsa, bu veriyi hiç kullanmayan sayfalarda bile bu ağırlığı taşırsınız.

# wp_options tablosunu MySQL üzerinden incelemek için
mysql -u wordpress_user -p wordpress_db -e "
SELECT option_name, 
       LENGTH(option_value) as value_size_bytes,
       autoload 
FROM wp_options 
WHERE autoload = 'yes' 
ORDER BY LENGTH(option_value) DESC 
LIMIT 20;"

Bu sorguyu çalıştırıp çıktıya baktığınızda, bazı eklentilerin autoload havuzuna megabaytlarca veri döktüğünü görebilirsiniz. Bunu görünce ne hissediyorsunuzdur bilemem, ama ben ilk gördüğümde gerçekten irkilmiştim.

Temel CRUD İşlemleri

Options API dört ana fonksiyon üzerine kuruludur.

<?php
// Seçenek ekleme - eğer yoksa ekler, varsa dokunmaz
add_option( 'my_plugin_settings', array(
    'api_key'     => '',
    'max_items'   => 10,
    'enable_log'  => false,
), '', 'no' ); // Son parametre autoload değeri

// Seçenek okuma - ikinci parametre default değerdir
$settings = get_option( 'my_plugin_settings', array() );

// Seçenek güncelleme - yoksa ekler, varsa günceller
update_option( 'my_plugin_settings', array(
    'api_key'     => 'abc123',
    'max_items'   => 25,
    'enable_log'  => true,
) );

// Seçenek silme
delete_option( 'my_plugin_settings' );

add_option ile update_option arasındaki fark şudur: add_option, kayıt zaten varsa işlem yapmaz ve false döner. Eklenti aktivasyonunda varsayılan değerleri set ederken add_option kullanmak doğru yaklaşımdır; böylece kullanıcının önceden kaydettiği ayarları ezmezsiniz.

Eklenti Aktivasyon ve Deaktivasyon Yönetimi

Profesyonel bir eklentide, veri yaşam döngüsünü doğru yönetmek kritiktir. Aktivasyonda varsayılanları kur, deaktivasyonda temizleme yapma (kullanıcı eklentiyi yeniden aktive edebilir), ama eklentinin tamamen silinmesinde her şeyi temizle.

<?php
class My_Plugin {

    const OPTION_KEY     = 'my_plugin_settings';
    const VERSION_KEY    = 'my_plugin_version';
    const CURRENT_VERSION = '2.1.0';

    public static function activate() {
        // Varsayılan ayarları yalnızca yoksa ekle
        $defaults = array(
            'api_endpoint' => 'https://api.example.com/v2',
            'timeout'      => 30,
            'retry_count'  => 3,
            'debug_mode'   => false,
            'cache_ttl'    => 3600,
        );

        add_option( self::OPTION_KEY, $defaults, '', 'no' );

        // Versiyon bilgisini her zaman güncelle (migration için gerekli)
        update_option( self::VERSION_KEY, self::CURRENT_VERSION, 'no' );

        // Veritabanı migration kontrolü
        self::maybe_migrate();
    }

    public static function deactivate() {
        // Kasıtlı olarak boş bırakıyoruz
        // Kullanıcı verisini deaktivasyonda silmek agresif bir davranış
    }

    private static function maybe_migrate() {
        $installed_version = get_option( self::VERSION_KEY, '1.0.0' );

        if ( version_compare( $installed_version, '2.0.0', '<' ) ) {
            self::migrate_to_v2();
        }
    }

    private static function migrate_to_v2() {
        // Eski yapıdaki veriyi yeni yapıya taşı
        $old_data = get_option( 'my_plugin_old_key' );
        if ( $old_data ) {
            $new_data = array(
                'api_endpoint' => isset( $old_data['url'] ) ? $old_data['url'] : '',
                'timeout'      => 30,
            );
            update_option( self::OPTION_KEY, $new_data );
            delete_option( 'my_plugin_old_key' );
        }
    }
}

register_activation_hook( __FILE__, array( 'My_Plugin', 'activate' ) );
register_deactivation_hook( __FILE__, array( 'My_Plugin', 'deactivate' ) );

uninstall.php dosyasında ise asıl temizliği yaparsınız:

<?php
// uninstall.php
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
    exit;
}

// Tüm eklenti verilerini temizle
delete_option( 'my_plugin_settings' );
delete_option( 'my_plugin_version' );

// Kullanıcı bazlı meta varsa onu da temizle
delete_metadata( 'user', 0, 'my_plugin_user_prefs', '', true );

// Çok siteli kurulum için
if ( is_multisite() ) {
    $sites = get_sites( array( 'number' => 0 ) );
    foreach ( $sites as $site ) {
        switch_to_blog( $site->blog_id );
        delete_option( 'my_plugin_settings' );
        restore_current_blog();
    }
}

Veri Doğrulama ve Sanitizasyon

Options API, verinizi kaydederken herhangi bir doğrulama yapmaz. Bu tamamen sizin sorumluluğunuzdadır. Settings API ile birlikte kullandığınızda sanitizasyon callback’leri tanımlayabilirsiniz, ama doğrudan update_option çağırıyorsanız veriyi kendiniz temizlemeniz gerekir.

<?php
class My_Plugin_Settings {

    public function save_settings( $raw_input ) {
        $sanitized = $this->sanitize_settings( $raw_input );

        if ( is_wp_error( $sanitized ) ) {
            // Hata yönetimi
            add_settings_error(
                'my_plugin_settings',
                'invalid_data',
                $sanitized->get_error_message()
            );
            return false;
        }

        return update_option( 'my_plugin_settings', $sanitized );
    }

    private function sanitize_settings( $input ) {
        $output = array();

        // API endpoint doğrulama
        if ( ! empty( $input['api_endpoint'] ) ) {
            $url = esc_url_raw( $input['api_endpoint'] );
            if ( ! filter_var( $url, FILTER_VALIDATE_URL ) ) {
                return new WP_Error( 'invalid_url', 'Geçersiz API endpoint URL adresi.' );
            }
            $output['api_endpoint'] = $url;
        }

        // Integer doğrulama
        $output['timeout'] = isset( $input['timeout'] ) 
            ? absint( $input['timeout'] ) 
            : 30;

        // Timeout makul bir aralıkta mı?
        if ( $output['timeout'] < 5 || $output['timeout'] > 120 ) {
            $output['timeout'] = 30;
        }

        // Boolean doğrulama
        $output['debug_mode'] = ! empty( $input['debug_mode'] );

        // Whitelist ile enum doğrulama
        $allowed_modes = array( 'simple', 'advanced', 'expert' );
        $output['mode'] = in_array( $input['mode'] ?? '', $allowed_modes, true )
            ? $input['mode']
            : 'simple';

        return $output;
    }
}

Autoload Stratejisi: Ne Zaman Evet, Ne Zaman Hayır?

Bu konuda net bir kural koymak gerekiyor: Sayfa yüklenişinin her noktasında ihtiyaç duyulan küçük veriler için autoload=yes, büyük veri blokları veya nadiren kullanılan ayarlar için autoload=no.

<?php
// Her sayfada kullanılan küçük ayarlar - autoload YES (varsayılan)
update_option( 'my_plugin_display_settings', array(
    'show_widget' => true,
    'theme'       => 'dark',
) );
// Bu değer açıkça belirtilmediğinde WordPress 'yes' kullanır

// Büyük veri veya admin-only ayarlar - autoload NO
update_option( 'my_plugin_api_cache', $large_api_response, false );
update_option( 'my_plugin_log_data', $log_array, 'no' );

// Sadece admin panelinde kullanılan ayarlar
update_option( 'my_plugin_advanced_config', $config, 'no' );

WP-CLI ile mevcut durumu analiz etmek her sysadmin’in araç çantasında bulunmalı:

# Autoload toplam boyutunu kontrol et
wp option list --autoload=on --format=table | head -30

# Belirli bir eklentinin option kayıtlarını bul
wp option list --search="my_plugin*" --format=table

# Büyük autoload kayıtlarını tespit et
wp eval '
$results = $wpdb->get_results(
    "SELECT option_name, LENGTH(option_value) as size 
     FROM wp_options 
     WHERE autoload = "yes" 
     ORDER BY size DESC LIMIT 10"
);
foreach($results as $r) {
    echo $r->option_name . " => " . size_format($r->size) . "n";
}
'

Transient API: Options API’nin Kardeşi

Transient’lar, Options API üzerine inşa edilmiş ancak süresi dolan veriler için tasarlanmış bir mekanizmadır. Harici API sonuçlarını, hesaplama maliyeti yüksek verileri saklamak için idealdir.

<?php
class My_Plugin_Cache {

    private const CACHE_KEY    = 'my_plugin_external_data';
    private const CACHE_EXPIRY = 3600; // 1 saat

    public function get_external_data() {
        // Önce transient'ı kontrol et
        $cached = get_transient( self::CACHE_KEY );

        if ( false !== $cached ) {
            return $cached;
        }

        // Cache miss - API'den çek
        $response = wp_remote_get( 'https://api.example.com/data', array(
            'timeout' => 15,
            'headers' => array(
                'Authorization' => 'Bearer ' . $this->get_api_key(),
            ),
        ) );

        if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
            // Hata durumunda kısa süreli boş cache - flood koruması
            set_transient( self::CACHE_KEY, array(), 60 );
            return array();
        }

        $data = json_decode( wp_remote_retrieve_body( $response ), true );

        if ( ! is_array( $data ) ) {
            return array();
        }

        set_transient( self::CACHE_KEY, $data, self::CACHE_EXPIRY );

        return $data;
    }

    public function flush_cache() {
        delete_transient( self::CACHE_KEY );
    }

    private function get_api_key() {
        $settings = get_option( 'my_plugin_settings', array() );
        return $settings['api_key'] ?? '';
    }
}

Transient’lar varsayılan olarak wp_options tablosunda _transient_ ve _transient_timeout_ ön eki ile saklanır. Redis veya Memcached gibi bir object cache kuruluysa, WordPress otomatik olarak bu harici cache sistemini kullanır ve veritabanını meşgul etmez. Bu detay önemlidir; aynı kod hem standart hem de cache’lenmiş ortamlarda düzgün çalışır.

Çok Siteli (Multisite) WordPress’te Network Options

Multisite kurulumlarında iki farklı düzey var: site bazlı seçenekler (her site için ayrı) ve network bazlı seçenekler (tüm ağ için ortak).

<?php
// Network genelinde geçerli ayarlar
// wp_sitemeta tablosuna kaydedilir
add_site_option( 'my_plugin_network_settings', array(
    'license_key'    => '',
    'allowed_sites'  => array(),
    'global_api_url' => 'https://api.example.com',
) );

$network_settings = get_site_option( 'my_plugin_network_settings', array() );

update_site_option( 'my_plugin_network_settings', $new_settings );

// Her site için ayrı ayar kontrolü
// Önce site bazlı bak, yoksa network default'a dön
function my_plugin_get_setting( $key ) {
    $site_settings    = get_option( 'my_plugin_settings', array() );
    $network_settings = get_site_option( 'my_plugin_network_settings', array() );

    // Site ayarı varsa onu kullan, yoksa network ayarına bak
    if ( isset( $site_settings[ $key ] ) && '' !== $site_settings[ $key ] ) {
        return $site_settings[ $key ];
    }

    return $network_settings[ $key ] ?? null;
}

Güvenlik: Nonce ve Capability Kontrolü

Options API’nin kendisi güvenlik kontrolü yapmaz. Kullanıcıdan gelen veriyi kaydetmeden önce mutlaka yetki ve nonce doğrulaması yapmalısınız.

<?php
class My_Plugin_Admin {

    public function __construct() {
        add_action( 'admin_post_save_my_plugin_settings', array( $this, 'handle_save' ) );
    }

    public function handle_save() {
        // 1. Nonce doğrulama
        if ( ! isset( $_POST['my_plugin_nonce'] ) || 
             ! wp_verify_nonce( $_POST['my_plugin_nonce'], 'my_plugin_save_settings' ) ) {
            wp_die( 'Güvenlik doğrulaması başarısız.' );
        }

        // 2. Kullanıcı yetkisi kontrolü
        if ( ! current_user_can( 'manage_options' ) ) {
            wp_die( 'Bu işlem için yetkiniz bulunmuyor.' );
        }

        // 3. Veriyi sanitize et ve kaydet
        $raw_input = $_POST['my_plugin'] ?? array();
        $handler   = new My_Plugin_Settings();
        $result    = $handler->save_settings( $raw_input );

        // 4. Geri yönlendir
        $redirect_url = add_query_arg(
            array(
                'page'    => 'my-plugin-settings',
                'updated' => $result ? 'true' : 'false',
            ),
            admin_url( 'options-general.php' )
        );

        wp_safe_redirect( $redirect_url );
        exit;
    }

    public function render_settings_form() {
        $settings = get_option( 'my_plugin_settings', array() );
        $nonce    = wp_create_nonce( 'my_plugin_save_settings' );
        ?>
        <form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
            <input type="hidden" name="action" value="save_my_plugin_settings">
            <input type="hidden" name="my_plugin_nonce" value="<?php echo esc_attr( $nonce ); ?>">
            <!-- Form alanları -->
        </form>
        <?php
    }
}

Performans Optimizasyonu: Options API’yi Doğru Kullanmak

Büyük veri setlerini tek bir option anahtarında saklamak, küçük parçalara bölmekten genellikle daha verimlidir. Her get_option çağrısı potansiyel bir veritabanı sorgusudur (cache olmadığında). Ama WordPress, bir kez çekilen option’ları bellekte tutar, bu yüzden aynı sayfa yüklenişinde aynı option’ı defalarca çekmek sorun yaratmaz.

<?php
// Kötü pratik: Her ayar için ayrı option
get_option( 'my_plugin_api_key' );
get_option( 'my_plugin_timeout' );
get_option( 'my_plugin_debug_mode' );
get_option( 'my_plugin_cache_ttl' );

// İyi pratik: İlişkili ayarları tek option'da grupla
$settings = get_option( 'my_plugin_settings', array() );
$api_key    = $settings['api_key']    ?? '';
$timeout    = $settings['timeout']    ?? 30;
$debug_mode = $settings['debug_mode'] ?? false;
$cache_ttl  = $settings['cache_ttl']  ?? 3600;

// Erken erişim için option preloading
// 'wp_loaded' öncesinde çağırın
function my_plugin_preload_options() {
    wp_cache_add_non_persistent_groups( array( 'my_plugin' ) );
    // Sık kullanılan option'ları burada preload edebilirsiniz
    get_option( 'my_plugin_settings' );
}
add_action( 'init', 'my_plugin_preload_options', 1 );

Yapısal olarak büyük ve karmaşık veri için wp_options yerine özel tablo düşünülmelidir. Binlerce kaydı saklamanız, karmaşık sorgular yapmanız ya da ilişkisel veri yapısına ihtiyaç duymanız durumunda Options API’nin sınırlarına gelirsiniz.

Gerçek Dünya Senaryosu: WooCommerce Entegrasyon Eklentisi

Son olarak, tüm bu kavramları bir araya getiren gerçekçi bir örnek:

<?php
/**
 * ERP entegrasyon eklentisi için ayar yönetimi
 */
class ERP_Integration_Options {

    private const SETTINGS_KEY  = 'erp_integration_v2_settings';
    private const SYNC_LOG_KEY  = 'erp_integration_last_sync';
    private const CACHE_KEY_ERP = 'erp_product_categories';

    /**
     * Bağlantı ayarlarını kaydet
     * Admin sayfasından form submit edildiğinde çağrılır
     */
    public function save_connection_settings( array $posted_data ): bool {
        if ( ! current_user_can( 'manage_woocommerce' ) ) {
            return false;
        }

        $current = get_option( self::SETTINGS_KEY, array() );

        // Sadece bağlantı ayarlarını güncelle, diğerlerine dokunma
        $current['erp_url']      = esc_url_raw( $posted_data['erp_url'] ?? '' );
        $current['erp_username'] = sanitize_text_field( $posted_data['erp_username'] ?? '' );
        
        // Şifreyi yalnızca değiştirilmişse güncelle
        if ( ! empty( $posted_data['erp_password'] ) ) {
            // Gerçek projede şifreyi plain-text saklamayın
            // Encrypted veya wp-config'den environment variable kullanın
            $current['erp_password_hash'] = wp_hash_password( $posted_data['erp_password'] );
        }

        $current['sync_interval'] = in_array(
            $posted_data['sync_interval'] ?? '',
            array( 'hourly', 'twicedaily', 'daily' ),
            true
        ) ? $posted_data['sync_interval'] : 'daily';

        // Bağlantı ayarları admin-only, autoload gerekmez
        $result = update_option( self::SETTINGS_KEY, $current, 'no' );

        if ( $result ) {
            // Cache'i temizle, yeni ayarlarla tekrar çekilecek
            delete_transient( self::CACHE_KEY_ERP );
        }

        return $result;
    }

    /**
     * Senkronizasyon logunu güncelle
     * Cron job'dan çağrılır
     */
    public function update_sync_log( string $status, int $synced_count, string $message = '' ): void {
        $log = array(
            'timestamp'    => current_time( 'mysql' ),
            'status'       => $status,
            'synced_count' => $synced_count,
            'message'      => sanitize_text_field( $message ),
            'memory_peak'  => size_format( memory_get_peak_usage( true ) ),
        );

        // Senkronizasyon logu büyüyebilir, autoload kesinlikle NO
        update_option( self::SYNC_LOG_KEY, $log, 'no' );
    }

    /**
     * ERP'den kategori listesi - 1 saatlik cache ile
     */
    public function get_erp_categories(): array {
        $cached = get_transient( self::CACHE_KEY_ERP );
        if ( false !== $cached ) {
            return $cached;
        }

        $settings = get_option( self::SETTINGS_KEY, array() );
        
        if ( empty( $settings['erp_url'] ) ) {
            return array();
        }

        $response = wp_remote_get( trailingslashit( $settings['erp_url'] ) . 'api/categories', array(
            'headers' => array(
                'X-ERP-User' => $settings['erp_username'] ?? '',
            ),
            'timeout' => 20,
        ) );

        if ( is_wp_error( $response ) ) {
            return array();
        }

        $categories = json_decode( wp_remote_retrieve_body( $response ), true ) ?? array();
        set_transient( self::CACHE_KEY_ERP, $categories, HOUR_IN_SECONDS );

        return $categories;
    }
}

Sonuç

Options API, WordPress ekosisteminin en temel ve en güvenilir bileşenlerinden biridir. Doğru kullanıldığında özel tablo oluşturma ihtiyacını ortadan kaldırır, kod karmaşıklığını azaltır ve WordPress güncellemelerinden bağımsız çalışır.

Yazı boyunca üzerinde durduğumuz kritik noktaları özetlemek gerekirse:

  • Autoload kararınızı bilinçli verin. Büyük veri ve nadiren erişilen ayarlar için no kullanın.
  • Veriyi gruplandırın. Her ayar için ayrı option anahtarı açmak yerine ilişkili değerleri array içinde tutun.
  • Aktivasyon ve uninstall döngüsünü eksiksiz yönetin. Kullanıcının veritabanını kirletmeyin.
  • Sanitizasyon ve yetki kontrolünü asla atlamamayın. Options API güvenlik sunmaz, bu sizin işinizdir.
  • Harici veri ve pahalı hesaplamalar için Transient kullanın. Object cache ile otomatik entegrasyonu ücretsiz gelir.
  • Multisite’da network ve site bazlı options’ı ayırt edin. Yanlış fonksiyon, yanlış tabloya yazmanıza neden olur.

WP-CLI’ı geliştirme ve production ortamlarında aktif olarak kullanın; wp option komutları hem debugging hem de deployment süreçlerinde size ciddi zaman kazandırır. Projenizin ilerleyen aşamalarında seçeneklerinizi gözden geçirmeyi alışkanlık haline getirin; zamanla büyüyen eklentilerde autoload kirliliği sessiz sedasız performans sorunlarına kapı aralar.

Bir yanıt yazın

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