WordPress Geliştirmede Kod Standartları ve En İyi Pratikler

WordPress eklenti geliştirme dünyasına adım attığımda, ilk birkaç yılda yazdığım kodlara şimdi baktığımda yüzüm kızarıyor. Fonksiyon isimleri rastgele, sanitizasyon yok, hook’lar karmakarışık. Sonra bir gün production ortamında ciddi bir güvenlik açığı yakaladık ve o andan itibaren “çalışıyor mu?” sorusundan “doğru çalışıyor mu?” sorusuna geçtim. Bu yazıda WordPress eklenti geliştirmede kod standartları ve en iyi pratikler konusunda bildiklerimi aktaracağım.

Neden Standartlara İhtiyacımız Var?

WordPress’in resmi kod standartları var ve bunlar keyfi değil. Ekip içinde tutarlılık sağlamak, kod review süreçlerini hızlandırmak ve özellikle güvenlik açıklarını minimize etmek için bu standartlar hayati önem taşıyor. Bir eklenti geliştirdiğinizde sadece kendi sisteminizde değil, belki binlerce WordPress kurulumunda çalışıyor. Bu sorumluluk hafife alınacak bir şey değil.

PHP kodlama standartları açısından WordPress, PSR standartlarından biraz farklı bir yol izliyor. Tab kullanımı (boşluk değil), Allman tarzı süslü parantez yerleşimi ve yMethodName yerine method_name formatı bunların başında geliyor.

Dosya ve Dizin Yapısı

İyi bir eklenti mimarisi, ileride bakım yapacak kişinin (ki bu çoğu zaman 6 ay sonraki kendiniz olur) işini kolaylaştırmalıdır.

my-plugin/
├── my-plugin.php          # Ana eklenti dosyası
├── readme.txt
├── includes/
│   ├── class-my-plugin.php
│   ├── class-my-plugin-admin.php
│   └── class-my-plugin-public.php
├── admin/
│   ├── css/
│   ├── js/
│   └── partials/
├── public/
│   ├── css/
│   ├── js/
│   └── partials/
└── languages/
    └── my-plugin.pot

Ana eklenti dosyasında mutlaka şu başlık bloğunu kullanın:

<?php
/**
 * Plugin Name: My Awesome Plugin
 * Plugin URI:  https://example.com/my-plugin
 * Description: Eklenti açıklaması buraya gelir
 * Version:     1.0.0
 * Author:      Adınız
 * Author URI:  https://example.com
 * License:     GPL-2.0+
 * Text Domain: my-plugin
 * Domain Path: /languages
 */

// Doğrudan erişimi engelle
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

Bu ABSPATH kontrolü çok kritik. Birisi eklenti dosyasını doğrudan URL üzerinden çağırmaya çalıştığında WordPress ortamı yüklü olmayacak ve bu satır çalışmayı durduracak. Bunu eklentinizdeki her PHP dosyasına koyun.

Namespace ve Prefix Kullanımı

WordPress global bir PHP ortamında çalışıyor. Yani yazdığınız her fonksiyon ismi, sınıf adı ve global değişken diğer eklentilerle çakışabilir. Bu yüzden prefix kullanımı zorunlu.

<?php
// YANLIŞ - çakışma riski yüksek
function get_settings() {
    return get_option('settings');
}

// DOĞRU - prefix ile güvenli
function myplugin_get_settings() {
    return get_option('myplugin_settings');
}

// Daha iyi - OOP yaklaşımı
class MyPlugin_Settings {
    public function get_settings() {
        return get_option('myplugin_settings');
    }
}

PHP 5.3 ve üzeri için namespace kullanabilirsiniz ama WordPress’in kendisi hâlâ geniş namespace desteğine tam geçiş yapmadığından, özellikle eski tema/eklenti uyumluluğu önemliyse prefix’li sınıf isimleri daha güvenli bir tercih.

Güvenlik: Sanitizasyon, Validasyon ve Escaping

Bu üç kavramı karıştıran çok fazla geliştirici var. Kısaca özetleyeyim:

  • Sanitizasyon: Veriyi veritabanına yazmadan önce temizleme
  • Validasyon: Verinin beklediğiniz formatta olup olmadığını kontrol etme
  • Escaping: Veriyi çıktıya vermeden önce güvenli hale getirme
<?php
// Form verisi işleme örneği
function myplugin_save_settings() {
    // Nonce kontrolü - CSRF koruması
    if ( ! isset( $_POST['myplugin_nonce'] ) || 
         ! wp_verify_nonce( $_POST['myplugin_nonce'], 'myplugin_save_settings' ) ) {
        wp_die( __( 'Güvenlik kontrolü başarısız.', 'my-plugin' ) );
    }
    
    // Yetki kontrolü
    if ( ! current_user_can( 'manage_options' ) ) {
        wp_die( __( 'Bu işlem için yetkiniz yok.', 'my-plugin' ) );
    }
    
    // Sanitizasyon
    $email = sanitize_email( $_POST['admin_email'] );
    $title = sanitize_text_field( $_POST['plugin_title'] );
    $content = wp_kses_post( $_POST['plugin_content'] );
    $count = absint( $_POST['item_count'] );
    
    // Validasyon
    if ( ! is_email( $email ) ) {
        add_settings_error( 'myplugin_settings', 'invalid_email', 
            __( 'Geçerli bir e-posta adresi girin.', 'my-plugin' ) );
        return;
    }
    
    // Kaydetme
    update_option( 'myplugin_admin_email', $email );
    update_option( 'myplugin_title', $title );
}
add_action( 'admin_post_myplugin_save_settings', 'myplugin_save_settings' );

Çıktıda ise escaping fonksiyonları kullanmak zorundasınız:

<?php
// HTML içeriği için
echo esc_html( $user_input );

// HTML attribute için
echo '<input type="text" value="' . esc_attr( $value ) . '">';

// URL için
echo '<a href="' . esc_url( $link ) . '">';

// JavaScript için
echo '<script>var data = ' . wp_json_encode( $data ) . ';</script>';

// Çeviri ile birlikte
echo esc_html__( 'Kaydet', 'my-plugin' );
esc_html_e( 'Kaydet', 'my-plugin' );

Canlıya almadan önce plugin dosyalarını grep ile tarayın. $_POST, $_GET ve $_REQUEST geçen her satırda ya sanitizasyon ya da validasyon olmalı. Bu kural değil, prensip.

Hook Sistemi ve Doğru Kullanımı

WordPress’in güzelliği hook sistemi. Ama yanlış kullanıldığında performans kabusu ve bakım cehennemi haline geliyor.

<?php
class MyPlugin_Core {
    
    private static $instance = null;
    
    // Singleton pattern - eklentiyi bir kez yükle
    public static function get_instance() {
        if ( null === self::$instance ) {
            self::$instance = new self();
        }
        return self::$instance;
    }
    
    private function __construct() {
        $this->define_hooks();
    }
    
    private function define_hooks() {
        // init hook'u genel başlatma için
        add_action( 'init', array( $this, 'load_plugin_textdomain' ) );
        
        // admin_init sadece admin panelinde çalışır
        add_action( 'admin_init', array( $this, 'register_settings' ) );
        
        // Enqueue işlemleri doğru hook'ta yapılmalı
        add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_public_assets' ) );
        add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_assets' ) );
        
        // Öncelik belirtme - sayı küçüldükçe önce çalışır
        add_filter( 'the_content', array( $this, 'modify_content' ), 20 );
    }
    
    public function enqueue_public_assets() {
        // Sadece gerekli sayfalarda yükle
        if ( ! is_singular( 'post' ) ) {
            return;
        }
        
        wp_enqueue_style(
            'myplugin-public',
            plugin_dir_url( __FILE__ ) . 'public/css/myplugin-public.css',
            array(),
            MY_PLUGIN_VERSION
        );
    }
}

wp_enqueue_scripts içinde koşullu yükleme yapmak önemli. Her sayfada jQuery UI yükleyen eklentiler sayfa hızını öldürüyor. Kullanıcı sadece iletişim formunu olan sayfada o CSS/JS’e ihtiyaç duyuyor.

Veritabanı İşlemleri

WordPress $wpdb sınıfı ile çalışırken prepared statement kullanımı tartışmasız bir kural olmalı.

<?php
global $wpdb;

// YANLIŞ - SQL injection açığı
$user_id = $_GET['user_id'];
$results = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}users WHERE ID = $user_id" );

// DOĞRU - Prepared statement
$user_id = absint( $_GET['user_id'] );
$results = $wpdb->get_results(
    $wpdb->prepare(
        "SELECT ID, user_login, user_email FROM {$wpdb->prefix}users WHERE ID = %d",
        $user_id
    )
);

// Eklenti tablosu oluşturma - aktivasyon hook'unda
function myplugin_create_tables() {
    global $wpdb;
    
    $table_name = $wpdb->prefix . 'myplugin_logs';
    $charset_collate = $wpdb->get_charset_collate();
    
    $sql = "CREATE TABLE IF NOT EXISTS $table_name (
        id bigint(20) NOT NULL AUTO_INCREMENT,
        user_id bigint(20) NOT NULL,
        action varchar(100) NOT NULL,
        created_at datetime DEFAULT CURRENT_TIMESTAMP,
        PRIMARY KEY  (id),
        KEY user_id (user_id)
    ) $charset_collate;";
    
    require_once ABSPATH . 'wp-admin/includes/upgrade.php';
    dbDelta( $sql );
}
register_activation_hook( __FILE__, 'myplugin_create_tables' );

dbDelta() fonksiyonu hem tablo oluşturma hem güncelleme işlemlerini yönetiyor. Eklenti güncellemelerinde şema değişikliği yapmanız gerektiğinde bu fonksiyon hayat kurtarıyor. PRIMARY KEY satırından sonra çift boşluk bırakın, yoksa dbDelta düzgün çalışmıyor. Garip ama gerçek.

Hata Yönetimi ve Loglama

Production ortamında hataları kullanıcıya göstermek yerine loglayın. Ama ne aşırı, ne de yetersiz loglama yapın.

<?php
function myplugin_process_data( $data ) {
    // WP_Error kullanımı
    if ( empty( $data['required_field'] ) ) {
        return new WP_Error(
            'missing_field',
            __( 'Zorunlu alan eksik.', 'my-plugin' ),
            array( 'field' => 'required_field' )
        );
    }
    
    // Dış API çağrısı
    $response = wp_remote_post( 'https://api.example.com/endpoint', array(
        'timeout'     => 15,
        'redirection' => 5,
        'headers'     => array(
            'Content-Type' => 'application/json',
            'Authorization' => 'Bearer ' . myplugin_get_api_key(),
        ),
        'body'        => wp_json_encode( $data ),
    ) );
    
    if ( is_wp_error( $response ) ) {
        // Debug modunda logla
        if ( defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) {
            error_log( 'MyPlugin API Hatası: ' . $response->get_error_message() );
        }
        return $response;
    }
    
    $response_code = wp_remote_retrieve_response_code( $response );
    
    if ( 200 !== $response_code ) {
        return new WP_Error(
            'api_error',
            sprintf( __( 'API %d kodu döndürdü.', 'my-plugin' ), $response_code )
        );
    }
    
    return json_decode( wp_remote_retrieve_body( $response ), true );
}

// Kullanım
$result = myplugin_process_data( $form_data );

if ( is_wp_error( $result ) ) {
    echo esc_html( $result->get_error_message() );
    return;
}

// Başarılı işlem devam ediyor

WP_Error sınıfını sevin. Birden fazla hatayı biriktirip toplu döndürebilirsiniz. is_wp_error() kontrolünü alışkanlık haline getirin.

Çeviri Desteği (i18n)

Eklentinizi yalnızca Türkçe kullanıcılar kullanmayacak ya da Türk geliştiricilerle çalışmıyorsunuz diye ihmal etmeyin. Ayrıca WordPress.org’a gönderecekseniz bu zorunlu.

<?php
// Metin etki alanını yükle
function myplugin_load_textdomain() {
    load_plugin_textdomain(
        'my-plugin',
        false,
        dirname( plugin_basename( __FILE__ ) ) . '/languages/'
    );
}
add_action( 'init', 'myplugin_load_textdomain' );

// Kullanım örnekleri
$message = __( 'Kayıt başarılı.', 'my-plugin' );
_e( 'Kayıt başarılı.', 'my-plugin' );

// Değişkenli
$message = sprintf( 
    /* translators: %s: Kullanıcı adı */
    __( 'Merhaba, %s!', 'my-plugin' ), 
    esc_html( $username ) 
);

// Tekil/çoğul
$message = sprintf(
    _n( '%d ürün bulundu.', '%d ürün bulundu.', $count, 'my-plugin' ),
    $count
);

WP-CLI ile .pot dosyası oluşturabilirsiniz:

# WP-CLI ile pot dosyası oluşturma
wp i18n make-pot . languages/my-plugin.pot --domain=my-plugin

# Derleme
wp i18n make-mo languages/my-plugin-tr_TR.po

Performans İpuçları

Eklentinizin her sayfa yüklemesinde çalıştığını unutmayın. Küçük optimizasyonlar çarpan etkisi yapıyor.

  • Transient kullan: Dış API çağrıları, ağır veritabanı sorguları için set_transient() ve get_transient() kullanın. 5 dakikalık cache bile ciddi fark yaratır.
  • Koşullu yükleme: Asset’leri sadece gerekli sayfada enqueue edin. is_page(), is_singular() gibi koşullu tag’leri kullanın.
  • Autoload dikkatli kullan: add_option() fonksiyonunun autoload parametresi varsayılan olarak yes. Büyük veriler için bunu no yapın.
  • WP_Query’yi doğru kullan: Sadece ihtiyacınız olan alanları çekin. 'fields' => 'ids' gibi parametreler gereksiz veri taşımasını önler.
  • Object cache: wp_cache_get() ve wp_cache_set() kullanımını alışkanlık haline getirin. Redis veya Memcached varsa otomatik olarak kullanılır.

Uninstall ve Temizlik

Eklenti silindiğinde arkasında çöp bırakmamalı. Bu hem iyi ahlak hem kullanıcı güveni meselesi.

<?php
// uninstall.php dosyası - eklenti silindiğinde çalışır
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
    exit;
}

// Seçenekleri temizle
delete_option( 'myplugin_settings' );
delete_option( 'myplugin_version' );

// Custom tabloları sil
global $wpdb;
$wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}myplugin_logs" );

// Tüm kullanıcı meta verilerini temizle
$wpdb->query( "DELETE FROM {$wpdb->prefix}usermeta WHERE meta_key LIKE 'myplugin_%'" );

// Zamanlanmış görevleri temizle
wp_clear_scheduled_hook( 'myplugin_daily_cleanup' );

uninstall.php dosyası register_uninstall_hook() callback’inden daha güvenli. Çünkü eklenti devre dışıyken bile çalışabiliyor ve ayrı bir güvenlik kontrolü içeriyor.

Code Review Checklist

Bir eklentiyi production’a almadan önce şu listeyi gözden geçirmenizi öneririm:

  • Her PHP dosyasında ABSPATH kontrolü var mı?
  • Tüm kullanıcı girdileri sanitize ediliyor mu?
  • Tüm çıktılar escape ediliyor mu?
  • Form işlemlerinde nonce kontrolü var mı?
  • Yetki kontrolleri current_user_can() ile yapılıyor mu?
  • Veritabanı sorgularında prepare() kullanılıyor mu?
  • Asset’ler sadece gerekli sayfalarda yükleniyor mu?
  • uninstall.php dosyası mevcut mu?
  • Çeviri desteği (__(), _e()) eklenmiş mi?
  • Debug modunda bile hassas veri ekrana basılıyor mu?

Sonuç

WordPress eklenti geliştirme, yüzeysel bakıldığında birkaç PHP dosyası yazmak gibi görünüyor. Ama gerçekte güvenlik, performans, uyumluluk ve bakım kolaylığı dengesini kurmak zorunda olduğunuz nüanslı bir iş. Bu yazıda anlattığım standartların büyük kısmı WordPress Kodeks ve Eklenti El Kitabı’nda da yer alıyor, ama orada kuru bir dokümantasyon olarak duruyor. Burada ise gerçek senaryolar ve “neden önemli?” sorusunun cevapları var.

Kod standartları boğucu kurallar değil, sizi ve ekibinizi koruma altına alan güvenlik ağları. Bir süre sonra bu kalıplar ikinci doğanız haline geliyor ve o noktadan sonra temiz kod yazmak ekstra efor gerektirmez. Hatta kirli kodu okumak rahatsızlık vermeye başlar. O noktaya gelin. Sisteminiz ve production sunucularınız size teşekkür eder.

Bir yanıt yazın

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