ReviewManager   F
last analyzed

Complexity

Total Complexity 68

Size/Duplication

Total Lines 381
Duplicated Lines 0 %

Test Coverage

Coverage 54.03%

Importance

Changes 3
Bugs 2 Features 0
Metric Value
wmc 68
eloc 214
c 3
b 2
f 0
dl 0
loc 381
ccs 114
cts 211
cp 0.5403
rs 2.96

23 Methods

Rating   Name   Duplication   Size   Complexity  
A assignTerm() 0 13 3
A assignUser() 0 13 3
A createFromPost() 0 8 2
A assignPost() 0 17 4
A create() 0 14 4
A deleteRating() 0 7 2
A createRaw() 0 39 2
A deleteRevisions() 0 5 2
A duplicate() 0 20 6
A unassignTerm() 0 11 2
A unassignUser() 0 11 2
A reviews() 0 8 1
A get() 0 3 1
A updateResponse() 0 18 4
A updateCustom() 0 6 2
A updateAssignedPost() 0 7 1
B update() 0 29 6
A unassignPost() 0 14 3
A updateRating() 0 20 5
A isPublishedPost() 0 4 1
A updateReview() 0 27 5
A postStatus() 0 12 6
A total() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like ReviewManager 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 ReviewManager, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace GeminiLabs\SiteReviews\Database;
4
5
use GeminiLabs\SiteReviews\Commands\CreateReview;
6
use GeminiLabs\SiteReviews\Database;
7
use GeminiLabs\SiteReviews\Defaults\CustomFieldsDefaults;
8
use GeminiLabs\SiteReviews\Defaults\RatingDefaults;
9
use GeminiLabs\SiteReviews\Defaults\UpdateReviewDefaults;
10
use GeminiLabs\SiteReviews\Helpers\Arr;
11
use GeminiLabs\SiteReviews\Helpers\Cast;
12
use GeminiLabs\SiteReviews\Modules\Sanitizer;
13
use GeminiLabs\SiteReviews\Request;
14
use GeminiLabs\SiteReviews\Review;
15
use GeminiLabs\SiteReviews\Reviews;
16
17
class ReviewManager
18
{
19 2
    /**
20
     * Foreign indexes ensure that $postId is a valid Post ID.
21 2
     */
22 1
    public function assignPost(Review $review, int $postId): bool
23
    {
24 2
        if (!glsr()->can('assign_post', $postId)) {
25 2
            return false;
26 2
        }
27 2
        $data = [
28 2
            'is_published' => $this->isPublishedPost($postId),
29 2
            'post_id' => $postId,
30 2
            'rating_id' => $review->rating_id,
31 2
        ];
32 2
        if ($result = glsr(Database::class)->insert('assigned_posts', $data)) {
33
            glsr(Cache::class)->delete($review->ID, 'reviews');
34
            if (!defined('WP_IMPORTING')) {
35 2
                glsr(CountManager::class)->posts($postId);
36
            }
37
        }
38 2
        return Cast::toInt($result) > 0;
39
    }
40 2
41 2
    /**
42 2
     * Foreign indexes ensure that $termId is a valid Term ID.
43 2
     */
44 2
    public function assignTerm(Review $review, int $termId): bool
45 2
    {
46 2
        $data = [
47 2
            'rating_id' => $review->rating_id,
48
            'term_id' => $termId,
49
        ];
50 2
        if ($result = glsr(Database::class)->insert('assigned_terms', $data)) {
51
            glsr(Cache::class)->delete($review->ID, 'reviews');
52
            if (!defined('WP_IMPORTING')) {
53 2
                glsr(CountManager::class)->terms($termId);
54
            }
55 2
        }
56 2
        return Cast::toInt($result) > 0;
57 2
    }
58 2
59 2
    /**
60 2
     * Foreign indexes ensure that $userId is a valid User ID.
61 2
     */
62 2
    public function assignUser(Review $review, int $userId): bool
63
    {
64
        $data = [
65 2
            'rating_id' => $review->rating_id,
66
            'user_id' => $userId,
67
        ];
68
        if ($result = glsr(Database::class)->insert('assigned_users', $data)) {
69
            glsr(Cache::class)->delete($review->ID, 'reviews');
70
            if (!defined('WP_IMPORTING')) {
71 24
                glsr(CountManager::class)->users($userId);
72
            }
73 24
        }
74 24
        return Cast::toInt($result) > 0;
75
    }
76 24
77
    /**
78
     * @return Review|false
79 24
     */
80 24
    public function create(CreateReview $command, ?int $postId = null)
81 24
    {
82 24
        if (empty($postId)) {
83
            $postId = $this->createRaw($command);
84
        }
85
        if (empty($postId)) {
86
            return false;
87
        }
88
        $review = $this->get($postId);
89
        if ($review->isValid()) {
90
            glsr()->action('review/created', $review, $command);
91
            return $this->get($review->ID); // return a fresh copy of the review
92
        }
93
        return false;
94
    }
95
96
    /**
97
     * @return Review|false
98
     */
99
    public function createFromPost(int $postId, array $data = [])
100
    {
101
        if (!Review::isReview($postId)) {
102
            return false;
103 24
        }
104
        $command = new CreateReview(new Request($data));
105 24
        glsr()->action('review/create', $postId, $command);
106 24
        return $this->create($command, $postId);
107 24
    }
108 24
109 24
    /**
110 24
     * @return int|false
111 24
     */
112 24
    public function createRaw(CreateReview $command)
113 24
    {
114 24
        $values = glsr()->args($command->toArray()); // this filters the values
115 24
        $submitted = $command->request->toArray($excludedKeys = [
116 24
            '_frcaptcha',
117 24
            '_hcaptcha',
118 24
            '_nonce',
119 24
            '_procaptcha',
120 24
            '_recaptcha',
121 24
            '_turnstile',
122 24
            'form_signature',
123
        ]);
124
        $metaInput = [
125
            '_submitted' => $submitted, // save the original submitted request in metadata
126 24
            '_submitted_hash' => md5(maybe_serialize($submitted)),
127 24
        ];
128
        $values = [
129
            'comment_status' => 'closed',
130
            'meta_input' => $metaInput,
131
            'ping_status' => 'closed',
132
            'post_author' => $values->author_id,
133
            'post_content' => $values->content,
134
            'post_date' => $values->date,
135
            'post_date_gmt' => $values->date_gmt,
136
            'post_modified' => $values->date,
137
            'post_modified_gmt' => $values->date_gmt,
138
            'post_name' => uniqid($values->type),
139
            'post_status' => $this->postStatus($command),
140
            'post_title' => $values->title,
141
            'post_type' => glsr()->post_type,
142
        ];
143
        $values = glsr()->filterArray('review/create/post_data', $values, $command);
144
        $postId = wp_insert_post($values, true);
145
        if (is_wp_error($postId)) {
146
            glsr_log()->error($postId->get_error_message())->debug($values);
147 24
            return false;
148
        }
149 24
        glsr()->action('review/create', $postId, $command);
150
        return $postId;
151
    }
152 2
153
    public function deleteRating(int $reviewId): bool
154 2
    {
155 2
        $result = glsr(Database::class)->delete('ratings', ['review_id' => $reviewId]);
156 2
        if ($result) {
157 2
            glsr(Cache::class)->delete($reviewId, 'reviews');
158 2
        }
159 2
        return Cast::toInt($result) > 0;
160
    }
161
162 2
    public function deleteRevisions(int $reviewId): void
163
    {
164 2
        $revisionIds = glsr(Query::class)->revisionIds((int) $reviewId);
165
        foreach ($revisionIds as $revisionId) {
166
            wp_delete_post_revision($revisionId);
167 1
        }
168
    }
169 1
170
    /**
171
     * @return Review|false
172 1
     */
173 1
    public function duplicate(int $reviewId)
174 1
    {
175 1
        if (!Review::isReview($reviewId)) {
176 1
            return false;
177 1
        }
178 1
        $review = glsr_get_review($reviewId);
179
        if (!$review->isValid()) {
180 1
            return false;
181
        }
182
        $data = $review->toArray();
183 1
        $data['author_id'] = get_current_user_id();
184
        $data['is_approved'] = $data['is_approved'] && glsr()->can('publish_posts');
185 1
        $duplicate = glsr_create_review($data);
186 1
        foreach ($review->meta() as $key => $value) {
187 1
            if (!str_starts_with($key, '_submitted')) {
188 1
                update_post_meta($duplicate->ID, $key, $value);
189 1
            }
190 1
        }
191 1
        update_post_meta($duplicate->ID, '_duplicated_from', $review->ID);
192
        return $duplicate;
193 1
    }
194
195
    public function get(int $reviewId, bool $bypassCache = false): Review
196 1
    {
197
        return glsr(Query::class)->review($reviewId, $bypassCache);
198 1
    }
199 1
200 1
    public function reviews(array $args = []): Reviews
201 1
    {
202 1
        $args = (new NormalizePaginationArgs($args))->toArray();
203 1
        $results = glsr(Query::class)->reviews($args);
204 1
        $total = $this->total($args, $results);
205
        $reviews = new Reviews($results, $total, $args);
206 1
        glsr()->action('get/reviews', $reviews, $args);
207
        return $reviews;
208
    }
209
210
    public function total(array $args = [], array $reviews = []): int
211
    {
212
        return glsr(Query::class)->totalReviews($args, $reviews);
213
    }
214
215
    public function unassignPost(Review $review, int $postId): bool
216
    {
217
        if (!glsr()->can('unassign_post', $postId)) {
218
            return false;
219
        }
220
        $where = [
221
            'post_id' => $postId,
222
            'rating_id' => $review->rating_id,
223
        ];
224
        if ($result = glsr(Database::class)->delete('assigned_posts', $where)) {
225
            glsr(Cache::class)->delete($review->ID, 'reviews');
226
            glsr(CountManager::class)->posts($postId);
227
        }
228
        return Cast::toInt($result) > 0;
229
    }
230
231
    public function unassignTerm(Review $review, int $termId): bool
232
    {
233
        $where = [
234
            'rating_id' => $review->rating_id,
235
            'term_id' => $termId,
236
        ];
237
        if ($result = glsr(Database::class)->delete('assigned_terms', $where)) {
238
            glsr(Cache::class)->delete($review->ID, 'reviews');
239
            glsr(CountManager::class)->terms($termId);
240
        }
241
        return Cast::toInt($result) > 0;
242
    }
243
244
    public function unassignUser(Review $review, int $userId): bool
245
    {
246
        $where = [
247
            'rating_id' => $review->rating_id,
248
            'user_id' => $userId,
249
        ];
250
        if ($result = glsr(Database::class)->delete('assigned_users', $where)) {
251
            glsr(Cache::class)->delete($review->ID, 'reviews');
252
            glsr(CountManager::class)->users($userId);
253
        }
254
        return Cast::toInt($result) > 0;
255
    }
256
257
    /**
258
     * @return Review|false
259
     */
260
    public function update(int $reviewId, array $data = [])
261
    {
262
        $oldPost = get_post($reviewId);
263
        if (-1 === $this->updateRating($reviewId, $data)) {
264
            glsr_log('update rating failed');
265
            return false;
266
        }
267
        if (-1 === $this->updateReview($reviewId, $data)) {
268
            glsr_log('update review failed');
269
            return false;
270
        }
271
        $this->updateCustom($reviewId, $data);
272
        $this->updateResponse($reviewId, $data);
273
        $review = $this->get($reviewId);
274
        if (isset($data['assigned_posts'])) {
275
            $assignedPosts = glsr(Sanitizer::class)->sanitizePostIds($data['assigned_posts']);
276
            glsr()->action('review/updated/post_ids', $review, $assignedPosts); // triggers a recount of assigned posts
277
        }
278
        if (isset($data['assigned_terms'])) {
279
            $assignedTerms = glsr(Sanitizer::class)->sanitizeTermIds($data['assigned_terms']);
280
            wp_set_object_terms($reviewId, $assignedTerms, glsr()->taxonomy); // triggers a recount of assigned_terms
281
        }
282
        if (isset($data['assigned_users'])) {
283
            $assignedUsers = glsr(Sanitizer::class)->sanitizeUserIds($data['assigned_users']);
284
            glsr()->action('review/updated/user_ids', $review, $assignedUsers); // triggers a recount of assigned posts
285
        }
286
        $review = $this->get($reviewId); // get a fresh copy of the review
287
        glsr()->action('review/updated', $review, $data, $oldPost);
288
        return $review;
289
    }
290
291
    public function updateAssignedPost(int $postId): bool
292
    {
293
        $result = glsr(Database::class)->update('assigned_posts',
294
            ['is_published' => $this->isPublishedPost($postId)],
295
            ['post_id' => Cast::toInt($postId)]
296
        );
297
        return Cast::toInt($result) > 0;
298
    }
299
300
    public function updateCustom(int $reviewId, array $values = []): void
301
    {
302
        $data = glsr(CustomFieldsDefaults::class)->merge($values);
303
        $data = Arr::prefixKeys($data, 'custom_');
304
        foreach ($data as $metaKey => $metaValue) {
305
            glsr(Database::class)->metaSet($reviewId, $metaKey, $metaValue);
306
        }
307
    }
308
309
    public function updateRating(int $reviewId, array $values = []): int
310
    {
311
        $review = $this->get($reviewId);
312
        glsr(Cache::class)->delete($reviewId, 'reviews');
313
        $sanitized = glsr(RatingDefaults::class)->restrict($values);
314
        $data = array_intersect_key($sanitized, $values);
315
        if (empty($data)) {
316
            return 0;
317
        }
318
        $result = glsr(Database::class)->update('ratings', $data, [
319
            'review_id' => $reviewId,
320 2
        ]);
321
        if (false === $result) {
322 2
            return -1;
323 2
        }
324
        $rating = $data['rating'] ?? '';
325
        if (is_numeric($rating) && $review->rating !== $data['rating']) {
326 24
            glsr(CountManager::class)->recalculate();
327
        }
328 24
        return Cast::toInt($result);
329 24
    }
330 24
331 21
    public function updateResponse(int $reviewId, array $values): int
332 21
    {
333 21
        if (!array_key_exists('response', $values)) {
334
            return 0;
335 24
        }
336 1
        $response = Arr::getAs('string', $values, 'response');
337 24
        $response = glsr(Sanitizer::class)->sanitizeTextHtml($response);
338
        $review = glsr_get_review($reviewId);
339
        if (empty($response) && empty($review->response)) {
340
            return 0;
341
        }
342
        glsr(Database::class)->metaSet($review->ID, 'response', $response); // prefixed metakey
343
        // This should run immediately after saving the response
344
        // but before adding the "response_by" meta_value!
345
        glsr()->action('review/responded', $review, $response);
346
        glsr(Database::class)->metaSet($review->ID, 'response_by', get_current_user_id()); // prefixed metakey
347
        glsr(Cache::class)->delete($review->ID, 'reviews');
348
        return 1;
349
    }
350
351
    public function updateReview(int $reviewId, array $values = []): int
352
    {
353
        if (glsr()->post_type !== get_post_type($reviewId)) {
354
            glsr_log()->error("Review update failed: Post ID [{$reviewId}] is not a review.");
355
            return -1;
356
        }
357
        glsr(Cache::class)->delete($reviewId, 'reviews');
358
        $sanitized = glsr(UpdateReviewDefaults::class)->restrict($values);
359
        if ($data = array_intersect_key($sanitized, $values)) {
360
            $data = array_filter([
361
                'post_content' => Arr::get($data, 'content'),
362
                'post_date' => Arr::get($data, 'date'),
363
                'post_date_gmt' => Arr::get($data, 'date_gmt'),
364
                'post_status' => Arr::get($data, 'status'),
365
                'post_title' => Arr::get($data, 'title'),
366
            ]);
367
        }
368
        if (empty($data)) {
369
            return 0;
370
        }
371
        $data = wp_parse_args(['ID' => $reviewId], $data);
372
        $result = wp_update_post($data, true);
373
        if (is_wp_error($result)) {
374
            glsr_log()->error($result->get_error_message())->debug($data);
375
            return -1;
376
        }
377
        return Cast::toInt($result);
378
    }
379
380
    protected function isPublishedPost(int $postId): bool
381
    {
382
        $isPublished = 'publish' === get_post_status($postId);
383
        return glsr()->filterBool('post/is-published', $isPublished, $postId);
384
    }
385
386
    protected function postStatus(CreateReview $command): string
387
    {
388
        $isApproved = $command->is_approved;
389
        $isFormSubmission = !defined('WP_IMPORTING') && !glsr()->retrieve('glsr_create_review', false);
390
        if ($isFormSubmission) {
391
            $requireApproval = glsr(OptionManager::class)->getBool('settings.general.require.approval');
392
            $requireApprovalForRating = glsr(OptionManager::class)->getInt('settings.general.require.approval_for', 5);
393
            $isApproved = !$requireApproval || $command->rating > $requireApprovalForRating;
394
        }
395
        return !$isApproved || $command->isBlacklisted()
396
            ? 'pending'
397
            : 'publish';
398
    }
399
}
400