WordPress’te Özel Widget Oluşturma: WP_Widget Sınıfı Kullanımı

WordPress ile ciddi anlamda uğraşmaya başladığınızda, hazır widget’ların yetersiz kaldığı anları kaçınılmaz olarak yaşarsınız. Müşteri “şu köşeye son ürünlerimiz gelsin ama sadece belirli bir kategoriden” ya da “sidebar’a özel duyuru kutusu ekleyelim, admin panelinden kolayca değiştirebilelim” gibi istekler sıralamaya başlar. İşte tam bu noktada WP_Widget sınıfı devreye giriyor ve hayatınızı kurtarıyor.

WP_Widget Sınıfı Nedir?

WordPress, widget sistemini nesne yönelimli programlama (OOP) mantığıyla yönetir. WP_Widget sınıfı, kendi özel widget’larınızı oluşturmak için extend (genişletme) yapmanız gereken temel sınıftır. Bu sınıfı extend ettiğinizde, dört temel metodu override etmeniz beklenir:

  • __construct(): Widget’ın kimliğini, adını ve varsayılan ayarlarını tanımlar
  • widget(): Widget’ın frontend’de nasıl görüneceğini belirler
  • form(): Admin panelinde widget ayarları formunu oluşturur
  • update(): Form verilerini temizleyip kaydeder

Bu yapı sayesinde her widget, hem görsel hem de yönetimsel açıdan tamamen izole bir bileşen olarak çalışır. functions.php dosyasına ya da ayrı bir plugin dosyasına ekleyebilirsiniz, ikisi de geçerli yaklaşım.

Temel Widget Yapısı

Önce iskelet yapıyı görelim, sonra üzerine inşa edelim:

class My_Custom_Widget extends WP_Widget {

    public function __construct() {
        parent::__construct(
            'my_custom_widget',           // Widget ID (benzersiz olmalı)
            'Özel Widget',                // Widget adı (admin panelinde görünür)
            array(
                'description' => 'Bu benim özel widget'ım.',
                'classname'   => 'my-custom-widget-class',
            )
        );
    }

    // Frontend görünümü
    public function widget( $args, $instance ) {
        echo $args['before_widget'];
        echo $args['before_title'] . apply_filters( 'widget_title', $instance['title'] ) . $args['after_title'];
        echo '<p>Merhaba Dünya!</p>';
        echo $args['after_widget'];
    }

    // Admin formu
    public function form( $instance ) {
        $title = ! empty( $instance['title'] ) ? $instance['title'] : 'Varsayılan Başlık';
        ?>
        <p>
            <label for="<?php echo $this->get_field_id('title'); ?>">Başlık:</label>
            <input class="widefat" id="<?php echo $this->get_field_id('title'); ?>"
                   name="<?php echo $this->get_field_name('title'); ?>"
                   type="text" value="<?php echo esc_attr($title); ?>">
        </p>
        <?php
    }

    // Veri güncelleme
    public function update( $new_instance, $old_instance ) {
        $instance = array();
        $instance['title'] = ( ! empty( $new_instance['title'] ) )
            ? strip_tags( $new_instance['title'] )
            : '';
        return $instance;
    }
}

// Widget'ı kaydet
function register_my_custom_widget() {
    register_widget( 'My_Custom_Widget' );
}
add_action( 'widgets_init', 'register_my_custom_widget' );

Bu yapı, her özel widget’ın başlangıç noktasıdır. Şimdi gerçek dünya senaryolarına geçelim.

Senaryo 1: Son Yazılar Widget’ı (Özelleştirilebilir)

WordPress’in varsayılan “Son Yazılar” widget’ı oldukça kısıtlıdır. Müşteriler genellikle “kategori filtresi olsun”, “küçük resim görünsün”, “özet de çıksın” gibi istekler sıralar. İşte tam ihtiyaca göre yazılmış bir örnek:

class Custom_Recent_Posts_Widget extends WP_Widget {

    public function __construct() {
        parent::__construct(
            'custom_recent_posts',
            'Gelişmiş Son Yazılar',
            array( 'description' => 'Kategori filtreli, görsel destekli son yazılar widget'ı.' )
        );
    }

    public function widget( $args, $instance ) {
        $title      = apply_filters( 'widget_title', $instance['title'] ?? 'Son Yazılar' );
        $post_count = ! empty( $instance['post_count'] ) ? absint( $instance['post_count'] ) : 5;
        $category   = ! empty( $instance['category'] ) ? absint( $instance['category'] ) : 0;
        $show_thumb = ! empty( $instance['show_thumb'] ) ? (bool) $instance['show_thumb'] : false;

        $query_args = array(
            'post_type'      => 'post',
            'posts_per_page' => $post_count,
            'post_status'    => 'publish',
        );

        if ( $category > 0 ) {
            $query_args['cat'] = $category;
        }

        $recent_posts = new WP_Query( $query_args );

        echo $args['before_widget'];
        if ( $title ) {
            echo $args['before_title'] . $title . $args['after_title'];
        }

        if ( $recent_posts->have_posts() ) {
            echo '<ul class="custom-recent-posts">';
            while ( $recent_posts->have_posts() ) {
                $recent_posts->the_post();
                echo '<li>';
                if ( $show_thumb && has_post_thumbnail() ) {
                    echo '<a href="' . get_permalink() . '">';
                    the_post_thumbnail( 'thumbnail' );
                    echo '</a>';
                }
                echo '<a href="' . get_permalink() . '">' . get_the_title() . '</a>';
                echo '<span class="post-date">' . get_the_date() . '</span>';
                echo '</li>';
            }
            echo '</ul>';
            wp_reset_postdata();
        } else {
            echo '<p>Henüz yazı bulunamadı.</p>';
        }

        echo $args['after_widget'];
    }

    public function form( $instance ) {
        $title      = $instance['title'] ?? 'Son Yazılar';
        $post_count = $instance['post_count'] ?? 5;
        $category   = $instance['category'] ?? 0;
        $show_thumb = $instance['show_thumb'] ?? false;
        $categories = get_categories( array( 'hide_empty' => false ) );
        ?>
        <p>
            <label for="<?php echo $this->get_field_id('title'); ?>">Başlık:</label>
            <input class="widefat" id="<?php echo $this->get_field_id('title'); ?>"
                   name="<?php echo $this->get_field_name('title'); ?>"
                   type="text" value="<?php echo esc_attr($title); ?>">
        </p>
        <p>
            <label for="<?php echo $this->get_field_id('post_count'); ?>">Gösterilecek Yazı Sayısı:</label>
            <input class="tiny-text" id="<?php echo $this->get_field_id('post_count'); ?>"
                   name="<?php echo $this->get_field_name('post_count'); ?>"
                   type="number" min="1" max="20" value="<?php echo absint($post_count); ?>">
        </p>
        <p>
            <label for="<?php echo $this->get_field_id('category'); ?>">Kategori:</label>
            <select class="widefat" id="<?php echo $this->get_field_id('category'); ?>"
                    name="<?php echo $this->get_field_name('category'); ?>">
                <option value="0">Tümü</option>
                <?php foreach ( $categories as $cat ) : ?>
                    <option value="<?php echo $cat->term_id; ?>"
                        <?php selected( $category, $cat->term_id ); ?>>
                        <?php echo esc_html( $cat->name ); ?>
                    </option>
                <?php endforeach; ?>
            </select>
        </p>
        <p>
            <input type="checkbox" id="<?php echo $this->get_field_id('show_thumb'); ?>"
                   name="<?php echo $this->get_field_name('show_thumb'); ?>"
                   value="1" <?php checked( $show_thumb, true ); ?>>
            <label for="<?php echo $this->get_field_id('show_thumb'); ?>">Küçük Resim Göster</label>
        </p>
        <?php
    }

    public function update( $new_instance, $old_instance ) {
        $instance               = array();
        $instance['title']      = sanitize_text_field( $new_instance['title'] );
        $instance['post_count'] = absint( $new_instance['post_count'] );
        $instance['category']   = absint( $new_instance['category'] );
        $instance['show_thumb'] = ! empty( $new_instance['show_thumb'] ) ? 1 : 0;
        return $instance;
    }
}

add_action( 'widgets_init', function() {
    register_widget( 'Custom_Recent_Posts_Widget' );
});

Senaryo 2: İletişim Bilgileri Widget’ı

Müşterilerin en sık istediği şeylerden biri, sidebar’a telefon, adres ve e-posta bilgilerinin kolayca yönetilebildiği bir widget. Bunu her seferinde tema dosyalarına hard-code yazmak hem tehlikeli hem de müşteriyi bağımlı kılar:

class Contact_Info_Widget extends WP_Widget {

    public function __construct() {
        parent::__construct(
            'contact_info_widget',
            'İletişim Bilgileri',
            array( 'description' => 'Adres, telefon ve e-posta bilgilerini gösterir.' )
        );
    }

    public function widget( $args, $instance ) {
        $title   = apply_filters( 'widget_title', $instance['title'] ?? 'İletişim' );
        $phone   = $instance['phone'] ?? '';
        $email   = $instance['email'] ?? '';
        $address = $instance['address'] ?? '';

        echo $args['before_widget'];
        if ( $title ) {
            echo $args['before_title'] . esc_html( $title ) . $args['after_title'];
        }

        echo '<div class="contact-info-widget">';

        if ( $phone ) {
            echo '<p class="contact-phone">';
            echo '<strong>Telefon:</strong> ';
            echo '<a href="tel:' . esc_attr( preg_replace('/D/', '', $phone) ) . '">';
            echo esc_html( $phone );
            echo '</a></p>';
        }

        if ( $email ) {
            echo '<p class="contact-email">';
            echo '<strong>E-posta:</strong> ';
            echo '<a href="mailto:' . sanitize_email( $email ) . '">';
            echo esc_html( $email );
            echo '</a></p>';
        }

        if ( $address ) {
            echo '<p class="contact-address">';
            echo '<strong>Adres:</strong> ';
            echo nl2br( esc_html( $address ) );
            echo '</p>';
        }

        echo '</div>';
        echo $args['after_widget'];
    }

    public function form( $instance ) {
        ?>
        <p>
            <label>Başlık:</label>
            <input class="widefat" name="<?php echo $this->get_field_name('title'); ?>"
                   type="text" value="<?php echo esc_attr( $instance['title'] ?? 'İletişim' ); ?>">
        </p>
        <p>
            <label>Telefon:</label>
            <input class="widefat" name="<?php echo $this->get_field_name('phone'); ?>"
                   type="text" value="<?php echo esc_attr( $instance['phone'] ?? '' ); ?>">
        </p>
        <p>
            <label>E-posta:</label>
            <input class="widefat" name="<?php echo $this->get_field_name('email'); ?>"
                   type="email" value="<?php echo esc_attr( $instance['email'] ?? '' ); ?>">
        </p>
        <p>
            <label>Adres:</label>
            <textarea class="widefat" name="<?php echo $this->get_field_name('address'); ?>"
                      rows="3"><?php echo esc_textarea( $instance['address'] ?? '' ); ?></textarea>
        </p>
        <?php
    }

    public function update( $new_instance, $old_instance ) {
        return array(
            'title'   => sanitize_text_field( $new_instance['title'] ),
            'phone'   => sanitize_text_field( $new_instance['phone'] ),
            'email'   => sanitize_email( $new_instance['email'] ),
            'address' => sanitize_textarea_field( $new_instance['address'] ),
        );
    }
}

Senaryo 3: WooCommerce Uyumlu Kampanya Banner Widget’ı

E-ticaret sitelerinde sidebar’a indirim banneri ya da çağrı butonu koymak çok yaygın bir ihtiyaç. Bu örnek, WooCommerce’in yüklü olup olmadığını kontrol ederek çalışır:

class Promo_Banner_Widget extends WP_Widget {

    public function __construct() {
        parent::__construct(
            'promo_banner_widget',
            'Kampanya Banner',
            array( 'description' => 'Özel kampanya metni ve CTA butonu gösterir.' )
        );
    }

    public function widget( $args, $instance ) {
        $title      = $instance['title'] ?? '';
        $content    = $instance['content'] ?? '';
        $btn_text   = $instance['btn_text'] ?? 'Hemen İncele';
        $btn_url    = $instance['btn_url'] ?? '#';
        $bg_color   = $instance['bg_color'] ?? '#ff6b35';
        $text_color = $instance['text_color'] ?? '#ffffff';

        echo $args['before_widget'];

        $style = sprintf(
            'background-color: %s; color: %s; padding: 20px; border-radius: 8px; text-align: center;',
            esc_attr( $bg_color ),
            esc_attr( $text_color )
        );

        echo '<div class="promo-banner-widget" style="' . $style . '">';

        if ( $title ) {
            echo '<h3 style="color: ' . esc_attr($text_color) . '; margin-top: 0;">';
            echo esc_html( $title );
            echo '</h3>';
        }

        if ( $content ) {
            echo '<p>' . wp_kses_post( $content ) . '</p>';
        }

        if ( $btn_text && $btn_url ) {
            echo '<a href="' . esc_url( $btn_url ) . '" class="promo-btn" ';
            echo 'style="display:inline-block; padding: 10px 20px; background: rgba(0,0,0,0.2); ';
            echo 'color: ' . esc_attr($text_color) . '; text-decoration: none; border-radius: 4px;">';
            echo esc_html( $btn_text );
            echo '</a>';
        }

        echo '</div>';
        echo $args['after_widget'];
    }

    public function form( $instance ) {
        ?>
        <p>
            <label>Başlık:</label>
            <input class="widefat" name="<?php echo $this->get_field_name('title'); ?>"
                   type="text" value="<?php echo esc_attr( $instance['title'] ?? '' ); ?>">
        </p>
        <p>
            <label>İçerik:</label>
            <textarea class="widefat" name="<?php echo $this->get_field_name('content'); ?>"
                      rows="4"><?php echo esc_textarea( $instance['content'] ?? '' ); ?></textarea>
        </p>
        <p>
            <label>Buton Metni:</label>
            <input class="widefat" name="<?php echo $this->get_field_name('btn_text'); ?>"
                   type="text" value="<?php echo esc_attr( $instance['btn_text'] ?? 'Hemen İncele' ); ?>">
        </p>
        <p>
            <label>Buton URL:</label>
            <input class="widefat" name="<?php echo $this->get_field_name('btn_url'); ?>"
                   type="url" value="<?php echo esc_url( $instance['btn_url'] ?? '' ); ?>">
        </p>
        <p>
            <label>Arkaplan Rengi:</label>
            <input type="color" name="<?php echo $this->get_field_name('bg_color'); ?>"
                   value="<?php echo esc_attr( $instance['bg_color'] ?? '#ff6b35' ); ?>">
        </p>
        <p>
            <label>Yazı Rengi:</label>
            <input type="color" name="<?php echo $this->get_field_name('text_color'); ?>"
                   value="<?php echo esc_attr( $instance['text_color'] ?? '#ffffff' ); ?>">
        </p>
        <?php
    }

    public function update( $new_instance, $old_instance ) {
        return array(
            'title'      => sanitize_text_field( $new_instance['title'] ),
            'content'    => wp_kses_post( $new_instance['content'] ),
            'btn_text'   => sanitize_text_field( $new_instance['btn_text'] ),
            'btn_url'    => esc_url_raw( $new_instance['btn_url'] ),
            'bg_color'   => sanitize_hex_color( $new_instance['bg_color'] ),
            'text_color' => sanitize_hex_color( $new_instance['text_color'] ),
        );
    }
}

add_action( 'widgets_init', function() {
    register_widget( 'Promo_Banner_Widget' );
});

Birden Fazla Widget’ı Tek Seferde Kaydetmek

Projenizde birden fazla özel widget varsa, hepsini tek bir widgets_init action’ına bağlamak temiz bir yaklaşımdır:

function register_all_custom_widgets() {
    $widgets = array(
        'My_Custom_Widget',
        'Custom_Recent_Posts_Widget',
        'Contact_Info_Widget',
        'Promo_Banner_Widget',
    );

    foreach ( $widgets as $widget_class ) {
        if ( class_exists( $widget_class ) ) {
            register_widget( $widget_class );
        }
    }
}
add_action( 'widgets_init', 'register_all_custom_widgets' );

Widget’lara CSS Eklemek

Widget’ınızın özel CSS’e ihtiyacı varsa, bunu doğrudan widget() metoduna inline yazmak yerine düzgün şekilde sıraya almak gerekir:

class Styled_Widget extends WP_Widget {

    public function __construct() {
        parent::__construct( 'styled_widget', 'Stillenmiş Widget' );
        // Sadece bu widget frontend'de varsa CSS yükle
        add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_widget_styles' ) );
    }

    public function enqueue_widget_styles() {
        // Widget aktif sidebar'da mı kontrol et
        if ( is_active_widget( false, false, $this->id_base, true ) ) {
            wp_enqueue_style(
                'styled-widget-css',
                get_template_directory_uri() . '/assets/css/styled-widget.css',
                array(),
                '1.0.0'
            );
        }
    }

    public function widget( $args, $instance ) {
        echo $args['before_widget'];
        echo '<div class="styled-widget-wrapper">';
        echo '<p>' . esc_html( $instance['content'] ?? 'Widget içeriği buraya gelecek.' ) . '</p>';
        echo '</div>';
        echo $args['after_widget'];
    }

    public function form( $instance ) {
        ?>
        <p>
            <label>İçerik:</label>
            <input class="widefat" name="<?php echo $this->get_field_name('content'); ?>"
                   type="text" value="<?php echo esc_attr( $instance['content'] ?? '' ); ?>">
        </p>
        <?php
    }

    public function update( $new_instance, $old_instance ) {
        return array(
            'content' => sanitize_text_field( $new_instance['content'] ),
        );
    }
}

Widget Önbellekleme (Caching)

Eğer widget’ınız ağır bir veritabanı sorgusu çalıştırıyorsa, transient API ile önbelleğe almak performansı ciddi ölçüde artırır:

public function widget( $args, $instance ) {
    $cache_key = 'my_widget_cache_' . $this->id;
    $cached    = get_transient( $cache_key );

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

    ob_start();

    echo $args['before_widget'];
    // ... ağır sorgu ve çıktı işlemleri ...
    $heavy_query = new WP_Query( array(
        'post_type'      => 'product',
        'posts_per_page' => 10,
        'meta_key'       => '_featured',
        'meta_value'     => 'yes',
    ));

    if ( $heavy_query->have_posts() ) {
        echo '<ul>';
        while ( $heavy_query->have_posts() ) {
            $heavy_query->the_post();
            echo '<li><a href="' . get_permalink() . '">' . get_the_title() . '</a></li>';
        }
        echo '</ul>';
        wp_reset_postdata();
    }
    echo $args['after_widget'];

    $output = ob_get_clean();

    // 1 saat önbellekte tut
    set_transient( $cache_key, $output, HOUR_IN_SECONDS );

    echo $output;
}

// Widget güncellendiğinde önbelleği temizle
public function update( $new_instance, $old_instance ) {
    delete_transient( 'my_widget_cache_' . $this->id );
    // ... normal update işlemleri
    return $new_instance;
}

Dikkat Edilmesi Gereken Noktalar

Widget geliştirirken sık yapılan hatalar ve pratik uyarılar:

  • Sanitizasyon asla ihmal edilmemeli: form() metodunda kullanıcı verisini ekrana yazdırırken esc_attr(), esc_html(), esc_url() fonksiyonlarını kullanmadan geçmeyin
  • update() metodunda doğru sanitizasyon türünü seçin: Metin için sanitize_text_field(), HTML içerikli alanlar için wp_kses_post(), URL için esc_url_raw(), renk kodları için sanitize_hex_color() kullanın
  • wp_reset_postdata() unutmayın: WP_Query ile sorgu yaptıktan sonra bunu çağırmazsanız, sayfanın geri kalanındaki the loop bozulabilir
  • Widget ID’leri benzersiz olsun: __construct() içindeki ilk parametre sitenizde başka hiçbir widget ile çakışmamalı
  • $args değerlerine dokunmayın: before_widget, after_widget, before_title, after_title değerleri tema tarafından belirlenir, bunları kendi isteğinize göre değiştirmeyin
  • PHP 8 uyumluluğu: Null coalescing operator (??) kullanımında dikkatli olun, hedef PHP sürümünüzü göz önünde bulundurun

Block Editor ve Widget’ların Geleceği

WordPress 5.8 ile birlikte Gutenberg block editörü sidebar widget’larını da kapsama almaya başladı. Bu durum WP_Widget tabanlı widget’ları geçersiz kılmıyor, ancak uzun vadeli projelerde Block Widget yazmayı da düşünmek gerekiyor.

Mevcut temalar için functions.php‘ye şu satırı ekleyerek klasik widget editörünü koruyabilirsiniz:

// Klasik widget editörünü zorla kullan (Gutenberg widget editörünü devre dışı bırak)
add_filter( 'gutenberg_use_widgets_block_editor', '__return_false' );
add_filter( 'use_widgets_block_editor', '__return_false' );

Bu, özellikle eski temalar ve çok sayıda aktif WP_Widget kullanan sitelerde geçiş döneminde hayat kurtarır.

Sonuç

WP_Widget sınıfı, WordPress’in en sağlam ve test edilmiş yapı taşlarından biri. Doğru kullanıldığında, müşterinize admin panelinden kolayca yönetebileceği, güvenli ve performanslı bileşenler sunar. Her proje için sıfırdan yazmak zorunda değilsiniz; bu yazıdaki iskelet yapıyı alıp ihtiyacınıza göre şekillendirebilirsiniz.

Önemli olan, sanitizasyonu ve escaping’i asla atlamak, sorgu yaptıktan sonra wp_reset_postdata() çağırmak ve widget ID’lerinin benzersizliğini sağlamak. Bunlara dikkat ettiğinizde, hazır plugin almak yerine tam istediğiniz işi yapan, sitenizin geri kalanıyla uyumlu özel widget’lar geliştirmek oldukça tatmin edici bir hal alıyor.

Bir yanıt yazın

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