PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.1
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.1
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 1.3.19 All 49 releases
fluent-cart / app / Hooks / Handlers / ShortCodes / ShopAppHandler.php

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

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