Test Failed
Push — develop ( 33b521...cdbd76 )
by Paul
10:16 queued 21s
created

ProductController   F

Complexity

Total Complexity 64

Size/Duplication

Total Lines 407
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 64
eloc 194
c 1
b 0
f 0
dl 0
loc 407
rs 3.28

19 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 registerBlockPatterns() 0 8 1
A renderProductTableInlineStyles() 0 12 2
A filterPaginationDefaults() 0 26 5
A filterReviewAuthorTagValue() 0 11 3
A filterReviewCallbackHasProductOwner() 0 16 4
A registerBlocks() 0 4 1
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\ControllerContract;
6
use GeminiLabs\SiteReviews\Contracts\ShortcodeContract;
7
use GeminiLabs\SiteReviews\Database\CountManager;
8
use GeminiLabs\SiteReviews\Helper;
9
use GeminiLabs\SiteReviews\Helpers\Arr;
10
use GeminiLabs\SiteReviews\Helpers\Svg;
11
use GeminiLabs\SiteReviews\HookProxy;
12
use GeminiLabs\SiteReviews\Modules\Html\Builder;
13
use GeminiLabs\SiteReviews\Modules\Html\ReviewForm;
14
use GeminiLabs\SiteReviews\Modules\Html\Tags\ReviewTag;
15
use GeminiLabs\SiteReviews\Modules\Sanitizer;
16
use GeminiLabs\SiteReviews\Modules\SchemaParser;
17
use GeminiLabs\SiteReviews\Review;
18
use SureCart\Models\Product;
0 ignored issues
show
Bug introduced by
The type SureCart\Models\Product was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
19
use SureCart\Models\Purchase;
0 ignored issues
show
Bug introduced by
The type SureCart\Models\Purchase was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
20
use SureCart\Models\User;
0 ignored issues
show
Bug introduced by
The type SureCart\Models\User was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
21
22
class ProductController implements ControllerContract
23
{
24
    use HookProxy;
25
26
    /**
27
     * @filter render_block_core/shortcode
28
     */
29
    public function filterAssignedPostsPostId(string $content, array $parsedBlock, ?\WP_Block $parentBlock): string
0 ignored issues
show
Unused Code introduced by
The parameter $parsedBlock is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

29
    public function filterAssignedPostsPostId(string $content, /** @scrutinizer ignore-unused */ array $parsedBlock, ?\WP_Block $parentBlock): string

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
30
    {
31
        $postId = $parentBlock->context['postId'] ?? 0;
32
        $postType = $parentBlock->context['postType'] ?? '';
33
        if ('sc_product' !== $postType) {
34
            return $content;
35
        }
36
        if (!str_contains($content, '[site_review')) {
37
            return $content;
38
        }
39
        if (!str_contains($content, 'assigned_posts')) {
40
            return $content;
41
        }
42
        if (!str_contains($content, 'post_id')) {
43
            return $content;
44
        }
45
        $pattern = '/(assigned_posts\s*=\s*(["\']?))(.*?)\2(?=\s|$)/';
46
        return preg_replace_callback($pattern, function ($match) use ($postId) {
47
            $value = preg_replace('/\bpost_id\b/', $postId, $match[3]);
48
            return $match[1].$value.$match[2];
49
        }, $content);
50
    }
51
52
    /**
53
     * @filter block_type_metadata_settings
54
     */
55
    public function filterBlockRenderCallback(array $settings, array $metadata): array
56
    {
57
        $name = $metadata['name'] ?? '';
58
        $targets = [
59
            'surecart/product-list-sort',
60
            'surecart/product-list-sort-radio-group-template',
61
        ];
62
        if (!in_array($name, $targets)) {
63
            return $settings;
64
        }
65
        $controllerPath = wp_normalize_path(
66
            realpath(dirname($metadata['file']).'/'.remove_block_asset_path_prefix('file:./controller.php'))
67
        );
68
        if (!file_exists($controllerPath)) {
69
            return $settings;
70
        }
71
        $settings['render_callback'] = static function ($attributes, $content, $block) use ($controllerPath, $metadata) {
72
            $view = require $controllerPath;
73
            $templatePath = wp_normalize_path(
74
                realpath(dirname($metadata['file']).'/'.remove_block_asset_path_prefix($view))
75
            );
76
            if (isset($options)
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $options seems to never exist and therefore isset should always be false.
Loading history...
77
                && isset($params)
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $params seems to never exist and therefore isset should always be false.
Loading history...
78
                && isset($query_order)
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $query_order seems to never exist and therefore isset should always be false.
Loading history...
79
                && isset($query_order_by)) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $query_order_by seems to never exist and therefore isset should always be false.
Loading history...
80
                $options[] = [
81
                    'checked' => 'asc' === $query_order && 'rating' === $query_order_by,
82
                    'href' => $params->addArg('order', 'asc')->addArg('orderby', 'rating')->url(),
83
                    'label' => esc_html__('Rating, low to high', 'site-reviews'),
84
                    'value' => 'rating:asc',
85
                ];
86
                $options[] = [
87
                    'checked' => 'desc' === $query_order && 'rating' === $query_order_by,
88
                    'href' => $params->addArg('order', 'desc')->addArg('orderby', 'rating')->url(),
89
                    'label' => esc_html__('Rating, high to low', 'site-reviews'),
90
                    'value' => 'rating:desc',
91
                ];
92
            }
93
            ob_start();
94
            require $templatePath;
95
            return ob_get_clean();
96
        };
97
        return $settings;
98
    }
99
100
    /**
101
     * @filter site-reviews/defaults/pagination/defaults
102
     */
103
    public function filterPaginationDefaults(array $defaults): array
104
    {
105
        $postId = get_the_ID();
106
        if (false === $postId) {
107
            $input = Helper::filterInputArray(glsr()->id);
108
            if (empty($input['url'])) {
109
                return $defaults;
110
            }
111
            $postId = url_to_postid(sanitize_url($input['url']));
112
            if (0 === $postId) {
113
                return $defaults;
114
            }
115
        }
116
        if ('sc_product' !== get_post_type($postId)) {
117
            return $defaults;
118
        }
119
        $allowedHtml = sc_allowed_svg_html();
0 ignored issues
show
Bug introduced by
The function sc_allowed_svg_html was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

119
        $allowedHtml = /** @scrutinizer ignore-call */ sc_allowed_svg_html();
Loading history...
120
        $defaults['next_text'] = sprintf('%s %s',
121
            __('Next', 'site-reviews'),
122
            wp_kses(\SureCart::svg()->get('arrow-right', ['aria-hidden' => true]), $allowedHtml) // @phpstan-ignore-line
0 ignored issues
show
Bug introduced by
The type SureCart was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
123
        );
124
        $defaults['prev_text'] = sprintf('%s %s',
125
            wp_kses(\SureCart::svg()->get('arrow-left', ['aria-hidden' => true]), $allowedHtml), // @phpstan-ignore-line
126
            __('Previous', 'site-reviews')
127
        );
128
        return $defaults;
129
    }
130
131
    /**
132
     * @param string[] $columns
133
     *
134
     * @filter manage_sc-products_columns
135
     */
136
    public function filterProductColumns(array $columns): array
137
    {
138
        $svg = Svg::get('assets/images/icon.svg', [
139
            'height' => 24,
140
            'style' => 'display:flex; flex-shrink:0; margin: -4px 0;',
141
        ]);
142
        $columns[glsr()->prefix.'rating'] = glsr(Builder::class)->div([
143
            'style' => 'display:flex; align-items:center; justify-content:center;',
144
            'text' => sprintf('%s<span>%s</span>', $svg, _x('Reviews', 'admin-text', 'site-reviews')),
145
        ]);
146
        return $columns;
147
    }
148
149
    /**
150
     * @filter surecart/product/json_schema
151
     */
152
    public function filterProductSchema(array $schema): array
153
    {
154
        $data = glsr(SchemaParser::class)->generate();
155
        $aggregateRatingSchema = Arr::get($data, 'aggregateRating');
156
        $reviewSchema = Arr::get($data, 'review');
157
        if ($aggregateRatingSchema) {
158
            $schema['aggregateRating'] = $aggregateRatingSchema;
159
        }
160
        if ($reviewSchema) {
161
            $schema['review'] = $reviewSchema;
162
        }
163
        // remove Site Reviews generated schema
164
        add_filter('site-reviews/schema/all', '__return_empty_array');
165
        return $schema;
166
    }
167
168
    /**
169
     * @filter site-reviews/review/value/author
170
     */
171
    public function filterReviewAuthorTagValue(string $value, ReviewTag $tag): string
172
    {
173
        $ownership = glsr_get_option('integrations.surecart.ownership', [], 'array');
174
        if (!in_array('labeled', $ownership)) {
175
            return $value;
176
        }
177
        if ($tag->review->hasProductOwner()) {
0 ignored issues
show
Bug introduced by
The method hasProductOwner() does not exist on GeminiLabs\SiteReviews\Review. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

177
        if ($tag->review->/** @scrutinizer ignore-call */ hasProductOwner()) {
Loading history...
178
            $text = esc_attr__('verified owner', 'site-reviews');
179
            $value = sprintf('%s <em data-verified-owner="1">(%s)</em>', $value, $text);
180
        }
181
        return $value;
182
    }
183
184
    /**
185
     * @filter site-reviews/review/call/hasProductOwner
186
     */
187
    public function filterReviewCallbackHasProductOwner(Review $review): bool
188
    {
189
        $verified = get_post_meta($review->ID, '_sc_verified', true);
190
        if ('' !== $verified) {
191
            return (bool) $verified;
192
        }
193
        $review->refresh(); // refresh the review first!
194
        $verified = false;
195
        foreach ($review->assigned_posts as $postId) {
196
            if ('sc_product' === get_post_type($postId)) {
197
                $verified = $this->isProductOwner($review->author_id, $postId);
198
                break; // only check the first product
199
            }
200
        }
201
        update_post_meta($review->ID, '_sc_verified', (int) $verified);
202
        return $verified;
203
    }
204
205
    /**
206
     * @filter site-reviews/build/template/reviews-form
207
     */
208
    public function filterReviewFormBuild(string $template, array $data): string
0 ignored issues
show
Unused Code introduced by
The parameter $data is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

208
    public function filterReviewFormBuild(string $template, /** @scrutinizer ignore-unused */ array $data): string

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
209
    {
210
        if ('sc_product' !== get_post_type()) {
211
            return $template;
212
        }
213
        $ownership = glsr_get_option('integrations.surecart.ownership', [], 'array');
214
        if (!in_array('restricted', $ownership)) {
215
            return $template;
216
        }
217
        if ($this->isProductOwner(get_current_user_id(), get_the_ID())) {
0 ignored issues
show
Bug introduced by
It seems like get_the_ID() can also be of type false; however, parameter $productId of GeminiLabs\SiteReviews\I...oller::isProductOwner() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

217
        if ($this->isProductOwner(get_current_user_id(), /** @scrutinizer ignore-type */ get_the_ID())) {
Loading history...
218
            return $template;
219
        }
220
        return glsr(Builder::class)->p([
221
            'text' => esc_html__('Only logged in customers who have purchased this product may leave a review.', 'woocommerce'),
222
        ]);
223
    }
224
225
    /**
226
     * @param \GeminiLabs\SiteReviews\Modules\Html\ReviewField[] $fields
227
     *
228
     * @return \GeminiLabs\SiteReviews\Modules\Html\ReviewField[]
229
     *
230
     * @filter site-reviews/review-form/fields/visible
231
     */
232
    public function filterReviewFormFields(array $fields, ReviewForm $form): array
233
    {
234
        if (!is_user_logged_in()) {
235
            return $fields;
236
        }
237
        if ('sc_product' !== get_post_type()) {
238
            return $fields;
239
        }
240
        $user = wp_get_current_user();
241
        array_walk($fields, function ($field) use ($form, $user) {
242
            if (in_array($field->original_name, $form->args()->hide)) {
243
                return;
244
            }
245
            if ('email' === $field->original_name && empty($field->value)) {
246
                $field->value = glsr(Sanitizer::class)->sanitizeUserEmail($user->user_email);
247
                return;
248
            }
249
            if ('name' === $field->original_name && empty($field->value)) {
250
                $field->value = glsr(Sanitizer::class)->sanitizeUserName(
251
                    $user->display_name,
252
                    $user->user_nicename
253
                );
254
            }
255
        });
256
        return $fields;
257
    }
258
259
    /**
260
     * @filter site-reviews/shortcode/site_reviews/attributes
261
     * @filter site-reviews/shortcode/site_reviews_form/attributes
262
     * @filter site-reviews/shortcode/site_reviews_summary/attributes
263
     */
264
    public function filterShortcodeAttributes(array $attributes, ShortcodeContract $shortcode): array
0 ignored issues
show
Unused Code introduced by
The parameter $shortcode is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

264
    public function filterShortcodeAttributes(array $attributes, /** @scrutinizer ignore-unused */ ShortcodeContract $shortcode): array

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
265
    {
266
        $refererQuery = wp_parse_args(wp_parse_url((string) wp_get_referer(), \PHP_URL_QUERY));
267
        $template = $refererQuery['p'] ?? ''; // Get the current Site Editor template
268
        if (!str_starts_with((string) $template, '/wp_template/surecart/') && 'sc_product' !== get_post_type()) {
269
            return $attributes;
270
        }
271
        if ($style = glsr_get_option('integrations.surecart.style')) {
272
            $attributes['data-style'] = $style;
273
        }
274
        return $attributes;
275
    }
276
277
    /**
278
     * @action parse_query
279
     */
280
    public function parseProductQuery(\WP_Query $query): void
281
    {
282
        if ('sc_product' !== $query->get('post_type')) {
283
            return;
284
        }
285
        if ('rating' !== $query->get('orderby')) {
286
            return;
287
        }
288
        $metaQuery = $query->get('meta_query', []);
289
        $order = $query->get('order', 'desc');
290
        if ('bayesian' === glsr_get_option('integrations.surecart.sorting')) {
291
            $metaQuery[] = $this->buildMetaQuery('glsr_ranking', CountManager::META_RANKING);
292
            $query->set('meta_query', $metaQuery);
293
            $query->set('orderby', ['glsr_ranking' => $order]);
294
        } else {
295
            $metaQuery[] = $this->buildMetaQuery('glsr_average', CountManager::META_AVERAGE);
296
            $metaQuery[] = $this->buildMetaQuery('glsr_reviews', CountManager::META_REVIEWS);
297
            $query->set('meta_query', $metaQuery);
298
            $query->set('orderby', ['glsr_average' => $order, 'glsr_reviews' => $order]);
299
        }
300
    }
301
302
    /**
303
     * @action init
304
     */
305
    public function registerBlockPatterns(): void
306
    {
307
        register_block_pattern(glsr()->id.'/surecart-product-reviews', [
308
            'title' => _x('Product Reviews', 'admin-text', 'site-reviews'),
309
            'categories' => ['surecart_product_page'],
310
            'blockTypes' => ['surecart/product-page'],
311
            'priority' => 2,
312
            'content' => '
313
                <!-- wp:group {"layout":{"type":"constrained"}} -->
314
                <div class="wp-block-group">
315
                    <!-- wp:columns {"align":"wide"} -->
316
                    <div class="wp-block-columns alignwide">
317
                        <!-- wp:column {"width":"100%","className":"is-style-default","layout":{"type":"default"}} -->
318
                        <div class="wp-block-column is-style-default" style="flex-basis:100%">
319
                            <!-- wp:heading {"className":"is-style-text-subtitle"} -->
320
                            <h2 class="wp-block-heading is-style-text-subtitle">Reviews</h2>
321
                            <!-- /wp:heading -->
322
                            <!-- wp:site-reviews/reviews {"assigned_posts":["post_id"],"hide":["title"],"id":"reviews","pagination":"ajax","schema":1} /-->
323
                            <!-- wp:heading {"className":"is-style-text-subtitle"} -->
324
                            <h2 class="wp-block-heading is-style-text-subtitle">Submit a Review</h2>
325
                            <!-- /wp:heading -->
326
                            <!-- wp:site-reviews/form {"assigned_posts":["post_id"],"hide":["name","email","title"],"reviews_id":"reviews"} /--></div>
327
                        <!-- /wp:column -->
328
                        </div>
329
                    <!-- /wp:columns -->
330
                    </div>
331
                <!-- /wp:group -->',
332
            // 'postTypes' => ['sc_product'],
333
            // 'templateTypes' => ['sc_product'],
334
        ]);
335
    }
336
337
    /**
338
     * @action init:11
339
     */
340
    public function registerBlocks(): void
341
    {
342
        register_block_type_from_metadata(glsr()->path('assets/blocks/surecart_product_rating'));
343
        register_block_type_from_metadata(glsr()->path('assets/blocks/surecart_product_reviews'));
344
    }
345
346
    /**
347
     * @filter surecart/product/attributes_set
348
     */
349
    public function registerProductAttributes(Product $product): void
350
    {
351
        $postId = $product->post->ID ?? 0;
352
        if (0 === $postId) {
353
            return;
354
        }
355
        $ratingInfo = glsr_get_ratings([
356
            'assigned_posts' => $postId,
357
        ]);
358
        $product->setAttribute('rating', $ratingInfo->average);
359
        $product->setAttribute('reviews', $ratingInfo->reviews);
360
        $product->setAttribute('ranking', $ratingInfo->ranking);
361
    }
362
363
    /**
364
     * @action manage_sc-products_custom_column
365
     */
366
    public function renderProductColumnValues(string $column, $product): void
367
    {
368
        if (glsr()->prefix.'rating' !== $column) {
369
            return;
370
        }
371
        echo glsr(Builder::class)->a([
372
            'href' => add_query_arg('assigned_post', $product->post->ID, glsr_admin_url()),
373
            'text' => $product->reviews,
374
        ]);
375
    }
376
377
    /**
378
     * @param string $which
379
     *
380
     * @action manage_products_extra_tablenav
381
     */
382
    public function renderProductTableInlineStyles($which): void
383
    {
384
        if ('top' !== $which) {
385
            return;
386
        }
387
        echo '<style>'.
388
        '@media screen and (min-width: 783px) {'.
389
            '.fixed .column-glsr_rating { width: 5%; }'.
390
            'th.column-glsr_rating span { display: none; }'.
391
            'td.column-glsr_rating { text-align: center; }'.
392
        '}'.
393
        '</style>';
394
    }
395
396
    /**
397
     * @action site-reviews/review/created
398
     */
399
    public function verifyProductOwner(Review $review): void
400
    {
401
        $review->hasProductOwner();
402
    }
403
404
    protected function buildMetaQuery(string $orderbyKey, string $metaKey): array
405
    {
406
        return [
407
            'relation' => 'OR',
408
            $orderbyKey => ['key' => $metaKey, 'compare' => 'NOT EXISTS'], // this comes first!
409
            ['key' => $metaKey, 'compare' => 'EXISTS'],
410
        ];
411
    }
412
413
    protected function isProductOwner(int $userId, int $productId): bool
414
    {
415
        if (!$user = User::getUserBy('id', $userId)) { // @phpstan-ignore-line
416
            return false;
417
        }
418
        if (!$customer = $user->customer()) { // @phpstan-ignore-line
419
            return false;
420
        }
421
        if (!$product = sc_get_product($productId)) {
0 ignored issues
show
Bug introduced by
The function sc_get_product was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

421
        if (!$product = /** @scrutinizer ignore-call */ sc_get_product($productId)) {
Loading history...
422
            return false;
423
        }
424
        $purchases = Purchase::where([ // @phpstan-ignore-line
425
            'customer_ids' => [$customer->id],
426
            'product_ids' => [$product->id], // @phpstan-ignore-line
427
        ])->get();
428
        return !empty($purchases);
429
    }
430
}
431