Passed
Push — master ( 90df9f...04d195 )
by Paul
05:54
created

SqlSchema::tablesExist()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 4

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 4
c 1
b 0
f 0
dl 0
loc 6
ccs 5
cts 5
cp 1
rs 10
cc 4
nc 4
nop 0
crap 4
1
<?php
2
3
namespace GeminiLabs\SiteReviews\Database;
4
5
use GeminiLabs\SiteReviews\Database;
6
use GeminiLabs\SiteReviews\Helpers\Str;
7
8
class SqlSchema
9
{
10
    /**
11
     * @var array|null
12
     */
13
    protected $constraints;
14
15
    /**
16
     * @var \wpdb
17
     */
18
    protected $db;
19
20
    /**
21
     * @var array|null
22
     */
23
    protected $tables;
24
25 15
    public function __construct()
26
    {
27 15
        require_once ABSPATH.'wp-admin/includes/upgrade.php';
28 15
        global $wpdb;
29 15
        $this->db = $wpdb;
30 15
    }
31
32
    /**
33
     * @return void
34
     */
35
    public function addAssignedPostsForeignConstraints()
36
    {
37
        $this->addForeignConstraint(
38
            $table = $this->table('assigned_posts'),
39
            $constraint = $this->foreignConstraint('assigned_posts_rating_id'),
40
            $foreignKey = 'rating_id',
41
            $foreignTable = $this->table('ratings'), 
42
            $foreignColumn = 'ID'
43
        );
44
        $this->addForeignConstraint(
45
            $table = $this->table('assigned_posts'),
46
            $constraint = $this->foreignConstraint('assigned_posts_post_id'),
47
            $foreignKey = 'post_id',
48
            $foreignTable = $this->db->posts, 
49
            $foreignColumn = 'ID'
50
        );
51
    }
52
53
    /**
54
     * @return void
55
     */
56
    public function addAssignedTermsForeignConstraints()
57
    {
58
        $this->addForeignConstraint(
59
            $table = $this->table('assigned_terms'),
60
            $constraint = $this->foreignConstraint('assigned_terms_rating_id'),
61
            $foreignKey = 'rating_id',
62
            $foreignTable = $this->table('ratings'), 
63
            $foreignColumn = 'ID'
64
        );
65
        $this->addForeignConstraint(
66
            $table = $this->table('assigned_terms'),
67
            $constraint = $this->foreignConstraint('assigned_terms_term_id'),
68
            $foreignKey = 'term_id',
69
            $foreignTable = $this->db->terms, 
70
            $foreignColumn = 'term_id'
71
        );
72
    }
73
74
    /**
75
     * @return void
76
     */
77
    public function addAssignedUsersForeignConstraints()
78
    {
79
        $this->addForeignConstraint(
80
            $table = $this->table('assigned_users'),
81
            $constraint = $this->foreignConstraint('assigned_users_rating_id'),
82
            $foreignKey = 'rating_id',
83
            $foreignTable = $this->table('ratings'), 
84
            $foreignColumn = 'ID'
85
        );
86
        $this->addForeignConstraint(
87
            $table = $this->table('assigned_users'),
88
            $constraint = $this->foreignConstraint('assigned_users_user_id'),
89
            $foreignKey = 'user_id',
90
            $foreignTable = $this->db->users, 
91
            $foreignColumn = 'ID'
92
        );
93
    }
94
95
    /**
96
     * @return void
97
     */
98
    public function addReviewsForeignConstraints()
99
    {
100
        $this->addForeignConstraint(
101
            $table = $this->table('ratings'),
102
            $constraint = $this->foreignConstraint('assigned_posts_review_id'),
103
            $foreignKey = 'review_id',
104
            $foreignTable = $this->db->posts, 
105
            $foreignColumn = 'ID'
106
        );
107
    }
108
109
    /**
110
     * This method expects the fully formed foreign constraint key
111
     * @param string $table
112
     * @param string $constraint
113
     * @param string $foreignKey
114
     * @param string $foreignTable
115
     * @param string $foreignColumn
116
     * @return int|bool
117
     * @see $this->foreignConstraint()
118
     */
119
    public function addForeignConstraint($table, $constraint, $foreignKey, $foreignTable, $foreignColumn)
120
    {
121
        if ($this->foreignConstraintExists($constraint, $foreignTable)) {
122
            return false;
123
        }
124
        return glsr(Database::class)->dbQuery(glsr(Query::class)->sql("
125
            ALTER TABLE {$table}
126
            ADD CONSTRAINT {$constraint}
127
            FOREIGN KEY ({$foreignKey})
128
            REFERENCES {$foreignTable} ({$foreignColumn})
129
            ON DELETE CASCADE
130
        "));
131
    }
132
133
    /**
134
     * @return void
135
     */
136 7
    public function addForeignConstraints()
137
    {
138 7
        if (!defined('GLSR_UNIT_TESTS')) {
139
            $this->addAssignedPostsForeignConstraints();
140
            $this->addAssignedTermsForeignConstraints();
141
            $this->addAssignedUsersForeignConstraints();
142
            $this->addReviewsForeignConstraints();
143
        }
144 7
    }
145
146
    /**
147
     * @param string $table
148
     * @return bool|int
149
     */
150
    public function convertTableEngine($table)
151
    {
152
        $result = -1;
153
        $table = $this->table($table);
154
        if ($this->isMyisam($table)) {
155
            $result = glsr(Database::class)->dbQuery(glsr(Query::class)->sql("
156
                ALTER TABLE {$this->db->dbname}.{$table} ENGINE = InnoDB;
157
            "));
158
        }
159
        if (true === $result) {
160
            update_option(glsr()->prefix.'engine_'.$table, 'innodb');
161
            $this->addForeignConstraints(); // apply InnoDB constraints
162
        }
163
        return $result;
164
    }
165
166
    /**
167
     * @return bool
168
     */
169 7
    public function createAssignedPostsTable()
170
    {
171 7
        if ($this->tableExists('assigned_posts')) {
172 7
            return false;
173
        }
174
        dbDelta(glsr(Query::class)->sql("
175
            CREATE TABLE {$this->table('assigned_posts')} (
176
                rating_id bigint(20) unsigned NOT NULL,
177
                post_id bigint(20) unsigned NOT NULL,
178
                is_published tinyint(1) NOT NULL DEFAULT '1',
179
                UNIQUE KEY {$this->prefix('assigned_posts_rating_id_post_id_unique')} (rating_id,post_id)
180
            ) ENGINE=InnoDB {$this->db->get_charset_collate()};
181
        "));
182
        glsr(Database::class)->logErrors();
183
        return true;
184
    }
185
186
    /**
187
     * @return bool
188
     */
189 7
    public function createAssignedTermsTable()
190
    {
191 7
        if ($this->tableExists('assigned_terms')) {
192 7
            return false;
193
        }
194
        dbDelta(glsr(Query::class)->sql("
195
            CREATE TABLE {$this->table('assigned_terms')} (
196
                rating_id bigint(20) unsigned NOT NULL,
197
                term_id bigint(20) unsigned NOT NULL,
198
                UNIQUE KEY {$this->prefix('assigned_terms_rating_id_term_id_unique')} (rating_id,term_id)
199
            ) ENGINE=InnoDB {$this->db->get_charset_collate()};
200
        "));
201
        glsr(Database::class)->logErrors();
202
        return true;
203
    }
204
205
    /**
206
     * @return bool
207
     */
208 7
    public function createAssignedUsersTable()
209
    {
210 7
        if ($this->tableExists('assigned_users')) {
211 7
            return false;
212
        }
213
        dbDelta(glsr(Query::class)->sql("
214
            CREATE TABLE {$this->table('assigned_users')} (
215
                rating_id bigint(20) unsigned NOT NULL,
216
                user_id bigint(20) unsigned NOT NULL,
217
                UNIQUE KEY {$this->prefix('assigned_users_rating_id_user_id_unique')} (rating_id,user_id)
218
            ) ENGINE=InnoDB {$this->db->get_charset_collate()};
219
        "));
220
        glsr(Database::class)->logErrors();
221
        return true;
222
    }
223
224
    /**
225
     * WordPress codex says there must be two spaces between PRIMARY KEY and the key definition.
226
     * @return bool
227
     * @see https://codex.wordpress.org/Creating_Tables_with_Plugins
228
     */
229 7
    public function createRatingTable()
230
    {
231 7
        if ($this->tableExists('ratings')) {
232 7
            return false;
233
        }
234
        dbDelta(glsr(Query::class)->sql("
235
            CREATE TABLE {$this->table('ratings')} (
236
                ID bigint(20) unsigned NOT NULL AUTO_INCREMENT,
237
                review_id bigint(20) unsigned NOT NULL,
238
                rating int(11) NOT NULL DEFAULT '0',
239
                type varchar(20) DEFAULT 'local',
240
                is_approved tinyint(1) NOT NULL DEFAULT '0',
241
                is_pinned tinyint(1) NOT NULL DEFAULT '0',
242
                name varchar(250) DEFAULT NULL,
243
                email varchar(100) DEFAULT NULL,
244
                avatar varchar(200) DEFAULT NULL,
245
                ip_address varchar(100) DEFAULT NULL,
246
                url varchar(250) DEFAULT NULL,
247
                PRIMARY KEY  (ID),
248
                UNIQUE KEY {$this->prefix('ratings_review_id_unique')} (review_id),
249
                KEY {$this->prefix('ratings_rating_type_is_pinned_index')} (rating,type,is_pinned)
250
            ) ENGINE=InnoDB {$this->db->get_charset_collate()};
251
        "));
252
        glsr(Database::class)->logErrors();
253
        return true;
254
    }
255
256
    /**
257
     * @return void
258
     */
259 7
    public function createTables()
260
    {
261 7
        $this->createAssignedPostsTable();
262 7
        $this->createAssignedTermsTable();
263 7
        $this->createAssignedUsersTable();
264 7
        $this->createRatingTable();
265 7
        if ($this->tablesExist()) {
266 7
            add_option(glsr()->prefix.'db_version', '1.0');
267
        }
268 7
    }
269
270
    /**
271
     * @param string $table
272
     * @param string $constraint
273
     * @return int|bool
274
     */
275
    public function dropForeignConstraint($table, $constraint)
276
    {
277
        $table = $this->table($table);
278
        $constraint = $this->foreignConstraint($constraint);
279
        if (!$this->foreignConstraintExists($constraint)) {
280
            return false;
281
        }
282
        return glsr(Database::class)->dbQuery(glsr(Query::class)->sql("
283
            ALTER TABLE {$table} DROP FOREIGN KEY {$constraint};
284
        "));
285
    }
286
287
    /**
288
     * This method expects the fully formed foreign constraint key
289
     * @param string $constraint
290
     * @param string $foreignTable
291
     * @return bool
292
     * @see $this->foreignConstraint()
293
     */
294
    public function foreignConstraintExists($constraint, $foreignTable = '')
295
    {
296
        if (!empty($foreignTable) && !$this->isInnodb($foreignTable)) {
297
            glsr_log()->debug("Cannot check for a foreign constraint because {$foreignTable} does not use the InnoDB engine.");
298
            return true; // we cannot create foreign contraints on MyISAM tables
299
        }
300
        // we don't need to cache this since it only runs on install
301
        if (!is_array($this->constraints)) {
302
            $this->constraints = $this->db->get_col("
303
                SELECT CONSTRAINT_NAME
304
                FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS
305
                WHERE CONSTRAINT_SCHEMA = '{$this->db->dbname}'
306
            ");
307
        }
308
        return in_array($constraint, $this->constraints);
309
    }
310
311
    /**
312
     * @param string $table
313
     * @return bool
314
     */
315
    public function isMyisam($table)
316
    {
317
        return 'myisam' === $this->tableEngine($table);
318
    }
319
320
    /**
321
     * @param string $table
322
     * @return bool
323
     */
324
    public function isInnodb($table)
325
    {
326
        return 'innodb' === $this->tableEngine($table);
327
    }
328
329
    /**
330
     * @param string $table
331
     * @param string $prefix
332
     * @return string
333
     */
334 15
    public function prefix($table, $prefix = '')
335
    {
336 15
        $table = Str::prefix($table, glsr()->prefix);
337 15
        return Str::prefix($table, $prefix);
338
    }
339
340
    /**
341
     * @param string $constraint
342
     * @return string
343
     */
344
    public function foreignConstraint($constraint)
345
    {
346
        $constraint = Str::prefix($constraint, glsr()->prefix);
347
        $constraint = Str::suffix($constraint, '_foreign');
348
        if (is_multisite() && $this->db->blogid > 1) {
349
            return Str::suffix($constraint, '_'.$this->db->blogid);
350
        }
351
        return $constraint;
352
    }
353
354
    /**
355
     * @param string $table
356
     * @return string
357
     */
358 15
    public function table($table)
359
    {
360 15
        if (in_array($table, $this->db->tables())) {
361
            return $table; // WordPress table is already prefixed
362
        }
363
        // do this next in case another plugin has created a similar table
364 15
        if (Str::endsWith(['ratings', 'assigned_posts', 'assigned_terms', 'assigned_users'], $table)) {
365 15
            $table = Str::removePrefix($table, $this->db->get_blog_prefix());
366 15
            $table = Str::removePrefix($table, glsr()->prefix);
367 15
            return $this->prefix($table, $this->db->get_blog_prefix());
368
        }
369
        if (array_key_exists($table, $this->db->tables())) {
370
            return $this->db->{$table}; // the prefixed WordPress table
371
        }
372
        glsr_log()->error("The {$table} table does not exist.");
373
        return $table; // @todo maybe throw an exception here instead...
374
    }
375
376
    /**
377
     * @param string $table
378
     * @return string (lowercased)
379
     */
380
    public function tableEngine($table)
381
    {
382
        $table = $this->table($table);
383
        $option = glsr()->prefix.'engine_'.$table;
384
        $engine = get_option($option);
385
        if (empty($engine)) {
386
            $engine = $this->db->get_var("
387
                SELECT ENGINE
388
                FROM INFORMATION_SCHEMA.TABLES
389
                WHERE TABLE_SCHEMA = '{$this->db->dbname}' AND TABLE_NAME = '{$table}'
390
            ");
391
            if (empty($engine)) {
392
                glsr_log()->warning(sprintf('DB Table Engine: The %s table does not exist in %s.', $table, $this->db->dbname));
393
                return '';
394
            }
395
            $engine = strtolower($engine);
396
            update_option($option, $engine);
397
        }
398
        return $engine;
399
    }
400
401
    /**
402
     * @param string $table
403
     * @return bool
404
     */
405 7
    public function tableExists($table)
406
    {
407 7
        if (!is_array($this->tables)) {
408 7
            $prefix = $this->db->get_blog_prefix().glsr()->prefix;
409 7
            $this->tables = $this->db->get_col(
410 7
                $this->db->prepare("SHOW TABLES LIKE %s", $this->db->esc_like($prefix).'%')
411
            );
412
        }
413 7
        return in_array($this->table($table), $this->tables);
414
    }
415
416
    /**
417
     * @param bool $removeDbPrefix
418
     * @return array
419
     */
420
    public function tableEngines($removeDbPrefix = false)
421
    {
422
        $results = $this->db->get_results("
423
            SELECT TABLE_NAME, ENGINE
424
            FROM INFORMATION_SCHEMA.TABLES
425
            WHERE TABLE_SCHEMA = '{$this->db->dbname}'
426
            AND TABLE_NAME IN ('{$this->db->options}','{$this->db->posts}','{$this->db->terms}','{$this->db->users}')
427
        ");
428
        $engines = [];
429
        foreach ($results as $result) {
430
            if (!array_key_exists($result->ENGINE, $engines)) {
431
                $engines[$result->ENGINE] = [];
432
            }
433
            if ($removeDbPrefix) {
434
                $result->TABLE_NAME = Str::removePrefix($result->TABLE_NAME, $this->db->get_blog_prefix());
435
            }
436
            $engines[$result->ENGINE][] = $result->TABLE_NAME;
437
        }
438
        return $engines;
439
    }
440
441
    /**
442
     * @return bool
443
     */
444 7
    public function tablesExist()
445
    {
446 7
        return $this->tableExists('assigned_posts')
447 7
            && $this->tableExists('assigned_terms')
448 7
            && $this->tableExists('assigned_users')
449 7
            && $this->tableExists('ratings');
450
    }
451
}
452