Dashboard Widget Ekleyen WordPress Eklentisi Nasıl Oluşturulur

WordPress ile biraz vakit geçirdiyseniz, admin panelinin sol tarafındaki dashboard’un aslında ne kadar güçlü bir yer olduğunu fark etmişsinizdir. Çoğu geliştirici bunu görmezden gelir, müşteriler ise oraya bakıp “şu kutucuklar ne işe yarıyor?” diye sorar. Oysa dashboard widget’ları, site yöneticisine gerçek zamanlı bilgi sunmanın en temiz yollarından biridir. Bu yazıda sıfırdan bir WordPress eklentisi yazacağız ve bu eklenti admin dashboard’una özel widget’lar ekleyecek. Basit bir “Hello World” değil, gerçekten işe yarayan, production ortamında kullanabileceğiniz bir şey.

Neden Dashboard Widget?

Müşteriye teslim ettiğiniz bir WordPress sitesini düşünün. Adam panele giriyor, onlarca menü, onlarca ayar. Ama “bugün kaç sipariş geldi?” sorusunun cevabını bulmak için WooCommerce raporlarına kadar gitmesi gerekiyor. Ya da “en son hangi kullanıcı kayıt oldu?” için Users sayfasını açması gerekiyor.

Dashboard widget’ı bu sorunu çözer. Doğru bilgiyi, doğru kişiye, doğru yerde gösterir. Biz de bu yazıda şu özelliklere sahip bir eklenti yazacağız:

  • Site istatistiklerini gösteren bir widget (post sayısı, kullanıcı sayısı, yorum sayısı)
  • Son kayıt olan kullanıcıları listeleyen bir widget
  • Widget başlıklarını sürükle-bırak ile yeniden düzenleyebilme
  • Kullanıcı rolüne göre widget görünürlüğü

Eklenti Yapısını Kuralım

WordPress eklenti geliştirmede dosya yapısı önemlidir. Karmaşık bir şey yapmıyoruz ama gene de düzenli olmak işimizi kolaylaştırır.

wp-content/plugins/my-dashboard-widgets/
├── my-dashboard-widgets.php
├── includes/
│   ├── class-widget-stats.php
│   └── class-widget-recent-users.php
├── assets/
│   ├── css/
│   │   └── dashboard.css
│   └── js/
│       └── dashboard.js
└── readme.txt

Ana eklenti dosyasını oluşturalım:

<?php
/**
 * Plugin Name: My Dashboard Widgets
 * Plugin URI:  https://example.com/my-dashboard-widgets
 * Description: Admin paneline özel dashboard widget'ları ekler.
 * Version:     1.0.0
 * Author:      Ahmet Yılmaz
 * Author URI:  https://example.com
 * License:     GPL-2.0+
 * Text Domain: my-dashboard-widgets
 * Domain Path: /languages
 */

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

// Eklenti sabitlerini tanımla
define( 'MDW_VERSION', '1.0.0' );
define( 'MDW_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
define( 'MDW_PLUGIN_URL', plugin_dir_url( __FILE__ ) );

// Gerekli dosyaları dahil et
require_once MDW_PLUGIN_DIR . 'includes/class-widget-stats.php';
require_once MDW_PLUGIN_DIR . 'includes/class-widget-recent-users.php';

// Ana sınıfı başlat
class My_Dashboard_Widgets {

    private static $instance = null;

    public static function get_instance() {
        if ( null === self::$instance ) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    private function __construct() {
        add_action( 'wp_dashboard_setup', array( $this, 'register_widgets' ) );
        add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) );
    }

    public function register_widgets() {
        $stats_widget = new MDW_Stats_Widget();
        $stats_widget->register();

        $users_widget = new MDW_Recent_Users_Widget();
        $users_widget->register();
    }

    public function enqueue_assets( $hook ) {
        if ( 'index.php' !== $hook ) {
            return;
        }
        wp_enqueue_style(
            'mdw-dashboard',
            MDW_PLUGIN_URL . 'assets/css/dashboard.css',
            array(),
            MDW_VERSION
        );
        wp_enqueue_script(
            'mdw-dashboard',
            MDW_PLUGIN_URL . 'assets/js/dashboard.js',
            array( 'jquery' ),
            MDW_VERSION,
            true
        );
    }
}

My_Dashboard_Widgets::get_instance();

Burada Singleton pattern kullandık. WordPress eklentilerinde bu pattern çok yaygındır çünkü aynı sınıfın birden fazla instance oluşturulmasını engeller. wp_dashboard_setup hook’u ise WordPress’in dashboard widget’larını yüklemek için belirlediği özel bir nokta.

İstatistik Widget’ını Yazalım

<?php
// includes/class-widget-stats.php

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

class MDW_Stats_Widget {

    private $widget_id   = 'mdw_site_stats';
    private $widget_name = 'Site İstatistikleri';

    public function register() {
        // Sadece yöneticilere göster
        if ( ! current_user_can( 'manage_options' ) ) {
            return;
        }

        wp_add_dashboard_widget(
            $this->widget_id,
            $this->widget_name,
            array( $this, 'render' ),
            array( $this, 'render_control' )
        );
    }

    public function render() {
        $stats = $this->get_stats();
        ?>
        <div class="mdw-stats-grid">
            <div class="mdw-stat-item">
                <span class="mdw-stat-number"><?php echo esc_html( $stats['posts'] ); ?></span>
                <span class="mdw-stat-label">Yayındaki Yazılar</span>
            </div>
            <div class="mdw-stat-item">
                <span class="mdw-stat-number"><?php echo esc_html( $stats['pages'] ); ?></span>
                <span class="mdw-stat-label">Sayfalar</span>
            </div>
            <div class="mdw-stat-item">
                <span class="mdw-stat-number"><?php echo esc_html( $stats['comments'] ); ?></span>
                <span class="mdw-stat-label">Onaylı Yorumlar</span>
            </div>
            <div class="mdw-stat-item">
                <span class="mdw-stat-number"><?php echo esc_html( $stats['users'] ); ?></span>
                <span class="mdw-stat-label">Toplam Kullanıcı</span>
            </div>
            <div class="mdw-stat-item">
                <span class="mdw-stat-number"><?php echo esc_html( $stats['plugins'] ); ?></span>
                <span class="mdw-stat-label">Aktif Eklenti</span>
            </div>
            <div class="mdw-stat-item <?php echo $stats['updates'] > 0 ? 'mdw-stat-warning' : ''; ?>">
                <span class="mdw-stat-number"><?php echo esc_html( $stats['updates'] ); ?></span>
                <span class="mdw-stat-label">Bekleyen Güncelleme</span>
            </div>
        </div>
        <p class="mdw-last-updated">
            Son güncelleme: <?php echo esc_html( current_time( 'd.m.Y H:i' ) ); ?>
        </p>
        <?php
    }

    private function get_stats() {
        // Transient kullanarak sorgu sayısını azalt
        $cached = get_transient( 'mdw_site_stats' );
        if ( false !== $cached ) {
            return $cached;
        }

        $update_data  = get_site_transient( 'update_plugins' );
        $update_count = isset( $update_data->response ) ? count( $update_data->response ) : 0;

        $stats = array(
            'posts'    => wp_count_posts()->publish,
            'pages'    => wp_count_posts( 'page' )->publish,
            'comments' => wp_count_comments()->approved,
            'users'    => count_users()['total_users'],
            'plugins'  => count( get_option( 'active_plugins', array() ) ),
            'updates'  => $update_count,
        );

        // 5 dakika cache'le
        set_transient( 'mdw_site_stats', $stats, 5 * MINUTE_IN_SECONDS );

        return $stats;
    }

    public function render_control() {
        // Widget ayarları formu buraya gelebilir
        echo '<p>Bu widget site istatistiklerini 5 dakikalık aralıklarla günceller.</p>';
    }
}

Burada dikkat çekmek istediğim kritik bir nokta var: Transient kullanımı. Her dashboard yüklemesinde veritabanı sorgusu yapmak, özellikle yoğun sitelerde performans sorununa yol açar. get_transient ve set_transient kullanarak verileri 5 dakika boyunca cache’liyoruz. Bu, küçük ama önemli bir production pratiği.

Son Kayıt Olan Kullanıcılar Widget’ı

<?php
// includes/class-widget-recent-users.php

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

class MDW_Recent_Users_Widget {

    private $widget_id   = 'mdw_recent_users';
    private $widget_name = 'Son Kayıt Olan Kullanıcılar';
    private $option_key  = 'mdw_recent_users_options';

    public function register() {
        if ( ! current_user_can( 'list_users' ) ) {
            return;
        }

        wp_add_dashboard_widget(
            $this->widget_id,
            $this->widget_name,
            array( $this, 'render' ),
            array( $this, 'render_control' )
        );
    }

    public function render() {
        $options      = $this->get_options();
        $recent_users = $this->get_recent_users( $options['count'] );

        if ( empty( $recent_users ) ) {
            echo '<p>Henüz kayıtlı kullanıcı yok.</p>';
            return;
        }

        echo '<ul class="mdw-user-list">';
        foreach ( $recent_users as $user ) {
            $avatar     = get_avatar( $user->ID, 32 );
            $edit_link  = get_edit_user_link( $user->ID );
            $registered = date_i18n( 'd.m.Y H:i', strtotime( $user->user_registered ) );
            $role_names = $this->get_role_display_names( $user->roles );

            printf(
                '<li class="mdw-user-item">
                    <span class="mdw-user-avatar">%s</span>
                    <span class="mdw-user-info">
                        <a href="%s"><strong>%s</strong></a>
                        <span class="mdw-user-meta">%s - %s</span>
                    </span>
                </li>',
                $avatar,
                esc_url( $edit_link ),
                esc_html( $user->display_name ),
                esc_html( $registered ),
                esc_html( implode( ', ', $role_names ) )
            );
        }
        echo '</ul>';

        $all_users_url = admin_url( 'users.php?orderby=registered&order=desc' );
        printf(
            '<p class="mdw-widget-footer"><a href="%s">Tüm kullanıcıları gör &rarr;</a></p>',
            esc_url( $all_users_url )
        );
    }

    private function get_recent_users( $count = 5 ) {
        return get_users( array(
            'number'  => absint( $count ),
            'orderby' => 'registered',
            'order'   => 'DESC',
            'fields'  => array( 'ID', 'display_name', 'user_registered', 'user_email' ),
        ) );
    }

    private function get_role_display_names( $roles ) {
        global $wp_roles;
        $names = array();
        foreach ( $roles as $role ) {
            if ( isset( $wp_roles->roles[ $role ]['name'] ) ) {
                $names[] = translate_user_role( $wp_roles->roles[ $role ]['name'] );
            }
        }
        return $names;
    }

    public function render_control() {
        $options = $this->get_options();

        // Form gönderildiyse kaydet
        if ( isset( $_POST['mdw_recent_users_count'] ) ) {
            check_admin_referer( 'mdw_widget_control' );
            $options['count'] = absint( $_POST['mdw_recent_users_count'] );
            $options['count'] = max( 1, min( 20, $options['count'] ) );
            update_option( $this->option_key, $options );
        }

        wp_nonce_field( 'mdw_widget_control' );
        ?>
        <p>
            <label for="mdw_recent_users_count">Gösterilecek kullanıcı sayısı:</label>
            <input
                type="number"
                id="mdw_recent_users_count"
                name="mdw_recent_users_count"
                value="<?php echo esc_attr( $options['count'] ); ?>"
                min="1"
                max="20"
                style="width: 60px;"
            />
        </p>
        <?php
    }

    private function get_options() {
        $defaults = array( 'count' => 5 );
        $options  = get_option( $this->option_key, $defaults );
        return wp_parse_args( $options, $defaults );
    }
}

render_control fonksiyonu önemli bir detay içeriyor: Widget ayarları. wp_add_dashboard_widget fonksiyonunun dördüncü parametresi, widget başlığının yanında bir “Configure” linki oluşturur. Kullanıcı bu linke tıkladığında render_control çalışır. Basit ama oldukça kullanışlı bir özellik.

CSS ile Görünümü Düzeltelim

/* assets/css/dashboard.css */

.mdw-stats-grid {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 12px;
    padding: 8px 0;
}

.mdw-stat-item {
    background: #f8f9fa;
    border: 1px solid #e2e4e7;
    border-radius: 4px;
    padding: 12px;
    text-align: center;
    transition: background 0.2s ease;
}

.mdw-stat-item:hover {
    background: #fff;
    border-color: #0073aa;
}

.mdw-stat-item.mdw-stat-warning {
    background: #fff8e5;
    border-color: #ffb900;
}

.mdw-stat-number {
    display: block;
    font-size: 28px;
    font-weight: 700;
    color: #1d2327;
    line-height: 1.2;
}

.mdw-stat-item.mdw-stat-warning .mdw-stat-number {
    color: #996800;
}

.mdw-stat-label {
    display: block;
    font-size: 11px;
    color: #646970;
    margin-top: 4px;
    text-transform: uppercase;
    letter-spacing: 0.5px;
}

.mdw-last-updated {
    color: #646970;
    font-size: 11px;
    margin: 8px 0 0;
    padding-top: 8px;
    border-top: 1px solid #f0f0f1;
}

/* Kullanıcı listesi */
.mdw-user-list {
    margin: 0;
    padding: 0;
    list-style: none;
}

.mdw-user-item {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 8px 0;
    border-bottom: 1px solid #f0f0f1;
}

.mdw-user-item:last-child {
    border-bottom: none;
}

.mdw-user-avatar img {
    border-radius: 50%;
    display: block;
}

.mdw-user-info {
    flex: 1;
    min-width: 0;
}

.mdw-user-info a {
    display: block;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.mdw-user-meta {
    display: block;
    font-size: 11px;
    color: #646970;
    margin-top: 2px;
}

.mdw-widget-footer {
    margin: 8px 0 0;
    padding-top: 8px;
    border-top: 1px solid #f0f0f1;
    text-align: right;
}

AJAX ile Otomatik Yenileme Ekleyelim

Statik bir widget yeterli ama biraz daha dinamik bir şey istiyorsanız, AJAX ile belirli aralıklarda otomatik yenileme yapabilirsiniz:

// assets/js/dashboard.js

(function($) {
    'use strict';

    var MDWDashboard = {

        init: function() {
            this.bindEvents();
            this.autoRefresh();
        },

        bindEvents: function() {
            $(document).on('click', '.mdw-refresh-btn', function(e) {
                e.preventDefault();
                MDWDashboard.refreshWidget($(this).data('widget'));
            });
        },

        autoRefresh: function() {
            // 5 dakikada bir istatistikleri yenile
            setInterval(function() {
                MDWDashboard.refreshWidget('mdw_site_stats');
            }, 5 * 60 * 1000);
        },

        refreshWidget: function(widgetId) {
            var $widget = $('#' + widgetId);
            if (!$widget.length) return;

            $widget.find('.inside').css('opacity', '0.5');

            $.ajax({
                url: ajaxurl,
                type: 'POST',
                data: {
                    action: 'mdw_refresh_widget',
                    widget_id: widgetId,
                    nonce: mdwData.nonce
                },
                success: function(response) {
                    if (response.success) {
                        $widget.find('.inside').html(response.data.html);
                    }
                },
                complete: function() {
                    $widget.find('.inside').css('opacity', '1');
                }
            });
        }
    };

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

})(jQuery);

Bu JavaScript kodu için ana eklenti dosyasına AJAX handler da eklememiz gerekiyor:

// Ana eklenti dosyasına ekle - __construct içinde
add_action( 'wp_ajax_mdw_refresh_widget', array( $this, 'ajax_refresh_widget' ) );

// Yeni method
public function ajax_refresh_widget() {
    check_ajax_referer( 'mdw_ajax_nonce', 'nonce' );

    if ( ! current_user_can( 'manage_options' ) ) {
        wp_send_json_error( array( 'message' => 'Yetki hatası' ) );
    }

    $widget_id = sanitize_text_field( $_POST['widget_id'] ?? '' );

    ob_start();
    if ( 'mdw_site_stats' === $widget_id ) {
        delete_transient( 'mdw_site_stats' );
        $widget = new MDW_Stats_Widget();
        $widget->render();
    }
    $html = ob_get_clean();

    wp_send_json_success( array( 'html' => $html ) );
}

// enqueue_assets methoduna ekle - script_add_data öncesinde
wp_localize_script(
    'mdw-dashboard',
    'mdwData',
    array( 'nonce' => wp_create_nonce( 'mdw_ajax_nonce' ) )
);

Gerçek Dünya Senaryosu: WooCommerce Entegrasyonu

Eğer site WooCommerce kullanıyorsa, dashboard widget’ınızı sipariş istatistikleriyle zenginleştirebilirsiniz. Bu çok istenilen bir özellik, özellikle mağaza sahipleri sürekli “bugün kaç sipariş geldi?” diye soruyor:

// Sadece WooCommerce aktifse çalışır
public function maybe_add_woo_widget() {
    if ( ! class_exists( 'WooCommerce' ) ) {
        return;
    }

    if ( ! current_user_can( 'view_woocommerce_reports' ) ) {
        return;
    }

    wp_add_dashboard_widget(
        'mdw_woo_stats',
        'WooCommerce Özet',
        array( $this, 'render_woo_widget' )
    );
}

public function render_woo_widget() {
    $today_start = strtotime( 'today midnight' );
    $today_end   = strtotime( 'tomorrow midnight' ) - 1;

    $today_orders = wc_get_orders( array(
        'status'     => array( 'processing', 'completed' ),
        'date_after'  => date( 'Y-m-d', $today_start ),
        'date_before' => date( 'Y-m-d', $today_end ),
        'return'     => 'ids',
        'limit'      => -1,
    ) );

    $pending_orders = wc_get_orders( array(
        'status' => array( 'pending', 'on-hold' ),
        'return' => 'ids',
        'limit'  => -1,
    ) );

    echo '<p><strong>Bugünkü siparişler:</strong> ' . count( $today_orders ) . '</p>';
    echo '<p><strong>Bekleyen siparişler:</strong> ' . count( $pending_orders ) . '</p>';

    if ( ! empty( $pending_orders ) ) {
        printf(
            '<p><a href="%s" class="button button-small">Bekleyen Siparişleri Gör</a></p>',
            esc_url( admin_url( 'edit.php?post_status=wc-pending&post_type=shop_order' ) )
        );
    }
}

Eklenti Geliştirirken Kaçınılması Gereken Hatalar

Yıllar içinde gördüğüm yaygın hataları paylaşayım:

  • Capability kontrolü atlamak: Her widget’ta current_user_can() kontrolü şart. Aksi halde editör rolündeki bir kullanıcı admin bilgilerini görebilir.
  • Doğrudan SQL sorguları: WordPress’in hazır fonksiyonları varken $wpdb->query() kullanmak gereksiz risk. get_users(), wp_count_posts() gibi fonksiyonlar zaten optimize edilmiş.
  • Output escaping atlamak: Widget içinde yazdığınız her şeyi esc_html(), esc_url(), esc_attr() ile kaçırmak zorunlu. XSS açığı bırakmak, özellikle admin panelinde, ciddi sonuçlar doğurur.
  • Transient kullanmamak: Her sayfa yüklemesinde ağır sorgular çalıştırmak performansı mahveder. İstatistik gibi “anlık güncelleme şart değil” verileri mutlaka cache’lenecek.
  • wp_dashboard_setup dışında widget kayıt etmek: Widget’ları farklı bir hook’ta kaydetmeye çalışmak ya hiç çalışmaz ya da beklenmedik davranışlara yol açar.

Eklentiyi Test Etmek

Geliştirme sürecinde şu ortamı kullanmanızı öneririm:

# Local WP veya wp-env ile test ortamı kur
npm install -g @wordpress/env

# Proje dizininde .wp-env.json oluştur
echo '{
  "core": "WordPress/WordPress",
  "plugins": [ "." ],
  "config": {
    "WP_DEBUG": true,
    "WP_DEBUG_LOG": true
  }
}' > .wp-env.json

# Ortamı başlat
wp-env start

# Admin paneline giriş: http://localhost:8888/wp-admin
# Kullanıcı: admin / Şifre: password

Test sırasında farklı kullanıcı rolleriyle (admin, editor, subscriber) giriş yapıp widget’ların doğru şekilde gösterilip gösterilmediğini kontrol edin. Özellikle current_user_can() kontrollerinin düzgün çalıştığını doğrulayın.

Sonuç

Dashboard widget’ı yazmak kulağa basit gelebilir ama üzerine düşününce oldukça derinlikli bir konu. Capability kontrolü, transient caching, output escaping, AJAX entegrasyonu… bunların hepsi bir arada yapılması gerekiyor. Bu yazıda yazdığımız eklenti bu gerekliliklerin tamamını karşılıyor.

Kodun tamamını kendi projenize uyarlarken şunları göz önünde bulundurun: Widget içeriğini ne sıklıkta güncellemek istediğinize göre transient süresini ayarlayın. Çok kullanıcılı sitelerde capability kontrollerini titizlikle yapın. WooCommerce entegrasyonu için class_exists() kontrolü her zaman önce gelmeli.

Bir sonraki adım olarak bu eklentiyi WordPress.org’a gönderebilir ya da müşterilerinize özel, beyaz etiketli versiyonlar üretebilirsiniz. Her iki durumda da temiz kod yazma alışkanlığı sizi uzun vadede kurtarır. Müşteri 3 yıl sonra “şunu ekle” dediğinde, kendi yazdığınız kod size tanıdık gelir.

Bir yanıt yazın

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