Хранение повторителей ACF в произвольных таблицах

Во время работы над сайтом Delicious Brains у нас возникла необходимость хранения некоторых метаданных о произвольном типе записей (CPT).

Из-за природы данных не имело смысла пытаться впихнуть невпихуемое и сохранять их в метатаблицу wp_postmeta в виде сериализованного массива. Вместо этого создание произвольной таблицы для хранения было лучшим подходом.

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

Чтобы реализовать эту функциональность, нам нужно сохранить все версии плагина с датой их выпуска и именем zip-файла. Структура произвольной таблицы будет выглядеть примерно так:

CREATE TABLE `wp_woocommerce_software_product_versions` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `product_id` bigint(20) NOT NULL,
  `version` varchar(200) COLLATE utf8mb4_unicode_520_ci DEFAULT NULL,
  `filename` varchar(200) COLLATE utf8mb4_unicode_520_ci DEFAULT NULL,
  `date_released` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
  `created_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
  `updated_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
  PRIMARY KEY (`id`),
  KEY `product_version` (`product_id`,`version`(191))
) ENGINE=InnoDB AUTO_INCREMENT=33 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;

После первоначального заполнения таблицы историческими версиями и датами выпуска мне нужно дать администраторам сайта возможность добавлять новую запись о версии во время выпуска и установить дату и имя файла zip-архива.

Им может потребоваться отредактировать неверную дату или даже удалить какую-либо версию (в редких случаях).

Это администрирование должно происходить на странице редактирования товара в том же месте, что и существующий произвольный метабокс товара.

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

Звучит как какая-то дичь.

Должен быть способ быстрее и проще? Я уже большой поклонник ACF, и совсем недавно мы начали использовать этот плагин у нас на сайте.

Поле Repeater, что есть только в Pro версии, позволяет настраивать подполя, которые отображаются в табличной форме, с возможностью добавления/редактирования/удаления строк.

Это идеально подходит для того, что мне нужно, и я подумал, что было бы интересно посмотреть, как легко я смогу нагнуть ACF, чтобы выполнить то, что мне нужно.

Кто-то, должно быть, делал это раньше? Но после быстрого поиска в Google все, что я нашел, — это сообщение на форуме ACF, в котором спрашивается о хранении данных в произвольной таблице вместо метаданных.

Эллиот Кондон ответил кодом с несколькими фильтрами, чтобы показать, как это работает.

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

Настройка поля

Нашему повторителю потребуется поле text для хранения номера версии и имени файла, а также поле datetime для выбора даты выпуска. Нам не нужно иметь поле для плагина (внешний ключ product_id), мы будем знать post ID, как на странице edit-post.php.

Правило местоположения для группы полей настроено на отображение только в типе записей Product. Экран редактирования товара теперь выглядит следующим образом:

Загрузка данных из таблицы

В настоящий момент повторитель загружает данные из таблицы wp_postmeta записи, в которой нет данных для поля. Мы хотим научить ACF загружать данные этого поля из произвольной таблицы.

К счастью, ACF настолько удобен для разработчиков, что события и фильтры есть почти в каждой области кодовой базы. Нам нужно использовать фильтр acf/load_value, чтобы перехватить данные, возвращаемые из метатаблицы записи, и заменить их строками, полученными из нашей произвольной таблицы:

add_filter( 'acf/load_value', 'my_load_product_versions', 11, 3 );

/**
 * Get versions from the table and return to the repeater field.
 *
 * @param bool  $value
 * @param int   $post_id
 * @param array $field
 *
 * @return array|bool
*/
function my_load_product_versions( $value, $post_id, $field ) {
	if ( 'product_versions' !== $field['name'] ) {
		return $value;
	}

	$data = my_get_product_versions( $post_id, $field );

	if ( empty( $data ) ) {
		return false;
	}

	return $data;
}

Итак, здесь я проверяю, является ли поле повторителем product_versions, а затем использую вспомогательный метод для извлечения данных из таблицы и преобразования их в формат, ожидаемый полем повторителя:

/**
 * Get version records in an ACF repeater friendly format.
 *
 * @param int   $post_id
 * @param array $field
 *
 * @return array
*/
function my_get_product_versions( $post_id, $field ) {
	$versions = WoocommerceSoftwareProductVersions::where( 'product_id', $post_id )
		                                              ->orderBy( 'date_released', 'desc' )
		                                              ->get();
	$data = array();

	foreach ( $versions as $version ) {
		$data[] = array(
			$field['sub_fields'][0]['key'] => $version->version,
			$field['sub_fields'][1]['key'] => $version->filename,
			$field['sub_fields'][2]['key'] => date( 'Ymd', strtotime( $version->date_released ) ),
		);
	}

	return $data;
}

Я использую пакет wp-eloquent, чтобы упростить запросы к моей произвольной таблице.

Теперь наш интерфейс выглядит так:

Это только половина истории, давайте убедимся, что все изменения сохраняются в таблице.

Сохранение данных в таблицу

Фильтр, который нам понадобится для этого, называется acf/update_value.

add_filter( 'acf/update_value', 'my_update_product_versions', 11, 4 );

/**
 * Update the versions table with the field data on update.
 *
 * @param $value
 * @param $post_id
 * @param $field
 * @param $_value
 *
 * @return null
 */
function my_update_product_versions( $value, $post_id, $field, $_value ) {
	if ( 0 === strpos( $field['name'], 'product_versions_' ) ) {
		return null;
	}

	if ( 'product_versions' !== $field['name'] ) {
		return $value;
	}

	if ( empty( $_value ) ) {
		WoocommerceSoftwareProductVersions::where( 'product_id', $post_id )->delete();

		return null;
	}

	$data = $this->get_product_versions( $post_id, $field );
	if ( serialize( $_value ) === serialize( $data ) ) {
		// No changes
		return null;
	}

	WoocommerceSoftwareProductVersions::where( 'product_id', $post_id )->delete();

	$versions = array();
	foreach ( $_value as $row ) {
		$data = array(
			'product_id'    => $post_id,
			'version'       => $row[ $field['sub_fields'][0]['key'] ],
			'filename'      => $row[ $field['sub_fields'][1]['key'] ],
			'date_released' => date( 'Y-m-d 00:00:00', strtotime( $row[ $field['sub_fields'][2]['key'] ] ) ),
		);

		$versions[] = $data;
		WoocommerceSoftwareProductVersions::create( $data );
	}

	return null;
}

Здесь я снова проверяю, является ли поле моим повторителем. На этот раз мне также нужно убедиться, что вложенные поля репитера возвращают значение null, чтобы они не сохранялись в таблицу метаданных.

Если сохраняемое значение пусто, то есть все строки были удалены или ни одна из них еще не добавлена, то мне нужно удалить все записи из моей таблицы для этого идентификатора товара.

Если в повторителе есть записи, я сравниваю их с существующими записями в таблице и возвращаю null, если они идентичны. Это нужно для обновлений товара, но без изменений для повторителя.

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

Минусы

Это, безусловно, быстрый способ отображения и редактирования даты в произвольной таблице, но подходит далеко не для всех кейсов.

Также необходим плагин ACF, которые очень часто является ненужной зависимостью в проекте.

Если вы ищете способ хранить данные ACF в произвольных таблицах вместо таблицы метаданных и при этом вы не разработчик, то я рекомендую вам попробовать плагин ACF Custom Database Tables.

Заключение

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

Если вы когда-нибудь кастомизировали ACF подобным образом, дайте мне знать в комментариях ниже.

Источник: Managing WordPress Custom Tables with an Advanced Custom Fields Repeater Field.

Источник: https://www.kobzarev.com/programming/acf-repeater-custom-table/

Михаил Кобзарёв

Суровый русский тимлид. Жил в Магадане, в офисе московских веб студий и в Тульской деревне. Виртуозно знает WordPress, PHP, ООП, Vue.js и вот это вот все. Делает крутые высоконагруженные сайты, поэтому уже почти захватил весь рынок WordPress разработки в России. Не дает никому делать сайты без спроса. Ведет блог о разработке, дайджест в телеграмме и в ВК. Любит сиськи, баню и радиоэлектронику. 100% патриот (но это не точно). Тролль 542 уровня. Ездит в отпуск раз в 5 лет.

Добавить комментарий

%d такие блоггеры, как: