Test Failed
Push — develop ( 9319a1...a4d1ea )
by Paul
10:52
created

ProductController   D

Complexity

Total Complexity 59

Size/Duplication

Total Lines 376
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 59
eloc 176
c 1
b 0
f 0
dl 0
loc 376
rs 4.08

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 registerBlockPatterns() 0 8 1
A renderProductTableInlineStyles() 0 12 2
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\Helpers\Arr;
9
use GeminiLabs\SiteReviews\Helpers\Svg;
10
use GeminiLabs\SiteReviews\HookProxy;
11
use GeminiLabs\SiteReviews\Modules\Html\Builder;
12
use GeminiLabs\SiteReviews\Modules\Html\ReviewForm;
13
use GeminiLabs\SiteReviews\Modules\Html\Tags\ReviewTag;
14
use GeminiLabs\SiteReviews\Modules\Sanitizer;
15
use GeminiLabs\SiteReviews\Modules\SchemaParser;
16
use GeminiLabs\SiteReviews\Review;
17
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...
18
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...
19
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...
20
21
class ProductController implements ControllerContract
22
{
23
    use HookProxy;
24
25
    /**
26
     * @filter render_block_core/shortcode
27
     */
28
    public function filterAssignedPostsPostId(string $content, array $parsedBlock, ?\WP_Block $parentBlock): string
1 ignored issue
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

28
    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...
29
    {
30
        $postId = $parentBlock->context['postId'] ?? 0;
31
        $postType = $parentBlock->context['postType'] ?? '';
32
        if ('sc_product' !== $postType) {
33
            return $content;
34
        }
35
        if (!str_contains($content, '[site_review')) {
36
            return $content;
37
        }
38
        if (!str_contains($content, 'assigned_posts')) {
39
            return $content;
40
        }
41
        if (!str_contains($content, 'post_id')) {
42
            return $content;
43
        }
44
        $pattern = '/(assigned_posts\s*=\s*(["\']?))(.*?)\2(?=\s|$)/';
45
        return preg_replace_callback($pattern, function ($match) use ($postId) {
46
            $value = preg_replace('/\bpost_id\b/', $postId, $match[3]);
47
            return $match[1].$value.$match[2];
48
        }, $content);
49
    }
50
51
    /**
52
     * @filter block_type_metadata_settings
53
     */
54
    public function filterBlockRenderCallback(array $settings, array $metadata): array
55
    {
56
        $name = $metadata['name'] ?? '';
57
        $targets = [
58
            'surecart/product-list-sort',
59
            'surecart/product-list-sort-radio-group-template',
60
        ];
61
        if (!in_array($name, $targets)) {
62
            return $settings;
63
        }
64
        $controllerPath = wp_normalize_path(
65
            realpath(dirname($metadata['file']).'/'.remove_block_asset_path_prefix('file:./controller.php'))
66
        );
67
        if (!file_exists($controllerPath)) {
68
            return $settings;
69
        }
70
        $settings['render_callback'] = static function ($attributes, $content, $block) use ($controllerPath, $metadata) {
71
            $view = require $controllerPath;
72
            $templatePath = wp_normalize_path(
73
                realpath(dirname($metadata['file']).'/'.remove_block_asset_path_prefix($view))
74
            );
75
            if (isset($options)
1 ignored issue
show
Comprehensibility Best Practice introduced by
The variable $options seems to never exist and therefore isset should always be false.
Loading history...
76
                && isset($params)
1 ignored issue
show
Comprehensibility Best Practice introduced by
The variable $params seems to never exist and therefore isset should always be false.
Loading history...
77
                && isset($query_order)
1 ignored issue
show
Comprehensibility Best Practice introduced by
The variable $query_order seems to never exist and therefore isset should always be false.
Loading history...
78
                && isset($query_order_by)) {
1 ignored issue
show
Comprehensibility Best Practice introduced by
The variable $query_order_by seems to never exist and therefore isset should always be false.
Loading history...
79
                $options[] = [
80
                    'checked' => 'asc' === $query_order && 'rating' === $query_order_by,
81
                    'href' => $params->addArg('order', 'asc')->addArg('orderby', 'rating')->url(),
82
                    'label' => esc_html__('Rating, low to high', 'site-reviews'),
83
                    'value' => 'rating:asc',
84
                ];
85
                $options[] = [
86
                    'checked' => 'desc' === $query_order && 'rating' === $query_order_by,
87
                    'href' => $params->addArg('order', 'desc')->addArg('orderby', 'rating')->url(),
88
                    'label' => esc_html__('Rating, high to low', 'site-reviews'),
89
                    'value' => 'rating:desc',
90
                ];
91
            }
92
            ob_start();
93
            require $templatePath;
94
            return ob_get_clean();
95
        };
96
        return $settings;
97
    }
98
99
    /**
100
     * @param string[] $columns
101
     *
102
     * @filter manage_sc-products_columns
103
     */
104
    public function filterProductColumns(array $columns): array
105
    {
106
        $svg = Svg::get('assets/images/icon.svg', [
107
            'height' => 24,
108
            'style' => 'display:flex; flex-shrink:0; margin: -4px 0;',
109
        ]);
110
        $columns[glsr()->prefix.'rating'] = glsr(Builder::class)->div([
111
            'style' => 'display:flex; align-items:center; justify-content:center;',
112
            'text' => sprintf('%s<span>%s</span>', $svg, _x('Reviews', 'admin-text', 'site-reviews')),
113
        ]);
114
        return $columns;
115
    }
116
117
    /**
118
     * @filter surecart/product/json_schema
119
     */
120
    public function filterProductSchema(array $schema): array
121
    {
122
        $data = glsr(SchemaParser::class)->generate();
123
        $aggregateRatingSchema = Arr::get($data, 'aggregateRating');
124
        $reviewSchema = Arr::get($data, 'review');
125
        if ($aggregateRatingSchema) {
126
            $schema['aggregateRating'] = $aggregateRatingSchema;
127
        }
128
        if ($reviewSchema) {
129
            $schema['review'] = $reviewSchema;
130
        }
131
        // remove Site Reviews generated schema
132
        add_filter('site-reviews/schema/all', '__return_empty_array');
133
        return $schema;
134
    }
135
136
    /**
137
     * @filter site-reviews/review/value/author
138
     */
139
    public function filterReviewAuthorTagValue(string $value, ReviewTag $tag): string
140
    {
141
        $ownership = glsr_get_option('integrations.surecart.ownership', [], 'array');
142
        if (!in_array('labeled', $ownership)) {
143
            return $value;
144
        }
145
        if ($tag->review->hasProductOwner()) {
1 ignored issue
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

145
        if ($tag->review->/** @scrutinizer ignore-call */ hasProductOwner()) {
Loading history...
146
            $text = esc_attr__('verified owner', 'site-reviews');
147
            $value = sprintf('%s <em data-verified-owner="1">(%s)</em>', $value, $text);
148
        }
149
        return $value;
150
    }
151
152
    /**
153
     * @filter site-reviews/review/call/hasProductOwner
154
     */
155
    public function filterReviewCallbackHasProductOwner(Review $review): bool
156
    {
157
        $verified = get_post_meta($review->ID, '_sc_verified', true);
158
        if ('' !== $verified) {
159
            return (bool) $verified;
160
        }
161
        $review->refresh(); // refresh the review first!
162
        $verified = false;
163
        foreach ($review->assigned_posts as $postId) {
164
            if ('sc_product' === get_post_type($postId)) {
165
                $verified = $this->isProductOwner($review->author_id, $postId);
166
                break; // only check the first product
167
            }
168
        }
169
        update_post_meta($review->ID, '_sc_verified', (int) $verified);
170
        return $verified;
171
    }
172
173
    /**
174
     * @filter site-reviews/build/template/reviews-form
175
     */
176
    public function filterReviewFormBuild(string $template, array $data): string
1 ignored issue
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

176
    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...
177
    {
178
        if ('sc_product' !== get_post_type()) {
179
            return $template;
180
        }
181
        $ownership = glsr_get_option('integrations.surecart.ownership', [], 'array');
182
        if (!in_array('restricted', $ownership)) {
183
            return $template;
184
        }
185
        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

185
        if ($this->isProductOwner(get_current_user_id(), /** @scrutinizer ignore-type */ get_the_ID())) {
Loading history...
186
            return $template;
187
        }
188
        return glsr(Builder::class)->p([
189
            'text' => esc_html__('Only logged in customers who have purchased this product may leave a review.', 'woocommerce'),
190
        ]);
191
    }
192
193
    /**
194
     * @param \GeminiLabs\SiteReviews\Modules\Html\ReviewField[] $fields
195
     *
196
     * @return \GeminiLabs\SiteReviews\Modules\Html\ReviewField[]
197
     *
198
     * @filter site-reviews/review-form/fields/visible
199
     */
200
    public function filterReviewFormFields(array $fields, ReviewForm $form): array
201
    {
202
        if (!is_user_logged_in()) {
203
            return $fields;
204
        }
205
        if ('sc_product' !== get_post_type()) {
206
            return $fields;
207
        }
208
        $user = wp_get_current_user();
209
        array_walk($fields, function ($field) use ($form, $user) {
210
            if (in_array($field->original_name, $form->args()->hide)) {
211
                return;
212
            }
213
            if ('email' === $field->original_name && empty($field->value)) {
214
                $field->value = glsr(Sanitizer::class)->sanitizeUserEmail($user->user_email);
215
                return;
216
            }
217
            if ('name' === $field->original_name && empty($field->value)) {
218
                $field->value = glsr(Sanitizer::class)->sanitizeUserName(
219
                    $user->display_name,
220
                    $user->user_nicename
221
                );
222
            }
223
        });
224
        return $fields;
225
    }
226
227
    /**
228
     * @filter site-reviews/shortcode/site_reviews/attributes
229
     * @filter site-reviews/shortcode/site_reviews_form/attributes
230
     * @filter site-reviews/shortcode/site_reviews_summary/attributes
231
     */
232
    public function filterShortcodeAttributes(array $attributes, ShortcodeContract $shortcode): array
1 ignored issue
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

232
    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...
233
    {
234
        $refererQuery = wp_parse_args(wp_parse_url((string) wp_get_referer(), \PHP_URL_QUERY));
235
        $template = $refererQuery['p'] ?? ''; // Get the current Site Editor template
236
        if (!str_starts_with((string) $template, '/wp_template/surecart/') && 'sc_product' !== get_post_type()) {
237
            return $attributes;
238
        }
239
        if ($style = glsr_get_option('integrations.surecart.style')) {
240
            $attributes['data-style'] = $style;
241
        }
242
        return $attributes;
243
    }
244
245
    /**
246
     * @action parse_query
247
     */
248
    public function parseProductQuery(\WP_Query $query): void
249
    {
250
        if ('sc_product' !== $query->get('post_type')) {
251
            return;
252
        }
253
        if ('rating' !== $query->get('orderby')) {
254
            return;
255
        }
256
        $metaQuery = $query->get('meta_query', []);
257
        $order = $query->get('order', 'desc');
258
        if ('bayesian' === glsr_get_option('integrations.surecart.sorting')) {
259
            $metaQuery[] = $this->buildMetaQuery('glsr_ranking', CountManager::META_RANKING);
260
            $query->set('meta_query', $metaQuery);
261
            $query->set('orderby', ['glsr_ranking' => $order]);
262
        } else {
263
            $metaQuery[] = $this->buildMetaQuery('glsr_average', CountManager::META_AVERAGE);
264
            $metaQuery[] = $this->buildMetaQuery('glsr_reviews', CountManager::META_REVIEWS);
265
            $query->set('meta_query', $metaQuery);
266
            $query->set('orderby', ['glsr_average' => $order, 'glsr_reviews' => $order]);
267
        }
268
    }
269
270
    /**
271
     * @action init
272
     */
273
    public function registerBlockPatterns(): void
274
    {
275
        register_block_pattern(glsr()->id.'/surecart-product-reviews', [
276
            'title' => _x('Product Reviews', 'admin-text', 'site-reviews'),
277
            'categories' => ['surecart_product_page'],
278
            'blockTypes' => ['surecart/product-page'],
279
            'priority' => 2,
280
            'content' => '
281
                <!-- wp:group {"layout":{"type":"constrained"}} -->
282
                <div class="wp-block-group">
283
                    <!-- wp:columns {"align":"wide"} -->
284
                    <div class="wp-block-columns alignwide">
285
                        <!-- wp:column {"width":"100%","className":"is-style-default","layout":{"type":"default"}} -->
286
                        <div class="wp-block-column is-style-default" style="flex-basis:100%">
287
                            <!-- wp:heading {"className":"is-style-text-subtitle"} -->
288
                            <h2 class="wp-block-heading is-style-text-subtitle">Reviews</h2>
289
                            <!-- /wp:heading -->
290
                            <!-- wp:site-reviews/reviews {"assigned_posts":["post_id"],"hide":["title"],"id":"reviews","pagination":"ajax","schema":1} /-->
291
                            <!-- wp:heading {"className":"is-style-text-subtitle"} -->
292
                            <h2 class="wp-block-heading is-style-text-subtitle">Submit a Review</h2>
293
                            <!-- /wp:heading -->
294
                            <!-- wp:site-reviews/form {"assigned_posts":["post_id"],"hide":["name","email","title"],"reviews_id":"reviews"} /--></div>
295
                        <!-- /wp:column -->
296
                        </div>
297
                    <!-- /wp:columns -->
298
                    </div>
299
                <!-- /wp:group -->',
300
            // 'postTypes' => ['sc_product'],
301
            // 'templateTypes' => ['sc_product'],
302
        ]);
303
    }
304
305
    /**
306
     * @action init:11
307
     */
308
    public function registerBlocks(): void
309
    {
310
        register_block_type_from_metadata(glsr()->path('assets/blocks/surecart_product_rating'));
311
        register_block_type_from_metadata(glsr()->path('assets/blocks/surecart_product_reviews'));
312
    }
313
314
    /**
315
     * @filter surecart/product/attributes_set
316
     */
317
    public function registerProductAttributes(Product $product): void
318
    {
319
        $postId = $product->post->ID ?? 0;
320
        if (0 === $postId) {
321
            return;
322
        }
323
        $ratingInfo = glsr_get_ratings([
324
            'assigned_posts' => $postId,
325
        ]);
326
        $product->setAttribute('rating', $ratingInfo->average);
327
        $product->setAttribute('reviews', $ratingInfo->reviews);
328
        $product->setAttribute('ranking', $ratingInfo->ranking);
329
    }
330
331
    /**
332
     * @action manage_sc-products_custom_column
333
     */
334
    public function renderProductColumnValues(string $column, $product): void
335
    {
336
        if (glsr()->prefix.'rating' !== $column) {
337
            return;
338
        }
339
        echo glsr(Builder::class)->a([
340
            'href' => add_query_arg('assigned_post', $product->post->ID, glsr_admin_url()),
341
            'text' => $product->reviews,
342
        ]);
343
    }
344
345
    /**
346
     * @param string $which
347
     *
348
     * @action manage_products_extra_tablenav
349
     */
350
    public function renderProductTableInlineStyles($which): void
351
    {
352
        if ('top' !== $which) {
353
            return;
354
        }
355
        echo '<style>'.
356
        '@media screen and (min-width: 783px) {'.
357
            '.fixed .column-glsr_rating { width: 5%; }'.
358
            'th.column-glsr_rating span { display: none; }'.
359
            'td.column-glsr_rating { text-align: center; }'.
360
        '}'.
361
        '</style>';
362
    }
363
364
    /**
365
     * @action site-reviews/review/created
366
     */
367
    public function verifyProductOwner(Review $review): void
368
    {
369
        $review->hasProductOwner();
370
    }
371
372
    protected function buildMetaQuery(string $orderbyKey, string $metaKey): array
373
    {
374
        return [
375
            'relation' => 'OR',
376
            $orderbyKey => ['key' => $metaKey, 'compare' => 'NOT EXISTS'], // this comes first!
377
            ['key' => $metaKey, 'compare' => 'EXISTS'],
378
        ];
379
    }
380
381
    protected function isProductOwner(int $userId, int $productId): bool
382
    {
383
        if (!$user = User::getUserBy('id', $userId)) { // @phpstan-ignore-line
384
            return false;
385
        }
386
        if (!$customer = $user->customer()) { // @phpstan-ignore-line
387
            return false;
388
        }
389
        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

389
        if (!$product = /** @scrutinizer ignore-call */ sc_get_product($productId)) {
Loading history...
390
            return false;
391
        }
392
        $purchases = Purchase::where([ // @phpstan-ignore-line
393
            'customer_ids' => [$customer->id],
394
            'product_ids' => [$product->id], // @phpstan-ignore-line
395
        ])->get();
396
        return !empty($purchases);
397
    }
398
}
399