Tworzenie własnych widżetów to świetny sposób na rozszerzenie możliwości motywu lub wtyczki oraz dostarczenie redaktorom wygodnych, wielokrotnego użytku modułów do zarządzania treścią. W praktyce jest to połączenie niewielkiej logiki PHP, kilku bezpiecznych pól formularzy i przemyślanego HTML/CSS, które razem dowożą elastyczny komponent dla panelu bocznego, stopki albo dowolnego obszaru widgetów. Poniżej znajdziesz kompletny przewodnik: od architektury i podstaw API, przez implementację, aż po bezpieczeństwo, wydajność i testowanie.
Podstawy działania i architektura API widżetów
Zanim napiszesz pierwszy własny widżet, warto zrozumieć jego architekturę. Widget to klasa PHP, która rozszerza trzon systemu i obsługuje trzy główne strumienie: wyświetlanie w front-endzie, formularz konfiguracji w panelu i zapis/aktualizację ustawień. Architektura jest dojrzała, przewidywalna i dobrze udokumentowana, a całość spina mechanizm inicjalizacji podczas hooka widgets_init, gdzie następuje rejestracja klas widgetów.
Najistotniejsze elementy, które warto zapamiętać:
- Widget jest klasą rozszerzającą WP_Widget. To ona definiuje interfejs (metody), który musisz zaimplementować.
- Rejestracja odbywa się przy pomocy funkcji register_widget wywołanej podczas inicjalizacji widżetów.
- Wyświetlanie w szablonie odbywa się przez dynamic_sidebar lub przypisane obszary widgetów (sidebary, stopki, kolumny), które motyw deklaruje w functions.php.
- Formularz konfiguracji w panelu wp-admin renderujesz w metodzie form(), a logikę walidacji i zapisu implementujesz w metodzie update().
- Warstwy front-end i admin są od siebie oddzielone – to pozwala dbać o czystość odpowiedzialności i testowalność.
Warto też pamiętać o spójności nazw i przestrzeganiu standardów kodowania. Dzięki temu widget jest przewidywalny dla innych deweloperów i łatwy w utrzymaniu, a Twoje API lepiej integruje się z ekosystemem WordPress.
Pierwszy customowy widżet – struktura wtyczki i szkic klasy
Najczystszy sposób na stworzenie własnego widgetu to umieszczenie go w oddzielnej wtyczce. Pozwoli to ponownie wykorzystać komponent w innych motywach i środowiskach oraz ułatwi aktualizacje. Minimalna struktura katalogów może wyglądać tak:
- wp-content/plugins/moje-widgets/
- wp-content/plugins/moje-widgets/moje-widgets.php
- wp-content/plugins/moje-widgets/includes/ (plik z klasą widgetu)
- wp-content/plugins/moje-widgets/assets/css/ (opcjonalnie)
- wp-content/plugins/moje-widgets/assets/js/ (opcjonalnie)
- wp-content/plugins/moje-widgets/languages/ (opcjonalnie do tłumaczeń)
Plik główny wtyczki może zawierać standardowy nagłówek i hook inicjalizujący rejestrację:
/*
Plugin Name: Moje Widżety
Description: Zestaw przykładowych widżetów
Version: 1.0.0
Author: Ja
Text Domain: moje-widgets
*/
if ( ! defined( 'ABSPATH’ ) ) {
exit;
}
require_once plugin_dir_path( __FILE__ ) . 'includes/class-moje-news-widget.php’;
add_action( 'widgets_init’, function() {
register_widget( 'Moje_News_Widget’ );
} );
Klasa widgetu (w includes/class-moje-news-widget.php) rozszerzy WP_Widget i zdefiniuje niezbędne metody. Szkic minimalnej wersji:
class Moje_News_Widget extends WP_Widget {
public function __construct() {
parent::__construct(
'moje_news_widget’,
__( 'Najnowsze wpisy (custom)’, 'moje-widgets’ ),
[ 'description’ => __( 'Prosty widżet wyświetlający ostatnie wpisy.’, 'moje-widgets’ ) ]
);
}
public function widget()( $args, $instance ) {
// render front-end
}
public function form()( $instance ) {
// formularz w panelu
}
public function update()( $new_instance, $old_instance ) {
// walidacja i zapis
}
}
Powyższy szkielet ilustruje minimalny kontrakt: konstruktor z nazwą i opisem, metoda renderująca front (widget), formularz ustawień (form) i funkcja walidująca oraz zapisująca dane (update). Dzięki konsekwentnemu wykorzystaniu __() i _e() od początku zyskujesz gotowość do tłumaczeń (i18n).
Kluczowy krok to rejestracja przy użyciu register_widget (zwróć uwagę na poprawną nazwę klasy). Najwięcej błędów początkujących wynika właśnie z braku rejestracji, literówek lub nieodpowiedniego ładowania pliku z klasą.
Formularz w panelu, typy pól i zapis ustawień
Konfiguracja w panelu powinna być intuicyjna i maksymalnie prosta; użytkownik nie musi rozumieć kulis technicznych. Zacznijmy od wspólnych pól: tytuł, liczba elementów do wyświetlenia oraz kategoria (opcjonalnie). W metodzie form($instance) pobieramy bieżące wartości i wyświetlamy odpowiednie inputy. Skorzystaj z $this->get_field_id() i $this->get_field_name(), aby zapewnić unikalność atrybutów:
public function form( $instance ) {
$title = isset( $instance[’title’] ) ? $instance[’title’] : ”;
$limit = isset( $instance[’limit’] ) ? (int) $instance[’limit’] : 5;
$title_id = $this->get_field_id( 'title’ );
$title_name = $this->get_field_name( 'title’ );
$limit_id = $this->get_field_id( 'limit’ );
$limit_name = $this->get_field_name( 'limit’ );
?>
<p>
<label for=”<?php echo esc_attr( $title_id ); ?>”><?php _e( 'Tytuł’, 'moje-widgets’ ); ?></label>
<input class=”widefat” id=”<?php echo esc_attr( $title_id ); ?>” name=”<?php echo esc_attr( $title_name ); ?>” type=”text” value=”<?php echo esc_attr( $title ); ?>” />
</p>
<p>
<label for=”<?php echo esc_attr( $limit_id ); ?>”><?php _e( 'Liczba wpisów’, 'moje-widgets’ ); ?></label>
<input id=”<?php echo esc_attr( $limit_id ); ?>” name=”<?php echo esc_attr( $limit_name ); ?>” type=”number” min=”1″ max=”20″ value=”<?php echo esc_attr( $limit ); ?>” />
</p>
<?php
}
Teraz zapiszmy wartości i oczyśćmy dane wejściowe w update(). Nawet w formularzu admina obowiązuje twarda dyscyplina sanitizacji, bo dane mogą pochodzić z różnych źródeł, w tym importów czy narzędzi zewnętrznych:
public function update( $new_instance, $old_instance ) {
$instance = [];
$instance[’title’] = isset( $new_instance[’title’] ) ? sanitize_text_field( $new_instance[’title’] ) : ”;
$instance[’limit’] = isset( $new_instance[’limit’] ) ? absint( $new_instance[’limit’] ) : 5;
return $instance;
}
Lista wskazówek przy formularzach:
- Unikaj zbędnych pól – każde pole zwiększa ryzyko błędu i obciążenie poznawcze.
- Waliduj wartości graniczne (np. limit wpisów), aby nie doprowadzić do ciężkich zapytań.
- Stosuj sanitize_text_field, esc_attr, esc_url, absint, floatval itp. w zależności od typu danych.
- Używaj etykiet i opisów – są ważne dla dostępności i lepszego UX w panelu.
- Zadbaj o zgodność z tłumaczeniami (__, _e) oraz o kontekst (esc_html_x, _x) dla wieloznacznych wyrażeń.
Renderowanie front-endu, stylistyka, wydajność i ważne niuanse
Kluczowa jest zgodność z ramami motywu, dlatego podczas renderowania korzystaj z argumentów przekazanych do metody widget($args, $instance): $args[’before_widget’], $args[’before_title’], $args[’after_title’], $args[’after_widget’]. Dzięki temu wygląd pozostanie spójny z motywem i innymi elementami interfejsu.
Przykład prostej implementacji:
public function widget( $args, $instance ) {
echo $args[’before_widget’];
$title = ! empty( $instance[’title’] ) ? $instance[’title’] : ”;
if ( $title ) {
echo $args[’before_title’] . esc_html( $title ) . $args[’after_title’];
}
$limit = isset( $instance[’limit’] ) ? absint( $instance[’limit’] ) : 5;
$q = new WP_Query([
'posts_per_page’ => $limit,
'no_found_rows’ => true,
'ignore_sticky_posts’ => true,
]);
if ( $q->have_posts() ) {
echo '<ul class=”mw-news”>’;
while ( $q->have_posts() ) {
$q->the_post();
echo '<li class=”mw-news__item”><a href=”’ . esc_url( get_permalink() ) . '”>’ . esc_html( get_the_title() ) . '</a></li>’;
}
echo '</ul>’;
wp_reset_postdata();
} else {
echo '<p class=”mw-news__empty”>’ . esc_html__( 'Brak wpisów do wyświetlenia.’, 'moje-widgets’ ) . '</p>’;
}
echo $args[’after_widget’];
}
Ustawienia wydajności:
- no_found_rows => true ogranicza koszty liczenia wyników, co ma znaczenie, gdy wyświetlasz tylko listę.
- ignore_sticky_posts => true, jeżeli nie chcesz specjalnie traktować sticky.
- fields => 'ids’ i dodatkowe zapytania na post_meta, jeśli pobierasz tylko fragment danych.
Aby odciążyć bazę, rozważ zastosowanie mechanizmów cache (transienty lub obiektowy cache):
$cache_key = 'mw_news_’ . md5( serialize( $instance ) . get_locale() );
$html = get_transient( $cache_key );
if ( false === $html ) {
ob_start();
// … wygeneruj HTML (jak wyżej)
$html = ob_get_clean();
set_transient( $cache_key, $html, HOUR_IN_SECONDS );
}
echo $html;
Skuteczna strategia to czyszczenie transientów na wydarzeniach, np. save_post. Możesz też rozważyć wp_cache_set/wp_cache_get, jeśli środowisko korzysta z zewnętrznego back-endu cache.
Porządek w CSS/JS:
- Nadawaj unikatowe klasy prefiksowane (np. .mw-news, .mw-news__item), aby unikać konfliktów.
- Ładuj style i skrypty warunkowo – tylko jeśli widget jest aktywny. Pomaga w tym is_active_widget().
- Unikaj inline CSS poza drobnymi wyjątkami; preferuj wp_enqueue_style/wp_enqueue_script z wersjonowaniem.
- Dbaj o dostępność: rozmiar interakcji, fokus, aria-label, odpowiedni kontrast.
Nigdy nie wyświetlaj niesprawdzonych danych bez właściwego esc_* – to pierwsza linia obrony przed XSS. W przykładach korzystaj z esc_html, esc_attr oraz esc_url w zależności od kontekstu.
Bezpieczeństwo, internacjonalizacja i zgodność z edytorem blokowym
Widgety dziedziczą wszystkie ryzyka typowych wtyczek. Najważniejsze praktyki:
- Sanityzacja i escapowanie – sanitize_* przy zapisie, esc_* przy wyjściu. Inne konteksty wymagają innych funkcji (np. atrybuty HTML vs treść).
- Dopasuj pojemność pól – ograniczaj długość (np. mb_strlen) i zakres wartości (min/max), aby uniknąć ataków polegających na nadmuchiwaniu danych.
- Uważaj na wywołania zewnętrznych API – waliduj i buforuj odpowiedzi, zabezpieczaj time-outy i błędy.
- Trzymaj się capability checks, gdy dodajesz funkcje konfiguracji wykraczające poza form() (np. własne ekrany ustawień).
Internacjonalizacja:
- Każdy tekst w PHP opakuj w __(), _e(), _x() lub ich warianty kontekstowe.
- Używaj spójnego text domain (np. moje-widgets) i ładuj tłumaczenia load_plugin_textdomain.
- Zadbaj o formaty dat/liczb – date_i18n, number_format_i18n.
Zgodność z blokami i Gutenberg:
- Klasyczne widgety nadal działają w „Widżetach” zarządzanych jako bloki. Istnieje blok „Legacy Widget”, który opakowuje klasyczne implementacje.
- Jeśli chcesz iść krok dalej, rozważ wariant blokowy (dynamic block) korzystający z tego samego modelu danych. Reużyj logiki PHP z widgetu w callbacku render_callback bloku.
- Dodaj wsparcie dla podglądu na żywo w Customizerze (selective refresh) – widgety świetnie współgrają z tym mechanizmem.
Pamiętaj też o kompatybilności wstecznej: unikaj funkcji dostępnych tylko w najnowszych wersjach, o ile nie ustawisz wymaganej wersji wtyczki i nie zakomunikujesz tego w readme.
Testowanie, debugowanie i najczęstsze problemy
Widżet jest jednostką łatwą do testowania. Możesz osobno testować formularz (czy renderuje pola), logikę walidacji oraz wynik końcowy HTML. Wdroż poniższe praktyki:
- Tryb debugowania: w wp-config.php ustaw WP_DEBUG, WP_DEBUG_LOG, WP_DEBUG_DISPLAY, by szybko wyłapać ostrzeżenia i notice’y.
- Narzędzia: Query Monitor (zapytania, hooki, błędy PHP), Log Viewer, a w przeglądarce zakładka Network/Performance.
- Testy jednostkowe: wykorzystaj WP_UnitTestCase. Zasymulujesz instancje widgetu i sprawdzisz output funkcją wp_filter_nohtml_kses lub buforowaniem ob_start.
- Testy wizualne: porównania screenshotów (np. w Percy, Loki) przy większej skali projektu.
- Testy dostępności: axe DevTools, Lighthouse, klawiaturą i czytnikiem ekranu.
Typowe błędy i ich rozwiązania:
- Fatal error: Class not found – nie załączyłeś pliku z klasą lub rejestracja następuje przed require_once. Upewnij się, że require jest wykonywany przed add_action(’widgets_init’, …).
- Brak wyświetlania tytułu – często wynika z pominięcia $args[’before_title’] i $args[’after_title’] lub z braku escapingu i pustego stringa.
- Powielanie markup – niektórzy autorzy ręcznie dodają wrappery widgetu, dublując $args[’before_widget’]/[’after_widget’].
- Widget nie zapisuje ustawień – sprawdź name/id inputów wygenerowanych przez get_field_name/get_field_id oraz nazwy indeksów w update().
- Problemy z CSS – konflikt nazw klas. Używaj prefiksów i unikalnych bloków (np. .mw-).
- Wydajność – ciężkie WP_Query w wielu instancjach. Buforuj wyniki (transienty) i ogranicz pola zapytań.
Debugowanie instancji i warunkowej widoczności:
add_filter( 'widget_display_callback’, function( $instance, $widget, $args ) {
// Przykład: ukryj na stronach kategorii
if ( is_category() ) {
return false; // nie wyświetlaj
}
return $instance;
}, 10, 3 );
Ten filtr jest bardzo użyteczny, gdy chcesz wprowadzić logikę widoczności bez modyfikowania wnętrza metody widget().
Rozszerzenia: warunki wyświetlania, API zewnętrzne, dystrybucja i utrzymanie
Po opanowaniu podstaw możesz rozbudować swój widget o nowe funkcje. Oto kilka kierunków wraz z sugestiami implementacyjnymi.
Warunki wyświetlania i kontrola kontekstu:
- Wewnętrznie: w metodzie widget() sprawdź funkcje warunkowe (is_home, is_single, is_page_template) i warunkowo przerwij renderowanie.
- Zewnętrznie: jak pokazano wyżej, użyj widget_display_callback i dynamic_sidebar_params, aby odseparować logikę widoczności od samej klasy.
- Konfigurator w panelu: dodaj checkboksy „Pokaż tylko na stronie głównej”, „Ukryj na kategoriach”, itp. i zapisz w $instance.
Integracja z API zewnętrznymi (np. prognoza pogody, kursy walut, oceny produktów):
- Użyj wp_remote_get/wp_remote_post z rozsądnymi time-outami i obsługą błędów.
- Sparsuj odpowiedź i zawsze weryfikuj typy. Nie zakładaj, że API zawsze zwróci poprawne dane.
- Buforuj odpowiedzi (transienty), by ograniczyć liczbę zapytań i wpływ awarii API na stronę.
- Zapewnij przejrzyste komunikaty, gdy dane są chwilowo niedostępne.
Minimalny szkic dla prostego widgetu pogody:
$resp = wp_remote_get( 'https://api.weather.example/v1?city=’ . rawurlencode( $city ), [ 'timeout’ => 5 ] );
if ( is_wp_error( $resp ) ) {
echo '<p>’ . esc_html__( 'Brak danych pogodowych.’, 'moje-widgets’ ) . '</p>’;
} else {
$data = json_decode( wp_remote_retrieve_body( $resp ), true );
if ( ! is_array( $data ) || ! isset( $data[’temp’] ) ) {
echo '<p>’ . esc_html__( 'Nieprawidłowa odpowiedź API.’, 'moje-widgets’ ) . '</p>’;
} else {
echo '<p class=”mw-weather”>’ . esc_html( $data[’temp’] ) . „°C” . '</p>’;
}
}
Dystrybucja i utrzymanie:
- Readme z wymaganiami (minimalna wersja PHP i WP), instrukcją instalacji i użycia oraz changelogiem.
- Licencja (np. GPLv2+) oraz informacja o autorach, wsparciu, linku do dokumentacji.
- System wersjonowania (SemVer), testy automatyczne i CI dla krytycznej logiki.
- Kompatybilność z popularnymi motywami i builderami – testy na kilku layoutach.
Praktyki Clean Code dla widgetów:
- Podziel klasę na małe metody pomocnicze (np. render_list_item, get_query_args), co poprawi czytelność i testy.
- Unikaj mieszania logiki prezentacji z logiką pobierania danych – rozważ klasę serwisu do zapytań.
- Zadbaj o jasne komunikaty błędów dla redaktorów (np. gdy limit jest poza zakresem).
Dostępność, UX w panelu i zgodność multisite
Dobry widget to nie tylko kod – to również ergonomia dla redaktora i dostępność dla użytkownika końcowego. W panelu:
- Czytelne etykiety i opisy pól, informacja o akceptowanych formatach.
- Rozsądne wartości domyślne – widget powinien po dodaniu „po prostu działać”.
- Kompaktowy interfejs – bez przewijania i nadmiaru opcji. Jeśli konfiguracji jest dużo, rozważ rozbicie na osobne moduły albo tryb „zaawansowany”.
Na froncie:
- Semantyczny HTML – listy dla list, nagłówki w odpowiedniej hierarchii, alt dla obrazów.
- Obsługa klawiatury i czytników ekranu – focus styles, aria-attributes, skip links, jeśli element jest rozbudowany.
- Dostosowanie do motywu – kolory, kontrasty i spacing współgrają z resztą interfejsu.
Multisite:
- Ustawienia widgetu są przechowywane per instancja i per strona. To zwykle pożądane, ale miej to na uwadze przy importach/eksportach.
- Jeśli łączysz dane między stronami sieci, uwzględnij ograniczenia uprawnień i różnice w dostępnych treściach.
Checklisty wdrożeniowe i wzorce, które warto powtarzać
Wzorzec bezpiecznego widgetu do powtarzalnego użycia:
- Konstruktor z name, description, i18n oraz przemyślane id_base.
- form(): tylko potrzebne pola, pełna sanitizacja, atrybuty generowane przez get_field_*.
- update(): walidacja typów, zakresów, długości; normalizacja danych.
- widget(): wrappery z $args, escapowanie outputu, fallback dla braku danych.
- Caching: transienty z przejrzystym kluczem i rozsądnym TTL, odświeżanie przy zmianach treści.
- Style/Skrypty: warunkowe enqueue, prefiksy klas, brak wycieków globalnych.
- Dostępność: aria, focus, klawiatura, opisy.
- Testy: manualne (panel/front), automatyczne (podstawowa logika), tryb debug.
Wzorce kodu godne naśladowania:
- Separacja: metoda get_items() zwraca dane, a metoda render_items() odpowiada za widok.
- Idempotencja: widget nie powinien modyfikować globalnego stanu poza tymczasową pętlą (pamiętaj o wp_reset_postdata).
- Konfiguracja zewnętrzna: stałe i filtry (apply_filters) dla limitów i zachowań – ułatwia to modyfikacje bez rozwidleń kodu.
Przykładowa konfiguracja filtrów:
$limit = isset( $instance[’limit’] ) ? absint( $instance[’limit’] ) : 5;
$limit = apply_filters( 'mw_news_widget_limit’, $limit, $instance, $this );
Tym samym inni deweloperzy mogą zmienić limit globalnie (np. wymusić mniejszą liczbę wpisów na stronach archiwów).
Podsumowanie i dalsze kroki rozwoju
Tworzenie własnych widgetów to solidna inwestycja w modułowość serwisu i lepsze doświadczenie redaktorów. Opanowując fundamenty – konstrukcję klasy, rejestrację, metody widget()/form()/update(), a także spójne style i buforowanie – uzyskasz komponent, który łatwo przeniesiesz między projektami i rozbudujesz o kolejne funkcje.
W dalszych krokach rozważ:
- Dodanie panelu widoczności (proste reguły oparte o is_* i filtry).
- Przejście do wariantu blokowego przy zachowaniu zgodności wstecznej.
- Wspólne zaplecze danych dla kilku widgetów (np. serwis do pobierania wpisów/produktów).
- Automatyczne testy krytycznego pathu i kontrola regresji wydajności.
- Dokumentację dla redaktorów z krótkim przewodnikiem „krok po kroku”.
Jeżeli zachowasz dyscyplinę w sanitizacji danych, zadbasz o przejrzysty interfejs i modułową architekturę, Twoje widgety będą stabilne, szybkie i przyjazne w utrzymaniu. Dzięki temu zbudujesz bibliotekę komponentów, które przyśpieszą prace w kolejnych projektach i pomogą utrzymać spójną jakość realizacji.