Passed
Push — develop ( d01060...4713f8 )
by Paul
14:54
created

ListTableController   F

Complexity

Total Complexity 81

Size/Duplication

Total Lines 428
Duplicated Lines 0 %

Test Coverage

Coverage 1.17%

Importance

Changes 0
Metric Value
wmc 81
eloc 233
dl 0
loc 428
ccs 3
cts 257
cp 0.0117
rs 2
c 0
b 0
f 0

21 Methods

Rating   Name   Duplication   Size   Complexity  
A isOrderbyWithIsNull() 0 7 1
A setQueryForTable() 0 18 5
A filterScreenFilters() 0 25 5
A modifyClauseJoin() 0 22 4
A filterSearchQuery() 0 11 4
A isListFiltered() 0 3 1
A modifyClauseOrderby() 0 18 3
B overrideInlineSaveAjax() 0 32 6
A filterListTableClass() 0 10 3
A filterDateColumnStatus() 0 6 2
A filterByValues() 0 5 1
A isListOrdered() 0 5 1
B filterCheckLockedReviews() 0 39 10
A filterDefaultHiddenColumns() 0 7 2
A renderColumnValues() 0 12 2
A filterColumnsForPostType() 0 9 4
B filterPostClauses() 0 28 9
A filterSortableColumns() 0 10 3
A renderColumnFilters() 0 9 3
B modifyClauseWhere() 0 27 7
A filterRowActions() 0 32 5

How to fix   Complexity   

Complex Class

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

1
<?php
2
3
namespace GeminiLabs\SiteReviews\Controllers;
4
5
use GeminiLabs\SiteReviews\Database\ReviewManager;
6
use GeminiLabs\SiteReviews\Database\Tables;
7
use GeminiLabs\SiteReviews\Defaults\ColumnFilterbyDefaults;
8
use GeminiLabs\SiteReviews\Defaults\ColumnOrderbyDefaults;
9
use GeminiLabs\SiteReviews\Defaults\ListtableFiltersDefaults;
10
use GeminiLabs\SiteReviews\Defaults\SqlClauseDefaults;
11
use GeminiLabs\SiteReviews\Helper;
12
use GeminiLabs\SiteReviews\Helpers\Arr;
13
use GeminiLabs\SiteReviews\Helpers\Cast;
14
use GeminiLabs\SiteReviews\Helpers\Str;
15
use GeminiLabs\SiteReviews\Modules\Html\Builder;
16
use GeminiLabs\SiteReviews\Modules\Migrate;
17
use GeminiLabs\SiteReviews\Modules\Sanitizer;
18
use GeminiLabs\SiteReviews\Overrides\ReviewsListTable;
19
20
class ListTableController extends AbstractController
21
{
22
    /**
23
     * @filter heartbeat_received
24
     */
25
    public function filterCheckLockedReviews(array $response, array $data): array
26
    {
27
        $checked = [];
28
        $postIds = Arr::consolidate(Arr::get($data, 'wp-check-locked-posts'));
29
        foreach ($postIds as $key) {
30
            $postId = absint(substr($key, 5));
31
            $userId = (int) wp_check_post_lock($postId);
32
            $user = get_user_by('id', $userId);
33
            if (!$user) {
34
                continue;
35
            }
36
            $name = glsr(Sanitizer::class)->sanitizeUserName($user);
37
            if (!glsr()->can('edit_post', $postId) && glsr()->can('respond_to_post', $postId)) {
38
                $send = [
39
                    'text' => sprintf(_x('%s is currently editing', 'admin-text', 'site-reviews'), $name),
40
                ];
41
                if (get_option('show_avatars')) {
42
                    $send['avatar_src'] = get_avatar_url($user->ID, ['size' => 18]);
43
                    $send['avatar_src_2x'] = get_avatar_url($user->ID, ['size' => 36]);
44
                }
45
                $checked[$key] = $send;
46
            }
47
            if (glsr()->can('edit_post', $postId)) {
48
                continue;
49
            }
50
            if (!glsr()->can('respond_to_post', $postId)) {
51
                continue;
52
            }
53
            $send = ['text' => sprintf(_x('%s is currently editing', 'admin-text', 'site-reviews'), $name)];
54
            if (get_option('show_avatars')) {
55
                $send['avatar_src'] = get_avatar_url($user->ID, ['size' => 18]);
56
                $send['avatar_src_2x'] = get_avatar_url($user->ID, ['size' => 36]);
57
            }
58
            $checked[$key] = $send;
59
        }
60
        if (!empty($checked)) {
61
            $response['wp-check-locked-posts'] = $checked;
62
        }
63
        return $response;
64
    }
65
66
    /**
67
     * @param string[] $columns
68
     *
69
     * @filter manage_{glsr()->post_type}_posts_columns
70
     */
71
    public function filterColumnsForPostType(array $columns): array
72
    {
73
        $postTypeColumns = glsr()->retrieveAs('array', 'columns.'.glsr()->post_type, []);
74
        foreach ($postTypeColumns as $key => &$value) {
75
            if (array_key_exists($key, $columns) && empty($value)) {
76
                $value = $columns[$key];
77
            }
78
        }
79
        return array_filter($postTypeColumns, 'strlen'); // @phpstan-ignore-line
80
    }
81
82
    /**
83
     * @filter post_date_column_status
84
     */
85
    public function filterDateColumnStatus(string $status, \WP_Post $post): string
86
    {
87
        if (glsr()->post_type === $post->post_type) {
88
            return _x('Submitted', 'admin-text', 'site-reviews');
89
        }
90
        return $status;
91
    }
92
93
    /**
94
     * @param string[] $hidden
95
     *
96
     * @filter default_hidden_columns
97
     */
98
    public function filterDefaultHiddenColumns(array $hidden, \WP_Screen $screen): array
99
    {
100
        if ('edit-'.glsr()->post_type === $screen->id) {
101
            $hiddenColumns = glsr()->retrieveAs('array', 'columns_hidden.'.glsr()->post_type, []);
102
            return array_unique(array_merge($hidden, $hiddenColumns));
103
        }
104
        return $hidden;
105
    }
106
107
    /**
108
     * @filter wp_list_table_class_name
109
     */
110
    public function filterListTableClass(string $className): string
111
    {
112
        $screen = glsr_current_screen();
113
        if (glsr()->post_type !== $screen->post_type) {
114
            return $className;
115
        }
116
        if ('edit' !== $screen->base) {
117
            return $className;
118
        }
119
        return ReviewsListTable::class;
120
    }
121
122
    /**
123
     * @filter posts_clauses
124
     */
125
    public function filterPostClauses(array $postClauses, \WP_Query $query): array
126
    {
127
        if (!$this->hasQueryPermission($query)) {
128
            return $postClauses;
129
        }
130
        $clauses = array_fill_keys(array_keys($postClauses), []);
131
        if ($this->isListFiltered() || $this->isListOrdered()) {
132
            foreach ($postClauses as $key => $clause) {
133
                $method = Helper::buildMethodName('modifyClause', $key);
134
                if (method_exists($this, $method)) {
135
                    $clauses[$key] = call_user_func([$this, $method], $clause, $query);
136
                }
137
            }
138
        }
139
        $clauses = glsr()->filterArray('review-table/clauses', $clauses, $postClauses, $query);
140
        foreach ($clauses as $key => $clause) {
141
            $clause = glsr(SqlClauseDefaults::class)->restrict($clause);
142
            if (empty($clause['clauses'])) {
143
                continue;
144
            }
145
            $values = array_values(array_unique($clause['clauses']));
146
            $values = implode(' ', $values);
147
            if (!$clause['replace']) {
148
                $values = trim($postClauses[$key])." {$values}";
149
            }
150
            $postClauses[$key] = " {$values} ";
151
        }
152
        return $postClauses;
153
    }
154
155
    /**
156
     * @param string[] $actions
157
     *
158
     * @filter post_row_actions
159
     */
160
    public function filterRowActions(array $actions, \WP_Post $post): array
161
    {
162
        if (glsr()->post_type !== Arr::get($post, 'post_type') || 'trash' === $post->post_status) {
163
            return $actions;
164
        }
165
        unset($actions['inline hide-if-no-js']);
166
        $baseurl = admin_url("post.php?post={$post->ID}&plugin=".glsr()->id);
167
        $newActions = ['id' => sprintf('<span>ID: %d</span>', $post->ID)];
168
        if (glsr()->can('publish_post', $post->ID)) {
169
            $newActions['approve'] = glsr(Builder::class)->a([
170
                'aria-label' => esc_attr(_x('Approve this review', 'admin-text', 'site-reviews')),
171
                'class' => 'glsr-toggle-status',
172
                'href' => wp_nonce_url(add_query_arg('action', 'approve', $baseurl), "approve-review_{$post->ID}"),
173
                'text' => _x('Approve', 'admin-text', 'site-reviews'),
174
            ]);
175
            $newActions['unapprove'] = glsr(Builder::class)->a([
176
                'aria-label' => esc_attr(_x('Unapprove this review', 'admin-text', 'site-reviews')),
177
                'class' => 'glsr-toggle-status',
178
                'href' => wp_nonce_url(add_query_arg('action', 'unapprove', $baseurl), "unapprove-review_{$post->ID}"),
179
                'text' => _x('Unapprove', 'admin-text', 'site-reviews'),
180
            ]);
181
        }
182
        if (glsr()->can('respond_to_post', $post->ID)) {
183
            $newActions['respond hide-if-no-js'] = glsr(Builder::class)->button([
184
                'aria-expanded' => false,
185
                'aria-label' => esc_attr(sprintf(_x('Respond inline to &#8220;%s&#8221;', 'admin-text', 'site-reviews'), _draft_or_post_title())),
186
                'class' => 'button-link editinline',
187
                'text' => _x('Respond', 'admin-text', 'site-reviews'),
188
                'type' => 'button',
189
            ]);
190
        }
191
        return $newActions + $actions;
192
    }
193
194
    /**
195
     * @filter screen_settings
196
     */
197
    public function filterScreenFilters(?string $settings, \WP_Screen $screen): string
198
    {
199
        if ('edit-'.glsr()->post_type === $screen->id) {
200
            $userId = get_current_user_id();
201
            $filters = glsr(ListtableFiltersDefaults::class)->defaults();
202
            if (count(glsr()->retrieveAs('array', 'review_types')) < 2) {
203
                unset($filters['type']);
204
            }
205
            foreach ($filters as $key => &$value) {
206
                $value = glsr($value)->title();
207
            }
208
            ksort($filters);
209
            $setting = 'edit_'.glsr()->post_type.'_filters';
210
            $enabled = get_user_meta($userId, $setting, true);
211
            if (!is_array($enabled)) {
212
                $enabled = ['rating']; // the default enabled filters
213
                update_user_meta($userId, $setting, $enabled);
214
            }
215
            $settings .= glsr()->build('partials/screen/filters', [
216
                'enabled' => $enabled,
217
                'filters' => $filters,
218
                'setting' => $setting,
219
            ]);
220
        }
221
        return (string) $settings;
222
    }
223
224
    /**
225
     * @action posts_search
226
     */
227
    public function filterSearchQuery(string $search, \WP_Query $query): string
228
    {
229
        if (!$this->hasQueryPermission($query)) {
230
            return $search;
231
        }
232
        if (!is_numeric($query->get('s')) || empty($search)) {
233
            return $search;
234
        }
235
        global $wpdb;
236
        $replace = $wpdb->prepare("{$wpdb->posts}.ID = %d", $query->get('s'));
237
        return str_replace('AND (((', "AND ((({$replace}) OR (", $search);
238
    }
239
240
    /**
241
     * @filter manage_edit-{glsr()->post_type}_sortable_columns
242
     */
243
    public function filterSortableColumns(array $columns): array
244
    {
245
        $postTypeColumns = glsr()->retrieveAs('array', 'columns.'.glsr()->post_type, []);
246
        unset($postTypeColumns['cb']);
247
        foreach ($postTypeColumns as $key => $value) {
248
            if (!Str::startsWith($key, ['assigned', 'taxonomy'])) {
249
                $columns[$key] = $key;
250
            }
251
        }
252
        return $columns;
253
    }
254
255
    /**
256
     * @action wp_ajax_inline_save
257
     */
258
    public function overrideInlineSaveAjax(): void
259
    {
260
        $screen = filter_input(INPUT_POST, 'screen');
261
        if ('edit-'.glsr()->post_type !== $screen) {
262
            return; // don't override
263
        }
264
        check_ajax_referer('inlineeditnonce', '_inline_edit');
265
        if (empty($postId = filter_input(INPUT_POST, 'post_ID', FILTER_VALIDATE_INT))) {
266
            wp_die();
267
        }
268
        if (!glsr()->can('respond_to_post', $postId)) {
269
            wp_die(_x('Sorry, you are not allowed to respond to this review.', 'admin-text', 'site-reviews'));
270
        }
271
        if ($last = wp_check_post_lock($postId)) {
272
            $name = _x('Someone', 'admin-text', 'site-reviews');
273
            $user = get_user_by('id', $last);
274
            if ($user) {
275
                $name = glsr(Sanitizer::class)->sanitizeUserName($user);
276
            }
277
            $message = esc_html_x('Saving is disabled: %s is currently editing this review.', 'admin-text', 'site-reviews');
278
            printf($message, $name);
279
            wp_die();
280
        }
281
        $response = (string) filter_input(INPUT_POST, '_response');
282
        glsr(ReviewManager::class)->updateResponse($postId, compact('response'));
283
        $review = glsr_get_review($postId);
284
        glsr()->action('cache/flush', "review_{$review->ID}_responded", $review);
285
        global $mode;
286
        $mode = Str::restrictTo(['excerpt', 'list'], (string) filter_input(INPUT_POST, 'post_view'), 'list');
287
        $table = new ReviewsListTable(['screen' => convert_to_screen($screen)]);
288
        $table->display_rows([get_post($postId)], 0);
289
        wp_die();
290
    }
291
292
    /**
293
     * @action restrict_manage_posts
294
     */
295
    public function renderColumnFilters(string $postType): void
296
    {
297
        if (glsr()->post_type === $postType) {
298
            $filters = glsr(ListtableFiltersDefaults::class)->defaults();
299
            $enabledFilters = Arr::consolidate(
300
                get_user_meta(get_current_user_id(), 'edit_'.glsr()->post_type.'_filters', true)
301
            );
302
            foreach ($filters as $filter) {
303
                echo Cast::toString(glsr()->runIf($filter, $enabledFilters));
304
            }
305
        }
306
    }
307
308
    /**
309
     * @action manage_{glsr()->post_type}_posts_custom_column
310
     */
311
    public function renderColumnValues(string $column, int $postId): void
312
    {
313
        $review = glsr(ReviewManager::class)->get((int) $postId);
314
        if (!$review->isValid()) {
315
            glsr(Migrate::class)->reset(); // looks like a migration is needed!
316
            return;
317
        }
318
        $className = Helper::buildClassName(['ColumnValue', $column], 'Controllers\ListTableColumns');
319
        $className = glsr()->filterString("column/{$column}", $className);
320
        $value = glsr()->runIf($className, $review);
321
        $value = glsr()->filterString("columns/{$column}", $value, $postId);
322
        echo Helper::ifEmpty($value, '&mdash;');
323
    }
324
325
    /**
326
     * @action pre_get_posts
327
     */
328 22
    public function setQueryForTable(\WP_Query $query): void
329
    {
330 22
        if (!$this->hasQueryPermission($query)) {
331 22
            return;
332
        }
333
        $orderby = $query->get('orderby');
334
        if ('response' === $orderby) {
335
            $query->set('meta_key', Str::prefix($orderby, '_'));
336
            $query->set('orderby', 'meta_value');
337
        }
338
        if ($termId = filter_input(INPUT_GET, 'category', FILTER_SANITIZE_NUMBER_INT)) {
339
            $taxQuery = ['taxonomy' => glsr()->taxonomy];
340
            if (-1 === Cast::toInt($termId)) {
341
                $taxQuery['operator'] = 'NOT EXISTS';
342
            } else {
343
                $taxQuery['terms'] = $termId;
344
            }
345
            $query->set('tax_query', [$taxQuery]);
346
        }
347
    }
348
349
    protected function filterByValues(): array
350
    {
351
        $filterBy = glsr(ColumnFilterbyDefaults::class)->defaults();
352
        $filterBy = filter_input_array(INPUT_GET, $filterBy);
353
        return Arr::removeEmptyValues(Arr::consolidate($filterBy));
354
    }
355
356
    protected function isListFiltered(): bool
357
    {
358
        return !empty($this->filterByValues());
359
    }
360
361
    protected function isListOrdered(): bool
362
    {
363
        $columns = glsr(ColumnOrderbyDefaults::class)->defaults();
364
        $column = Cast::toString(get_query_var('orderby')); // get_query_var output is unpredictable
365
        return array_key_exists($column, $columns);
366
    }
367
368
    protected function isOrderbyWithIsNull(string $column): bool
369
    {
370
        $columns = [
371
            'email', 'name', 'ip_address', 'type',
372
        ];
373
        $columns = glsr()->filterArray('columns/orderby-is-null', $columns);
374
        return in_array($column, $columns);
375
    }
376
377
    protected function modifyClauseJoin(string $join, \WP_Query $query): array
378
    {
379
        $clause = glsr(SqlClauseDefaults::class)->restrict([
380
            'clauses' => [],
381
            'replace' => false,
382
        ]);
383
        $posts = glsr(Tables::class)->table('posts');
384
        $ratings = glsr(Tables::class)->table('ratings');
385
        $clause['clauses'][] = "INNER JOIN {$ratings} ON ({$ratings}.review_id = {$posts}.ID)";
386
        foreach ($this->filterByValues() as $key => $value) {
387
            if (!in_array($key, ['assigned_post', 'assigned_user'])) {
388
                continue;
389
            }
390
            $assignedTable = glsr(Tables::class)->table($key.'s');
391
            $value = Cast::toInt($value);
392
            if (0 === $value) {
393
                $clause['clauses'][] = "LEFT JOIN {$assignedTable} ON ({$assignedTable}.rating_id = {$ratings}.ID)";
394
            } else {
395
                $clause['clauses'][] = "INNER JOIN {$assignedTable} ON ({$assignedTable}.rating_id = {$ratings}.ID)";
396
            }
397
        }
398
        return $clause;
399
    }
400
401
    protected function modifyClauseOrderby(string $orderby, \WP_Query $query): array
402
    {
403
        $clause = glsr(SqlClauseDefaults::class)->restrict([
404
            'clauses' => [],
405
            'replace' => true,
406
        ]);
407
        $columns = glsr(ColumnOrderbyDefaults::class)->defaults();
408
        $column = Arr::get($columns, $query->get('orderby'));
409
        if (empty($column)) {
410
            return $clause;
411
        }
412
        $ratings = glsr(Tables::class)->table('ratings');
413
        if ($this->isOrderbyWithIsNull($column)) {
414
            $clause['clauses'][] = "NULLIF({$ratings}.{$column}, '') IS NULL, {$orderby}";
415
        } else {
416
            $clause['clauses'][] = "{$ratings}.{$column} {$query->get('order')}";
417
        }
418
        return $clause;
419
    }
420
421
    protected function modifyClauseWhere(string $where, \WP_Query $query): array
422
    {
423
        $clause = glsr(SqlClauseDefaults::class)->restrict([
424
            'clauses' => [],
425
            'replace' => false,
426
        ]);
427
        $ratings = glsr(Tables::class)->table('ratings');
428
        $posts = glsr(Tables::class)->table('posts');
429
        foreach ($this->filterByValues() as $key => $value) {
430
            if (in_array($key, ['assigned_post', 'assigned_user'])) {
431
                $assignedTable = glsr(Tables::class)->table($key.'s');
432
                $column = Str::suffix(Str::removePrefix($key, 'assigned_'), '_id');
433
                $value = Cast::toInt($value);
434
                if (0 === $value) {
435
                    $clause['clauses'][] = "AND {$assignedTable}.{$column} IS NULL";
436
                } else {
437
                    $clause['clauses'][] = "AND {$assignedTable}.{$column} = {$value}";
438
                }
439
            } elseif (in_array($key, ['rating', 'terms', 'type'])) {
440
                $clause['clauses'][] = "AND {$ratings}.{$key} = '{$value}'";
441
            } elseif ('author' === $key && '0' === $value) {
442
                // Filtering by the "author" URL parameter is automatically done
443
                // by WordPress when the value is not empty
444
                $clause['clauses'][] = "AND {$posts}.post_author IN (0)";
445
            }
446
        }
447
        return $clause;
448
    }
449
}
450