*/
namespace App\WooCommerce\Extensions;
class StorefrontExtensionManager {
private array $priority_map = [
'early' => 5,
'standard' => 10,
'late' => 15,
'critical' => 20
];
public function __construct() {
$this->register_catalog_hooks();
$this->register_product_hooks();
$this->register_cart_hooks();
$this->register_checkout_hooks();
}
private function register_catalog_hooks(): void {
add_action(
'woocommerce_before_shop_loop',
[$this, 'inject_category_banner'],
$this->priority_map['early']
);
add_filter(
'loop_shop_per_page',
[$this, 'adjust_catalog_pagination'],
$this->priority_map['standard'],
1
);
}
private function register_product_hooks(): void {
add_action(
'woocommerce_single_product_summary',
[$this, 'render_inventory_alert'],
$this->priority_map['late']
);
add_filter(
'woocommerce_product_single_add_to_cart_text',
[$this, 'modify_purchase_label'],
$this->priority_map['standard'],
1
);
}
private function register_cart_hooks(): void {
add_filter(
'woocommerce_cart_item_name',
[$this, 'append_custom_meta_to_cart'],
$this->priority_map['standard'],
2
);
add_action(
'woocommerce_cart_collaterals',
[$this, 'display_cross_sell_promo'],
$this->priority_map['late']
);
}
private function register_checkout_hooks(): void {
add_filter(
'woocommerce_checkout_fields',
[$this, 'optimize_checkout_form'],
$this->priority_map['standard'],
1
);
add_action(
'woocommerce_checkout_process',
[$this, 'validate_custom_checkout_data'],
$this->priority_map['critical']
);
}
// --- Handler Implementations ---
public function inject_category_banner(): void {
if (!is_product_category()) return;
$category = get_queried_object();
$description = $category->description ?? '';
if (empty($description)) return;
echo '<div class="catalog-intro-banner">';
echo wp_kses_post($description);
echo '</div>';
}
public function adjust_catalog_pagination(int $current_count): int {
return is_admin() ? $current_count : 24;
}
public function render_inventory_alert(): void {
global $product;
if (!$product instanceof \WC_Product) return;
$stock = $product->get_stock_quantity();
if ($stock === null || $stock > 10) return;
printf(
'<p class="low-stock-notice" style="color: #d63638; font-weight: 600;">%s</p>',
esc_html(sprintf('Only %d units remaining in warehouse.', $stock))
);
}
public function modify_purchase_label(string $default_text): string {
return 'Proceed to Checkout';
}
public function append_custom_meta_to_cart(string $product_name, \WC_Product $product): string {
$custom_sku = $product->get_sku();
if (empty($custom_sku)) return $product_name;
return sprintf(
'%s <span class="cart-sku-badge">SKU: %s</span>',
$product_name,
esc_html($custom_sku)
);
}
public function display_cross_sell_promo(): void {
if (WC()->cart->is_empty()) return;
echo '<div class="promo-banner">Free shipping on orders over $50</div>';
}
public function optimize_checkout_form(array $fields): array {
if (isset($fields['billing']['billing_company'])) {
$fields['billing']['billing_company']['required'] = false;
$fields['billing']['billing_company']['class'] = ['form-field-wide'];
}
return $fields;
}
public function validate_custom_checkout_data(): void {
$delivery_date = sanitize_text_field(wp_unslash($_POST['delivery_date'] ?? ''));
if (!empty($delivery_date) && !strtotime($delivery_date)) {
wc_add_notice('Please provide a valid delivery date.', 'error');
}
}
}
// Initialize
add_action('plugins_loaded', function() {
new StorefrontExtensionManager();
});
*Architecture Decisions & Rationale*
- **Object-Oriented Encapsulation:** Wrapping hooks in a class prevents global function namespace pollution and enables dependency injection for future extensions.
- **Priority Mapping:** Centralizing priority values in `$priority_map` ensures consistent execution ordering across all contexts. Early hooks (`5`) handle structural injections, standard (`10`) manages default transformations, and late (`15-20`) handles overrides or validations.
- **Context Guards:** Every handler validates its execution environment (`is_product_category()`, `WC()->cart->is_empty()`, `is_admin()`). This prevents unnecessary processing on unrelated requests and reduces server load.
- **Sanitization & Escaping:** All user-generated or dynamic output passes through `esc_html()`, `wp_kses_post()`, or `sanitize_text_field()`. WooCommerce hooks frequently output directly to the browser; skipping sanitization introduces XSS vulnerabilities.
- **Lazy Initialization:** The manager instantiates on `plugins_loaded` rather than `init`, ensuring WooCommerce core classes are fully available before hook registration.
## Pitfall Guide
1. **Priority Collision**
- *Explanation:* Multiple plugins attach to the same hook at default priority `10`. Execution order becomes unpredictable, causing UI overlaps or data overwrites.
- *Fix:* Define explicit priority ranges. Document your plugin’s priority usage. Use `add_action('hook', 'callback', 15)` instead of relying on defaults.
2. **Filter Return Omission**
- *Explanation:* Forgetting to return the modified value in a filter callback breaks the data chain. WooCommerce receives `null` and may throw fatal errors or render empty fields.
- *Fix:* Always return the first parameter, even if unmodified. Use early returns only after preserving the original value.
3. **Context Blindness**
- *Explanation:* Running cart-specific hooks on checkout or admin pages. This causes performance degradation and unexpected UI artifacts.
- *Fix:* Wrap handlers in conditional checks (`is_cart()`, `is_checkout()`, `is_admin()`). Leverage WooCommerce conditional tags to scope execution.
4. **Direct Global Mutation**
- *Explanation:* Modifying `$woocommerce->cart` or `$_POST` directly bypasses WooCommerce’s internal validation and session management.
- *Fix:* Use provided filters (`woocommerce_add_cart_item_data`, `woocommerce_checkout_posted_data`) and action hooks (`woocommerce_before_calculate_totals`) to interact with cart/order state.
5. **Output Escaping Neglect**
- *Explanation:* Echoing raw product data, user input, or database values without sanitization. Hooks like `woocommerce_single_product_summary` render directly to the DOM.
- *Fix:* Apply `esc_html()` for text, `esc_url()` for links, and `wp_kses_post()` for HTML content. Never trust dynamic data.
6. **Deprecated Hook Reliance**
- *Explanation:* Using hooks removed in recent WooCommerce versions (e.g., legacy template structure hooks). Code fails silently or throws errors after updates.
- *Fix:* Check WooCommerce release notes. Use `has_action()` or `did_action()` to verify hook availability. Implement fallback logic for backward compatibility.
7. **Performance Drag in High-Frequency Hooks**
- *Explanation:* Executing heavy database queries or external API calls inside hooks that fire on every page load (e.g., `woocommerce_before_shop_loop`).
- *Fix:* Cache results using WordPress transients. Limit queries to necessary contexts. Offload heavy processing to background jobs or REST API endpoints.
## Production Bundle
### Action Checklist
- [ ] Audit existing hooks: Map all customizations to specific WooCommerce contexts and remove redundant overrides.
- [ ] Assign explicit priorities: Replace default `10` with documented ranges (`5`, `10`, `15`, `20`) to prevent collisions.
- [ ] Namespace all callbacks: Use class methods or prefixed functions to avoid global function conflicts.
- [ ] Implement context guards: Wrap every handler in conditional checks (`is_cart()`, `is_admin()`, etc.) to limit execution scope.
- [ ] Enforce sanitization/escaping: Audit all `echo` statements and filter returns for proper WordPress security functions.
- [ ] Add version compatibility checks: Use `defined('WC_VERSION')` or `function_exists()` to prevent fatal errors on outdated installations.
- [ ] Test upgrade paths: Simulate WooCommerce minor/major updates in staging to verify hook stability and template fallbacks.
### Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|----------|---------------------|-----|-------------|
| UI layout adjustment (banner, spacing) | Action Hook (`woocommerce_before_*`) | Non-invasive, renders at precise DOM insertion points | Low (frontend only) |
| Data transformation (price, labels, fields) | Filter Hook (`woocommerce_*_fields`, `*_text`) | Modifies data before rendering, preserves core logic | Low (CPU negligible) |
| Payment gateway routing | Action Hook (`woocommerce_checkout_process`) + Filter (`woocommerce_available_payment_gateways`) | Validates cart state, dynamically enables/disables gateways | Medium (requires gateway API integration) |
| Bulk inventory sync | Cron Job + REST API | Hooks fire per-request; cron handles batch operations efficiently | High (server resources, but scalable) |
| Custom checkout validation | Filter (`woocommerce_checkout_posted_data`) + Action (`woocommerce_checkout_process`) | Separates data sanitization from business rule validation | Low (standard WC pattern) |
### Configuration Template
```php
<?php
/**
* Plugin Name: WC Hook Orchestrator
* Description: Centralized hook management for WooCommerce extensions
* Version: 1.0.0
* Requires at least: 6.0
* Requires PHP: 7.4
*/
if (!defined('ABSPATH')) exit;
final class WC_Hook_Orchestrator {
private const PRIORITY_EARLY = 5;
private const PRIORITY_STANDARD = 10;
private const PRIORITY_LATE = 15;
public static function init(): void {
add_action('plugins_loaded', [self::class, 'register_hooks']);
}
public static function register_hooks(): void {
if (!class_exists('WooCommerce')) return;
add_filter('loop_shop_per_page', [self::class, 'set_catalog_limit'], self::PRIORITY_STANDARD, 1);
add_action('woocommerce_single_product_summary', [self::class, 'inject_stock_notice'], self::PRIORITY_LATE);
add_filter('woocommerce_checkout_fields', [self::class, 'simplify_billing'], self::PRIORITY_STANDARD, 1);
}
public static function set_catalog_limit(int $default): int {
return is_admin() ? $default : 20;
}
public static function inject_stock_notice(): void {
global $product;
if (!$product instanceof WC_Product || $product->get_stock_quantity() > 5) return;
printf('<p class="stock-alert">%s</p>', esc_html__('Low inventory: order now.', 'wc-orchestrator'));
}
public static function simplify_billing(array $fields): array {
unset($fields['billing']['billing_company']);
$fields['billing']['billing_phone']['required'] = false;
return $fields;
}
}
WC_Hook_Orchestrator::init();
Quick Start Guide
- Create Plugin Directory: Navigate to
wp-content/plugins/ and create a folder named wc-hook-orchestrator.
- Deploy Configuration Template: Save the provided template as
wc-hook-orchestrator.php inside the new directory.
- Activate & Verify: Enable the plugin in WordPress admin. Visit a product page and cart to confirm hooks execute without errors.
- Extend Safely: Add new handlers to
register_hooks() using explicit priorities and context guards. Test in staging before production deployment.