PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.7.0
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.7.0
1.7.0 1.6.6 1.6.5 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 All 50 releases
fluent-cart / app / Modules / Templating / TemplateActions.php

TemplateActions.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.7.0, at app/Modules/Templating/TemplateActions.php

418 lines 13.5 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\Modules\Templating;
4
5 use FluentCart\Api\Resource\ShopResource;
6 use FluentCart\Api\StoreSettings;
7 use FluentCart\Api\Taxonomy;
8 use FluentCart\App\CPT\FluentProducts;
9 //use FluentCart\App\Hooks\Handlers\ShortCodes\Buttons\AddToCartShortcode;
10 use FluentCart\App\Hooks\Handlers\ShortCodes\SingleProductShortCode;
11 use FluentCart\App\Modules\Data\ProductDataSetup;
12 use FluentCart\App\Modules\Data\ProductQuery;
13 use FluentCart\App\Services\Renderer\ProductListRenderer;
14 use FluentCart\App\Services\Renderer\ProductRenderer;
15 use FluentCart\App\Services\Renderer\ShopAppRenderer;
16 use FluentCart\App\Services\Schema\ProductSchema;
17 use FluentCart\App\Services\TemplateService;
18 use FluentCart\Framework\Support\Arr;
19
20 class TemplateActions
21 {
22 public function register()
23 {
24 add_action('fluent_cart/template/main_content', [$this, 'renderMainContent']);
25 add_action('fluent_cart/template/product_archive', [$this, 'renderProductArchive']);
26 add_action('fluent_cart/product/render_product_header', [$this, 'renderProductHeader']);
27 // Block themes render reviews from the product template, so the
28 // built-in listener is not attached there — the hook still fires for
29 // third-party listeners. Safe to decide here: the theme is resolved
30 // at after_setup_theme, before init runs this.
31 if (!function_exists('wp_is_block_theme') || !wp_is_block_theme()) {
32 add_action('fluent_cart/product/after_product_content', [$this, 'renderProductReviews']);
33 }
34 add_action('wp_head', [$this, 'renderProductJsonLd']);
35
36 // Universal hook for after_product_content — fires on both classic and block themes
37 // Block themes render <!-- wp:post-content --> which runs the_content filter
38 add_filter('the_content', function ($content) {
39 if (!is_singular('fluent-products')) {
40 return $content;
41 }
42
43 global $post;
44
45 if (!$post || $post->post_type !== FluentProducts::CPT_NAME) {
46 return $content;
47 }
48
49 ob_start();
50 do_action('fluent_cart/product/after_product_content', $post->ID);
51 $extra = ob_get_clean();
52
53 if ($extra) {
54 $content .= $extra;
55 }
56
57 return $content;
58 }, 999);
59
60 add_shortcode('fluent_cart_product_header', function ($atts = []) {
61 $atts = shortcode_atts([
62 'id' => 0,
63 ], $atts);
64
65 $productId = absint($atts['id']);
66
67 if (!$productId) {
68 $productId = get_the_ID();
69 }
70
71 if (!$productId) {
72 return 'Product ID not found';
73 }
74
75 $product = ProductDataSetup::getProductModel($productId);
76 if (!$product || !$product->detail) {
77 return 'Product not found';
78 }
79
80 ob_start();
81 $this->renderProductHeader($product->ID);
82 $content = ob_get_clean();
83
84 return $this->tempFixShortcodeContent($content);
85 });
86
87 add_shortcode('fluent_cart_related_products', function ($atts = []) {
88 $atts = shortcode_atts([
89 'id' => 0,
90 ], $atts);
91
92 $productId = absint($atts['id']);
93
94 if (!$productId) {
95 $productId = get_the_ID();
96 }
97
98 if (!$productId) {
99 return __('Product ID not found', 'fluent-cart');
100 }
101
102 $product = ProductDataSetup::getProductModel($productId);
103 if (!$product || !$product->detail) {
104 return __('Product not found', 'fluent-cart');
105 }
106
107 // On the product page this shortcode is the block theme's
108 // equivalent of what filterSingleProductContent appends for
109 // classic themes, so it answers to the same setting and filter.
110 // Placed anywhere else it is an explicit choice and still renders.
111 if (is_singular(FluentProducts::CPT_NAME)) {
112 $showRelevant = (new StoreSettings())->get('show_relevant_product_in_single_page') == 'yes';
113 $showRelevant = apply_filters(
114 'fluent_cart/single_product_page/show_relevant_products',
115 $showRelevant,
116 $productId
117 );
118
119 if (!$showRelevant) {
120 return '';
121 }
122 }
123
124 $products = ShopResource::getSimilarProducts($productId, false);
125 if (empty($products)) {
126 return '';
127 }
128
129 ob_start();
130 (new ProductListRenderer(
131 $products,
132 __('Related Products', 'fluent-cart'),
133 'fct-similar-product-list-container',
134 ['rating_context' => 'relevant']
135 ))->render();
136
137 $content = ob_get_clean();
138
139 return $this->tempFixShortcodeContent($content);
140 });
141
142 add_action('fluent_cart/template/before_content', [$this, 'renderArchiveHeader']);
143
144 }
145
146 public function renderMainContent()
147 {
148 $isTaxPages = is_tax(get_object_taxonomies('fluent-products'));
149 if ($isTaxPages) {
150 do_action('fluent_cart/template/product_archive');
151 }
152 }
153
154 public function renderArchiveHeader()
155 {
156 if (TemplateService::getCurrentFcPageType() !== 'product_taxonomy') {
157 return;
158 }
159
160 ?>
161 <div class="fct-archive-header-wrap">
162 <h1 class="fct-archive-title">
163 <?php
164 $queried_object = get_queried_object();
165 if ($queried_object && !empty($queried_object->name)) {
166 echo esc_html($queried_object->name);
167 } else {
168 echo esc_html(get_the_archive_title());
169 }
170 ?>
171 </h1>
172 <?php
173 $termDescription = term_description();
174 if ($termDescription) {
175 ?>
176 <div class="fct-archive-description">
177 <?php echo wp_kses_post(wpautop($termDescription)); ?>
178 </div>
179 <?php
180 }
181 ?>
182 </div>
183 <?php
184
185 }
186
187 public function renderProductArchive()
188 {
189 $queried_object = get_queried_object();
190
191 if (empty($queried_object->taxonomy) || !is_tax(get_object_taxonomies('fluent-products'))) {
192 return;
193 }
194
195 $productsQuery = (new ProductQuery([
196 'is_main_query' => true,
197 'paginate' => 'simple',
198 'with' => ['detail', 'variants']
199 ]));
200
201 $publicTaxonomies = Taxonomy::getTaxonomies();
202
203
204 unset($publicTaxonomies[$queried_object->taxonomy]);
205
206 $connectedTerms = $productsQuery->getConnectedTerms(array_values($publicTaxonomies), true);
207 //TODO: also add the child terms
208 $taxFilters = [];
209
210 foreach ($connectedTerms as $taxonomyName => $term) {
211
212 $taxObj = get_taxonomy($taxonomyName);
213 if (!$taxObj) {
214 continue;
215 }
216
217 $taxFilters[$taxonomyName] = [
218 'options' => $term['terms'],
219 'term' => $taxonomyName,
220 'label' => $taxObj->label
221 ];
222 }
223
224 $products = $productsQuery->get();
225 (new ShopAppRenderer([
226 'products' => $products,
227 'total' => $products->total(),
228 ], [
229 'default_filters' => $productsQuery->getDefaultFilters(),
230 'custom_filters' => [
231 'taxonomies' => [
232 $taxFilters
233 ],
234 'search' => true,
235 'price_range' => true,
236 ],
237 'pagination_type' => 'simple'
238 ]))->render();
239 }
240
241 public function initSingleProductHooks()
242 {
243 add_filter('the_title', function ($title, $post_id) {
244 if (apply_filters('fluent_cart/disable_auto_single_product_page', false)) {
245 return $title;
246 }
247
248 global $post;
249 global $wp_query;
250
251 if (
252 // this is the main query for a single product page
253 $wp_query->is_main_query()
254 // post_id is get_queried id
255 && $post_id == $wp_query->get_queried_object_id()
256 && $post->post_type === FluentProducts::CPT_NAME
257 ) {
258 return '';
259 }
260
261 return $title;
262 }, 10, 2);
263 add_filter('get_the_excerpt', function ($excerpt, $post) {
264 if (apply_filters('fluent_cart/disable_auto_single_product_page', false)) {
265 return $excerpt;
266 }
267
268 global $wp_query;
269
270 if (
271 $wp_query->is_main_query()
272 && $post->ID == $wp_query->get_queried_object_id()
273 && $post->post_type === FluentProducts::CPT_NAME
274 ) {
275 return '';
276 }
277
278 return $excerpt;
279 }, 10, 2);
280
281 add_filter('the_content', [$this, 'filterSingleProductContent'], 999);
282 }
283
284 public function filterSingleProductContent($content)
285 {
286 if (apply_filters('fluent_cart/disable_auto_single_product_page', false)) {
287 return $content;
288 }
289
290 global $post;
291 global $wp_query;
292
293 if (!$wp_query->is_main_query() && $post->post_type !== FluentProducts::CPT_NAME) {
294 return $content;
295 }
296
297 remove_filter('the_content', [$this, 'filterSingleProductContent']);
298 ob_start();
299 do_action('fluent_cart/product/render_product_header', $post->ID);
300 $headerContent = ob_get_clean();
301 $content = $headerContent . $content;
302
303 $storeSettings = new StoreSettings();
304
305 $showRelevant = $storeSettings->get('show_relevant_product_in_single_page') == 'yes';
306 $showRelevant = apply_filters('fluent_cart/single_product_page/show_relevant_products', $showRelevant, $post->ID);
307 if ($showRelevant) {
308 $products = ShopResource::getSimilarProducts($post->ID, false);
309 ob_start();
310 (new ProductListRenderer(
311 $products,
312 __('Related Products', 'fluent-cart'),
313 'fct-similar-product-list-container',
314 ['rating_context' => 'relevant']
315 ))->render();
316
317 $relevantProducts = ob_get_clean();
318 $content .= $relevantProducts;
319 }
320
321 return $content;
322 }
323
324 public function renderProductHeader($productId = false)
325 {
326 if (!$productId) {
327 $productId = get_the_ID();
328 }
329
330 if (!$productId) {
331 return;
332 }
333
334 $product = ProductDataSetup::getProductModel($productId);
335 if (!$product || !$product->detail) {
336 return;
337 }
338
339 $storeSettings = new StoreSettings();
340 (new ProductRenderer($product, [
341 'view_type' => $storeSettings->get('variation_view', 'both'),
342 'column_type' => $storeSettings->get('variation_columns', 'masonry')
343 ]))->render();
344 }
345
346 private function tempFixShortcodeContent($content)
347 {
348
349 if (!$content) {
350 return $content;
351 }
352
353 // Remove empty <p> tags (including those with whitespace), <br> tags, and new lines
354 $cleaned = preg_replace([
355 '/<p>\s*<\/p>/i', // Remove empty <p> tags
356 '/<p>\s*<br\s*\/?>\s*<\/p>/i', // Remove <p> tags containing only <br>
357 '/<br\s*\/?>/i', // Remove all <br> tags
358 '/[\r\n]+/' // Remove all new lines
359 ], '', $content);
360
361 // Remove extra whitespace between tags to clean up the output
362 return preg_replace('/>\s+</', '><', $cleaned);
363 }
364
365 /**
366 * Whether the product page should carry its review section at all.
367 *
368 * Sits beside the relevant-products switch in Product Page settings and
369 * works the same way, so one control governs the section wherever it is
370 * rendered from — a review block, or the append below.
371 *
372 * @param int $postId
373 * @return bool
374 */
375 public static function shouldShowReviewsOnProductPage($postId): bool
376 {
377 $show = (new StoreSettings())->get('show_reviews_in_single_page', 'yes') == 'yes';
378
379 return (bool) apply_filters('fluent_cart/single_product_page/show_reviews', $show, $postId);
380 }
381
382 private function isReviewModuleAvailable()
383 {
384 return \FluentCart\Api\ModuleSettings::isActive('reviews')
385 && class_exists(\FluentCart\App\Services\ProductReviewService::class);
386 }
387
388 public function renderProductReviews($postId)
389 {
390 if (!$postId || !$this->isReviewModuleAvailable()) {
391 return;
392 }
393
394 $renderer = new \FluentCart\App\Services\Renderer\ProductReviewRenderer($postId);
395 $renderer->render();
396 $renderer->renderForm();
397 }
398
399 public function renderProductJsonLd()
400 {
401 if (!is_singular(\FluentCart\App\CPT\FluentProducts::CPT_NAME)) {
402 return;
403 }
404
405 $postId = get_the_ID();
406 if (!$postId) {
407 return;
408 }
409
410 // The node itself — product, offers, aggregate rating, the reviews
411 // the page shows — is ProductSchema's; this is only the page gate.
412 // The reviews module gate lives there too, on the review parts
413 // alone: a product is for sale whether or not it takes reviews.
414 ProductSchema::render($postId);
415 }
416
417 }
418