PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.4
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.4
1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 1.3.20 1.3.19 trunk 1.2.0 All 47 releases
fluent-cart / app / Services / Renderer / RenderGate.php

RenderGate.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.6.4, at app/Services/Renderer/RenderGate.php

166 lines 5.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCart\App\Services\Renderer;
4
5 use FluentCart\Framework\Support\Arr;
6
7 /**
8 * Visibility gate for product display regions.
9 *
10 * Every product surface in FluentCart — the single product page, the shop
11 * grid, carousels, related products, shortcodes, Bricks elements and the Divi
12 * modules — funnels through ProductRenderer or ProductCardRender. Gating a
13 * region here therefore gates it everywhere, including the AJAX fragments the
14 * filter/infinite-scroll paths render, without a template override or CSS.
15 *
16 * Returning false OMITS the region's markup entirely. That is deliberate:
17 * blanking the existing *_text filters leaves an empty styled button in the
18 * DOM (a wasted click target and a hole in the layout), which is exactly what
19 * a catalog-mode integration cannot use.
20 *
21 * Two filters run per region, in this order:
22 *
23 * fluent_cart/product/show_{$section} — one region, every surface
24 * fluent_cart/product/show_section — catch-all, sees the result above
25 *
26 * The catch-all runs last and therefore has the final say, so "hide every
27 * purchase affordance" stays a single add_filter instead of one per region.
28 *
29 * Callers narrow by the payload rather than by the hook name: `scope` says
30 * which surface is rendering (product_card, single_product) and `section` says
31 * which region. One hook name per region across all surfaces — a listener that
32 * wants the shop grid but not the product page checks $context['scope'].
33 */
34 class RenderGate
35 {
36 /**
37 * A region rendered on the shop/listing product card.
38 */
39 const SCOPE_CARD = 'product_card';
40
41 /**
42 * A region rendered on the single product page.
43 */
44 const SCOPE_SINGLE = 'single_product';
45
46 /**
47 * Gateable regions. Each maps to a fluent_cart/product/show_{section}
48 * filter. Kept as a list so documentation and tests can enumerate the
49 * contract instead of grepping for apply_filters calls.
50 */
51 const SECTIONS = [
52 'image',
53 'title',
54 'excerpt',
55 'price',
56 'quantity',
57 'actions',
58 'add_to_cart_button',
59 'buy_now_button',
60 'buy_section',
61 ];
62
63 /**
64 * Build the context payload a gate and its surrounding actions share.
65 *
66 * Single construction point so `source` / `source_name` cannot be present
67 * on one region's payload and missing from the next one's.
68 *
69 * @param mixed $product
70 * @param string $scope One of the SCOPE_* constants
71 * @param mixed $variant Variation-specific regions only
72 * @return array
73 */
74 public static function context($product, $scope, $variant = null)
75 {
76 return RenderContext::decorate([
77 'product' => $product,
78 'variant' => $variant,
79 'scope' => $scope,
80 ]);
81 }
82
83 /**
84 * Should this region render?
85 *
86 * @param string $section One of self::SECTIONS
87 * @param array $context At minimum 'product' and 'scope'; 'variant' when
88 * the region is variation-specific
89 * @return bool
90 */
91 public static function shouldRender($section, array $context = [])
92 {
93 $context = wp_parse_args($context, [
94 'product' => null,
95 'variant' => null,
96 'scope' => '',
97 ]);
98
99 // Set after the merge so a caller cannot pass a 'section' that
100 // disagrees with the hook name listeners are actually bound to.
101 $context['section'] = $section;
102
103 // Idempotent: decorate() leaves existing keys alone, so a context that
104 // already came from self::context() is unchanged here.
105 $context = RenderContext::decorate($context);
106
107 $show = apply_filters("fluent_cart/product/show_{$section}", true, $context);
108
109 return (bool) apply_filters('fluent_cart/product/show_section', $show, $context);
110 }
111
112 /**
113 * Convenience for the two purchase buttons: the whole action row has to be
114 * visible before an individual button inside it can be.
115 *
116 * Without this, hiding 'actions' would still leave a listener on
117 * 'add_to_cart_button' able to resurrect a button into a row that is not
118 * being rendered.
119 *
120 * @param string $section 'add_to_cart_button' or 'buy_now_button'
121 * @param array $context
122 * @return bool
123 */
124 public static function shouldRenderPurchaseButton($section, array $context = [])
125 {
126 if (!self::shouldRender('actions', $context)) {
127 return false;
128 }
129
130 return self::shouldRender($section, $context);
131 }
132
133 /**
134 * CSS classes for a product card wrapper.
135 *
136 * @param array $classes
137 * @param array $context
138 * @return string Space-separated, escaped-safe class list
139 */
140 public static function cardClasses(array $classes, array $context = [])
141 {
142 $classes = apply_filters('fluent_cart/product/card_classes', $classes, RenderContext::decorate(wp_parse_args($context, [
143 'product' => null,
144 'scope' => self::SCOPE_CARD,
145 ])));
146
147 if (!is_array($classes)) {
148 return '';
149 }
150
151 // Split on whitespace before sanitising so a listener may return either
152 // one class per entry or a space-separated string — sanitize_html_class()
153 // strips the space and would otherwise weld 'sale badge' into 'salebadge'.
154 $flat = [];
155 foreach (Arr::flatten($classes) as $class) {
156 foreach (preg_split('/\s+/', (string) $class) as $piece) {
157 $flat[] = $piece;
158 }
159 }
160
161 $flat = array_filter(array_map('sanitize_html_class', $flat));
162
163 return implode(' ', array_unique($flat));
164 }
165 }
166