HUSKY
Фасетний фільтр товарів для WooCommerce: таксономії, атрибути, ціна, наявність, мета-поля й текст; у вигляді віджета або шорткоду, з перезавантаженням сторінки чи через AJAX.
- Перший реліз
- Активні встановлення
- 80 000+
- Рейтинг
- Читати відгуки
Про мене
HUSKY фільтрує товари WooCommerce за категоріями, атрибутами, мітками, ціною, статусом наявності, рейтингом, автором, SKU, довільними мета-полями й текстом. Він є на wordpress.org з 2014 року. Безкоштовна й преміум-версії збираються з однієї кодової бази; версію визначає мажорний номер релізу, а преміум-частини в безкоштовній збірці або замінені заглушками, або обмежені.
Кожен стан фільтра зрештою стає запитом WordPress до wp_posts, wp_postmeta і таблиць термінів, а магазини працюють із десятками тисяч товарів і варіацій. Показ кількості товарів, яку дасть кожна опція, означає ще один запит на кожен видимий термін під час кожного перегляду сторінки. Варіативні товари, видимість у каталозі, податки та плагіни валют по-різному змінюють те, що вважати збігом.
Плагін також працює всередині обробки запиту, якою не володіє: розбір rewrite-правил WordPress, головний запит і шаблони товарів WooCommerce, конструктори сторінок, багатомовні плагіни та шари кешування. Більшість описаного нижче коду існує тому, що один із цих шарів зробив прямий підхід хибним або надто повільним.
HUSKY · фільтр товарів
що розв'язано
SEO-адреси для станів фільтра
у продуктіЩо це даєВідфільтровані сторінки отримують чисті стабільні адреси, які пошукові системи можуть індексувати. Власник вирішує, які сторінки фільтра потрапляють у Google і з яким заголовком.
- Проблема
- Стани фільтра — це рядки запиту, які пошукові системи або ігнорують, або сканують без обмежень. Шляхові адреси на кшталт /swoof/color-red/size-m/ невідомі rewrite-правилам WordPress і конфліктують із канонічними редіректами.
- Рішення
Перехопити розбір запиту, відрізати сегмент фільтра, дати WordPress зіставити решту й відновити стан фільтра із сегмента за фіксованим кодуванням і правилами індексації залежно від глибини.
- Стек
/shop/swoof/color-red/size-xl/ не відповідає жодному rewrite-правилу й дає 404Відрізати сегмент фільтра, решту маршрутизувати через rewrite-правила ядра, з нього відновити фільтри
Посилання фільтра — це адреса архіву плюс slug фільтра (за замовчуванням swoof) і по одному сегменту на кожен фільтр: /product-category/shoes/swoof/color-red-and-blue/size-xl/. Плагін не реєструє жодних rewrite-правил. url_process() запускається на do_parse_request на фронтенді й одразу завершується, якщо get_search_request() не знаходить /<slug>/ у шляху. Вона бере сегмент фільтра до /page/, ? або #, видаляє його й зіставляє залишок (/product-category/shoes/ або /shop/page/2) з $wp_rewrite->wp_rewrite_rules() за допомогою коду, скопійованого з WP::parse_request(): докладні правила сторінок, публічні й приватні змінні запиту, доступні для перегляду типи записів, таксономії, недоступні для запитів, wp_resolve_numeric_slug_conflicts() і фільтр request. Потім вона викликає query_posts(), handle_404() і register_globals() (до WordPress 6.0 — дію parse_request) та повертає false, тож ядро не розбирає URL удруге.
parse_url_query() ділить сегмент за '/' і зіставляє кожну частину з картою назв від get_all_items(): slug-и атрибутів без pa_, slug-и таксономій, перекладені WPML, price та фіксовані слова instock, onsale, featured, backorder_not_in. -and- і -or- стають комами, -to- — роздільником діапазону, + — пробілом; price-10-to-50 стає min_price і max_price. add_request() зливає результат через фільтр woof_get_request_data, тож get_request_data() повертає той самий масив, що й для рядка запиту ?swoof=1&pa_color=red. Далі в головному запиті parse_query() задає post_type product, позначає запит як архів типу запису й додає мета-запити, а tax_query() на woocommerce_product_query_tax_query замінює записи WooCommerce для тих самих таксономій на tax query плагіна.
Оскільки залишок маршрутизується наявними правилами, база магазину, база категорії товарів і пагінація архіву працюють без нових правил: /page/N/ відрізається до того, як знайдено сегмент фільтра, і залишається в маршрутизованому шляху. parse_query() вимикає is_tax на сторінках фільтра, тож поточну категорію відновлюють з останнього сегмента очищеного URL (get_term_from_cleared_url()). Коли сторінка магазину водночас є головною, виправлення redirect_canonical, описане нижче, не дає WooCommerce перенаправити URL фільтра на /.
Для URL, що відповідає SEO-правилу, meta_head() виводить canonical: URL фільтра без рядка запиту, якщо стан індексується, при цьому canonical від Yoast і SEOPress вимикаються; інакше — URL, обрізаний на slug-у фільтра, тобто звичайний архів. З Yoast SEO sitemap_index() на wpseo_sitemap_index додає адреси, які адміністратор перелічує в текстовому полі налаштувань по одній у рядку; преміум-збірка екранує їх через esc_url_raw() і esc_xml().
Порядок, у якому застосовують фільтри, не повинен змінювати URLПисати фільтри у відсортованому порядку ключів; відтворити граматику -and- / -to- / + у PHP і JS
woof_get_submit_link() у url_parser.js сортує активні фільтри за ключем і записує кожен як <name>-<values>, де ',' — це -and-, роздільник діапазону — -to-, а пробіли — +. Ціна стає price-<min>-to-<max>; «в наявності», «зі знижкою», «рекомендовані» й backorder стають фіксованими словами. parse_url_query() виконує зворотне відображення.
Завдяки сортуванню вибір спершу color, а потім size дає той самий шлях, що й у зворотному порядку. Значення в межах одного фільтра записуються в порядку їх вибору.
Атрибут pa_niveau і мета-поле niveau претендують на одну й ту саму назву в URLВиявляти збіги назв: таксономія зберігає коротку назву, інший отримує унікальну запасну
Назви в URL відкидають префікс pa_, тому атрибут і мета-ключ можуть дати однакову назву, і один запис карти сховав би другий. resolve_seo_key_conflict() віддає коротку назву зареєстрованій таксономії незалежно від порядку фільтрів у налаштуваннях, тож наявні URL лишаються чинними, а для іншого фільтра виводить назву з його справжнього ключа, додаючи за потреби числовий суфікс. slug-и таксономій, перекладені WPML, розв'язуються до цього кроку.
Кожен запит перерахунку знову читає стан фільтра зі шляху URLМемоїзувати parse_url_query() для кожного URL, але лише після wp_loaded
get_request_data() викликається з кожного запиту перерахунку, а з SEO-адресами кожен виклик іде через woof_get_request_data до parse_url_query(). З Dynamic Recount і Hide Empty (преміум) одна сторінка може робити понад тисячу таких викликів, тому результат розбору кешується для кожного URL.
Кеш вмикається лише після wp_loaded, тож виклик, зроблений до появи таксономій і налаштувань, не може заморозити неповну карту назв до кінця запиту.
Які сторінки фільтра індексувати і з яким заголовком та мета-данимиІндексувати за глибиною та за regex-правилами з мета-даними для кожного правила; несправні шаблони тихо пропускати
WOOF_SEO рахує активні фільтри. Понад index_deep (за замовчуванням 2) сторінка отримує налаштування 'Disable page indexing' (за замовчуванням yes, тобто noindex,nofollow через wp_robots), а правила не перевіряються. На цьому рівні або нижче збіг із правилом робить сторінку індексованою. Правила — це шаблони URL із заповнювачами {any}, скомпільовані в регулярні вирази, кожне з заголовком, мета-описом, H1 і SEO-текстом, що можуть містити назви вибраних термінів; зберігаються окремо для кожної локалі. Теги canonical виводяться лише для URL, що відповідають правилу.
У преміум-збірці preg_match виконується під тимчасовим обробником помилок, тож правило, чий URL не є коректним виразом, пропускається й потрапляє в сповіщення в адмінці, а порожнє правило пропускається. Безкоштовна версія використовує лише перші два правила.
WooCommerce 11 перенаправляв URL фільтра на головній сторінці, що є магазиномСкасовувати лише редірект на home_url і лише для URL зі slug-ом фільтра
Коли сторінка магазину водночас є статичною головною, WooCommerce 11.0.0 робить її queried object в архівах товарів. Тоді redirect_canonical() вважає /swoof/... неканонічною копією головної й перенаправляє на /, втрачаючи всі фільтри. fix_front_page_shop_canonical() повертає false лише тоді, коли головна — це сторінка магазину, шлях містить slug фільтра, а ціль редіректу — головна адреса, тож нормалізація слешів і slug-ів працює в усіх інших випадках.
Динамічний перерахунок і кешування запитів
у продуктіЩо це даєКожна опція фільтра показує, скільки товарів вона поверне, тож покупці не потрапляють у глухий кут. Лічильники кешуються, тому магазин лишається швидким на великих каталогах.
- Проблема
- Кожна опція фільтра показує, скільки товарів вона дасть за поточного вибору. Це один запит товарів на кожен видимий термін під час кожного перегляду сторінки, і це головні витрати на великих каталогах.
- Рішення
Лічильники беруться зі спрощеного запиту лише за ID, який відтворює семантику фільтра. Результати кешуються в межах запиту, у виділеній таблиці й у transients, а форму можна відкласти, доки відвідувач її не попросить.
- Стек
Лічильник терміна має враховувати решту поточного виборуПеребудовувати запит для кожного терміна, підставляючи термін за правилами одиничного або множинного вибору
dynamic_count() перебудовує tax_query для кожного терміна. Уже вибрані терміни лічильника не отримують. Для radio і select (одиничний вибір) термін замінює вибране значення своєї таксономії; для checkbox і multi-select (множинний вибір) він додається до нього. Терміни поточної сторінки категорії та таксономій з атрибута taxonomies шорткоду завжди додаються через AND, тож лічильники на сторінці категорії лишаються в межах цієї категорії.
Логіка для кожної таксономії (OR, AND, NOT IN) з налаштувань, діапазони ціни й рейтингу, мета-умови розширень, мова WPML і видимість у каталозі застосовуються так само, як у запиті товарів. Розширення оголошують свою поведінку перерахунку через html_type_dynamic_recount_behavior, тож типи color, image і label повторно використовують цей код.
Приховування опцій із нульовим результатом побудоване на цих лічильниках: термін підраховується й пропускається, якщо лічильник дорівнює 0, тож це не може бути дешевше за показ лічильників. Цей перемикач доступний лише в преміумі; безкоштовна версія примусово вимикає його у відображеннях.
Запит лічильника не повинен завантажувати записи чи виконувати FOUND_ROWSУспадкувати WP_Query, зробивши методи found-rows і циклу порожніми, і брати лише ID
WP_QueryWOOFCounterIn розширює WP_Query і перевизначає set_found_posts(), setup_postdata(), the_post() і have_posts(). Запит лічильника виконується з fields=ids і nopaging, тож MySQL повертає список ID, WordPress не будує об'єктів записів і не виконує другий запит FOUND_ROWS. Лічильник — це довжина списку.
Фільтри posts_clauses за популярністю й рейтингом від WooCommerce прибираються перед запитом лічильника, бо порядок для лічильника не має значення, а ці фільтри додають у SQL об'єднання.
Ті самі лічильники перераховуються для кожного відвідувача й сторінкиЗберігати лічильники в таблиці ключ-значення за ключем md5 від повних аргументів запиту, очищати її за cron
З увімкненим 'cache count data' WP_QueryWoofCounter хешує JSON фінальних аргументів запиту, які вже містять частини мови й видимості, і шукає хеш у {prefix}woof_query_cache — двоколонковій таблиці з хешем як первинним ключем. Запис іде через INSERT ... ON DUPLICATE KEY UPDATE, тож два перегляди сторінки, що одночасно пишуть той самий ключ, не призводять до помилки. Сайт зі стійким кешем об'єктів може перенести лічильники в wp_cache_* через фільтр woof_use_wp_cache.
Дешевого способу дізнатися, на які кешовані лічильники впливає новий або змінений товар, немає, тому запланована подія очищає всю таблицю через налаштований період. Якщо користувач MySQL не має привілею CREATE, сторінка налаштувань виводить SQL для ручного виконання й залишає опцію вимкненою.
Запит min/max для повзунка ціни сканує postmeta у великих магазинахМемоїзувати min/max за хешем SQL у межах запиту та в transient, скидати при збереженні товару
get_filtered_price() будує один SELECT MIN/MAX за _price, об'єднаний із поточним tax- і meta-запитом. Той самий діапазон запитувався кілька разів на сторінці (min і max для кожного фільтра ціни), тож результат мемоїзується в статичному масиві за ключем md5 від SQL. З увімкненим 'Optimize price filter' результати також потрапляють в один transient на добу, а будь-який woocommerce_update_product його видаляє. SQL проходить через woof_get_filtered_price_query, тож сайт може додати підказки індексів.
Дерева термінів будуються з нуля для кожної таксономії на кожній сторінціКешувати зібрані дерева в transients за ключем із аргументів і поточної мови
WOOF_HELPER::get_terms() завантажує всі терміни таксономії через get_categories() і збирає дерево «батько — нащадок» у PHP. Результат мемоїзується в межах запиту й, з увімкненим 'Cache terms', зберігається в transient, ключ якого містить таксономію, hide_empty, прапорці глибини та поточну мову WPML або Polylang, тож кожна мова має власне кешоване дерево.
Очищення проходить по wp_options за _transient_woof_terms_cache_% і видаляє кожен transient, оскільки WordPress не має видалення transients за префіксом.
Перерахунок виконується, навіть якщо відвідувач ніколи не відкриває фільтрВідображати кнопку замість форми й отримувати форму через AJAX після першого кліку
З start_filtering_btn=1 сервер виводить лише кнопку, тож під час завантаження сторінки запити термінів не виконуються. Після кліку front.js надсилає woof_draw_products із шорткодом товарів, заданим як 'woof_nothing': сервер відображає форму фільтра без товарів, зона перемальовування замінюється, а віджети ініціалізуються заново. Кнопка пропускається, якщо пошук уже активний, тож відфільтрована сторінка й далі показує свій вибір.
Приховування форми через CSS чи правила display нічого не заощаджує, бо лічильники обчислюються на сервері до відправлення HTML. У темах про продуктивність на форумі підтримки часто доводилося наголошувати на цьому.
Робота всередині запиту товарів WooCommerce
у продуктіЩо це даєФільтрація продовжує працювати з власною темою магазину, сторінками категорій, варіативними товарами, податками й валютами, тож покупці ніколи не бачать позицій у розмірах, яких немає в наявності.
- Проблема
- Фільтр змінює головний запит магазину й покладається на шаблони WooCommerce, якими не володіє. Він має працювати попри зміни моделі даних WooCommerce, з варіативними товарами, видимістю в каталозі, податками та плагінами валют.
- Рішення
Підключатися пізно до parse_query і запиту товарів WooCommerce, відновлювати стан архіву, якого очікують шаблони, і явно обробляти кожну відмінність моделі даних за версією або за таблицею.
- Стек
Відфільтровані результати мають відображатися через шаблон магазину з темиПереписувати прапорці головного запиту на parse_query з пріоритетом 9999, щоб він відображався як архів товарів
Коли slug пошуку є в запиті, parse_query() задає post_type=product і is_post_type_archive та скидає is_tax, is_tag, is_home, is_single і is_search. Тоді WooCommerce і тема відображають цикл магазину для будь-якого відфільтрованого URL. Розширення додають свої мета-умови через assemble_query_params(), а оператори для кожної таксономії надходять з фільтра woof_main_query_tax_relations.
Ті самі хуки не повинні зачіпати запити REST і Store API. Обробники завершуються одразу на URL /wp-json/, а parse_query відключається на rest_api_init, тож блокові теми та headless-клієнти отримують власні результати WooCommerce.
Фільтрація на сторінці категорії втрачає категоріюПередавати термін як really_curr_tax і зберігати його в transient за ключем IP відвідувача
URL фільтра більше не є архівом терміна, тож get_queried_object() перестає повертати категорію. Коли архів відображається, set_really_current_term() зберігає термін через WOOF_STORAGE, який плагін запускає в режимі transient: один transient на відвідувача, за ключем із хеша IP відвідувача, на добу. JS також додає really_curr_tax=<term_id>-<taxonomy> до посилання фільтра, а get_really_current_term() використовує його, якщо нічого не збережено. Потім термін додається через AND до запиту товарів, запитів перерахунку й текстового пошуку. AJAX-запити читають збережений термін, але ніколи не перезаписують його.
Видимість у каталозі зберігається по-різному залежно від версії WooCommerceРозгалужуватися за версією: мета _visibility до 3.0, product_visibility NOT IN після
WooCommerce 3.0 переніс прапорці видимості, рекомендованості й відсутності в наявності з postmeta в таксономію product_visibility. product_visibility_not_in() перетворює потрібні прапорці на умови term_taxonomy_id NOT IN. generate_visibility_keys() додає outofstock, коли магазин ховає товари, яких немає в наявності; коли опція 'listen catalog visibility' увімкнена, вона також завжди додає exclude-from-search, а поза пошуком — exclude-from-catalog. Для старіших магазинів натомість додається мета-умова _visibility.
Ті самі правила застосовуються до головного запиту, запиту шорткоду, запитів перерахунку, текстового пошуку й даних turbo-режиму. Якщо одне з них відрізняється, лічильники й результати розходяться.
Варіативний товар показується, коли вибраного розміру немає в наявностіЗнаходити батьків розпроданих підходящих варіацій у таблиці пошуку атрибутів і виключати їх
Варіативний товар вважається таким, що є в наявності, якщо є хоча б одна варіація, тож простий запит «в наявності + розмір M» повертає сорочку, чия варіація M розпродана. Коли фільтр наявності поєднано з фільтрами атрибутів, posts_where() збирає вибрані терміни pa_*, зокрема термін, що рахується під час перерахунку, і виконує два згруповані запити до wc_product_attributes_lookup: батьки з підходящою варіацією, якої немає в наявності, і батьки з підходящою варіацією в наявності. Різниця додається як ID NOT IN.
Старіший режим, який можна вибрати в налаштуваннях фільтра наявності, натомість запитує записи product_variation за мета-атрибутами та _stock_status замість таблиці пошуку. Він повільніший і за замовчуванням виключає кожного батька, що має розпродану підходящу варіацію; батьки, які також мають підходящу варіацію в наявності, зберігаються лише тоді, коли фільтр woof_exclude_existing_variations повертає true.
Межі ціни відрізняються від збережених цін через податки й валютуКонвертувати межі назад у базову валюту й відняти податок, що входить у ціну, перед мета-запитом
_price зберігається в базовій валюті магазину і, у багатьох магазинах, без податку. Якщо відвідувач використовує валюту WPML (WCML), assemble_price_params() конвертує межі назад у базову валюту. Якщо ціни показуються з податком, а зберігаються без нього, податок за стандартною ставкою віднімається від обох меж. Для верхнього краю повзунка get_max_price() знаходить товар з максимальною ціною й запитує в WooCommerce його ціну з податком.
З FOX (WOOCS) значення повзунка, що показуються, конвертуються через woocs_exchange_value, а запит і далі використовує числа в базовій валюті.
Режим AJAX: перемальовувати результати, лічильники й частини сторінки без перезавантаженняОдна AJAX-точка відображає товари й форму через шорткоди; JS підмінює зони й оновлює URL
woof_draw_products() перебудовує $_GET із посилання, яке відкрив би відвідувач, відображає [woof_products] і [woof] з цим станом і повертає у JSON товари, форму та додаткові фрагменти (кількість результатів, сортування, пагінацію). front.js замінює кожен фрагмент, повертає фрагменти, яких не було у відповіді, повторно ініціалізує віджети й викликає history.pushState, щоб URL можна було поширити. onpopstate перезавантажує сторінку, якщо рядок запиту змінився. Прапорець блокування відкидає кліки, поки виконується запит.
Цикл магазину обгорнуто через хуки woocommerce_before_shop_loop і woocommerce_after_shop_loop. Теми, які не викликають обидва хуки, ламають режим AJAX; це пояснює цілу групу тем підтримки з різними симптомами й однією причиною.
Turbo-режим: фільтрація в браузері
у продуктіЩо це даєНа великих каталогах фільтрація виконується в браузері покупця, а не на сервері. Результати з'являються швидко, а сервер менше навантажений у години пік.
- Проблема
- На великих каталогах кожен клік фільтра коштує серверу набору об'єднань за таксономіями й мета-даними плюс один запит лічильника на кожен видимий термін.
- Рішення
Попередньо обчислити плоский JSON-індекс товарів і варіацій, завантажити його в браузер і виконувати фільтр як SQL над ним за допомогою AlaSQL. Сервер лише відображає відповідні ID. Входить в обидві версії.
- Стек
Дані товару розкидані по записах, postmeta, термінах і варіаціяхЗвести кожен товар і варіацію до одного JSON-запису: ціни, терміни, мета, наявність
get_data_by_id() записує ID, назву, SKU, автора, батька, статус наявності, slug-и термінів кожної таксономії, за якою можна фільтрувати (для варіацій — атрибути варіації), налаштовані мета-поля та список пар звичайної/акційної ціни. Варіативний товар зберігає по одній парі на кожного нащадка, тож браузер може обчислити діапазон його цін за тим самим правилом, що й WooCommerce: акційна ціна враховується лише тоді, коли вона нижча за звичайну.
Ціни зберігаються з податком, коли магазин показує ціни з податком, а FOX перед їх читанням перемикається на валюту за замовчуванням, тож індекс складено в одній валюті.
Індексація тисяч товарів впирається в ліміти часу PHPБудувати AJAX-кроками по 10 товарів, один файл на кожну мову WPML і кожен блог мультисайту
create_data_search_files() приймає зсув, обробляє 10 ID товарів і варіацій із застосованими правилами видимості, зливає їх у JSON-файл і повертає наступний зсув; скрипт адмінки працює в циклі, доки не отримає -1. WPML перемикається на мову за замовчуванням, і кожен товар записується у файл кожної мови через його ID перекладу. check_id_file() додає ID блогу на мультисайті, щоб сайти не ділили один файл.
Ліміт за замовчуванням — 15,000 записів, його можна змінити фільтром woof_turbo_mode_products_limit.
Запланована перебудова не повинна віддавати напівзаписаний індексВласний cron-виконавець обробляє зрізи ID за запит і в кінці підміняє файл _temp
PN_WP_CRON_WOOF_TURBO_MODE зберігає стан події (повний список ID, позицію, наступний запуск) в опції та виконується на init. Кожен виклик process() обробляє один зріз із 10 ID, а 5-секундне обмеження, збережене в woof_cron_limit_do, не дає сплеску переглядів сторінок запускати зрізи один за одним. Зрізи записуються в data_<id>_temp_<lang>.json; коли список вичерпано, do_recreate_file() переміщує тимчасовий файл поверх робочого, а відвідувачі й далі бачать старий індекс, доки новий не буде завершено.
Ціна такого рішення — застарілість: товари, додані після останньої перебудови, невидимі для фільтра, а перебудова залежить від трафіку сайту так само, як WP-Cron.
Стан фільтра має стати запитом, який може виконати браузерГенерувати умови WHERE для AlaSQL із власними функціями для діапазонів ціни й видимості
turbo_mode.js перетворює поточні значення на рядок WHERE: належність до таксономії через indexOf на масивах термінів, порівняння мета-полів, LIKE для SKU і назви, BETWEEN для повзунків і дат. Власні функції AlaSQL обчислюють мінімальну й максимальну ціну товару зі списку його варіацій та застосовують видимість у каталозі. Для варіативних товарів фільтр наявності спершу вибирає батьків варіацій, яких немає в наявності й які відповідають іншим фільтрам. Перерахунок виконується над тією самою таблицею в пам'яті.
Отримані ID надсилаються на woof_draw_products як turbo_mode_ids, а сервер відображає запит post__in замість об'єднань фільтра.
Браузеру потрібні налаштування, але в них є серверні секретиВидаляти з локалізованих налаштувань облікові дані БД статистики й секретні ключі cron
JS відтворює логіку фільтра з woof_settings, тож йому потрібна на сторінці більша частина цієї опції. get_public_settings() видаляє блок статистики, який містить облікові дані зовнішнього MySQL, і кожен cron_secret_key, а решту пропускає через фільтр для подальшого вилучення. У преміум-збірці точка перебудови вимагає nonce і manage_woocommerce.
Статистика пошуку
у продуктіЩо це даєВласник бачить, за чим покупці насправді фільтрують і що шукають, не сповільнюючи магазин і не переповнюючи його базу даних журналом, що безупинно росте.
- Проблема
- Запис того, за чим фільтрують покупці, означає запис на гарячому шляху кожного перегляду відфільтрованої сторінки, від анонімних відвідувачів і ботів, часто на спільному хостингу.
- Рішення
Записувати один малий сирий рядок на запит у проміжну таблицю, а довготривалу статистику тримати на окремому сервері MySQL, якого вимагає розширення.
- Стек
Логування не повинно множити записи й не повинно захлинатися від одного клієнтаЛогувати раз на запит, усувати дублі за хешем запит+IP+день, обмежити кількість проміжних рядків на IP
Розширення статистики підключається до woof_get_request_data, який виконується багато разів на сторінку; статичний прапорець обмежує логування першим викликом, а AJAX-виклики, окрім відображення товарів, ігноруються. Зберігаються лише ключі фільтра, вибрані в налаштуваннях, запит обрізається до request_max_deep ключів (за замовчуванням 5) із збереженням діапазону ціни, і хешується разом з IP та датою.
Рядок вставляється лише якщо цей хеш новий і в проміжній таблиці для цього IP менше ніж user_max_requests рядків (за замовчуванням 10).
Статистика необмежено росте всередині бази даних магазинуЗберігати статистику на окремому сервері MySQL через PDO, по одній таблиці на місяць
Розширенню потрібен власний сервер MySQL, заданий в опціях. init_pdo() підключається до нього через PDO, бо $wpdb не може тримати друге з'єднання з іншими обліковими даними. place_statdata_into_db() створює таблицю на місяць з назвою <year>_<month> і вставляє рядки підготовленими запитами.
Звіти запитують місячні таблиці за діапазоном дат, тож база даних магазину не містить довготривалої статистики.
Текстовий пошук і збережені пошуки
у продуктіЩо це даєПокупці знаходять товари за SKU, атрибутом чи словами в описах варіацій. Зареєстровані клієнти можуть зберегти пошук і отримувати сповіщення, коли з'являються нові відповідні товари.
- Проблема
- Пошук WordPress охоплює заголовок і вміст, а покупці вводять SKU, назви атрибутів і слова, що є лише в описах варіацій. Точка підказок публічна, а збережені пошуки (розширення Products Messenger) треба буде пізніше виконати знову, не довіряючи збереженому вводу.
- Рішення
Будувати умову WHERE з підготовлених фрагментів для кожного слова, додаючи об'єднання лише за потреби, зіставляти варіації з батьками окремим запитом, обмежити те, що може запитувати публічна точка, і повторно виконувати збережені пошуки із записаного сервером SQL.
- Стек
Шукати за назвою, вмістом, SKU, мета-даними й назвами термінів в одному запитіПідготовлені групи OR для кожного слова, з'єднані через AND, з об'єднаннями лише коли вони використовуються
create_where_query() ділить фразу на слова. Для кожного слова вона з'єднує через OR вибрані поля запису й перелічені мета-ключі (SKU — це просто _sku), плюс збіг назви терміна з усією фразою, а групи слів з'єднуються через AND. Початковий '-' перетворює слово на NOT LIKE, з'єднаний через AND. Фрагменти проходять через $wpdb->prepare().
Об'єднання з термінами й postmeta додаються в posts_join лише коли відповідні опції ввімкнено, а posts_groupby згортає дубльовані рядки, які створюють об'єднання.
Знаходити слова, що є лише в описах варіаційСпершу знайти підхожі варіації, зіставити їх з ID батьків, додати їх через OR до головної умови
Варіації — окремі записи, і запит батьків не бачить _variation_description. Коли опцію ввімкнено, один запит знаходить записи product_variation, чий опис містить усі слова, другий зіставляє їх із post_parent, а головна умова стає (умови слів OR ID IN (батьки)). Список батьків може кешуватися в кеші об'єктів для кожного списку слів.
Точка підказок публічна й бере розмір сторінки із запитуРозбивати запит на сторінки в SQL; обмежити max_posts діапазоном 1..max(100, налаштування адміна) (преміум)
ajax_search() відкрита для гостей і розбиває запит на сторінки в SQL з max_posts як розміром сторінки, тож завантажується лише одна сторінка збігів. У преміум-збірці max_posts із запиту обмежується діапазоном від 1 до більшого з налаштування адміна й 100, яке можна змінити фільтром. Підказки враховують поточний стан фільтра, сторінку категорії та обмеження «лише зі знижкою».
Деяким сайтам потрібні текстові збіги від окремого пошукового рушіяНадати woof_text_search_ids, щоб розробник міг підставити відповідні ID
Текстовий фільтр будує власний пошуковий запит, тож окремий пошуковий плагін не може визначити набір результатів у тому самому запиті; у темах підтримки серед таких конфліктів згадуються Ivory Search, Relevanssi і Advanced Woo Search. get_all_ids() спершу застосовує фільтр woof_text_search_ids. Коли зворотний виклик повертає масив, наприклад ID із SearchWP, цей масив стає набором збігів, а вбудований SQL пропускається. Підключення конкретного рушія потребує власного коду.
Сповіщати авторизованого клієнта, коли з'являються нові товари, що відповідають збереженому фільтру (розширення Products Messenger)Зберігати SQL справжнього запиту результатів, повторно виконувати його за cron, порівнювати ID товарів
Відображення Products Messenger зберігає $wp_query->request — SQL, згенерований WordPress для відфільтрованих результатів, — у сховищі плагіна для кожного відвідувача під ID користувача. Під час підписки, яка вимагає авторизованого користувача, сервер зчитує його назад із цього сховища, а не із запиту, видаляє LIMIT, перевіряє, що це один SELECT без ключових слів DML чи DDL, виконує його один раз, щоб зафіксувати початковий набір ID, і зберігає підписку в мета користувача.
Запланована задача повторно виконує кожен збережений запит і надсилає листом лише ті ID, яких немає в початковому наборі, після чого розширює цей набір. Підписка закінчується за кількістю повідомлень або за датою. Мета користувача оновлюється лише тоді, коли wp_mail() завершується успішно, тож невдала відправка повторюється під час наступного запуску.
Захист і цілісність налаштувань
у продуктіЩо це даєАнонімні відвідувачі не можуть використати фільтр, щоб виконати код чи дістатися до прихованих даних, а збережені налаштування не губляться й не перезаписуються під час внесення змін.
- Проблема
- Фільтр приймає рядки запиту й AJAX-запити від анонімних відвідувачів, відображає шорткоди та шаблони, названі в цих запитах, і зберігає налаштування, що містять HTML, JavaScript і SQL. У журналі змін зазначено сторонніх дослідників, які надіслали низку повідомлень про безпеку з 2022 по 2026 рік.
- Рішення
Ставитися до кожного значення із запиту як до даних: дозволяти лише відомі шорткоди й шаблони, рекурсивно санітизувати налаштування за шляхом ключів, вимагати права для всього, що зберігає код, і зливати налаштування замість перезапису.
- Стек
AJAX-запити несуть назву PHP-шаблону для відображенняПриймати лише назви шаблонів, уперше побачені у вмісті сторінки; AJAX не може додавати нові
[woof_products custom_tpl=...] цілком легітимно передається назад в AJAX-викликах, тож значенню не можна ні довіряти, ні відкинути його. sanitize_custom_tpl() залишає лише літери, цифри, '_', '-' і '/', що прибирає крапки та послідовності обходу каталогів. Назва, відображена зі справжнього вмісту сторінки, додається в опцію-білий список (зберігаються останні 20); під час AJAX приймаються лише назви, що вже є в цьому списку.
Шляхи include, побудовані з назв типів відображення, проходять через safe_view_slug(), яка відхиляє все, що не є простим slug-ом.
AJAX-запити несуть текст шорткоду, який не повинен запускати довільні шорткодиВитягувати з надісланого тексту лише власні теги плагіна через get_shortcode_regex
woof_draw_products() отримує зі сторінки текст шорткодів товарів і форми, щоб вивід AJAX збігався з початковим викликом. check_shortcode() будує get_shortcode_regex() лише для тегів плагіна ([woof_products] або [woof], плюс псевдоніми шорткоду товарів плагіна) і зберігає перший збіг; усе інше в рядку відкидається перед do_shortcode().
Налаштування змішують HTML, JavaScript, slug-и й звичайний текст на будь-якій глибиніСанітизувати рекурсивно; класифікувати кожне поле за його ключем або шляхом у дереві налаштувань
sanitize_array() обходить дерево налаштувань, зберігає кожне значення під санітизованим ключем і класифікує кожне поле за іменем або за шляхом у дереві: текст SEO-правил — як HTML через wp_kses_post, описи — як textarea, ключі, що використовуються в шляхах include, — як суворі slug-и, решта — як звичайний текст. Правила за шляхом потрібні, бо ключі глибоко в дереві, як-от text чи description, надто загальні, щоб перелічувати їх глобально.
Налаштування 'JavaScript after AJAX' можуть змінювати лише користувачі з unfiltered_html.
Відповідь AJAX вставляє фрагменти HTML, а запит відновлюється з URL відвідувачаВідкидати woof_redraw_elements з розібраного посилання (преміум); пропускати кожен фрагмент через wp_kses
Відображення AJAX відновлює $_GET із посилання сторінки, надісланого браузером, а відповідь вставляє woof_redraw_elements на сторінку як HTML, тож підроблене посилання не повинно мати змоги передати цей ключ. У преміум-збірці ключ видаляється з розібраного посилання. Легітимні фрагменти (SEO-заголовок і H1, кількість результатів, сортування) додаються серверними фільтрами, і кожен фрагмент проходить через wp_kses перед поверненням.
Кольори й зображення термінів слід задавати один раз для всіх мовЗливати надіслані кольори термінів поверх збережених; повертатися до терміна мови за замовчуванням
Сторінка налаштувань показує лише терміни поточної мови WPML або Polylang, тож при збереженні надсилаються кольори лише для цих термінів. woof_save_options() зливає збережені записи color, color_img і images_term_* під надісланими значеннями, тож інші мови зберігають свої.
Перекладений термін без власного кольору використовує свій оригінал мови за замовчуванням, знайдений через get_original_language_term(). Вона читає slug безпосередньо з wp_terms, бо з коригуванням ID у WPML get_term() може знову повернути перекладений термін.
Налагодження в реальних умовах
support forumЩо це даєПроблеми на живому магазині знаходять за їхньою справжньою причиною, навіть коли це тема, сервер, кеш чи інший плагін.
- Проблема
- Повідомлення з живого магазину називає симптом, а не причину. Той самий симптом може походити від шаблону теми, шару кешування чи оптимізації, сервера, іншого плагіна або самого фільтра.
- Рішення
Відтворити на чистій установці або на копії сайту, виключати по одному шару за раз і виправляти там, де причина: у плагіні для всіх або цільовою зміною на цьому сайті.
- Стек
Перемикачі підкатегорій зникають, а фільтрація ламається, щойно в URL є будь-який рядок запитуЛокалізував несправність на сервері тестом з довільним GET-параметром, потім виправив rewrite в Nginx
Навіть безглуздий параметр на кшталт ?dwdadd=1adw ламав сторінку, що довело: несправність у обробці рядка запиту, а не у фільтрі. Конфігурація Nginx вставляла зайвий ? після php і відкидала кожен параметр, тож фільтр ніколи не отримував своїх аргументів.
Магазин перестає оновлюватися після фільтрації, коли встановлено LiteSpeed CacheЗалогував відповідь AJAX, щоб виявити помилку розбору JSON, потім половинним поділом звузив налаштування LiteSpeed до CDN Mapping
Відповідь надходила, але JSON.parse падав через хибний екранований символ, а це означало, що CDN Mapping у LiteSpeed переписував URL усередині неї. Вимкнення CDN Mapping відновило фільтрацію.
Кнопки Filter і Reset оновлюють URL, але список товарів не перезавантажуєтьсяПростежив до збірки jQuery slim, відновив повний jQuery і замінив змінене ядро WordPress від хостингу на чисту установку
Сайт завантажував jQuery slim, у якому немає модуля AJAX, тож запит на відфільтровані результати ніколи не надсилався, хоча URL змінювався на клієнті; вимкнення всіх інших плагінів нічого не змінило. Збірка slim прийшла зі зміненим ядром WordPress від автоінсталятора хостингу; відновлення повного jQuery і перевстановлення ядра усунули проблему.
Мета-повзунки на малих десяткових значеннях ACF (близько .005) не фільтрують нічого або фільтрують усеДодав фільтр woof_slider_meta_query_type, щоб приведення типу в SQL можна було задати як DECIMAL для кожного мета-ключа
Автор повідомлення перебудував поле як звичайне довільне поле, і помилка лишилась, тож ACF не був причиною; підтримка відтворила її. Мета-запит порівнював значення без приведення до DECIMAL, і плагін отримав хук woof_slider_meta_query_type, який приймає $type і $key.
Фільтр зависає після кнопки браузера «Назад»; Search нічого не робитьДодав слухач pageshow, який скидає блокування надсилання й спливне вікно при відновленні з bfcache, випущено в оновленнях
Сторінки, відновлені з кешу «назад/вперед», зберігали встановленим woof_submit_link_locked і відкритим спливне вікно завантаження. Слухач перевіряє event.persisted або тип навігації 2 і скидає обидва; зміну внесено в плагін.
Цифри
wordpress.org- Активні встановлення 80 000+ лічильник wordpress.org.
- Відгуки 336 Середня оцінка: 4.6 з 5.
- Років підтримки 12 З 1 грудня 2014 р., останнє оновлення 9 жовтня 2026 р..