Test Failed
Push — develop ( 425fc0...4f2be8 )
by Paul
13:11
created

ProductController::buildMetaQuery()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 4
dl 0
loc 6
ccs 0
cts 5
cp 0
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 2
crap 2
1
<?php
2
3
namespace GeminiLabs\SiteReviews\Integrations\WooCommerce\Controllers;
4
5
use GeminiLabs\SiteReviews\Arguments;
6
use GeminiLabs\SiteReviews\Contracts\ControllerContract;
7
use GeminiLabs\SiteReviews\Database\CountManager;
8
use GeminiLabs\SiteReviews\Defaults\SiteReviewsDefaults;
9
use GeminiLabs\SiteReviews\Helpers\Arr;
10
use GeminiLabs\SiteReviews\Helpers\Cast;
11
use GeminiLabs\SiteReviews\Helpers\Str;
12
use GeminiLabs\SiteReviews\Helpers\Svg;
13
use GeminiLabs\SiteReviews\HookProxy;
14
use GeminiLabs\SiteReviews\Integrations\WooCommerce\Metaboxes\ReviewsMetabox;
15
use GeminiLabs\SiteReviews\Modules\Html\Builder;
16
use GeminiLabs\SiteReviews\Modules\Html\Template;
17
use GeminiLabs\SiteReviews\Modules\Rating;
18
use GeminiLabs\SiteReviews\Modules\Sanitizer;
19
use GeminiLabs\SiteReviews\Modules\Schema;
20
use GeminiLabs\SiteReviews\Modules\Style;
21
22
class ProductController implements ControllerContract
23
{
24
    use HookProxy;
25
26
    /**
27
     * @param string $template
28
     *
29
     * @filter comments_template
30
     */
31
    public function filterCommentsTemplate($template): string
32
    {
33
        if (current_theme_supports('woocommerce') && 'product' === get_post_type()) {
34
            return glsr()->path('views/integrations/woocommerce/overrides/single-product-reviews.php');
35
        }
36
        return Cast::toString($template);
37
    }
38
39
    /**
40
     * @param string $html
41
     * @param int    $rating
42
     * @param int    $count
43
     *
44
     * @filter woocommerce_product_get_rating_html
45
     */
46
    public function filterGetRatingHtml($html, $rating, $count): string
47
    {
48
        if (str_contains($html, 'wc-block')) {
49
            $html = str_replace('wc-block-grid__product-rating__stars', '', $html);
50
            return $html;
51
        }
52
        $starsHtml = glsr_star_rating($rating, $count, [
53
            'theme' => glsr_get_option('integrations.woocommerce.style'),
54
        ]);
55
        return glsr(Builder::class)->div([
56
            'class' => glsr(Style::class)->styleClasses(),
57
            'text' => $starsHtml,
58
        ]);
59
    }
60
61
    /**
62
     * @param string $html
63
     * @param int    $rating
64
     * @param int    $count
65
     *
66
     * @filter woocommerce_get_star_rating_html
67
     */
68
    public function filterGetStarRatingHtml($html, $rating, $count): string
69
    {
70
        return glsr_star_rating($rating, $count, [
71
            'theme' => glsr_get_option('integrations.woocommerce.style'),
72
        ]);
73
    }
74
75
    /**
76
     * @param mixed       $value
77
     * @param \WC_Product $product
78
     *
79
     * @filter woocommerce_product_get_average_rating
80
     */
81
    public function filterProductAverageRating($value, $product): float
82
    {
83
        // return glsr_get_ratings(['assigned_posts' => $product->get_id()])->average;
84
        return Cast::toFloat(get_post_meta($product->get_id(), CountManager::META_AVERAGE, true));
85
    }
86
87
    /**
88
     * @param array $tabs
89
     *
90
     * @filter woocommerce_product_data_tabs
91
     */
92
    public function filterProductDataTabs($tabs): array
93
    {
94
        $tabs = Arr::consolidate($tabs);
95
        $tabs[glsr()->id] = [
96
            'label' => glsr()->name,
97
            'target' => glsr()->id,
98
            'priority' => 100,
99
            'class' => [],
100
        ];
101
        return $tabs;
102
    }
103
104
    /**
105
     * @param array $metaQuery
106
     *
107
     * @filter woocommerce_product_query_meta_query
108
     */
109
    public function filterProductMetaQuery($metaQuery): array
110
    {
111
        global $wp_query;
112
        $metaQuery = Arr::consolidate($metaQuery);
113
        $orderby = filter_input(INPUT_GET, 'orderby');
114
        if (!$orderby && !is_search()) {
115
            $orderby = apply_filters('woocommerce_default_catalog_orderby', get_option('woocommerce_default_catalog_orderby'));
116
        }
117
        if ('rating' !== $orderby) {
118
            return $metaQuery;
119
        }
120
        if ('bayesian' === glsr_get_option('integrations.woocommerce.sorting')) {
121
            $metaQuery[] = $this->buildMetaQuery('glsr_ranking', CountManager::META_RANKING);
122
            $wp_query->set('orderby', ['glsr_ranking' => 'DESC']);
123
        } else {
124
            $metaQuery[] = $this->buildMetaQuery('glsr_average', CountManager::META_AVERAGE);
125
            $metaQuery[] = $this->buildMetaQuery('glsr_reviews', CountManager::META_REVIEWS);
126
            $wp_query->set('orderby', ['glsr_average' => 'DESC', 'glsr_reviews' => 'DESC']);
127
        }
128
        return $metaQuery;
129
    }
130
131
    /**
132
     * @param array  $args
133
     * @param string $orderby
134
     *
135
     * @filter woocommerce_get_catalog_ordering_args
136
     */
137
    public function filterProductPostClauses($args, $orderby): array
138
    {
139
        $args = Arr::consolidate($args);
140
        if ('rating' === $orderby) {
141
            remove_filter('posts_clauses', [WC()->query, 'order_by_rating_post_clauses']);
142
        }
143
        return $args;
144
    }
145
146
    /**
147
     * @param mixed       $value
148
     * @param \WC_Product $product
149
     *
150
     * @filter woocommerce_product_get_rating_counts
151
     */
152
    public function filterProductRatingCounts($value, $product): array
153
    {
154
        return glsr_get_ratings(['assigned_posts' => $product->get_id()])->ratings;
155
    }
156
157
    /**
158
     * @param mixed       $value
159
     * @param \WC_Product $product
160
     *
161
     * @filter woocommerce_product_get_review_count
162
     */
163
    public function filterProductReviewCount($value, $product): int
164
    {
165
        // return glsr_get_ratings(['assigned_posts' => $product->get_id()])->reviews;
166
        return Cast::toInt(get_post_meta($product->get_id(), CountManager::META_REVIEWS, true));
167
    }
168
169
    /**
170
     * @param array $tabs
171
     *
172
     * @filter woocommerce_product_tabs
173
     */
174
    public function filterProductTabs($tabs): array
175
    {
176
        global $product;
177
        $tabs = Arr::consolidate($tabs);
178
        if ($product instanceof \WC_Product && $product->get_reviews_allowed()) {
179
            $tabs['reviews'] = [
180
                'callback' => [$this, 'renderSingleProductReviews'],
181
                'priority' => 30,
182
                'title' => sprintf(__('Reviews (%d)', 'site-reviews'), $product->get_review_count()),
183
            ];
184
        }
185
        return $tabs;
186
    }
187
188
    /**
189
     * @param array $taxQuery
190
     *
191
     * @filter woocommerce_product_query_tax_query
192
     */
193
    public function filterProductTaxQuery($taxQuery): array
194
    {
195
        $taxQuery = Arr::consolidate($taxQuery);
196
        foreach ($taxQuery as $key => $query) {
197
            if (!empty($query['rating_filter'])) {
198
                $filteredRatings = [];
199
                $field = Arr::get($query, 'field');
200
                $taxonomy = Arr::get($query, 'taxonomy');
201
                foreach (Arr::consolidate(Arr::get($query, 'terms')) as $value) {
202
                    $term = get_term_by($field, $value, $taxonomy);
203
                    $filteredRatings[] = Cast::toInt(Str::removePrefix(Arr::get($term, 'slug'), 'rated-'));
204
                }
205
                unset($taxQuery[$key]);
206
                break;
207
            }
208
        }
209
        if (!empty($filteredRatings)) {
210
            $this->setMetaQueriesForFilteredRatings($filteredRatings);
211
        }
212
        return $taxQuery;
213
    }
214
215
    /**
216
     * @param array       $markup
217
     * @param \WC_Product $product
218
     *
219
     * @filter woocommerce_structured_data_product
220
     */
221
    public function filterStructuredData($markup, $product): array
222
    {
223
        $args = glsr(SiteReviewsDefaults::class)->merge([
224
            'assigned_posts' => $product->get_id(),
225
            'display' => 5, // only get the latest 5 reviews
226
            'rating' => 1, // minimum rating
227
        ]);
228
        $markup = Arr::consolidate($markup);
229
        $schema = glsr(Schema::class)->build($args, glsr_get_reviews($args));
230
        if (array_key_exists('review', $schema)) {
231
            $markup['review'] = $schema['review'];
232
        } else {
233
            unset($markup['review']);
234
        }
235
        return $markup;
236
    }
237
238
    /**
239
     * @param array $args
240
     *
241
     * @filter woocommerce_top_rated_products_widget_args
242
     */
243
    public function filterWidgetArgsTopRatedProducts($args): array
244
    {
245
        $args = Arr::consolidate($args);
246
        if ('bayesian' === glsr_get_option('integrations.woocommerce.sorting')) {
247
            $args['meta_query'][] = $this->buildMetaQuery('glsr_ranking', CountManager::META_RANKING);
248
            $args['orderby'] = ['glsr_ranking' => 'DESC'];
249
        } else {
250
            $args['meta_query'][] = $this->buildMetaQuery('glsr_average', CountManager::META_AVERAGE);
251
            $args['meta_query'][] = $this->buildMetaQuery('glsr_reviews', CountManager::META_REVIEWS);
252
            $args['orderby'] = ['glsr_average' => 'DESC', 'glsr_reviews' => 'DESC'];
253
        }
254
        return $args;
255
    }
256
257
    /**
258
     * @param string $template
259
     * @param string $templateName
260
     *
261
     * @filter wc_get_template
262
     */
263
    public function filterWoocommerceTemplate($template, $templateName): string
264
    {
265
        if ('loop/rating.php' === $templateName) {
266
            return glsr()->path('views/integrations/woocommerce/overrides/loop-rating.php');
267
        }
268
        if ('single-product/rating.php' === $templateName) {
269
            return glsr()->path('views/integrations/woocommerce/overrides/single-product-rating.php');
270
        }
271
        if ('single-product-reviews.php' === $templateName) {
272
            return glsr()->path('views/integrations/woocommerce/overrides/single-product-reviews.php');
273
        }
274
        return Cast::toString($template);
275
    }
276
277
    /**
278
     * @param \WP_Query $query
279
     *
280
     * @action pre_get_posts
281
     */
282
    public function modifyProductQuery($query): void
283
    {
284
        if (!is_a($query, 'Automattic\WooCommerce\Blocks\Utils\BlocksWpQuery')) {
285
            return;
286
        }
287
        if ('rating' !== $query->get('orderby')) {
288
            return;
289
        }
290
        $metaQuery = $query->get('meta_query');
291
        if (empty($metaQuery)) {
292
            $metaQuery = [];
293
        }
294
        if ('bayesian' === glsr_get_option('integrations.woocommerce.sorting')) {
295
            $metaQuery[] = $this->buildMetaQuery('glsr_ranking', CountManager::META_RANKING);
296
            $query->set('meta_query', $metaQuery);
297
            $query->set('orderby', ['glsr_ranking' => 'DESC']);
298
        } else {
299
            $metaQuery[] = $this->buildMetaQuery('glsr_average', CountManager::META_AVERAGE);
300
            $metaQuery[] = $this->buildMetaQuery('glsr_reviews', CountManager::META_REVIEWS);
301
            $query->set('meta_query', $metaQuery);
302
            $query->set('orderby', ['glsr_average' => 'DESC', 'glsr_reviews' => 'DESC']);
303
        }
304
    }
305
306
    /**
307
     * @action admin_head
308
     */
309
    public function printInlineStyle(): void
310
    {
311
        $icon = Svg::encoded('assets/images/icon-static.svg');
312
        echo '<style type="text/css">'.
313
            '#woocommerce-product-data ul.wc-tabs li.site-reviews_tab a::before {'.
314
                'background-color: currentColor;'.
315
                'mask-image: url("'.$icon.'");'.
316
                'mask-position: center;'.
317
                'mask-repeat: no-repeat;'.
318
                'mask-size: 1em;'.
319
            '}'.
320
        '</style>';
321
    }
322
323
    /**
324
     * @action site-reviews/woocommerce/render/loop/rating
325
     */
326
    public function renderLoopRating(): void
327
    {
328
        global $product;
329
        if (!wc_review_ratings_enabled()) {
330
            return;
331
        }
332
        $ratings = glsr_get_ratings(['assigned_posts' => 'post_id']);
333
        if (0 >= $ratings->average && !glsr_get_option('integrations.woocommerce.display_empty', false, 'bool')) {
334
            return;
335
        }
336
        glsr(Template::class)->render('templates/woocommerce/loop/rating', [
337
            'product' => $product,
338
            'ratings' => $ratings,
339
            'style' => glsr(Style::class)->styleClasses(),
340
            'theme' => glsr_get_option('integrations.woocommerce.style'),
341
        ]);
342
    }
343
344
    /**
345
     * @param \WP_Post $post
346
     *
347
     * @action add_meta_boxes_product
348
     */
349
    public function registerMetaBoxes($post): void
350
    {
351
        glsr(ReviewsMetabox::class)->register($post);
352
    }
353
354
    /**
355
     * @param string $columnName
356
     * @param string $postType
357
     *
358
     * @action bulk_edit_custom_box
359
     */
360
    public function renderBulkEditField($columnName, $postType): void
361
    {
362
        if ('price' === $columnName && 'product' === $postType) {
363
            glsr()->render('views/integrations/woocommerce/bulk-edit');
364
        }
365
    }
366
367
    /**
368
     * @action woocommerce_product_data_panels
369
     */
370
    public function renderProductDataPanel(): void
371
    {
372
        global $product_object;
373
        glsr(Template::class)->render('views/integrations/woocommerce/product-data-panel', [
374
            'product' => $product_object,
375
        ]);
376
    }
377
378
    /**
379
     * @param string $columnName
380
     * @param string $postType
381
     *
382
     * @action quick_edit_custom_box
383
     */
384
    public function renderQuickEditField($columnName, $postType): void
385
    {
386
        if ('price' === $columnName && 'product' === $postType) {
387
            glsr()->render('views/integrations/woocommerce/quick-edit');
388
        }
389
    }
390
391
    /**
392
     * @callback filterProductTabs
393
     * 
394
     * @action site-reviews/woocommerce/render/single-product-reviews
395
     */
396
    public function renderSingleProductReviews(): void
397
    {
398
        global $product;
399
        if ($product instanceof \WC_Product && $product->get_reviews_allowed()) {
400
            $isVerifiedOwner = wc_customer_bought_product('', get_current_user_id(), $product->get_id());
401
            glsr(Template::class)->render('templates/woocommerce/reviews', [
402
                'form' => do_shortcode($this->option($product, 'form')),
403
                'product' => $product,
404
                'ratings' => glsr_get_ratings(['assigned_posts' => 'post_id']),
405
                'reviews' => do_shortcode($this->option($product, 'reviews')),
406
                'summary' => do_shortcode($this->option($product, 'summary')),
407
                'verified' => $isVerifiedOwner || 'no' === get_option('woocommerce_review_rating_verification_required'),
408
            ]);
409
        }
410
    }
411
412
    /**
413
     * @action site-reviews/woocommerce/render/single-product/rating
414
     */
415
    public function renderSingleProductRating(): void
416
    {
417
        global $product;
418
        $ratings = glsr_get_ratings(['assigned_posts' => 'post_id']);
419
        if (0 >= $ratings->average && !glsr_get_option('integrations.woocommerce.display_empty', false, 'bool')) {
420
            return;
421
        }
422
        glsr(Template::class)->render('templates/woocommerce/rating', [
423
            'product' => $product,
424
            'ratings' => $ratings,
425
            'style' => glsr(Style::class)->styleClasses(),
426
            'theme' => glsr_get_option('integrations.woocommerce.style'),
427
        ]);
428
    }
429
430
    /**
431
     * This updates the product_visibility rated-* categories.
432
     *
433
     * @action site-reviews/ratings/count/post
434
     */
435
    public function updateProductRatingCounts(int $postId, Arguments $counts): void
436
    {
437
        if ('product' === get_post_type($postId)) {
438
            $product = wc_get_product($postId);
439
            $product->set_rating_counts($counts->ratings);
440
            $product->set_average_rating($counts->average);
441
            $product->set_review_count($counts->reviews);
442
            $product->save();
443
        }
444
    }
445
446
    /**
447
     * @action woocommerce_admin_process_product_object
448
     */
449
    public function updateProductData(\WC_Product $product): void
450
    {
451
        $shortcodes = [
452
            'site_reviews',
453
            'site_reviews_form',
454
            'site_reviews_summary',
455
        ];
456
        foreach ($shortcodes as $shortcode) {
457
            $value = trim(filter_input(INPUT_POST, $shortcode));
458
            $value = glsr(Sanitizer::class)->sanitizeText($value);
459
            if (empty($value)) {
460
                $product->delete_meta_data($shortcode);
461
                continue;
462
            }
463
            if (1 !== preg_match("/^\[{$shortcode}(\s[^\]]*\]|\])$/", $value)) {
464
                continue;
465
            }
466
            if (!str_contains($value, 'assigned_posts')) {
467
                $value = str_replace($shortcode, sprintf('%s assigned_posts="post_id"', $shortcode), $value);
468
            }
469
            $product->update_meta_data($shortcode, $value);
470
        }
471
    }
472
473
    protected function buildMetaQuery(string $orderbyKey, string $metaKey): array
474
    {
475
        return [
476
            'relation' => 'OR',
477
            $orderbyKey => ['key' => $metaKey, 'compare' => 'NOT EXISTS'], // this comes first!
478
            ['key' => $metaKey, 'compare' => 'EXISTS'],
479
        ];
480
    }
481
482
    protected function option(\WC_Product $product, string $key): string
483
    {
484
        $shortcodes = [
485
            'form' => 'site_reviews_form',
486
            'reviews' => 'site_reviews',
487
            'summary' => 'site_reviews_summary',
488
        ];
489
        if (!array_key_exists($key, $shortcodes)) {
490
            return '';
491
        }
492
        if ($override = $product->get_meta($shortcodes[$key])) {
493
            return $override;
494
        }
495
        return glsr_get_option("integrations.woocommerce.{$key}");
496
    }
497
498
    /**
499
     * @param int[] $ratings
500
     */
501
    protected function setMetaQueriesForFilteredRatings(array $ratings): void
502
    {
503
        global $wp_query;
504
        $ratings = Arr::uniqueInt($ratings);
505
        if (!empty($ratings)) {
506
            $metaQuery = Arr::consolidate($wp_query->get('meta_query'));
507
            $metaQueries = ['relation' => 'OR'];
508
            foreach ($ratings as $rating) {
509
                $metaQueries[] = [
510
                    'key' => CountManager::META_AVERAGE,
511
                    'compare' => 'BETWEEN',
512
                    'value' => [$rating - .5, $rating + .49], // compare the rating to a rounded average range
513
                ];
514
            }
515
            $metaQuery[] = $metaQueries;
516
            $wp_query->set('meta_query', $metaQuery);
517
        }
518
    }
519
}
520