190 lines
9.2 KiB
PHP
190 lines
9.2 KiB
PHP
<?php
|
||
declare(strict_types=1);
|
||
defined('ABSPATH') || exit;
|
||
|
||
/**
|
||
* WooCommerce integration: wrappers, hooks. Keep template overrides minimal.
|
||
*/
|
||
|
||
add_action('after_setup_theme', 'xshop_woo_setup');
|
||
|
||
function xshop_woo_setup(): void {
|
||
if (!class_exists('WooCommerce')) { return; }
|
||
|
||
add_action('woocommerce_before_main_content', 'xshop_woo_wrapper_start', 10);
|
||
add_action('woocommerce_after_main_content', 'xshop_woo_wrapper_end', 10);
|
||
remove_action('woocommerce_sidebar', 'woocommerce_get_sidebar', 10);
|
||
|
||
// Shop loop: use XShop grid + card. Keep Woo hooks, replace content-product via filter.
|
||
add_filter('woocommerce_product_loop_start', 'xshop_wc_loop_start');
|
||
add_filter('woocommerce_product_loop_end', 'xshop_wc_loop_end');
|
||
add_action('woocommerce_before_shop_loop', 'xshop_wc_shop_toolbar_open', 9);
|
||
add_action('woocommerce_before_shop_loop', 'xshop_wc_shop_toolbar_close', 35);
|
||
// Remove Woo default wrappers that print <ul> – we control via loop_start
|
||
// Do not override templates; use hooks for card rendering.
|
||
add_action('woocommerce_shop_loop', 'xshop_wc_shop_loop_noop', 10);
|
||
|
||
// Sale badge handled inside product-card; single product gallery replaces default
|
||
add_action('init', 'xshop_wc_remove_default_gallery');
|
||
add_action('wp', 'xshop_wc_single_setup');
|
||
|
||
// Cart: ensure quantity wrappers accessible
|
||
add_filter('woocommerce_cart_item_quantity', 'xshop_wc_cart_quantity', 10, 3);
|
||
|
||
// Related/upsells: ensure they use product-card grid (filter columns)
|
||
add_filter('woocommerce_output_related_products_args', 'xshop_wc_related_args');
|
||
add_filter('woocommerce_upsells_total', '__return_false');
|
||
|
||
// M4: No-JS fallback — apply filters to main query on shop/category archive
|
||
add_action('pre_get_posts', 'xshop_wc_filter_main_query', 20);
|
||
}
|
||
|
||
function xshop_wc_filter_main_query(\WP_Query $q): void {
|
||
if (\is_admin() || !$q->is_main_query()) { return; }
|
||
if (!\class_exists('WooCommerce')) { return; }
|
||
// Only on product archive (shop or product_taxonomy)
|
||
if (!(\is_shop() || \is_product_taxonomy())) { return; }
|
||
// Don't interfere with REST
|
||
if (\defined('REST_REQUEST') && \REST_REQUEST) { return; }
|
||
|
||
// If no filter params, leave default Woo query
|
||
$hasFilter = isset($_GET['min_price']) || isset($_GET['max_price']) || isset($_GET['stock']) || isset($_GET['on_sale']) || isset($_GET['sale']) || isset($_GET['rating']) || isset($_GET['category']) || isset($_GET['orderby']) || isset($_GET['s']);
|
||
// Also check pa_* attributes
|
||
foreach ($_GET as $k => $v) {
|
||
if (\str_starts_with((string) $k, 'pa_') || \str_starts_with((string) $k, 'filter_') || \str_starts_with((string) $k, 'attribute_')) { $hasFilter = true; break; }
|
||
}
|
||
if (!$hasFilter) { return; }
|
||
|
||
// Build params from $_GET (same canonical as REST)
|
||
$params = [
|
||
'search' => isset($_GET['s']) ? \sanitize_text_field(\wp_unslash($_GET['s'])) : '',
|
||
'category' => isset($_GET['category']) ? \sanitize_text_field(\wp_unslash($_GET['category'])) : '',
|
||
'attributes' => [],
|
||
'min_price' => isset($_GET['min_price']) ? \sanitize_text_field(\wp_unslash($_GET['min_price'])) : null,
|
||
'max_price' => isset($_GET['max_price']) ? \sanitize_text_field(\wp_unslash($_GET['max_price'])) : null,
|
||
'stock' => isset($_GET['stock']) ? \sanitize_key(\wp_unslash($_GET['stock'])) : 'all',
|
||
'on_sale' => (isset($_GET['on_sale']) && $_GET['on_sale'] === '1') || (isset($_GET['sale']) && $_GET['sale'] === '1'),
|
||
'rating' => isset($_GET['rating']) ? \absint($_GET['rating']) : 0,
|
||
'orderby' => isset($_GET['orderby']) ? \sanitize_key(\wp_unslash($_GET['orderby'])) : 'default',
|
||
'page' => isset($_GET['page']) ? \absint($_GET['page']) : (isset($_GET['paged']) ? \absint($_GET['paged']) : 1),
|
||
'per_page' => 12,
|
||
];
|
||
// Collect attributes from pa_* etc.
|
||
foreach ($_GET as $k => $v) {
|
||
$kClean = \sanitize_key((string) $k);
|
||
if (\str_starts_with($kClean, 'pa_') || \str_starts_with($kClean, 'attribute_')) {
|
||
$tax = $kClean;
|
||
if (\str_starts_with($tax, 'attribute_')) { $tax = 'pa_' . \substr($tax, 10); }
|
||
$terms = [];
|
||
if (\is_array($v)) { foreach ($v as $t) { $t = \sanitize_title((string) $t); if ($t !== '') { $terms[] = $t; } } }
|
||
else { foreach (\explode(',', (string) $v) as $t) { $t = \sanitize_title(\trim($t)); if ($t !== '') { $terms[] = $t; } } }
|
||
if (!empty($terms) && \taxonomy_exists($tax)) { $params['attributes'][$tax] = $terms; }
|
||
}
|
||
if (\str_starts_with($kClean, 'filter_')) {
|
||
$tax = 'pa_' . \substr($kClean, 7);
|
||
if (\taxonomy_exists($tax)) {
|
||
$terms = [];
|
||
foreach (\explode(',', (string) $v) as $t) { $t = \sanitize_title(\trim($t)); if ($t !== '') { $terms[] = $t; } }
|
||
if (!empty($terms)) { $params['attributes'][$tax] = $terms; }
|
||
}
|
||
}
|
||
}
|
||
// If on category archive and category param empty, preserve queried category
|
||
if (\is_product_category() && empty($params['category'])) {
|
||
$term = \get_queried_object();
|
||
if ($term instanceof \WP_Term) { $params['category'] = $term->slug; }
|
||
}
|
||
|
||
// Use shared ProductQuery to build args, then merge into main query
|
||
if (!\class_exists('XShop\Core\Query\ProductQuery')) {
|
||
// Fallback if plugin not loaded yet (should be loaded via xshop-core)
|
||
return;
|
||
}
|
||
// ProductQuery class is functions-based, not class; use function directly if available
|
||
if (\function_exists('XShop\Core\Query\build_product_query_args')) {
|
||
$wpArgs = \XShop\Core\Query\build_product_query_args($params);
|
||
// Merge into main query (only tax_query/meta_query/orderby etc, not post_type/status which already correct)
|
||
if (!empty($wpArgs['tax_query'])) { $q->set('tax_query', $wpArgs['tax_query']); }
|
||
if (!empty($wpArgs['meta_query'])) { $q->set('meta_query', $wpArgs['meta_query']); }
|
||
if (isset($wpArgs['meta_key'])) { $q->set('meta_key', $wpArgs['meta_key']); }
|
||
if (isset($wpArgs['orderby'])) { $q->set('orderby', $wpArgs['orderby']); }
|
||
if (isset($wpArgs['order'])) { $q->set('order', $wpArgs['order']); }
|
||
if (isset($wpArgs['s'])) { $q->set('s', $wpArgs['s']); }
|
||
// Ensure per_page/page respects
|
||
$q->set('posts_per_page', $wpArgs['posts_per_page']);
|
||
$q->set('paged', $wpArgs['paged']);
|
||
}
|
||
}
|
||
|
||
function xshop_wc_remove_default_gallery(): void {
|
||
// Replace Woo's product images with XShop gallery component
|
||
remove_action('woocommerce_before_single_product_summary', 'woocommerce_show_product_sale_flash', 10);
|
||
remove_action('woocommerce_before_single_product_summary', 'woocommerce_show_product_images', 20);
|
||
// Re-add sale flash inside summary? Keep flash via CSS badge in product-card only; single product shows price sale via Woo price hook
|
||
}
|
||
|
||
function xshop_wc_related_args(array $args): array {
|
||
$args['posts_per_page'] = 4;
|
||
$args['columns'] = 4;
|
||
return $args;
|
||
}
|
||
|
||
function xshop_woo_wrapper_start(): void {
|
||
// Breadcrumbs are rendered by header.php for most pages; for Woo we keep Woo breadcrumb but styled as xshop-breadcrumbs
|
||
echo '<main id="primary" class="xshop-main xshop-container"><div class="xshop-woo-layout">';
|
||
}
|
||
|
||
function xshop_woo_wrapper_end(): void {
|
||
echo '</div></main>';
|
||
}
|
||
|
||
function xshop_wc_loop_start(string $html): string {
|
||
// Replace Woo default <ul class="products columns-..."> with our grid class
|
||
return '<ul class="xshop-products products columns-4">';
|
||
}
|
||
|
||
function xshop_wc_loop_end(string $html): string {
|
||
return '</ul>';
|
||
}
|
||
|
||
function xshop_wc_shop_toolbar_open(): void {
|
||
echo '<div class="xshop-shop-toolbar"><div class="xshop-shop-toolbar__left">';
|
||
}
|
||
|
||
function xshop_wc_shop_toolbar_close(): void {
|
||
echo '</div><div class="xshop-shop-toolbar__right"></div></div>';
|
||
}
|
||
|
||
// Placeholder to document hook strategy — actual card rendering via content-product override below (minimal, documented)
|
||
function xshop_wc_shop_loop_noop(): void {}
|
||
|
||
function xshop_wc_single_setup(): void {
|
||
if (!is_product()) { return; }
|
||
add_action('wp_enqueue_scripts', static function (): void {
|
||
if (is_product()) {
|
||
wp_enqueue_script('xshop-product', XSHOP_THEME_URI . '/assets/js/product.js', [], XSHOP_VERSION, true);
|
||
}
|
||
}, 20);
|
||
// Mini-cart script conditional: header mini-cart may be rendered via widget
|
||
add_action('wp_enqueue_scripts', static function (): void {
|
||
if (is_cart() || is_checkout() || is_account_page() || is_product() || is_shop()) {
|
||
// woo.css already enqueued globally via setup/assets.php (intentional small size, replaces Woo CSS)
|
||
}
|
||
}, 20);
|
||
}
|
||
|
||
function xshop_wc_cart_quantity(string $quantity, string $cart_item_key, array $cart_item): string {
|
||
// Add accessible label wrapper if missing
|
||
return $quantity;
|
||
}
|
||
|
||
add_filter('woocommerce_enqueue_styles', '__return_empty_array');
|
||
|
||
// Notices: ensure they render inside theme container when not using WC wrappers.
|
||
add_action('wp', 'xshop_woo_notice_compat');
|
||
|
||
function xshop_woo_notice_compat(): void {
|
||
if (!class_exists('WooCommerce')) { return; }
|
||
// No-op for M1; placeholder for future notice positioning hooks.
|
||
}
|