single-{$post_type}-{slug}.php для пользовательских типов записей
Моя любимая часть иерархии шаблонов WordPress — это возможность быстро создавать файлы шаблонов для страниц по их slug, без необходимости редактировать страницу в WordPress для выбора шаблона.
Сейчас мы можем делать так:
page-{slug}.php
Но я хотел бы иметь возможность делать так:
single-{post_type}-{slug}.php
Например, для типа записи review
я мог бы создать шаблон для записи "My Great Review" в файле single-review-my-great-review.php
Кто-нибудь уже реализовывал подобное? single-{post_type}-{slug}.php

A) Основа в ядре
Как вы можете увидеть в Кодексе в объяснении Иерархии шаблонов, single-{$post_type}.php
уже поддерживается.
B) Расширение иерархии ядра
К счастью, в /wp-includes/template-loader.php
есть несколько фильтров и хуков.
do_action('template_redirect');
apply_filters( 'template_include', $template )
- И: конкретный фильтр внутри
get_query_template( $type, ... )
с названием:"$type}_template"
B.1) Как это работает
- Внутри файла загрузчика шаблонов шаблон загружается с помощью условного запроса var/wp_query:
is_*()
. - Условное выражение затем запускает (в случае шаблона "single"):
is_single() && $template = get_single_template()
- Это вызывает
get_query_template( $type, $templates )
, где$type
этоsingle
- Затем у нас есть фильтр
"{$type}_template"
C) Решение
Поскольку мы хотим только расширить иерархию одним шаблоном, который загружается перед фактическим шаблоном "single-{$object->post_type}.php"
, мы перехватим иерархию и добавим новый шаблон в начало массива шаблонов.
// Расширяем иерархию
function add_posttype_slug_template( $templates )
{
$object = get_queried_object();
// Новый шаблон
$templates[] = "single-{$object->post_type}-{$object->post_name}.php";
// Как в ядре
$templates[] = "single-{$object->post_type}.php";
$templates[] = "single.php";
return locate_template( $templates );
}
// Добавляем фильтр к соответствующему хуку
function intercept_template_hierarchy()
{
add_filter( 'single_template', 'add_posttype_slug_template', 10, 1 );
}
add_action( 'template_redirect', 'intercept_template_hierarchy', 20 );
ПРИМЕЧАНИЕ: (Если вы хотите использовать что-то отличное от стандартного слага объекта) Вам нужно будет настроить $slug
в соответствии со структурой постоянных ссылок. Просто используйте то, что вам нужно из глобального (object) $post
.
Trac Тикеты
Поскольку вышеуказанный подход в настоящее время не поддерживается (вы можете только фильтровать абсолютный путь таким образом), вот список тикетов в Trac:
- Расширение иерархии с помощью post_type->slug
- Введение фильтра для
get_query_template()
- Фильтрация всей иерархии - наиболее перспективный тикет ⤎ следите за этим тикетом от @scribu как cc

Я хочу протестировать это, но похоже, что в вашей строке add_filter в конце чего-то не хватает.

@supertrue Хорошее замечание. :) Обнаружил еще одну пропущенную )
внутри фильтра. Исправил. Возможно, вы захотите заменить дефис на подчеркивание перед слагом внутри шаблона. Просто чтобы суффикс лучше выделялся при просмотре шаблонов.

Вызывает эту ошибку на всем сайте: Предупреждение: array_unshift() [function.array-unshift]: Первый аргумент должен быть массивом в [строке, содержащей array_unshift]

Хорошо, но тогда что-то другое перехватывает основные шаблоны. Функция работает корректно, и $templates
является массивом. Смотрите основные функции в этом pastebin (без срока действия). Убедитесь, что тестируете это на чистой установке без плагинов и с темой по умолчанию. Затем активируйте их по одному и проверьте, возникает ли ошибка.

Да, я отладил это и получаю окончательный абсолютный путь первого найденного шаблона в виде строки. Мне нужно будет обсудить это с разработчиками ядра, прежде чем изменять ответ. Также: я кое-что перепутал — slug
доступен только для терминов и таксономий. Вам следует заменить $post->post_name
на то, что соответствует вашей структуре постоянных ссылок. В настоящее время нет автоматического способа сделать это для всех случаев, включая получение и замену пути в зависимости от структуры постоянных ссылок и правил перезаписи. Ожидайте обновления.

Я оставил вам рабочую версию — хотя это всё ещё неудовлетворительно (копирование/вставка массива ядра и его повторное добавление внутри пользовательской функции).

это, кажется, не дало никакого эффекта... правильно ли указана последняя часть? Я не вижу, чтобы intercept_template_hierarchy
где-либо вызывался. Замена последнего add_posttype_slug_template
на intercept_template_hierarchy
привела к Фатальной ошибке: оператор [] не поддерживается для строк

@supertrue Да, это проблема. Это просто не работает. Я уже обсуждаю это с разработчиком ядра, чтобы исправить в цикле 3.4. Просто вернитесь через несколько дней и поднимите этот вопрос снова с комментарием здесь. Спасибо. Примечание: ядро ведет себя крайне нелогично в этом месте, так как массив шаблонов не фильтруется, а фильтруется найденный путь к шаблону. Я склоняюсь к тому, что это фактически баг.

@supertrue Обсуждение уже начато. Ожидайте обновления в ближайшие дни. Если не обновление, то возможно тикет в trac :P

@supertrue Доминик Шиллинг (также известный как Ocean90) добавил себя в некоторые тикеты. Ожидайте прогресса с версией 3.4.

@kaiser твоё решение выглядит очень хорошо, но не работает ($templates в моём случае не является массивом… я получаю фатальную ошибку). Есть ли шанс заставить это работать?

Следуя изображению иерархии шаблонов, я не вижу такого варианта.
Вот как бы я решил эту задачу:
Решение 1 (на мой взгляд, лучшее)
Создайте файл шаблона и свяжите его с обзором
<?php
/*
Template Name: Мой отличный обзор
*/
?>
Добавив php-файл шаблона в директорию вашей темы, он появится как вариант шаблона на странице редактирования записи.
Решение 2
Этого можно добиться, используя хук template_redirect
.
В файле functions.php:
function my_redirect()
{
global $post;
if( get_post_type( $post ) == "my_cpt" && is_single() )
{
if( file_exists( get_template_directory() . '/single-my_cpt-' . $post->post_name . '.php' ) )
{
include( get_template_directory() . '/single-my_cpt-' . $post->post_name . '.php' );
exit;
}
}
}
add_action( 'template_redirect', 'my_redirect' );
ИЗМЕНЕНИЕ
Добавлена проверка file_exists

@kaiser Должно быть, это было в каком-то руководстве, которое я тогда читал. Если это не обязательно, я уберу.

@kaiser: exit()
необходим, чтобы предотвратить загрузку шаблона по умолчанию.

Лучший ответ (4-летней давности) больше не работает, но в кодексе WordPress есть решение здесь:
<?php
function add_posttype_slug_template( $single_template )
{
$object = get_queried_object();
$single_postType_postName_template = locate_template("single-{$object->post_type}-{$object->post_name}.php");
if( file_exists( $single_postType_postName_template ) )
{
return $single_postType_postName_template;
} else {
return $single_template;
}
}
add_filter( 'single_template', 'add_posttype_slug_template', 10, 1 );
?>

В моем случае у меня есть пользовательские типы записей Album и Track, связанные таксономией Album. Я хотел иметь возможность использовать разные шаблоны Single для записей Album и Track в зависимости от их таксономии Album.
Основываясь на ответе Kaiser выше, я написал этот код. Он работает хорошо.
Примечание: Мне не понадобился add_action().
// Добавляем дополнительный вариант шаблона в иерархию шаблонов
add_filter( 'single_template', 'add_albumtrack_taxslug_template', 10, 1 );
function add_albumtrack_taxslug_template( $orig_template_path )
{
// на этом этапе $orig_template_path представляет собой абсолютный путь к предпочитаемому шаблону single.
$object = get_queried_object();
if ( ! (
// указываем другой вариант шаблона только для типов записей Album и Track.
in_array( $object->post_type, array( 'gregory-cpt-album','gregory-cpt-track' )) &&
// проверяем, что таксономия Album зарегистрирована.
taxonomy_exists( 'gregory-tax-album' ) &&
// получаем термин таксономии Album для текущей записи.
$album_tax = wp_get_object_terms( $object->ID, 'gregory-tax-album' )
))
return $orig_template_path;
// формируем имя шаблона
// предположение: только один термин таксономии Album на запись. используем первый объект в массиве.
$template = "single-{$object->post_type}-{$album_tax[0]->slug}.php";
$template = locate_template( $template );
return ( !empty( $template ) ? $template : $orig_template_path );
}
Теперь я могу создавать шаблоны с именами single-gregory-cpt-track-tax-serendipity.php и single-gregory-cpt-album-tax-serendipity.php, и WordPress будет использовать их автоматически; 'tax-serendipity' — это ярлык (slug) для первого термина таксономии Album.
Для справки, фильтр 'single_template' объявлен в:
/wp-includes/theme.php: get_query_template()
Спасибо Kaiser за пример кода.
С уважением, Григорий

Привет, Greg - добро пожаловать на WPSE. Пожалуйста, публикуйте ответы только как ответы на вопросы, а не уточняющие вопросы. Если у вас есть вопрос, на который нет ответа среди существующих, и он слишком объемный для комментария, создайте, пожалуйста, новый вопрос :)

это работает у вас? во-первых, '$template' не должен быть закомментирован в вашем коде.. и я думаю, что вместо '$album_tax[0]->slug' должно быть '$object->post_name', разве нет?

Использование шаблонов страниц
Другой подход для масштабируемости — дублирование функционала выбора шаблона страницы для вашего пользовательского типа записи, аналогично тому, как это реализовано для типа записи page
.
Повторно используемый код
Дублирование кода — не лучшая практика. Со временем это может привести к сильному разбуханию кодовой базы, что значительно усложнит управление кодом для разработчика. Вместо создания отдельного шаблона для каждого слага, скорее всего, вам понадобится шаблон типа "один ко многим", который можно повторно использовать, вместо соотношения "один к одному" между записью и шаблоном.
Код
# Определяем строку вашего пользовательского типа записи
define('MY_CUSTOM_POST_TYPE', 'my-cpt');
/**
* Регистрируем метабокс
*/
add_action('add_meta_boxes', 'page_templates_dropdown_metabox');
function page_templates_dropdown_metabox(){
add_meta_box(
MY_CUSTOM_POST_TYPE.'-page-template',
__('Template', 'rainbow'),
'render_page_template_dropdown_metabox',
MY_CUSTOM_POST_TYPE,
'side', # Я предпочитаю размещение под метабоксом действий записи
'low'
);
}
/**
* Отрисовываем метабокс - этот код аналогичен тому, что используется для типа записи page
* @return void
*/
function render_page_template_dropdown_metabox(){
global $post;
$template = get_post_meta($post->ID, '_wp_page_template', true);
echo "
<label class='screen-reader-text' for='page_template'>Шаблон страницы</label>
<select name='_wp_page_template' id='page_template'>
<option value='default'>Шаблон по умолчанию</option>";
page_template_dropdown($template);
echo "</select>";
}
/**
* Сохраняем шаблон страницы
* @return void
*/
function save_page_template($post_id){
# Пропускаем автосохранения
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE )
return;
elseif ( defined( 'DOING_AJAX' ) && DOING_AJAX )
return;
elseif ( defined( 'DOING_CRON' ) && DOING_CRON )
return;
# Обновляем метаданные шаблона только для нашего типа записи
elseif(MY_CUSTOM_POST_TYPE === $_POST['post_type'])
update_post_meta($post_id, '_wp_page_template', esc_attr($_POST['_wp_page_template']));
}
add_action('save_post', 'save_page_template');
/**
* Устанавливаем шаблон страницы
* @param string $template Определенный шаблон из "мозгов" WordPress
* @return string $template Полный путь к предопределенному или кастомному шаблону
*/
function set_page_template($template){
global $post;
if(MY_CUSTOM_POST_TYPE === $post->post_type){
$custom_template = get_post_meta($post->ID, '_wp_page_template', true);
if($custom_template)
# Поскольку наш выпадающий список дает только базовое имя, используем функцию locate_template() для поиска полного пути
return locate_template($custom_template);
}
return $template;
}
add_filter('single_template', 'set_page_template');
Это несколько запоздалый ответ, но я подумал, что он будет полезен, так как, насколько мне известно, никто в интернете не документировал этот подход. Надеюсь, это поможет кому-то.

Обновление для кода Брайана: я обнаружил, что когда выпадающий список не используется, опция "default" (по умолчанию) сохранялась в wp_page_template, что приводило к попытке найти шаблон с именем default. Это изменение просто проверяет наличие опции "default" при сохранении и удаляет метаданные поста вместо сохранения (полезно, если вы изменили опцию шаблона обратно на значение по умолчанию).
elseif(MY_CUSTOM_POST_TYPE === $_POST['post_type']) {
if ( esc_attr($_POST['_wp_page_template']) === "default" ) :
delete_post_meta($post_id, '_wp_page_template');
else :
update_post_meta($post_id, '_wp_page_template', esc_attr($_POST['_wp_page_template']));
endif;
}
