Некоторое время назад мне понадобилось сделать интернет-магазин на WordPress. В официальном репозитории есть немало хороших решений. Среди них уже давно выделился лидер — Woocommerce. Думаю он не нуждается в представлении. Многомилионная армия пользователей, сотни платных и бесплатных расширений и невероятная гибкость. Вот почему Woocommerce имеет более 5 миллионов активных установок и охватывает крупную долю интернет-магазинов во всём мире.

Всё же я решил изобрести свой велосипед. Отчасти, чтобы прокачать навыки, отчасти, чтобы попробовать сделать не требовательный к ресурсам и достаточно быстрый ecommerce плагин. Недавно я разместил его в официальный репозиторий, поэтому, приглашаю всех желающих его потестировать. В этой статье я не буду делать обзор возможностей, а расскажу лишь о некоторых интересных технических решениях.



Проблема постоянных ссылок


Вообще, пермалинки в WordPress — один из самых сложных участков, так как требует решения ряда проблем связей и зависимостей. В WP Store встроена возможность управления постоянными ссылками. Например, вы можете удалить слаг product из урл товара, изменить на свой или добавить в него слаг категории или даже вложенность категорий. Ссылки на товары могут выглядеть так: вашсайт.рф/родительская-категория/дочерняя-категория-второго-уровня/дочерняя-категория-третьего-уровня/имя-товара/. Неплохо.

Особо попотеть пришлось, чтобы добиться вложенности категорий. Используя функцию wp_get_object_terms, получаем категории указанного товара, а затем в цикле собираем слаги и формируем урл по иерархии от родительской к дочерней. Чтобы выводить нужные ссылки используем фильтр post_link. Вот лишь часть кода (полный код вы сможете посмотреть в исходниках):

add_filter( 'post_link', 'wpsl_post_type_permalink', 20, 3 );
add_filter( 'post_type_link', 'wpsl_post_type_permalink', 20, 3 );
function wpsl_post_type_permalink( $permalink, $post_id, $leavename ) {

	....

	/**
	 * Works only in the admin panel when changing the structure of permanent links or creating/updating the product
	 * In the frontend to display links to products using $post->guid
	 * Relevant if the structure of permalinks are used %category% or %categories%
	 */
	if ( is_admin() ) {
		// get all terms (product categories) of this post (product) by hierarchicaly
		// change %category%
		if ( strpos( get_option( 'product_permalink' ), '%category%' ) !== false && $terms = wpsl_get_terms_hierarchicaly( $post->ID, 'product_cat' ) ) {
			$custom_slug = str_replace( '%category%', isset( $terms[0] ) && is_object( $terms[0] ) ? $terms[0]->slug : '', $custom_slug );
		}
		
		// change %categories%
		if ( strpos( get_option( 'product_permalink' ), '%categories%' ) !== false && $terms = wpsl_get_terms_hierarchicaly( $post->ID, 'product_cat' ) ) {
			foreach( $terms as $term ) {
				$hierarchical_slugs[] = $term->slug;
			}
			$custom_slug = str_replace( '%categories%', implode( '/', $hierarchical_slugs ), $custom_slug );
		} else {
			$custom_slug = str_replace( '%categories%', 'product', $custom_slug );
		}
	}
	
	....

	return $permalink;
}

Но на этом этапе появилась проблема производительности. Сайт стал работать медленнее, особенно на странице вывода нескольких товаров. Например, на странице категории с выводом всего лишь 16 товаров производилось почти 90 запросов в базу данных и резко увеличивалось время ответа сервера примерно на 25-30%.

Оказалось, что при вызове функции the_permalink WordPress производит множество операций: получает тип ЧПУ и собирает данные записи в зависимости от этого типа. Чтобы вывести 1 ссылку WordPress производит несколько некешируемых запросов в базу. При этом процесс получения таксономий и иерархий товара не самый быстрый.

Так как постоянная генерация ссылки нам не подходит, логично её где-нибудь хранить и затем просто подтягивать оттуда. Было решено использовать специальное поле guid таблицы wp_posts. Хотя разработчики WP не рекомендуют его менять, всё же мы можем игнорировать это предупреждение по нескольким причинам:

  • guid используется для формирования RSS, а его мало кто использует.
  • в RSS выводятся только записи, а у нас тип поста — product.

Чтобы в базу ссылки правильно сохранялись повесим на событие save_post функцию, которое обновляет поле guid:

add_action( 'save_post', 'wpsl_guid_rewrite', 100 );
function wpsl_guid_rewrite( $id ) {
	
	if( !is_admin() ) return;
	
	if( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) return false;

	if ( strpos( get_option( 'product_permalink' ), '%category%' ) !== false || strpos( get_option( 'product_permalink' ), '%categories%' ) !== false ) {
		if( $id && get_post_type( (int)$id ) == 'product' ){
			global $wpdb;
			$wpdb->update( $wpdb->posts, [ 'guid' => ( get_permalink( $id ) ) ], [ 'ID' => intval( $id ) ] );
		}

		clean_post_cache( $id );
	}
}

Нам остаётся перехватить вывод ссылки на хуке post_type_link и вывести сгенерированную ссылку:

add_filter( 'post_type_link', 'wpsl_get_permalink_change', 10, 4 );
function wpsl_get_permalink_change( $post_link, $post, $leavename, $sample ){
	if ( isset( $post->guid ) && $post->guid && $post->post_type == 'product' && ( strpos( get_option( 'product_permalink' ), '%category%' ) !== false || strpos( get_option( 'product_permalink' ), '%categories%' ) !== false ) ) {
		return $post->guid;
	}
	return $post_link;
}

Здесь мы проверяем заполненность поля guid, тип поста и тип ЧПУ. Если значения трёх параметров нас устраивают, выводим заранее сохранённую ссылку.

Теперь, самое приятное: посмотреть на результаты. Количество запросов в базу снизилось почти в 2 раза — с почти 90 до 44! А время ответа сервера на тестовом хостинге опустилось до приемлемых 0.24 секунды.

Обновление поля guid происходит не только во время создания и редактирования товара. В плагин встроена система импорта товаров через csv, поэтому обновление происходит также и при завершении импорта.

Комментарии (17)


  1. slaFFik
    18.09.2019 20:49
    +1

    Ох, сколько вы костылей собрали в плагине…
    1) Начнем с того, что в ридми вы линкуетесь на свой сайт, где есть ссылка на "ПЛАГИН WP STORE", которая ведет… никуда, 404 ошибка.
    2) В самом плагине — вы поломали авто-загрузку переводов ВП. Домен для переводов должна равняться слагу вашего плагина на WP.org. У нас домен wpsl, а слаг — wp-store-lite. Если совпадает, можно не писать свой код load_plugin_textdomain() вообще, ВП сам будет это делать.
    3) Использовать CPT для хранения товаров — вот сразу нет, очень плохая идея, ОЧЕНЬ. Еще небось и заказы там храните? WC так продолжает делать из-за безысходности, EDD переписывается на собственные таблицы (будет в 3.0). Вот пока у вас нет пользователей — переписывайте на собственные таблицы. Поверьте, вы махом решите огромное количество проблем в будущем. Ну и использование слишком общего product тоже не очень хорошая идея. А если какой-то другой плагин до этого использовался? Тогда ваш плагин при активации подтянет те товары тоже, и будет поломан, потому что данные в базе не будут в ожидаемом формате, а просто совпал CPT. Я бы добавил префикс wpsl_product и так далее.
    4) Вот за это: wp-content/themes/THEME/combine-script.js — хочется сделать большое а-та-та. Вы суете файл не в предназначенное для него место. Обновилась тема — удалился файл, его создание — доп.нагрузка. Если вам надо создавать файл — делайте это в wp-content/uploads/wp-store-lite, или вообще используйте Media Library.
    5) В файлах темплейтов я вижу большое количество проблем с эскейпом данных перед выводом. К примеру, вы печатаете $order->post_title и $ticket->post_title напрямую. А вы проверяли с кавычками и слешами? Ну как минимум esc_html() надо использовать.
    6) Создание таблиц при активации плагина. А если плагин деактивировать и заново активировать? Дважды запускается создание таблиц (и все другие вещи в class-wpsl-install.php). Это надо бы переделать на хранение версии в базе и сравнение при загрузке в админке текущей версии и той, что в базе, и запуск необходимых инсталляций.


    И много всего другого. Я бы посоветовал вам поставить PhpStorm, активировать его WordPress плагин, настроить WordPress Coding Standards, и поставить плагин PHP Inspections (EA Extended). Вы узнаете очень много нового и нужного. Для VS Code тоже есть эти плагины, но шторм имхо вне конкуренции как IDE.
    А в целом, желаю удачи!


    1. slaFFik
      18.09.2019 20:57
      +1

      Еще один момент, вы написали "сделать не требовательный к ресурсам и достаточно быстрый ecommerce плагин". А вы это проверяли вообще? Что будет, если у вас 1000 товаров, 100000 заказов, 100500 пользователей, 123523 саппорт тикетов (вы их тоже как CPT храните) и прочее в большом количестве?
      Требовательность плагина к ресурсам проверяться должна не на пустой базе (и 40 запросах). На пустой базе и виджеты Вуукомерса нормально работают, а когда у вас тысячи товаров и десятки категорий — они тупо дохнут и надо писать свои.
      У вас очень много 'posts_per_page' => -1, используется, и вы выгребаете ВСЕ данные для СПТ, вместо тех, что вам нужны (айди и заголовок). Посмотрите в сторону id=>post_title.


      1. YanAlexandrov Автор
        18.09.2019 21:03

        Спасибо за комменты. Я проверял на тестовом магазине в 5800 товаров и 5+ характеристик для каждого. Работает очень бодро, в том числе фильтр. Приведённый в статье ответ сервера как раз с тестового сайта.


    1. edogs
      19.09.2019 00:03

      Респект, давно не видели такого профессионального ответа по делу, даже на WP-овских форумах такое и то редко бывает.


  1. SDKiller
    19.09.2019 04:58

    Вы все еще делаете интернет-магазины на вордпрессе...


    1. Mangol31
      19.09.2019 05:38

      Почему нет? Насколько я понимаю, до определенного уровня — вполне. Достаточно знаний вордпресса, html/css, немного php (последние два — чтобы немного выйти за рамки тем оформления, там где это необходимо). Выходит куда дешевле и проще, чем связываться с сайтом на битриксе. И гораздо функциональнее, чем на тильде.


      1. redpax
        19.09.2019 07:13

        Я всегда делаю на битрикс магазины. Хотел бы услышать аргументы почему я не прав.


        1. Mangol31
          19.09.2019 07:18

          А где я написал что вы не правы? Умеете делать, получается, клиент доволен — почему нет?
          Мой вопрос был к SDKiller. Хотелось бы услышать, чем так плохи магазины на WP+WC. Это не саркастический вопрос, мне действительно интересно.
          Я не разработчик на WP+WC, скорее продвинутый пользователь WP. Когда весной появилась потребность сделать инет-магазин, проанализировал платформы для этого и в итоге остановился на WP+WC. Для поставленной задачи (объем до 200 товаров, посещаемость не дикая, делать будем своими силами) — возможностей у платформы выше крыши. И на данный момент оплата только за хостинг. Та же Тильда рядом не стояла по возможностям и цене. На Битриксе порог входа существенно выше — я не разраб.


          1. Kenya
            19.09.2019 14:54

            Wordpress сам по себе не самая шустрая CMS. И реализация WC только подливает масла в огонь. В итоге магазин на WP+WC хорош только до определенного количества товаров, потом начинаются просадки по производительности сайта


            1. Mangol31
              19.09.2019 15:23

              Абсолютно согласен с вами. Именно об этом ограничении я и написал выше, указав, что в магазине планируется до 200 товаров. Насколько я знаю, WC+WP успешно справляется с примерно до 1000 товаров. Потом уже надо искать что-то другое. Но это не значит, что WP+WC плох в своей нише. Для практически бесплатного решения, как по мне, так вообще идеально подходит.


          1. redpax
            19.09.2019 17:29

            Я про эту фразу

            «Выходит куда дешевле и проще, чем связываться с сайтом на битриксе. „


            1. Mangol31
              19.09.2019 19:02

              А что здесь не так?
              1) Дешевле. Однозначно. Стоимость минимальной лицензии для версии под магазин 35 000 руб. WP+WC — бесплатен. При этом за 35 000 приобретается конечное количество модулей, а под WP+WC плагинов до и больше, под любые хотелки.
              2) Проще. Так и тут все верно. Т.к. WP предельно прост в освоении с т.з. пользователя, по нему просто немереное количество материалов в сети.

              Вот и выходит, что WP+WC для средних размеров магазина (до 1000 позиций) это практически идеальный вариант с минимальными финансовыми вложениями. Если что-то большее, то там уже совсем другой разговор и совсем другие деньги на старте должны быть.


              1. redpax
                19.09.2019 19:12

                В битрикс за 20000-35000 (редакция малый бизнес) есть почти всё для запуска кроме кассы сразу из коробки но да то стоит денег. Но сколько будет стоить работа собрать всё это на вордпрессе и насколько это будет удобно?


                1. Mangol31
                  19.09.2019 21:07

                  Думаю, стоить будет дешевле чем на битриксе. Особенно с учетом стоимости лицензии. По удобству не могу сказать, т.к. в админке магазина на битриксе не лазал. Но из того что вижу в WC — вполне себе норм. Особенно с учетом возможности загрузки товаров списком.


        1. neyrowebster
          19.09.2019 17:27

          Битрикс это правла безархитектурное дерьмо? или это только я такое видел?


  1. preslilvs
    20.09.2019 09:21
    +1

    Классно что кто то еще делает плагины и рассказывает об этом на хабре!
    Присоединюсь к словам slaFFik и добавлю еще немножко советов.
    1. Словил fatall error когда установил плагин на свой сайт, могу кинуть в личку
    2. Измените название плагина, что бы оно вообще не совпадало ни с одним плагином. Сейчас ваш плагин не найти в поиске а это важно если вы хотите что бы плагин был популярен.
    3. Поработайте над описанием, что бы юзер понял почему ваш плагин лучше еще сотни таких же. Возможно так вы продвинете свой плагин в поиске WordPress по не самым популярным запросам в топ.
    4. Замените скриншоты, нужно делать их полностью на английском, вы же рассчитываете на западных пользователей верно? Да и сделайте видео о том как пользоваться вашим плагином, подумайте что вызовет трудности у пользователей и покажите как их решить, либо на видео, либо составьте FAQ


    1. YanAlexandrov Автор
      20.09.2019 09:25

      Спасибо за комментарий. Отправьте fatall error в личку, пожалуйста.