Test Failed
Push — master ( ef6440...235b13 )
by Paul
13:12 queued 06:00
created

Database::logErrors()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2.2559

Importance

Changes 1
Bugs 0 Features 1
Metric Value
eloc 4
c 1
b 0
f 1
dl 0
loc 7
ccs 3
cts 5
cp 0.6
rs 10
cc 2
nc 2
nop 1
crap 2.2559
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
    public function dbGetCol($sql)
51
    {
52
        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 8
    public function dbGetRow($sql, $output)
72
    {
73 8
        $output = Str::restrictTo(['ARRAY_A', 'ARRAY_N', 'OBJECT'], $output, OBJECT);
74 8
        return $this->logErrors($this->db->get_row($sql, $output));
75
    }
76
77
    /**
78
     * @param string $sql
79
     * @return string|null
80
     */
81
    public function dbGetVar($sql)
82
    {
83
        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 8
    public function insert($table, array $data)
236
    {
237 8
        $this->db->insert_id = 0;
238 8
        $table = glsr(Query::class)->table($table);
239 8
        $fields = glsr(Query::class)->escFieldsForInsert(array_keys($data));
240 8
        $values = glsr(Query::class)->escValuesForInsert($data);
241 8
        $sql = glsr(Query::class)->sql("INSERT IGNORE INTO {$table} {$fields} VALUES {$values}");
242 8
        $result = $this->dbQuery($sql);
243 8
        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
            glsr_trace();
291
        }
292 22
        return $result;
293
    }
294
295
    /**
296
     * @param int $postId
297
     * @param string $key
298
     * @param bool $single
299
     * @return mixed
300
     */
301
    public function meta($postId, $key, $single = true)
302
    {
303
        $key = Str::prefix($key, '_');
304
        $postId = Cast::toInt($postId);
305
        return get_post_meta($postId, $key, $single);
306
    }
307
308
    /**
309
     * @param int $postId
310
     * @param string $key
311
     * @param mixed $value
312
     * @return int|bool
313
     */
314
    public function metaSet($postId, $key, $value)
315
    {
316
        $key = Str::prefix($key, '_');
317
        $postId = Cast::toInt($postId);
318
        return update_metadata('post', $postId, $key, $value); // update_metadata works with revisions
319
    }
320
321
    /**
322
     * @param string $searchTerm
323
     * @return string
324
     */
325
    public function searchPosts($searchTerm)
326
    {
327
        $args = [
328
            'post_status' => 'publish',
329
            'post_type' => 'any',
330
        ];
331
        if (is_numeric($searchTerm)) {
332
            $args['post__in'] = [$searchTerm];
333
        } else {
334
            $args['orderby'] = 'relevance';
335
            $args['posts_per_page'] = 10;
336
            $args['s'] = $searchTerm;
337
        }
338
        add_filter('posts_search', [$this, 'filterSearchByTitle'], 500, 2);
339
        $search = new WP_Query($args);
340
        remove_filter('posts_search', [$this, 'filterSearchByTitle'], 500);
341
        $results = '';
342
        while ($search->have_posts()) {
343
            $search->the_post();
344
            $results .= glsr()->build('partials/editor/search-result', [
345
                'ID' => get_the_ID(),
346
                'permalink' => esc_url((string) get_permalink()),
347
                'title' => esc_attr(get_the_title()),
348
            ]);
349
        }
350
        // @phpstan-ignore-next-line
351
        if ($search->have_posts()) {
352
            wp_reset_postdata();
353
        }
354
        return $results;
355
    }
356
357
    /**
358
     * @param string $searchTerm
359
     * @return void|string
360
     */
361
    public function searchUsers($searchTerm)
362
    {
363
        $args = [
364
            'fields' => ['ID', 'user_login', 'display_name'],
365
            'number' => 10,
366
            'orderby' => 'display_name',
367
        ];
368
        if (is_numeric($searchTerm)) {
369
            $args['include'] = [$searchTerm];
370
        } else {
371
            $args['search'] = '*'.$searchTerm.'*';
372
            $args['search_columns'] = ['user_login', 'user_nicename', 'display_name'];
373
        }
374
        $users = (new WP_User_Query($args))->get_results();
375
        if (!empty($users)) {
376
            return array_reduce($users, function ($carry, $user) {
377
                return $carry.glsr()->build('partials/editor/search-result', [
378
                    'ID' => $user->ID,
379
                    'permalink' => esc_url(get_author_posts_url($user->ID)),
380
                    'title' => esc_attr($user->display_name.' ('.$user->user_login.')'),
381
                ]);
382
            });
383
        }
384
    }
385
386
    /**
387
     * @return array
388
     */
389 7
    public function terms(array $args = [])
390
    {
391 7
        $args = wp_parse_args($args, [
392 7
            'count' => false,
393 7
            'fields' => 'id=>name',
394
            'hide_empty' => false,
395 7
            'taxonomy' => glsr()->taxonomy,
396
        ]);
397 7
        $terms = get_terms($args);
398 7
        if (is_wp_error($terms)) {
399
            glsr_log()->error($terms->get_error_message());
400
            return [];
401
        }
402 7
        return $terms;
403
    }
404
405
    /**
406
     * @param string $table
407
     * @return int|bool
408
     */
409
    public function update($table, array $data, array $where)
410
    {
411
        $result = $this->db->update(glsr(Query::class)->table($table), $data, $where);
412
        glsr(Query::class)->sql($this->db->last_query); // for logging use only
413
        return $this->logErrors($result);
414
    }
415
416
    /**
417
     * @return array
418
     */
419
    public function users(array $args = [])
420
    {
421
        $args = wp_parse_args($args, [
422
            'fields' => ['ID', 'display_name'],
423
            'orderby' => 'display_name',
424
        ]);
425
        $users = get_users($args);
426
        return wp_list_pluck($users, 'display_name', 'ID');
427
    }
428
429
    /**
430
     * @param string $compareToVersion
431
     * @return bool|string
432
     */
433 24
    public function version($compareToVersion = null)
434
    {
435 24
        $dbVersion = Cast::toString(get_option(glsr()->prefix.'db_version'));
436 24
        if (version_compare($dbVersion, '2', '>')) { // @compat version should always be less than 2 for now
437
            update_option(glsr()->prefix.'db_version', '1.0');
438
            $dbVersion = '1.0';
439
        }
440 24
        return isset($compareToVersion)
441 24
            ? version_compare($dbVersion, Cast::toString($compareToVersion), '>=')
442 24
            : $dbVersion;
443
    }
444
}
445