<? phpukraine СПІВБЕСІДИ
Пошук по платформі

WordPress для Junior: питання на співбесіду

2 питання рівня Junior з теми WordPress з розгорнутими відповідями, порадами та перевіркою.

Тема
Рівень
2 питання
WP
WordPress·Junior ·хуки ·actions ·filters

Action виконує побічну дію в певній точці життєвого циклу й нічого не повертає; filter отримує значення, змінює його й обовʼязково повертає.

Що станеться, якщо фільтр нічого не поверне?
Як контролювати порядок виконання кількох хуків?
Чому після активації плагіна зник контент сторінок?

Хуки — це спосіб WordPress дозволити чужому коду втрутитись у свій без правки ядра. Action виконує побічну дію в певній точці: щось вивести, записати в лог, надіслати лист. Він нічого не повертає. Filter отримує значення, наприклад заголовок або контент, змінює його й повертає далі по ланцюжку. Внутрішньо обидва живуть в одному реєстрі WP_Hook, і add_action є обгорткою над add_filter; різниця лише в тому, чи використовується результат.

Найчастіша помилка новачків — фільтр без return. Колбек повертає null, null іде далі по ланцюжку і в код, який викликав apply_filters, і на сайті зникають заголовки, контент чи ціни. WordPress не перевіряє тип результату, тому помилка тиха.

Порядок задається пріоритетом: менше число виконується раніше, за замовчуванням 10. Якщо два плагіни фільтрують the_content з однаковим пріоритетом, результат залежить від порядку завантаження, і саме звідси більшість конфліктів. Четвертий аргумент accepted_args визначає, скільки параметрів отримає колбек; без нього другий аргумент фільтра не прийде.

Щоб зняти чужий хук, remove_filter потребує те саме імʼя, той самий колбек і той самий пріоритет. Тому у власних плагінах хуки краще реєструвати іменованими функціями або методами з доступним екземпляром, а не анонімними замиканнями.

// Action: побічна дія, нічого не повертає
add_action('wp_footer', function (): void {
    echo '<!-- rendered at '.date('c').' -->';
});

// Filter: отримує значення й ОБОВʼЯЗКОВО повертає
add_filter('the_title', function (string $title, int $postId): string {
    return is_admin() ? $title : trim($title).' ✦';
}, 10, 2); // пріоритет 10, колбек приймає 2 аргументи

// Класична помилка: контент зникає, бо повертається null
add_filter('the_content', function (string $content): void {
    str_replace('[year]', date('Y'), $content); // немає return
});

// Свій код стає розширюваним
function shop_shipping_cost(int $cents): int
{
    return (int) apply_filters('shop_shipping_cost', $cents);
}
do_action('shop_order_shipped', $orderId);

// Знімаємо чужий хук: імʼя + колбек + пріоритет мають збігатись
add_action('init', fn () => remove_filter('the_content', 'wpautop', 10), 20);
Що обидва механізми побудовані на одному реєстрі: add_filter і add_action це та сама функція, різниця в тому, чи використовується повернене значення.
Що забутий return у фільтрі повертає null, і саме так зникають контент, заголовки чи ціни після активації плагіна.
Що пріоритет визначає порядок: менше число виконується раніше, за замовчуванням 10, і конфлікти плагінів часто саме через однакові пріоритети.
Що четвертий аргумент accepted_args обмежує, скільки параметрів отримає колбек, і без нього другий аргумент фільтра просто не прийде.
Що remove_action і remove_filter потребують той самий колбек і пріоритет, тому анонімні функції в хуках не можна зняти.
Казати, що це синоніми або що різниця лише в назві.
Забувати return у фільтрі й ламати вивід усього сайту.
Використовувати action там, де треба змінити значення: писати в глобальну змінну замість повернення з фільтра.
Вішати хуки на анонімні функції або в неправильний момент, наприклад до plugins_loaded, коли потрібні класи ще не завантажені.
Не вказувати accepted_args і дивуватись, що $post чи $context у колбеку відсутній.
ПОРАДА

Додайте, що пріоритет хука визначає порядок, і конфлікти плагінів найчастіше саме через однакові пріоритети. Згадайте apply_filters і do_action як точки, де ваш код сам стає розширюваним.

Сторінка питання →
WP
WordPress·Junior ·ієрархія шаблонів ·child theme ·template-loader

Після резолву запиту wp-includes/template-loader.php перебирає умовні теги (is_404, is_search, is_front_page, is_home, is_singular, is_archive…) і для першого збігу будує список кандидатів від найконкретнішого до index.php; locate_template() шукає кожного кандидата спершу в child theme, потім у parent, тому перекриття — це файл із такою самою назвою в дочірній темі.

Я поклав у тему файл page-contacts.php, а сторінка «Контакти» все одно рендериться через page.php. Чому?
Який файл теми відповість за URL /category/news/, якщо в темі є index.php, archive.php і category.php?
Чому після оновлення батьківської теми зникли всі мої правки в single.php?
У чому різниця між get_template_directory() і get_stylesheet_directory()?

Коли WordPress відпрацював WP_Query і знає, що саме запитали, керування отримує wp-includes/template-loader.php. Він послідовно перевіряє умовні теги — is_embed(), is_404(), is_search(), is_front_page(), is_home(), is_post_type_archive(), is_tax(), is_attachment(), is_single(), is_page(), is_singular(), is_category(), is_tag(), is_author(), is_date(), is_archive() — і для першого збігу викликає відповідну get_*_template(). Ця функція не шукає файл сама: вона будує масив імен-кандидатів від найконкретнішого до найзагальнішого і передає його в get_query_template(), а той у locate_template(). Тобто «ієрархія» — це буквально впорядкований масив рядків, і будь-який запит завершується щонайпізніше на index.php, який тому й обовʼязковий для валідної теми.

Конкретність кандидатів рахується від даних поточного обʼєкта. Для одного запису це single-{post_type}-{post_name}.php, single-{post_type}.php, single.php, singular.php, index.php; для сторінки — спершу кастомний шаблон із метаполя _wp_page_template, далі page-{slug}.php, page-{id}.php, page.php, singular.php; для категорії — category-{slug}.php, category-{id}.php, category.php, archive.php. Звідси дві типові пастки джуна: слаг береться з post_name, а не з заголовка, тому сторінка «Контакти» з URL /kontakty/ шукає page-kontakty.php; і ID/слаг беруться саме поточного терміна, без успадкування від батьківської категорії. Окремо стоїть головна: front-page.php перекриває все, а якщо його немає, вибір залежить від налаштування «Головна сторінка відображає» — статична сторінка йде в page-гілку, стрічка постів у home.php.

Дочірня тема виграє не тому, що має якийсь пріоритет, а через порядок пошуку всередині locate_template(): спершу STYLESHEETPATH (активна, тобто дочірня, тема), потім TEMPLATEPATH (батьківська), потім wp-includes/theme-compat. Тому перекриття будь-якого шаблону — це просто файл з такою самою назвою в теці дочірньої теми, без жодної реєстрації. Сама дочірня тема — це тека з style.css, у заголовку якого є рядок Template: з іменем теки батьківської теми. Стилі батьківської теми дочірня має підключити сама через wp_enqueue_style на хуку wp_enqueue_scripts — застарілий @import у CSS сповільнює завантаження і його давно не використовують.

Виняток один, але важливий: functions.php через locate_template() не проходить. WordPress підключає обидва файли — спершу дочірній, потім батьківський, — тому копіювати батьківський цілком не можна, це фатальна помилка про повторне оголошення. Замінити функцію батьківської теми вдасться лише тоді, коли вона обгорнута в if (! function_exists()); інакше залишаються remove_action, remove_filter або передбачені темою фільтри. Так само не перекриється частина шаблону, яку батьківська тема підключає жорстким include get_template_directory().'/...' замість get_template_part() — це найчастіша причина «чому мій файл ігнорується».

Коли файлу створювати не хочеться або код живе в плагіні, у вибір можна втрутитися фільтрами: {$type}_template_hierarchy (зʼявився у WP 4.7) міняє список кандидатів до пошуку, {$type}_template — уже знайдений шлях, а template_include спрацьовує останнім і перекриває всіх. Обмеження теж варто памʼятати: у блокових темах (WP 5.9 і новіші) та сама логіка додатково шукає HTML-файли в теці templates/ і шаблони, збережені користувачем у базі як записи wp_template, і знайдений блоковий шаблон має перевагу над однойменним PHP-файлом. І практична порада замість зубріння схеми: повісьте на template_include логер або поставте Query Monitor — він показує і перелік кандидатів, і файл-переможець.

// wp-content/themes/parent-child/style.css має містити заголовок:
// Theme Name: Parent Child
// Template: parent      <- імʼя ТЕКИ батьківської теми, не її назва

// functions.php дочірньої теми: він не перекриває батьківський, а додається до нього
add_action('wp_enqueue_scripts', function (): void {
    // get_template_directory_uri() = батьківська тема, get_stylesheet_* = активна (дочірня)
    wp_enqueue_style('parent-style', get_template_directory_uri().'/style.css');

    wp_enqueue_style(
        'child-style',
        get_stylesheet_uri(),
        ['parent-style'], // вантажимо після батьківського, щоб правила перебивали
        wp_get_theme()->get('Version')
    );
});

// Перекриття файлом: wp-content/themes/parent-child/single-product.php
// Достатньо однакової назви — locate_template() перевіряє дочірню тему першою.

// Частини шаблону підключаємо так, щоб їх теж можна було перекрити з дочірньої теми
get_template_part('parts/product', 'card'); // шукає parts/product-card.php, потім parts/product.php
// а НЕ: include get_template_directory().'/parts/product-card.php'; — це жорстко батьківська тема

// Додати власного кандидата перед стандартними, без правки ядра теми
add_filter('single_template_hierarchy', function (array $templates): array {
    if (has_term('sale', 'product_cat')) {
        array_unshift($templates, 'single-product-sale.php');
    }

    return $templates; // фільтр мусить повернути масив
});

// Діагностика: який саме файл виграв
add_filter('template_include', function (string $template): string {
    error_log('template: '.$template); // спрацьовує останнім, після всіх *_template

    return $template;
}, PHP_INT_MAX);
Що ієрархія — це не магія, а список кандидатів: get_query_template() віддає масив імен від конкретного до загального, а locate_template() бере перший наявний файл.
Що child theme виграє не через «пріоритет теми», а через порядок пошуку в locate_template(): STYLESHEETPATH (дочірня), потім TEMPLATEPATH (батьківська), потім theme-compat.
Що functions.php — виняток: він не перекривається, а завантажується додатково, причому дочірній раніше за батьківський.
Що index.php обовʼязковий і є останнім кандидатом для будь-якого запиту, тому «білий екран» через ієрархію не буває — буває неправильний файл.
Що для front page і блогу правила різні: front-page.php перекриває обидва варіанти, а далі все залежить від налаштування «Головна сторінка відображає» — статична сторінка йде в page-ієрархію, стрічка постів у home.php.
Що перекрити вибір можна й без файлу: фільтрами {$type}_template_hierarchy (з WP 4.7), {$type}_template і template_include, який спрацьовує останнім.
Плутати page-{slug}.php і page-{id}.php з назвою заголовка: слаг береться з post_name, тому сторінка «Контакти» з URL /kontakty/ шукає page-kontakty.php, а не page-contacts.php.
Правити файли батьківської теми напряму й втрачати зміни при оновленні.
Копіювати в дочірню тему functions.php батьківської цілком — отримуєте фатальну помилку «Cannot redeclare function», бо обидва файли виконуються.
Підключати частини шаблону через include get_template_directory().'/parts/card.php' замість get_template_part('parts/card') — жорсткий шлях у батьківську тему вимикає перекриття з дочірньої.
Вважати, що category-5.php спрацює для дочірньої категорії: ієрархія бере слаг і ID саме поточного терміна, без успадкування від батьківського.
Забувати рядок Template: у style.css дочірньої теми або писати туди назву теми замість імені теки батьківської.
ПОРАДА

Скажіть, що будь-яку сторінку можна продіагностувати за пів хвилини: у дочірній тезі повісити на template_include функцію, яка логує обраний файл, або поставити Query Monitor — він показує і список кандидатів, і переможця. Це сильніше за перелік файлів напамʼять.

Сторінка питання →
Прогрес карток і тестів зберігається у профілі. Створити профіль·Увійти