Passed
Push — master ( 6268c8...c33117 )
by Paul
14:11 queued 01:11
created

Database   B

Complexity

Total Complexity 47

Size/Duplication

Total Lines 426
Duplicated Lines 0 %

Test Coverage

Coverage 53.11%

Importance

Changes 1
Bugs 0 Features 1
Metric Value
wmc 47
eloc 161
dl 0
loc 426
ccs 94
cts 177
cp 0.5311
rs 8.64
c 1
b 0
f 1

27 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 8 1
A delete() 0 5 1
A dbGetRow() 0 4 1
A dbQuery() 0 3 1
A dbGetResults() 0 4 1
A dbGetVar() 0 3 1
A deleteInvalidPostAssignments() 0 12 1
A dbGetCol() 0 3 1
A beginTransaction() 0 6 2
A users() 0 8 1
A logErrors() 0 6 2
A deleteMeta() 0 8 1
A finishTransaction() 0 6 2
A update() 0 5 1
A meta() 0 5 1
A deleteInvalidReviews() 0 11 1
A deleteInvalidTermAssignments() 0 13 1
A isMigrationNeeded() 0 9 2
A searchUsers() 0 20 3
A insertBulk() 0 16 3
A filterSearchByTitle() 0 16 6
A version() 0 10 3
A terms() 0 14 2
A deleteInvalidUserAssignments() 0 12 1
A insert() 0 9 2
A metaSet() 0 5 1
A searchPosts() 0 30 4

How to fix   Complexity   

Complex Class

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

1
<?php
2
3
namespace GeminiLabs\SiteReviews;
4
5
use GeminiLabs\SiteReviews\Database\Query;
6
use GeminiLabs\SiteReviews\Database\SqlSchema;
7
use GeminiLabs\SiteReviews\Helpers\Arr;
8
use GeminiLabs\SiteReviews\Helpers\Cast;
9
use GeminiLabs\SiteReviews\Helpers\Str;
10
use WP_Query;
11
use WP_User_Query;
12
13
/**
14
 * @property array $mappedDeprecatedMethods
15
 */
16
class Database
17
{
18
    use Deprecated;
19
20
    protected $db;
21
22 24
    public function __construct()
23
    {
24 24
        global $wpdb;
25 24
        $this->db = $wpdb;
26 24
        $this->mappedDeprecatedMethods = [
27
            'get' => 'meta',
28
            'getTerms' => 'terms',
29
            'set' => 'metaSet',
30
        ];
31 24
    }
32
33
    /**
34
     * Use this before bulk insert (see: $this->finishTransaction()).
35
     * @param string $table
36
     * @return void
37
     */
38 14
    public function beginTransaction($table)
39
    {
40 14
        $sql = glsr(SqlSchema::class)->isInnodb($table)
41 14
            ? 'START TRANSACTION;'
42 14
            : 'SET autocommit = 0;';
43 14
        $this->dbQuery($sql);
44 14
    }
45
46
    /**
47
     * @param string $sql
48
     * @return array
49
     */
50 1
    public function dbGetCol($sql)
51
    {
52 1
        return $this->logErrors($this->db->get_col($sql));
53
    }
54
55
    /**
56
     * @param string $sql
57
     * @param string $output
58
     * @return array|object|null
59
     */
60 20
    public function dbGetResults($sql, $output)
61
    {
62 20
        $output = Str::restrictTo(['ARRAY_A', 'ARRAY_N', 'OBJECT', 'OBJECT_K'], $output, OBJECT);
63 20
        return $this->logErrors($this->db->get_results($sql, $output));
64
    }
65
66
    /**
67
     * @param string $sql
68
     * @param string $output
69
     * @return array|object|void|null
70
     */
71 14
    public function dbGetRow($sql, $output)
72
    {
73 14
        $output = Str::restrictTo(['ARRAY_A', 'ARRAY_N', 'OBJECT'], $output, OBJECT);
74 14
        return $this->logErrors($this->db->get_row($sql, $output));
75
    }
76
77
    /**
78
     * @param string $sql
79
     * @return string|null
80
     */
81 1
    public function dbGetVar($sql)
82
    {
83 1
        return $this->logErrors($this->db->get_var($sql));
84
    }
85
86
    /**
87
     * @param string $sql
88
     * @return int|bool
89
     */
90 22
    public function dbQuery($sql)
91
    {
92 22
        return $this->logErrors($this->db->query($sql));
93
    }
94
95
    /**
96
     * @param string $table
97
     * @return int|false
98
     */
99 3
    public function delete($table, array $where)
100
    {
101 3
        $result = $this->db->delete(glsr(Query::class)->table($table), $where);
102 3
        glsr(Query::class)->sql($this->db->last_query); // for logging use only
103 3
        return $this->logErrors($result);
104
    }
105
106
    /**
107
     * @return int|bool
108
     */
109 14
    public function deleteInvalidPostAssignments()
110
    {
111 14
        return $this->dbQuery(sprintf(
112 14
            glsr(Query::class)->sql("
113
                DELETE t
114
                FROM %s AS t
115
                LEFT JOIN %s AS r ON t.rating_id = r.ID
116 14
                LEFT JOIN {$this->db->posts} AS f ON t.post_id = f.ID
117
                WHERE (r.ID IS NULL OR f.ID IS NULL)
118
            "),
119 14
            glsr(Query::class)->table('assigned_posts'),
120 14
            glsr(Query::class)->table('ratings')
121
        ));
122
    }
123
124
    /**
125
     * @return int|bool
126
     */
127 14
    public function deleteInvalidReviews()
128
    {
129 14
        return $this->dbQuery(sprintf(
130 14
            glsr(Query::class)->sql("
131
                DELETE r
132
                FROM %s AS r
133 14
                LEFT JOIN {$this->db->posts} AS p ON r.review_id = p.ID
134
                WHERE (p.post_type IS NULL OR p.post_type != '%s')
135
            "),
136 14
            glsr(Query::class)->table('ratings'),
137 14
            glsr()->post_type
138
        ));
139
    }
140
141
    /**
142
     * @return int|bool
143
     */
144 14
    public function deleteInvalidTermAssignments()
145
    {
146 14
        return $this->dbQuery(sprintf(
147 14
            glsr(Query::class)->sql("
148
                DELETE t
149
                FROM %s AS t
150
                LEFT JOIN %s AS r ON t.rating_id = r.ID
151 14
                LEFT JOIN {$this->db->term_taxonomy} AS f ON t.term_id = f.term_id
152
                WHERE (r.ID IS NULL OR f.term_id IS NULL) OR f.taxonomy != '%s'
153
            "),
154 14
            glsr(Query::class)->table('assigned_terms'),
155 14
            glsr(Query::class)->table('ratings'),
156 14
            glsr()->taxonomy
157
        ));
158
    }
159
160
    /**
161
     * @return int|bool
162
     */
163 14
    public function deleteInvalidUserAssignments()
164
    {
165 14
        return $this->dbQuery(sprintf(
166 14
            glsr(Query::class)->sql("
167
                DELETE t
168
                FROM %s AS t
169
                LEFT JOIN %s AS r ON t.rating_id = r.ID
170 14
                LEFT JOIN {$this->db->users} AS f ON t.user_id = f.ID
171
                WHERE (r.ID IS NULL OR f.ID IS NULL)
172
            "),
173 14
            glsr(Query::class)->table('assigned_users'),
174 14
            glsr(Query::class)->table('ratings')
175
        ));
176
    }
177
178
    /**
179
     * @param string|string[] $keys
180
     * @param string $table
181
     * @return int|bool
182
     */
183 14
    public function deleteMeta($keys, $table = 'postmeta')
184
    {
185 14
        $table = glsr(Query::class)->table($table);
186 14
        $metaKeys = glsr(Query::class)->escValuesForInsert(Arr::convertFromString($keys));
187 14
        $sql = glsr(Query::class)->sql("
188 14
            DELETE FROM {$table} WHERE meta_key IN {$metaKeys}
189
        ");
190 14
        return $this->dbQuery($sql);
191
    }
192
193
    /**
194
     * Search SQL filter for matching against post title only.
195
     * @see http://wordpress.stackexchange.com/a/11826/1685
196
     * @param string $search
197
     * @return string
198
     * @filter posts_search
199
     */
200
    public function filterSearchByTitle($search, WP_Query $query)
201
    {
202
        if (empty($search) || empty($query->get('search_terms'))) {
203
            return $search;
204
        }
205
        $n = empty($query->get('exact'))
206
            ? '%'
207
            : '';
208
        $search = [];
209
        foreach ((array) $query->get('search_terms') as $term) {
210
            $search[] = $this->db->prepare("{$this->db->posts}.post_title LIKE %s", $n.$this->db->esc_like($term).$n);
211
        }
212
        if (!is_user_logged_in()) {
213
            $search[] = "{$this->db->posts}.post_password = ''";
214
        }
215
        return ' AND '.implode(' AND ', $search);
216
    }
217
218
    /**
219
     * Use this after bulk insert (see: $this->beginTransaction()).
220
     * @param string $table
221
     * @return void
222
     */
223 14
    public function finishTransaction($table)
224
    {
225 14
        $sql = glsr(SqlSchema::class)->isInnodb($table)
226 14
            ? 'COMMIT;'
227 14
            : 'SET autocommit = 1;';
228 14
        $this->dbQuery($sql);
229 14
    }
230
231
    /**
232
     * @param string $table
233
     * @return int|bool
234
     */
235 14
    public function insert($table, array $data)
236
    {
237 14
        $this->db->insert_id = 0;
238 14
        $table = glsr(Query::class)->table($table);
239 14
        $fields = glsr(Query::class)->escFieldsForInsert(array_keys($data));
240 14
        $values = glsr(Query::class)->escValuesForInsert($data);
241 14
        $sql = glsr(Query::class)->sql("INSERT IGNORE INTO {$table} {$fields} VALUES {$values}");
242 14
        $result = $this->dbQuery($sql);
243 14
        return empty($result) ? false : $result;
244
    }
245
246
    /**
247
     * @param string $table
248
     * @return int|false
249
     */
250
    public function insertBulk($table, array $values, array $fields)
251
    {
252
        $this->db->insert_id = 0;
253
        $data = [];
254
        foreach ($values as $value) {
255
            $value = array_intersect_key($value, array_flip($fields)); // only keep field values
256
            if (count($value) === count($fields)) {
257
                $value = array_merge(array_flip($fields), $value); // make sure the order is correct
258
                $data[] = glsr(Query::class)->escValuesForInsert($value);
259
            }
260
        }
261
        $table = glsr(Query::class)->table($table);
262
        $fields = glsr(Query::class)->escFieldsForInsert($fields);
263
        $values = implode(',', $data);
264
        $sql = glsr(Query::class)->sql("INSERT IGNORE INTO {$table} {$fields} VALUES {$values}");
265
        return $this->dbQuery($sql);
266
    }
267
268
    /**
269
     * @return bool
270
     */
271 7
    public function isMigrationNeeded()
272
    {
273 7
        $table = glsr(Query::class)->table('ratings');
274 7
        $postCount = wp_count_posts(glsr()->post_type)->publish;
275 7
        if (empty($postCount)) {
276 7
            return false;
277
        }
278
        $sql = glsr(Query::class)->sql("SELECT COUNT(*) FROM {$table} WHERE is_approved = 1");
279
        return empty($this->dbGetVar($sql));
280
    }
281
282
    /**
283
     * @param mixed $result
284
     * @return mixed
285
     */
286 22
    public function logErrors($result = null)
287
    {
288 22
        if ($this->db->last_error) {
289
            glsr_log()->error($this->db->last_error);
290
        }
291 22
        return $result;
292
    }
293
294
    /**
295
     * @param int $postId
296
     * @param string $key
297
     * @param bool $single
298
     * @return mixed
299
     */
300
    public function meta($postId, $key, $single = true)
301
    {
302
        $key = Str::prefix($key, '_');
303
        $postId = Cast::toInt($postId);
304
        return get_post_meta($postId, $key, $single);
305
    }
306
307
    /**
308
     * @param int $postId
309
     * @param string $key
310
     * @param mixed $value
311
     * @return int|bool
312
     */
313
    public function metaSet($postId, $key, $value)
314
    {
315
        $key = Str::prefix($key, '_');
316
        $postId = Cast::toInt($postId);
317
        return update_metadata('post', $postId, $key, $value); // update_metadata works with revisions
318
    }
319
320
    /**
321
     * @param string $searchTerm
322
     * @return string
323
     */
324
    public function searchPosts($searchTerm)
325
    {
326
        $args = [
327
            'post_status' => 'publish',
328
            'post_type' => 'any',
329
        ];
330
        if (is_numeric($searchTerm)) {
331
            $args['post__in'] = [$searchTerm];
332
        } else {
333
            $args['orderby'] = 'relevance';
334
            $args['posts_per_page'] = 10;
335
            $args['s'] = $searchTerm;
336
        }
337
        add_filter('posts_search', [$this, 'filterSearchByTitle'], 500, 2);
338
        $search = new WP_Query($args);
339
        remove_filter('posts_search', [$this, 'filterSearchByTitle'], 500);
340
        $results = '';
341
        while ($search->have_posts()) {
342
            $search->the_post();
343
            $results .= glsr()->build('partials/editor/search-result', [
344
                'ID' => get_the_ID(),
345
                'permalink' => esc_url((string) get_permalink()),
346
                'title' => esc_attr(get_the_title()),
347
            ]);
348
        }
349
        // @phpstan-ignore-next-line
350
        if ($search->have_posts()) {
351
            wp_reset_postdata();
352
        }
353
        return $results;
354
    }
355
356
    /**
357
     * @param string $searchTerm
358
     * @return void|string
359
     */
360
    public function searchUsers($searchTerm)
361
    {
362
        $args = [
363
            'fields' => ['ID', 'user_login', 'display_name'],
364
            'number' => 10,
365
            'orderby' => 'display_name',
366
        ];
367
        if (is_numeric($searchTerm)) {
368
            $args['include'] = [$searchTerm];
369
        } else {
370
            $args['search'] = '*'.$searchTerm.'*';
371
            $args['search_columns'] = ['user_login', 'user_nicename', 'display_name'];
372
        }
373
        $users = (new WP_User_Query($args))->get_results();
374
        if (!empty($users)) {
375
            return array_reduce($users, function ($carry, $user) {
376
                return $carry.glsr()->build('partials/editor/search-result', [
377
                    'ID' => $user->ID,
378
                    'permalink' => esc_url(get_author_posts_url($user->ID)),
379
                    'title' => esc_attr($user->display_name.' ('.$user->user_login.')'),
380
                ]);
381
            });
382
        }
383
    }
384
385
    /**
386
     * @return array
387
     */
388 7
    public function terms(array $args = [])
389
    {
390 7
        $args = wp_parse_args($args, [
391 7
            'count' => false,
392 7
            'fields' => 'id=>name',
393
            'hide_empty' => false,
394 7
            'taxonomy' => glsr()->taxonomy,
395
        ]);
396 7
        $terms = get_terms($args);
397 7
        if (is_wp_error($terms)) {
398
            glsr_log()->error($terms->get_error_message());
399
            return [];
400
        }
401 7
        return $terms;
402
    }
403
404
    /**
405
     * @param string $table
406
     * @return int|bool
407
     */
408
    public function update($table, array $data, array $where)
409
    {
410
        $result = $this->db->update(glsr(Query::class)->table($table), $data, $where);
411
        glsr(Query::class)->sql($this->db->last_query); // for logging use only
412
        return $this->logErrors($result);
413
    }
414
415
    /**
416
     * @return array
417
     */
418
    public function users(array $args = [])
419
    {
420
        $args = wp_parse_args($args, [
421
            'fields' => ['ID', 'display_name'],
422
            'orderby' => 'display_name',
423
        ]);
424
        $users = get_users($args);
425
        return wp_list_pluck($users, 'display_name', 'ID');
426
    }
427
428
    /**
429
     * @param string $compareToVersion
430
     * @return bool|string
431
     */
432 24
    public function version($compareToVersion = null)
433
    {
434 24
        $dbVersion = Cast::toString(get_option(glsr()->prefix.'db_version'));
435 24
        if (version_compare($dbVersion, '2', '>')) { // @compat version should always be less than 2 for now
436
            update_option(glsr()->prefix.'db_version', '1.0');
437
            $dbVersion = '1.0';
438
        }
439 24
        return isset($compareToVersion)
440 24
            ? version_compare($dbVersion, Cast::toString($compareToVersion), '>=')
441 24
            : $dbVersion;
442
    }
443
}
444