$3d
Patterns WooCommerce pour projets WordPress e-commerce. À lire avec wordpress-patterns (même plugin) qui couvre la couche WordPress de base.
Version minimum : WooCommerce 8.2+ (HPOS stable). Tout code antérieur suppose l'ancien stockage post-based et doit être migré.
HPOS (High-Performance Order Storage) est le nouveau stockage des orders dans des tables dédiées (wp_wc_orders, wp_wc_order_addresses, etc.) au lieu de wp_posts + wp_postmeta. Tout plugin tiers doit déclarer sa compatibilité sinon WooCommerce refuse l'activation de HPOS.
add_action('before_woocommerce_init', function () {
if (class_exists('\Automattic\WooCommerce\Utilities\FeaturesUtil')) {
\Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility(
'custom_order_tables',
__FILE__,
true
);
}
});
À ajouter dans le fichier principal du plugin, avant toute autre logique qui touche les orders.
Comprendre l'ordre dans lequel les actions/filters se déclenchent est crucial :
Utilisateur ajoute au cart
→ woocommerce_add_to_cart_validation
→ woocommerce_add_to_cart (après ajout réussi)
Utilisateur va au cart
→ woocommerce_before_cart
→ woocommerce_cart_contents
Utilisateur va au checkout
→ woocommerce_before_checkout_form
→ woocommerce_checkout_fields (filter pour modifier les champs)
→ woocommerce_checkout_before_customer_details
→ woocommerce_review_order_before_payment
Utilisateur soumet la commande
→ woocommerce_checkout_process (validation)
→ woocommerce_checkout_create_order (avant sauvegarde)
→ woocommerce_checkout_order_processed (après sauvegarde)
→ woocommerce_new_order
Paiement traité
→ woocommerce_payment_complete
→ woocommerce_order_status_{from}_to_{to}
→ woocommerce_order_status_changed
→ woocommerce_order_status_processing (statut final typique)
$product = wc_get_product($product_id);
if (!$product instanceof WC_Product) {
return;
}
echo $product->get_name();
echo $product->get_price_html();
echo $product->is_in_stock() ? 'In stock' : 'Out of stock';
// 1. Enregistrer le type
add_filter('product_type_selector', function ($types) {
$types['subscription_box'] = __('Subscription Box', 'atum');
return $types;
});
// 2. Créer la classe
class WC_Product_Subscription_Box extends WC_Product {
public function get_type() {
return 'subscription_box';
}
public function get_frequency() {
return $this->get_meta('_sb_frequency', true);
}
}
// 3. Mapper le type au class
add_filter('woocommerce_product_class', function ($classname, $product_type) {
if ($product_type === 'subscription_box') {
return 'WC_Product_Subscription_Box';
}
return $classname;
}, 10, 2);
// Préférer wc_get_products() à WP_Query pour HPOS-compat
$products = wc_get_products([
'status' => 'publish',
'limit' => 10,
'orderby' => 'date',
'order' => 'DESC',
'category' => ['featured'],
'tax_query' => [
[
'taxonomy' => 'product_visibility',
'field' => 'name',
'terms' => 'exclude-from-catalog',
'operator' => 'NOT IN',
],
],
]);
foreach ($products as $product) {
echo $product->get_name() . "\n";
}
add_action('woocommerce_before_cart', function () {
if (WC()->cart->get_cart_contents_count() >= 3) {
wc_print_notice(
__('Free shipping unlocked!', 'atum'),
'success'
);
}
});
add_action('woocommerce_cart_calculate_fees', function () {
if (is_admin() && !defined('DOING_AJAX')) {
return;
}
$total = WC()->cart->get_subtotal();
if ($total < 50) {
WC()->cart->add_fee(__('Small order fee', 'atum'), 2.5, true);
}
});
add_filter('woocommerce_checkout_fields', function ($fields) {
$fields['billing']['billing_company_vat'] = [
'label' => __('VAT number', 'atum'),
'placeholder' => 'FR12345678901',
'required' => false,
'class' => ['form-row-wide'],
'priority' => 35,
];
return $fields;
});
add_action('woocommerce_checkout_process', function () {
if (!empty($_POST['billing_company_vat'])) {
$vat = sanitize_text_field($_POST['billing_company_vat']);
if (!preg_match('/^[A-Z]{2}[0-9A-Z]{2,12}$/', $vat)) {
wc_add_notice(__('Invalid VAT number format.', 'atum'), 'error');
}
}
});
add_action('woocommerce_checkout_create_order', function ($order, $data) {
if (!empty($_POST['billing_company_vat'])) {
$order->update_meta_data(
'_billing_company_vat',
sanitize_text_field($_POST['billing_company_vat'])
);
}
}, 10, 2);
Note HPOS : utiliser les méthodes de WC_Order (update_meta_data, get_meta, save) au lieu de update_post_meta — ce dernier ne fonctionne pas avec HPOS.
add_filter('woocommerce_payment_gateways', function ($gateways) {
$gateways[] = 'WC_Gateway_MyProvider';
return $gateways;
});
add_action('plugins_loaded', function () {
if (!class_exists('WC_Payment_Gateway')) {
return;
}
class WC_Gateway_MyProvider extends WC_Payment_Gateway {
public function __construct() {
$this->id = 'my_provider';
$this->method_title = __('My Provider', 'atum');
$this->method_description = __('Accept payments via My Provider', 'atum');
$this->has_fields = true;
$this->supports = ['products', 'refunds'];
$this->init_form_fields();
$this->init_settings();
$this->title = $this->get_option('title');
$this->description = $this->get_option('description');
$this->api_key = $this->get_option('api_key');
add_action('woocommerce_update_options_payment_gateways_' . $this->id, [$this, 'process_admin_options']);
}
public function init_form_fields() {
$this->form_fields = [
'enabled' => [
'title' => __('Enable/Disable', 'atum'),
'type' => 'checkbox',
'label' => __('Enable My Provider', 'atum'),
'default' => 'yes',
],
'title' => [
'title' => __('Title', 'atum'),
'type' => 'text',
'description' => __('Payment method title on the checkout page', 'atum'),
'default' => __('Pay with My Provider', 'atum'),
],
'api_key' => [
'title' => __('API Key', 'atum'),
'type' => 'password',
'description' => __('Your My Provider API key', 'atum'),
],
];
}
public function process_payment($order_id) {
$order = wc_get_order($order_id);
// Appel API au provider
$response = wp_remote_post('https://api.myprovider.com/charge', [
'headers' => [
'Authorization' => 'Bearer ' . $this->api_key,
'Content-Type' => 'application/json',
],
'body' => wp_json_encode([
'amount' => (int) ($order->get_total() * 100),
'currency' => $order->get_currency(),
'order_id' => $order->get_id(),
]),
'timeout' => 30,
]);
if (is_wp_error($response)) {
wc_add_notice(__('Payment failed. Please try again.', 'atum'), 'error');
return ['result' => 'failure'];
}
$body = json_decode(wp_remote_retrieve_body($response), true);
if (empty($body['success'])) {
wc_add_notice(
sprintf(__('Payment error: %s', 'atum'), $body['error'] ?? 'unknown'),
'error'
);
return ['result' => 'failure'];
}
$order->payment_complete($body['transaction_id']);
$order->add_order_note(sprintf(__('Payment via My Provider. Transaction: %s', 'atum'), $body['transaction_id']));
WC()->cart->empty_cart();
return [
'result' => 'success',
'redirect' => $this->get_return_url($order),
];
}
}
});
PSD2/SCA : Pour l'Europe, implémenter 3D-Secure via redirection vers le provider qui gère le challenge bancaire. Ne jamais contourner SCA.
add_filter('woocommerce_shipping_methods', function ($methods) {
$methods['express_courier'] = 'WC_Shipping_Express_Courier';
return $methods;
});
add_action('woocommerce_shipping_init', function () {
if (!class_exists('WC_Shipping_Method')) {
return;
}
class WC_Shipping_Express_Courier extends WC_Shipping_Method {
public function __construct($instance_id = 0) {
$this->id = 'express_courier';
$this->instance_id = absint($instance_id);
$this->method_title = __('Express Courier', 'atum');
$this->method_description = __('Next-day delivery via our courier partner', 'atum');
$this->supports = ['shipping-zones', 'instance-settings'];
$this->init();
}
public function init() {
$this->init_form_fields();
$this->init_settings();
$this->title = $this->get_option('title');
$this->cost = (float) $this->get_option('cost', 10);
add_action('woocommerce_update_options_shipping_' . $this->id, [$this, 'process_admin_options']);
}
public function init_form_fields() {
$this->instance_form_fields = [
'title' => [
'title' => __('Method title', 'atum'),
'type' => 'text',
'description' => __('Name shown to the customer', 'atum'),
'default' => __('Express (next day)', 'atum'),
],
'cost' => [
'title' => __('Cost', 'atum'),
'type' => 'number',
'description' => __('Flat fee', 'atum'),
'default' => '10',
],
];
}
public function calculate_shipping($package = []) {
$this->add_rate([
'id' => $this->get_rate_id(),
'label' => $this->title,
'cost' => $this->cost,
]);
}
}
});
# GET products avec basic auth (consumer key + secret)
curl -u ck_xxx:cs_xxx \
"https://mysite.com/wp-json/wc/v3/products?per_page=10"
# GET products pour afficher sur un frontend headless
curl "https://mysite.com/wp-json/wc/store/v1/products"
# Cart et checkout sont également disponibles
curl "https://mysite.com/wp-json/wc/store/v1/cart"
Utiliser la Store API pour Next.js headless :
// lib/wc-store.ts
export async function getProducts() {
const res = await fetch(`${process.env.WC_URL}/wp-json/wc/store/v1/products?per_page=20`, {
next: { revalidate: 60 },
})
return res.json()
}
Créer un webhook via l'admin WooCommerce (Settings → Advanced → Webhooks) ou programmatically :
$webhook = new WC_Webhook();
$webhook->set_name('Order created to external system');
$webhook->set_topic('order.created');
$webhook->set_delivery_url('https://my-backend.com/webhooks/woocommerce');
$webhook->set_secret(wp_generate_password(32, false));
$webhook->set_status('active');
$webhook->save();
Vérifier la signature côté récepteur :
// Next.js webhook handler
import crypto from 'node:crypto'
export async function POST(request: Request) {
const rawBody = await request.text()
const signature = request.headers.get('x-wc-webhook-signature') ?? ''
const expected = crypto
.createHmac('sha256', process.env.WC_WEBHOOK_SECRET!)
.update(rawBody)
.digest('base64')
if (expected !== signature) {
return new Response('invalid signature', { status: 401 })
}
const payload = JSON.parse(rawBody)
// ...
}
get_post_meta($order_id, ...) sur un order → ne fonctionne pas avec HPOS, utiliser $order->get_meta()wp_insert_post pour créer une commande → utiliser wc_create_order()$_POST lu sans nonce dans un hook checkout → CSRFwc_update_product_stock() ou laisser WC gérerorder.created, order.updated, product.updatedno_found_rows => true si non utilisée)wordpress-patterns (dans ce plugin)wordpress-expert (dans ce plugin)cms-headless-architecture + command /cms-compareshopify-expert (dans ce plugin)security-reviewerdatabase-reviewer pour les slow queries