Регистрация таксономии без привязки к типу записи

Как выяснилось, WordPress не позволяет быстро и просто создать таксономию так, чтобы не привязать её к какому-либо типу записи. Точнее зарегистрировать таксономию без привязки можно, только вот при переходе на страницу создания элементов этой таксономии мы неизбежно будем находится в пункте меню «Записи». А нам нужно создать свой отдельный пункт меню для этой таксономии. Объясню по порядку...

Задача

Нужно хранить данные (строки), с возможностью добавлять к ним еще какие-то данные (заранее неизвестно, код будет расширяться). Дальше эти данные (строки) будут использоваться для пользователей WordPress (у пользователей будет настройка скилы, например, пользователь умеет, готовить, стирать, убирать).

Чтобы не писать кучу кода, для возможность создавать, изменять и удалять эти скилы, количество которых ожидается в размере 2000 шутк, было принято стратегическое решение использовать для этих целей таксономию WordPress.

Плюсы: регнуть таксономию очень просто и сразу же мы получаем таблицу с пагинацией и поиском, возможность добавлять, изменять, удалять данные, а также возможность расширять данные за счет метаполей. Более того, получаем целый пакет функций WP для вывода элементов этой таксономии. А если, например, сохранять эти данные в отдельную таблицу или в опции, то для управления всем этим нужно было бы писать отдельный код, для всего: начиная с создания страницы в админке и заканчивая функциями вывода элементов. А тут готово все и сразу.

Минусы: лишние неиспользуемые поля в таблице таксономий, а мы на спичках не экономим, поэтому минусов нет!

Итого, задача: создать таксономию не привязанную к типу записи и имеющую свой отдельный пункт меню в админ панели.

Решение

Регистрируем таксономию. Она нам нужна только для хранения данных, поэтому она будет не публичной (не видна на фронте) и без всяких привычных параметров таксономии:

// создадим таксономию skills
add_action( 'init', function (){
	register_taxonomy( 'skills', null, array(
		//'label'                 => 'Скилы', // определяется параметром $labels->name
		'labels'                => array(
			'name'          => 'Скилы',
			'singular_name' => 'Скил',
			'add_new_item'  => 'Добавить новый Скил',
		),
		'public'                => false,
		'show_ui'               => true, // равен аргументу public
		'show_in_rest'          => false, // добавить в REST API
		'hierarchical'          => false,
		'update_count_callback' => '__return_null',
	) );
}, 20 );

Получаем:

Как видно, все работает, только у таксы нет своего пункта меню и при входе на страницу таксы, мы находимся в разделе «Записи».

Создаем пункт меню под нашу таксу:

## добавим пункт меню таксономии в админ меню
add_action( 'admin_menu', 'add_skills_menu_item' );
function add_skills_menu_item(){
	add_menu_page( 'Скилы', 'Скилы', 'manage_options', "edit-tags.php?taxonomy=skills", null, 'dashicons-awards', 9 );
}

Получаем:

Теперь задача отключить подраздел Записи и сделать активным пункт меню нашей таксы. Тут у ВП все плохо - подходящих хуков нет, поэтому будем хакать.

Это код ради которого писалась текущая заметка:

/**
 * Добавим пункт меню таксономии в админ меню.
 */
add_action( 'admin_menu', 'add_skills_menu_item' );
function add_skills_menu_item() {
	global $menu;

	$tax_name = 'skills';
	$menu_title = 'Скилы';
	$capability = 'manage_options';

	$is_skills = ( ( $_GET['taxonomy'] ?? '' ) === $tax_name );

	// отменим 'current' для записей (по умолчанию такса туда привязывается, даже если при регистрации таксы не указать тип записи)
	if( $is_skills ){
		add_filter( 'parent_file', '__return_false' );
	}

	// добавим пункт меню
	add_menu_page( $menu_title, $menu_title, $capability, "edit-tags.php?taxonomy=$tax_name", null, 'dashicons-awards', 9 );

	// поправим некоторые параметры добавленого пункта меню
	$menu_item_key = key( wp_list_filter( $menu, [ $menu_title ] ) );
	$menu_item = & $menu[ $menu_item_key ];
	foreach( $menu_item as & $val ){
		// добавим класс 'current' где нужно
		if( false !== strpos( $val, 'menu-top' ) ){
			$val = 'menu-top' . ( $is_skills ? ' current' : '' );
		}

		$val = preg_replace( '~toplevel_page[^ ]+~', "toplevel_page_$tax_name", $val );
	}
}

Получаем:

Вот собственно и все!

Готовый код целиком

В моей задаче нужно было еще скрыть ненужные поля и добавить поле для массового добавления скилов.

Весь предыдущий код целиком, включая код для доп. задач:

<?php

Kama_Register_Single_Taxonomy::init();

/**
 * Регистрирует таксономию без привязки к типу записи со своим отдельным пунктом меню в админ меню.
 */
class Kama_Register_Single_Taxonomy {

	public static $tax_name = 'skills';
	public static $menu_title = 'Скилы';
	private static $capability = 'manage_options';
	private static $request_key = 'bulk_add_skills';
	private static $menu_labels = [
		'name'          => 'Скилы',
		'singular_name' => 'Скил',
		'add_new_item'  => 'Добавить новый Скил',
	];

	public static function init(){

		add_action( 'init', static function(){
			self::register_taxonomy();
			self::bulk_add_terms_handler();
		} );

		## Добавим пункт меню таксономии в админ меню.
		add_action( 'admin_menu', [ __CLASS__, 'add_tax_menu_item' ] );

		## форма массового добавления скилов
		add_action( self::$tax_name . '_add_form', [ __CLASS__, 'bulk_add_terms_form' ] );

		## свои стили на странице таксономии и на странице редактирования элемента.
		add_action( 'admin_head', [ __CLASS__, 'styles_on_edit_term_page' ] );

		## Удалим ненужные колонки
		$edit_page_key = 'edit-' . self::$tax_name;
		add_filter( "manage_{$edit_page_key}_columns", [ __CLASS__, 'remove_unused_columns' ] );
	}

	private static function register_taxonomy(): void {
		register_taxonomy(
			self::$tax_name,
			null,
			[
				'labels'       => self::$menu_labels,
				'public'       => false,
				'show_ui'      => true, // равен аргументу public
				'show_in_rest' => false, // добавить в REST API
				'hierarchical' => false,
				'update_count_callback' => '__return_null',
			]
		);
	}

	public static function add_tax_menu_item(): void {
		global $menu;

		$tax_name = self::$tax_name;

		$is_the_tax = ( ( $_GET['taxonomy'] ?? '' ) === self::$tax_name );

		// отменим 'current' для записей (по умолчанию такса туда привязывается, даже если при регистрации таксы не указать тип записи)
		if( $is_the_tax ){
			add_filter( 'parent_file', '__return_false' );
		}

		// добавим пункт меню
		add_menu_page(
			self::$menu_title,
			self::$menu_title,
			self::$capability,
			'edit-tags.php?taxonomy=' . self::$tax_name,
			null,
			'dashicons-awards',
			9
		);

		// поправим некоторые параметры добавленого пункта меню
		$menu_item_key = key( wp_list_filter( $menu, [ self::$menu_title ] ) );
		$menu_item = & $menu[ $menu_item_key ];
		foreach( $menu_item as & $val ){
			// добавим класс 'current' где нужно
			if( false !== strpos( $val, 'menu-top' ) ){
				$val = 'menu-top' . ( $is_the_tax ? ' current' : '' );
			}

			$val = preg_replace( '~toplevel_page[^ ]+~', "toplevel_page_$tax_name", $val );
		}
	}

	/**
	 * Обрабатывает запрос на массовое добавление скилов.
	 */
	public static function bulk_add_terms_handler(): void {
		$new_terms = trim( $_POST[ self::$request_key ] ?? '' );
		if(  ! $new_terms || ! current_user_can( self::$capability ) ){
			return;
		}

		$new_terms = wp_unslash( $new_terms );
		$new_terms = array_filter( array_map( 'trim', explode( "\n", $new_terms ) ) );

		$err_names = [];
		foreach( $new_terms as $new_term_name ){
			$data = wp_insert_term( $new_term_name, self::$tax_name );
			if( is_wp_error( $data ) ){
				$err_names[ $new_term_name ] = $data->get_error_message();
			}
		}

		// сообщение о результате рбработки запроса
		add_action( 'admin_notices', function() use ( $err_names, $new_terms ) {
			$added_count = count( $new_terms ) - count( $err_names );
			$message = "<p>Добавлено терминов: $added_count</p>";

			if( $err_names ){
				$message .= '<p style="color:red;">';
				$message .= 'Не удалось добавить: <br>';
				foreach( $err_names as $skill_name => $err_msg ){
					$message .= '<b>' . esc_html( $skill_name ) . "</b>: $err_msg <br>";
				}
				$message .= "</p>";
			}

			echo '<div class="notice notice-success is-dismissible"><div>' . $message . '</div></div>';
		} );
	}

	public static function bulk_add_terms_form(): void {
		if( ! current_user_can( self::$capability ) ){
			return;
		}

		// код выводиться внутри существующей формы, поэтому закроем её и откроем свою
		?>
		</form>

		<form method="POST" action="" class="bulk-add-terms-form">
			<div class="form-field">
				<h2>Массовое добавление скилов</h2>
				<p>Список скилов каждый на новой строке.</p>
				<textarea name="<?= self::$request_key ?>" rows="5" style="width:95%"></textarea>
			</div>
		<?php
		submit_button( 'Добавить скилы массово' );
	}

	public static function styles_on_edit_term_page() {
		$edit_page_key = 'edit-' . self::$tax_name;
		if( get_current_screen()->id !== $edit_page_key ){
			return;
		}
		// спрячем ненужные поля
		?>
		<style>
			.form-field.term-slug-wrap{ display:none; }
			.form-field.term-description-wrap{ display:none; }
		</style>
		<?php
	}

	public static function remove_unused_columns( $columns ) {
		unset( $columns['description'], $columns['posts'] );

		return $columns;
	}

}

Получаем:

8 комментариев