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

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

480 lines 19.9 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 'taxonomies' => '',
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' => Helper::getProductPlaceholderUrl(),
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 // --- Shortcode attribute: taxonomies="product-tags:1,2|product-brands:67" ---
369 // Any taxonomy registered on products, including store-registered ones.
370 foreach (Taxonomy::parseShortcodeTaxonomies($this->shortcodeAttributes['taxonomies']) as $taxonomy => $termIds) {
371 // A segment that resolved to nothing (unknown taxonomy, foreign or
372 // missing term) must match nothing — dropping it would widen the
373 // list to products the shortcode never asked for.
374 $termIds = $termIds ?: [0];
375 $existing = Arr::get($mergedTerms, $taxonomy, []);
376 $mergedTerms[$taxonomy] = array_values(array_unique(array_merge($existing, $termIds)));
377 }
378
379 // --- Shortcode attribute: sort_by ---
380 if (!empty($this->shortcodeAttributes['sort_by'])) {
381 $filters['sort_by'] = sanitize_text_field($this->shortcodeAttributes['sort_by']);
382 }
383
384 // --- Shortcode attribute: fulfillment_type / product_type, stock_availability, on_sale ---
385 // product_type is an alias for fulfillment_type
386 $productType = sanitize_text_field($this->shortcodeAttributes['product_type'] ?: $this->shortcodeAttributes['fulfillment_type']);
387 $onSale = in_array(strtolower($this->shortcodeAttributes['on_sale']), ['yes', '1', 'true'], true);
388
389 // merge $this->urlFilters and $filters
390 $filters = array_merge($filters, $this->urlFilters);
391
392 $params = [
393 "select" => '*',
394 "with" => ['detail', 'variants', 'categories', 'licensesMeta'],
395 "selected_status" => true,
396 "status" => $status,
397 "shop_app_default_filters" => $defaultFilters,
398 "default_filters" => $defaultFilters,
399 "taxonomy_filters" => $mergedTerms,
400 "paginate" => $this->shortcodeAttributes['per_page'],
401 "per_page" => $this->shortcodeAttributes['per_page'],
402 'filters' => $filters,
403 'paginate_using' => $paginatorMethod,
404 'pagination_type' => $paginatorMethod,
405 'allow_out_of_stock' => $allowOutOfStock,
406 'order_type' => $this->shortcodeAttributes['order_type'],
407 'live_filter' => $this->shortcodeAttributes['live_filter'],
408 'enable_filters' => $enableFilters,
409 'custom_filters' => $customFilters,
410 'include_ids' => $includeIds,
411 'exclude_ids' => $excludeIds,
412 'product_type' => $productType,
413 'on_sale' => $onSale,
414 'product_box_grid_size' => $this->shortcodeAttributes['product_box_grid_size'],
415 'view_mode' => $this->shortcodeAttributes['view_mode'],
416 'price_format' => $this->shortcodeAttributes['price_format'],
417 ];
418
419 return $params;
420 }
421
422 public function renderView()
423 {
424 ob_start();
425 (new ShopAppRenderer($this->getInitialProducts(), $this->getDefaultConfig()))
426 ->render();
427 return ob_get_clean();
428 }
429
430 public function enqueueAssets()
431 {
432 if (App::request()->get('action') === 'elementor') {
433 return;
434 }
435
436 AssetLoader::loadProductArchiveAssets();
437
438
439 //SingleProductHandler::enqueueAssets();
440 }
441
442 public function enqueueStyles()
443 {
444
445 }
446
447 public function enqueueScripts()
448 {
449
450 }
451
452
453 public function getTermIdsForFilter($defaultFilters): array
454 {
455 $ids = [];
456
457 $taxonomies = Taxonomy::getTaxonomies();
458 foreach ($taxonomies as $key => $taxonomy) {
459
460 $termIds = Arr::get($defaultFilters, $key, '');
461
462
463 if (is_array($termIds)) {
464 $ids = array_merge($ids, $termIds);
465 continue;
466 }
467 if (strlen($termIds) || Str::contains($termIds, ',')) {
468 $termIds = explode(',', $termIds);
469
470 } else {
471 $termIds = [];
472 }
473
474 $ids = array_merge($ids, $termIds);
475
476 }
477 return $ids;
478 }
479 }
480