PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.7.0
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.7.0
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 1.3.20 All 50 releases
fluent-cart / app / Services / Filter / ReviewFilter.php

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

295 lines 10.7 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\Models\ProductReview;
6 use FluentCart\Framework\Support\Arr;
7
8 class ReviewFilter extends BaseFilter
9 {
10 public string $defaultSortBy = 'id';
11
12 public function applySimpleFilter(?string $search = null): void
13 {
14 $isApplied = $this->applySimpleOperatorFilter();
15 if ($isApplied) {
16 return;
17 }
18
19 $searchTerm = $search ?? $this->search;
20
21 $this->query = $this->query->when($searchTerm, function ($query, $search) {
22 // Escape LIKE wildcards first — the search() scope adds the
23 // surrounding %, so a literal % or _ in the term must not widen
24 // the match.
25 $searchValue = addcslashes(trim($search), '\\%_');
26
27 return $query->where(function ($query) use ($searchValue) {
28 $query->search([
29 'reviewer_name' => ['column' => 'reviewer_name', 'operator' => 'like_all', 'value' => $searchValue],
30 'reviewer_email' => ['column' => 'reviewer_email', 'operator' => 'or_like_all', 'value' => $searchValue],
31 'title' => ['column' => 'title', 'operator' => 'or_like_all', 'value' => $searchValue],
32 'review' => ['column' => 'review', 'operator' => 'or_like_all', 'value' => $searchValue],
33 ])->orWhereHas('product', function ($productQuery) use ($searchValue) {
34 $productQuery->search([
35 'post_title' => ['column' => 'post_title', 'operator' => 'like_all', 'value' => $searchValue],
36 ]);
37 });
38 });
39 });
40 }
41
42 /**
43 * Maps tab keys to the column used for active view filtering.
44 */
45 public function tabsMap(): array
46 {
47 return [
48 'approved' => 'status',
49 'pending' => 'status',
50 'spam' => 'status',
51 'trash' => 'status',
52 ];
53 }
54
55 public function getModel(): string
56 {
57 return ProductReview::class;
58 }
59
60 /**
61 * Whitelists `with[]=product` (admin Product column, filter banner) and
62 * `with[]=customer` (reviewer link). No extra gate: already behind
63 * `reviews/manage`.
64 *
65 * @return array<string, callable>
66 */
67 protected function allowedWiths(): array
68 {
69 return [
70 'product' => static function ($query) {
71 return $query->with(['product']);
72 },
73 'customer' => static function ($query) {
74 return $query->with(['customer']);
75 },
76 ];
77 }
78
79 public static function getFilterName(): string
80 {
81 return 'reviews';
82 }
83
84 public function applyActiveViewFilter(?string $activeView = null): void
85 {
86 $view = $activeView ?? $this->activeView;
87
88 if (!$view || $view === 'all') {
89 return;
90 }
91
92 $column = Arr::get($this->tabsMap(), $view);
93
94 if (!$column) {
95 return;
96 }
97
98 $this->query->where($column, $view);
99 }
100
101 public static function getSearchableFields(): array
102 {
103 return [
104 'id' => [
105 'column' => 'id',
106 'description' => __('Review ID', 'fluent-cart'),
107 'type' => 'numeric',
108 'examples' => [
109 'id = 1',
110 'id > 5',
111 'id :: 1-10',
112 ],
113 ],
114 ];
115 }
116
117 public static function advanceFilterOptions(): array
118 {
119 return [
120 'review_property' => [
121 'label' => __('Review Property', 'fluent-cart'),
122 'value' => 'review_property',
123 'children' => [
124 [
125 'label' => __('Star Rating', 'fluent-cart'),
126 'value' => 'star_rating',
127 'filter_type' => 'custom',
128 'type' => 'selections',
129 'options' => [
130 '5' => __('5 Stars', 'fluent-cart'),
131 '4' => __('4 Stars', 'fluent-cart'),
132 '3' => __('3 Stars', 'fluent-cart'),
133 '2' => __('2 Stars', 'fluent-cart'),
134 '1' => __('1 Star', 'fluent-cart'),
135 ],
136 'is_multiple' => false,
137 'callback' => static function ($query, $item) {
138 $operator = Arr::get($item, 'operator', '=');
139 if (!in_array($operator, ['=', '!=', '>', '<', '>=', '<='])) {
140 $operator = '=';
141 }
142 $value = absint(Arr::get($item, 'value', 0));
143 if ($value < 1 || $value > 5) {
144 return;
145 }
146 $query->where('rating', $operator, $value);
147 },
148 ],
149 [
150 'label' => __('Verified Purchase', 'fluent-cart'),
151 'value' => 'verified_purchase',
152 'filter_type' => 'custom',
153 'type' => 'selections',
154 'options' => [
155 'yes' => __('Yes', 'fluent-cart'),
156 'no' => __('No', 'fluent-cart'),
157 ],
158 'is_multiple' => false,
159 'is_only_in' => true,
160 'callback' => static function ($query, $item) {
161 $value = Arr::get($item, 'value');
162 if ($value === 'yes') {
163 $query->where('is_verified', 1);
164 } elseif ($value === 'no') {
165 $query->where('is_verified', 0);
166 }
167 },
168 ],
169 [
170 'label' => __('Has Admin Reply', 'fluent-cart'),
171 'value' => 'has_admin_reply',
172 'filter_type' => 'custom',
173 'type' => 'selections',
174 'options' => [
175 'yes' => __('Yes', 'fluent-cart'),
176 'no' => __('No', 'fluent-cart'),
177 ],
178 'is_multiple' => false,
179 'is_only_in' => true,
180 'callback' => static function ($query, $item) {
181 $value = Arr::get($item, 'value');
182 if ($value === 'yes') {
183 $query->hasReplies();
184 } elseif ($value === 'no') {
185 $query->withoutReplies();
186 }
187 },
188 ],
189 [
190 'label' => __('Review Date', 'fluent-cart'),
191 'value' => 'created_at',
192 'filter_type' => 'date',
193 'type' => 'dates',
194 'is_multiple' => false,
195 ],
196 [
197 'label' => __('Reviewer Name', 'fluent-cart'),
198 'value' => 'reviewer_name',
199 'filter_type' => 'column',
200 'type' => 'text',
201 'is_multiple' => false,
202 ],
203 [
204 'label' => __('Reviewer Email', 'fluent-cart'),
205 'value' => 'reviewer_email',
206 'filter_type' => 'column',
207 'type' => 'text',
208 'is_multiple' => false,
209 ],
210 ],
211 ],
212 ];
213 }
214
215 protected function parseSortBy(): string
216 {
217 $sortBy = Arr::get($this->args, 'sort_by');
218
219 if (empty($sortBy)) {
220 return $this->defaultSortBy;
221 }
222
223 $allowedSorts = [
224 'id' => 'id',
225 'created_at' => 'created_at',
226 'reviewer_name' => 'reviewer_name',
227 'rating' => 'rating',
228 ];
229
230 return Arr::get($allowedSorts, $sortBy, $this->defaultSortBy);
231 }
232
233 protected function applySort()
234 {
235 // Pass the raw requested sort_by (e.g. 'rating', 'id') to the hook
236 // so extensions can detect custom sort keys before they are mapped to columns.
237 $rawSortBy = Arr::get($this->args, 'sort_by', $this->sortBy);
238 $this->query = apply_filters('fluent_cart/review/sort_query', $this->query, $rawSortBy, $this->sortType);
239
240 if ($this->sortBy === 'rating') {
241 $this->query->orderBy('rating', $this->sortType);
242 // Deterministic tie-breaker: ratings are 1-5, so ties are the norm and an
243 // unstable order makes rows repeat or vanish across pages.
244 $this->query->orderBy('id', $this->sortType);
245 return;
246 }
247
248 parent::applySort();
249 }
250
251 public function customQuery()
252 {
253 /** @var \FluentCart\Framework\Database\Orm\Builder $query */
254 $query = $this->query;
255
256 // Only show top-level reviews, not admin replies. Spelled out rather than
257 // calling ProductReview::scopeTopLevel(): model scopes resolve through
258 // Builder::__call(), which static analysis cannot see. Keep in step with
259 // that scope if its predicate ever changes.
260 $query->whereNull('parent_id');
261
262 // Backward compat: honor legacy post_id and rating params
263 $postId = Arr::get($this->args, 'post_id');
264 if ($postId) {
265 $query->where('post_id', (int) $postId);
266 }
267
268 $rating = Arr::get($this->args, 'rating');
269 if ($rating && $rating >= 1 && $rating <= 5) {
270 $query->where('rating', (int) $rating);
271 }
272
273 return $query;
274 }
275
276 /**
277 * Disable user-controlled scopes entirely.
278 *
279 * BaseFilter::applyScopes() calls arbitrary methods on the query builder
280 * from the `scopes` request param (e.g. scopes=["delete"] would execute
281 * $query->delete()). The controller whitelist already excludes `scopes`,
282 * but this is defense-in-depth in case make() is called directly.
283 */
284 protected function applyScopes()
285 {
286 // No-op — scopes not used. The top-level predicate is applied via customQuery().
287 }
288
289 public function dateColumns(): array
290 {
291 return ['created_at'];
292 }
293
294 }
295