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 / Hooks / Handlers / ShortCodes / ShopAppHandler.php

ShopAppHandler.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.7.0, at app/Hooks/Handlers/ShortCodes/ShopAppHandler.php

473 lines 19.4 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\Hooks\Handlers\ShortCodes;
4
5 use FluentCart\Api\CurrencySettings;
6 use FluentCart\Api\Resource\ShopResource;
7 use FluentCart\Api\StoreSettings;
8 use FluentCart\Api\Taxonomy;
9 use FluentCart\App\App;
10 use FluentCart\App\Helpers\Helper;
11 //use FluentCart\App\Hooks\Handlers\ShortCodes\Buttons\AddToCartShortcode;
12 use FluentCart\App\Models\ProductDetail;
13 use FluentCart\App\Modules\Templating\AssetLoader;
14 use FluentCart\App\Services\Renderer\RenderContext;
15 use FluentCart\App\Services\Renderer\ShopAppRenderer;
16 use FluentCart\App\Services\TemplateService;
17 use FluentCart\Framework\Support\Arr;
18 use FluentCart\Framework\Support\Str;
19
20 class ShopAppHandler
21 {
22 protected array $viewData = [];
23 protected array $defaultViewData = [];
24 protected array $shortcodeAttributes = [];
25
26 protected string $slug = '';
27 protected string $assetsPath = '';
28 protected bool $shouldLoadRangePlugin = false;
29
30 protected array $urlFilters = [];
31
32 const SHORT_CODE = 'fluent_cart_products';
33
34 public function register()
35 {
36 add_action('wp_enqueue_scripts', function () {
37 if (App::request()->get('action') === 'elementor') {
38 return;
39 }
40
41 if (TemplateService::getCurrentFcPageType() === 'shop') {
42 $this->enqueueStyles();
43 } else if (has_shortcode(get_the_content(), static::SHORT_CODE) || has_block('fluent-cart/products')) {
44 $this->enqueueStyles();
45 }
46 }, 5);
47 add_shortcode(static::SHORT_CODE, function ($shortcodeAttributes, $content, $block) {
48 // The shop shortcode registers its own closure rather than going
49 // through ShortCode::register(), so it declares itself separately.
50 return RenderContext::declaring(
51 RenderContext::SOURCE_SHORTCODE,
52 static::SHORT_CODE,
53 function () use ($shortcodeAttributes) {
54 return $this->handelShortcodeCall($shortcodeAttributes);
55 }
56 );
57 });
58 }
59
60 public function handelShortcodeCall($shortcodeAttributes)
61 {
62 $urlFilters = Arr::get(App::request()->all(), 'filters', []);
63 if (!is_array($urlFilters)) {
64 $urlFilters = [];
65 }
66
67 $this->urlFilters = $urlFilters;
68
69 $this->buildPaths();
70 $this->buildShortcodeAttributes($shortcodeAttributes);
71 $this->buildFilters();
72 $this->enqueueAssets();
73 $this->prepareInitialViewData();
74
75 return $this->renderView();
76 }
77
78 private function buildPaths()
79 {
80 $app = App::getInstance();
81 $this->slug = $app->config->get('app.slug');
82 $this->assetsPath = $app['url.assets'];
83 }
84
85 private function buildShortcodeAttributes($shortcodeAttributes)
86 {
87 $this->shortcodeAttributes = shortcode_atts(array(
88 'per_page' => '10',
89 // 'view_mode' => 'default',
90 'view_mode' => 'grid',
91 'with_cart' => 'no',
92 'cats' => '',
93 'exclude_carts' => '',
94 'show_cat_filter' => 'no',
95 'block_class' => '',
96 'paginator' => 'scroll',
97 'uid' => 'fluent_products_container_' . Helper::getUidSerial(),
98 'enable_filter' => false,
99 'enable_wildcard_filter' => false,
100 'enable_wildcard_for_post_content' => false,
101 'default_filters' => json_encode(['enabled' => false]),
102 'use_default_style' => true,
103 'search_grid_size' => 1,
104 'product_grid_size' => 4,
105 'product_box_grid_size' => 4,
106 'colors' => json_encode([]),
107 'price_format' => 'starts_from',
108 'order_type' => 'DESC',
109 'live_filter' => false,
110 'custom_filters' => json_encode([]),
111 'filters' => json_encode([]),
112 'ids' => '',
113 'exclude_ids' => '',
114 'category' => '',
115 'category_id' => '',
116 'fulfillment_type' => '',
117 'product_type' => '',
118 'on_sale' => '',
119 'sort_by' => '',
120 'columns' => '',
121 'orderby' => '',
122 'order' => '',
123 'limit' => '',
124 ), $shortcodeAttributes, static::SHORT_CODE);
125
126 // Alias: limit → per_page
127 if (!empty($this->shortcodeAttributes['limit']) && is_numeric($this->shortcodeAttributes['limit'])) {
128 $this->shortcodeAttributes['per_page'] = $this->shortcodeAttributes['limit'];
129 }
130
131 // Alias: columns → product_box_grid_size (controls the CSS grid columns)
132 if (!empty($this->shortcodeAttributes['columns']) && is_numeric($this->shortcodeAttributes['columns'])) {
133 $this->shortcodeAttributes['product_box_grid_size'] = $this->shortcodeAttributes['columns'];
134 }
135
136 // Map orderby+order → sort_by (WooCommerce-style)
137 if (!empty($this->shortcodeAttributes['orderby']) && empty($this->shortcodeAttributes['sort_by'])) {
138 $order = strtoupper($this->shortcodeAttributes['order'] ?: 'DESC');
139 $orderbyMap = [
140 'date' => ['ASC' => 'date-oldest', 'DESC' => 'date-newest'],
141 'title' => ['ASC' => 'name-asc', 'DESC' => 'name-desc'],
142 'price' => ['ASC' => 'price-low', 'DESC' => 'price-high'],
143 'id' => ['ASC' => 'date-oldest', 'DESC' => 'date-newest'],
144 ];
145 $orderby = strtolower($this->shortcodeAttributes['orderby']);
146 if (isset($orderbyMap[$orderby][$order])) {
147 $this->shortcodeAttributes['sort_by'] = $orderbyMap[$orderby][$order];
148 }
149 }
150
151 $this->shortcodeAttributes['per_page'] = is_numeric($this->shortcodeAttributes['per_page']) ? (int) $this->shortcodeAttributes['per_page'] : 10;
152
153 $viewMode = $this->shortcodeAttributes['view_mode'];
154
155 if (!($viewMode === 'list' || $viewMode === 'grid')) {
156 $viewMode = 'grid';
157 }
158
159 $this->shortcodeAttributes['view_mode'] = $viewMode;
160
161 $this->handelGridSizes('product_box_grid_size', 3);
162 $this->handelGridSizes('search_grid_size', 2);
163 $this->handelGridSizes('product_grid_size');
164
165 if ($this->shortcodeAttributes['enable_filter'] && Arr::get($shortcodeAttributes, 'filters', false)) {
166 if (is_array($shortcodeAttributes['filters'])) {
167 $this->shortcodeAttributes['filters'] = $shortcodeAttributes['filters'];
168 } else {
169 $jsonData = stripslashes(html_entity_decode($shortcodeAttributes['filters']));
170 $this->shortcodeAttributes['filters'] = json_decode($jsonData, true);
171 }
172 } else {
173 $this->shortcodeAttributes['filters'] = [
174 'enabled' => false
175 ];
176 }
177 }
178
179 public function handelGridSizes($key, $defaultValue = 6)
180 {
181 $this->shortcodeAttributes[$key] = intval($this->shortcodeAttributes[$key]);
182
183 if (empty($this->shortcodeAttributes[$key])) {
184 return;
185 }
186
187 if ($this->shortcodeAttributes[$key] > 6 || $this->shortcodeAttributes[$key] < 1) {
188 $this->shortcodeAttributes[$key] = $defaultValue;
189 }
190 }
191
192 private function buildFilters()
193 {
194 $this->viewData['filters'] = Arr::get($this->shortcodeAttributes, 'filters', []);
195
196 $filters = [];
197
198
199 foreach ($this->viewData['filters'] ?? [] as $key => $val) {
200 //Filter Out The filters are disabled
201 $enabled = Arr::get($val, 'enabled', false);
202
203 $isEnabled = in_array($enabled, [true, '1', 'true'], true);
204
205
206 if (!$isEnabled) {
207 continue;
208 }
209
210 $filters[$key]['label'] = Arr::get($val, 'label', ucfirst($key));
211 $filters[$key]['filter_type'] = Arr::get($val, 'filter_type', '');
212
213 if (is_array($val) && Arr::get($val, 'enabled', false) !== false && Arr::get($val, 'is_meta', false) !== false) {
214 $prefilled = Arr::get($this->urlFilters, $key);
215 $filters[$key]['options'] = $this->getMetaFilterOptions($key, $prefilled);
216 }
217
218 if ($filters[$key]['filter_type'] === 'range') {
219 $this->shouldLoadRangePlugin = true;
220 $minValue = Helper::toDecimalWithoutComma(ProductDetail::query()->min('min_price'));
221 $maxValue = Helper::toDecimalWithoutComma(ProductDetail::query()->max('max_price'));
222
223 $minFromUrl = Arr::get($this->urlFilters, $key . '_from', 0);
224 $maxFromUrl = Arr::get($this->urlFilters, $key . '_to', $maxValue);
225
226 $filters[$key]['min_value'] = ($minFromUrl < $minValue) ? $minValue : (min($minFromUrl, $maxValue));
227 $filters[$key]['max_value'] = ($maxFromUrl < 0) ? 0 : (min($maxFromUrl, $maxValue));
228
229 $filters[$key]['min'] = $minValue;
230 $filters[$key]['max'] = $maxValue;
231 }
232 }
233
234 $this->viewData['filters'] = $filters;
235 }
236
237 private function getMetaFilterOptions($key, $prefilled = []): array
238 {
239 return Taxonomy::getFormattedTerms($key, false, null, 'value', 'label', $prefilled);
240 }
241
242 private function prepareInitialViewData()
243 {
244
245 $allProducts = $this->getInitialProducts();
246 $this->defaultViewData = [
247 'products' => Arr::get($allProducts, 'products', []),
248 'placeholder_image' => Helper::getProductPlaceholderUrl(),
249 'paginator' => $this->shortcodeAttributes['paginator'],
250 'view_mode' => $this->shortcodeAttributes['view_mode'],
251 'price_format' => $this->shortcodeAttributes['price_format'],
252 'per_page' => $this->shortcodeAttributes['per_page'],
253 'enable_filter' => $this->shortcodeAttributes['enable_filter'],
254 'enable_wildcard_filter' => $this->shortcodeAttributes['enable_wildcard_filter'],
255 'enable_wildcard_for_post_content' => $this->shortcodeAttributes['enable_wildcard_for_post_content'],
256 'default_filters' => $this->shortcodeAttributes['default_filters'],
257 'custom_filters' => is_array($this->shortcodeAttributes['custom_filters'])? $this->shortcodeAttributes['custom_filters']: json_decode($this->shortcodeAttributes['custom_filters'], true),
258 'use_default_style' => $this->shortcodeAttributes['use_default_style'],
259 'colors' => is_array($this->shortcodeAttributes['colors'])? $this->shortcodeAttributes['colors']: json_decode($this->shortcodeAttributes['colors'], true),
260 'store_settings' => new StoreSettings(),
261 'filters' => $this->shortcodeAttributes['filters']
262 ];
263 }
264
265 protected function getGridAttributes(): array
266 {
267 $product_grid_size = $this->shortcodeAttributes['product_grid_size'];
268 $product_grid_size = empty($product_grid_size) ? 4 : $product_grid_size;
269
270 $search_grid_size = $this->shortcodeAttributes['search_grid_size'];
271 $search_grid_size = empty($search_grid_size) ? 1 : $search_grid_size;
272
273
274 $product_default_grid_size = $product_grid_size;
275
276 if (!empty($this->defaultViewData['enable_filter'])) {
277 $product_default_grid_size += $search_grid_size;
278 }
279 return [
280 'search_grid_size' => $search_grid_size,
281 'product_grid_size' => $product_grid_size,
282 'product_default_grid_size' => $product_default_grid_size,
283 'product_box_grid_size' => $this->shortcodeAttributes['product_box_grid_size'] ?? 0
284 ];
285 }
286
287 public function getBlockClassName(): array
288 {
289 $blockClass = $this->shortcodeAttributes['block_class'];
290 return [
291 'block_class' => $blockClass,
292 ];
293 }
294
295 private function getViewData(): array
296 {
297 return array_merge(
298 $this->defaultViewData,
299 $this->viewData,
300 $this->getGridAttributes(),
301 $this->getBlockClassName(),
302 [
303 'shortcode_settings' => $this->shortcodeAttributes,
304 ]
305 );
306 }
307
308 private function getInitialProducts()
309 {
310 $params = $this->getDefaultConfig();
311
312 $products = ShopResource::get($params);
313
314 return [
315 'products' => ($products['products']->setCollection(
316 $products['products']->getCollection()->transform(function ($product) {
317 $product->setAppends(['view_url', 'has_subscription']);
318 return $product;
319 })
320 )),
321 'total' => $products['total']
322 ];
323 }
324
325 private function getDefaultConfig()
326 {
327 $paginatorMethod = $this->shortcodeAttributes['paginator'] === 'numbers' ? 'simple' : 'cursor';
328
329 $defaultFilters = $this->shortcodeAttributes['default_filters'];
330 $customFilters = $this->shortcodeAttributes['custom_filters'];
331
332 $filters = $this->shortcodeAttributes['filters'];
333 $enableFilters = Arr::get($filters, 'enabled', false) === true;
334
335
336 $allowOutOfStock = (Arr::get($defaultFilters, 'enabled', false) === true) &&
337 filter_var(Arr::get($defaultFilters, 'allow_out_of_stock', false), FILTER_VALIDATE_BOOLEAN);
338
339 if (Arr::get($defaultFilters, 'enabled') != 1) {
340 $defaultFilters = [];
341 }
342
343 $status = ["post_status" => ["column" => "post_status", "operator" => "in", "value" => ["publish"]]];
344
345 $urlTerms = Helper::parseTermIdsForFilter($this->urlFilters);
346 $defaultTerms = Helper::parseTermIdsForFilter($defaultFilters);
347 $mergedTerms = Helper::mergeTermIdsForFilter($defaultTerms, $urlTerms);
348
349 // --- Shortcode attribute: include/exclude IDs ---
350 $includeIds = array_values(array_filter(array_map('intval', array_map('trim', explode(',', $this->shortcodeAttributes['ids'])))));
351 $excludeIds = array_values(array_filter(array_map('intval', array_map('trim', explode(',', $this->shortcodeAttributes['exclude_ids'])))));
352
353 // --- Shortcode attribute: category (by slug) and category_id ---
354 $categoryTermIds = [];
355 if (!empty($this->shortcodeAttributes['category'])) {
356 $categoryTermIds = Taxonomy::getTermIdsBySlugs($this->shortcodeAttributes['category'], 'product-categories');
357 }
358 if (!empty($this->shortcodeAttributes['category_id'])) {
359 $catIds = array_values(array_filter(array_map('intval', array_map('trim', explode(',', $this->shortcodeAttributes['category_id'])))));
360 $categoryTermIds = array_unique(array_merge($categoryTermIds, $catIds));
361 }
362 if (!empty($categoryTermIds)) {
363 $existing = Arr::get($mergedTerms, 'product-categories', []);
364 $mergedTerms['product-categories'] = array_unique(array_merge($existing, $categoryTermIds));
365 }
366
367 // The former tag= / tag_id= attributes filtered on the product-tags
368 // taxonomy, which FluentCart does not register (won't-ship decision
369 // 2026-08-06). They never matched anything — worse, tag_id= filtered
370 // every product out. Unknown attributes are now simply ignored.
371
372 // --- Shortcode attribute: sort_by ---
373 if (!empty($this->shortcodeAttributes['sort_by'])) {
374 $filters['sort_by'] = sanitize_text_field($this->shortcodeAttributes['sort_by']);
375 }
376
377 // --- Shortcode attribute: fulfillment_type / product_type, stock_availability, on_sale ---
378 // product_type is an alias for fulfillment_type
379 $productType = sanitize_text_field($this->shortcodeAttributes['product_type'] ?: $this->shortcodeAttributes['fulfillment_type']);
380 $onSale = in_array(strtolower($this->shortcodeAttributes['on_sale']), ['yes', '1', 'true'], true);
381
382 // merge $this->urlFilters and $filters
383 $filters = array_merge($filters, $this->urlFilters);
384
385 $params = [
386 "select" => '*',
387 "with" => ['detail', 'variants', 'categories', 'licensesMeta'],
388 "selected_status" => true,
389 "status" => $status,
390 "shop_app_default_filters" => $defaultFilters,
391 "default_filters" => $defaultFilters,
392 "taxonomy_filters" => $mergedTerms,
393 "paginate" => $this->shortcodeAttributes['per_page'],
394 "per_page" => $this->shortcodeAttributes['per_page'],
395 'filters' => $filters,
396 'paginate_using' => $paginatorMethod,
397 'pagination_type' => $paginatorMethod,
398 'allow_out_of_stock' => $allowOutOfStock,
399 'order_type' => $this->shortcodeAttributes['order_type'],
400 'live_filter' => $this->shortcodeAttributes['live_filter'],
401 'enable_filters' => $enableFilters,
402 'custom_filters' => $customFilters,
403 'include_ids' => $includeIds,
404 'exclude_ids' => $excludeIds,
405 'product_type' => $productType,
406 'on_sale' => $onSale,
407 'product_box_grid_size' => $this->shortcodeAttributes['product_box_grid_size'],
408 'view_mode' => $this->shortcodeAttributes['view_mode'],
409 'price_format' => $this->shortcodeAttributes['price_format'],
410 ];
411
412 return $params;
413 }
414
415 public function renderView()
416 {
417 ob_start();
418 (new ShopAppRenderer($this->getInitialProducts(), $this->getDefaultConfig()))
419 ->render();
420 return ob_get_clean();
421 }
422
423 public function enqueueAssets()
424 {
425 if (App::request()->get('action') === 'elementor') {
426 return;
427 }
428
429 AssetLoader::loadProductArchiveAssets();
430
431
432 //SingleProductHandler::enqueueAssets();
433 }
434
435 public function enqueueStyles()
436 {
437
438 }
439
440 public function enqueueScripts()
441 {
442
443 }
444
445
446 public function getTermIdsForFilter($defaultFilters): array
447 {
448 $ids = [];
449
450 $taxonomies = Taxonomy::getTaxonomies();
451 foreach ($taxonomies as $key => $taxonomy) {
452
453 $termIds = Arr::get($defaultFilters, $key, '');
454
455
456 if (is_array($termIds)) {
457 $ids = array_merge($ids, $termIds);
458 continue;
459 }
460 if (strlen($termIds) || Str::contains($termIds, ',')) {
461 $termIds = explode(',', $termIds);
462
463 } else {
464 $termIds = [];
465 }
466
467 $ids = array_merge($ids, $termIds);
468
469 }
470 return $ids;
471 }
472 }
473