Dosya Yapısı ve Boilerplate: WordPress Eklenti Temelleri

Bir WordPress eklentisi geliştirmeye başladığınızda, ilk birkaç saatin nasıl geçtiğini çok iyi biliyorum: boş bir PHP dosyası açıyorsunuz, ne yazacağınızı düşünüyorsunuz, sonra Stack Overflow’a dalıyorsunuz. Oysa eklenti geliştirmenin temeli olan dosya yapısını ve boilerplate kodunu bir kez kavradığınızda, geri kalanı gerçekten oturmuş bir zemine oturuyor. Bu yazıda sıfırdan bir WordPress eklentisi oluşturmanın anatomisini, neden bu şekilde yapılandırıldığını ve production ortamında nasıl bir yapı kurmanız gerektiğini anlatacağım.

WordPress Eklenti Sisteminin Çalışma Mantığı

WordPress, eklenti sistemini wp-content/plugins/ dizini üzerinden yönetir. Her eklenti ya tek bir PHP dosyasından ya da kendi klasörü içinde organize edilmiş bir dosya setinden oluşur. WordPress bu dizini tarar, her PHP dosyasının başındaki yorum bloğunu okur ve eklenti bilgilerini buradan çeker.

Basit bir eklenti için tek dosya yeterli olabilir, ama gerçek dünyada üretim ortamına alacağınız bir eklenti için bu asla yeterli değildir. Codebase büyüdükçe tek dosya yaklaşımı bakımı imkansız hale getirir.

WordPress’in bir dosyayı eklenti olarak tanıması için minimum gereksinim şu yorum bloğudur:

<?php
/**
 * Plugin Name: Benim Eklentim
 * Plugin URI:  https://example.com/eklentim
 * Description: Bu eklenti şunu yapar.
 * Version:     1.0.0
 * Author:      Ahmet Yılmaz
 * Author URI:  https://example.com
 * License:     GPL-2.0+
 * Text Domain: benim-eklentim
 * Domain Path: /languages
 */

Bu blok olmadan WordPress dosyayı hiç görmez. Text Domain ve Domain Path alanları çokdil desteği için kritik, production eklentilerinde bunları atlamamanızı tavsiye ederim.

Klasör Yapısı: Gerçek Dünyadan Bir Örnek

Yıllar içinde farklı eklenti yapıları denedim. Küçük bir yardımcı eklenti için tek dosya, orta büyüklükteki bir eklenti için flat yapı, büyük eklentiler için ise OOP tabanlı modüler yapı. Bugün anlattığım yapı, production’da test edilmiş ve ekip içinde kolayca geliştirilebilen bir yapı:

benim-eklentim/
├── benim-eklentim.php          # Ana giriş dosyası
├── uninstall.php               # Kaldırma işlemleri
├── readme.txt                  # WordPress.org formatı
├── includes/
│   ├── class-benim-eklentim.php        # Ana sınıf
│   ├── class-benim-eklentim-loader.php # Hook yöneticisi
│   ├── class-benim-eklentim-i18n.php   # Çokdil desteği
│   └── class-benim-eklentim-activator.php # Aktivasyon işlemleri
├── admin/
│   ├── class-benim-eklentim-admin.php
│   ├── css/
│   │   └── benim-eklentim-admin.css
│   ├── js/
│   │   └── benim-eklentim-admin.js
│   └── partials/
│       └── benim-eklentim-admin-display.php
├── public/
│   ├── class-benim-eklentim-public.php
│   ├── css/
│   │   └── benim-eklentim-public.css
│   └── js/
│       └── benim-eklentim-public.js
└── languages/
    └── benim-eklentim.pot

Bu yapının mantığı şu: admin/ ve public/ klasörlerini ayırmak, hangi kodun nerede çalıştığını netleştirir. Admin paneline özel CSS ve JS’i front-end’e yüklemezsiniz, bu da performans açısından kritiktir.

Ana Giriş Dosyası: benim-eklentim.php

Ana dosya mümkün olduğunca temiz tutulmalıdır. İş mantığı burada değil, include ettiğiniz sınıflarda olmalıdır:

<?php
/**
 * Plugin Name: Benim Eklentim
 * Description: Örnek bir WordPress eklentisi
 * Version:     1.0.0
 * Author:      Ahmet Yılmaz
 * Text Domain: benim-eklentim
 * Domain Path: /languages
 */

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

// Eklenti sabitleri
define( 'BENIM_EKLENTIM_VERSION', '1.0.0' );
define( 'BENIM_EKLENTIM_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
define( 'BENIM_EKLENTIM_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
define( 'BENIM_EKLENTIM_PLUGIN_FILE', __FILE__ );

// Aktivasyon ve deaktivasyon hook'ları
register_activation_hook( __FILE__, array( 'Benim_Eklentim_Activator', 'activate' ) );
register_deactivation_hook( __FILE__, array( 'Benim_Eklentim_Deactivator', 'deactivate' ) );

// Gerekli dosyaları yükle
require_once BENIM_EKLENTIM_PLUGIN_DIR . 'includes/class-benim-eklentim.php';
require_once BENIM_EKLENTIM_PLUGIN_DIR . 'includes/class-benim-eklentim-activator.php';

// Eklentiyi başlat
function run_benim_eklentim() {
    $plugin = new Benim_Eklentim();
    $plugin->run();
}
run_benim_eklentim();

if ( ! defined( 'ABSPATH' ) ) { exit; } satırı kritiktir. Birisi eklenti dosyasına doğrudan URL üzerinden erişmeye çalışırsa bu satır çalışmayı durdurur. Bunu her PHP dosyanızın başına koyun.

Sabitleri define etmek de önemlidir. plugin_dir_path() ve plugin_dir_url() fonksiyonları trailing slash ile döner, bu yüzden path birleştirmelerinizde ekstra / eklemenize gerek kalmaz.

Ana Sınıf: class-benim-eklentim.php

Bu sınıf eklentinin iskeletidir. Hook’ları yükler, bileşenleri bir araya getirir:

<?php

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

class Benim_Eklentim {

    protected $loader;
    protected $plugin_name;
    protected $version;

    public function __construct() {
        $this->version     = defined( 'BENIM_EKLENTIM_VERSION' ) ? BENIM_EKLENTIM_VERSION : '1.0.0';
        $this->plugin_name = 'benim-eklentim';

        $this->load_dependencies();
        $this->set_locale();
        $this->define_admin_hooks();
        $this->define_public_hooks();
    }

    private function load_dependencies() {
        require_once BENIM_EKLENTIM_PLUGIN_DIR . 'includes/class-benim-eklentim-loader.php';
        require_once BENIM_EKLENTIM_PLUGIN_DIR . 'includes/class-benim-eklentim-i18n.php';
        require_once BENIM_EKLENTIM_PLUGIN_DIR . 'admin/class-benim-eklentim-admin.php';
        require_once BENIM_EKLENTIM_PLUGIN_DIR . 'public/class-benim-eklentim-public.php';

        $this->loader = new Benim_Eklentim_Loader();
    }

    private function set_locale() {
        $plugin_i18n = new Benim_Eklentim_i18n();
        $this->loader->add_action( 'plugins_loaded', $plugin_i18n, 'load_plugin_textdomain' );
    }

    private function define_admin_hooks() {
        $plugin_admin = new Benim_Eklentim_Admin( $this->get_plugin_name(), $this->get_version() );
        $this->loader->add_action( 'admin_enqueue_scripts', $plugin_admin, 'enqueue_styles' );
        $this->loader->add_action( 'admin_enqueue_scripts', $plugin_admin, 'enqueue_scripts' );
        $this->loader->add_action( 'admin_menu', $plugin_admin, 'add_plugin_admin_menu' );
    }

    private function define_public_hooks() {
        $plugin_public = new Benim_Eklentim_Public( $this->get_plugin_name(), $this->get_version() );
        $this->loader->add_action( 'wp_enqueue_scripts', $plugin_public, 'enqueue_styles' );
        $this->loader->add_action( 'wp_enqueue_scripts', $plugin_public, 'enqueue_scripts' );
    }

    public function run() {
        $this->loader->run();
    }

    public function get_plugin_name() {
        return $this->plugin_name;
    }

    public function get_version() {
        return $this->version;
    }

    public function get_loader() {
        return $this->loader;
    }
}

Hook Loader Sınıfı

Bu sınıf, tüm action ve filter kayıtlarını merkezi bir yerden yönetmek için kullanılır. Neden bu kadar önemli? Çünkü eklentinizin kayıtlı tüm hook’larını tek bir yerden görebilirsiniz, hata ayıklamak kolaylaşır ve deaktivasyon anında tüm hook’ları temizlemek için net bir nokta oluşur:

<?php

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

class Benim_Eklentim_Loader {

    protected $actions;
    protected $filters;

    public function __construct() {
        $this->actions = array();
        $this->filters = array();
    }

    public function add_action( $hook, $component, $callback, $priority = 10, $accepted_args = 1 ) {
        $this->actions = $this->add( $this->actions, $hook, $component, $callback, $priority, $accepted_args );
    }

    public function add_filter( $hook, $component, $callback, $priority = 10, $accepted_args = 1 ) {
        $this->filters = $this->add( $this->filters, $hook, $component, $callback, $priority, $accepted_args );
    }

    private function add( $hooks, $hook, $component, $callback, $priority, $accepted_args ) {
        $hooks[] = array(
            'hook'          => $hook,
            'component'     => $component,
            'callback'      => $callback,
            'priority'      => $priority,
            'accepted_args' => $accepted_args,
        );
        return $hooks;
    }

    public function run() {
        foreach ( $this->filters as $hook ) {
            add_filter(
                $hook['hook'],
                array( $hook['component'], $hook['callback'] ),
                $hook['priority'],
                $hook['accepted_args']
            );
        }

        foreach ( $this->actions as $hook ) {
            add_action(
                $hook['hook'],
                array( $hook['component'], $hook['callback'] ),
                $hook['priority'],
                $hook['accepted_args']
            );
        }
    }
}

Aktivasyon Sınıfı

Eklenti ilk aktif edildiğinde yapılması gereken işlemler, örneğin veritabanı tablosu oluşturma, default ayarları kaydetme, gibi görevler bu sınıfta tanımlanır:

<?php

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

class Benim_Eklentim_Activator {

    public static function activate() {
        global $wpdb;

        $charset_collate = $wpdb->get_charset_collate();
        $table_name      = $wpdb->prefix . 'benim_eklentim_kayitlar';

        $sql = "CREATE TABLE IF NOT EXISTS $table_name (
            id bigint(20) NOT NULL AUTO_INCREMENT,
            kullanici_id bigint(20) NOT NULL,
            islem_turu varchar(50) NOT NULL,
            tarih datetime DEFAULT CURRENT_TIMESTAMP,
            veri longtext,
            PRIMARY KEY (id),
            KEY kullanici_id (kullanici_id)
        ) $charset_collate;";

        require_once ABSPATH . 'wp-admin/includes/upgrade.php';
        dbDelta( $sql );

        // Varsayılan ayarları kaydet
        $default_options = array(
            'aktif'          => true,
            'bildirim_email' => get_option( 'admin_email' ),
            'max_kayit'      => 100,
        );

        if ( ! get_option( 'benim_eklentim_options' ) ) {
            add_option( 'benim_eklentim_options', $default_options );
        }

        // Aktivasyon flag'i set et, admin bildirimi için
        set_transient( 'benim_eklentim_aktivasyon_bildirimi', true, 5 );
    }
}

dbDelta() fonksiyonu WordPress’in veritabanı schema yönetim fonksiyonudur. Tablo yoksa oluşturur, varsa gerekli sütunları ekler ama var olan verilere dokunmaz. Eklenti güncellemelerinde şema değişikliği yaparken hayat kurtarıcıdır.

uninstall.php: Temiz Kaldırma

Bu dosyayı atlamak en sık yapılan hatalardan biridir. Kullanıcı eklentinizi sildiğinde, arkasında çöp bırakmamalısınız. Veritabanı tabloları, option kayıtları, kullanıcı meta verileri, hepsi temizlenmelidir:

<?php
// Doğrudan erişim kontrolü - uninstall.php için özel kontrol
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
    exit;
}

global $wpdb;

// Options temizle
delete_option( 'benim_eklentim_options' );
delete_option( 'benim_eklentim_version' );

// Transient'ları temizle
delete_transient( 'benim_eklentim_aktivasyon_bildirimi' );

// Veritabanı tablosunu sil
$table_name = $wpdb->prefix . 'benim_eklentim_kayitlar';
$wpdb->query( "DROP TABLE IF EXISTS {$table_name}" );

// Kullanıcı meta temizliği
$wpdb->delete(
    $wpdb->usermeta,
    array( 'meta_key' => 'benim_eklentim_tercihler' ),
    array( '%s' )
);

// Scheduled event'ları temizle
wp_clear_scheduled_hook( 'benim_eklentim_gunluk_gorev' );

WP_UNINSTALL_PLUGIN sabitinin kontrolü şarttır. Bu sabit yalnızca WordPress’in kaldırma süreci sırasında tanımlanır, bu yüzden dosya doğrudan çağrılırsa hiçbir şey çalışmaz.

Admin Sınıfı: Gerçek Bir Senaryo

Bir müşteri projesi düşünün: WooCommerce sipariş durumlarını özelleştiren bir eklenti. Admin tarafında menü eklemek ve ayar sayfası göstermek için:

<?php

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

class Benim_Eklentim_Admin {

    private $plugin_name;
    private $version;

    public function __construct( $plugin_name, $version ) {
        $this->plugin_name = $plugin_name;
        $this->version     = $version;
    }

    public function enqueue_styles() {
        $screen = get_current_screen();
        // Sadece kendi sayfamızda yükle
        if ( strpos( $screen->id, $this->plugin_name ) === false ) {
            return;
        }

        wp_enqueue_style(
            $this->plugin_name,
            BENIM_EKLENTIM_PLUGIN_URL . 'admin/css/benim-eklentim-admin.css',
            array(),
            $this->version,
            'all'
        );
    }

    public function enqueue_scripts() {
        $screen = get_current_screen();
        if ( strpos( $screen->id, $this->plugin_name ) === false ) {
            return;
        }

        wp_enqueue_script(
            $this->plugin_name,
            BENIM_EKLENTIM_PLUGIN_URL . 'admin/js/benim-eklentim-admin.js',
            array( 'jquery' ),
            $this->version,
            true
        );

        // JavaScript'e PHP verisi aktar
        wp_localize_script(
            $this->plugin_name,
            'BenimEklentimAdmin',
            array(
                'ajax_url' => admin_url( 'admin-ajax.php' ),
                'nonce'    => wp_create_nonce( 'benim_eklentim_nonce' ),
                'strings'  => array(
                    'kayit_basarili' => __( 'Kayıt başarılı!', 'benim-eklentim' ),
                    'hata_olustu'    => __( 'Bir hata oluştu.', 'benim-eklentim' ),
                ),
            )
        );
    }

    public function add_plugin_admin_menu() {
        add_menu_page(
            __( 'Benim Eklentim', 'benim-eklentim' ),
            __( 'Benim Eklentim', 'benim-eklentim' ),
            'manage_options',
            $this->plugin_name,
            array( $this, 'display_plugin_admin_page' ),
            'dashicons-admin-tools',
            65
        );

        add_submenu_page(
            $this->plugin_name,
            __( 'Ayarlar', 'benim-eklentim' ),
            __( 'Ayarlar', 'benim-eklentim' ),
            'manage_options',
            $this->plugin_name . '-settings',
            array( $this, 'display_plugin_settings_page' )
        );
    }

    public function display_plugin_admin_page() {
        include_once BENIM_EKLENTIM_PLUGIN_DIR . 'admin/partials/benim-eklentim-admin-display.php';
    }

    public function display_plugin_settings_page() {
        include_once BENIM_EKLENTIM_PLUGIN_DIR . 'admin/partials/benim-eklentim-settings-display.php';
    }
}

enqueue_styles ve enqueue_scripts içindeki get_current_screen() kontrolüne dikkat edin. CSS ve JS dosyalarını tüm admin sayfalarına değil, sadece kendi sayfanıza yükleyin. Bu hem performansı artırır hem de diğer eklentilerle çakışma riskini azaltır.

Dosya İsimlendirme Konvansiyonları

WordPress Coding Standards’a göre dosya isimlendirmesinde bazı kurallar vardır:

  • Sınıf dosyaları class- prefix’i alır: class-benim-eklentim.php
  • Dosya isimleri küçük harf ve tire kullanır, alt tire değil
  • Sınıf adları büyük harf ve alt tire kullanır: Benim_Eklentim
  • Fonksiyon ve değişken adlarında alt tire kullanılır: benim_eklentim_helper_func()
  • Sabitler tamamen büyük harf: BENIM_EKLENTIM_VERSION

Bu kurallara uymak, özellikle ekip çalışmasında kod tutarlılığı açısından kritiktir. Eklentinizi WordPress.org’da yayınlamayı düşünüyorsanız bu kurallar zaten zorunlu hale gelir.

Sürüm Yönetimi ve Güncelleme Kontrolü

Eklentiniz güncelleme aldığında veritabanı şemasının da güncellenmesi gerekebilir. Bunun için aktivasyon hook’u yeterli değildir çünkü güncelleme sırasında çalışmaz. Doğru yaklaşım:

<?php
// Ana sınıfın __construct veya run metodunda
public function check_version() {
    $installed_version = get_option( 'benim_eklentim_version' );

    if ( $installed_version !== BENIM_EKLENTIM_VERSION ) {
        require_once BENIM_EKLENTIM_PLUGIN_DIR . 'includes/class-benim-eklentim-activator.php';
        Benim_Eklentim_Activator::activate();
        update_option( 'benim_eklentim_version', BENIM_EKLENTIM_VERSION );
    }
}

Bu metodu plugins_loaded hook’una bağlayın. Her WordPress yüklenişinde sürüm kontrolü yapar, fark varsa migration işlemlerini çalıştırır. dbDelta() idempotent çalıştığı için bu yaklaşım güvenlidir.

WP-CLI ile Eklenti Scaffold

Her şeyi sıfırdan yazmak zorunda değilsiniz. WP-CLI’ın scaffold komutu bu yapıyı otomatik oluşturur:

# WP-CLI kurulumu yoksa önce kurun
curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
chmod +x wp-cli.phar
sudo mv wp-cli.phar /usr/local/bin/wp

# Eklenti scaffold oluştur
wp scaffold plugin benim-eklentim 
    --plugin_name="Benim Eklentim" 
    --plugin_description="Örnek eklenti açıklaması" 
    --plugin_author="Ahmet Yılmaz" 
    --plugin_author_uri="https://example.com" 
    --plugin_uri="https://example.com/eklenti" 
    --activate

# Oluşturulan yapıyı kontrol et
ls -la wp-content/plugins/benim-eklentim/

WP-CLI scaffold komutu tam olarak anlattığım yapıyı oluşturur. Üzerine kendi iş mantığınızı eklemeye başlayabilirsiniz. Bu, üretim ortamlarında hızlı başlamak için tercih ettiğim yöntemdir.

Güvenlik Temelleri: Boilerplate’e Dahil Edilmesi Gerekenler

Her dosyanın başındaki ABSPATH kontrolü zaten bahsettim. Bunlara ek olarak boilerplate’inize şunları ekleyin:

  • Nonce doğrulama: Form işlemlerinde ve AJAX çağrılarında wp_verify_nonce() kullanın
  • Yetki kontrolü: Admin işlemlerinde current_user_can() ile kontrol edin
  • Veri sanitizasyonu: $_POST ve $_GET verilerini her zaman sanitize edin, sanitize_text_field(), absint(), wp_kses_post() gibi fonksiyonları kullanın
  • Çıktı escape etme: esc_html(), esc_attr(), esc_url() kullanmadan hiçbir şey ekrana yazdırmayın
  • SQL injection koruması: Direkt SQL yazıyorsanız $wpdb->prepare() kullanın

Bunları baştan boilerplate’e dahil etmek, sonradan “acaba güvenli mi?” diye düşünmek zorunda kalmaktan kurtarır.

Sonuç

WordPress eklenti geliştirme, doğru temelle başlandığında gerçekten keyifli bir süreç. Anlattığım yapı, küçük bir yardımcı eklentiden büyük bir SaaS ürününe kadar ölçeklenebilir. Ana giriş dosyasının temiz tutulması, sorumlulukların ayrı sınıflara dağıtılması, hook’ların merkezi loader üzerinden yönetilmesi ve güvenlik kontrollerinin baştan yerleştirilmesi; bu dört prensip boyunca gittiğinizde bakımı kolay, güvenli ve ölçeklenebilir bir eklenti ortaya çıkar.

WP-CLI scaffold ile iskelet oluşturun, bu yazıdaki sınıf yapılarını referans alın ve kendi iş mantığınızı admin/ ile public/ sınıflarına ekleyerek ilerlein. Sonraki adım olarak WordPress Settings API ile ayar sayfası oluşturma ve AJAX ile dinamik işlemler konularını incelemenizi öneririm; bu temel olmadan o konular havada kalıyor.

Bir yanıt yazın

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