Jak zacząć tworzyć własne wtyczki PHP do WordPressa krok po kroku

0
14
Rate this post

Nawigacja:

Po co własna wtyczka i kiedy przestać „kombinować” w functions.php

Typowe powody pisania własnych wtyczek

Wiele osób zaczyna zabawę z WordPressem od wklejania fragmentów kodu do pliku functions.php motywu. Działa to, dopóki motyw jest stały, a snippetów jest kilka. W pewnym momencie kod w motywie staje się jednak nieczytelny, trudny w utrzymaniu i blokuje zmianę szablonu. Osobna wtyczka PHP do WordPressa rozwiązuje ten problem – logika biznesowa i modyfikacje zachowania WordPressa są odseparowane od wyglądu strony.

Różnica jest prosta: motyw odpowiada za prezentację, a wtyczka za funkcjonalność. Jeśli fragment kodu wpływa na to, jak działa system (np. wysyłanie dodatkowego maila, integracja z API, automatyczne oznaczanie wpisów), powinien żyć w wtyczce. Jeśli zmienia wyłącznie wygląd (np. rejestracja obszarów widgetów dedykowanych konkretnemu layoutowi), może zostać w motywie.

Własna wtyczka ma sens przede wszystkim wtedy, gdy:

  • funkcjonalność jest powtarzalna między motywami lub stronami (np. własne shortcody, integracja z CRM, logika publikacji treści),
  • kod jest ważny biznesowo – nie chcesz go stracić po zmianie motywu,
  • planowana jest dystrybucja – chcesz używać tego samego pluginu na wielu projektach lub sprzedać go dalej,
  • potrzebujesz odrębnych ustawień, ekranu konfiguracji, własnych tabel w bazie danych,
  • łączysz WordPressa z zewnętrznymi systemami (API, płatności, marketing automation, hurtownie).

Są też przypadki, gdzie tworzenie własnego pluginu jest przerostem formy nad treścią. Jednorazowy filtr zmieniający format daty w jednym miejscu, dostosowanie jednego hooka WooCommerce czy pojedynczy snippet kosmetyczny – jeśli wiesz, że nie będziesz tego przenosić między motywami, lepiej zostawić to w functions.php lub w gotowej lekkiej wtyczce typu „Code Snippets”.

Dobrym filtrem decyzyjnym jest pytanie: „Czy po zmianie motywu chcę zachować to zachowanie WordPressa?”. Jeśli odpowiedź brzmi „tak” – zrób z tego wtyczkę.

Co trzeba już umieć przed pierwszą wtyczką

Do sensownego tworzenia wtyczek nie wystarczy wiedza, gdzie wkleić kod. Potrzebny jest minimalny poziom znajomości PHP: funkcje, tablice, operatory logiczne, podstawy obiektówki (nawet jeśli zaczniesz proceduralnie), a przede wszystkim umiejętność czytania błędów z logów. Jeśli widok komunikatu „Fatal error” powoduje panikę – poświęć kilka dni na usystematyzowanie podstaw PHP.

Drugim filarem jest rozumienie, jak działa WordPress: cykl życia żądania (ładowanie plików wp-load.php, wp-settings.php), rola motywów, wtyczek, hooków, pętli The Loop, systemu szablonów. Nie trzeba znać każdego pliku w wp-includes, ale trzeba wiedzieć, gdzie WordPress „wpuszcza” twój kod – właśnie poprzez akcje i filtry.

Od strony narzędzi potrzebne są:

  • edytor kodu (VS Code, PhpStorm, Sublime Text) z podświetlaniem składni PHP,
  • dostęp do plików – FTP lub, znacznie lepiej, SSH z możliwością korzystania z Gita,
  • lokalne środowisko lub staging, gdzie można bezkarnie psuć i testować,
  • opcjonalnie Git jako repozytorium – nawet solo praca z gałęziami i commitami daje olbrzymi komfort cofania zmian.
Minimalistyczne biurko z laptopem, notesem i długopisem na drewnianym blacie
Źródło: Pexels | Autor: Startup Stock Photos

Środowisko pracy i konfiguracja WordPressa pod developerkę

Lokalne środowisko: jak i po co

Praca nad wtyczką bez lokalnego środowiska to proszenie się o kłopoty. Edycja plików „na żywo” przez FTP, bez backupu i debugowania, kończy się białym ekranem na produkcji. Lokalna instancja WordPressa pozwala testować dowolne pomysły, generować błędy, odpalać Xdebug i nie stresować się ruchem użytkowników.

Do wyboru jest kilka popularnych stacków:

  • Local WP – bardzo wygodny dla WordPressa, klikane tworzenie instalacji, SSL lokalne, przejrzysty panel. Dla wielu początkujących to najszybszy start.
  • XAMPP / WAMP / MAMP – klasyczne paczki Apache + PHP + MySQL. Działają, choć bywają cięższe i mniej „wordpressowe” z pudełka.
  • Docker – większa elastyczność, kontrola wersji PHP/MySQL, izolacja. Dla kogoś, kto już zna Dockera, to najbardziej powtarzalne podejście.
  • Laragon / Devilbox – lekkie środowiska dla Windowsa / *nixów, wygodne do wielu projektów PHP.

Niezależnie od wyboru, pierwszym krokiem jest oddzielna instancja WordPressa do testów wtyczek. Wersja WordPressa powinna odpowiadać produkcji (lub być nowsza – wtedy testujesz przyszłą kompatybilność). Można ograniczyć się do domyślnego motywu, kilka przykładowych wpisów i mediów – więcej na tym etapie nie potrzeba.

Jeśli wtyczka ma docelowo działać na już istniejącym serwisie, przydaje się klonowanie produkcji na lokal:

  • zrób zrzut bazy danych (np. przez mysqldump albo phpMyAdmin),
  • skopiuj katalog wp-content (szczególnie uploads, themes, plugins),
  • zmień adresy URL w bazie (np. za pomocą WP-CLI search-replace lub narzędzi do migracji).

Po takim klonie możesz symulować rzeczywiste środowisko: obciążenie bazą, obecność innych wtyczek, niestandardowe motywy.

Ustawienia developerskie WordPressa

WordPress w trybie produkcyjnym ukrywa wiele problemów – błędy są wyciszane, notice’y ignorowane, a ostrzeżenia znikają. Podczas tworzenia wtyczki warto włączyć pełny zestaw narzędzi diagnostycznych przez odpowiednie stałe w wp-config.php.

Podstawowy zestaw dla developera:

  • WP_DEBUG – włącza tryb debugowania w WordPressie (błędy, notice, warning).
  • WP_DEBUG_LOG – zapisuje błędy do pliku wp-content/debug.log.
  • WP_DEBUG_DISPLAY – kontroluje wyświetlanie błędów w przeglądarce.
  • SCRIPT_DEBUG – wymusza ładowanie nieskompresowanych wersji skryptów i stylów.
  • SAVEQUERIES – loguje zapytania SQL, co pomaga przy optymalizacji.

Fragment konfiguracji może wyglądać tak:

define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );
define( 'WP_DEBUG_DISPLAY', false );
define( 'SCRIPT_DEBUG', true );
define( 'SAVEQUERIES', true );

W trybie prac nad wtyczką wygodniej jest logować błędy do pliku niż „krzyczeć” na ekranie. Pozwala to normalnie testować UI, a jednocześnie mieć podgląd do błędów w tle.

Oddzielny config dla środowiska developerskiego

Dobrym nawykiem jest rozdzielenie konfiguracji produkcyjnej i developerskiej. Zamiast za każdym razem przepisywać wp-config.php, można wykorzystać dodatkowy plik, np. wp-config-local.php, który będzie ładowany tylko na lokalnym środowisku.

Przykładowy schemat:

// wp-config.php
if ( file_exists( __DIR__ . '/wp-config-local.php' ) ) {
    include __DIR__ . '/wp-config-local.php';
} else {
    define( 'WP_DEBUG', false );
}

W pliku wp-config-local.php ustawiasz wszystkie developerskie stałe, dane dostępu do lokalnej bazy i inne parametry, które nie powinny pojawić się na produkcji. Dzięki temu repozytorium może przechowywać jeden ogólny wp-config.php, a każdy deweloper ma swój lokalny plik poza Git.

Kolorowy kod PHP na ekranie laptopa podczas tworzenia wtyczki WordPress
Źródło: Pexels | Autor: Pixabay

Anatomia wtyczki – od pustego folderu do poprawnego nagłówka

Minimalna struktura folderów pluginu

WordPress szuka wtyczek wyłącznie w katalogu wp-content/plugins. Każda wtyczka to oddzielny folder (lub pojedynczy plik wtyczki w katalogu, co dziś praktycznie się nie stosuje). Pierwszy krok to zatem utworzenie nowego folderu, np. my-first-plugin.

Dobra praktyka nazewnicza:

  • małe litery, myślniki: my-first-plugin,
  • plik główny: my-first-plugin.php (taka sama nazwa jak folder),
  • podkatalog na logikę: includes/ lub inc/,
  • podkatalog na frontend: assets/js, assets/css, assets/img,
  • opcjonalnie languages/ na pliki tłumaczeń.

Na samym początku kusi, żeby wszystko wrzucić do jednego pliku. To działa przy demo „Hello World”, ale przy pierwszej realnej funkcjonalności szybko robi się bałagan. Minimum porządku to:

  • plik główny zawierający nagłówek pluginu, rejestrację hooków i ewentualnie inicjację klasy,
  • osobny plik dla logiki admina (np. includes/admin.php),
  • osobny plik dla logiki frontendu (np. includes/frontend.php),
  • jeśli korzystasz z OOP – wydzielone klasy w includes/ lub src/.

Przy rosnącej wtyczce najlepiej od początku myśleć modułowo – osobne pliki na shortcody, custom post types, integracje z API. Przenoszenie tego później bez regresji jest trudniejsze niż start w lekkim „mikromonolicie”, ale z sensownie rozbitymi odpowiedzialnościami.

Plik główny wtyczki i nagłówek

WordPress wykrywa wtyczki przez komentarz nagłówkowy na początku pliku PHP. W głównym pliku muszą znaleźć się przynajmniej dwie linie: Plugin Name i znacznik otwarcia PHP. Przykład kompletnego nagłówka:

<?php
/**
 * Plugin Name: My First Plugin
 * Plugin URI:  https://example.com/my-first-plugin
 * Description: Prosta wtyczka demonstracyjna dodająca tekst do stopki.
 * Version:     1.0.0
 * Author:      Jan Kowalski
 * Author URI:  https://example.com
 * Text Domain: my-first-plugin
 * Domain Path: /languages
 */

Najważniejsze pola:

  • Plugin Name – nazwa wyświetlana w panelu wtyczek.
  • Description – krótki opis funkcjonalności.
  • Version – numer wersji (semver: np. 1.0.0, 1.1.0).
  • Text Domain – identyfikator tłumaczeń (musi zgadzać się z nazwą pliku .pot/.mo).
  • Domain Path – ścieżka do katalogu z tłumaczeniami względem głównego folderu wtyczki.

Po zapisaniu pliku z takim nagłówkiem WordPress zeskanuje katalog plugins, znajdzie komentarz i doda wtyczkę do listy w panelu „Wtyczki”. Proces ten jest częściowo cache’owany, więc czasem po dodaniu nowej wtyczki trzeba odświeżyć stronę lub wyczyścić cache (jeśli jest agresywny cache obiektowy).

Ustawienie text domain jest kluczowe dla lokalizacji (i18n). Nawet jeśli początkowo nie planujesz tłumaczeń, lepiej od razu wprowadzić Text Domain i od samego początku zawijać ciągi tekstowe w funkcje tłumaczeń __(), _e(), esc_html__() itd. Ułatwia to późniejsze generowanie plików .pot i tłumaczenie pluginu.

Pierwszy „Hello World” jako wtyczka

Najprostszy test, czy wtyczka działa, to podpięcie się do hooka i wypisanie jakiegoś tekstu. Przykładowo można dopisać linię w stopce strony.

W pliku my-first-plugin.php poniżej nagłówka wklej:

if ( ! defined( 'ABSPATH' ) ) {
    exit; // Bezpośredni dostęp zablokowany.
}

function my_first_plugin_footer_text() {
    echo '<p style="text-align:center;">Wiadomość z mojej pierwszej wtyczki.</p>';
}

add_action( 'wp_footer', 'my_first_plugin_footer_text' );

Co tu się dzieje:

  • sprawdzenie defined( 'ABSPATH' ) chroni plik przed bezpośrednim uruchomieniem przez przeglądarkę,
  • Bezpieczeństwo i dobre praktyki w pliku głównym

    Przy nawet najprostszej wtyczce opłaca się od razu wyrabiać kilka nawyków bezpieczeństwa i porządku w kodzie. Dzięki temu przejście z „Hello World” do produkcyjnego pluginu nie oznacza przepisywania wszystkiego od zera.

    W pliku głównym można od razu zdefiniować kilka stałych:

    if ( ! defined( 'ABSPATH' ) ) {
        exit;
    }
    
    define( 'MY_FIRST_PLUGIN_VERSION', '1.0.0' );
    define( 'MY_FIRST_PLUGIN_FILE', __FILE__ );
    define( 'MY_FIRST_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
    define( 'MY_FIRST_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
    

    Takie stałe przydają się później do ładowania plików, enqueue’owania skryptów, wersjonowania assetów czy generowania linków. Stała z numerem wersji pozwala np. podmieniać cache przeglądarki dla CSS/JS przy aktualizacji pluginu.

    Dodatkowym, ale bardzo praktycznym zasobem jest stałe źródło wiedzy o PHP i webmasteringu – kursy, blogi techniczne, dokumentacja. Serwisy w rodzaju Porady-IT.pl – Kurs PHP, Webmastering i Skrypty dla Nowoczesnych Webma pozwalają pokryć luki pomiędzy „umiem z Google” a rzeczywistym rozumieniem mechanizmów.

    Dobrą praktyką jest także przestrzeganie prefiksów w nazwach funkcji, stałych i hooków. Zamiast ogólnego add_footer_text() – coś w rodzaju my_first_plugin_add_footer_text(). Minimalizuje to ryzyko kolizji z innymi wtyczkami lub motywem.

    Przykładowa organizacja pliku głównego po dopracowaniu „Hello World” może wyglądać tak:

    <?php
    /**
     * Plugin Name: My First Plugin
     * Description: Prosta wtyczka demonstracyjna dodająca tekst do stopki.
     * Version:     1.0.0
     * Author:      Jan Kowalski
     * Text Domain: my-first-plugin
     */
    
    if ( ! defined( 'ABSPATH' ) ) {
        exit;
    }
    
    define( 'MY_FIRST_PLUGIN_VERSION', '1.0.0' );
    define( 'MY_FIRST_PLUGIN_FILE', __FILE__ );
    define( 'MY_FIRST_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
    define( 'MY_FIRST_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
    
    // Ładowanie plików z logiką.
    require_once MY_FIRST_PLUGIN_DIR . 'includes/admin.php';
    require_once MY_FIRST_PLUGIN_DIR . 'includes/frontend.php';
    
    // Inicjowanie funkcjonalności.
    my_first_plugin_init();
    
    function my_first_plugin_init() {
        if ( is_admin() ) {
            my_first_plugin_admin_init();
        } else {
            my_first_plugin_frontend_init();
        }
    }
    

    Dzięki takiej strukturze główny plik staje się tylko punktem wejścia (bootstrapem), a reszta logiki ląduje w dedykowanych modułach.

    Pliki aktywacji i dezaktywacji pluginu

    Wtyczka może potrzebować wykonać jednorazowe operacje przy włączeniu lub wyłączeniu: stworzyć własną tabelę w bazie, zapisać opcję w wp_options, wyczyścić cache. Do tego służą hooki aktywacyjne i dezaktywacyjne.

    Najprostsze podpięcie w pliku głównym:

    register_activation_hook( MY_FIRST_PLUGIN_FILE, 'my_first_plugin_activate' );
    register_deactivation_hook( MY_FIRST_PLUGIN_FILE, 'my_first_plugin_deactivate' );
    
    function my_first_plugin_activate() {
        // Np. dodanie opcji domyślnych.
        add_option( 'my_first_plugin_enabled', 1 );
    }
    
    function my_first_plugin_deactivate() {
        // Np. wyłączenie zaplanowanych zadań cron.
        wp_clear_scheduled_hook( 'my_first_plugin_cron_event' );
    }
    

    Uwaga: funkcje rejestrowane w register_activation_hook() i register_deactivation_hook() muszą być dostępne w momencie wywołania (czyli plik musi być załadowany). Z tego powodu logika aktywacji często zostaje w pliku głównym lub w osobnym pliku wczytywanym bezwarunkowo.

    Dodatkowo możesz przewidzieć hook odinstalowania (kiedy użytkownik usuwa wtyczkę z panelu). Tu zwykle czyści się dane po sobie:

    register_uninstall_hook( MY_FIRST_PLUGIN_FILE, 'my_first_plugin_uninstall' );
    
    function my_first_plugin_uninstall() {
        delete_option( 'my_first_plugin_enabled' );
        // Ewentualnie kasowanie własnych tabel lub custom post types.
    }
    

    Alternatywą jest plik uninstall.php w głównym katalogu pluginu. Jeśli istnieje, WordPress użyje go zamiast funkcji z register_uninstall_hook(). W tym pliku również trzeba zadbać o zabezpieczenie przed bezpośrednim dostępem i sprawdzenie kontekstu uruchomienia.

    Zbliżenie ekranu komputera z kolorowym kodem PHP w edytorze
    Źródło: Pexels | Autor: luis gomes

    Hooki (akcje i filtry) – fundament komunikacji z WordPressem

    Model zdarzeniowy WordPressa

    Cała platforma działa w oparciu o model zdarzeń (event-driven). WordPress w określonych momentach „wywołuje” hooki, a wtyczki mogą się do nich podpiąć. To podstawowy sposób ingerencji w zachowanie core, motywów i innych wtyczek.

    W praktyce mamy dwa typy hooków:

  • akcje (actions) – reagują na zdarzenie i coś wykonują (efekt uboczny: echo, zapis do bazy, rejestracja typu wpisu),
  • filtry (filters) – modyfikują wartość przekazywaną przez WordPressa i zwracają przetworzony wynik.

Różnica jest prosta: akcja nic nie musi zwracać, filtr musi zwrócić wartość. Jeśli filtr nie zwróci nic, WordPress dostanie null i może się posypać.

Podpinanie się do akcji – add_action()

Do akcji podłączasz się funkcją add_action(). Przykłady:

add_action( 'init', 'my_first_plugin_register_post_type' );
add_action( 'admin_menu', 'my_first_plugin_register_menu_page' );
add_action( 'wp_footer', 'my_first_plugin_footer_text' );

Struktura jest zawsze podobna:

add_action( $hook_name, $callback, $priority, $accepted_args );
  • $hook_name – nazwa hooka, do którego się podłączasz (łańcuch znaków),
  • $callback – nazwa funkcji lub metoda klasy, która ma się wykonać,
  • $priority – kolejność wykonania przy wielu callbackach (domyślnie 10, im mniejsza liczba, tym wcześniej),
  • $accepted_args – ile argumentów ma odebrać twoja funkcja (domyślnie 1).

Akcja z dodatkowymi argumentami:

add_action( 'save_post', 'my_first_plugin_on_save_post', 10, 3 );

function my_first_plugin_on_save_post( $post_ID, $post, $update ) {
    // $post_ID – ID zapisywanego wpisu,
    // $post – obiekt WP_Post,
    // $update – bool, czy to aktualizacja istniejącego wpisu.
}

Jeśli ustawisz zbyt niski $accepted_args, kolejne argumenty zostaną obcięte. Jeśli ustawisz go zbyt wysoko, nie ma to negatywnych skutków – po prostu nadmiarowe parametry będą miały wartość null.

Filtry – zmiana danych „w locie”

Filtry wykorzystuje się do modyfikacji tekstów, HTML, tablic konfiguracyjnych i praktycznie każdej wartości, którą WordPress przepuszcza przez mechanizm hooków. Kluczowa zasada: funkcja filtrująca musi zwracać zmodyfikowaną wartość.

Przykład prostego filtra tytułu wpisu:

add_filter( 'the_title', 'my_first_plugin_filter_title', 10, 2 );

function my_first_plugin_filter_title( $title, $post_id ) {
    if ( is_admin() ) {
        return $title;
    }

    return $title . ' ⭐';
}

Struktura add_filter() jest identyczna jak add_action(). Różni się semantyką – WordPress oczekuje, że twój callback odda mu przetworzoną wartość. Jeśli chcesz tylko „podsłuchać” filtr i niczego nie zmieniać, po prostu zwróć wejście bez zmian.

Przykład filtra zawartości wpisu z lekką modyfikacją HTML:

add_filter( 'the_content', 'my_first_plugin_add_box_to_content' );

function my_first_plugin_add_box_to_content( $content ) {
    if ( ! is_singular( 'post' ) ) {
        return $content;
    }

    $box = '<div class="my-first-plugin-box">'
         . esc_html__( 'Dziękujemy za przeczytanie!', 'my-first-plugin' )
         . '</div>';

    return $content . $box;
}

Tip: w filtrach bardzo często stosuje się warunki (np. is_admin(), is_singular()), żeby ograniczyć działanie tylko do wybranych widoków, typów wpisów czy stref strony.

Własne hooki w twojej wtyczce

Wtyczka może nie tylko korzystać z hooków WordPressa, ale też eksponować własne. To przydaje się, gdy przewidujesz rozszerzenia pluginu przez innych developerów albo chcesz rozbić logikę na moduły, które komunikują się za pomocą akcji i filtrów.

Definicja własnej akcji:

Dobrym uzupełnieniem będzie też materiał: Jak wdrażać kulturę testowania automatycznego — warto go przejrzeć w kontekście powyższych wskazówek.

do_action( 'my_first_plugin_before_render_box', $post_id, $settings );

Gdzieś indziej w kodzie można się do niej podpiąć:

add_action( 'my_first_plugin_before_render_box', 'my_first_plugin_custom_logic', 10, 2 );

function my_first_plugin_custom_logic( $post_id, $settings ) {
    // Dodatkowe elementy przed boxem.
}

Analogicznie z filtrami. Jeśli twoja wtyczka generuje np. konfigurację widgetu, możesz udostępnić filtr na tablicy wyjściowej:

$config = array(
    'color' => 'blue',
    'size'  => 'medium',
);

$config = apply_filters( 'my_first_plugin_widget_config', $config );

Inny kod (w tej samej wtyczce lub w osobnej) może ten filtr nadpisać:

add_filter( 'my_first_plugin_widget_config', 'my_first_plugin_tweak_widget_config' );

function my_first_plugin_tweak_widget_config( $config ) {
    $config['color'] = 'red';
    return $config;
}

Taki wzorzec mocno ułatwia późniejsze utrzymanie i otwiera drogę do budowania ekosystemu mini-rozszerzeń dla twojego pluginu.

Priorytety i konflikt wielu wtyczek

Kiedy kilka wtyczek podłącza się do tego samego hooka, znaczenie ma priorytet. Domyślnie jest to 10, ale można wymusić kolejność wywołań, np.:

add_filter( 'the_content', 'first_plugin_filter', 5 );
add_filter( 'the_content', 'second_plugin_filter', 15 );

W tym przypadku first_plugin_filter() wykona się jako pierwsza, potem wynik trafi do second_plugin_filter(). Jeśli chcesz „nadpisać” wynik innej wtyczki, nadawaj priorytet wyższy (np. 999), ale rób to świadomie – zbyt agresywne wymuszanie pierwszeństwa bywa przyczyną konfliktów.

Czasem konieczne jest zdjęcie cudzej funkcji z hooka. Używa się wtedy remove_action() lub remove_filter(), ale trzeba znać dokładnie nazwę funkcji i priorytet, pod którym została dodana:

remove_filter( 'the_content', 'some_other_plugin_filter', 10 );

Takie ingerencje najlepiej wykonywać po zainicjowaniu wszystkich wtyczek, np. na hooku plugins_loaded.

Struktura kodu wtyczki – proceduralnie czy obiektowo?

Minimalistyczne podejście proceduralne

Na start kod proceduralny jest najszybszy do napisania i zrozumienia. Kilka funkcji, parę hooków, jeden główny plik. Do małych wtyczek typu „dodaj przycisk w edytorze”, „wyłącz emoji”, „zmień domyślny excerpt length” – to w zupełności wystarczy.

Typowy proceduralny układ:

  • główny plik: rejestracja hooków, stałe, require plików,
  • includes/admin.php: funkcje dla panelu, np. strony opcji, metabox, kolumny w listach wpisów,
  • includes/frontend.php: shortcody, filtry the_content, enqueue assetów,
  • opcjonalnie inne pliki typu includes/cpt.php, includes/ajax.php.

Przykład prostego modułu proceduralnego dla shortcodu:

// includes/frontend.php

function my_first_plugin_frontend_init() {
    add_shortcode( 'my_first_box', 'my_first_plugin_shortcode_box' );
}

function my_first_plugin_shortcode_box( $atts, $content = '' ) {
    $atts = shortcode_atts(
        array(
            'color' => 'blue',
        ),
        $atts,
        'my_first_box'
    );

    $content = $content ? $content : esc_html__( 'Domyślna treść boxa.', 'my-first-plugin' );

    $html  = '<div class="my-first-plugin-box my-first-plugin-box-' . esc_attr( $atts['color'] ) . '">';
    $html .= wp_kses_post( $content );
    $html .= '</div>';

    return $html;
}

Przy takim podejściu kluczowa jest dyscyplina nazewnicza: prefiksy i jasne nazwy funkcji. Bez tego globalna przestrzeń nazw szybko zamieni się w „zupę” trudną do debugowania.

Wady rosnącego proceduralnego pluginu

Główny problem pojawia się przy rozbudowie: przybywa funkcji, hooków, plików i zależności. Ciężko jest wtedy:

  • ogarnąć kolejność inicjacji modułów,
  • wstrzyknąć różne zależności (np. klienta API, logger) w testach,
  • wydzielić fragmenty logiki do ponownego wykorzystania w innych projektach.

Przykładowo, jeśli w pięciu funkcjach używasz tego samego sposobu generowania boxa, zaczynają się kopiuj-wklej lub dziwne helpery typu my_first_plugin_render_box_html(). Przy większej liczbie takich helperów rośnie ryzyko regresji przy zmianie jednego z nich.

Wprowadzenie do podejścia obiektowego (OOP)

Dlaczego OOP w wtyczkach WordPress ma sens

W podejściu obiektowym grupujesz funkcje i dane w klasy. Zamiast dziesiątek funkcji typu my_first_plugin_*, masz kilka klas o jasnych odpowiedzialnościach: np. jedna ogarnia panel admina, inna front, jeszcze inna integrację z API.

Największe zyski:

  • enkapsulacja – właściwości i metody klasy są „schowane” przed globalnym zanieczyszczeniem przestrzeni nazw,
  • możliwość tworzenia wielu instancji (tam, gdzie to ma sens) z różnymi ustawieniami,
  • łatwiejsze testowanie – logikę zamykasz w małych klasach, które da się przetestować w izolacji,
  • lepsze modelowanie domeny – np. osobne obiekty dla „Boxu”, „Ustawień”, „Integracji z WooCommerce”.

Do małych wtyczek OOP nie jest obowiązkowe, ale przy projektach, które mają żyć kilka lat, szybko staje się wygodniejsze od rosnącego proceduralnego „skryptu”.

Prosty szkielet wtyczki w OOP – jedna klasa główna

Najłagodniejsze wejście w OOP to jeden główny obiekt wtyczki, który rejestruje hooki i deleguje robotę do metod. Bez namespace’ów i autoloadera można zmieścić się w jednym pliku:

<?php
/*
Plugin Name: My First Plugin (OOP)
Description: Przykładowa wtyczka oparta na klasie.
Version: 1.0.0
Author: Ty
*/

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

class My_First_Plugin {

    public function __construct() {
        add_action( 'init', array( $this, 'register_shortcodes' ) );
        add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_assets' ) );
        add_action( 'admin_menu', array( $this, 'add_settings_page' ) );
    }

    public function register_shortcodes() {
        add_shortcode( 'my_first_box', array( $this, 'render_box_shortcode' ) );
    }

    public function render_box_shortcode( $atts, $content = '' ) {
        $atts = shortcode_atts(
            array(
                'color' => 'blue',
            ),
            $atts,
            'my_first_box'
        );

        $content = $content ? $content : esc_html__( 'Domyślna treść boxa.', 'my-first-plugin' );

        $html  = '<div class="my-first-plugin-box my-first-plugin-box-' . esc_attr( $atts['color'] ) . '">';
        $html .= wp_kses_post( $content );
        $html .= '</div>';

        return $html;
    }

    public function enqueue_assets() {
        wp_enqueue_style(
            'my-first-plugin-frontend',
            plugin_dir_url( __FILE__ ) . 'assets/css/frontend.css',
            array(),
            '1.0.0'
        );
    }

    public function add_settings_page() {
        add_options_page(
            __( 'My First Plugin', 'my-first-plugin' ),
            __( 'My First Plugin', 'my-first-plugin' ),
            'manage_options',
            'my-first-plugin',
            array( $this, 'render_settings_page' )
        );
    }

    public function render_settings_page() {
        ?>
        <div class="wrap">
            <h1>My First Plugin</h1>
            <p>Tutaj byłyby opcje wtyczki.</p>
        </div>
        <?php
    }
}

function my_first_plugin_bootstrap() {
    static $instance = null;

    if ( null === $instance ) {
        $instance = new My_First_Plugin();
    }

    return $instance;
}

my_first_plugin_bootstrap();

Parę rzeczy jest tutaj istotnych:

  • hooki rejestrujesz w konstruktorze, więc instancja klasy = aktywny plugin,
  • metody klasy są callbackami dla hooków (array( $this, 'method_name' )),
  • my_first_plugin_bootstrap() pełni rolę prostego kontenera – zwraca tę samą instancję (pseudo-singelton).

Takie podejście już izoluje logikę w klasie, ale jeszcze nie rozbija pluginu na mniejsze moduły.

Rozbijanie wtyczki na moduły – klasy per odpowiedzialność

Kiedy funkcji i hooków przybywa, wygodniej podzielić logikę na mniejsze klasy. Zazwyczaj kończy się na czymś w tym stylu:

  • My_First_Plugin – klasa główna (bootstrap, inicjacja modułów),
  • My_First_Plugin_Admin – wszystko, co dotyczy panelu admina,
  • My_First_Plugin_Frontend – shortcode’y, filtry frontowe, enqueue skryptów,
  • My_First_Plugin_Settings – rejestracja ustawień (Settings API) i ich obsługa,
  • opcjonalnie np. My_First_Plugin_REST, My_First_Plugin_Logger itd.

Minimalny przykład modułowego układu plików:

my-first-plugin/
├─ my-first-plugin.php        (główny plik)
└─ includes/
   ├─ class-admin.php
   ├─ class-frontend.php
   └─ class-settings.php

Główny plik może wyglądać tak:

<?php
/*
Plugin Name: My First Plugin (OOP modular)
*/

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

require_once __DIR__ . '/includes/class-admin.php';
require_once __DIR__ . '/includes/class-frontend.php';
require_once __DIR__ . '/includes/class-settings.php';

class My_First_Plugin {

    protected $admin;
    protected $frontend;
    protected $settings;

    public function __construct() {
        $this->settings = new My_First_Plugin_Settings();
        $this->admin    = new My_First_Plugin_Admin( $this->settings );
        $this->frontend = new My_First_Plugin_Frontend( $this->settings );

        add_action( 'plugins_loaded', array( $this, 'init' ) );
    }

    public function init() {
        $this->settings->init();
        $this->admin->init();
        $this->frontend->init();
    }
}

function my_first_plugin() {
    static $instance = null;

    if ( null === $instance ) {
        $instance = new My_First_Plugin();
    }

    return $instance;
}

my_first_plugin();

Każda klasa modułu implementuje własną metodę init(), gdzie rejestruje hooki. Przykład class-frontend.php:

<?php

class My_First_Plugin_Frontend {

    protected $settings;

    public function __construct( My_First_Plugin_Settings $settings ) {
        $this->settings = $settings;
    }

    public function init() {
        add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_assets' ) );
        add_shortcode( 'my_first_box', array( $this, 'shortcode_box' ) );
    }

    public function enqueue_assets() {
        if ( ! $this->settings->is_enabled_on_front() ) {
            return;
        }

        wp_enqueue_style(
            'my-first-plugin-frontend',
            plugin_dir_url( dirname( __FILE__ ) ) . 'assets/css/frontend.css',
            array(),
            '1.0.0'
        );
    }

    public function shortcode_box( $atts, $content = '' ) {
        if ( ! $this->settings->is_box_enabled() ) {
            return '';
        }

        // Logika podobna do wcześniejszego przykładu...
    }
}

Tu pojawia się podstawowy wzorzec – każdy moduł dostaje w konstruktorze zależności, których używa (tu: obiekt ustawień). To ułatwia późniejsze testowanie, a także wymianę implementacji (np. inny storage ustawień).

Jeśli chcesz pójść krok dalej, pomocny może być też wpis: Jak tworzyć własne wyzwania rozwojowe.

Namespace’y i autoloading – krok w stronę nowoczesnego PHP

Przy większych wtyczkach rozważ użycie namespace’ów i autoloadera (np. z Composera). Dzięki namespace’om unikasz kolizji nazw klas, a autoloader wycina ręczne require_once.

Przykładowy układ z Composerem:

my-first-plugin/
├─ my-first-plugin.php
├─ composer.json
└─ src/
   ├─ Admin/
   │  └─ Page.php
   ├─ Frontend/
   │  └─ Shortcodes.php
   └─ Settings/
      └─ Options.php

composer.json:

{
  "name": "you/my-first-plugin",
  "autoload": {
    "psr-4": {
      "MyFirstPlugin": "src/"
    }
  }
}

Po uruchomieniu composer dump-autoload w głównym pliku wtyczki doładowujesz tylko autoloader:

require __DIR__ . '/vendor/autoload.php';

use MyFirstPluginAdminPage as AdminPage;
use MyFirstPluginFrontendShortcodes;
use MyFirstPluginSettingsOptions as SettingsOptions;

class Plugin {

    private $settings;
    private $admin_page;
    private $shortcodes;

    public function __construct() {
        $this->settings   = new SettingsOptions();
        $this->admin_page = new AdminPage( $this->settings );
        $this->shortcodes = new Shortcodes( $this->settings );

        add_action( 'plugins_loaded', array( $this, 'init' ) );
    }

    public function init() {
        $this->settings->init();
        $this->admin_page->init();
        $this->shortcodes->init();
    }
}

Namespace’y (np. MyFirstPluginFrontendShortcodes) sprawiają, że nazwa Shortcodes nie koliduje z inną klasą o tej samej nazwie w innym pluginie. Bez tego duże projekty szybko zaczynają stosować agresywne prefiksy w nazwach klas, co jest mniej czytelne.

Jak łączyć hooki z OOP, żeby nie zwariować

Najczęstszy błąd przy przechodzeniu na OOP w WordPressie to wygenerowanie chaosu typu „klasa w klasie, hook w hooku”. Kilka prostych zasad trzyma układ w ryzach:

  • każda klasa wie, za jakie hooki odpowiada – rejestruje je w swojej metodzie init(),
  • metody callbacków są małe – z callbacka wydzielasz logikę do prywatnych metod,
  • brak logiki w konstruktorze poza podstawową konfiguracją i zapisaniem zależności.

Przykład klasy admina z jasnym podziałem ról:

class My_First_Plugin_Admin {

    public function init() {
        add_action( 'admin_menu', array( $this, 'register_menu' ) );
        add_action( 'admin_init', array( $this, 'register_settings' ) );
    }

    public function register_menu() {
        add_options_page(
            __( 'My First Plugin', 'my-first-plugin' ),
            __( 'My First Plugin', 'my-first-plugin' ),
            'manage_options',
            'my-first-plugin',
            array( $this, 'render_settings_page' )
        );
    }

    public function register_settings() {
        // Tutaj tylko rejestracja settingów, bez biznesowej logiki.
    }

    public function render_settings_page() {
        // Tylko widok: formularz opcji, nonce, submit.
    }
}

Hooki stają się wtedy wyłącznie „wyzwalaczami” konkretnych metod. Jeśli cała logika zagnieżdża się w callbackach hooków, debugowanie szybko zamienia się w przeklikiwanie się między kilkoma plikami w te i z powrotem.

Stan wtyczki, konfiguracja i przechowywanie ustawień

Wtyczki rzadko są w 100% stateless. Zwykle masz jakieś ustawienia (włącz / wyłącz moduł, kolory, teksty), cache, dane o licencji. W podejściu OOP warto te elementy skupić w osobnych klasach, zamiast operować wszędzie na get_option()/update_option().

Prosty wrapper na ustawienia:

class My_First_Plugin_Settings {

    const OPTION_KEY = 'my_first_plugin_settings';

    protected $defaults = array(
        'enabled'     => true,
        'box_enabled' => true,
    );

    protected $values = null;

    public function init() {
        register_setting(
            'my_first_plugin',
            self::OPTION_KEY,
            array( $this, 'sanitize' )
        );
    }

    public function sanitize( $input ) {
        $output = $this->get_all();

        $output['enabled']     = ! empty( $input['enabled'] );
        $output['box_enabled'] = ! empty( $input['box_enabled'] );

        return $output;
    }

    public function get_all() {
        if ( null === $this->values ) {
            $stored        = get_option( self::OPTION_KEY, array() );
            $this->values  = wp_parse_args( $stored, $this->defaults );
        }

        return $this->values;
    }

    public function is_enabled_on_front() {
        $settings = $this->get_all();
        return (bool) $settings['enabled'];
    }

    public function is_box_enabled() {
        $settings = $this->get_all();
        return (bool) $settings['box_enabled'];
    }
}

Zamiast powtarzać w kilku miejscach get_option( 'my_first_plugin_settings' ) i ręcznie sprawdzać klucze, odwołujesz się do metod typu is_enabled_on_front(). Logika walidacji i domyślnych wartości także jest trzymana w jednym miejscu.

Gdzie zatrzymać „oczyszczanie” architektury

W dużych projektach łatwo przesadzić z formalizmem – osobne interfejsy, fabryki, kontenery DI (dependency injection), kilkadziesiąt klas. WordPress jest dość pragmatycznym ekosystemem, więc sensowny jest balans.

Praktyczny punkt odniesienia:

  • do ~300–400 linii kodu spokojnie wystarczy prosty proceduralny układ z 2–3 plikami,
  • powyżej tego progu, gdy zaczyna się panel opcji, shortcode’y, metaboksy – jedna główna klasa + 2–3 moduły mocno pomaga,
  • jeśli plugin ma własne API, integruje się z kilkoma innymi wtyczkami, ma rozbudowane UI – namespace’y, autoloader i klasy per moduł stają się dużo wygodniejsze niż dalsze rozbudowywanie skryptu proceduralnego.

Dobrym testem jest sytuacja, w której trzeba usunąć/wyłączyć część funkcjonalności. Jeśli do tego celu modyfikujesz 8–10 miejsc w kodzie rozrzuconych po różnych plikach, sygnał jest prosty: czas na rozbicie pluginu na moduły albo doprecyzowanie, za co która klasa odpowiada.

Najczęściej zadawane pytania (FAQ)

Kiedy lepiej napisać własną wtyczkę zamiast używać functions.php?

Jeśli kod wpływa na działanie WordPressa (logikę), a nie tylko na wygląd, powinien trafić do wtyczki. Chodzi o rzeczy typu integracje z API, dodatkowe maile, automatyczne tagowanie, własne shortcody, reguły publikacji treści.

Dobry test to pytanie: „Czy po zmianie motywu chcę zachować to zachowanie WordPressa?”. Jeżeli odpowiedź brzmi „tak”, to znak, że to kandydat na wtyczkę. Pojedyncze, kosmetyczne poprawki związane ściśle z danym motywem nadal mogą zostać w functions.php lub w wtyczce typu „Code Snippets”.

Jakie minimalne umiejętności PHP są potrzebne, żeby zacząć pisać wtyczki WordPress?

Do pisania sensownych wtyczek trzeba znać podstawy PHP: funkcje, tablice, operatory logiczne, instrukcje warunkowe, pętle oraz podstawy programowania obiektowego (klasy, metody, właściwości). Absolutnym obowiązkiem jest też umiejętność czytania komunikatów błędów i logów – bez tego debugowanie będzie zgadywaniem.

Przydaje się także rozumienie zasięgu zmiennych, różnicy między require/include, a także podstaw pracy z przestrzeniami nazw (namespaces), jeśli chcesz pisać bardziej zorganizowany kod. Uwaga: jeśli widok „Fatal error” paraliżuje, warto poświęcić parę dni na usystematyzowanie samego PHP przed wejściem w API WordPressa.

Jak przygotować lokalne środowisko do tworzenia wtyczek WordPress?

Najbezpieczniej jest pracować na lokalnej instalacji WordPressa, a nie na serwerze produkcyjnym. Do wyboru są gotowe paczki typu Local WP, XAMPP/WAMP/MAMP, Docker czy Laragon. Ważne, aby dało się szybko postawić świeży WordPress, mieć dostęp do plików oraz bazy danych.

Praktyczny workflow to: osobna instancja WordPressa pod development, dopasowana wersja WP/PHP do produkcji, kilka przykładowych wpisów i mediów. Jeśli wtyczka ma działać na istniejącej stronie, dobrym ruchem jest klon produkcji (zrzut bazy + kopia wp-content + podmiana URL-i), żeby od razu testować na realnych danych i zestawie innych wtyczek.

Jak włączyć tryb deweloperski i logowanie błędów w WordPressie przy pracy nad wtyczką?

Konfiguracja odbywa się w pliku wp-config.php. Typowy zestaw dla developera to: WP_DEBUG (włącza debug), WP_DEBUG_LOG (zapisuje błędy do wp-content/debug.log), WP_DEBUG_DISPLAY (kontroluje wyświetlanie w przeglądarce), SCRIPT_DEBUG (ładuje nieskompresowane skrypty) oraz SAVEQUERIES (loguje zapytania SQL).

Przykład konfiguracji:

  • define( 'WP_DEBUG', true );
  • define( 'WP_DEBUG_LOG', true );
  • define( 'WP_DEBUG_DISPLAY', false ); – błędy idą do loga, a nie w UI
  • define( 'SCRIPT_DEBUG', true );
  • define( 'SAVEQUERIES', true );

Tip: warto trzymać te ustawienia w dodatkowym pliku, np. wp-config-local.php, który będzie dołączany tylko na środowisku developerskim i nie trafi do produkcji.

Jaka jest minimalna struktura plików dla własnej wtyczki PHP w WordPressie?

WordPress widzi wtyczki wyłącznie w katalogu wp-content/plugins. Podstawowy wariant to folder z nazwą wtyczki (np. my-first-plugin) oraz główny plik PHP o tej samej nazwie, np. my-first-plugin.php, zawierający nagłówek wtyczki (plugin header) rozpoznawany przez WordPressa.

Bardziej uporządkowany szablon startowy to:

  • my-first-plugin/
  • my-first-plugin.php – nagłówek, rejestracja hooków, inicjalizacja
  • includes/ lub inc/ – logika wtyczki (podział na pliki)
  • assets/js, assets/css, assets/img – zasoby frontendowe
  • languages/ – pliki tłumaczeń, jeśli planujesz wielojęzyczność

Uwaga: wrzucenie wszystkiego do jednego pliku działa przy „Hello World”, ale przy pierwszej poważniejszej funkcji natychmiast zamienia się w chaos, trudny do utrzymania i debugowania.

Czy każdą zmianę w WordPressie trzeba robić przez własną wtyczkę?

Nie. Własna wtyczka ma sens, gdy funkcjonalność jest wielokrotnego użytku (na różnych motywach lub stronach), jest biznesowo istotna (nie możesz jej „zgubić” przy zmianie motywu) albo integruje WordPressa z zewnętrznymi systemami. Dodatkowy argument to potrzeba osobnych ustawień, panelu konfiguracji lub własnych tabel w bazie.

Drobne, jednorazowe poprawki – np. zmiana formatu daty w jednym miejscu czy delikatny filtr WooCommerce – spokojnie mogą zostać jako snippet: w functions.php bieżącego motywu lub w lekkiej wtyczce do fragmentów kodu. Kluczem jest odpowiedź na pytanie, czy ta modyfikacja ma żyć niezależnie od motywu i czy przewidujesz jej ponowne wykorzystanie.

Czym różni się motyw od wtyczki w WordPressie pod kątem tworzenia własnego kodu?

Motyw (theme) odpowiada za warstwę prezentacji: wygląd strony, układ, CSS, szablony pojedynczych wpisów. Wtyczka (plugin) dostarcza funkcjonalność – modyfikuje sposób działania WordPressa, dodaje nowe typy treści, integracje, hooki, endpointy API itd.

Praktyczna zasada: wszystko, co jest „logiką biznesową” albo dotyczy działania systemu niezależnie od skórki, powinno trafić do wtyczki. Przykład z praktyki: integracja z CRM-em, która wysyła dane klientów – to wtyczka. Dodatkowy sidebar pasujący tylko do konkretnego layoutu w jednym motywie – to część motywu.