Passed
Push — master ( 68f720...5e727b )
by Paul
10:53
created

SqlSchema   A

Complexity

Total Complexity 40

Size/Duplication

Total Lines 383
Duplicated Lines 0 %

Test Coverage

Coverage 26.29%

Importance

Changes 8
Bugs 3 Features 2
Metric Value
wmc 40
eloc 152
c 8
b 3
f 2
dl 0
loc 383
ccs 46
cts 175
cp 0.2629
rs 9.2

20 Methods

Rating   Name   Duplication   Size   Complexity  
A createAssignedTermsTable() 0 14 2
A isInnodb() 0 12 2
A addReviewsForeignConstraints() 0 8 1
A addAssignedPostsForeignConstraints() 0 15 1
A createAssignedPostsTable() 0 15 2
A createTables() 0 7 1
A addForeignConstraints() 0 7 2
A tableExists() 0 9 2
A dropForeignConstraint() 0 9 2
A foreignConstraintExists() 0 14 4
A createRatingTable() 0 25 2
A table() 0 16 4
A prefix() 0 4 1
A foreignConstraint() 0 8 3
A tableEngines() 0 20 4
A addForeignConstraint() 0 10 2
A createAssignedUsersTable() 0 14 2
A addAssignedUsersForeignConstraints() 0 15 1
A addAssignedTermsForeignConstraints() 0 15 1
A __construct() 0 5 1

How to fix   Complexity   

Complex Class

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

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
0 ignored issues
show
Bug introduced by
The type wpdb was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
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
     * @return bool
148
     */
149 7
    public function createAssignedPostsTable()
150
    {
151 7
        if ($this->tableExists('assigned_posts')) {
152 7
            glsr_log()->debug(sprintf('Cannot create the %s table because it already exists.', $this->table('assigned_posts')));
153 7
            return false;
154
        }
155
        dbDelta(glsr(Query::class)->sql("
0 ignored issues
show
Bug introduced by
The function dbDelta was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

155
        /** @scrutinizer ignore-call */ 
156
        dbDelta(glsr(Query::class)->sql("
Loading history...
156
            CREATE TABLE {$this->table('assigned_posts')} (
157
                rating_id bigint(20) unsigned NOT NULL,
158
                post_id bigint(20) unsigned NOT NULL,
159
                is_published tinyint(1) NOT NULL DEFAULT '1',
160
                UNIQUE KEY {$this->prefix('assigned_posts_rating_id_post_id_unique')} (rating_id,post_id)
161
            ) ENGINE=InnoDB {$this->db->get_charset_collate()};
162
        "));
163
        return true;
164
    }
165
166
    /**
167
     * @return bool
168
     */
169 7
    public function createAssignedTermsTable()
170
    {
171 7
        if ($this->tableExists('assigned_terms')) {
172 7
            glsr_log()->debug(sprintf('Cannot create the %s table because it already exists.', $this->table('assigned_terms')));
173 7
            return false;
174
        }
175
        dbDelta(glsr(Query::class)->sql("
0 ignored issues
show
Bug introduced by
The function dbDelta was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

175
        /** @scrutinizer ignore-call */ 
176
        dbDelta(glsr(Query::class)->sql("
Loading history...
176
            CREATE TABLE {$this->table('assigned_terms')} (
177
                rating_id bigint(20) unsigned NOT NULL,
178
                term_id bigint(20) unsigned NOT NULL,
179
                UNIQUE KEY {$this->prefix('assigned_terms_rating_id_term_id_unique')} (rating_id,term_id)
180
            ) ENGINE=InnoDB {$this->db->get_charset_collate()};
181
        "));
182
        return true;
183
    }
184
185
    /**
186
     * @return bool
187
     */
188 7
    public function createAssignedUsersTable()
189
    {
190 7
        if ($this->tableExists('assigned_users')) {
191 7
            glsr_log()->debug(sprintf('Cannot create the %s table because it already exists.', $this->table('assigned_users')));
192 7
            return false;
193
        }
194
        dbDelta(glsr(Query::class)->sql("
0 ignored issues
show
Bug introduced by
The function dbDelta was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

194
        /** @scrutinizer ignore-call */ 
195
        dbDelta(glsr(Query::class)->sql("
Loading history...
195
            CREATE TABLE {$this->table('assigned_users')} (
196
                rating_id bigint(20) unsigned NOT NULL,
197
                user_id bigint(20) unsigned NOT NULL,
198
                UNIQUE KEY {$this->prefix('assigned_users_rating_id_user_id_unique')} (rating_id,user_id)
199
            ) ENGINE=InnoDB {$this->db->get_charset_collate()};
200
        "));
201
        return true;
202
    }
203
204
    /**
205
     * WordPress codex says there must be two spaces between PRIMARY KEY and the key definition.
206
     * @return bool
207
     * @see https://codex.wordpress.org/Creating_Tables_with_Plugins
208
     */
209 7
    public function createRatingTable()
210
    {
211 7
        if ($this->tableExists('ratings')) {
212 7
            glsr_log()->debug(sprintf('Cannot create the %s table because it already exists.', $this->table('ratings')));
213 7
            return false;
214
        }
215
        dbDelta(glsr(Query::class)->sql("
0 ignored issues
show
Bug introduced by
The function dbDelta was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

215
        /** @scrutinizer ignore-call */ 
216
        dbDelta(glsr(Query::class)->sql("
Loading history...
216
            CREATE TABLE {$this->table('ratings')} (
217
                ID bigint(20) unsigned NOT NULL AUTO_INCREMENT,
218
                review_id bigint(20) unsigned NOT NULL,
219
                rating int(11) NOT NULL DEFAULT '0',
220
                type varchar(20) DEFAULT 'local',
221
                is_approved tinyint(1) NOT NULL DEFAULT '0',
222
                is_pinned tinyint(1) NOT NULL DEFAULT '0',
223
                name varchar(250) DEFAULT NULL,
224
                email varchar(100) DEFAULT NULL,
225
                avatar varchar(200) DEFAULT NULL,
226
                ip_address varchar(100) DEFAULT NULL,
227
                url varchar(250) DEFAULT NULL,
228
                PRIMARY KEY  (ID),
229
                UNIQUE KEY {$this->prefix('ratings_review_id_unique')} (review_id),
230
                KEY {$this->prefix('ratings_rating_type_is_pinned_index')} (rating,type,is_pinned)
231
            ) ENGINE=InnoDB {$this->db->get_charset_collate()};
232
        "));
233
        return true;
234
    }
235
236
    /**
237
     * @return void
238
     */
239 7
    public function createTables()
240
    {
241 7
        $this->createAssignedPostsTable();
242 7
        $this->createAssignedTermsTable();
243 7
        $this->createAssignedUsersTable();
244 7
        $this->createRatingTable();
245 7
        add_option(glsr()->prefix.'db_version', glsr()->version('minor'));
0 ignored issues
show
Bug introduced by
The function add_option was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

245
        /** @scrutinizer ignore-call */ 
246
        add_option(glsr()->prefix.'db_version', glsr()->version('minor'));
Loading history...
246 7
    }
247
248
    /**
249
     * @param string $table
250
     * @param string $constraint
251
     * @return int|bool
252
     */
253
    public function dropForeignConstraint($table, $constraint)
254
    {
255
        $table = $this->table($table);
256
        $constraint = $this->foreignConstraint($constraint);
257
        if (!$this->foreignConstraintExists($constraint)) {
258
            return false;
259
        }
260
        return glsr(Database::class)->dbQuery(glsr(Query::class)->sql("
261
            ALTER TABLE {$table} DROP FOREIGN KEY {$constraint};
262
        "));
263
    }
264
265
    /**
266
     * This method expects the fully formed foreign constraint key
267
     * @param string $constraint
268
     * @param string $foreignTable
269
     * @return bool
270
     * @see $this->foreignConstraint()
271
     */
272
    public function foreignConstraintExists($constraint, $foreignTable = '')
273
    {
274
        if (!empty($foreignTable) && !$this->isInnodb($foreignTable)) {
275
            glsr_log()->debug("Cannot check for a foreign constraint because {$foreignTable} does not use the InnoDB engine.");
276
            return true; // we cannot create foreign contraints on MyISAM tables
277
        }
278
        if (!is_array($this->constraints)) {
279
            $this->constraints = $this->db->get_col("
280
                SELECT CONSTRAINT_NAME
281
                FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS
282
                WHERE CONSTRAINT_SCHEMA = '{$this->db->dbname}'
283
            ");
284
        }
285
        return in_array($constraint, $this->constraints);
286
    }
287
288
    /**
289
     * @param string $table
290
     * @return bool
291
     */
292
    public function isInnodb($table)
293
    {
294
        $engine = $this->db->get_var("
295
            SELECT ENGINE
296
            FROM INFORMATION_SCHEMA.TABLES
297
            WHERE TABLE_SCHEMA = '{$this->db->dbname}' AND TABLE_NAME = '{$this->table($table)}'
298
        ");
299
        if (empty($engine)) {
300
            glsr_log()->warning(sprintf('InnoDB: The %s database table does not exist.', $this->table($table)));
301
            return false;
302
        }
303
        return 'innodb' === strtolower($engine);
304
    }
305
306
    /**
307
     * @param string $table
308
     * @param string $prefix
309
     * @return string
310
     */
311 15
    public function prefix($table, $prefix = '')
312
    {
313 15
        $table = Str::prefix($table, glsr()->prefix);
314 15
        return Str::prefix($table, $prefix);
315
    }
316
317
    /**
318
     * @param string $constraint
319
     * @return string
320
     */
321
    public function foreignConstraint($constraint)
322
    {
323
        $constraint = Str::prefix($constraint, glsr()->prefix);
324
        $constraint = Str::suffix($constraint, '_foreign');
325
        if (is_multisite() && $this->db->blogid > 1) {
0 ignored issues
show
Bug introduced by
The function is_multisite was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

325
        if (/** @scrutinizer ignore-call */ is_multisite() && $this->db->blogid > 1) {
Loading history...
326
            return Str::suffix($constraint, '_'.$this->db->blogid);
327
        }
328
        return $constraint;
329
    }
330
331
    /**
332
     * @param string $table
333
     * @return string
334
     */
335 15
    public function table($table)
336
    {
337 15
        if (in_array($table, $this->db->tables())) {
338
            return $table; // WordPress table is already prefixed
339
        }
340
        // do this next in case another plugin has created a similar table
341 15
        if (Str::endsWith(['ratings', 'assigned_posts', 'assigned_terms', 'assigned_users'], $table)) {
342 15
            $table = Str::removePrefix($table, $this->db->get_blog_prefix());
343 15
            $table = Str::removePrefix($table, glsr()->prefix);
344 15
            return $this->prefix($table, $this->db->get_blog_prefix());
345
        }
346
        if (array_key_exists($table, $this->db->tables())) {
347
            return $this->db->{$table}; // the prefixed WordPress table
348
        }
349
        glsr_log()->error("The {$table} table does not exist.");
350
        return $table; // @todo maybe throw an exception here instead...
351
    }
352
353
    /**
354
     * @param string $table
355
     * @return bool
356
     */
357 7
    public function tableExists($table)
358
    {
359 7
        if (!is_array($this->tables)) {
360 7
            $prefix = $this->db->get_blog_prefix().glsr()->prefix;
361 7
            $this->tables = $this->db->get_col(
362 7
                $this->db->prepare("SHOW TABLES LIKE %s", $this->db->esc_like($prefix).'%')
363
            );
364
        }
365 7
        return in_array($this->table($table), $this->tables);
366
    }
367
368
    /**
369
     * @return string[]
370
     */
371
    public function tableEngines()
372
    {
373
        $results = $this->db->get_results("
374
            SELECT TABLE_NAME, ENGINE
375
            FROM INFORMATION_SCHEMA.TABLES
376
            WHERE TABLE_SCHEMA = '{$this->db->dbname}'
377
            AND TABLE_NAME IN ('{$this->db->options}','{$this->db->posts}','{$this->db->terms}','{$this->db->users}')
378
        ");
379
        $engines = [];
380
        foreach ($results as $result) {
381
            if (!array_key_exists($result->ENGINE, $engines)) {
382
                $engines[$result->ENGINE] = [];
383
            }
384
            $engines[$result->ENGINE][] = Str::removePrefix($result->TABLE_NAME, $this->db->get_blog_prefix());
385
        }
386
        $tableEngines = [];
387
        foreach ($engines as $engine => $tables) {
388
          $tableEngines[] = sprintf('%s (%s)', $engine, implode('|', $tables));
389
        }
390
        return $tableEngines;
391
    }
392
}
393