Иерархия шаблонов WordPress (Template Hierarchy) представляет собой декларативную систему маршрутизации представлений (View Routing Subsystem), сопоставляющую внутреннее состояние глобального запроса WP_Query с файловой структурой активной темы. После завершения фазы разбора URL (WP::parse_request) и исполнения SQL-запроса в wp-blog-header.php, ядро передает управление диспетчеру wp-includes/template-loader.php.
В современном WordPress (версии 6.7–7.0+ «Armstrong») сосуществуют два параллельных движка резолвинга шаблонов:
is_single(), is_archive(), is_page()) и каскадный поиск файлов .php от наиболее специфичных (содержащих слаг или ID сущности) к общему запасному варианту index.php. Выбор осуществляется через функцию get_query_template($type, $templates) с проверкой файловой системы через locate_template().wp_is_block_theme()). Архитектура FSE радикально меняет вектор поиска: шаблоны хранятся в формате блочной HTML-разметки (templates/{slug}.html, parts/{slug}.html). При этом ядро реализует четырехслойный приоритет: пользовательские модификации из базы данных (Custom Post Types wp_template и wp_template_part, созданные в Site Editor) имеют абсолютный приоритет над файлами дочерней темы, файлы дочерней темы — над родительской, а на уровне плагинов используется нативный API register_block_template(). Рендеринг блочного дерева делегируется обработчику wp-includes/template-canvas.php.Точкой расширения для разработчиков выступает конвейер фильтров: {$type}_template_hierarchy (модификация списка кандидатов до обращения к диску), {$type}_template (фильтрация пути конкретного типа), template_include (финальная подмена исполняемого файла), register_block_template() (программная регистрация блочного шаблона из плагина) и экшен template_redirect (контрольная точка перед формированием вывода).
В современном WordPress разрешение представлений происходит не просто по именам файлов на диске, а через строгий многослойный каскад: 1. Пользовательские правки из базы данных (wp_template CPT из Site Editor) → 2. Файлы дочерней темы (STYLESHEETPATH) → 3. Файлы родительской темы (TEMPLATEPATH) → 4. Нативные шаблоны плагинов (register_block_template). Любая кастомизация в Site Editor имеет абсолютный приоритет над файловой системой до явного сброса настроек.
Маршрутизацию шаблонов необходимо представлять как двухуровневый нисходящий конвейер: сначала вычисляется семантический тип запроса по флагам WP_Query, после чего запускается каскадный резолвер с проверкой слоев хранения (База данных → Дочерняя тема → Родительская тема → Фолбэк ядра).
[Входящий запрос: WP::main()]
│
▼
[WP_Query компилирует флаги]
(is_single, is_page, is_category, is_404...)
│
▼
[wp-includes/template-loader.php]
│
Проверка: wp_is_block_theme()?
┌──────────────┴──────────────┐
▼ ▼
ДА (Block Theme) НЕТ (Classic Theme)
│ │
locate_block_template() get_query_template()
│ │
1. CPT wp_template в БД? │
(Правки Site Editor) │
2. Child: /templates/*.html 1. Child: child-theme/*.php
3. Parent: /templates/*.html 2. Parent: parent-theme/*.php
│ │
▼ ▼
template-canvas.php template_include фильтр
│ │
▼ ▼
Рендеринг блоков Gutenberg Подключение PHP-файлаВычисление булевых условных тегов (is_front_page, is_single, is_archive, is_search, is_404) на основе разобранных query_vars.
Флаги вычисляются строго до запуска template-loader.php; любое вмешательство через query_posts исказит контекст.
Генерация упорядоченного массива имен файлов от узкоспецифичных к общим. Прогон через фильтр {$type}_template_hierarchy.
Плагины могут внедрять кастомные шаблоны на основе A/B когорт или геолокации без HTTP-редиректов.
Для FSE-тем: проверка наличия опубликованного поста CPT wp_template или wp_template_part с соответствующим слагом.
Записи в базе данных имеют абсолютный приоритет над файловой системой диска до явного сброса кастомизаций.
Проверка наличия файла в STYLESHEETPATH (дочерняя тема), затем в TEMPLATEPATH (родительская тема) через locate_template().
Каждый шаг проверяет файловую систему, поэтому в High-Load средах критически важен прогрев кэша OPcache.
Финальная фильтрация через template_include и запуск буфера вывода через template-canvas.php (для блоков) или прямой include.
Последний рубеж для перехвата ответа: можно вернуть путь к альтернативному файлу или отдать JSON.
Диспетчер template-loader.php опрашивает флаги запроса: is_404(), is_search(), is_front_page(), is_home(), is_singular(), is_archive().
Условные теги вычисляются строго на основе свойств $wp_query->is_*.
Ядро вызывает get_query_template($type), где $type — определенный тип страницы (например, single, page, category).
Запускается динамический фильтр {$type}_template_hierarchy, куда плагины могут внедрить дополнительные имена файлов.
Если активна блочная тема, ядро проверяет таблицу wp_posts на наличие CPT wp_template с указанным post_name. При отсутствии записи проверяются файлы /templates/*.html темы.
При обнаружении блочного шаблона управление передается в template-canvas.php.
Для классических тем перебираются кандидаты в дочерней теме (STYLESHEETPATH), затем в родительской теме (TEMPLATEPATH).
Первый найденный на диске файл считается победителем. Если ничего не найдено, берется index.php темы.
Путь к найденному файлу пропускается через фильтр template_include. Возвращенный файл подключается через include $template.
Если фильтр вернул пустую строку или false, рендеринг прекращается без вывода.
При обработке каждого классического HTTP-запроса функция locate_template() выполняет серию системных вызовов file_exists() к диску для каждого кандидата в дочерней и родительской теме. На высоконагруженных порталах с сотнями запросов в секунду это создает бутылочное горлышко дисковой подсистемы. Решение: использование прогретого OPcache (opcache.validate_timestamps=0 на продакшене) либо in-memory кэширование найденных путей в памяти процесса.
В панели управления в разделе «Настройки чтения» выбрана опция «Статическая страница» (Главная: «О компании», Страница записей: «Новости»). Какой шаблон выберет ядро для отображения главной страницы сайта, если в теме присутствуют оба файла: front-page.php и home.php?
your-theme/my-plugin/single-item.php) с сохранением каскада дочерних тем и фолбэком на дистрибутив плагина.template_redirect для генерации ответов Edge-кэша, SSR-проксирования на внешние сервисы (Next.js, Remix) или выдачи высокоскоростных JSON API ответов до загрузки тяжелых ресурсов темы.single_template_hierarchy на основе cookie сегмента аудитории, геолокации или подписки пользователя без смены канонического URL и без внешних HTTP 302 редиректов.header.php, footer.php) на блочные фрагменты с помощью вызова block_template_part('header'), позволяющий контент-менеджерам использовать редактор Gutenberg без переписывания всего бэкенда темы.В гибридных темах вы можете сохранять привычные PHP-шаблоны (single.php, page.php), но делегировать шапку и подвал визуальному редактору блоков. Функция block_template_part('header') ищет блочный шаблон в директории parts/header.html, парсит Gutenberg-блоки и компилирует чистый HTML, поддерживая полный функционал Block Bindings и Site Editor.
Для осознанного выбора между типами тем и проектирования расширяемых интерфейсов необходимо сопоставить архитектурные различия трех подходов.
В классическом конвейере ядро последовательно перебирает условные теги в wp-includes/template-loader.php. Рассмотрим детальный порядок поиска для трех ключевых сценариев:
single-{post_type})Если запрошен пост CPT project со слагом alpha и ID 42:
single-project-alpha.php — шаблон для конкретной записи по слагу.single-project.php — шаблон для всех записей типа project.single.php — общий шаблон для любых одиночных записей.singular.php — универсальный шаблон для всех единичных сущностей (посты, страницы, CPT, вложения).index.php — финальный системный фолбэк.taxonomy-{taxonomy})Если запрошен терм fintech таксономии industry:
taxonomy-industry-fintech.php — шаблон для конкретного терма по слагу.taxonomy-industry.php — шаблон для всей таксономии industry.taxonomy.php — общий шаблон для любых кастомных таксономий.archive.php — общий шаблон для всех типов архивов (авторы, даты, категории, CPT).index.php — финальный фолбэк.front-page.php vs home.phpfront-page.php. Если его нет, загружается home.php, а при его отсутствии — index.php.front-page.php (наивысший приоритет). Если его нет — применяется стандартная иерархия страниц (Custom Template → page-{slug}.php → page.php → singular.php → index.php).home.php, затем index.php.Когда активна блочная тема (wp_is_block_theme() === true), функция locate_block_template() выполняет поиск по следующему строгому алгоритму:
[Запрос шаблона: "single-project"]
│
▼
1. Поиск в БД: CPT wp_template (post_name = 'single-project', status = 'publish')
└── Найдено в БД? ──► ДА ──► Рендеринг из БД (пользовательский шаблон из Site Editor)
│ НЕТ
▼
2. Поиск в ФС Дочерней темы: {STYLESHEETPATH}/templates/single-project.html
└── Найдено на диске? ──► ДА ──► Рендеринг файла дочерней темы
│ НЕТ
▼
3. Поиск в ФС Родительской темы: {TEMPLATEPATH}/templates/single-project.html
└── Найдено на диске? ──► ДА ──► Рендеринг файла родительской темы
│ НЕТ
▼
4. Переход к следующему менее специфичному кандидату иерархии (single.html ──► index.html)<?php
/**
* Plugin Name: Fullstack Agentic Lab — Enterprise Template Hierarchy Engine
* Description: Высокоскоростной резолвер шаблонов с in-memory кэшированием и поддержкой theme overrides.
* Version: 2.1.0
* Author: Artur Yusupov (Fullstack Agentic Lab)
*/
declare(strict_types=1);
namespace AgenticLab\Core\Templates;
if (!defined('ABSPATH')) {
exit;
}
final class EnterpriseTemplateEngine
{
private static array $resolvedCache = [];
private static string $pluginTemplateDir = '';
public static function init(string $pluginPath): void
{
self::$pluginTemplateDir = trailingslashit($pluginPath) . 'templates/';
// Встраивание в цепочку резолвинга иерархии ядра (First-Class Callable PHP 8.4+)
add_filter('template_include', self::resolveTemplateCascade(...), 99);
add_filter('single_template_hierarchy', self::injectCustomHierarchyCandidates(...), 10, 1);
}
/**
* Динамическое расширение списка кандидатов иерархии для кастомных типов записей.
* Позволяет добавлять кандидатов на основе метаданных, региона или A/B сегмента.
*/
public static function injectCustomHierarchyCandidates(array $templates): array
{
if (!is_singular('project')) {
return $templates;
}
$projectId = get_queried_object_id();
$projectLayout = get_post_meta($projectId, '_project_layout_variant', true);
if (!empty($projectLayout) && is_string($projectLayout)) {
// Внедряем специфичный шаблон варианта с наивысшим приоритетом
array_unshift($templates, sprintf('single-project-variant-%s.php', sanitize_file_name($projectLayout)));
}
return $templates;
}
/**
* Финальный каскадный резолвинг шаблона с защитой от лишних обращений к диску (I/O Bottlenecks).
*/
public static function resolveTemplateCascade(string $defaultTemplate): string
{
// Пропускаем нестандартные запросы (REST, AJAX, CLI)
if (wp_is_json_request() || (defined('DOING_AJAX') && DOING_AJAX)) {
return $defaultTemplate;
}
$cacheKey = md5((string)($_SERVER['REQUEST_URI'] ?? '') . ':' . (int)is_user_logged_in());
if (isset(self::$resolvedCache[$cacheKey])) {
return self::$resolvedCache[$cacheKey];
}
// Пример: перехват вывода архива проектов
if (is_post_type_archive('project')) {
$candidates = [
'agentic-lab/archive-project.php', // Приоритет 1: Папка в теме оформления
'archive-project.php', // Приоритет 2: Корень темы
];
$locatedInTheme = locate_template($candidates, false, false);
if (!empty($locatedInTheme)) {
self::$resolvedCache[$cacheKey] = $locatedInTheme;
return $locatedInTheme;
}
// Приоритет 3: Дефолтный шаблон плагина
$fallbackPluginTemplate = self::$pluginTemplateDir . 'archive-project.php';
if (file_exists($fallbackPluginTemplate)) {
self::$resolvedCache[$cacheKey] = $fallbackPluginTemplate;
return $fallbackPluginTemplate;
}
}
self::$resolvedCache[$cacheKey] = $defaultTemplate;
return $defaultTemplate;
}
/**
* Публичный хелпер для подключения изолированных частей шаблона (Template Parts)
* с передачей типизированного контекста без загрязнения глобальной области видимости.
*/
public static function renderPart(string $slug, array $context = []): void
{
$templateFile = locate_template([
sprintf('agentic-lab/parts/%s.php', $slug),
sprintf('parts/%s.php', $slug),
], false, false);
if (empty($templateFile)) {
$templateFile = self::$pluginTemplateDir . sprintf('parts/%s.php', $slug);
}
if (!file_exists($templateFile)) {
error_log(sprintf('[AGENTIC-LAB] Template part "%s" not found at path: %s', $slug, $templateFile));
return;
}
// Изолированное замыкание для исключения утечки $context в глобальный scope
(static function(string $__path, array $__args): void {
extract($__args, EXTR_SKIP);
include $__path;
})($templateFile, $context);
}
}
// Инициализация сервиса
EnterpriseTemplateEngine::init(WP_CONTENT_DIR . '/plugins/agentic-lab-core/');Разработчик внес правки в файл темы templates/single.html и выполнил деплой на сервер. Однако на сайте по-прежнему отображается старая версия шаблона. В чем причина такого поведения ядра в FSE-темах?
В чем суть ошибки: Разработчик вешает колбэк на экшен template_redirect и выводит HTML, JSON или выполняет echo до завершения работы загрузчика шаблонов.
Почему разработчики так делают: Попытка быстро обработать вебхук, форму или отдать кастомный ответ до загрузки тяжелого шаблона темы.
Что происходит в продакшене: Срабатывание echo немедленно отправляет тело ответа и HTTP-заголовки. WordPress в дальнейшем пытается отправить канонические заголовки и куки, генерируя фатальную ошибку Warning: Cannot modify header information - headers already sent. Плагины полностраничного кэширования кэшируют неполный вывод.
Как правильно: Если на стадии template_redirect формируется кастомный ответ, выполнение скрипта ОБЯЗАНО завершаться немедленно через exit; или системные функции wp_send_json() / wp_die():
// ОШИБКА:
add_action('template_redirect', function(): void {
if (is_page('api-proxy')) {
echo json_encode(['status' => 'ok']); // Headers already sent!
}
});
// ПРАВИЛЬНО:
add_action('template_redirect', function(): void {
if (is_page('api-proxy')) {
wp_send_json(['status' => 'ok'], 200); // Отправляет правильные заголовки и вызывает exit
}
});В чем суть ошибки: Использование константы TEMPLATEPATH или функции get_template_directory() для подключения подшаблонов через include: include get_template_directory() . '/parts/header.php';.
Почему разработчики так делают: Кажется очевидным способом получить абсолютный путь к файлам темы.
Что происходит в продакшене: Полная потеря поддержки дочерних тем (Child Themes). Если клиент или разработчик создает дочернюю тему и переопределяет файл parts/header.php, WordPress продолжит загружать файл из родительской темы, игнорируя кастомизацию.
Как правильно: Всегда использовать функцию get_template_part() или locate_template(), проверяющие сначала дочернюю тему (STYLESHEETPATH), а затем родительскую (TEMPLATEPATH):
// ОШИБКА: Игнорирует дочернюю тему
require get_template_directory() . '/parts/card.php';
// ПРАВИЛЬНО (стандартный механизм):
get_template_part('parts/card', null, ['data' => $itemData]);
// ПРАВИЛЬНО (получение пути для прямого контроля):
$file = locate_template(['parts/card.php']);
if (!empty($file)) {
include $file;
}В чем суть ошибки: Разработчик создает файл home.php, ожидая, что он будет отображать главную страницу сайта, когда в настройках чтения (Settings -> Reading) включена статическая главная страница.
Почему разработчики так делают: В бытовом понимании «home» ассоциируется с главной страницей сайта.
Что происходит в продакшене: По спецификации ядра WordPress:
front-page.php ВСЕГДА отвечает за главную страницу (статическую или ленту записей).home.php отвечает за страницу блога (ленту последних записей), когда статическая главная страница включена.
В результате верстка главной страницы отображается на странице новостей, а главная страница выводит стандартный page.php.Как правильно:
front-page.php.home.php.В чем суть ошибки: Разработчик вносит изменения в файл темы templates/single.html через Git/IDE, деплоит на сервер, но изменения не отображаются на сайте.
Почему разработчики так делают: Привычка к классической разработке, где файл на диске всегда определяет вывод.
Что происходит в продакшене: Если администратор сайта хотя бы один раз нажал кнопку «Сохранить» в визуальном Site Editor (Gutenberg FSE), ядро создает запись в таблице wp_posts с типом wp_template. Согласно архитектуре FSE, запись из БД имеет абсолютный приоритет над файлом темы. Изменения в файле на диске будут игнорироваться до тех пор, пока шаблон в БД не будет сброшен (Clear Customizations).
Как правильно: При разработке блочных тем сбрасывать пользовательские кастомизации через Site Editor перед деплоем, либо управлять шаблонами через фильтр pre_get_block_template.
Любой незапланированный вывод символов (пробелы перед <?php, BOM-маркеры UTF-8 или преждевременные echo) немедленно переводит сокет в режим отправки тела ответа HTTP. Это блокирует вызовы header(), setcookie(), wp_redirect() и session_start(), ломая авторизацию и порождая ошибку headers already sent.