PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.3
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.3
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.3, at app/Hooks/Handlers/ShortCodes/ShopAppHandler.php

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