PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.4.1
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.4.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 / Services / Filter / BaseFilter.php

BaseFilter.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.4.1, at app/Services/Filter/BaseFilter.php

1,227 lines 35.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\Services\Filter;
4
5 use FluentCart\App\App;
6 use FluentCart\App\Helpers\Helper;
7 use FluentCart\App\Models\Model;
8 use FluentCart\App\Services\DateTime\DateTime;
9 use FluentCart\App\Services\Filter\Concerns\HandleDateFilter;
10 use FluentCart\App\Services\Filter\Concerns\HandleRelationalFilter;
11 use FluentCart\Framework\Database\Orm\Builder;
12 use FluentCart\Framework\Http\Request\Request;
13 use FluentCart\Framework\Pagination\LengthAwarePaginator;
14 use FluentCart\Framework\Support\Arr;
15 use FluentCart\Framework\Support\Str;
16 use InvalidArgumentException;
17
18 /**
19 * Class BaseFilter
20 *
21 * Base class for filtering and querying models with simple and advanced filters.
22 *
23 * @package FluentCart\App\Services\Filter
24 */
25 abstract class BaseFilter
26 {
27 use HandleRelationalFilter, HandleDateFilter;
28
29
30 /**
31 * Determines if the filter type is simple or advanced.
32 *
33 * @var string
34 */
35 public string $filterType = 'simple';
36
37 /**
38 * default primary key of the table
39 *
40 * @var ?string
41 */
42 public ?string $primaryKey = null;
43
44 /**
45 * Default column used for sorting.
46 *
47 * @var string
48 */
49 public string $defaultSortBy = 'id';
50
51 /**
52 * Column used for sorting, dynamically set from arguments.
53 *
54 * @var string
55 */
56 public string $sortBy = '';
57
58 /**
59 * Default sorting order.
60 *
61 * @var string
62 */
63 public string $defaultSortType = 'desc';
64
65 /**
66 * Sorting order (asc/desc).
67 *
68 * @var string
69 */
70 public string $sortType = '';
71
72 /**
73 * Ids that must be loaded.
74 *
75 * @var array
76 */
77 public array $includeIds = [];
78
79 /**
80 * Relations to be loaded with the query.
81 *
82 * @var array
83 */
84 public array $with = [];
85
86 /**
87 * Select fields for the query.
88 *
89 * @var ?array
90 */
91 public array $select = [];
92
93
94 /**
95 * Model scopes.
96 *
97 * @var ?array
98 */
99 public ?array $scopes = [];
100
101 /**
102 * Search query string for simple filtering.
103 *
104 * @var string
105 */
106 public string $search = '';
107
108 /**
109 * Limit of records.
110 *
111 * @var ?int
112 */
113 public ?int $limit = null;
114
115
116 /**
117 * Number of records to retrieve per page.
118 *
119 * @var int
120 */
121 public int $perPage = 10;
122
123
124 /**
125 * Current page number
126 *
127 * @var ?int
128 */
129 public ?int $page = null;
130
131 /**
132 * The offset for paginated results.
133 *
134 * @var ?int
135 */
136 public ?int $offset = null;
137
138 /**
139 * The active view/tab to be filtered.
140 *
141 * @var string|null
142 */
143 public ?string $activeView = '';
144
145 /**
146 * HTTP request instance.
147 *
148 * @var Request
149 */
150 protected Request $request;
151
152 /**
153 * Query builder instance used for filtering and retrieving data.
154 *
155 * @var Builder|LengthAwarePaginator
156 */
157 public $query;
158
159 /**
160 * Additional filtering arguments.
161 *
162 * @var array
163 */
164 public array $args = [];
165
166 /**
167 * Parsed search groups for advanced filtering.
168 *
169 * @var array
170 */
171 public array $searchGroups = [];
172
173
174 /**
175 * User timezone
176 *
177 * @var ?string
178 */
179 public ?string $userTz = null;
180
181 /**
182 * The resolved SavedView model when active_view is a saved view slug.
183 *
184 * @var SavedView|null
185 */
186 protected $activeSavedView = null;
187
188
189 /**
190 * BaseFilter constructor.
191 *
192 * @param array $args Filtering arguments.
193 */
194 public function __construct(array $args = [])
195 {
196 $this->validateModel();
197 $this->parseArgs($args);
198 $this->query = $this->customQuery();
199 }
200
201 /**
202 * Validates the model instance.
203 *
204 * @return void
205 * @throws InvalidArgumentException
206 */
207 protected function validateModel()
208 {
209 $modelClass = $this->getModel();
210 $model = new $modelClass;
211 if (!$model instanceof Model) {
212 throw new InvalidArgumentException('Model class must be an instance of Model');
213 }
214
215 $this->primaryKey = $model->getKeyName();
216 $this->query = $model->newQuery();
217 }
218
219 /**
220 * Parses filtering arguments.
221 *
222 * @param array $args Filtering arguments.
223 * @return void
224 */
225 protected function parseArgs(array $args)
226 {
227
228 $this->args = $args;
229 $this->select = $this->parseSelect();
230 $this->filterType = Arr::get($args, $this->getParsableKey('filter_type'), $this->filterType);
231 $this->search = Arr::get($args, $this->getParsableKey('search'), $this->search);
232 $this->with = Arr::get($args, $this->getParsableKey('with'), $this->with);
233 $this->scopes = Arr::get($args, $this->getParsableKey('scopes'), $this->scopes);
234 $this->limit = Arr::get($args, $this->getParsableKey('limit'), $this->limit);
235 $this->offset = Arr::get($args, $this->getParsableKey('offset'), $this->offset);
236 $this->userTz = Arr::get($args, $this->getParsableKey('user_tz'), $this->userTz);
237 $this->includeIds = $this->parseIncludeIds();
238 $this->activeView = $this->parseAcceptedView();
239 $this->sortBy = $this->parseSortBy();
240 $this->sortType = $this->parseSortType();
241 $this->searchGroups = $this->parseSearchGroups();
242 $this->perPage = $this->parsePerPage();
243 $this->page = $this->parsePageNumber();
244 }
245
246 protected function parseSelect(): array
247 {
248 $select = Arr::get($this->args, $this->getParsableKey('select'));
249
250 if (!$select) {
251 return [];
252 }
253
254 if (is_string($select)) {
255 $select = explode(',', $select);
256 }
257
258 $parsedSelect = [];
259 foreach ($select as $selectItem) {
260 if (is_string($selectItem)) {
261 $parsedSelect[] = sanitize_text_field($selectItem);
262 }
263 }
264
265 return $parsedSelect;
266 }
267
268
269 protected function parseIncludeIds(): array
270 {
271 $includedIds = Arr::get($this->args, $this->getParsableKey('include_ids'), []);
272 if (is_string($includedIds)) {
273 $includedIds = explode(',', $includedIds);
274 }
275 return empty($includedIds) ? [] : Arr::wrap($includedIds);
276 }
277
278 protected function parsePerPage()
279 {
280 $perPage = Arr::get($this->args, $this->getParsableKey('per_page'));
281 if (is_numeric($perPage) && $perPage > 0 && $perPage < 200) {
282 return $perPage;
283 }
284 return $this->perPage;
285 }
286
287 protected function parsePageNumber(): ?int
288 {
289 $page = Arr::get($this->args, $this->getParsableKey('page'), $this->page);
290 return is_numeric($page) ? (int)$page : null;
291 }
292
293 /**
294 * Parses and validates the sorting column.
295 *
296 * @return string
297 */
298 protected function parseSortBy(): string
299 {
300 $sortBy = Arr::get($this->args, $this->getParsableKey('sort_by'));
301
302 if (empty($sortBy)) {
303 return $this->defaultSortBy;
304 }
305
306 /**
307 * @var Model $modelObject
308 */
309 $modelClass = $this->getModel();
310 $modelObject = new $modelClass;
311
312 return in_array($sortBy, $modelObject->getFillable()) ? $sortBy : $this->defaultSortBy;
313 }
314
315 /**
316 * Parses and validates the sorting order.
317 *
318 * @return string
319 */
320 protected function parseSortType(): string
321 {
322 $sortType = strtolower((string)Arr::get($this->args, $this->getParsableKey('sort_type'), ''));
323
324 return in_array($sortType, ['desc', 'asc']) ? $sortType : $this->defaultSortType;
325 }
326
327 /**
328 * Parses and validates the accepted view.
329 * First checks static tabs; if not found, fires a filter so Pro can resolve
330 * the slug to a saved view (returns an object/array with query_params).
331 *
332 * @return string|null
333 */
334 protected function parseAcceptedView(): ?string
335 {
336 $activeView = Arr::get($this->args, $this->getParsableKey('active_view'), $this->activeView);
337
338 if (empty($activeView)) {
339 return null;
340 }
341
342 // Static tab takes priority
343 if (Arr::has($this->tabsMap(), $activeView)) {
344 return $activeView;
345 }
346
347 // Ask Pro to resolve the slug for the current filter only.
348 $resolvedView = apply_filters('fluent_cart/filter_resolve_saved_view', null, [
349 'slug' => $activeView,
350 'filter_name' => static::getFilterName(),
351 'user_id' => get_current_user_id(),
352 ]);
353
354 if (is_array($resolvedView) || is_object($resolvedView)) {
355 $this->activeSavedView = $resolvedView;
356 return null;
357 }
358
359 // Backward compatibility for older Pro versions that only inject the admin table config blob.
360 $tableConfig = apply_filters('fluent_cart/admin_table_saved_views', [], [
361 'filterOptions' => []
362 ]);
363 foreach ($tableConfig as $entry) {
364 $savedViews = isset($entry['saved_views']) ? $entry['saved_views'] : [];
365 foreach ($savedViews as $view) {
366 if (Arr::get($view, 'slug') === $activeView) {
367 $this->activeSavedView = $view;
368 break 2;
369 }
370 }
371 }
372
373 return null;
374 }
375
376 /**
377 * Parses advanced search filters.
378 *
379 * @return array
380 */
381 protected function parseSearchGroups(?string $json = null): array
382 {
383 if ($json === null) {
384 $json = Arr::get($this->args, $this->getParsableKey('advanced_filters'), '[]');
385 }
386
387 $filters = [];
388
389 try {
390 $filters = json_decode($json, true);
391 } catch (\Exception $exception) {
392 // Ignore exception, return empty filters
393 }
394
395 if (empty($filters)) {
396 return [];
397 }
398
399 $groups = [];
400
401 foreach ($filters as $filterGroup) {
402 $group = [];
403 foreach ($filterGroup as $filterItem) {
404 if (count($filterItem['source']) != 2 || empty($filterItem['source'][0]) || empty($filterItem['source'][1]) || empty($filterItem['operator'])) {
405 continue;
406 }
407 $provider = $filterItem['source'][0];
408
409 if (!isset($group[$provider])) {
410 $group[$provider] = [];
411 }
412
413 $property = $filterItem['source'][1];
414
415 $filterData = [
416 'property' => $property,
417 'operator' => Arr::get($filterItem, 'operator'),
418 'value' => Arr::get($filterItem, 'value'),
419 'filter_type' => Arr::get($filterItem, 'filter_type'),
420 ];
421
422 if (Arr::get($filterData, 'filter_type') === 'relation') {
423 $filterData['relation'] = Arr::get($filterItem, 'relation', $property);
424 $filterData['column'] = Arr::get($filterItem, 'column', 'id');
425 }
426
427 $group[$provider][] = $filterData;
428
429
430 }
431
432 if ($group) {
433 $groups[] = $group;
434 }
435 }
436
437 return $groups;
438 }
439
440 /**
441 * Sets the HTTP request instance.
442 *
443 * @param Request $request
444 * @return $this
445 */
446 public function setRequest(Request $request): BaseFilter
447 {
448 $this->request = $request;
449 return $this;
450 }
451
452 /**
453 * Builds the query based on filters.
454 *
455 * @return Builder
456 */
457 public function buildQuery(): Builder
458 {
459 $this->buildCommonQuery();
460 $this->applyCurrentFilterLayer();
461
462 if ($this->activeSavedView) {
463 $this->applySavedViewFilter();
464 }
465
466 return $this->query;
467 }
468
469 protected function applyCurrentFilterLayer(): void
470 {
471 if ($this->filterType == 'simple') {
472 $this->applyActiveViewFilter();
473 $this->applySimpleFilter();
474 } else if ($this->filterType == 'advanced') {
475 $this->applyAdvancedFilter();
476 }
477 }
478
479 protected function applySavedViewFilter(?array $params = null): void
480 {
481 $params = $params ?? $this->getSavedViewParams();
482
483 if (empty($params)) {
484 return;
485 }
486
487 $this->query->where(function ($savedQuery) use ($params) {
488 $originalQuery = $this->query;
489 $this->query = $savedQuery;
490
491 $savedFilterType = Arr::get($params, 'filter_type', 'simple');
492
493 if ($savedFilterType === 'advanced') {
494 $savedGroups = $this->getSavedViewSearchGroups($params);
495 if (!empty($savedGroups)) {
496 $this->applyAdvancedFilter($savedGroups);
497 }
498 } else {
499 $savedActiveView = Arr::get($params, 'active_view');
500 if ($savedActiveView && Arr::has($this->tabsMap(), $savedActiveView)) {
501 $this->applyActiveViewFilter($savedActiveView);
502 }
503
504 $savedSearch = Arr::get($params, 'search', '');
505 if (!empty($savedSearch)) {
506 $this->applySimpleFilter($savedSearch);
507 }
508 }
509
510 $this->query = $originalQuery;
511 });
512 }
513
514 protected function getSavedViewParams(): array
515 {
516 $savedView = $this->activeSavedView;
517
518 return is_array($savedView)
519 ? Arr::get($savedView, 'query_params', [])
520 : (is_object($savedView) ? $savedView->query_params : []);
521 }
522
523 protected function getSavedViewSearchGroups(array $params): array
524 {
525 $json = Arr::get($params, 'advanced_filters', '[]');
526 if (is_array($json)) {
527 $json = wp_json_encode($json);
528 }
529
530 return $this->parseSearchGroups($json);
531 }
532
533 /**
534 * Builds the common query that should be applied in every query.
535 *
536 * @return void
537 */
538
539 protected function buildCommonQuery()
540 {
541 $this->applySelect();
542 $this->applyWith();
543 $this->applyScopes();
544
545 if (count($this->includeIds) > 0) {
546 $this->applyMustLoadIds();
547 }
548 $this->applySort();
549 }
550
551
552 public function applySelect()
553 {
554 if (empty($this->select)) {
555 return;
556 }
557 $this->query->select($this->select);
558 }
559
560 protected function getPrimaryKey(): string
561 {
562 return $this->primaryKey;
563 }
564
565 protected function applyMustLoadIds()
566 {
567
568 if ($this->search === '') {
569 $this->query = $this->query->whereIn(
570 $this->getPrimaryKey(),
571 $this->includeIds
572 )->orWhereNotNull($this->getPrimaryKey());
573 } else {
574 $this->query = $this->query->orWhereIn(
575 $this->getPrimaryKey(),
576 $this->includeIds
577 );
578 }
579
580 }
581
582 protected function applyLimit()
583 {
584 $this->query = $this->query->limit($this->limit);
585 }
586
587 protected function applyOffset()
588 {
589 $this->query = $this->query->offset($this->offset);
590 }
591
592 protected function applySort()
593 {
594 $this->query = $this->query->orderBy($this->sortBy, $this->sortType);
595 }
596
597 protected function applyWith()
598 {
599 $withs = Arr::wrap($this->with);
600
601 foreach ($withs as $with) {
602 if (Str::of($with)->lower()->endsWith('count')) {
603 //Get the relation name from count
604 //e.g.: variantsCount converts to variants
605 $relationName = Str::of($with)->substr(0, -5)->toString();
606 $this->query = $this->query->withCount($relationName);
607 } else {
608 $this->query = $this->query->with($with);
609 }
610 }
611
612 }
613
614 protected function applyScopes()
615 {
616 $scopes = Arr::wrap($this->scopes);
617 foreach ($scopes as $scope) {
618 if (is_array($scope)) {
619 $this->query = $this->query->{$scope[0]}($scope[1]);
620 continue;
621 }
622 $this->query = $this->query->{$scope}();
623 }
624 }
625
626 /**
627 * Applies advanced filters to the query.
628 *
629 * @return void
630 */
631 protected function applyAdvancedFilter(?array $searchGroups = null): void
632 {
633
634 if (!App::isProActive()) {
635 return;
636 }
637
638 $filtersGroups = $searchGroups ?? $this->searchGroups;
639 if (empty($filtersGroups)) {
640 return;
641 }
642
643
644 $filterName = static::getFilterName();
645 foreach ($filtersGroups as $groupIndex => $group) {
646
647
648 $method = $groupIndex == 0 ? 'where' : 'orWhere';
649
650 $this->query->{$method}(function ($query) use ($group, $filterName) {
651 foreach ($group as $providerName => $items) {
652 $items = $this->mergeRelationFilters($items);
653 foreach ($items as $item) {
654 if ($item['filter_type'] === 'custom') {
655 $filters = static::advanceFilterOptions();
656 $filter = Arr::get($filters, $providerName . '.children', null);
657 $property = Arr::get($item, 'property');
658 $isCallbackFound = false;
659 if (is_array($filter)) {
660 foreach ($filter as $filterItem) {
661 if ($filterItem['value'] === $item['property']) {
662 $callback = Arr::get($filterItem, 'callback', null);
663 if ($callback) {
664 $callback($this->query, $item);
665 $isCallbackFound = true;
666 }
667 break;
668 }
669 }
670 }
671
672 if ($isCallbackFound) {
673 return;
674 }
675 do_action_ref_array("fluent_cart/{$filterName}_filter/{$providerName}/{$item['property']}", [&$this->query, $item]);
676 } else {
677 $this->handleAdvanceFilter($query, $item);
678 }
679
680 }
681 }
682 });
683 }
684 }
685
686 /**
687 * Merge relation filter items that target the same relation field with compatible operators.
688 * This prevents multiple whereHas() subqueries ANDed together for the same relation column,
689 * which would return no results (e.g., license status IN ('active') AND license status IN ('expired')).
690 */
691 private function mergeRelationFilters(array $items): array
692 {
693 $merged = [];
694 $relationGroups = [];
695
696 foreach ($items as $item) {
697 if (Arr::get($item, 'filter_type') !== 'relation') {
698 $merged[] = $item;
699 continue;
700 }
701
702 $value = Arr::get($item, 'value');
703 $operator = $item['operator'] ?? '';
704 $mergeableOperators = ['in', 'contains', 'not_in', 'not_contains'];
705
706 if (!in_array($operator, $mergeableOperators)) {
707 $merged[] = $item;
708 continue;
709 }
710
711 // Normalize string value to array for merging
712 if (is_string($value) && $value !== '') {
713 $value = [$value];
714 $item['value'] = $value;
715 }
716
717 if (!is_array($value) || empty($value)) {
718 $merged[] = $item;
719 continue;
720 }
721
722 $key = $item['property'] . ':' . $item['relation'] . ':' . $item['column'] . ':' . $operator;
723 if (!isset($relationGroups[$key])) {
724 $relationGroups[$key] = $item;
725 } else {
726 $relationGroups[$key]['value'] = array_values(
727 array_unique(array_merge($relationGroups[$key]['value'], $value))
728 );
729 }
730 }
731
732 return array_merge($merged, array_values($relationGroups));
733 }
734
735 private function handleAdvanceFilter($query, $filterItem)
736 {
737 if (Arr::get($filterItem, 'filter_type') === 'relation') {
738 $this->handleRelation($query, $filterItem);
739 } else if (Arr::get($filterItem, 'filter_type') === 'date') {
740 $this->handleDate($query, $filterItem);
741 } else {
742 $this->handleOperator($query, $filterItem);
743 }
744
745
746 //
747 }
748
749 private function handleOperator(Builder &$query, array $filterItem)
750 {
751
752 $searchTerm = $filterItem['value'];
753
754 if (is_array($searchTerm)) {
755 $this->searchFromArray($query, $filterItem);
756 } else {
757 $this->searchFromString($query, $filterItem);
758 }
759 }
760
761 private function searchFromArray(Builder &$query, array $filterItem)
762 {
763 $property = $filterItem['property'];
764 $operator = $filterItem['operator'];
765 $searchTerm = $filterItem['value'];
766 $methodName = 'modify' . Str::studly($property . '_value');
767
768 if (in_array($property, $this->centColumns())) {
769 $searchTerm = array_map(function ($value) {
770 return Helper::toCent($value);
771 }, $searchTerm);
772 }
773 if (method_exists($this, $methodName)) {
774 $searchTerm = $this->{$methodName}($searchTerm, $filterItem, $query);
775 if ($searchTerm === null) {
776 return;
777 }
778 }
779
780 if (in_array($operator, ['in', 'contains'])) {
781 $query = $query->whereIn($property, $searchTerm);
782 } else if (in_array($operator, ['not_in', 'not_contains'])) {
783 $query = $query->whereNotIn($property, $searchTerm);
784 } elseif (in_array($operator, ['in_all', 'not_in_all'])) {
785 $condition = $operator === 'in_all' ? '=' : '!=';
786 foreach ($searchTerm as $term) {
787 $query = $query->where($property, $condition, $term);
788 }
789 } else if (in_array($operator, $this->getSimpleOperators(['::']))) {
790 $query = $query->where($property, $operator, $searchTerm);
791 }
792 }
793
794 private function searchFromString(Builder &$query, array $filterItem)
795 {
796 $property = $filterItem['property'];
797 $operator = $filterItem['operator'];
798 $searchTerm = $filterItem['value'];
799
800 $methodName = 'modify' . Str::studly($property . '_value');
801
802 if (in_array($property, $this->centColumns())) {
803 $searchTerm = Helper::toCent($searchTerm);
804 }
805 if (method_exists($this, $methodName)) {
806 $searchTerm = $this->{$methodName}($searchTerm, $filterItem, $query);
807
808 if ($searchTerm === null) {
809 return;
810 }
811 }
812
813
814 if (in_array($operator, ['contains', 'in'])) {
815 $query = $query->where($property, 'LIKE', '%' . $searchTerm . '%');
816 } else if (in_array($operator, ['not_contains', 'not_in'])) {
817 $query = $query->where($property, 'NOT LIKE', '%' . $searchTerm . '%');
818 } else if ($operator === 'is_null') {
819 $query = $query->where(function (Builder $q) use ($property) {
820 return $q->whereNull($property)
821 ->orWhere($property, '=', '');
822 });
823 } else if ($operator === 'not_null') {
824 $query = $query->where(function (Builder $q) use ($property) {
825 return $q->whereNotNull($property)
826 ->orWhere($property, '!=', '');
827 });
828 } else if (in_array($operator, $this->getSimpleOperators(['::']))) {
829 $query = $query->where($property, $operator, $searchTerm);
830 } else {
831 $query = $query->where($property, $operator, $searchTerm);
832 }
833 }
834
835 /**
836 * Apply the simple Filters.
837 *
838 * @return void
839 */
840
841 public abstract function applySimpleFilter(?string $search = null): void;
842
843 public abstract function applyActiveViewFilter(?string $activeView = null): void;
844
845 /**
846 * Return the maps of [table-column, tabs-name]
847 *
848 * @return array
849 */
850 public abstract function tabsMap(): array;
851
852 /**
853 * Return Model name
854 *
855 * @return string
856 */
857 public abstract function getModel(): string;
858
859
860 private function getDbColumns(): array
861 {
862 $modelClass = $this->getModel();
863 $model = new $modelClass;
864 // Get fillable columns and add primary key
865 return array_merge($model->getFillable(), [$this->primaryKey]);
866 }
867
868
869 /**
870 * Return the columns that are searchable
871 *
872 * @return array
873 */
874 public static function getSearchableFields(): array
875 {
876 $self = (new static);
877 $columns = $self->getDbColumns();
878 // Create case-insensitive lookup array
879 $searchableColumns = [];
880 foreach ($columns as $column) {
881 $searchableColumns[strtolower($column)] = $column;
882 $searchableColumns[$column] = $column;
883 }
884
885 return $searchableColumns;
886 }
887
888 /**
889 * Return the operators that are supported for simple filters
890 *
891 * @return array
892 */
893 public function getSimpleOperators($except = []): array
894 {
895 return Arr::except(
896 ['=', '!=', '>', '<', '>=', '<=', '::'],
897 $except
898 );
899 }
900
901 public function applySimpleOperatorFilter(?string $search = null): bool
902 {
903 $operators = $this->getSimpleOperators();
904
905 // check if search has an operator with regexp
906 $operatorPattern = '/\s*(' . implode('|', $operators) . ')\s*/';
907
908 $search = trim($search ?? $this->search);
909 if (preg_match($operatorPattern, $search, $matches)) {
910 $operator = $matches[1];
911 $searchParts = explode($operator, $search);
912
913 if (count($searchParts) >= 2) {
914 $column = trim($searchParts[0]);
915 $value = trim($searchParts[1]);
916
917 // Check if the column is valid
918 $validColumns = static::getSearchableFields();
919 $column = strtolower($column);
920 if ($columnSchema = Arr::get($validColumns, $column, null)) {
921
922 $type = Arr::get($columnSchema, 'type', 'string');
923
924 if ($type === 'custom') {
925 $callback = $columnSchema['callback'];
926 $callback($this->query, $value, $operator, $this);
927 return true;
928 }
929 if (is_array($columnSchema)) {
930 $column = $columnSchema['column'];
931 } else {
932 $column = $columnSchema;
933 }
934
935 if ($operator == '::') {
936 $values = explode('-', $value);
937 if (count($values) == 2) {
938 if (in_array($column, $this->centColumns())) {
939 $values[0] = Helper::toCent($values[0]);
940 $values[1] = Helper::toCent($values[1]);
941 } else if (in_array($column, $this->dateColumns())) {
942 $values[0] = DateTime::anyTimeToGmt($values[0], $this->userTz)->format('Y-m-d H:i:s');
943 $values[1] = DateTime::anyTimeToGmt($values[1], $this->userTz)->format('Y-m-d H:i:s');
944 }
945 $this->query->whereBetween($column, $values);
946 return true;
947 }
948 }
949
950 if (in_array($column, $this->centColumns())) {
951 $value = Helper::toCent($value);
952 } else if (in_array($column, $this->dateColumns())) {
953 $value = DateTime::anyTimeToGmt($value, $this->userTz)->format('Y-m-d H:i:s');
954 }
955
956 if ($this->shouldApplyMatchFilter($operator)) {
957 $this->applyMatchFilter($this->query, $column, $value, $operator);
958 } else {
959 $this->query->where($column, $operator, $value);
960 }
961
962
963 return true;
964 }
965 }
966 }
967
968 return false;
969 }
970
971 public function shouldApplyMatchFilter(string $operator): bool
972 {
973 $operator = trim($operator);
974 return $operator === '=' || $operator==='!=';
975 }
976
977 public function applyMatchFilter(Builder $query, string $column, $value, string $operator = '='): Builder
978 {
979 $value = sanitize_text_field($value);
980
981 $hasStartWildcard = Str::startsWith($value, '*');
982 $hasEndWildcard = Str::endsWith($value, '*');
983
984 // No wildcards → strict equality / inequality
985 if (!$hasStartWildcard && !$hasEndWildcard) {
986 return $query->where($column, $operator === '!=' ? '!=' : '=', $value);
987 }
988
989 // Remove * wildcards
990 $likeValue = $value;
991
992 if ($hasStartWildcard) {
993 $likeValue = ltrim($likeValue, '*');
994 }
995
996 if ($hasEndWildcard) {
997 $likeValue = rtrim($likeValue, '*');
998 }
999
1000 // Convert to SQL LIKE pattern
1001 if ($hasStartWildcard && $hasEndWildcard) {
1002 $likeValue = '%' . $likeValue . '%'; // *value*
1003 } elseif ($hasStartWildcard) {
1004 $likeValue = '%' . $likeValue; // *value
1005 } else {
1006 $likeValue = $likeValue . '%'; // value*
1007 }
1008
1009 $sqlOperator = $operator === '!=' ? 'NOT LIKE' : 'LIKE';
1010
1011 return $query->where($column, $sqlOperator, $likeValue);
1012 }
1013
1014
1015
1016
1017 public function centColumns(): array
1018 {
1019 return [];
1020 }
1021
1022 public function dateColumns(): array
1023 {
1024 return ['updated_at', 'created_at'];
1025 }
1026
1027 /**
1028 * Return the name of filter
1029 *
1030 * @return string
1031 */
1032
1033 public static abstract function getFilterName(): string;
1034
1035
1036 /**
1037 * Return the maps of [key, key-name]
1038 * It's used for parse the data
1039 *
1040 * @return array
1041 */
1042 public static function parseableKeyMap(): array
1043 {
1044 return [
1045 'filter_type' => 'filter_type',
1046 'with' => 'with',
1047 'search' => 'search',
1048 'limit' => 'limit',
1049 'offset' => 'offset',
1050 'active_view' => 'active_view',
1051 'sort_by' => 'sort_by',
1052 'sort_type' => 'sort_type',
1053 'advanced_filters' => 'advanced_filters',
1054 'per_page' => 'per_page',
1055 'include_ids' => 'include_ids',
1056 'scopes' => 'scopes',
1057 'user_tz' => 'user_tz',
1058 'select' => 'select',
1059 'page' => 'page'
1060 ];
1061 }
1062
1063 /**
1064 * Return the names of allowed keys preserved in data
1065 *
1066 * @return array
1067 */
1068
1069 public static function parseableKeys(): array
1070 {
1071 return static::parseableKeyMap();
1072 }
1073
1074 /**
1075 * Return the names of the kye, which should be used to parse the value
1076 *
1077 * @param string $key // Name of the Key
1078 * @return string
1079 */
1080 private function getParsableKey(string $key): string
1081 {
1082 return Arr::has(static::parseableKeyMap(), $key) ?
1083 static::parseableKeyMap()[$key] : $key;
1084
1085 }
1086
1087 public function query()
1088 {
1089 return $this->query;
1090 }
1091
1092 public function setQuery(Builder $query): BaseFilter
1093 {
1094 $this->query = $query;
1095 return $this;
1096 }
1097
1098 public function customQuery()
1099 {
1100 return $this->query;
1101 }
1102
1103 public function get()
1104 {
1105 //Apply limit and offset only when using get
1106 //While using pagination, limit and offset are auto calculated
1107
1108 $this->buildQuery();
1109
1110 if (!empty($this->limit)) {
1111 $this->applyLimit();
1112 }
1113
1114 if (!empty($this->offset)) {
1115 $this->applyOffset();
1116 }
1117 $filter = $this->getFilterName();
1118 $this->query = apply_filters("fluent_cart/{$filter}_list_filter_query", $this->query, $this->toArray());
1119 return $this->query->get();
1120 }
1121
1122 public function paginate($perPage = null): LengthAwarePaginator
1123 {
1124 $this->buildQuery();
1125 $perPage = empty($perPage) ? $this->perPage : $perPage;
1126 $filter = $this->getFilterName();
1127 $this->query = apply_filters("fluent_cart/{$filter}_list_filter_query", $this->query, $this->toArray());
1128 return $this->query->paginate(
1129 $perPage,
1130 ['*'],
1131 'page',
1132 $this->page
1133 );
1134 }
1135
1136 public static function fromRequest(Request $request): BaseFilter
1137 {
1138 return new static($request->only(
1139 static::parseableKeys()
1140 ));
1141 }
1142
1143 public static function make(array $args): BaseFilter
1144 {
1145 return new static($args);
1146 }
1147
1148 public static function getAdvanceFilterOptions(): ?array
1149 {
1150 $filterName = static::getFilterName();
1151 $options = apply_filters("fluent_cart/{$filterName}_filter_options", static::advanceFilterOptions());
1152 return is_array($options) ? array_values($options) : null;
1153 }
1154
1155 private static function advanceFilterOptions(): ?array
1156 {
1157 return null;
1158 }
1159
1160 public static function getCustomColumns()
1161 {
1162 // $data = [
1163 // [
1164 // 'title' => 'Product One',
1165 // 'meta' => [
1166 // 'max_price' => '100',
1167 // 'min_price' => '80',
1168 // ]
1169 // ],
1170 // [
1171 // 'title' => 'Product Two',
1172 // 'meta' => [
1173 // 'max_price' => '150',
1174 // 'min_price' => '90',
1175 // ]
1176 // ]
1177 // ];
1178 // $example_columns = [
1179 // 'title' => [
1180 // 'label' => 'Title',
1181 // 'accessor' => 'title',
1182 // 'as_link' => false,
1183 // ],
1184 // 'max_price' => [
1185 // 'label' => 'Max Price',
1186 // 'accessor' => 'meta.max_price'
1187 // ],
1188 // 'min_price' => [
1189 // 'label' => 'Min Price',
1190 // 'accessor' => 'meta.min_price'
1191 // ]
1192 // ];
1193 $filterName = static::getFilterName();
1194 return apply_filters("fluent_cart/{$filterName}_table_columns", []);
1195 }
1196
1197 public static function getTableFilterOptions(): array
1198 {
1199 return [
1200 'advance' => static::getAdvanceFilterOptions(),
1201 'guide' => static::getSearchableFields(),
1202 'columns' => static::getCustomColumns(),
1203 ];
1204 }
1205
1206 public function toArray(): array
1207 {
1208 return [
1209 'select' => $this->select,
1210 'filterType' => $this->filterType,
1211 'search' => $this->search,
1212 'with' => $this->with,
1213 'scopes' => $this->scopes,
1214 'limit' => $this->limit,
1215 'offset' => $this->offset,
1216 'userTz' => $this->userTz,
1217 'includeIds' => $this->includeIds,
1218 'activeView' => $this->activeView,
1219 'sortBy' => $this->sortBy,
1220 'sortType' => $this->sortType,
1221 'searchGroups' => $this->searchGroups,
1222 'perPage' => $this->perPage,
1223 ];
1224 }
1225
1226 }
1227