ProductController   F
last analyzed

Complexity

Total Complexity 67

Size/Duplication

Total Lines 384
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 2
Bugs 1 Features 0
Metric Value
wmc 67
eloc 200
dl 0
loc 384
ccs 0
cts 229
cp 0
rs 3.04
c 2
b 1
f 0

18 Methods

Rating   Name   Duplication   Size   Complexity  
A filterShortcodeAttributes() 0 11 4
A buildMetaQuery() 0 6 1
A renderProductColumnValues() 0 8 2
B filterReviewFormFields() 0 25 8
A filterProductSchema() 0 14 3
A parseProductQuery() 0 19 4
A renderProductTableInlineStyles() 0 12 2
A filterReviewAuthorTagValue() 0 11 3
A filterReviewCallbackHasProductOwner() 0 16 4
A registerBlocks() 0 4 1
B filterPaginatedLink() 0 41 9
A verifyProductOwner() 0 3 1
B filterBlockRenderCallback() 0 43 9
A filterAssignedPostsPostId() 0 21 5
A filterReviewFormBuild() 0 14 4
A filterProductColumns() 0 11 1
A registerProductAttributes() 0 12 2
A isProductOwner() 0 16 4

How to fix   Complexity   

Complex Class

Complex classes like ProductController often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ProductController, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace GeminiLabs\SiteReviews\Integrations\SureCart\Controllers;
4
5
use GeminiLabs\SiteReviews\Contracts\BuilderContract;
6
use GeminiLabs\SiteReviews\Contracts\ControllerContract;
7
use GeminiLabs\SiteReviews\Contracts\ShortcodeContract;
8
use GeminiLabs\SiteReviews\Database\CountManager;
9
use GeminiLabs\SiteReviews\Database\PostMeta;
10
use GeminiLabs\SiteReviews\Helpers\Arr;
11
use GeminiLabs\SiteReviews\Helpers\Svg;
12
use GeminiLabs\SiteReviews\HookProxy;
13
use GeminiLabs\SiteReviews\Modules\Html\Builder;
14
use GeminiLabs\SiteReviews\Modules\Html\ReviewForm;
15
use GeminiLabs\SiteReviews\Modules\Html\Tags\ReviewTag;
16
use GeminiLabs\SiteReviews\Modules\Paginate;
17
use GeminiLabs\SiteReviews\Modules\Sanitizer;
18
use GeminiLabs\SiteReviews\Modules\SchemaParser;
19
use GeminiLabs\SiteReviews\Review;
20
use SureCart\Models\Product;
21
use SureCart\Models\Purchase;
22
use SureCart\Models\User;
23
24
class ProductController implements ControllerContract
25
{
26
    use HookProxy;
27
28
    /**
29
     * @filter render_block_core/shortcode
30
     */
31
    public function filterAssignedPostsPostId(string $content, array $parsedBlock, ?\WP_Block $parentBlock): string
32
    {
33
        $postId = $parentBlock->context['postId'] ?? 0;
34
        $postType = $parentBlock->context['postType'] ?? '';
35
        if ('sc_product' !== $postType) {
36
            return $content;
37
        }
38
        if (!str_contains($content, '[site_review')) {
39
            return $content;
40
        }
41
        if (!str_contains($content, 'assigned_posts')) {
42
            return $content;
43
        }
44
        if (!str_contains($content, 'post_id')) {
45
            return $content;
46
        }
47
        $pattern = '/(assigned_posts\s*=\s*(["\']?))(.*?)\2(?=\s|$)/';
48
        return preg_replace_callback($pattern, function ($match) use ($postId) {
49
            $value = preg_replace('/\bpost_id\b/', $postId, $match[3]);
50
            return $match[1].$value.$match[2];
51
        }, $content);
52
    }
53
54
    /**
55
     * @filter block_type_metadata_settings
56
     */
57
    public function filterBlockRenderCallback(array $settings, array $metadata): array
58
    {
59
        $name = $metadata['name'] ?? '';
60
        $targets = [
61
            'surecart/product-list-sort',
62
            'surecart/product-list-sort-radio-group-template',
63
        ];
64
        if (!in_array($name, $targets)) {
65
            return $settings;
66
        }
67
        $controllerPath = wp_normalize_path(
68
            realpath(dirname($metadata['file']).'/'.remove_block_asset_path_prefix('file:./controller.php'))
69
        );
70
        if (!file_exists($controllerPath)) {
71
            return $settings;
72
        }
73
        $settings['render_callback'] = static function ($attributes, $content, $block) use ($controllerPath, $metadata) {
74
            $view = require $controllerPath;
75
            $templatePath = wp_normalize_path(
76
                realpath(dirname($metadata['file']).'/'.remove_block_asset_path_prefix($view))
77
            );
78
            if (isset($options)
79
                && isset($params)
80
                && isset($query_order)
81
                && isset($query_order_by)) {
82
                $options[] = [
83
                    'checked' => 'asc' === $query_order && 'rating' === $query_order_by,
84
                    'href' => $params->addArg('order', 'asc')->addArg('orderby', 'rating')->url(),
85
                    'label' => esc_html__('Rating, low to high', 'site-reviews'),
86
                    'value' => 'rating:asc',
87
                ];
88
                $options[] = [
89
                    'checked' => 'desc' === $query_order && 'rating' === $query_order_by,
90
                    'href' => $params->addArg('order', 'desc')->addArg('orderby', 'rating')->url(),
91
                    'label' => esc_html__('Rating, high to low', 'site-reviews'),
92
                    'value' => 'rating:desc',
93
                ];
94
            }
95
            ob_start();
96
            require $templatePath;
97
            return ob_get_clean();
98
        };
99
        return $settings;
100
    }
101
102
    public function filterPaginatedLink(array $link, array $args, BuilderContract $builder, Paginate $paginate): array
103
    {
104
        $type = $link['type'] ?? '';
105
        if (!in_array($type, ['next', 'prev'])) {
106
            return $link;
107
        }
108
        $referer = urldecode((string) wp_get_referer());
109
        if (str_contains($referer, 'site-editor.php')) {
110
            parse_str(parse_url($referer, PHP_URL_QUERY), $params);
111
            $template = $params['p'] ?? '';
112
            if (!str_ends_with($template, 'surecart//single-sc_product')) {
113
                return $link;
114
            }
115
        } else {
116
            $baseUrl = str_replace('%_%', '', $paginate->args->base);
117
            $postId = url_to_postid($baseUrl);
118
            if ('sc_product' !== get_post_type($postId)) {
119
                return $link;
120
            }
121
        }
122
        if ('prev' === $type) {
123
            $svg = \SureCart::svg()->get('arrow-left', ['aria-hidden' => true]); // @phpstan-ignore-line
124
            $svg = wp_kses($svg, sc_allowed_svg_html());
125
            $args['text'] = $svg.$args['text'];
126
            if (1 >= $paginate->args->current) {
127
                $tag = 'span';
128
            }
129
        }
130
        if ('next' === $type) {
131
            $svg = \SureCart::svg()->get('arrow-right', ['aria-hidden' => true]); // @phpstan-ignore-line
132
            $svg = wp_kses($svg, sc_allowed_svg_html());
133
            $args['text'] = $args['text'].$svg;
134
            if ($paginate->args->current >= $paginate->args->total) {
135
                $tag = 'span';
136
            }
137
        }
138
        $link['link'] = $builder->build('div', [
139
            'data-type' => $type,
140
            'text' => $builder->build($tag ?? 'a', $args),
141
        ]);
142
        return $link;
143
    }
144
145
    /**
146
     * @param string[] $columns
147
     *
148
     * @filter manage_sc-products_columns
149
     */
150
    public function filterProductColumns(array $columns): array
151
    {
152
        $svg = Svg::get('assets/images/icon.svg', [
153
            'height' => 24,
154
            'style' => 'display:flex; flex-shrink:0; margin: -4px 0;',
155
        ]);
156
        $columns[glsr()->prefix.'rating'] = glsr(Builder::class)->div([
157
            'style' => 'display:flex; align-items:center; justify-content:center;',
158
            'text' => sprintf('%s<span>%s</span>', $svg, _x('Reviews', 'admin-text', 'site-reviews')),
159
        ]);
160
        return $columns;
161
    }
162
163
    /**
164
     * @filter surecart/product/json_schema
165
     */
166
    public function filterProductSchema(array $schema): array
167
    {
168
        $data = glsr(SchemaParser::class)->generate();
169
        $aggregateRatingSchema = Arr::get($data, 'aggregateRating');
170
        $reviewSchema = Arr::get($data, 'review');
171
        if ($aggregateRatingSchema) {
172
            $schema['aggregateRating'] = $aggregateRatingSchema;
173
        }
174
        if ($reviewSchema) {
175
            $schema['review'] = $reviewSchema;
176
        }
177
        // remove Site Reviews generated schema
178
        add_filter('site-reviews/schema/all', '__return_empty_array');
179
        return $schema;
180
    }
181
182
    /**
183
     * @filter site-reviews/review/value/author
184
     */
185
    public function filterReviewAuthorTagValue(string $value, ReviewTag $tag): string
186
    {
187
        $ownership = glsr_get_option('integrations.surecart.ownership', [], 'array');
188
        if (!in_array('labeled', $ownership)) {
189
            return $value;
190
        }
191
        if ($tag->review->hasProductOwner()) {
192
            $text = esc_attr__('verified owner', 'site-reviews');
193
            $value = sprintf('%s <em data-verified-owner="1">(%s)</em>', $value, $text);
194
        }
195
        return $value;
196
    }
197
198
    /**
199
     * @filter site-reviews/review/call/hasProductOwner
200
     */
201
    public function filterReviewCallbackHasProductOwner(Review $review): bool
202
    {
203
        $verified = glsr(PostMeta::class)->get($review->ID, 'sc_verified');
204
        if ('' !== $verified) {
205
            return (bool) $verified;
206
        }
207
        $review->refresh(); // refresh the review first!
208
        $verified = false;
209
        foreach ($review->assigned_posts as $postId) {
210
            if ('sc_product' === get_post_type($postId)) {
211
                $verified = $this->isProductOwner($review->author_id, $postId);
212
                break; // only check the first product
213
            }
214
        }
215
        glsr(PostMeta::class)->set($review->ID, 'sc_verified', (int) $verified);
216
        return $verified;
217
    }
218
219
    /**
220
     * @filter site-reviews/build/template/reviews-form
221
     */
222
    public function filterReviewFormBuild(string $template, array $data): string
223
    {
224
        if ('sc_product' !== get_post_type()) {
225
            return $template;
226
        }
227
        $ownership = glsr_get_option('integrations.surecart.ownership', [], 'array');
228
        if (!in_array('restricted', $ownership)) {
229
            return $template;
230
        }
231
        if ($this->isProductOwner(get_current_user_id(), (int) get_the_ID())) {
232
            return $template;
233
        }
234
        return glsr(Builder::class)->p([
235
            'text' => esc_html__('Only logged in customers who have purchased this product may leave a review.', 'woocommerce'),
236
        ]);
237
    }
238
239
    /**
240
     * @param \GeminiLabs\SiteReviews\Modules\Html\ReviewField[] $fields
241
     *
242
     * @return \GeminiLabs\SiteReviews\Modules\Html\ReviewField[]
243
     *
244
     * @filter site-reviews/review-form/fields/visible
245
     */
246
    public function filterReviewFormFields(array $fields, ReviewForm $form): array
247
    {
248
        if (!is_user_logged_in()) {
249
            return $fields;
250
        }
251
        if ('sc_product' !== get_post_type()) {
252
            return $fields;
253
        }
254
        $user = wp_get_current_user();
255
        array_walk($fields, function ($field) use ($form, $user) {
256
            if (in_array($field->original_name, $form->args()->hide)) {
257
                return;
258
            }
259
            if ('email' === $field->original_name && empty($field->value)) {
260
                $field->value = glsr(Sanitizer::class)->sanitizeUserEmail($user->user_email);
261
                return;
262
            }
263
            if ('name' === $field->original_name && empty($field->value)) {
264
                $field->value = glsr(Sanitizer::class)->sanitizeUserName(
265
                    $user->display_name,
266
                    $user->user_nicename
267
                );
268
            }
269
        });
270
        return $fields;
271
    }
272
273
    /**
274
     * @filter site-reviews/shortcode/site_reviews/attributes
275
     * @filter site-reviews/shortcode/site_reviews_form/attributes
276
     * @filter site-reviews/shortcode/site_reviews_summary/attributes
277
     */
278
    public function filterShortcodeAttributes(array $attributes, ShortcodeContract $shortcode): array
279
    {
280
        $refererQuery = wp_parse_args(wp_parse_url((string) wp_get_referer(), \PHP_URL_QUERY));
281
        $template = $refererQuery['p'] ?? ''; // Get the current Site Editor template
282
        if (!str_starts_with((string) $template, '/wp_template/surecart/') && 'sc_product' !== get_post_type()) {
283
            return $attributes;
284
        }
285
        if ($style = glsr_get_option('integrations.surecart.style')) {
286
            $attributes['data-style'] = $style;
287
        }
288
        return $attributes;
289
    }
290
291
    /**
292
     * @action parse_query
293
     */
294
    public function parseProductQuery(\WP_Query $query): void
295
    {
296
        if ('sc_product' !== $query->get('post_type')) {
297
            return;
298
        }
299
        if ('rating' !== $query->get('orderby')) {
300
            return;
301
        }
302
        $metaQuery = $query->get('meta_query', []);
303
        $order = $query->get('order', 'desc');
304
        if ('bayesian' === glsr_get_option('integrations.surecart.sorting')) {
305
            $metaQuery[] = $this->buildMetaQuery('glsr_ranking', CountManager::META_RANKING);
306
            $query->set('meta_query', $metaQuery);
307
            $query->set('orderby', ['glsr_ranking' => $order]);
308
        } else {
309
            $metaQuery[] = $this->buildMetaQuery('glsr_average', CountManager::META_AVERAGE);
310
            $metaQuery[] = $this->buildMetaQuery('glsr_reviews', CountManager::META_REVIEWS);
311
            $query->set('meta_query', $metaQuery);
312
            $query->set('orderby', ['glsr_average' => $order, 'glsr_reviews' => $order]);
313
        }
314
    }
315
316
    /**
317
     * @action init:11
318
     */
319
    public function registerBlocks(): void
320
    {
321
        register_block_type_from_metadata(glsr()->path('assets/blocks/surecart_product_rating'));
322
        register_block_type_from_metadata(glsr()->path('assets/blocks/surecart_product_reviews'));
323
    }
324
325
    /**
326
     * @filter surecart/product/attributes_set
327
     */
328
    public function registerProductAttributes(Product $product): void
329
    {
330
        $postId = $product->post->ID ?? 0;
331
        if (0 === $postId) {
332
            return;
333
        }
334
        $ratingInfo = glsr_get_ratings([
335
            'assigned_posts' => $postId,
336
        ]);
337
        $product->setAttribute('rating', $ratingInfo->average);
338
        $product->setAttribute('reviews', $ratingInfo->reviews);
339
        $product->setAttribute('ranking', $ratingInfo->ranking);
340
    }
341
342
    /**
343
     * @action manage_sc-products_custom_column
344
     */
345
    public function renderProductColumnValues(string $column, $product): void
346
    {
347
        if (glsr()->prefix.'rating' !== $column) {
348
            return;
349
        }
350
        echo glsr(Builder::class)->a([
351
            'href' => add_query_arg('assigned_post', $product->post->ID, glsr_admin_url()),
352
            'text' => $product->reviews,
353
        ]);
354
    }
355
356
    /**
357
     * @param string $which
358
     *
359
     * @action manage_products_extra_tablenav
360
     */
361
    public function renderProductTableInlineStyles($which): void
362
    {
363
        if ('top' !== $which) {
364
            return;
365
        }
366
        echo '<style>'.
367
        '@media screen and (min-width: 783px) {'.
368
            '.fixed .column-glsr_rating { width: 5%; }'.
369
            'th.column-glsr_rating span { display: none; }'.
370
            'td.column-glsr_rating { text-align: center; }'.
371
        '}'.
372
        '</style>';
373
    }
374
375
    /**
376
     * @action site-reviews/review/created
377
     */
378
    public function verifyProductOwner(Review $review): void
379
    {
380
        $review->hasProductOwner();
381
    }
382
383
    protected function buildMetaQuery(string $orderbyKey, string $metaKey): array
384
    {
385
        return [
386
            'relation' => 'OR',
387
            $orderbyKey => ['key' => $metaKey, 'compare' => 'NOT EXISTS'], // this comes first!
388
            ['key' => $metaKey, 'compare' => 'EXISTS'],
389
        ];
390
    }
391
392
    protected function isProductOwner(int $userId, int $productId): bool
393
    {
394
        if (!$user = User::getUserBy('id', $userId)) { // @phpstan-ignore-line
395
            return false;
396
        }
397
        if (!$customer = $user->customer()) { // @phpstan-ignore-line
398
            return false;
399
        }
400
        if (!$product = sc_get_product($productId)) {
401
            return false;
402
        }
403
        $purchases = Purchase::where([ // @phpstan-ignore-line
404
            'customer_ids' => [$customer->id],
405
            'product_ids' => [$product->id], // @phpstan-ignore-line
406
        ])->get();
407
        return !empty($purchases);
408
    }
409
}
410