PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.4
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.4
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 trunk 1.2.0 All 47 releases
fluent-cart / app / Http / Controllers / ShopController.php

ShopController.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.6.4, at app/Http/Controllers/ShopController.php

359 lines 12.8 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\Http\Controllers;
4
5 use FluentCart\Api\Resource\ShopResource;
6 use FluentCart\Api\Sanitizer\Sanitizer;
7 use FluentCart\Api\StoreSettings;
8 use FluentCart\App\Helpers\Helper;
9 use FluentCart\App\Models\Product;
10 use FluentCart\App\Services\Renderer\ProductListRenderer;
11 use FluentCart\App\Services\Renderer\ProductModalRenderer;
12 use FluentCart\App\Services\Renderer\ProductRenderer;
13 use FluentCart\App\Vite;
14 use FluentCart\Framework\Http\Request\Request;
15 use FluentCart\Framework\Pagination\CursorPaginator;
16 use FluentCart\Framework\Support\Arr;
17 use FluentCart\Framework\Support\Collection;
18 use FluentCart\Api\Taxonomy;
19 use FluentCart\App\Services\Renderer\SearchBarRenderer;
20
21 class ShopController extends Controller
22 {
23 public function getProducts(Request $request): array
24 {
25 $defaultFilters = $request->get('default_filters', []);
26 $filters = $request->get('filters', []);
27
28 // Merge sort_by from default_filters (shortcode) if not set by interactive filters
29 $defaultSortBy = Arr::get($defaultFilters, 'sort_by', '');
30 if ($defaultSortBy && empty(Arr::get($filters, 'sort_by'))) {
31 if (!is_array($filters)) {
32 $filters = [];
33 }
34 $filters['sort_by'] = sanitize_text_field($defaultSortBy);
35 }
36
37 $defaultTermIds = Helper::parseTermIdsForFilter($defaultFilters);
38 $filterTermIds = Helper::parseTermIdsForFilter($filters);
39 $mergedTermIds = Helper::mergeTermIdsForFilter($defaultTermIds, $filterTermIds);
40
41 $status = ["post_status" => ["column" => "post_status", "operator" => "in", "value" => ["publish"]]];
42 $allowOutOfStock = $request->get('allow_out_of_stock', false) == true;
43 $cursor = $request->get('cursor', null);
44 $orderType = $request->get('order_type', 'DESC');
45 // The storefront never chooses its own eager loads. This route is public
46 // (frontend_routes.php, PublicPolicy), and forwarding the request's `with`
47 // let an anonymous caller walk arbitrary relation chains — e.g.
48 // `?with[]=orderItems.order.customer.wpUser` reached the WordPress users
49 // table. The two relations below are the ones the appends applied after
50 // this call already touch (`thumbnail` reads `detail`, `has_subscription`
51 // reads `variants`), so the response shape is unchanged and they are now
52 // eager loaded instead of lazily fetched per row.
53 $with = ['detail', 'variants'];
54
55 // Shortcode filter params from AJAX
56 $includeIds = array_slice(array_values(array_filter(array_map('intval', (array) $request->get('include_ids', [])), function ($id) { return $id > 0; })), 0, 100);
57 $excludeIds = array_slice(array_values(array_filter(array_map('intval', (array) $request->get('exclude_ids', [])), function ($id) { return $id > 0; })), 0, 100);
58 $productType = sanitize_text_field($request->get('product_type', ''));
59 $onSale = !empty($request->get('on_sale'));
60
61 $params = [
62 'cursor' => $cursor,
63 "order_type" => $orderType,
64 "select" => '*',
65 "with" => $with,
66 "selected_status" => true,
67 "status" => $status,
68 "default_filters" => $defaultFilters,
69 "filters" => $filters,
70 'taxonomy_filters' => $mergedTermIds,
71 'allow_out_of_stock' => $allowOutOfStock,
72 "per_page" => $request->getSafe('per_page', Sanitizer::SANITIZE_TEXT_FIELD) ?: 10,
73 'paginate_using' => $request->getSafe('paginate_using', Sanitizer::SANITIZE_TEXT_FIELD),
74 'include_ids' => $includeIds,
75 'exclude_ids' => $excludeIds,
76 'product_type' => $productType,
77 'on_sale' => $onSale,
78 ];
79
80 $products = ShopResource::get($params);
81
82 $collection = $products['products']->getCollection();
83
84 // The appended has_subscription accessor reads $product->variants during
85 // serialization, so variants reach the response even without with[]=variants.
86 // Eager-load them once for the whole page (single query) so the sensitive
87 // fields can be hidden before serialization.
88 $collection->loadMissing('variants');
89
90 $products['products']->setCollection(
91 $collection->transform(function ($product) {
92 $product->setAppends(['view_url', 'has_subscription', 'thumbnail']);
93 $product->makeHidden(['post_content']);
94 if ($product->detail !== null) {
95 $product->detail->makeHidden(['item_cost', 'editing_stage', 'stock', 'manage_stock', 'manage_cost', 'settings']);
96 }
97 $product->variants->each(function ($variant) {
98 $variant->makeHidden(['item_cost', 'manage_cost', 'manage_stock', 'total_stock', 'available', 'committed', 'on_hold']);
99
100 // other_info is a JSON blob that makeHidden cannot reach into; strip its
101 // admin-only keys while keeping the storefront display fields (payment_type,
102 // repeat_interval, trial_days, billing_summary, weight/dimensions).
103 $info = $variant->other_info;
104 if (is_array($info)) {
105 foreach (['tax_class', 'tax_exempt', 'package_slug', 'bundle_child_ids', 'variation_type', 'is_bundle_product'] as $internalKey) {
106 unset($info[$internalKey]);
107 }
108 $variant->other_info = $info;
109 }
110 });
111 return $product;
112 })
113 );
114
115 return [
116 'products' => $products,
117 ];
118
119 }
120
121 public function getProductViews(Request $request): array
122 {
123 // $products = $this->getProducts($request)['products'];
124 // $products = $products->toArray();
125 $page = Arr::get($request->all(), 'current_page', 1);
126 $perPage = Arr::get($request->all(), 'per_page', 10);
127
128 $products = $this->getProducts($request);
129 $total = Arr::get($products, 'products.total', 0);
130 $templateProvider = $request->get('template_provider', '');
131 $clientId = $request->get('client_id', '');
132 if ($templateProvider) {
133 $preLoadedView = apply_filters('fluent_cart/products_views/preload_collection_' . $templateProvider, '', [
134 'client_id' => $clientId,
135 'products' => Arr::get($products, 'products.products', []),
136 'total' => $total,
137 'requestData' => $request->all()
138 ]);
139
140 if ($preLoadedView) {
141
142 $from = $total > 0 ? (($page - 1) * $perPage) + 1 : 0;
143 $to = $total > 0 ? min($total, $page * $perPage) : 0;
144
145 if ($from <= 0) {
146 $from = 1;
147 }
148
149
150 if ($to == 0) {
151 $to = 1;
152 }
153
154 if ($page == 0) {
155 $page = 1;
156 }
157
158 return [
159 'products' => [
160 'views' => $preLoadedView,
161 'current_page' => $page,
162 'last_page' => max((int)ceil($total / $request->get('per_page', 10)), 1),
163 'total' => $total,
164 'per_page' => $perPage,
165 'from' => $from,
166 'to' => $to,
167 ]
168 ];
169
170 }
171 }
172
173 $clientId = 'fct_product_loop_client_' . $clientId;
174
175 $variable = null;
176
177 if ($clientId) {
178 $variable = get_transient($clientId);
179 $variable = $variable['markup'] ?? null;
180 }
181
182 if ($variable) {
183 $view = do_blocks($variable);
184
185 $from = $total > 0 ? (($page - 1) * $perPage) + 1 : 0;
186 $to = $total > 0 ? min($total, $page * $perPage) : 0;
187
188 if ($from <= 0) {
189 $from = 1;
190 }
191
192
193 if ($to == 0) {
194 $to = 1;
195 }
196
197 if ($page == 0) {
198 $page = 1;
199 }
200
201 return [
202 'products' => [
203 'views' => $view,
204 'current_page' => $page,
205 'last_page' => max((int)ceil($total / $request->get('per_page', 10)), 1),
206 'total' => $total,
207 'per_page' => $perPage,
208 'from' => $from,
209 'to' => $to,
210 ]
211 ];
212
213 }
214
215 $products = $this->getProducts($request);
216
217 $originalProducts = $products['products'];
218
219 if ($originalProducts instanceof CursorPaginator) {
220 $cursor = wp_parse_args(wp_parse_url($originalProducts->nextPageUrl(), PHP_URL_QUERY));
221 }
222
223
224 $total = $products['products']['total'];
225
226 $perPage = $request->get('per_page', 10);
227 $products['total'] = $total;
228 $products['last_page'] = max((int)ceil($total / $perPage), 1);
229 $hideExcerpt = filter_var($request->get('hide_excerpt', false), FILTER_VALIDATE_BOOLEAN);
230 ob_start();
231 if (($products['total'])) {
232 (new ProductListRenderer(Arr::get($products, 'products.products'), null, null, ['hide_excerpt' => $hideExcerpt]))->renderProductList();
233 } else {
234 ProductRenderer::renderNoProductFound();
235 }
236
237 $view = ob_get_clean();
238
239 $products['views'] = $view;
240 $products['per_page'] = $perPage;
241 $from = ($page - 1) * $perPage + 1;
242 $to = min($total, $page * $perPage);
243
244 if ($from <= 0) {
245 $from = 1;
246 }
247
248 $products['from'] = $from;
249
250 if ($to == 0) {
251 $to = 1;
252 }
253
254 if ($page == 0) {
255 $page = 1;
256 }
257
258 $products['to'] = $to;
259 $products['page'] = $page;
260 $products['current_page'] = $page;
261
262 unset($products['data']);
263 unset($products['products']);
264
265 return [
266 'products' => $products
267 ];
268 }
269
270 public function getTermIdsFromDefaultFilter($defaultFilters): array
271 {
272 $ids = [];
273 $taxonomies = Taxonomy::getTaxonomies();
274 foreach ($taxonomies as $key => $taxonomy) {
275 $defaultTerms = array_filter(explode(',', Arr::get($defaultFilters, $key, '')));
276 $ids = array_merge(
277 $ids,
278 $defaultTerms
279 );
280 }
281
282 return Collection::make($ids)->map(function ($termId) {
283 return sanitize_text_field((string)$termId);
284 })->toArray();
285 }
286
287 public function getTermIdsFromFilter($filters): array
288 {
289 $taxonomies = Taxonomy::getTaxonomies();
290 if (is_string($filters)) {
291 $filters = json_decode($filters, true);
292 }
293 $formattedFilters = [];
294
295 foreach ($taxonomies as $key => $taxonomy) {
296 $terms = Arr::get($filters, $key, []);
297 if (!is_array($terms)) {
298 $terms = [$terms];
299 }
300
301 if (!empty($terms)) {
302 $terms = array_map(function ($term) {
303 return sanitize_text_field((string)$term);
304 }, $terms);
305
306 $formattedFilters[$key] = $terms;
307 }
308
309
310 }
311
312 return $formattedFilters;
313 }
314
315 public function searchProduct(Request $request)
316 {
317
318 $searchValue = $request->getSafe('post_title', 'sanitize_text_field');
319 $urlMode = $request->getSafe('url_mode', 'sanitize_text_field');
320 $termId = $request->getSafe('termId', 'intval');
321 $showThumbnail = filter_var($request->get('show_thumbnail', true), FILTER_VALIDATE_BOOLEAN);
322
323 $defaultFilters =
324 [
325 "wildcard" => $searchValue,
326 ];
327
328 $status = ["post_status" => ["column" => "post_status", "operator" => "in", "value" => ["publish"]]];
329
330 $params = [
331 "select" => ['ID', 'guid', 'post_title'],
332 "with" => ['wpTerms', 'detail.galleryImage'],
333 "selected_status" => true,
334 "status" => $status,
335 "default_filters" => $defaultFilters,
336 ];
337
338 if (!empty($termId)) {
339 $params["taxonomy_filters"] = [
340 'product-categories' => Arr::wrap($termId)
341 ];
342 }
343
344 $results = ShopResource::get($params);
345 $products = $results['products'];
346 ob_start();
347
348 (new SearchBarRenderer([
349 'url_mode' => $urlMode,
350 'show_thumbnail' => $showThumbnail,
351 ]))->renderResultItems($products);
352
353 $view = ob_get_clean();
354 return $this->response->sendSuccess([
355 'htmlView' => $view
356 ]);
357 }
358 }
359