Будь-яка кастомізація WooCommerce починається з wc_get_template(). Функція отримує ім'я шаблону й через wc_locate_template() шукає його спершу в {дочірня тема}/woocommerce/{шлях}, потім у {батьківська тема}/woocommerce/{шлях}, і тільки якщо не знайшла, бере файл із plugins/woocommerce/templates/. Звідси й вимога копіювати зі збереженням відносного шляху: так влаштований пошук. templates/cart/cart-totals.php мусить стати themes/mytheme/woocommerce/cart/cart-totals.php, інакше ваш файл ніхто не побачить. У шапці кожного шаблону стоїть @version, а сторінка WooCommerce → Статус порівнює її з версією в плагіні й показує список копій, які відстали. Інших вбудованих запобіжників проти тихо зламаного магазину після апдейту немає, тож заглядайте туди після кожного мінорного оновлення.
Копія шаблону дорога в супроводі, тому спершу шукають хук. Розмітка каталогу й картки товару майже вся зібрана з actions: woocommerce_before_shop_loop_item, woocommerce_single_product_summary, woocommerce_after_add_to_cart_button. Переставити блоки можна remove_action + add_action з іншим пріоритетом, не торкаючись файлів. У кошику й оформленні підхід той самий, набір хуків інший: woocommerce_before_calculate_totals дає змінити ціну позиції, woocommerce_cart_calculate_fees додає доплату або знижку окремим рядком, woocommerce_package_rates фільтрує методи доставки, woocommerce_checkout_create_order дозволяє дописати дані в замовлення до збереження. Різниця, на якій легко обпектися: woocommerce_cart_calculate_fees виконується на кожному перерахунку totals, а woocommerce_add_cart_item_data лише один раз, при додаванні в кошик. Порахуєте доплату в другому - отримаєте суму, яка не реагує на зміну кількості.
Власні поля товару й власні поля замовлення роблять по-різному. Для товару це хелпери woocommerce_wp_text_input() і сусідні, повішені на woocommerce_product_options_general_product_data, зі збереженням на woocommerce_admin_process_product_object, куди приходить уже готовий WC_Product. Із замовленнями складніше. Після появи HPOS (High-Performance Order Storage, типово увімкнене для нових інсталяцій з WooCommerce 8.2) даних у wp_postmeta просто немає: замовлення лежать у wp_wc_orders і wp_wc_orders_meta. Код, який робить get_post_meta($order_id, '_gift_note', true), повертає порожній рядок і не падає, тому баг знаходять уже на проді. Працює тільки CRUD: $order->get_meta(), $order->update_meta_data(), $order->save(). Плагін ще має заявити сумісність через Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility('custom_order_tables', __FILE__, true) на хуку before_woocommerce_init, інакше WooCommerce покаже попередження й у магазина не буде можливості увімкнути HPOS.
Найсильніше старі статті розходяться з реальністю на checkout. Класична сторінка це shortcode [woocommerce_checkout] плюс шаблони checkout/form-*.php, де форма збирається на PHP і все кастомізується фільтром woocommerce_checkout_fields. Блоковий checkout, типовий для нових магазинів з WooCommerce 8.3, рендериться React'ом, а дані ходять через Store API, тому на нього не впливають ні ці фільтри, ні копії шаблонів. Власні поля там реєструють через woocommerce_register_additional_checkout_field() (з'явилось у WooCommerce 8.9) з локаціями contact, address або order, валідують фільтром woocommerce_validate_additional_field, а в замовлення дописують на woocommerce_store_api_checkout_update_order_from_request. Перевірити, який режим у клієнта, можна за вмістом сторінки оформлення: shortcode чи блок woocommerce/checkout.
Вирішує тут шар, на якому написаний код. Усе, що живе в WC_Cart, WC_Order і класах доставки та оплати, працює однаково в обох режимах, бо це серверна логіка: комісії, тарифи, статуси, листи, інтеграції. Все, що стосується форми й верстки checkout, доводиться писати двічі або свідомо лишатись на shortcode. Для магазину, який уже їде на класичному оформленні з десятком правок, міграція на блоки це окремий проєкт із переписуванням, а не галочка в налаштуваннях, і оцінювати його треба саме так.
// 1. Поле товару: вкладка «Загальні» в картці товару
add_action('woocommerce_product_options_general_product_data', function (): void {
woocommerce_wp_text_input([
'id' => '_engraving_fee',
'label' => 'Доплата за гравіювання, грн',
'type' => 'number',
'custom_attributes' => ['step' => '0.01', 'min' => '0'],
]);
});
// Зберігаємо через CRUD: об'єкт уже готовий, save() викличе WooCommerce
add_action('woocommerce_admin_process_product_object', function (WC_Product $product): void {
$fee = wc_clean(wp_unslash($_POST['_engraving_fee'] ?? ''));
$product->update_meta_data('_engraving_fee', wc_format_decimal($fee));
});
// 2. Кошик: доплата рахується на КОЖНОМУ перерахунку totals,
// тому реагує на зміну кількості й видалення позиції
add_action('woocommerce_cart_calculate_fees', function (WC_Cart $cart): void {
if (is_admin() && ! wp_doing_ajax()) {
return;
}
$sum = 0.0;
foreach ($cart->get_cart() as $item) {
if (! empty($item['engraving'])) {
$sum += (float) $item['data']->get_meta('_engraving_fee') * $item['quantity'];
}
}
if ($sum > 0) {
$cart->add_fee('Гравіювання', $sum, true); // true = оподатковувана
}
});
// 3. Замовлення: при HPOS даних у wp_postmeta немає, пишемо в об'єкт
add_action('woocommerce_checkout_create_order', function (WC_Order $order): void {
$order->update_meta_data('_gift_note', sanitize_textarea_field(wp_unslash($_POST['gift_note'] ?? '')));
});
// Для блокового checkout той самий запис вішають на інший хук:
// woocommerce_store_api_checkout_update_order_from_request($order, $request)
Назвіть сходинку пріоритетів: налаштування → хук → фільтр рядка/ціни → і тільки потім копія шаблону. Окремо згадайте, що для блокового checkout ваш звичний набір фільтрів не працює, і покажіть, що знаєте `woocommerce_register_additional_checkout_field()` та `woocommerce_store_api_checkout_update_order_from_request`. Це та деталь, яка відрізняє людину з бойовим магазином від людини з туторіалу 2019 року.