ListTableController   F
last analyzed

Complexity

Total Complexity 81

Size/Duplication

Total Lines 434
Duplicated Lines 0 %

Test Coverage

Coverage 1.14%

Importance

Changes 0
Metric Value
wmc 81
eloc 237
dl 0
loc 434
ccs 3
cts 263
cp 0.0114
rs 2
c 0
b 0
f 0

21 Methods

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