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

1,177 lines 33.2 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 foreach ($items as $item) {
653 if ($item['filter_type'] === 'custom') {
654 $filters = static::advanceFilterOptions();
655 $filter = Arr::get($filters, $providerName . '.children', null);
656 $property = Arr::get($item, 'property');
657 $isCallbackFound = false;
658 if (is_array($filter)) {
659 foreach ($filter as $filterItem) {
660 if ($filterItem['value'] === $item['property']) {
661 $callback = Arr::get($filterItem, 'callback', null);
662 if ($callback) {
663 $callback($this->query, $item);
664 $isCallbackFound = true;
665 }
666 break;
667 }
668 }
669 }
670
671 if ($isCallbackFound) {
672 return;
673 }
674 do_action_ref_array("fluent_cart/{$filterName}_filter/{$providerName}/{$item['property']}", [&$this->query, $item]);
675 } else {
676 $this->handleAdvanceFilter($query, $item);
677 }
678
679 }
680 }
681 });
682 }
683 }
684
685 private function handleAdvanceFilter($query, $filterItem)
686 {
687 if (Arr::get($filterItem, 'filter_type') === 'relation') {
688 $this->handleRelation($query, $filterItem);
689 } else if (Arr::get($filterItem, 'filter_type') === 'date') {
690 $this->handleDate($query, $filterItem);
691 } else {
692 $this->handleOperator($query, $filterItem);
693 }
694
695
696 //
697 }
698
699 private function handleOperator(Builder &$query, array $filterItem)
700 {
701
702 $searchTerm = $filterItem['value'];
703
704 if (is_array($searchTerm)) {
705 $this->searchFromArray($query, $filterItem);
706 } else {
707 $this->searchFromString($query, $filterItem);
708 }
709 }
710
711 private function searchFromArray(Builder &$query, array $filterItem)
712 {
713 $property = $filterItem['property'];
714 $operator = $filterItem['operator'];
715 $searchTerm = $filterItem['value'];
716 $methodName = 'modify' . Str::studly($property . '_value');
717
718 if (in_array($property, $this->centColumns())) {
719 $searchTerm = array_map(function ($value) {
720 return Helper::toCent($value);
721 }, $searchTerm);
722 }
723 if (method_exists($this, $methodName)) {
724 $searchTerm = $this->{$methodName}($searchTerm, $filterItem, $query);
725 if ($searchTerm === null) {
726 return;
727 }
728 }
729
730 if (in_array($operator, ['in', 'contains'])) {
731 $query = $query->whereIn($property, $searchTerm);
732 } else if (in_array($operator, ['not_in', 'not_contains'])) {
733 $query = $query->whereNotIn($property, $searchTerm);
734 } elseif (in_array($operator, ['in_all', 'not_in_all'])) {
735 $condition = $operator === 'in_all' ? '=' : '!=';
736 foreach ($searchTerm as $term) {
737 $query = $query->where($property, $condition, $term);
738 }
739 } else if (in_array($operator, $this->getSimpleOperators(['::']))) {
740 $query = $query->where($property, $operator, $searchTerm);
741 }
742 }
743
744 private function searchFromString(Builder &$query, array $filterItem)
745 {
746 $property = $filterItem['property'];
747 $operator = $filterItem['operator'];
748 $searchTerm = $filterItem['value'];
749
750 $methodName = 'modify' . Str::studly($property . '_value');
751
752 if (in_array($property, $this->centColumns())) {
753 $searchTerm = Helper::toCent($searchTerm);
754 }
755 if (method_exists($this, $methodName)) {
756 $searchTerm = $this->{$methodName}($searchTerm, $filterItem, $query);
757
758 if ($searchTerm === null) {
759 return;
760 }
761 }
762
763
764 if (in_array($operator, ['contains', 'in'])) {
765 $query = $query->where($property, 'LIKE', '%' . $searchTerm . '%');
766 } else if (in_array($operator, ['not_contains', 'not_in'])) {
767 $query = $query->where($property, 'NOT LIKE', '%' . $searchTerm . '%');
768 } else if ($operator === 'is_null') {
769 $query = $query->where(function (Builder $q) use ($property) {
770 return $q->whereNull($property)
771 ->orWhere($property, '=', '');
772 });
773 } else if ($operator === 'not_null') {
774 $query = $query->where(function (Builder $q) use ($property) {
775 return $q->whereNotNull($property)
776 ->orWhere($property, '!=', '');
777 });
778 } else if (in_array($operator, $this->getSimpleOperators(['::']))) {
779 $query = $query->where($property, $operator, $searchTerm);
780 } else {
781 $query = $query->where($property, $operator, $searchTerm);
782 }
783 }
784
785 /**
786 * Apply the simple Filters.
787 *
788 * @return void
789 */
790
791 public abstract function applySimpleFilter(?string $search = null): void;
792
793 public abstract function applyActiveViewFilter(?string $activeView = null): void;
794
795 /**
796 * Return the maps of [table-column, tabs-name]
797 *
798 * @return array
799 */
800 public abstract function tabsMap(): array;
801
802 /**
803 * Return Model name
804 *
805 * @return string
806 */
807 public abstract function getModel(): string;
808
809
810 private function getDbColumns(): array
811 {
812 $modelClass = $this->getModel();
813 $model = new $modelClass;
814 // Get fillable columns and add primary key
815 return array_merge($model->getFillable(), [$this->primaryKey]);
816 }
817
818
819 /**
820 * Return the columns that are searchable
821 *
822 * @return array
823 */
824 public static function getSearchableFields(): array
825 {
826 $self = (new static);
827 $columns = $self->getDbColumns();
828 // Create case-insensitive lookup array
829 $searchableColumns = [];
830 foreach ($columns as $column) {
831 $searchableColumns[strtolower($column)] = $column;
832 $searchableColumns[$column] = $column;
833 }
834
835 return $searchableColumns;
836 }
837
838 /**
839 * Return the operators that are supported for simple filters
840 *
841 * @return array
842 */
843 public function getSimpleOperators($except = []): array
844 {
845 return Arr::except(
846 ['=', '!=', '>', '<', '>=', '<=', '::'],
847 $except
848 );
849 }
850
851 public function applySimpleOperatorFilter(?string $search = null): bool
852 {
853 $operators = $this->getSimpleOperators();
854
855 // check if search has an operator with regexp
856 $operatorPattern = '/\s*(' . implode('|', $operators) . ')\s*/';
857
858 $search = trim($search ?? $this->search);
859 if (preg_match($operatorPattern, $search, $matches)) {
860 $operator = $matches[1];
861 $searchParts = explode($operator, $search);
862
863 if (count($searchParts) >= 2) {
864 $column = trim($searchParts[0]);
865 $value = trim($searchParts[1]);
866
867 // Check if the column is valid
868 $validColumns = static::getSearchableFields();
869 $column = strtolower($column);
870 if ($columnSchema = Arr::get($validColumns, $column, null)) {
871
872 $type = Arr::get($columnSchema, 'type', 'string');
873
874 if ($type === 'custom') {
875 $callback = $columnSchema['callback'];
876 $callback($this->query, $value, $operator, $this);
877 return true;
878 }
879 if (is_array($columnSchema)) {
880 $column = $columnSchema['column'];
881 } else {
882 $column = $columnSchema;
883 }
884
885 if ($operator == '::') {
886 $values = explode('-', $value);
887 if (count($values) == 2) {
888 if (in_array($column, $this->centColumns())) {
889 $values[0] = Helper::toCent($values[0]);
890 $values[1] = Helper::toCent($values[1]);
891 } else if (in_array($column, $this->dateColumns())) {
892 $values[0] = DateTime::anyTimeToGmt($values[0], $this->userTz)->format('Y-m-d H:i:s');
893 $values[1] = DateTime::anyTimeToGmt($values[1], $this->userTz)->format('Y-m-d H:i:s');
894 }
895 $this->query->whereBetween($column, $values);
896 return true;
897 }
898 }
899
900 if (in_array($column, $this->centColumns())) {
901 $value = Helper::toCent($value);
902 } else if (in_array($column, $this->dateColumns())) {
903 $value = DateTime::anyTimeToGmt($value, $this->userTz)->format('Y-m-d H:i:s');
904 }
905
906 if ($this->shouldApplyMatchFilter($operator)) {
907 $this->applyMatchFilter($this->query, $column, $value, $operator);
908 } else {
909 $this->query->where($column, $operator, $value);
910 }
911
912
913 return true;
914 }
915 }
916 }
917
918 return false;
919 }
920
921 public function shouldApplyMatchFilter(string $operator): bool
922 {
923 $operator = trim($operator);
924 return $operator === '=' || $operator==='!=';
925 }
926
927 public function applyMatchFilter(Builder $query, string $column, $value, string $operator = '='): Builder
928 {
929 $value = sanitize_text_field($value);
930
931 $hasStartWildcard = Str::startsWith($value, '*');
932 $hasEndWildcard = Str::endsWith($value, '*');
933
934 // No wildcards → strict equality / inequality
935 if (!$hasStartWildcard && !$hasEndWildcard) {
936 return $query->where($column, $operator === '!=' ? '!=' : '=', $value);
937 }
938
939 // Remove * wildcards
940 $likeValue = $value;
941
942 if ($hasStartWildcard) {
943 $likeValue = ltrim($likeValue, '*');
944 }
945
946 if ($hasEndWildcard) {
947 $likeValue = rtrim($likeValue, '*');
948 }
949
950 // Convert to SQL LIKE pattern
951 if ($hasStartWildcard && $hasEndWildcard) {
952 $likeValue = '%' . $likeValue . '%'; // *value*
953 } elseif ($hasStartWildcard) {
954 $likeValue = '%' . $likeValue; // *value
955 } else {
956 $likeValue = $likeValue . '%'; // value*
957 }
958
959 $sqlOperator = $operator === '!=' ? 'NOT LIKE' : 'LIKE';
960
961 return $query->where($column, $sqlOperator, $likeValue);
962 }
963
964
965
966
967 public function centColumns(): array
968 {
969 return [];
970 }
971
972 public function dateColumns(): array
973 {
974 return ['updated_at', 'created_at'];
975 }
976
977 /**
978 * Return the name of filter
979 *
980 * @return string
981 */
982
983 public static abstract function getFilterName(): string;
984
985
986 /**
987 * Return the maps of [key, key-name]
988 * It's used for parse the data
989 *
990 * @return array
991 */
992 public static function parseableKeyMap(): array
993 {
994 return [
995 'filter_type' => 'filter_type',
996 'with' => 'with',
997 'search' => 'search',
998 'limit' => 'limit',
999 'offset' => 'offset',
1000 'active_view' => 'active_view',
1001 'sort_by' => 'sort_by',
1002 'sort_type' => 'sort_type',
1003 'advanced_filters' => 'advanced_filters',
1004 'per_page' => 'per_page',
1005 'include_ids' => 'include_ids',
1006 'scopes' => 'scopes',
1007 'user_tz' => 'user_tz',
1008 'select' => 'select',
1009 'page' => 'page'
1010 ];
1011 }
1012
1013 /**
1014 * Return the names of allowed keys preserved in data
1015 *
1016 * @return array
1017 */
1018
1019 public static function parseableKeys(): array
1020 {
1021 return static::parseableKeyMap();
1022 }
1023
1024 /**
1025 * Return the names of the kye, which should be used to parse the value
1026 *
1027 * @param string $key // Name of the Key
1028 * @return string
1029 */
1030 private function getParsableKey(string $key): string
1031 {
1032 return Arr::has(static::parseableKeyMap(), $key) ?
1033 static::parseableKeyMap()[$key] : $key;
1034
1035 }
1036
1037 public function query()
1038 {
1039 return $this->query;
1040 }
1041
1042 public function setQuery(Builder $query): BaseFilter
1043 {
1044 $this->query = $query;
1045 return $this;
1046 }
1047
1048 public function customQuery()
1049 {
1050 return $this->query;
1051 }
1052
1053 public function get()
1054 {
1055 //Apply limit and offset only when using get
1056 //While using pagination, limit and offset are auto calculated
1057
1058 $this->buildQuery();
1059
1060 if (!empty($this->limit)) {
1061 $this->applyLimit();
1062 }
1063
1064 if (!empty($this->offset)) {
1065 $this->applyOffset();
1066 }
1067 $filter = $this->getFilterName();
1068 $this->query = apply_filters("fluent_cart/{$filter}_list_filter_query", $this->query, $this->toArray());
1069 return $this->query->get();
1070 }
1071
1072 public function paginate($perPage = null): LengthAwarePaginator
1073 {
1074 $this->buildQuery();
1075 $perPage = empty($perPage) ? $this->perPage : $perPage;
1076 $filter = $this->getFilterName();
1077 $this->query = apply_filters("fluent_cart/{$filter}_list_filter_query", $this->query, $this->toArray());
1078 return $this->query->paginate(
1079 $perPage,
1080 ['*'],
1081 'page',
1082 $this->page
1083 );
1084 }
1085
1086 public static function fromRequest(Request $request): BaseFilter
1087 {
1088 return new static($request->only(
1089 static::parseableKeys()
1090 ));
1091 }
1092
1093 public static function make(array $args): BaseFilter
1094 {
1095 return new static($args);
1096 }
1097
1098 public static function getAdvanceFilterOptions(): ?array
1099 {
1100 $filterName = static::getFilterName();
1101 $options = apply_filters("fluent_cart/{$filterName}_filter_options", static::advanceFilterOptions());
1102 return is_array($options) ? array_values($options) : null;
1103 }
1104
1105 private static function advanceFilterOptions(): ?array
1106 {
1107 return null;
1108 }
1109
1110 public static function getCustomColumns()
1111 {
1112 // $data = [
1113 // [
1114 // 'title' => 'Product One',
1115 // 'meta' => [
1116 // 'max_price' => '100',
1117 // 'min_price' => '80',
1118 // ]
1119 // ],
1120 // [
1121 // 'title' => 'Product Two',
1122 // 'meta' => [
1123 // 'max_price' => '150',
1124 // 'min_price' => '90',
1125 // ]
1126 // ]
1127 // ];
1128 // $example_columns = [
1129 // 'title' => [
1130 // 'label' => 'Title',
1131 // 'accessor' => 'title',
1132 // 'as_link' => false,
1133 // ],
1134 // 'max_price' => [
1135 // 'label' => 'Max Price',
1136 // 'accessor' => 'meta.max_price'
1137 // ],
1138 // 'min_price' => [
1139 // 'label' => 'Min Price',
1140 // 'accessor' => 'meta.min_price'
1141 // ]
1142 // ];
1143 $filterName = static::getFilterName();
1144 return apply_filters("fluent_cart/{$filterName}_table_columns", []);
1145 }
1146
1147 public static function getTableFilterOptions(): array
1148 {
1149 return [
1150 'advance' => static::getAdvanceFilterOptions(),
1151 'guide' => static::getSearchableFields(),
1152 'columns' => static::getCustomColumns(),
1153 ];
1154 }
1155
1156 public function toArray(): array
1157 {
1158 return [
1159 'select' => $this->select,
1160 'filterType' => $this->filterType,
1161 'search' => $this->search,
1162 'with' => $this->with,
1163 'scopes' => $this->scopes,
1164 'limit' => $this->limit,
1165 'offset' => $this->offset,
1166 'userTz' => $this->userTz,
1167 'includeIds' => $this->includeIds,
1168 'activeView' => $this->activeView,
1169 'sortBy' => $this->sortBy,
1170 'sortType' => $this->sortType,
1171 'searchGroups' => $this->searchGroups,
1172 'perPage' => $this->perPage,
1173 ];
1174 }
1175
1176 }
1177