Добавление поста череp ajax на WordPress

Добавление поста произвольного типа с фронтэнда на ajax’е в WP

ajax-spray-uniwersalny-750-mlВсем йоханга!
Сегодня будем решать не простую задачу добавления поста с лицевой части сайта на WordPress, да еще и с помощью ajax, и не просто поста, а поста произвольного типа. В одной из прошлых статей мы разобралиськак быстро создать таксономии, посты произвольного типа, запилить к ним шаблоны и форму добавления, но этот случай добавления очень простой, работает на API плагина PODS и не освещает кучу подводных камней и сейчас мы это исправим и заодно разберемся как правильно юзать аякс в WordPress.

Итак, задача:

  • Сделать возможность добавления поста произвольного типа с фронт-энда, минуя админку.
  • Все должно работать без перезагрузки страницы, т.е. на аяксе.
  • Посты, помимо стандартных полей, имеют дополнительные поля, а также поле множественной загрузки картинок.
  • При добавлении, посты должны привязываться к терминам(элементам) таксономии, тоже произвольной.
  • Использовать можно только нативные механизмы WP.

У нас есть произвольный тип постов с полями:

  • заголовок(стандартное поле — post_title),
  • текст(стандартное — post_content),
  • миниатюра(стандартное поле типа файл, потом будем получать id загруженного файла — _thumbnail_id),
  • обычная строка(произвольное поле типа строка — string_field),
  • форматированный текст(произвольное, текстарея — text_field),
  • поле множественной загрузки файлов(произвольное типа файл, дополнительные поля добавляются с помощью jQuery — multifile_field).

Так же посты привязаны к двум произвольным таксономиям:

  • простая таксономия без вложенности, как тэги — custom_tax_like_tag,
  • таксономия с иерархией/вложенностью как у категорий — custom_tax_like_cat.

Причем если пост принадлежит к дочернему термину — он должен принадлежать и к родителю.
Жирным выделено название полей и слаги таксономий.

Решение: сделаем обычную форму со всеми полями, обработаем её отправку с помощью jQuery и плагинаjQuery-forms, примем полученные данные по всем правилам работы ajax в WordPress, все проверим, добавим пост с полями и медиафайлами, привяжем к таксономиям и вернем ответ.

Начнем.

0. Подготовим WP для использования аякса:
В WP существует свой механизм работы с аяксом и раз мы используем только стандартное API, то будем делать все по правилам.
Суем это в functions.php нашей темы:

add_action('wp_print_scripts','include_scripts'); // действие в котором прикрепим необходимые js скрипты и передадим данные 
function include_scripts(){
        wp_enqueue_script('jquery'); // добавим основную библиотеку jQuery
        wp_enqueue_script('jquery-form'); // добавим плагин jQuery forms, встроен в WP
        wp_enqueue_script('jquery-chained', '//www.appelsiini.net/projects/chained/jquery.chained.min.js'); // добавим плагин для связанных селект листов

        wp_localize_script( 'jquery', 'ajaxdata', // функция для передачи глобальных js переменных на страницу, первый аргумент означет перед каким скриптом вставить переменные, второй это название глобального js объекта в котором эти переменные будут храниться, последний аргумент это массив с самими переменными
			array( 
   				'url' => admin_url('admin-ajax.php'), // передадим путь до нативного обработчика аякс запросов в wp, в js можно будет обратиться к ней так: ajaxdata.url
   				'nonce' => wp_create_nonce('add_object') // передадим уникальную строку для механизма проверки аякс запроса, ajaxdata.nonce
			)
		);
}

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

1. Запиливаем форму добавления:
Так как у нас есть вложенная таксономия, для удобства воспользуемся плагином jQuery chained, это для того, чтобы после выбора родительского термина, в следующий селектлист нам добавились только дочерние термины к выбранному. Еще сделаем ссылку, которая будет добавлять нам поля типа файл, для мультизагрузки картинок.
Пусть поля тайтл, постконтент и выборы терминов таксономий будут обязательны.

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

Код формы:

<?php
// подготовим актуальные данные таксономий
$cats = get_terms('custom_tax_like_cat', 'orderby=name&hide_empty=0&parent=0'); // получим все термины(элементы) таксономии с иерархией
foreach ($cats as $cat) { // пробежим по каждому полученному термину
    $parents.="<option value='$cat->term_id' />$cat->name</option>"; // суем id и название термина в строку для вывода внутри тэга select
    $childs_array = get_terms('custom_tax_like_cat', 'orderby=name&hide_empty=0&parent='.$cat->term_id); // возьмем все дочерние термины к текущему
	foreach ($childs_array as $child){
		$childs.="<option value='$child->term_id' class='$cat->term_id' />$child->name</option>"; // делаем то же самое, класс должен быть равным id родительского термина чтобы плагин chained работал
	}
}
$tags_array = get_terms('custom_tax_like_tag', 'orderby=none&hide_empty=0&parent=0'); // получим все термины таксономии без вложенности
foreach ($tags_array as $tag) { // пробежим по каждому
  $tags .= '<label><input type="radio" name="tag" value="'.$tag->term_id.'">'.$tag->name.'</label>'; // суем все в radio баттоны
}
?>
<?php // Выводим форму ?>
<form method="post" enctype="multipart/form-data" id="add_object">
	<label>Кастом категории-родители:
		<select id="parent_cats" name="parent_cats" required>
			<option value="">Не выбрано</option>
			<?php echo $parents; // выводим все родительские термины ?>
		</select>
	</label>

	<label>Кастом категории-дети:
		<select id="child_cats" name="child_cats" required>
			<option value="">Не выбрано</option>
			<?php echo $childs; // выводим все дочерние термины, плагин chained сам покажет только нужные элементы в зависимости от выбранного родительского термина ?>
		</select>
	</label>

	Кастом тэги
  	<?php echo $tags; // выводим термины таксономии без иерархии в radio ?>

	<label>Тайтл(стандартное) <input type="text" name="post_title" required/></label>
	<label>Пост контент(стандартное) <textarea name="post_content" required/></textarea></label>
	<label>Поле типа строка(произвольное) <input type="text" name="string_field"/></label>
	<label>Пост типа текст(произвольное) <textarea name="text_field"/></textarea></label>
	<label>Миниатюра(стандартное): <input type="file" name="img"/></label>
	<label id="first_img" class='imgs'>Дополнительные фото(произвольное): <input type='file' name='imgs[]'/></label>
	<a href="#" id="add_img">Загрузить еще фото</a>
	<input type="submit" name="button" value="Отправить" id="sub"/>
	<div id="output"></div> <?php // сюда будем выводить ответ ?>

2. Перехват отправки формы и подготовка данных:
Суньте это куда удобнее: прямо на страницу или в специальный js файл для кастом скриптов =)

function ajax_go(data, jqForm, options) { //ф-я перед отправкой запроса
  	jQuery('#output').html('Отправляем...'); // в див для ответа напишем "отправляем.."
  	jQuery('#sub').attr("disabled", "disabled"); // кнопку выключим
}
function response_go(out)  { // ф-я обработки ответа от wp, в out будет элемент success(bool), который зависит от ф-и вывода которую мы использовали в обработке(wp_send_json_error() или wp_send_json_success()), и элемент data в котором будет все что мы передали аргументом к ф-и wp_send_json_success() или wp_send_json_error()
	console.log(out); // для дебага
	jQuery('#sub').prop("disabled", false); // кнопку включим
	jQuery('#output').html(out.data); // выведем результат
}
jQuery(document).ready(function(){ // после загрузки страницы
	jQuery("#child_cats").chained("#parent_cats");  // подключаем плагин для связи селект листов с терминами вложенной таксономии
  	add_form = jQuery('#add_object'); // запишем форму в переменную
  	var options = { // опции для отправки формы с помощью jquery form
  		data: { // дополнительные параметры для отправки вместе с данными формы
  			action : 'add_object_ajax', // этот параметр будет указывать wp какой экшн запустить, у нас это wp_ajax_nopriv_add_object_ajax
        	nonce: ajaxdata.nonce // строка для проверки, что форма отправлена откуда надо
    	},
      	dataType:  'json', // ответ ждем в json формате
      	beforeSubmit: ajax_go, // перед отправкой вызовем функцию ajax_go()
      	success: response_go, // после получении ответа вызовем response_go()
      	error: function(request, status, error) { // в случае ошибки
        	console.log(arguments); // напишем все в консоль
      	},
      	url: ajaxdata.url // куда слать форму, переменную с url мы определили вывели в нулевом шаге     
  }; 
  add_form.ajaxForm(options); // подрубаем плагин jquery form с опциями на нашу форму 

  jQuery('#add_img').click(function(e){ // по клику на ссылку "Добавить еще фото"
    e.preventDefault(); // выключим стандартное поведение ссылки
    jQuery(this).before("<label class='imgs'>Дополнительные фото(произвольное) <input type='file' name='imgs[]'/></label>"); // добавим перед ссылкой еще один инпут типа файл с таким же нэймом
  });  
});

3. Обработка данных и добавление поста:
Это тоже в functions.php

add_action( 'wp_ajax_nopriv_add_object_ajax', 'add_object' ); // крепим на событие wp_ajax_nopriv_add_object_ajax, где add_object_ajax это параметр action, который мы добавили в перехвате отправки формы, add_object - ф-я которую надо запустить
add_action('wp_ajax_add_object_ajax', 'add_object'); // если нужно чтобы вся бадяга работала для админов
function add_object() {
	$errors = ''; // сначала ошибок нет

	$nonce = $_POST['nonce']; // берем переданную формой строку проверки
	if (!wp_verify_nonce($nonce, 'add_object')) { // проверяем nonce код, второй параметр это аргумент из wp_create_nonce
		$errors .= 'Данные отправлены с левой страницы '; // пишим ошибку
	}

	// запишем все поля
	$parent_cat = (int)$_POST['parent_cats']; // переданный id термина таксономии с вложенностью (родитель)
	$child_cat = (int)$_POST['child_cats']; // id термина таксономии с вложенностью (его дочка)
	$tag = (int)$_POST['tag']; // id обычной таксономии
	$title = strip_tags($_POST['post_title']); // запишем название поста
	$content = wp_kses_post($_POST['post_content']); // контент
	$string_field = strip_tags($_POST['string_field']); // произвольное поле типа строка
	$text_field = wp_kses_post($_POST['text_field']); // произвольное поле типа текстарея

	// проверим заполненность, если пусто добавим в $errors строку
	if (!$parent_cat) $errors .= 'Не выбрано "Кастом категория-родитель"';
    if (!$child_cat) $errors .= 'Не выбрано "Кастом категория-ребенок xD"';				    
    if (!$tag) $errors .= 'Не выбрано "Кастом тэг"';
    if (!$title) $errors .= 'Не заполнено поле "Тайтл"';
    if (!$content) $errors .= 'Не заполнено поле "Пост контент"';

    // далее проверим все ли нормально с картинками которые нам отправили
    if ($_FILES['img']) { // если была передана миниатюра
   		if ($_FILES['img']['error']) $errors .= "Ошибка загрузки: " . $_FILES['img']['error'].". (".$_FILES['img']['name'].") "; // серверная ошибка загрузки
    	$type = $_FILES['img']['type']; 
		if (($type != "image/jpg") && ($type != "image/jpeg") && ($type != "image/png")) $errors .= "Формат файла может быть только jpg или png. (".$_FILES['img']['name'].")"; // неверный формат
	}

	if ($_FILES['imgs']) { // если были переданны дополнительные картинки, пробежимся по ним в цикле и проверим тоже самое
		foreach ($_FILES['imgs']['name'] as $key => $array) {
			if ($_FILES['imgs']['error'][$key]) $errors .= "Ошибка загрузки: " . $_FILES['imgs']['error'][$key].". (".$key.$_FILES['imgs']['name'][$key].") ";
    		$type = $_FILES['imgs']['type'][$key]; 
			if (($type != "image/jpg") && ($type != "image/jpeg") && ($type != "image/png")) $errors .= "Формат файла может быть только jpg или png. (".$_FILES['imgs']['name'][$key].")"; 
		}
	}  

	if (!$errors) { // если с полями все ок, значит можем добавлять пост
		$fields = array( // подготовим массив с полями поста, ключ это название поля, значение - его значение
			'post_type' => 'my_custom_post_type', // нужно указать какой тип постов добавляем, у нас это my_custom_post_type
	    	'post_title'   => $title, // заголовок поста
	        'post_content' => $content, // контент
	    );
	    $post_id = wp_insert_post($fields); // добавляем пост в базу и получаем его id

	    update_post_meta($post_id, 'string_field', $string_field); // заполняем произвольное поле типа строка
	    update_post_meta($post_id, 'text_field', $text_field); // заполняем произвольное поле типа текстарея

	    wp_set_object_terms($post_id, $parent_cat, 'custom_tax_like_cat', true); // привязываем к пост к таксономиям, третий параметр это слаг таксономии
	    wp_set_object_terms($post_id, $child_cat, 'custom_tax_like_cat', true);
	    wp_set_object_terms($post_id, $tag, 'custom_tax_like_tag', true);

	    if ($_FILES['img']) { // если основное фото было загружено
   			$attach_id_img = media_handle_upload( 'img', $post_id ); // добавляем картинку в медиабиблиотеку и получаем её id
   			update_post_meta($post_id,'_thumbnail_id',$attach_id_img); // привязываем миниатюру к посту
		}

		if ($_FILES['imgs']) { // если дополнительные фото были загружены
			$imgs = array(); // из-за того, что дефолтный массив с загруженными файлами в пхп выглядит не так как нужно, а именно вся инфа о файлах лежит в разных массивах но с одинаковыми ключами, нам нужно создать свой массив с блэкджеком, где у каждого файла будет свой массив со всеми данными
			foreach ($_FILES['imgs']['name'] as $key => $array) { // пробежим по массиву с именами загруженных файлов
				$file = array( // пишем новый массив
					'name' => $_FILES['imgs']['name'][$key],
					'type' => $_FILES['imgs']['type'][$key], 
					'tmp_name' => $_FILES['imgs']['tmp_name'][$key], 
					'error' => $_FILES['imgs']['error'][$key],
					'size' => $_FILES['imgs']['size'][$key]
				);
				$_FILES['imgs'.$key] = $file; // записываем новый массив с данными в глобальный массив с файлами
				$imgs[] = media_handle_upload( 'imgs'.$key, $post_id ); // добавляем текущий файл в медиабиблиотека, а id картинки суем в другой массив
			}
			update_post_meta($post_id,'multifile_field',$imgs); // привязываем все картинки к посту
		}  
	}

	if ($errors) wp_send_json_error($errors); // если были ошибки, выводим ответ в формате json с success = false и умираем
	else wp_send_json_success('Все прошло отлично! Добавлено ID:'.$post_id); // если все ок, выводим ответ в формате json с success = true и умираем
	
	die(); // умрем еще раз на всяк случ
}

После того как форму отправят, по переданному параметру action вордпресс поймет какой экшн запустить, а именно wp_ajax_add_object_ajax и выполнит нашу функцию add_object(), а дальше все просто.

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

4. Примечания:

  • Я специально усложнил все насколько смог, чтобы были примеры на все случаи жизни. Очевидно, что для добавления простого поста все проще, но по аналогии.
  • Все таксономии и произвольный тип постов были созданы с помощью плагина Pods Framework за 5 минут. Как работать с этим плагином и какие кнопки нажимать читайте здесь. Если обязательные поля нужны, то для добавления используйте api плагина pods.
  • Очень полезно будет прочитать про ajax в wordpress у Камы.

Ну вот и все. Мы запилили супер добавление кастом постов с произвольными полями, картинками и привязкой к таксономиям через ajax. оО

Шарьте статью, плюсуйте, спрашивайте, всем мир, развлекайтесь)

Связанные списки на jQuery

http://www.appelsiini.net/projects/chained

See standalone demo page for quick working example.

Class Based Usage
Child selects are chained to parent select. Child select options must have class names which match option values of parent select. When user selects something in parent select the options in child select are updated. Options which have matching classname with parents currently selected option will stay visible. Others are hidden.

NOTE! Class names are case sensitive. This means Audi and audi will not match.
First you must include jQuery and Chained in your code:



Then lets assume you have the following HTML code:



You can now chain the #series to #mark. There are two different ways to do it. Choose yourself if you prefer more english like or shorter version. I prefer the shorter version.

$(«#series»).chained(«#mark»); /* or $(«#series»).chainedTo(«#mark»); */