OOP ile WordPress Eklenti Geliştirme: Sınıf Yapısı
WordPress eklenti geliştirmeye yeni başlayanların yaptığı en büyük hatalardan biri, her şeyi tek bir PHP dosyasına doldurarak ilerlemek. Bir süre sonra o dosya 2000 satırı geçiyor, neyin ne olduğunu anlamak imkânsız hale geliyor ve en basit değişikliği yapmak bile saatler alıyor. OOP (Nesne Yönelimli Programlama) ile WordPress eklenti geliştirmek, bu kaosa son veriyor. Bu yazıda, gerçek dünya senaryolarından örneklerle sınıf yapısını nasıl kuracağınızı, neden bu şekilde kurmanız gerektiğini ve yaygın tuzaklardan nasıl kaçınacağınızı ele alacağız.
Neden OOP? Prosedürel Kodun Sınırları
Klasik WordPress eklentilerinde şöyle bir yapı görürsünüz:
<?php
// my-plugin.php
add_action('init', 'my_plugin_init');
add_filter('the_content', 'my_plugin_filter_content');
add_action('admin_menu', 'my_plugin_admin_menu');
function my_plugin_init() {
// 50 satır kod
}
function my_plugin_filter_content($content) {
// 80 satır kod
}
// ... devam eder
Bu yapının problemi ne? Global namespace kirliliği. my_plugin_init gibi bir fonksiyon adı bir gün başka bir eklentiyle çakışabilir. Üstelik değişkenler arasında veri taşımak için ya global değişkenler kullanıyorsunuz ya da her fonksiyona ayrı ayrı parametre geçiyorsunuz. 10 eklenti kurulu bir sitede bu kaos katlanarak büyür.
OOP ile bütün bu fonksiyonlar ve değişkenler bir sınıfın içinde yaşar. Çakışma riski neredeyse sıfıra iner, kod test edilebilir hale gelir ve en önemlisi başkası (ya da 6 ay sonraki siz) kodu okuyabilir.
Temel Sınıf Yapısı: Singleton Pattern
WordPress eklentilerinde en çok kullanılan pattern Singleton’dır. Eklentinizin tek bir örneğinin çalışmasını garanti eder.
<?php
/**
* Plugin Name: My Awesome Plugin
* Description: OOP ile geliştirilmiş örnek eklenti
* Version: 1.0.0
* Author: Ahmet Yılmaz
*/
if (!defined('ABSPATH')) {
exit;
}
final class My_Awesome_Plugin {
private static $instance = null;
private $version = '1.0.0';
public static function get_instance() {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct() {
$this->define_constants();
$this->includes();
$this->init_hooks();
}
// Clone ve unserialize engellemek için
private function __clone() {}
public function __wakeup() {
throw new Exception('Serializasyon izinli değil.');
}
private function define_constants() {
define('MAP_VERSION', $this->version);
define('MAP_PLUGIN_DIR', plugin_dir_path(__FILE__));
define('MAP_PLUGIN_URL', plugin_dir_url(__FILE__));
}
private function includes() {
require_once MAP_PLUGIN_DIR . 'includes/class-map-admin.php';
require_once MAP_PLUGIN_DIR . 'includes/class-map-frontend.php';
require_once MAP_PLUGIN_DIR . 'includes/class-map-api.php';
}
private function init_hooks() {
add_action('plugins_loaded', array($this, 'load_textdomain'));
register_activation_hook(__FILE__, array($this, 'activate'));
register_deactivation_hook(__FILE__, array($this, 'deactivate'));
}
public function load_textdomain() {
load_plugin_textdomain(
'my-awesome-plugin',
false,
dirname(plugin_basename(__FILE__)) . '/languages/'
);
}
public function activate() {
// Aktivasyon işlemleri
flush_rewrite_rules();
}
public function deactivate() {
// Deaktivasyon işlemleri
flush_rewrite_rules();
}
}
// Eklentiyi başlat
function run_my_awesome_plugin() {
return My_Awesome_Plugin::get_instance();
}
run_my_awesome_plugin();
final anahtar kelimesi sınıfın extend edilmesini engelliyor. __construct private olduğu için dışarıdan new My_Awesome_Plugin() çağrısı yapılamıyor. Bu yapı hem güvenli hem de öngörülebilir.
Dizin Yapısı: Her Şeyin Bir Yeri Olmalı
Sınıf yapısını kurmadan önce dosya organizasyonunu netleştirmeliyiz:
my-awesome-plugin/
├── my-awesome-plugin.php # Ana dosya (loader)
├── uninstall.php # Tamamen kaldırma işlemleri
├── includes/
│ ├── class-map-admin.php # Admin işlevleri
│ ├── class-map-frontend.php # Frontend işlevleri
│ ├── class-map-api.php # REST API endpoint'leri
│ ├── class-map-cpt.php # Custom Post Types
│ └── class-map-database.php # Veritabanı işlemleri
├── admin/
│ ├── css/
│ ├── js/
│ └── partials/ # Admin template dosyaları
├── public/
│ ├── css/
│ ├── js/
│ └── partials/ # Frontend template dosyaları
└── languages/
Bu yapı WordPress Plugin Boilerplate standardına yakın ve birçok geliştirici için tanıdık gelecektir. Bir şeyi nerede arayacağınızı hep bilirsiniz.
Admin Sınıfı: Yönetim Paneli İşlevleri
<?php
if (!defined('ABSPATH')) {
exit;
}
class MAP_Admin {
private $plugin_name;
private $version;
private $options;
public function __construct($plugin_name, $version) {
$this->plugin_name = $plugin_name;
$this->version = $version;
$this->options = get_option('map_settings', array());
$this->init_hooks();
}
private function init_hooks() {
add_action('admin_enqueue_scripts', array($this, 'enqueue_styles'));
add_action('admin_enqueue_scripts', array($this, 'enqueue_scripts'));
add_action('admin_menu', array($this, 'add_plugin_admin_menu'));
add_action('admin_init', array($this, 'register_settings'));
add_filter(
'plugin_action_links_' . MAP_BASENAME,
array($this, 'add_action_links')
);
}
public function enqueue_styles($hook) {
// Sadece eklenti sayfalarında yükle
if (strpos($hook, $this->plugin_name) === false) {
return;
}
wp_enqueue_style(
$this->plugin_name,
MAP_PLUGIN_URL . 'admin/css/map-admin.css',
array(),
$this->version,
'all'
);
}
public function enqueue_scripts($hook) {
if (strpos($hook, $this->plugin_name) === false) {
return;
}
wp_enqueue_script(
$this->plugin_name,
MAP_PLUGIN_URL . 'admin/js/map-admin.js',
array('jquery'),
$this->version,
true
);
// JavaScript'e PHP verisi geç
wp_localize_script($this->plugin_name, 'mapAdmin', array(
'ajaxUrl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('map_admin_nonce'),
));
}
public function add_plugin_admin_menu() {
add_options_page(
__('My Awesome Plugin Ayarları', 'my-awesome-plugin'),
__('MAP Ayarları', 'my-awesome-plugin'),
'manage_options',
$this->plugin_name,
array($this, 'display_plugin_admin_page')
);
}
public function display_plugin_admin_page() {
include_once MAP_PLUGIN_DIR . 'admin/partials/map-admin-display.php';
}
public function register_settings() {
register_setting(
'map_settings_group',
'map_settings',
array($this, 'sanitize_settings')
);
}
public function sanitize_settings($input) {
$sanitized = array();
if (isset($input['api_key'])) {
$sanitized['api_key'] = sanitize_text_field($input['api_key']);
}
if (isset($input['enable_feature'])) {
$sanitized['enable_feature'] = (bool) $input['enable_feature'];
}
return $sanitized;
}
public function add_action_links($links) {
$settings_link = '<a href="' . admin_url('options-general.php?page=' . $this->plugin_name) . '">'
. __('Ayarlar', 'my-awesome-plugin') . '</a>';
array_unshift($links, $settings_link);
return $links;
}
}
enqueue_styles ve enqueue_scripts metodlarındaki $hook kontrolüne dikkat edin. Bu kontrol olmadan betikleriniz WordPress’in her admin sayfasında yüklenir ve performans sorunlarına yol açar. Gerçek projelerde bu hatayı çok sık görüyorum.
Veritabanı Sınıfı: Temiz Sorgu Yönetimi
Eklentiniz özel tablo kullanıyorsa, veritabanı işlemlerini ayrı bir sınıfa almak hayat kurtarır:
<?php
if (!defined('ABSPATH')) {
exit;
}
class MAP_Database {
private $wpdb;
private $table_name;
private $charset_collate;
public function __construct() {
global $wpdb;
$this->wpdb = $wpdb;
$this->table_name = $wpdb->prefix . 'map_logs';
$this->charset_collate = $wpdb->get_charset_collate();
}
public function create_tables() {
$sql = "CREATE TABLE {$this->table_name} (
id bigint(20) NOT NULL AUTO_INCREMENT,
user_id bigint(20) NOT NULL,
action varchar(100) NOT NULL,
data longtext DEFAULT NULL,
created_at datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY user_id (user_id),
KEY created_at (created_at)
) {$this->charset_collate};";
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta($sql);
}
public function insert_log($user_id, $action, $data = array()) {
return $this->wpdb->insert(
$this->table_name,
array(
'user_id' => absint($user_id),
'action' => sanitize_key($action),
'data' => wp_json_encode($data),
'created_at' => current_time('mysql'),
),
array('%d', '%s', '%s', '%s')
);
}
public function get_logs_by_user($user_id, $limit = 50) {
$query = $this->wpdb->prepare(
"SELECT * FROM {$this->table_name}
WHERE user_id = %d
ORDER BY created_at DESC
LIMIT %d",
absint($user_id),
absint($limit)
);
return $this->wpdb->get_results($query);
}
public function delete_old_logs($days = 30) {
return $this->wpdb->query(
$this->wpdb->prepare(
"DELETE FROM {$this->table_name}
WHERE created_at < DATE_SUB(NOW(), INTERVAL %d DAY)",
absint($days)
)
);
}
public function drop_tables() {
$this->wpdb->query("DROP TABLE IF EXISTS {$this->table_name}");
}
}
$wpdb->prepare() kullanmak SQL injection saldırılarına karşı korumanın temel şartı. Bunu atlamak için hiçbir neden yok.
Abstract Sınıf ile Ortak Davranışları Tanımlamak
Büyüyen projelerde birden fazla widget veya post type eklemek gerekebilir. Abstract sınıflar burada çok işe yarıyor:
<?php
if (!defined('ABSPATH')) {
exit;
}
abstract class MAP_Base_Widget extends WP_Widget {
protected $widget_id;
protected $widget_name;
protected $widget_description;
protected $default_options;
public function __construct() {
parent::__construct(
$this->widget_id,
$this->widget_name,
array('description' => $this->widget_description)
);
$this->default_options = $this->get_default_options();
}
// Alt sınıfların doldurmak zorunda olduğu metodlar
abstract protected function get_default_options();
abstract public function widget_content($args, $instance);
// Her widget için ortak olan widget() metodu
public function widget($args, $instance) {
$instance = wp_parse_args($instance, $this->default_options);
echo wp_kses_post($args['before_widget']);
if (!empty($instance['title'])) {
echo wp_kses_post($args['before_title'])
. apply_filters('widget_title', $instance['title'])
. wp_kses_post($args['after_title']);
}
$this->widget_content($args, $instance);
echo wp_kses_post($args['after_widget']);
}
public function update($new_instance, $old_instance) {
$instance = array();
foreach ($this->default_options as $key => $value) {
if (isset($new_instance[$key])) {
$instance[$key] = sanitize_text_field($new_instance[$key]);
} else {
$instance[$key] = $value;
}
}
return $instance;
}
}
// Somut widget sınıfı
class MAP_Recent_Posts_Widget extends MAP_Base_Widget {
protected $widget_id = 'map_recent_posts';
protected $widget_name = 'MAP Son Yazılar';
protected $widget_description = 'Son yazıları listeler';
protected function get_default_options() {
return array(
'title' => __('Son Yazılar', 'my-awesome-plugin'),
'count' => 5,
);
}
public function widget_content($args, $instance) {
$posts = get_posts(array(
'numberposts' => absint($instance['count']),
'post_status' => 'publish',
));
if (empty($posts)) {
return;
}
echo '<ul class="map-recent-posts">';
foreach ($posts as $post) {
printf(
'<li><a href="%s">%s</a></li>',
esc_url(get_permalink($post->ID)),
esc_html($post->post_title)
);
}
echo '</ul>';
}
public function form($instance) {
$instance = wp_parse_args($instance, $this->default_options);
?>
<p>
<label for="<?php echo esc_attr($this->get_field_id('title')); ?>">
<?php esc_html_e('Başlık:', 'my-awesome-plugin'); ?>
</label>
<input class="widefat"
id="<?php echo esc_attr($this->get_field_id('title')); ?>"
name="<?php echo esc_attr($this->get_field_name('title')); ?>"
type="text"
value="<?php echo esc_attr($instance['title']); ?>">
</p>
<?php
}
}
Gerçek Dünya Senaryosu: Hook Yönetimi Sınıfı
Orta büyüklükte bir projede hook’lar dağınık hale gelebilir. Bunu yönetmek için ayrı bir loader sınıfı kullanmak standart bir pratik:
<?php
if (!defined('ABSPATH')) {
exit;
}
class MAP_Loader {
protected $actions = array();
protected $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']
);
}
}
}
Bu sınıfı ana plugin sınıfınızda şöyle kullanırsınız:
<?php
// Ana sınıfın __construct veya init metodunda
private function load_dependencies() {
require_once MAP_PLUGIN_DIR . 'includes/class-map-loader.php';
$this->loader = new MAP_Loader();
}
private function define_admin_hooks() {
$admin = new MAP_Admin($this->plugin_name, $this->version);
$this->loader->add_action('admin_enqueue_scripts', $admin, 'enqueue_styles');
$this->loader->add_action('admin_enqueue_scripts', $admin, 'enqueue_scripts');
$this->loader->add_action('admin_menu', $admin, 'add_plugin_admin_menu');
$this->loader->add_action('admin_init', $admin, 'register_settings');
}
private function define_public_hooks() {
$frontend = new MAP_Frontend($this->plugin_name, $this->version);
$this->loader->add_action('wp_enqueue_scripts', $frontend, 'enqueue_styles');
$this->loader->add_action('wp_enqueue_scripts', $frontend, 'enqueue_scripts');
$this->loader->add_filter('the_content', $frontend, 'filter_content');
}
public function run() {
$this->loader->run();
}
Bu yapıyla tüm hook kayıtlarını tek bir yerden takip edebiliyorsunuz. Hangi sınıfın hangi hook’a bağlı olduğunu anlamak için dosyalar arasında gezinmenize gerek kalmıyor.
Sıkça Yapılan Hatalar ve Kaçınma Yolları
Statik metod bağımlılığı: Her metodu statik yapmak cazip geliyor ama bu unit test yazmayı neredeyse imkânsız kılıyor. Sadece gerçekten statik olması gereken yerlerde kullanın.
Tek sınıfa her şeyi doldurmak: OOP kullanıyorsunuz diye tek bir 1500 satırlık sınıf yazmak, prosedürel koddan farklı değil. Sorumlulukları ayırın.
WordPress hook’larını constructor’da kaydetmek: __construct içinde doğrudan add_action ve add_filter çağrısı yapmak yerine özel bir init_hooks() metodu kullanın. Bu hem daha okunabilir hem de test sürecinde mock’lamayı kolaylaştırır.
global $wpdb her metodda: Veritabanı nesnesini constructor’da bir kez alıp sınıf property olarak saklayın. Her metodda global yazmak hem gereksiz hem de kötü pratik.
Aktivasyon hook’unu yanlış kaydetmek: register_activation_hook direkt __FILE__ sabitini bekler. Bir sınıf metodundan çağırırken array($this, 'activate') formatını kullanmak bazı WordPress sürümlerinde beklenmedik davranışlara neden olabilir. Ana plugin dosyasında kayıt yapıp oradan sınıf metodunu çağırın.
PHP Autoloading ile Performansı Artırmak
require_once ile her dosyayı tek tek dahil etmek yerine Composer’ın autoload mekanizmasını veya basit bir PSR-4 uyumlu autoloader kullanabilirsiniz:
<?php
// includes/class-map-autoloader.php
spl_autoload_register(function ($class_name) {
// Sadece bizim ön ekimizle başlayan sınıfları yükle
if (strpos($class_name, 'MAP_') !== 0) {
return;
}
// MAP_Admin -> class-map-admin.php
$class_file = 'class-' . str_replace(
array('MAP_', '_'),
array('map-', '-'),
strtolower($class_name)
) . '.php';
$file_path = MAP_PLUGIN_DIR . 'includes/' . $class_file;
if (file_exists($file_path)) {
require_once $file_path;
}
});
Bu autoloader sayesinde MAP_Admin, MAP_Frontend, MAP_Database gibi sınıfları kullandığınızda PHP otomatik olarak ilgili dosyayı yükleyecek. includes() metodundaki uzun require_once listesinden kurtuluyorsunuz.
Sonuç
OOP ile WordPress eklenti geliştirmek başlangıçta fazladan yapı kurma gibi görünebilir. Küçük, tek amaca hizmet eden bir eklenti için belki de öyledir. Ama eklenti büyüdükçe, ekip genişledikçe ya da ileride bakım yapmanız gerektiğinde bu yapı kendini kat kat geri öder.
Singleton pattern ile güvenli başlatma, Loader sınıfı ile merkezi hook yönetimi, Abstract sınıflar ile tekrar kullanılabilir bileşenler ve spl_autoload_register ile temiz dosya yönetimi, birlikte kurulduğunda sürdürülebilir bir eklenti mimarisi ortaya çıkarıyor.
Pratik olarak önereceğim yol şu: Sıfırdan başlayacaksanız WordPress Plugin Boilerplate Generator’ı inceleyin, nasıl bir temel yapı önerdiğini görün. Mevcut bir eklentiyi yeniden yazıyorsanız aşamalı gidin. Önce veritabanı sınıfını ayırın, sonra admin ve frontend’i, sonra rest’ini. Her adımda test edin. OOP’a geçiş bir kez yapıldığında, bir daha eski yönteme dönmek istemiyorsunuz.
