ProductController   F
last analyzed

Complexity

Total Complexity 67

Size/Duplication

Total Lines 384
Duplicated Lines 0 %

Importance

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

18 Methods

Rating   Name   Duplication   Size   Complexity  
B filterBlockRenderCallback() 0 43 9
A filterAssignedPostsPostId() 0 21 5
A filterShortcodeAttributes() 0 11 4
B filterReviewFormFields() 0 25 8
A filterProductSchema() 0 14 3
A parseProductQuery() 0 19 4
A filterReviewAuthorTagValue() 0 11 3
A filterReviewCallbackHasProductOwner() 0 16 4
B filterPaginatedLink() 0 41 9
A filterReviewFormBuild() 0 14 4
A filterProductColumns() 0 11 1
A buildMetaQuery() 0 6 1
A renderProductColumnValues() 0 8 2
A renderProductTableInlineStyles() 0 12 2
A registerBlocks() 0 4 1
A verifyProductOwner() 0 3 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\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\Paginate;
16
use GeminiLabs\SiteReviews\Modules\Sanitizer;
17
use GeminiLabs\SiteReviews\Modules\SchemaParser;
18
use GeminiLabs\SiteReviews\Review;
19
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...
20
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...
21
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...
22
23
class ProductController implements ControllerContract
24
{
25
    use HookProxy;
26
27
    /**
28
     * @filter render_block_core/shortcode
29
     */
30
    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

30
    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...
31
    {
32
        $postId = $parentBlock->context['postId'] ?? 0;
33
        $postType = $parentBlock->context['postType'] ?? '';
34
        if ('sc_product' !== $postType) {
35
            return $content;
36
        }
37
        if (!str_contains($content, '[site_review')) {
38
            return $content;
39
        }
40
        if (!str_contains($content, 'assigned_posts')) {
41
            return $content;
42
        }
43
        if (!str_contains($content, 'post_id')) {
44
            return $content;
45
        }
46
        $pattern = '/(assigned_posts\s*=\s*(["\']?))(.*?)\2(?=\s|$)/';
47
        return preg_replace_callback($pattern, function ($match) use ($postId) {
48
            $value = preg_replace('/\bpost_id\b/', $postId, $match[3]);
49
            return $match[1].$value.$match[2];
50
        }, $content);
51
    }
52
53
    /**
54
     * @filter block_type_metadata_settings
55
     */
56
    public function filterBlockRenderCallback(array $settings, array $metadata): array
57
    {
58
        $name = $metadata['name'] ?? '';
59
        $targets = [
60
            'surecart/product-list-sort',
61
            'surecart/product-list-sort-radio-group-template',
62
        ];
63
        if (!in_array($name, $targets)) {
64
            return $settings;
65
        }
66
        $controllerPath = wp_normalize_path(
67
            realpath(dirname($metadata['file']).'/'.remove_block_asset_path_prefix('file:./controller.php'))
68
        );
69
        if (!file_exists($controllerPath)) {
70
            return $settings;
71
        }
72
        $settings['render_callback'] = static function ($attributes, $content, $block) use ($controllerPath, $metadata) {
73
            $view = require $controllerPath;
74
            $templatePath = wp_normalize_path(
75
                realpath(dirname($metadata['file']).'/'.remove_block_asset_path_prefix($view))
76
            );
77
            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...
78
                && 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...
79
                && 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...
80
                && 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...
81
                $options[] = [
82
                    'checked' => 'asc' === $query_order && 'rating' === $query_order_by,
83
                    'href' => $params->addArg('order', 'asc')->addArg('orderby', 'rating')->url(),
84
                    'label' => esc_html__('Rating, low to high', 'site-reviews'),
85
                    'value' => 'rating:asc',
86
                ];
87
                $options[] = [
88
                    'checked' => 'desc' === $query_order && 'rating' === $query_order_by,
89
                    'href' => $params->addArg('order', 'desc')->addArg('orderby', 'rating')->url(),
90
                    'label' => esc_html__('Rating, high to low', 'site-reviews'),
91
                    'value' => 'rating:desc',
92
                ];
93
            }
94
            ob_start();
95
            require $templatePath;
96
            return ob_get_clean();
97
        };
98
        return $settings;
99
    }
100
101
    public function filterPaginatedLink(array $link, array $args, BuilderContract $builder, Paginate $paginate): array
102
    {
103
        $type = $link['type'] ?? '';
104
        if (!in_array($type, ['next', 'prev'])) {
105
            return $link;
106
        }
107
        $referer = urldecode(wp_get_referer());
0 ignored issues
show
Bug introduced by
It seems like wp_get_referer() can also be of type false; however, parameter $string of urldecode() does only seem to accept string, 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

107
        $referer = urldecode(/** @scrutinizer ignore-type */ wp_get_referer());
Loading history...
108
        if (str_contains($referer, 'site-editor.php')) {
109
            parse_str(parse_url($referer, PHP_URL_QUERY), $params);
110
            $template = $params['p'] ?? '';
111
            if (!str_ends_with($template, 'surecart//single-sc_product')) {
112
                return $link;
113
            }
114
        } else {
115
            $baseUrl = str_replace('%_%', '', $paginate->args->base);
116
            $postId = url_to_postid($baseUrl);
117
            if ('sc_product' !== get_post_type($postId)) {
118
                return $link;
119
            }
120
        }
121
        if ('prev' === $type) {
122
            $svg = \SureCart::svg()->get('arrow-left', ['aria-hidden' => true]); // @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
            $svg = wp_kses($svg, 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

123
            $svg = wp_kses($svg, /** @scrutinizer ignore-call */ sc_allowed_svg_html());
Loading history...
124
            $args['text'] = $svg.$args['text'];
125
            if (1 >= $paginate->args->current) {
126
                $tag = 'span';
127
            }
128
        }
129
        if ('next' === $type) {
130
            $svg = \SureCart::svg()->get('arrow-right', ['aria-hidden' => true]); // @phpstan-ignore-line
131
            $svg = wp_kses($svg, sc_allowed_svg_html());
132
            $args['text'] = $args['text'].$svg;
133
            if ($paginate->args->current >= $paginate->args->total) {
134
                $tag = 'span';
135
            }
136
        }
137
        $link['link'] = $builder->build('div', [
138
            'data-type' => $type,
139
            'text' => $builder->build($tag ?? 'a', $args),
140
        ]);
141
        return $link;
142
    }
143
144
    /**
145
     * @param string[] $columns
146
     *
147
     * @filter manage_sc-products_columns
148
     */
149
    public function filterProductColumns(array $columns): array
150
    {
151
        $svg = Svg::get('assets/images/icon.svg', [
152
            'height' => 24,
153
            'style' => 'display:flex; flex-shrink:0; margin: -4px 0;',
154
        ]);
155
        $columns[glsr()->prefix.'rating'] = glsr(Builder::class)->div([
156
            'style' => 'display:flex; align-items:center; justify-content:center;',
157
            'text' => sprintf('%s<span>%s</span>', $svg, _x('Reviews', 'admin-text', 'site-reviews')),
158
        ]);
159
        return $columns;
160
    }
161
162
    /**
163
     * @filter surecart/product/json_schema
164
     */
165
    public function filterProductSchema(array $schema): array
166
    {
167
        $data = glsr(SchemaParser::class)->generate();
168
        $aggregateRatingSchema = Arr::get($data, 'aggregateRating');
169
        $reviewSchema = Arr::get($data, 'review');
170
        if ($aggregateRatingSchema) {
171
            $schema['aggregateRating'] = $aggregateRatingSchema;
172
        }
173
        if ($reviewSchema) {
174
            $schema['review'] = $reviewSchema;
175
        }
176
        // remove Site Reviews generated schema
177
        add_filter('site-reviews/schema/all', '__return_empty_array');
178
        return $schema;
179
    }
180
181
    /**
182
     * @filter site-reviews/review/value/author
183
     */
184
    public function filterReviewAuthorTagValue(string $value, ReviewTag $tag): string
185
    {
186
        $ownership = glsr_get_option('integrations.surecart.ownership', [], 'array');
187
        if (!in_array('labeled', $ownership)) {
188
            return $value;
189
        }
190
        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

190
        if ($tag->review->/** @scrutinizer ignore-call */ hasProductOwner()) {
Loading history...
191
            $text = esc_attr__('verified owner', 'site-reviews');
192
            $value = sprintf('%s <em data-verified-owner="1">(%s)</em>', $value, $text);
193
        }
194
        return $value;
195
    }
196
197
    /**
198
     * @filter site-reviews/review/call/hasProductOwner
199
     */
200
    public function filterReviewCallbackHasProductOwner(Review $review): bool
201
    {
202
        $verified = get_post_meta($review->ID, '_sc_verified', true);
203
        if ('' !== $verified) {
204
            return (bool) $verified;
205
        }
206
        $review->refresh(); // refresh the review first!
207
        $verified = false;
208
        foreach ($review->assigned_posts as $postId) {
209
            if ('sc_product' === get_post_type($postId)) {
210
                $verified = $this->isProductOwner($review->author_id, $postId);
211
                break; // only check the first product
212
            }
213
        }
214
        update_post_meta($review->ID, '_sc_verified', (int) $verified);
215
        return $verified;
216
    }
217
218
    /**
219
     * @filter site-reviews/build/template/reviews-form
220
     */
221
    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

221
    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...
222
    {
223
        if ('sc_product' !== get_post_type()) {
224
            return $template;
225
        }
226
        $ownership = glsr_get_option('integrations.surecart.ownership', [], 'array');
227
        if (!in_array('restricted', $ownership)) {
228
            return $template;
229
        }
230
        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

230
        if ($this->isProductOwner(get_current_user_id(), /** @scrutinizer ignore-type */ get_the_ID())) {
Loading history...
231
            return $template;
232
        }
233
        return glsr(Builder::class)->p([
234
            'text' => esc_html__('Only logged in customers who have purchased this product may leave a review.', 'woocommerce'),
235
        ]);
236
    }
237
238
    /**
239
     * @param \GeminiLabs\SiteReviews\Modules\Html\ReviewField[] $fields
240
     *
241
     * @return \GeminiLabs\SiteReviews\Modules\Html\ReviewField[]
242
     *
243
     * @filter site-reviews/review-form/fields/visible
244
     */
245
    public function filterReviewFormFields(array $fields, ReviewForm $form): array
246
    {
247
        if (!is_user_logged_in()) {
248
            return $fields;
249
        }
250
        if ('sc_product' !== get_post_type()) {
251
            return $fields;
252
        }
253
        $user = wp_get_current_user();
254
        array_walk($fields, function ($field) use ($form, $user) {
255
            if (in_array($field->original_name, $form->args()->hide)) {
256
                return;
257
            }
258
            if ('email' === $field->original_name && empty($field->value)) {
259
                $field->value = glsr(Sanitizer::class)->sanitizeUserEmail($user->user_email);
260
                return;
261
            }
262
            if ('name' === $field->original_name && empty($field->value)) {
263
                $field->value = glsr(Sanitizer::class)->sanitizeUserName(
264
                    $user->display_name,
265
                    $user->user_nicename
266
                );
267
            }
268
        });
269
        return $fields;
270
    }
271
272
    /**
273
     * @filter site-reviews/shortcode/site_reviews/attributes
274
     * @filter site-reviews/shortcode/site_reviews_form/attributes
275
     * @filter site-reviews/shortcode/site_reviews_summary/attributes
276
     */
277
    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

277
    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...
278
    {
279
        $refererQuery = wp_parse_args(wp_parse_url((string) wp_get_referer(), \PHP_URL_QUERY));
280
        $template = $refererQuery['p'] ?? ''; // Get the current Site Editor template
281
        if (!str_starts_with((string) $template, '/wp_template/surecart/') && 'sc_product' !== get_post_type()) {
282
            return $attributes;
283
        }
284
        if ($style = glsr_get_option('integrations.surecart.style')) {
285
            $attributes['data-style'] = $style;
286
        }
287
        return $attributes;
288
    }
289
290
    /**
291
     * @action parse_query
292
     */
293
    public function parseProductQuery(\WP_Query $query): void
294
    {
295
        if ('sc_product' !== $query->get('post_type')) {
296
            return;
297
        }
298
        if ('rating' !== $query->get('orderby')) {
299
            return;
300
        }
301
        $metaQuery = $query->get('meta_query', []);
302
        $order = $query->get('order', 'desc');
303
        if ('bayesian' === glsr_get_option('integrations.surecart.sorting')) {
304
            $metaQuery[] = $this->buildMetaQuery('glsr_ranking', CountManager::META_RANKING);
305
            $query->set('meta_query', $metaQuery);
306
            $query->set('orderby', ['glsr_ranking' => $order]);
307
        } else {
308
            $metaQuery[] = $this->buildMetaQuery('glsr_average', CountManager::META_AVERAGE);
309
            $metaQuery[] = $this->buildMetaQuery('glsr_reviews', CountManager::META_REVIEWS);
310
            $query->set('meta_query', $metaQuery);
311
            $query->set('orderby', ['glsr_average' => $order, 'glsr_reviews' => $order]);
312
        }
313
    }
314
315
    /**
316
     * @action init:11
317
     */
318
    public function registerBlocks(): void
319
    {
320
        register_block_type_from_metadata(glsr()->path('assets/blocks/surecart_product_rating'));
321
        register_block_type_from_metadata(glsr()->path('assets/blocks/surecart_product_reviews'));
322
    }
323
324
    /**
325
     * @filter surecart/product/attributes_set
326
     */
327
    public function registerProductAttributes(Product $product): void
328
    {
329
        $postId = $product->post->ID ?? 0;
330
        if (0 === $postId) {
331
            return;
332
        }
333
        $ratingInfo = glsr_get_ratings([
334
            'assigned_posts' => $postId,
335
        ]);
336
        $product->setAttribute('rating', $ratingInfo->average);
337
        $product->setAttribute('reviews', $ratingInfo->reviews);
338
        $product->setAttribute('ranking', $ratingInfo->ranking);
339
    }
340
341
    /**
342
     * @action manage_sc-products_custom_column
343
     */
344
    public function renderProductColumnValues(string $column, $product): void
345
    {
346
        if (glsr()->prefix.'rating' !== $column) {
347
            return;
348
        }
349
        echo glsr(Builder::class)->a([
350
            'href' => add_query_arg('assigned_post', $product->post->ID, glsr_admin_url()),
351
            'text' => $product->reviews,
352
        ]);
353
    }
354
355
    /**
356
     * @param string $which
357
     *
358
     * @action manage_products_extra_tablenav
359
     */
360
    public function renderProductTableInlineStyles($which): void
361
    {
362
        if ('top' !== $which) {
363
            return;
364
        }
365
        echo '<style>'.
366
        '@media screen and (min-width: 783px) {'.
367
            '.fixed .column-glsr_rating { width: 5%; }'.
368
            'th.column-glsr_rating span { display: none; }'.
369
            'td.column-glsr_rating { text-align: center; }'.
370
        '}'.
371
        '</style>';
372
    }
373
374
    /**
375
     * @action site-reviews/review/created
376
     */
377
    public function verifyProductOwner(Review $review): void
378
    {
379
        $review->hasProductOwner();
380
    }
381
382
    protected function buildMetaQuery(string $orderbyKey, string $metaKey): array
383
    {
384
        return [
385
            'relation' => 'OR',
386
            $orderbyKey => ['key' => $metaKey, 'compare' => 'NOT EXISTS'], // this comes first!
387
            ['key' => $metaKey, 'compare' => 'EXISTS'],
388
        ];
389
    }
390
391
    protected function isProductOwner(int $userId, int $productId): bool
392
    {
393
        if (!$user = User::getUserBy('id', $userId)) { // @phpstan-ignore-line
394
            return false;
395
        }
396
        if (!$customer = $user->customer()) { // @phpstan-ignore-line
397
            return false;
398
        }
399
        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

399
        if (!$product = /** @scrutinizer ignore-call */ sc_get_product($productId)) {
Loading history...
400
            return false;
401
        }
402
        $purchases = Purchase::where([ // @phpstan-ignore-line
403
            'customer_ids' => [$customer->id],
404
            'product_ids' => [$product->id], // @phpstan-ignore-line
405
        ])->get();
406
        return !empty($purchases);
407
    }
408
}
409