COS WP Woo
Назад к блогу

Замена ACF: кастомные поля с Gutenberg-блоками и repeater'ами

ACF Pro стоит до $249/год, а после покупки WP Engine будущее плагина под вопросом. Рассказываю, как мы построили полную замену: 20+ типов полей, repeater, Flexible Content, Gutenberg-блоки — с хранением в оптимизированных кастомных таблицах вместо раздутой wp_postmeta.

Полгода назад мне позвонил старый знакомый — технический директор одного промышленного дистрибьютора, у которого интернет-магазин на WooCommerce с шестнадцатью тысячами товаров. Голос у него был как у человека, который только что получил счёт за ремонт бампера и одновременно узнал, что КАСКО не покрывает. «Олег, — говорит, — у нас ACF Pro подписка кончается, они поменяли ценообразование, и теперь за наш объём сайтов просят двести сорок девять долларов в год. А ещё я прочитал, что их купил WP Engine, и теперь все гадают, что будет дальше. Мы завязаны на них полностью — все карточки товаров, все характеристики, подбор по вязкости, температурные диапазоны. Если они закроются или сломают API — мы встанем.» И вот эта фраза — «мы встанем» — она меня зацепила, потому что я слышу её не впервые. Я слышу её минимум раз в месяц от разных людей, и за ней стоит одна и та же проблема, о которой мало кто задумывается до тех пор, пока она не стукнет по лбу.

Проблема звучит банально: зависимость от стороннего плагина для критически важной функциональности. ACF — Advanced Custom Fields — стал де-факто стандартом для кастомных полей в WordPress. Почти каждый WooCommerce-магазин, который вышел за рамки простого «название-цена-описание», так или иначе использует ACF или его аналоги. Характеристики товаров, технические спецификации, документация, связи между товарами, условия доставки для конкретных позиций — всё это живёт в кастомных полях. И всё это, по сути, находится в руках стороннего разработчика, который может в любой момент изменить правила игры. Что, собственно, и произошло.

Когда WP Engine приобрёл ACF, по сообществу прошла волна тревоги. Не потому что WP Engine — плохая компания, а потому что история WordPress полна примеров, когда поглощение плагина крупным игроком приводило к неожиданным последствиям. Иногда плагин просто перестаёт развиваться. Иногда меняется ценовая политика так, что малый бизнес вынужден искать альтернативы. Иногда вносятся изменения, которые ломают обратную совместимость. И ты сидишь со своими шестнадцатью тысячами товаров, у которых по двадцать-тридцать кастомных полей на каждом, и думаешь — а что делать-то? Переезжать? Куда? На чём? Эти вопросы не абстрактные — я получал их в виде прямых звонков от клиентов, которые вкладывали в свои WooCommerce-магазины годы работы.

Помню, как на одной конференции по WordPress в Москве я разговорился с владельцем сети автосервисов, у которого каталог запчастей на WooCommerce — двенадцать тысяч позиций. Он рассказал, что при обновлении ACF с версии 6.1 на 6.2 у него «отвалились» все условные поля на карточках товаров. Поля просто перестали показываться — потому что изменилась внутренняя механика conditional logic. Починили за два дня, но эти два дня сайт работал с некорректными карточками товаров. Двенадцать тысяч карточек с пропущенными характеристиками — это не просто «неудобство», это прямые потери в конверсии. И ведь ACF не виноват — они улучшали продукт. Но зависимость от стороннего кода означает, что ты принимаешь любые изменения, даже те, к которым не готов.

Я думал об этом не как теоретик, а как практик. У нас на основном проекте — крупный дистрибьютор смазочных материалов — таблица wp_postmeta разрослась до девятисот тридцати семи мегабайт. Почти гигабайт только метаданных. Шестнадцать тысяч восемьсот сорок четыре товара, у каждого — десятки полей. Вязкость, температура вспышки, температура застывания, допуски производителей, ГОСТы, серия, линейка, тип основы — и это я только начал перечислять. И всё это лежит в wp_postmeta, потому что так работает ACF. Каждое значение — отдельная строка в таблице. Каждый repeater — ещё несколько строк с метаинформацией о количестве элементов. И когда ты делаешь WP_Query с мета-запросом по трём-четырём полям одновременно — база данных начинает плакать. Я не преувеличиваю. Я видел запросы, которые выполнялись по восемь секунд на сервере с NVMe-дисками и тридцатью двумя гигабайтами оперативной памяти.

А что если посмотреть на это иначе? Не как на техническую задачу — заменить один плагин другим, — а как на стратегическое решение о том, кому вы доверяете данные своего бизнеса? Когда я работал с промышленными компаниями, которые строили ERP-системы, ни одна из них не рассматривала всерьёз вариант «а давайте ключевую логику вынесем в сторонний модуль, который мы не контролируем и за который платим ежегодную подписку». Это было бы абсурдно. Но почему-то в мире WordPress это считается нормой. Компания с каталогом на двадцать тысяч товаров хранит все характеристики, все связи, все технические параметры в стороннем плагине. И платит за эту привилегию. И рискует потерять доступ при каждом обновлении. Может быть, пора перестать считать это нормой и начать строить инфраструктуру, которая принадлежит тебе?

Именно поэтому, когда мы проектировали модуль кастомных полей для COS WP Woo, я поставил задачу шире, чем просто «сделать как ACF, только бесплатно». Бесплатность — это приятный бонус, но не цель. Цель — сделать правильно. Сделать так, чтобы не болело через два года, когда база вырастет в три раза. И что ещё важнее — не оказаться в ситуации, когда ключевой функционал твоего бизнеса зависит от решений вендора, которого ты не контролируешь.

Почему wp_postmeta — это архитектурная ловушка

Давайте разберёмся, почему я так настаиваю на том, что хранить кастомные поля в wp_postmeta — это плохая идея, особенно для интернет-магазинов с большим каталогом. WordPress создавался как блоговая платформа. Таблица postmeta задумывалась для хранения дополнительной информации о записях — пара «ключ-значение» на каждую запись. Это работает прекрасно, когда у тебя сто статей и к каждой прикреплено три-четыре поля. Но когда у тебя шестнадцать тысяч товаров и к каждому — двадцать пять полей, ты получаешь четыреста тысяч строк только для прямых значений. А если там ещё repeater’ы с тремя элементами в каждом, умножай на три. Плюс технические строки, которые ACF использует для хранения ключей полей, порядка элементов в repeater’е, флагов. В итоге мы легко получаем миллион-полтора строк в одной таблице.

И вот тут начинается самое интересное. MySQL прекрасно справляется с миллионом строк — если ты правильно индексируешь и запрашиваешь данные. Но wp_postmeta имеет определённую структуру: post_id, meta_key, meta_value. Индекс стоит на post_id и meta_key. Когда ты ищешь «все товары, у которых вязкость = 5W-30 И температура вспышки больше двухсот И одобрение КАМАЗ = да», MySQL должен сделать несколько JOIN’ов таблицы postmeta на саму себя. Каждый мета-запрос — это отдельный JOIN. Три условия — три JOIN’а одной и той же таблицы в девятьсот мегабайт. И оптимизатор запросов начинает творить чудеса, но не те чудеса, которых ты ждёшь. Он пытается найти оптимальный план выполнения, перебирает варианты, иногда выбирает полный перебор таблицы — и ты получаешь запрос, который подвешивает сервер.

Мы измеряли. На нашем продакшн-сервере с Redis-кэшем и оптимизированным MySQL запрос с тремя мета-условиями на каталоге в шестнадцать тысяч товаров занимал от трёх до восьми секунд без кэша. С Redis — быстрее, но только пока кэш тёплый. А кэш инвалидируется при каждом обновлении товара. И когда у тебя идёт синхронизация с 1С, которая обновляет остатки и цены раз в час — кэш инвалидируется циклически. Получается замкнутый круг: ты ставишь Redis, чтобы ускорить мета-запросы, но синхронизация убивает кэш, и пользователи снова ждут по пять-восемь секунд на странице фильтрации. Это не теоретическая проблема, это ежедневная реальность любого WooCommerce-магазина с интеграцией с учётной системой.

Когда мы проектировали модуль CF в COS WP Woo, мы сразу заложили другую архитектуру хранения. Три кастомные таблицы: wpaic_cf_field_groups для групп полей, wpaic_cf_fields для описания полей, и wpaic_cf_values для значений. Таблица значений структурирована иначе — она знает о типах данных, о связях между полями, о вложенности. Она не хранит всё как text в meta_value, а использует типизированные колонки. Числовые значения хранятся как числа, даты как даты, JSON — как JSON. Это позволяет MySQL использовать нормальные индексы и нормальную оптимизацию. Тот же запрос с тремя условиями, который занимал восемь секунд в postmeta, на кастомных таблицах выполняется за двести-триста миллисекунд. Разница в двадцать-тридцать раз. И это не маркетинговая цифра — это замер на реальных данных, на реальном продакшн-сервере с реальной нагрузкой.

Есть ещё один неочевидный нюанс, который редко обсуждают. Колонка meta_value в wp_postmeta имеет тип LONGTEXT. Это означает, что индекс на этой колонке — частичный, только по первым символам. Когда ты делаешь мета-запрос с оператором «равно», MySQL может использовать индекс. Но когда тебе нужен LIKE или числовое сравнение — индекс бесполезен, потому что MySQL не знает, что в meta_value хранится число. Он вынужден конвертировать строку в число для каждой строки таблицы. На девятистах мегабайтах данных это означает полный перебор. И никакие настройки MySQL не помогут — проблема в самой архитектуре хранения, а не в конфигурации сервера. Мы проверяли это с помощью EXPLAIN на реальных запросах — MySQL честно показывал type: ALL, то есть полное сканирование таблицы.

Но вот в чём штука. Быстрое хранение — это только фундамент. Пользователю в админке WordPress всё равно, где физически лежат его данные. Ему важно, чтобы интерфейс работал удобно, чтобы поля создавались легко, чтобы repeater’ы не глючили, чтобы Gutenberg нормально отображал кастомный контент. И тут мы подходим к тому, что на самом деле составляет основную ценность ACF — не хранение данных, а интерфейс управления. И здесь нам нужно было не просто повторить то, что сделал ACF, а сделать шаг вперёд.

Двадцать типов полей и интерфейс, который не требует инструкций

Когда я анализировал, как наши клиенты используют ACF, я составил список типов полей, которые встречаются чаще всего. Текстовое поле, textarea, число, email, URL — это базовые вещи, без которых никуда. Select, checkbox, radio — тоже стандарт. Но дальше начинается более интересное: отношения между объектами, галерея изображений, дата и время, цветовой пикер, WYSIWYG-редактор, вложения файлов, true/false переключатели, Google Maps (хотя этот последний, честно говоря, нужен единицам). И вот эти «продвинутые» типы — они и делают кастомные поля по-настоящему мощным инструментом.

В модуле CF для COS WP Woo мы реализовали более двадцати типов полей. Я не буду превращать статью в техническую документацию и перечислять каждый, но хочу остановиться на нескольких, которые я считаю ключевыми для WooCommerce-магазинов и которые мы реализовали принципиально иначе, чем ACF.

CfRelationshipField — связи между объектами. Это то, чем пользуются почти все наши клиенты, и то, что в ACF работает, мягко говоря, не идеально на больших каталогах. Когда у тебя шестнадцать тысяч товаров и тебе нужно выбрать связанные, AJAX-поиск в ACF начинает тормозить. Ты вводишь три буквы, ждёшь секунду-две, результаты приходят, но ты не уверен, что нашёл все подходящие товары, потому что ACF ограничивает выдачу. Наша реализация CfRelationshipField использует индексированный поиск и пагинацию. Ты начинаешь вводить название товара — и результаты появляются мгновенно, даже на каталоге в двадцать тысяч позиций. Потому что поиск идёт не по meta_value через LIKE, а по нормальным полнотекстовым индексам. И результаты пагинированы — ты можешь пролистать все совпадения, а не надеяться, что нужный товар попал в первые двадцать.

CfGalleryField — галерея изображений. Ещё один больной вопрос. В ACF галерея работает через стандартный медиа-загрузчик WordPress, и это... терпимо. Но когда тебе нужно загрузить десять фотографий товара, отсортировать их в правильном порядке, обрезать одну, заменить другую — процесс превращается в последовательность кликов по модальным окнам. Открыл медиа-библиотеку, выбрал файл, закрыл, понял что нужно обрезать — снова открыл, нашёл файл, открыл редактор, обрезал, сохранил, закрыл. Наш CfGalleryField встроен прямо в метабокс: drag-and-drop загрузка файлов прямо в область поля, сортировка перетаскиванием, обрезка изображений без выхода из редактора товара. Это кажется мелочью, но когда контент-менеджер обрабатывает по пятьдесят товаров в день — каждый сэкономленный клик умножается на пятьдесят. За неделю набегает час-полтора чистого времени. За месяц — целый рабочий день. Я не люблю считать эффективность в минутах, но когда разница настолько очевидна — цифры говорят сами за себя.

А теперь про то, что отличает профессиональную работу с кастомными полями от любительской — Repeater и Flexible Content. Два типа полей, которые ACF долгое время держал за платной стеной, и которые, по сути, определяют, можешь ли ты построить на WordPress серьёзный каталог со сложной структурой данных или нет.

Repeater и Flexible Content: когда простых полей недостаточно

Знаете, что забавно? Когда я показываю новым клиентам список типов полей, они обычно говорят: «Ну, нам нужно только text и select, остальное — лишнее.» А через три месяца работы они используют relationship, gallery, repeater и flexible content, и спрашивают: «А есть ли тип поля для...» Потребности растут с пониманием возможностей. И поэтому для нас было принципиально важно реализовать все двадцать с лишним типов сразу, а не добавлять их постепенно. Потому что когда клиент понимает, что ему нужен repeater, а его нет — он уходит обратно на ACF. А вернуть его потом практически невозможно. Первые впечатления решают, и если в первый день использования человек не нашёл нужный тип поля — всё, второго шанса не будет.

Честно говоря, именно repeater — тот функционал, ради которого большинство людей покупают ACF Pro. Бесплатная версия ACF его не включает, и это главная причина, по которой сорок девять долларов в год уходят на подписку. Что такое repeater в контексте WooCommerce? Представьте карточку товара — моторного масла. У него есть допуски производителей автомобилей: BMW Longlife-01, Mercedes-Benz MB 229.5, Volkswagen VW 502.00. Каждый допуск — это не просто строка текста. Это объект с названием, номером, статусом (действующий или устаревший), ссылкой на документ подтверждения. И таких допусков у одного масла может быть от двух до пятнадцати. Как это хранить без repeater’а? Можно, конечно, запихнуть всё в одно текстовое поле через запятую. Но тогда ты не сможешь нормально фильтровать по допускам, не сможешь выводить их в структурированном виде, не сможешь проверять валидность. А можно создать пятнадцать отдельных полей «Допуск 1», «Допуск 2»... «Допуск 15» — но это уродливо и негибко.

Repeater решает эту проблему элегантно. Ты создаёшь группу подполей — «Название допуска», «Номер», «Статус», «Документ» — и указываешь, что эта группа повторяемая. В редакторе товара контент-менеджер видит кнопку «Добавить допуск», нажимает — появляется новый блок с четырьмя полями. Заполнил, нажал ещё раз — появился второй блок. Нужно поменять порядок — перетащил мышкой. Нужно удалить — нажал крестик. Всё интуитивно, всё работает. Менеджеру не нужно знать ничего о базе данных, о JSON, о мета-полях — он просто нажимает «Добавить» и заполняет форму.

Наш CfRepeaterField в COS WP Woo идёт дальше стандартного ACF-repeater’а в нескольких важных аспектах. В ACF данные repeater’а хранятся в postmeta как набор отдельных мета-полей: _допуски_0_название, _допуски_0_номер, _допуски_1_название, _допуски_1_номер, и так далее. Плюс отдельное поле _допуски, в котором хранится количество элементов. На товар с десятью допусками и четырьмя подполями — это сорок одна запись в postmeta только для одного repeater’а. Сорок одна строка в таблице, которая уже весит почти гигабайт. А если у тебя на товаре три repeater’а — один для допусков, один для технических характеристик, один для документации — ты легко получаешь сто двадцать строк в postmeta на один товар. Помножь на шестнадцать тысяч товаров и получишь почти два миллиона строк только от repeater’ов. Наша реализация хранит данные repeater’а как JSON-документ в одном поле типизированной таблицы. Все десять допусков со всеми подполями — одна запись вместо сорока одной. При этом поиск и фильтрация работают через JSON-функции MySQL, которые в версии 8.0 и выше достаточно быстры и поддерживают индексирование.

Но repeater — это ещё цветочки. Настоящая мощь начинается с Flexible Content. Представьте, что вам нужно создать не просто повторяемый набор одинаковых полей, а конструктор из разных блоков. На странице бренда, например, может быть: текстовый блок с описанием, блок с характеристиками в виде таблицы, блок с галереей продукции, блок с видео-обзором, блок с отзывами дилеров. Каждый из этих блоков имеет свою структуру полей — у текстового блока только WYSIWYG-редактор, у галереи — набор изображений с подписями, у таблицы характеристик — repeater с парами «параметр-значение». И пользователь должен иметь возможность добавлять любые блоки в любом порядке — как конструктор Lego. Это и есть Flexible Content.

CfFlexibleContentField в нашем модуле реализует именно этот сценарий. Ты определяешь набор «макетов» — каждый со своими полями. А в редакторе контент-менеджер выбирает нужный макет, добавляет его на страницу, заполняет поля. Может добавить ещё один такой же или другой. Может перетащить блоки, поменять порядок. Результат — полностью кастомная структура страницы, собранная из готовых компонентов, без единой строчки кода. И я считаю, что именно Flexible Content — это то, что превращает WordPress из блоговой платформы в полноценную систему управления контентом корпоративного уровня. Потому что без него каждая нестандартная страница требует вмешательства разработчика, а с ним — контент-менеджер справляется сам. Для B2B-компаний это критически важно: страницы брендов, каталоги продукции по отраслям, технические разделы — всё это должно обновляться регулярно, и каждый раз привлекать программиста просто нерентабельно.

И тут мы подходим к теме, которая стала для меня настоящим откровением — интеграция кастомных полей с Gutenberg. Когда я начинал этот проект, я был скептиком. Я считал Gutenberg неповоротливым и избыточным для WooCommerce. Но я ошибался, и вот почему.

Gutenberg-блоки и визуальные конструкторы правил

Знаете, что меня больше всего раздражало в ACF? То, что кастомные поля живут отдельно от контента. Ты открываешь редактор товара, наверху — Gutenberg с описанием, а где-то внизу, под пятью экранами прокрутки — метабоксы ACF с кастомными полями. Две отдельные вселенные, которые ничего не знают друг о друге. Контент-менеджер вынужден скроллить туда-сюда, помнить, что за что отвечает, и надеяться, что он не пропустил обязательное поле где-то в нижних метабоксах. Это не проблема архитектуры — это проблема пользовательского опыта. И для контент-менеджера, который работает с пятьюдесятью товарами в день, этот опыт определяет его продуктивность.

Я сталкивался с проектами, где люди изобретали собственные повторяемые поля на уровне темы — через JavaScript и AJAX в админке. Это работало, но каждое обновление WordPress или WooCommerce превращалось в лотерею. Самописный repeater мог сломаться из-за изменения в jQuery, из-за нового формата метабоксов, из-за обновления TinyMCE-редактора. И каждый раз разработчик тратил часы на починку того, что по-хорошему должно быть стандартным функционалом платформы. Кастомные поля с повторяемыми блоками — это не роскошь, это базовая инфраструктура для любого серьёзного каталога. И то, что WordPress до сих пор не включает это в ядро, — одна из главных причин существования целой индустрии плагинов вокруг кастомных полей.

Когда мы проектировали модуль CF, я поставил задачу: каждая группа кастомных полей должна автоматически получать блочное представление в Gutenberg. Не «можно настроить через отдельный плагин», не «требует дополнительной разработки», а автоматически. Создал группу полей — в Gutenberg сразу появился соответствующий блок. Это реализовано через компонент CfBlocks, который динамически регистрирует Gutenberg-блоки на основе конфигурации групп полей. Технически это работает так: при сохранении группы полей PHP-код генерирует регистрацию блока через register_block_type(), а React-компонент рендерит форму ввода прямо в редакторе.

Как это выглядит на практике? Допустим, ты создал группу полей «Технические характеристики масла» с полями: вязкость, температура вспышки, температура застывания, допуски производителей, область применения. После сохранения группы в библиотеке блоков Gutenberg появляется блок «Технические характеристики масла». Контент-менеджер может вставить его прямо в контент товара, рядом с описанием, между абзацами — где угодно. И поля отображаются в контексте контента, а не в отдельном метабоксе внизу страницы. Это принципиально меняет рабочий процесс. Вместо «заполни описание наверху, потом прокрути вниз и заполни характеристики» получается единый поток — описание, потом блок с характеристиками, потом ещё описание, потом блок с допусками. Всё в одном месте, в правильном порядке.

Конечно, мы сохранили и классический подход с метабоксами. Не все пользователи готовы к Gutenberg, и не все сценарии требуют блочного представления. Есть компании, где редактор WordPress настроен на классический режим, и менять привычки людей — не наша задача. Если ты привык к метабоксам — пожалуйста, они работают ровно так же, как в ACF. Но возможность перейти на блоки — она есть, и она не требует дополнительных усилий. Это вопрос готовности команды, а не вопрос технических ограничений.

Теперь про визуальные конструкторы — одну из тех вещей, которыми я горжусь больше всего в этом модуле. CfLocationRulesBuilder — это визуальный конструктор, который определяет, где именно будут отображаться твои кастомные поля. В ACF это работает через выпадающие списки: «Показать эту группу полей, если тип записи равен product И категория товара равна masla». Выпадашки, мелкий текст, неочевидная логика AND/OR, которая запутывает даже опытных пользователей. Наш конструктор — это визуальный интерфейс, построенный на React, где правила представлены как карточки. Ты буквально видишь логику: этот блок условий соединён через И, а с другим блоком — через ИЛИ. Перетаскиваешь карточки, меняешь условия, и сразу понимаешь, что получится. Я показывал это нескольким контент-менеджерам, которые до этого месяцами боялись трогать настройки ACF — они разобрались за пять минут. Без документации, без обучения.

CfConditionalLogicBuilder идёт ещё дальше. Это условная логика на уровне отдельных полей: показывать или скрывать поле в зависимости от значений других полей. Например, если тип масла — «моторное», показать поле «допуски двигателей». Если тип — «гидравлическое», показать поле «класс ISO». Если выбран производитель «Shell», показать дополнительное поле «линейка Shell». Зачем это нужно? Затем, что карточка товара для моторного масла и для гидравлического масла — это две разные карточки с разным набором полей. И вместо того чтобы показывать менеджеру все тридцать полей сразу (половина из которых неприменима), условная логика показывает только релевантные. Меньше полей — меньше ошибок — быстрее заполнение. В ACF это есть, но реализовано как простой список условий без визуальной обратной связи. Наш builder показывает связи между полями визуально — ты видишь, какие поля от каких зависят, где какая логика применяется. И главное — ты можешь протестировать условия прямо в конструкторе, не переходя в редактор товара.

Я долго думал, стоит ли тратить время на эти визуальные конструкторы. Код — он и есть код, можно было бы просто дать конфигурационный файл или программный API. Но потом я вспомнил, сколько раз мне звонили клиенты с вопросом «а почему у меня это поле не показывается на такой-то категории?» — и ответ всегда был один: неправильно настроены правила расположения, потому что интерфейс настройки был непонятным. Визуальный конструктор не решает все проблемы мира, но он решает вот эту конкретную — люди перестают ошибаться при настройке, и мне перестают звонить. А это, поверьте, стоит тех недель разработки, которые мы потратили на эти компоненты.

Слой совместимости и экосистема: почему это не просто ещё один плагин

Когда мы начали проектировать модуль CF, я сразу понимал, что девяносто процентов потенциальных пользователей — это люди, которые уже используют ACF. И они не готовы просто «взять и переключиться». У них шаблоны темы, которые вызывают get_field() и the_field(). У них плагины, которые интегрируются с ACF через его API. У них кастомный код в functions.php, который использует ACF-хуки. Переезд без слоя совместимости означает: переписать весь фронтенд сайта, все шаблоны, все интеграции. Для сайта с шестнадцатью тысячами товаров это — месяцы работы и огромный риск что-то сломать. Это не тот риск, на который пойдёт разумный бизнес, даже если новый инструмент объективно лучше.

Поэтому мы реализовали register_cf_compat() — слой совместимости, который перехватывает все стандартные функции ACF и маршрутизирует их на наш модуль. Функции get_field(), the_field(), get_sub_field(), have_rows() — всё это работает. Вы можете деактивировать ACF, активировать наш модуль, и ваши шаблоны продолжат работать без изменения ни одной строчки кода. Слой совместимости работает на уровне функций-обёрток. Когда ваш шаблон вызывает get_field с ключом поля и ID поста, наша обёртка определяет, какой группе полей принадлежит поле, обращается к таблице wpaic_cf_values вместо postmeta и возвращает значение в том же формате, который вернул бы ACF. Для repeater’ов логика сложнее — have_rows() инициализирует внутренний итератор, the_row() двигает указатель, get_sub_field() извлекает значение из текущей строки. Вся эта механика воспроизведена точно, потому что мы понимаем: одно сломанное место в шаблоне — и клиент возвращается на ACF навсегда.

Но я хочу быть честным: стопроцентная совместимость — это утопия. Если ваш код использует внутренние классы ACF напрямую, или если вы привязались к специфическим хукам, которых нет в документации — могут быть нюансы. Мы покрыли публичный API, который описан в документации и который используется в девяноста пяти процентах случаев. Оставшиеся пять — экзотика, которую нужно разбирать индивидуально. Для подавляющего большинства сайтов миграция выглядит так: установил COS WP Woo, активировал модуль CF, импортировал группы полей из ACF, деактивировал ACF, проверил — работает.

Миграция данных — это отдельный процесс. Слой совместимости позволяет вашему сайту работать сразу, но данные физически всё ещё лежат в postmeta. Чтобы получить выигрыш в производительности, нужно перенести данные из postmeta в наши кастомные таблицы. Это отдельная операция, которая выполняется фоновым процессом через Action Scheduler — без остановки сайта, без даунтайма. На шестнадцати тысячах товаров миграция занимает примерно двадцать-тридцать минут. После миграции вы получаете и совместимость со старым кодом, и производительность нового хранилища. А старые данные в postmeta можно оставить как бэкап или удалить, когда убедитесь, что всё работает корректно.

А что с REST API? Это вопрос, который задают разработчики, и он абсолютно законный. В современных проектах фронтенд всё чаще отделён от бэкенда — headless-архитектура, мобильные приложения, интеграции с внешними системами. Доступ к кастомным полям через API — не роскошь, а необходимость. ACF Pro предоставляет REST API, но с оговорками: нужно явно включить поддержку API для каждой группы полей, формат ответа не всегда предсказуем, а для repeater’ов данные приходят в плоском виде, который нужно самостоятельно собирать в структуру. Наш модуль предоставляет полноценный REST API с самого начала. Один контроллер с полным CRUD для групп полей и значений. Значения полей доступны в nested-формате: repeater возвращается как массив объектов, Flexible Content — как массив блоков с указанием типа макета. Никакой ручной сборки данных на стороне клиента. Формат предсказуем, документирован и не меняется между версиями.

Девять сервисов обеспечивают работу модуля за кулисами. CfFieldGroupService управляет группами полей. CfFieldService — отдельными полями и их конфигурациями. CfFieldValueService — чтением и записью значений. CfRelationshipService — связями между объектами. Шесть React-страниц в админке дают полный контроль: список групп полей, редактор группы, конструктор правил расположения, конструктор условной логики, миграция с ACF, настройки модуля. Всё построено на стандартных компонентах WordPress, поэтому интерфейс выглядит нативно и не конфликтует с другими плагинами.

Я знаю, что многие скажут: «Зачем это всё, если есть Carbon Fields, Pods, Meta Box?» Справедливый вопрос, и я отвечу на него прямо. Carbon Fields — отличный бесплатный плагин, но у него нет визуального интерфейса для создания полей в админке, всё через код. Для разработчика это нормально, для контент-менеджера — тупик. Pods — мощный фреймворк, но избыточный для большинства задач и с крутой кривой обучения, которая отпугивает даже опытных вордпрессников. Meta Box — хорош, но платный для продвинутых функций, и снова завязан на свою экосистему из десятка аддонов. Каждый из них решает часть проблемы, но ни один не решает ту, которую я описал в начале: зависимость от стороннего вендора для критически важного функционала.

Но самое главное — ни один из этих плагинов не интегрирован с вашим поиском, вашей 1С, вашим B2B-модулем. Модуль CF в COS WP Woo — это часть экосистемы. Он работает вместе с модулем 1С-интеграции, который маппит атрибуты товаров из 1С:Управление Торговлей на кастомные поля. Он работает с модулем поиска, который индексирует кастомные поля для полнотекстового поиска через Typesense. Он работает с модулем B2B, который использует кастомные поля для настройки групповых прав доступа. Когда менеджер добавляет новый допуск в repeater, наш модуль записывает данные в wpaic_cf_values, обновляет поисковый индекс Typesense через хуки модуля поиска, и проверяет — если допуск относится к КАМАЗ или МАЗ, автоматически добавляет товар в категорию «Для российской техники». Одно действие менеджера — три результата в разных системах.

Я не говорю, что ACF — плохой продукт. Он отличный. Он заложил стандарт для кастомных полей в WordPress, и за это ему огромная благодарность. Но мир изменился. WordPress стал платформой для серьёзного e-commerce, для B2B-порталов, для интеграций с ERP-системами. И требования к кастомным полям выросли настолько, что подход «всё в postmeta, всё через мета-запросы» просто перестал масштабироваться. Это не вина ACF — это эволюция задач. Я часто привожу аналогию с транспортом. ACF — это надёжный седан, который отлично возит по городу. Но когда тебе нужно перевезти двадцать тонн груза из Челябинска в Москву — ты берёшь фуру. Не потому что седан плохой, а потому что задача другая.

Что касается стоимости — давайте посчитаем честно. ACF Pro стоит от сорока девяти до двухсот сорока девяти долларов в год, в зависимости от количества сайтов. За пять лет — от двухсот сорока пяти до тысячи двухсот сорока пяти долларов только за кастомные поля. Добавь к этому WooCommerce-расширения, которые нужны для фильтрации по мета-полям, для B2B-прайсинга на основе мета-полей, для поисковой индексации мета-полей — и итоговая стоимость владения вырастает в три-четыре раза. COS WP Woo включает всё это в одной лицензии. Кастомные поля, поиск, B2B, 1С — всё работает вместе, всё поддерживается одной командой, всё обновляется синхронно. Не нужно проверять совместимость пяти плагинов при каждом обновлении. Не нужно писать хуки для связки одного плагина с другим. Не нужно молиться, чтобы апдейт ACF не сломал интеграцию с поисковым плагином.

Но я не хочу, чтобы эта статья звучала как рекламный буклет. Поэтому скажу честно о том, чего наш модуль пока не делает. У нас нет отдельного плагина для фронтенда, который генерировал бы формы на основе кастомных полей — для этого есть наш модуль форм. У нас нет блоков для Elementor или WPBakery — мы делаем ставку на Gutenberg, и если ваш сайт полностью построен на Elementor, интеграция будет через шорткоды, а не через нативные виджеты. И наш слой совместимости с ACF покрывает не сто процентов — я уже говорил про экзотические сценарии. Это честная позиция, и я предпочитаю сказать об ограничениях открыто, чем потом разбираться с разочарованными пользователями.

Вот что мне кажется самым главным. Когда ты выбираешь инструмент для хранения и управления данными своего бизнеса — а кастомные поля товаров это и есть данные твоего бизнеса — ты должен думать на горизонте пяти-десяти лет. Будет ли ACF существовать через пять лет? Скорее всего, да. Но будет ли он стоить столько же? Будет ли он поддерживать ту же архитектуру? Будет ли WP Engine развивать его в том направлении, которое нужно твоему бизнесу? Этого никто не знает. А когда у тебя шестнадцать тысяч товаров и таблица postmeta весит гигабайт — каждый неправильный ответ на эти вопросы обходится очень дорого. Я выбираю предсказуемость — инструмент, который я контролирую, который хранит данные эффективно, который интегрирован с моей экосистемой и который не зависит от стратегических решений стороннего вендора.

Есть одна мысль, которую я хочу оставить напоследок. Мы живём в эпоху, когда данные — это главный актив бизнеса. Характеристики ваших товаров, их связи, их технические параметры — это не просто поля в базе данных. Это знания, которые ваша компания накапливала годами. Вязкость каждого масла, совместимость с каждым типом двигателя, рекомендации для каждой отрасли — за каждым заполненным полем стоят часы работы технических специалистов. И эти знания хранятся в инструменте, который вы не контролируете, в формате, который не оптимален для ваших задач, в таблице, которая весит почти гигабайт и тормозит при каждом запросе. Может быть, пора задуматься о том, как хранить эти знания правильно? Пора выбрать инструмент, который уважает ваши данные так же, как вы уважаете своих клиентов.

И если вы сейчас сидите перед экраном с открытым письмом о продлении подписки ACF Pro и думаете «а что, если не продлевать?» — попробуйте задать себе три вопроса. Сколько стоит ваша зависимость от ACF в деньгах — не только подписка, но и время на поддержку интеграций? Сколько весит ваша таблица postmeta и как это влияет на скорость сайта? И есть ли у вас план Б на случай, если ACF изменит API или ценовую политику? Если хотя бы один из ответов вас не устраивает — значит, пора думать о переменах.

Попробуйте COS WP Woo — четырнадцать дней бесплатно. Установите, активируйте модуль кастомных полей, импортируйте одну группу из ACF, поработайте с ней неделю. Почувствуйте разницу в скорости запросов, попробуйте визуальный конструктор правил, посмотрите, как repeater работает в Gutenberg-блоке. И потом решайте — своими руками, на своих данных, без маркетинговых обещаний. Потому что лучший аргумент — это ваш собственный опыт.