Interfejs programistyczny REST (ang. REST API — Representational State Transfer Application Programming Interface) to jedna z najpotężniejszych — i najczęściej niedostatecznie wykorzystywanych — funkcji WordPressa. Deweloperzy używają go do budowania aplikacji typu headless (z oddzielonym frontendem), ale jego prawdziwa siła leży w czymś innym: w integracji WordPressa z całym ekosystemem zewnętrznych systemów.
System CRM (ang. Customer Relationship Management — system zarządzania relacjami z klientem), bramka płatności, platforma mailingowa, system ERP (ang. Enterprise Resource Planning — system planowania zasobów przedsiębiorstwa), zewnętrzny magazyn produktów — wszystkie te systemy mogą wymieniać dane z WordPress przez REST API, automatycznie i w czasie rzeczywistym.
Ten przewodnik przeprowadzi Cię przez każdy etap: od zrozumienia architektury REST, przez implementację uwierzytelniania i własnych punktów końcowych, aż po gotowe do produkcji przykłady integracji z kodem PHP.
Filar 1: Podstawy REST API WordPress
Architektura REST — co musisz rozumieć
REST to styl architektoniczny komunikacji między systemami przez protokół HTTP (ang. HyperText Transfer Protocol — protokół przesyłania dokumentów hipertekstowych). Nie jest to standard ani protokół — to zestaw zasad projektowania interfejsów programistycznych.
Kluczowe zasady architektury REST:
Bezstanowość (ang. statelessness) — każde żądanie (ang. request — komunikat wysyłany do serwera) zawiera wszystkie informacje potrzebne do jego obsługi. Serwer nie przechowuje stanu sesji między żądaniami.
Jednolity interfejs — zasoby identyfikowane przez adresy URL, reprezentowane w formacie JSON (ang. JavaScript Object Notation — tekstowy format wymiany danych), manipulowane przez standardowe metody HTTP.
Architektura klient–serwer — klient (przeglądarka, aplikacja mobilna, zewnętrzny system) i serwer (WordPress) są od siebie niezależne.
Metody HTTP i ich znaczenie
Metoda HTTP Operacja Przykład zastosowania
──────────────────────────────────────────────────────────
GET Pobieranie danych Pobranie listy wpisów
POST Tworzenie zasobu Utworzenie nowego wpisu
PUT Pełna aktualizacja Zastąpienie całego wpisu
PATCH Częściowa aktualizacja Zmiana tylko tytułu wpisu
DELETE Usuwanie zasobu Usunięcie wpisu
Wbudowane punkty końcowe WordPress
WordPress udostępnia interfejs REST od wersji 4.7. Bazowy adres URL punktów końcowych (ang. endpoints — adresy pod którymi interfejs odpowiada na żądania):
<https://twojadomena.pl/wp-json/>
Główne zasoby dostępne bez konfiguracji:
# Wpisy — pobierz listę
GET /wp-json/wp/v2/posts
# Wpisy — pobierz konkretny
GET /wp-json/wp/v2/posts/42
# Wpisy — utwórz (wymaga uwierzytelnienia)
POST /wp-json/wp/v2/posts
# Strony
GET /wp-json/wp/v2/pages
# Typy wpisów niestandardowych (ang. custom post types)
GET /wp-json/wp/v2/{slug-typu}
# Użytkownicy (wymaga uprawnień)
GET /wp-json/wp/v2/users
# Kategorie
GET /wp-json/wp/v2/categories
# Media
GET /wp-json/wp/v2/media
Testowanie API narzędziem Postman
Postman (narzędzie do testowania interfejsów programistycznych) to aplikacja desktopowa niezbędna przy pracy z WordPress REST API integracje. Pozwala wysyłać żądania HTTP bez pisania kodu i natychmiast podglądać odpowiedzi.
Konfiguracja podstawowa w Postman:
1. Utwórz nową kolekcję (ang. collection — zbiór żądań)
2. Dodaj zmienną środowiskową: base_url = <https://twojadomena.pl/wp-json/wp/v2>
3. Pierwsze żądanie testowe:
Metoda: GET
URL: {{base_url}}/posts?per_page=5&_fields=id,title,status
4. Kliknij Send — powinieneś zobaczyć tablicę JSON z 5 wpisami
Kody statusu HTTP — co oznaczają w kontekście WordPress
200 OK — żądanie obsłużone poprawnie
201 Created — zasób został utworzony
204 No Content — sukces, brak danych do zwrócenia (np. DELETE)
400 Bad Request — błędne dane w żądaniu
401 Unauthorized — brak lub nieprawidłowe uwierzytelnienie
403 Forbidden — brak uprawnień (uwierzytelniony, ale bez dostępu)
404 Not Found — zasób nie istnieje
405 Method Not Allowed — niedozwolona metoda HTTP dla tej trasy
422 Unprocessable — dane zrozumiałe, ale nieprzechodzące walidacji
429 Too Many Req. — przekroczono limit żądań
500 Server Error — błąd po stronie serwera
Struktura odpowiedzi JSON
WordPress REST API integracje zwracają odpowiedzi w formacie JSON z kilkoma standardowymi polami:
{
"id": 42,
"date": "2025-03-15T10:30:00",
"date_gmt": "2025-03-15T09:30:00",
"guid": {
"rendered": "<https://twojadomena.pl/?p=42>"
},
"modified": "2025-03-20T14:15:00",
"slug": "przykladowy-wpis",
"status": "publish",
"type": "post",
"link": "<https://twojadomena.pl/przykladowy-wpis/>",
"title": {
"rendered": "Przykładowy wpis"
},
"content": {
"rendered": "<p>Treść wpisu...</p>",
"protected": false
},
"excerpt": {
"rendered": "<p>Skrót treści...</p>",
"protected": false
},
"author": 1,
"categories": [3, 7]
}
Filar 2: Uwierzytelnianie — jak zabezpieczyć WordPress REST API?
Uwierzytelnianie (ang. authentication — weryfikacja tożsamości użytkownika) i autoryzacja (ang. authorization — weryfikacja uprawnień) to najbardziej krytyczny aspekt WordPress REST API integracje z zewnętrznymi systemami. Zły wybór mechanizmu uwierzytelniania = luka bezpieczeństwa.
Metoda 1: Hasła Aplikacji (Application Passwords) — wbudowane od WP 5.6
Hasła Aplikacji (ang. Application Passwords) to wbudowany mechanizm WordPress dostępny od wersji 5.6. Generujesz osobne hasło dla każdej integracji — bez ujawniania głównego hasła konta.
Generowanie Hasła Aplikacji:
Panel WordPress → Użytkownicy → Twój profil → sekcja „Hasła aplikacji” → wpisz nazwę integracji → kliknij „Dodaj nowe hasło aplikacji”.
Użycie w żądaniu HTTP:
# Kodowanie Base64 (kodowanie szesnastkowe do tekstu ASCII)
# Format: login:haslo_aplikacji (spacje w haśle są prawidłowe)
echo -n "admin:AbCd EfGh IjKl MnOp QrSt UvWx" | base64
# Wynik: YWRtaW46QWJDZCBFZkdoIElqS2wgTW5PcCBRclN0IFV2V3g=
# Żądanie z nagłówkiem Authorization
curl -X GET \
<https://twojadomena.pl/wp-json/wp/v2/users/me> \
-H "Authorization: Basic YWRtaW46QWJDZCBFZkdoIElqS2wgTW5PcCBRclN0IFV2V3g="
Implementacja w PHP — zewnętrzny system wywołujący WordPress:
<?php
/**
* Klasa do komunikacji z WordPress REST API
* przez Hasła Aplikacji (Application Passwords)
*/
class KlientWordPressAPI {
private string $bazowy_url;
private string $naglowek_autoryzacji;
public function __construct(
string $url_witryny,
string $login,
string $haslo_aplikacji
) {
$this->bazowy_url = rtrim($url_witryny, '/') . '/wp-json/wp/v2';
$this->naglowek_autoryzacji = 'Basic ' . base64_encode(
$login . ':' . $haslo_aplikacji
);
}
/**
* Wysyła żądanie GET do WordPress REST API
*
* @param string $sciezka Ścieżka punktu końcowego, np. '/posts'
* @param array $parametry Parametry zapytania URL
* @return array Zdekodowane dane JSON
* @throws RuntimeException Gdy żądanie się nie powiedzie
*/
public function pobierz(string $sciezka, array $parametry = []): array {
$url = $this->bazowy_url . $sciezka;
if (!empty($parametry)) {
$url .= '?' . http_build_query($parametry);
}
$kontekst = stream_context_create([
'http' => [
'method' => 'GET',
'header' => 'Authorization: ' . $this->naglowek_autoryzacji . "\r\n" .
'Accept: application/json' . "\r\n",
'timeout' => 15,
],
]);
$odpowiedz = @file_get_contents($url, false, $kontekst);
if ($odpowiedz === false) {
throw new RuntimeException("Błąd połączenia z WordPress API: {$url}");
}
$dane = json_decode($odpowiedz, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new RuntimeException('Nieprawidłowa odpowiedź JSON z WordPress API');
}
return $dane;
}
/**
* Wysyła żądanie POST do WordPress REST API
*/
public function utworz(string $sciezka, array $dane): array {
$url = $this->bazowy_url . $sciezka;
$kontekst = stream_context_create([
'http' => [
'method' => 'POST',
'header' => 'Authorization: ' . $this->naglowek_autoryzacji . "\r\n" .
'Content-Type: application/json' . "\r\n" .
'Accept: application/json' . "\r\n",
'content' => json_encode($dane),
'timeout' => 15,
],
]);
$odpowiedz = @file_get_contents($url, false, $kontekst);
if ($odpowiedz === false) {
throw new RuntimeException("Błąd podczas tworzenia zasobu: {$url}");
}
return json_decode($odpowiedz, true);
}
}
// Przykład użycia — zewnętrzny system pobiera wpisy z WordPress
$klient = new KlientWordPressAPI(
url_witryny: '<https://twojadomena.pl>',
login: 'admin',
haslo_aplikacji: 'AbCd EfGh IjKl MnOp QrSt UvWx'
);
$wpisy = $klient->pobierz('/posts', [
'per_page' => 5,
'status' => 'publish',
'_fields' => 'id,title,excerpt,link',
]);
Metoda 2: JWT (JSON Web Tokens — Tokeny Sieciowe JSON)
JWT (ang. JSON Web Token — token sieciowy JSON) to podpisany kryptograficznie token zawierający informacje o użytkowniku. Idealny do autentykacji aplikacji mobilnych i frontendów React/Vue.
Instalacja: wtyczka JWT Authentication for WP REST API lub Simple JWT Login.
<?php
// 1. Konfiguracja w wp-config.php
define('JWT_AUTH_SECRET_KEY', 'twoj-bardzo-dlugi-i-losowy-klucz-sekretny-min-32-znaki');
define('JWT_AUTH_CORS_ENABLE', true);
// 2. Konfiguracja w .htaccess (Apache) — wymagana do przekazywania nagłówka Authorization
// Dodaj przed # BEGIN WordPress:
// SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1
// 3. Pobranie tokenu JWT
// POST /wp-json/jwt-auth/v1/token
// Body: { "username": "admin", "password": "twoje-haslo" }
//
// Odpowiedź:
// {
// "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
// "user_email": "admin@twojadomena.pl",
// "user_nicename": "admin",
// "user_display_name": "Administrator"
// }
Klasa do obsługi JWT w JavaScript (po stronie klienta):
// klient-jwt.js — użycie w aplikacji frontendowej
class KlientWordPressJWT {
#bazowy_url;
#token = null;
constructor(bazowy_url) {
this.#bazowy_url = bazowy_url.replace(/\/$/, '');
}
/**
* Logowanie i pobranie tokenu JWT
* @param {string} login
* @param {string} haslo
*/
async zaloguj(login, haslo) {
const odpowiedz = await fetch(
`${this.#bazowy_url}/wp-json/jwt-auth/v1/token`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: login, password: haslo }),
}
);
if (!odpowiedz.ok) {
throw new Error('Błąd logowania — sprawdź dane dostępowe');
}
const dane = await odpowiedz.json();
this.#token = dane.token;
// Zapisz token z czasem wygaśnięcia (zazwyczaj 7 dni)
sessionStorage.setItem('wp_jwt_token', this.#token);
return dane;
}
/**
* Wysyła żądanie do chronionego punktu końcowego
*/
async pobierz(sciezka, parametry = {}) {
if (!this.#token) {
// Spróbuj odczytać z sessionStorage
this.#token = sessionStorage.getItem('wp_jwt_token');
}
const url = new URL(`${this.#bazowy_url}/wp-json/wp/v2${sciezka}`);
Object.entries(parametry).forEach(([k, v]) => url.searchParams.set(k, v));
const odpowiedz = await fetch(url, {
headers: {
'Authorization': `Bearer ${this.#token}`,
'Content-Type': 'application/json',
},
});
if (odpowiedz.status === 401) {
// Token wygasł — wymagane ponowne logowanie
sessionStorage.removeItem('wp_jwt_token');
throw new Error('Sesja wygasła — zaloguj się ponownie');
}
return odpowiedz.json();
}
}
// Użycie
const klient = new KlientWordPressJWT('<https://twojadomena.pl>');
await klient.zaloguj('admin', 'haslo');
const wpisy = await klient.pobierz('/posts', { per_page: 10 });
Metoda 3: Jednorazowe tokeny bezpieczeństwa (Nonces) — dla żądań z przeglądarki
Jednorazowy token bezpieczeństwa (ang. nonce — skrót od „number used once”, czyli liczba użyta raz) to mechanizm weryfikacji żądań wysyłanych z przeglądarki przez zalogowanego użytkownika. Używany głównie dla własnych interfejsów administracyjnych.
<?php
// W funkcji render strony administracyjnej lub wp_enqueue_scripts
wp_localize_script('moj-skrypt', 'MojaKonfiguracja', [
'url_api' => rest_url('moj-plugin/v1/'),
'nonce' => wp_create_nonce('wp_rest'), // Jednorazowy token bezpieczeństwa
]);
// moj-skrypt.js — żądanie z jednorazowym tokenem bezpieczeństwa
const odpowiedz = await fetch(`${MojaKonfiguracja.url_api}ustawienia`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': MojaKonfiguracja.nonce, // Nagłówek jednorazowego tokenu
},
body: JSON.stringify({ klucz: 'wartosc' }),
});
Metoda 4: Klucze API — własna implementacja
Gdy chcesz własnego systemu kluczy API bez zewnętrznych wtyczek:
<?php
/**
* Funkcja sprawdzania uprawnień oparta na własnych kluczach API.
* Każda integracja zewnętrzna otrzymuje unikalny klucz przechowywany w bazie.
*/
function moj_plugin_sprawdz_klucz_api(WP_REST_Request $zadanie): bool|WP_Error {
// Pobierz klucz z nagłówka HTTP (ang. HTTP header)
$klucz_api = $zadanie->get_header('X-API-Key');
if (empty($klucz_api)) {
return new WP_Error(
'brak_klucza_api',
'Brakujący nagłówek X-API-Key.',
['status' => 401]
);
}
// Klucze przechowywane jako opcje WordPress (w produkcji: dedykowana tabela bazy)
$zarejestrowane_klucze = get_option('moj_plugin_klucze_api', []);
if (!in_array(hash('sha256', $klucz_api), $zarejestrowane_klucze, true)) {
return new WP_Error(
'nieprawidlowy_klucz_api',
'Nieprawidłowy lub wygasły klucz API.',
['status' => 403]
);
}
// Ustaw kontekst użytkownika jeśli wymagany przez inne funkcje
wp_set_current_user(0);
return true;
}
/**
* Generuje i zapisuje nowy klucz API
*/
function moj_plugin_generuj_klucz_api(string $nazwa_integracji): string {
$klucz = bin2hex(random_bytes(32)); // 64-znakowy losowy klucz
$hash = hash('sha256', $klucz);
$klucze = get_option('moj_plugin_klucze_api', []);
$klucze[$nazwa_integracji] = $hash;
update_option('moj_plugin_klucze_api', $klucze);
// Zwróć tylko raz — nie da się odtworzyć z hasha
return $klucz;
}
Filar 3: Własne punkty końcowe (Custom Endpoints) — pełna implementacja
Własne punkty końcowe (ang. custom endpoints) to trasy (ang. routes — adresy URL obsługiwane przez API) zarejestrowane przez Twoją wtyczkę lub motyw. Pozwalają na precyzyjne określenie, jakie dane WordPress udostępnia zewnętrznym systemom.
Anatomia rejestracji trasy REST
<?php
/**
* Rejestruje wszystkie trasy REST wtyczki
*/
add_action('rest_api_init', 'moj_plugin_rejestruj_trasy_rest');
function moj_plugin_rejestruj_trasy_rest(): void {
// Przestrzeń nazw (ang. namespace) — zawsze w formacie nazwa-wtyczki/wersja
$przestrzen_nazw = 'moj-plugin/v1';
// ── Zasób: produkty ─────────────────────────────────────────────────
register_rest_route($przestrzen_nazw, '/produkty', [
// Obsługa wielu metod HTTP dla tej samej ścieżki
[
'methods' => WP_REST_Server::READABLE, // GET
'callback' => 'moj_plugin_pobierz_produkty',
'permission_callback' => '__return_true', // Publiczny dostęp
'args' => moj_plugin_args_listy_produktow(),
],
[
'methods' => WP_REST_Server::CREATABLE, // POST
'callback' => 'moj_plugin_utworz_produkt',
'permission_callback' => 'moj_plugin_uprawnienia_edytora',
'args' => moj_plugin_args_tworzenia_produktu(),
],
]);
// ── Zasób: pojedynczy produkt ────────────────────────────────────────
register_rest_route($przestrzen_nazw, '/produkty/(?P<id>\d+)', [
[
'methods' => WP_REST_Server::READABLE,
'callback' => 'moj_plugin_pobierz_produkt',
'permission_callback' => '__return_true',
'args' => [
'id' => [
'description' => 'Unikalny identyfikator produktu.',
'type' => 'integer',
'required' => true,
'minimum' => 1,
],
],
],
[
'methods' => WP_REST_Server::EDITABLE, // PUT + PATCH
'callback' => 'moj_plugin_aktualizuj_produkt',
'permission_callback' => 'moj_plugin_uprawnienia_edytora',
],
[
'methods' => WP_REST_Server::DELETABLE, // DELETE
'callback' => 'moj_plugin_usun_produkt',
'permission_callback' => 'moj_plugin_uprawnienia_administratora',
],
]);
// ── Punkt końcowy do odbioru webhooków (ang. webhooks — haki sieciowe) ──
register_rest_route($przestrzen_nazw, '/webhook/platnosc', [
'methods' => WP_REST_Server::CREATABLE,
'callback' => 'moj_plugin_obsgruz_webhook_platnosci',
'permission_callback' => 'moj_plugin_weryfikuj_podpis_webhooka',
]);
}
Definicje argumentów — walidacja i oczyszczanie danych
<?php
/**
* Definicje argumentów dla listy produktów.
* Walidacja (ang. validation — sprawdzenie poprawności) i
* oczyszczanie danych (ang. sanitization — usuwanie niebezpiecznych znaków)
* są deklarowane razem z argumentami.
*/
function moj_plugin_args_listy_produktow(): array {
return [
'strona' => [
'description' => 'Numer strony wyników (paginacja).',
'type' => 'integer',
'default' => 1,
'minimum' => 1,
'sanitize_callback' => 'absint', // Zawsze dodatnia liczba całkowita
],
'na_strone' => [
'description' => 'Liczba produktów na stronę.',
'type' => 'integer',
'default' => 10,
'minimum' => 1,
'maximum' => 100, // WordPress automatycznie sprawdza minimum i maximum
],
'kategoria' => [
'description' => 'Filtruj po identyfikatorze kategorii.',
'type' => 'integer',
'sanitize_callback' => 'absint',
'validate_callback' => function(int $param): bool|WP_Error {
if (!term_exists($param, 'product_cat')) {
return new WP_Error(
'nieprawidlowa_kategoria',
"Kategoria o ID {$param} nie istnieje."
);
}
return true;
},
],
'szukaj' => [
'description' => 'Fraza do wyszukania w tytule i treści produktu.',
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
'validate_callback' => function(string $param): bool {
return mb_strlen($param) >= 3; // Minimum 3 znaki
},
],
'kolejnosc' => [
'description' => 'Kolejność sortowania wyników.',
'type' => 'string',
'default' => 'desc',
'enum' => ['asc', 'desc'], // WordPress automatycznie waliduje enum
],
'sortuj_po' => [
'description' => 'Pole, po którym sortujemy wyniki.',
'type' => 'string',
'default' => 'date',
'enum' => ['date', 'title', 'price', 'menu_order'],
],
];
}
Funkcja zwrotna z obsługą błędów i paginacją
<?php
/**
* Funkcja zwrotna (ang. callback) dla GET /produkty
* Zwraca paginowaną listę produktów z nagłówkami paginacji.
*/
function moj_plugin_pobierz_produkty(WP_REST_Request $zadanie): WP_REST_Response|WP_Error {
$strona = $zadanie->get_param('strona');
$na_strone = $zadanie->get_param('na_strone');
$kategoria = $zadanie->get_param('kategoria');
$szukaj = $zadanie->get_param('szukaj');
$kolejnosc = $zadanie->get_param('kolejnosc');
$sortuj_po = $zadanie->get_param('sortuj_po');
// Mapowanie parametru sortowania na pole WP_Query
$mapa_sortowania = [
'date' => 'date',
'title' => 'title',
'price' => 'meta_value_num',
'menu_order' => 'menu_order',
];
$argumenty_zapytania = [
'post_type' => 'product',
'post_status' => 'publish',
'posts_per_page' => $na_strone,
'paged' => $strona,
'orderby' => $mapa_sortowania[$sortuj_po] ?? 'date',
'order' => strtoupper($kolejnosc),
'no_found_rows' => false, // Wymagane dla paginacji
];
// Dodaj filtr kategorii jeśli podany
if (!empty($kategoria)) {
$argumenty_zapytania['tax_query'] = [[
'taxonomy' => 'product_cat',
'field' => 'term_id',
'terms' => $kategoria,
]];
}
// Dodaj filtr wyszukiwania jeśli podany
if (!empty($szukaj)) {
$argumenty_zapytania['s'] = $szukaj;
}
// Dodaj sortowanie po cenie (pole niestandardowe)
if ($sortuj_po === 'price') {
$argumenty_zapytania['meta_key'] = '_price';
}
$zapytanie = new WP_Query($argumenty_zapytania);
$wszystkich = $zapytanie->found_posts;
$stron = $zapytanie->max_num_pages;
if (!$zapytanie->have_posts()) {
return new WP_Error(
'brak_produktow',
'Nie znaleziono produktów dla podanych kryteriów.',
['status' => 404]
);
}
// Formatuj dane produktów do odpowiedzi
$produkty = array_map(
fn(WP_Post $produkt) => moj_plugin_formatuj_produkt($produkt),
$zapytanie->posts
);
wp_reset_postdata();
// Utwórz obiekt odpowiedzi (ang. response object)
$odpowiedz = new WP_REST_Response($produkty, 200);
// Dodaj standardowe nagłówki paginacji (zgodnie z konwencją REST API WordPress)
$odpowiedz->header('X-WP-Total', $wszystkich);
$odpowiedz->header('X-WP-TotalPages', $stron);
// Dodaj nagłówki CORS (ang. Cross-Origin Resource Sharing — współdzielenie zasobów)
$odpowiedz->header('Access-Control-Expose-Headers', 'X-WP-Total, X-WP-TotalPages');
return $odpowiedz;
}
/**
* Formatuje pojedynczy produkt do struktury API
*/
function moj_plugin_formatuj_produkt(WP_Post $produkt): array {
return [
'id' => $produkt->ID,
'slug' => $produkt->post_name,
'tytul' => html_entity_decode(
get_the_title($produkt->ID),
ENT_QUOTES,
'UTF-8'
),
'opis' => wpautop(wp_strip_all_tags($produkt->post_content)),
'krotki_opis' => wp_strip_all_tags($produkt->post_excerpt),
'cena' => (float) get_post_meta($produkt->ID, '_price', true),
'cena_regularna' => (float) get_post_meta($produkt->ID, '_regular_price', true),
'link' => get_permalink($produkt->ID),
'miniatura' => get_the_post_thumbnail_url($produkt->ID, 'medium') ?: null,
'kategorie' => wp_list_pluck(
get_the_terms($produkt->ID, 'product_cat') ?: [],
'name'
),
'data_zmiany' => get_the_modified_date('c', $produkt->ID),
'_links' => [
'self' => [['href' => rest_url("moj-plugin/v1/produkty/{$produkt->ID}")]],
],
];
}
Funkcja sprawdzania uprawnień
<?php
/**
* Funkcja sprawdzania uprawnień (ang. permission callback)
* dla operacji wymagających roli edytora lub wyżej.
*/
function moj_plugin_uprawnienia_edytora(WP_REST_Request $zadanie): bool|WP_Error {
// Sprawdź metodę uwierzytelniania — Hasło Aplikacji lub JWT
if (!is_user_logged_in()) {
return new WP_Error(
'brak_uwierzytelnienia',
'Musisz być zalogowany, aby wykonać tę operację.',
['status' => 401]
);
}
if (!current_user_can('edit_posts')) {
return new WP_Error(
'brak_uprawnien',
'Nie masz uprawnień do tej operacji. Wymagana rola: Edytor lub wyżej.',
['status' => 403]
);
}
return true;
}
/**
* Uprawnienia tylko dla administratorów
*/
function moj_plugin_uprawnienia_administratora(): bool|WP_Error {
if (!current_user_can('manage_options')) {
return new WP_Error(
'brak_uprawnien_admina',
'Operacja wymaga uprawnień administratora.',
['status' => 403]
);
}
return true;
}
Rejestracja pól niestandardowych w istniejących trasach WordPress
<?php
/**
* Dodaje niestandardowe pole do istniejącego zasobu /posts
* bez modyfikowania podstawowego kodu WordPress.
*/
add_action('rest_api_init', function(): void {
register_rest_field(
'post', // Typ wpisu lub 'user', 'term'
'czas_czytania', // Nazwa nowego pola w odpowiedzi JSON
[
'get_callback' => function(array $dane_wpisu): string {
$tresc = get_post_field('post_content', $dane_wpisu['id']);
$slowa = str_word_count(wp_strip_all_tags($tresc));
$minuty = (int) ceil($slowa / 200); // 200 słów na minutę
return $minuty . ' min';
},
'update_callback' => null, // Pole tylko do odczytu
'schema' => [
'description' => 'Szacowany czas czytania wpisu.',
'type' => 'string',
'context' => ['view', 'embed'],
],
]
);
// Dodaj pole z niestandardowymi danymi SEO do odpowiedzi
register_rest_field('post', 'seo', [
'get_callback' => function(array $dane_wpisu): array {
return [
'tytul_seo' => get_post_meta($dane_wpisu['id'], '_seo_title', true) ?: '',
'opis_meta' => get_post_meta($dane_wpisu['id'], '_seo_description', true) ?: '',
'kanoniczna' => get_permalink($dane_wpisu['id']),
];
},
'update_callback' => function(array $dane_seo, WP_Post $wpis): void {
if (!empty($dane_seo['tytul_seo'])) {
update_post_meta($wpis->ID, '_seo_title', sanitize_text_field($dane_seo['tytul_seo']));
}
if (!empty($dane_seo['opis_meta'])) {
update_post_meta($wpis->ID, '_seo_description', sanitize_textarea_field($dane_seo['opis_meta']));
}
},
'schema' => [
'description' => 'Dane SEO wpisu.',
'type' => 'object',
],
]);
});
Filar 4: Przykłady integracji — gotowy kod produkcyjny
Integracja 1: Wysyłanie danych formularza do zewnętrznego CRM
<?php
/**
* Integracja Contact Form 7 / własnego formularza z zewnętrznym CRM.
* Punkt końcowy odbiera dane z formularza WordPress i przekazuje do CRM.
*/
add_action('rest_api_init', function(): void {
register_rest_route('moj-plugin/v1', '/kontakt', [
'methods' => WP_REST_Server::CREATABLE,
'callback' => 'moj_plugin_przetworz_formularz_kontaktowy',
'permission_callback' => '__return_true',
'args' => [
'imie' => [
'required' => true,
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
'validate_callback' => fn($v) => strlen(trim($v)) >= 2,
],
'email' => [
'required' => true,
'type' => 'string',
'sanitize_callback' => 'sanitize_email',
'validate_callback' => 'is_email',
],
'wiadomosc' => [
'required' => true,
'type' => 'string',
'sanitize_callback' => 'sanitize_textarea_field',
'validate_callback' => fn($v) => strlen(trim($v)) >= 10,
],
'nonce' => [
'required' => true,
'type' => 'string',
],
],
]);
});
function moj_plugin_przetworz_formularz_kontaktowy(WP_REST_Request $zadanie): WP_REST_Response|WP_Error {
// Weryfikacja jednorazowego tokenu bezpieczeństwa
if (!wp_verify_nonce($zadanie->get_param('nonce'), 'formularz_kontaktowy')) {
return new WP_Error('nieprawidlowy_nonce', 'Błąd weryfikacji tokenu.', ['status' => 403]);
}
$dane = [
'imie' => $zadanie->get_param('imie'),
'email' => $zadanie->get_param('email'),
'wiadomosc' => $zadanie->get_param('wiadomosc'),
'ip' => $_SERVER['REMOTE_ADDR'] ?? 'nieznane',
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '',
'data' => current_time('c'),
'zrodlo_url' => wp_get_referer() ?: get_home_url(),
];
// Wyślij do zewnętrznego CRM
$wynik_crm = moj_plugin_wyslij_do_crm($dane);
if (is_wp_error($wynik_crm)) {
// Zapisz w bazie danych jako zapasowy mechanizm (ang. fallback)
moj_plugin_zapisz_kontakt_lokalnie($dane);
// Loguj błąd, ale nie pokazuj szczegółów użytkownikowi
error_log('[Integracja CRM] Błąd: ' . $wynik_crm->get_error_message());
}
// Wyślij e-mail z powiadomieniem bez względu na status CRM
wp_mail(
get_option('admin_email'),
'Nowe zapytanie kontaktowe: ' . $dane['imie'],
"Imię: {$dane['imie']}\nEmail: {$dane['email']}\nWiadomość:\n{$dane['wiadomosc']}"
);
return new WP_REST_Response([
'sukces' => true,
'wiadomosc' => 'Dziękujemy za wiadomość. Odezwiemy się w ciągu 24 godzin.',
], 201);
}
/**
* Wysyła dane kontaktu do zewnętrznego CRM przez API
*/
function moj_plugin_wyslij_do_crm(array $dane): bool|WP_Error {
$klucz_api = get_option('moj_plugin_klucz_crm_api');
$url_crm = get_option('moj_plugin_url_crm');
if (empty($klucz_api) || empty($url_crm)) {
return new WP_Error('brak_konfiguracji', 'Brak konfiguracji integracji CRM.');
}
// Funkcja HTTP WordPress (zamiast cURL — lepsza integracja z ekosystemem WP)
$odpowiedz = wp_remote_post($url_crm . '/api/v2/contacts', [
'headers' => [
'Authorization' => 'Bearer ' . $klucz_api,
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'body' => wp_json_encode([
'first_name' => $dane['imie'],
'email' => $dane['email'],
'note' => $dane['wiadomosc'],
'source' => 'WordPress formularz kontaktowy',
'tags' => ['strona-www', 'formularz-kontaktowy'],
'custom' => [
'zrodlo_url' => $dane['zrodlo_url'],
'ip_adres' => $dane['ip'],
],
]),
'timeout' => 15,
]);
// Sprawdź błąd połączenia (timeout, DNS, SSL)
if (is_wp_error($odpowiedz)) {
return $odpowiedz;
}
$kod_statusu = wp_remote_retrieve_response_code($odpowiedz);
$tresc_odpowiedzi = wp_remote_retrieve_body($odpowiedz);
if ($kod_statusu < 200 || $kod_statusu >= 300) {
return new WP_Error(
'blad_crm_api',
"CRM zwrócił błąd {$kod_statusu}: {$tresc_odpowiedzi}"
);
}
return true;
}
/**
* Zapasowy mechanizm — zapisuje kontakt w bazie danych WordPress
*/
function moj_plugin_zapisz_kontakt_lokalnie(array $dane): void {
global $wpdb;
$wpdb->insert(
$wpdb->prefix . 'moj_plugin_kontakty',
[
'imie' => $dane['imie'],
'email' => $dane['email'],
'wiadomosc' => $dane['wiadomosc'],
'data' => $dane['data'],
'ip' => $dane['ip'],
],
['%s', '%s', '%s', '%s', '%s']
);
}
Integracja 2: Odbieranie webhooków z zewnętrznego systemu
Webhook (ang. hak sieciowy — automatyczne powiadomienie HTTP wysyłane przez zewnętrzny system) pozwala na reagowanie w czasie rzeczywistym na zdarzenia w zewnętrznych usługach (np. nowa płatność w bramce, nowe zamówienie w marketplace).
<?php
/**
* Odbieranie webhooków z bramki płatności (np. Stripe, Przelewy24)
* z weryfikacją podpisu kryptograficznego (ang. cryptographic signature)
*/
add_action('rest_api_init', function(): void {
register_rest_route('moj-plugin/v1', '/webhook/platnosc', [
'methods' => WP_REST_Server::CREATABLE,
'callback' => 'moj_plugin_obsgruz_webhook_platnosci',
'permission_callback' => 'moj_plugin_weryfikuj_podpis_webhooka',
]);
});
/**
* Weryfikuje podpis webhooka przed przetworzeniem danych.
* Chroni przed fałszywymi żądaniami od nieuprawnionych źródeł.
*/
function moj_plugin_weryfikuj_podpis_webhooka(WP_REST_Request $zadanie): bool|WP_Error {
$podpis_naglowka = $zadanie->get_header('X-Stripe-Signature'); // lub inna bramka
$cialo_zadania = $zadanie->get_body();
$sekret_webhooka = get_option('moj_plugin_sekret_webhooka');
if (empty($podpis_naglowka)) {
return new WP_Error('brak_podpisu', 'Brakujący nagłówek podpisu webhooka.', ['status' => 401]);
}
// Format Stripe: t=timestamp,v1=hash (przykład — dostosuj do swojej bramki)
$czas_wyslania = null;
$otrzymany_hash = null;
foreach (explode(',', $podpis_naglowka) as $czesc) {
[$klucz, $wartosc] = explode('=', $czesc, 2);
if ($klucz === 't') $czas_wyslania = (int) $wartosc;
if ($klucz === 'v1') $otrzymany_hash = $wartosc;
}
// Sprawdź czy webhook nie jest zbyt stary (ochrona przed atakami ponownego odtwarzania)
if (abs(time() - $czas_wyslania) > 300) { // 5 minut tolerancji
return new WP_Error('webhook_przedawniony', 'Webhook jest zbyt stary.', ['status' => 400]);
}
// Oblicz oczekiwany hash HMAC (ang. Hash-based Message Authentication Code)
$ladunek_do_podpisu = "{$czas_wyslania}.{$cialo_zadania}";
$oczekiwany_hash = hash_hmac('sha256', $ladunek_do_podpisu, $sekret_webhooka);
if (!hash_equals($oczekiwany_hash, $otrzymany_hash ?? '')) {
return new WP_Error('nieprawidlowy_podpis', 'Weryfikacja podpisu nieudana.', ['status' => 403]);
}
return true;
}
function moj_plugin_obsgruz_webhook_platnosci(WP_REST_Request $zadanie): WP_REST_Response|WP_Error {
$dane = $zadanie->get_json_params();
$typ_zdarzenia = $dane['type'] ?? '';
$obiekt = $dane['data']['object'] ?? [];
// Natychmiast zwróć 200 — bramki płatności wymagają szybkiej odpowiedzi
// Długotrwałe przetwarzanie deleguj do zadania w tle (ang. background job)
switch ($typ_zdarzenia) {
case 'payment_intent.succeeded': // Płatność zakończona sukcesem
moj_plugin_zaplanuj_zadanie('przetworz_platnosc', $obiekt);
break;
case 'payment_intent.payment_failed': // Płatność odrzucona
moj_plugin_zaplanuj_zadanie('obsgruz_bledna_platnosc', $obiekt);
break;
case 'customer.subscription.deleted': // Anulowana subskrypcja
moj_plugin_zaplanuj_zadanie('obsgruz_anulowanie_subskrypcji', $obiekt);
break;
default:
// Nieobsługiwany typ zdarzenia — zwróć 200 żeby bramka nie próbowała ponownie
return new WP_REST_Response(['ignorowane' => true], 200);
}
// Zapisz zdarzenie do tabeli logów webhooków
moj_plugin_zapisz_log_webhooka($typ_zdarzenia, $obiekt, 'przetworzono');
return new WP_REST_Response(['otrzymano' => true], 200);
}
/**
* Planuje zadanie w tle przez WordPress Cron (ang. harmonogram zadań)
* Unika timeoutów (ang. przekroczeń czasu) przy długotrwałym przetwarzaniu
*/
function moj_plugin_zaplanuj_zadanie(string $nazwa_akcji, array $dane): void {
$klucz_danych = 'webhook_' . uniqid('', true);
set_transient($klucz_danych, $dane, HOUR_IN_SECONDS);
wp_schedule_single_event(time(), "moj_plugin_{$nazwa_akcji}", [$klucz_danych]);
}
// Obsługa zaplanowanego zadania przetworzenia płatności
add_action('moj_plugin_przetworz_platnosc', function(string $klucz_danych): void {
$dane_platnosci = get_transient($klucz_danych);
delete_transient($klucz_danych);
if (empty($dane_platnosci)) return;
$id_zamowienia = $dane_platnosci['metadata']['order_id'] ?? null;
$kwota = ($dane_platnosci['amount'] ?? 0) / 100; // Centsy → złote
$waluta = strtoupper($dane_platnosci['currency'] ?? 'pln');
if ($id_zamowienia) {
// Zaktualizuj status zamówienia w WooCommerce lub własnym systemie
$zamowienie = wc_get_order($id_zamowienia);
if ($zamowienie) {
$zamowienie->payment_complete($dane_platnosci['id']);
$zamowienie->add_order_note("Płatność {$kwota} {$waluta} potwierdzona przez webhook.");
}
}
});
Integracja 3: Buforowanie odpowiedzi z zewnętrznego API
<?php
/**
* Pobieranie kursów walut z API NBP z buforowaniem (ang. caching)
* przez przejściowe dane (ang. transients — WordPress Transients API)
* Unikamy nadmiernego obciążania zewnętrznego API
*/
function moj_plugin_pobierz_kursy_walut(string $tabela = 'A'): array|WP_Error {
$klucz_bufora = "kursy_walut_tabela_{$tabela}_" . gmdate('Y-m-d');
// Sprawdź bufor przed wysłaniem żądania do zewnętrznego API
$buforowane_kursy = get_transient($klucz_bufora);
if ($buforowane_kursy !== false) {
return $buforowane_kursy;
}
$odpowiedz = wp_remote_get(
"<https://api.nbp.pl/api/exchangerates/tables/{$tabela}/?format=json>",
[
'timeout' => 10,
'headers' => ['Accept' => 'application/json'],
]
);
if (is_wp_error($odpowiedz)) {
// Przy błędzie spróbuj zwrócić przestarzałe dane z bufora (jeśli istnieją)
$stary_klucz = get_option("moj_plugin_ostatnie_kursy_{$tabela}");
if ($stary_klucz) {
return $stary_klucz;
}
return $odpowiedz;
}
$kod_statusu = wp_remote_retrieve_response_code($odpowiedz);
if ($kod_statusu !== 200) {
return new WP_Error(
'blad_api_nbp',
"API NBP zwróciło kod {$kod_statusu}.",
['status' => $kod_statusu]
);
}
$dane = json_decode(wp_remote_retrieve_body($odpowiedz), true);
if (empty($dane[0]['rates'])) {
return new WP_Error('puste_dane_nbp', 'API NBP zwróciło puste dane.');
}
$kursy = $dane[0]['rates'];
// Buforuj na 6 godzin (kursy NBP aktualizowane raz dziennie)
set_transient($klucz_bufora, $kursy, 6 * HOUR_IN_SECONDS);
// Zapisz jako trwałą kopię zapasową na wypadek niedostępności API
update_option("moj_plugin_ostatnie_kursy_{$tabela}", $kursy, false);
return $kursy;
}
// Własny punkt końcowy udostępniający kursy walut przez REST API WordPress
add_action('rest_api_init', function(): void {
register_rest_route('moj-plugin/v1', '/kursy-walut', [
'methods' => WP_REST_Server::READABLE,
'callback' => function(WP_REST_Request $zadanie): WP_REST_Response|WP_Error {
$waluta = strtoupper($zadanie->get_param('waluta') ?? '');
$kursy = moj_plugin_pobierz_kursy_walut();
if (is_wp_error($kursy)) {
return $kursy;
}
// Filtruj po konkretnej walucie jeśli podana
if (!empty($waluta)) {
$znaleziona = array_filter($kursy, fn($k) => $k['code'] === $waluta);
if (empty($znaleziona)) {
return new WP_Error(
'nieznana_waluta',
"Waluta {$waluta} nie jest obsługiwana.",
['status' => 404]
);
}
$kursy = array_values($znaleziona);
}
$odpowiedz = new WP_REST_Response([
'data_aktualizacji' => gmdate('Y-m-d'),
'zrodlo' => 'Narodowy Bank Polski',
'kursy' => $kursy,
], 200);
// Ustaw nagłówki buforowania HTTP po stronie klienta
$odpowiedz->header('Cache-Control', 'public, max-age=3600'); // 1 godzina
$odpowiedz->header('Vary', 'Accept-Encoding');
return $odpowiedz;
},
'permission_callback' => '__return_true',
'args' => [
'waluta' => [
'description' => 'Kod waluty ISO 4217, np. EUR, USD, GBP.',
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
'validate_callback' => fn($v) => preg_match('/^[A-Z]{3}$/', strtoupper($v)),
],
],
]);
});
Integracja 4: Dwukierunkowa synchronizacja z systemem ERP
<?php
/**
* Synchronizacja produktów między WordPress/WooCommerce a zewnętrznym systemem ERP
* Kierunek: ERP → WordPress (importowanie produktów z ERP do WooCommerce)
*/
add_action('rest_api_init', function(): void {
register_rest_route('moj-plugin/v1', '/synchronizuj-produkty', [
'methods' => WP_REST_Server::CREATABLE,
'callback' => 'moj_plugin_synchronizuj_produkty_erp',
'permission_callback' => 'moj_plugin_sprawdz_klucz_api',
]);
});
function moj_plugin_synchronizuj_produkty_erp(WP_REST_Request $zadanie): WP_REST_Response|WP_Error {
$produkty_erp = $zadanie->get_json_params();
if (empty($produkty_erp) || !is_array($produkty_erp)) {
return new WP_Error('puste_dane', 'Brak danych produktów w żądaniu.', ['status' => 400]);
}
$wyniki = [
'zaktualizowane' => 0,
'utworzone' => 0,
'bledy' => [],
];
foreach ($produkty_erp as $dane_erp) {
try {
$wynik = moj_plugin_zsynchronizuj_pojedynczy_produkt($dane_erp);
$wyniki[$wynik]++;
} catch (Exception $e) {
$wyniki['bledy'][] = [
'sku' => $dane_erp['sku'] ?? 'nieznany',
'blad' => $e->getMessage(),
];
}
}
$kod_statusu = empty($wyniki['bledy']) ? 200 : 207; // 207 Multi-Status
return new WP_REST_Response($wyniki, $kod_statusu);
}
function moj_plugin_zsynchronizuj_pojedynczy_produkt(array $dane_erp): string {
// Sprawdź czy produkt istnieje po SKU (ang. Stock Keeping Unit — kod produktu)
$args = [
'post_type' => 'product',
'meta_key' => '_sku',
'meta_value' => sanitize_text_field($dane_erp['sku']),
'posts_per_page' => 1,
'post_status' => 'any',
'fields' => 'ids',
];
$istniejace = get_posts($args);
$id_produktu = !empty($istniejace) ? $istniejace[0] : null;
$dane_wpisu = [
'post_type' => 'product',
'post_status' => !empty($dane_erp['aktywny']) ? 'publish' : 'draft',
'post_title' => sanitize_text_field($dane_erp['nazwa']),
'post_content' => wp_kses_post($dane_erp['opis'] ?? ''),
'post_excerpt' => sanitize_textarea_field($dane_erp['krotki_opis'] ?? ''),
];
if ($id_produktu) {
// Aktualizacja istniejącego produktu
$dane_wpisu['ID'] = $id_produktu;
wp_update_post($dane_wpisu, true);
$wynik = 'zaktualizowane';
} else {
// Tworzenie nowego produktu
$id_produktu = wp_insert_post($dane_wpisu, true);
if (is_wp_error($id_produktu)) {
throw new RuntimeException($id_produktu->get_error_message());
}
$wynik = 'utworzone';
}
// Zaktualizuj pola meta (ang. meta fields — dodatkowe dane produktu)
$pola_meta = [
'_sku' => sanitize_text_field($dane_erp['sku']),
'_price' => (string) abs((float) ($dane_erp['cena'] ?? 0)),
'_regular_price' => (string) abs((float) ($dane_erp['cena_regularna'] ?? $dane_erp['cena'] ?? 0)),
'_stock' => (int) ($dane_erp['stan_magazynowy'] ?? 0),
'_manage_stock' => 'yes',
'_stock_status' => ($dane_erp['stan_magazynowy'] ?? 0) > 0 ? 'instock' : 'outofstock',
'_erp_id' => sanitize_text_field($dane_erp['id_erp'] ?? ''),
'_erp_synced_at' => current_time('c'),
];
foreach ($pola_meta as $klucz => $wartosc) {
update_post_meta($id_produktu, $klucz, $wartosc);
}
return $wynik;
}
Zaawansowane techniki — ograniczanie żądań i CORS
Ograniczanie liczby żądań (ang. Rate Limiting)
<?php
/**
* Prosty mechanizm ograniczania liczby żądań (ang. rate limiting)
* oparty na przejściowych danych WordPress i adresie IP.
*/
function moj_plugin_sprawdz_limit_zapytan(
string $klucz_akcji,
int $limit_zapytan = 60,
int $okno_czasowe = MINUTE_IN_SECONDS
): bool|WP_Error {
$ip_klienta = moj_plugin_pobierz_ip_klienta();
$klucz_bufora = "ratelimit_{$klucz_akcji}_" . md5($ip_klienta);
$licznik = (int) get_transient($klucz_bufora);
if ($licznik >= $limit_zapytan) {
return new WP_Error(
'przekroczono_limit',
"Przekroczono limit {$limit_zapytan} żądań na minutę. Spróbuj ponownie za chwilę.",
[
'status' => 429,
'retry_after' => $okno_czasowe,
]
);
}
if ($licznik === 0) {
set_transient($klucz_bufora, 1, $okno_czasowe);
} else {
// Zwiększ licznik bez resetowania czasu wygaśnięcia
set_transient($klucz_bufora, $licznik + 1, $okno_czasowe);
}
return true;
}
function moj_plugin_pobierz_ip_klienta(): string {
// Sprawdź nagłówki proxy (uwaga: mogą być sfałszowane)
$naglowki_proxy = ['HTTP_CF_CONNECTING_IP', 'HTTP_X_FORWARDED_FOR', 'REMOTE_ADDR'];
foreach ($naglowki_proxy as $naglowek) {
if (!empty($_SERVER[$naglowek])) {
$ip = explode(',', $_SERVER[$naglowek])[0];
return filter_var(trim($ip), FILTER_VALIDATE_IP) ?: 'nieznane';
}
}
return 'nieznane';
}
// Użycie w funkcji sprawdzania uprawnień
function moj_plugin_uprawnienia_z_limitem(): bool|WP_Error {
$limit = moj_plugin_sprawdz_limit_zapytan('produkty', 30, MINUTE_IN_SECONDS);
if (is_wp_error($limit)) {
return $limit;
}
return true;
}
Konfiguracja CORS (współdzielenie zasobów między domenami)
<?php
/**
* Konfiguracja CORS (ang. Cross-Origin Resource Sharing — współdzielenie
* zasobów między źródłami) dla WordPress REST API.
* Wymagana gdy frontend działa na innej domenie niż WordPress (headless).
*/
add_action('rest_api_init', function(): void {
remove_filter('rest_pre_serve_request', 'rest_send_cors_headers');
add_filter('rest_pre_serve_request', 'moj_plugin_niestandardowe_naglowki_cors');
});
function moj_plugin_niestandardowe_naglowki_cors(bool $serwuje): bool {
// Lista dozwolonych domen (ang. allowed origins)
$dozwolone_domeny = [
'<https://twoj-frontend.pl>',
'<https://app.twoja-firma.pl>',
'<http://localhost:3000>', // Środowisko deweloperskie
];
$domena_zrodlowa = $_SERVER['HTTP_ORIGIN'] ?? '';
if (in_array($domena_zrodlowa, $dozwolone_domeny, true)) {
header("Access-Control-Allow-Origin: {$domena_zrodlowa}");
header('Access-Control-Allow-Credentials: true');
header('Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Authorization, Content-Type, X-WP-Nonce, X-API-Key');
header('Access-Control-Expose-Headers: X-WP-Total, X-WP-TotalPages, Link');
header('Access-Control-Max-Age: 86400'); // Buforuj konfigurację CORS przez 24h
}
// Obsługa żądań wstępnych OPTIONS (ang. preflight requests)
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
header('HTTP/1.1 204 No Content');
exit;
}
return $serwuje;
}
Lista kontrolna — WordPress REST API integracje gotowe do produkcji
BEZPIECZEŃSTWO
□ Każda trasa ma jawnie zadeklarowaną funkcję sprawdzania uprawnień
□ Hasła Aplikacji lub JWT używane zamiast loginu/hasła głównego
□ Podpisy webhooków weryfikowane kryptograficznie (HMAC-SHA256)
□ Jednorazowe tokeny bezpieczeństwa weryfikowane dla żądań z przeglądarki
□ Dane wejściowe oczyszczane (sanitize_*) i walidowane (validate_callback)
□ Dane wyjściowe oczyszczane (esc_html, wp_kses_post) przed wysłaniem
□ HTTPS wymagane dla wszystkich żądań API
□ Klucze API przechowywane jako opcje WordPress, nie w kodzie źródłowym
WYDAJNOŚĆ I NIEZAWODNOŚĆ
□ Odpowiedzi z zewnętrznych API buforowane przez Transients API
□ Mechanizm zapasowy (fallback) gdy zewnętrzne API jest niedostępne
□ Ograniczanie liczby żądań (rate limiting) dla publicznych punktów końcowych
□ Timeout ustawiony dla wp_remote_get/post (min. 10–15 sekund)
□ Długotrwałe zadania delegowane do kolejki (wp_schedule_single_event)
□ Nagłówki buforowania HTTP ustawione dla odpowiedzi statycznych
JAKOŚĆ KODU I UTRZYMANIE
□ Przestrzeń nazw w formacie nazwa-wtyczki/v1 (wersjonowanie)
□ Schemat (schema) zdefiniowany dla każdego argumentu
□ Logowanie błędów przez error_log() z kontekstem
□ Tabela logów webhooków z przyczynami błędów
□ Kod podzielony na klasy/funkcje o jednej odpowiedzialności
□ Testy jednostkowe dla funkcji transformujących dane
DOKUMENTACJA
□ Każdy punkt końcowy ma opis w argumencie 'description'
□ Schemat argumentów pozwala na auto-generowanie dokumentacji
□ README z przykładami żądań dla każdej integracji
□ Środowisko testowe z przykładowymi danymi
Podsumowanie
WordPress REST API integracje z zewnętrznymi systemami to jeden z najbardziej wartościowych obszarów kompetencji nowoczesnego dewelopera WordPress. Wbudowany interfejs REST, rozbudowany system Haseł Aplikacji, możliwość rejestracji własnych punktów końcowych z pełną walidacją danych i mechanizmy buforowania — to kompletny zestaw narzędzi do budowania profesjonalnych, bezpiecznych integracji.
Kluczowe wnioski: zawsze deklaruj funkcję sprawdzania uprawnień — puste __return_true to świadoma decyzja o publicznym dostępie, nie zapomnienie. Używaj wp_remote_get/post zamiast cURL bezpośrednio — dostajesz integrację z filtrem http_api_args i obsługę błędów zgodną z ekosystemem WordPress. Buforuj odpowiedzi zewnętrznych API przez set_transient z mechanizmem zapasowym na wypadek niedostępności usługi. Webhooki obsługuj asynchronicznie — natychmiast zwróć 200, przetwarzaj w tle.
WordPress REST API integracje to nie tylko technologia — to architektura, która otwiera WordPress na cały ekosystem zewnętrznych narzędzi i czyni z niego centrum cyfrowego biznesu, a nie tylko silnik stron internetowych.
Budujesz konkretną integrację i natrafiłeś na problem? Opisz case w komentarzach — chętnie pomogę z implementacją.


