Tables   F
last analyzed

Complexity

Total Complexity 129

Size/Duplication

Total Lines 943
Duplicated Lines 0 %

Test Coverage

Coverage 58.6%

Importance

Changes 0
Metric Value
wmc 129
eloc 361
dl 0
loc 943
ccs 211
cts 360
cp 0.586
rs 2
c 0
b 0
f 0

36 Methods

Rating   Name   Duplication   Size   Complexity  
A name() 0 3 1
A addPrimaryKey() 0 20 5
A __construct() 0 7 1
A addIndex() 0 22 6
A addColumn() 0 28 6
B dropIndexes() 0 26 8
A dropTable() 0 9 2
A dropColumn() 0 11 2
A quoteIndexColumnName() 0 13 2
A insert() 0 20 6
B renderTableCreate() 0 26 7
A truncate() 0 10 2
A dropPrimaryKey() 0 10 2
A getColumnAttributes() 0 14 4
A getLastErrNo() 0 3 1
A delete() 0 16 4
A resetQueue() 0 4 1
A tableNotEstablished() 0 5 1
A getLastError() 0 3 1
A renameTable() 0 14 2
B alterColumn() 0 37 9
A executeQueue() 0 19 5
A dropIndex() 0 10 2
A copyTable() 0 23 4
A setTableOptions() 0 15 4
A dumpQueue() 0 5 1
B update() 0 24 8
A addTable() 0 26 4
A useTable() 0 11 3
A getTableIndexes() 0 8 3
A fetch() 0 3 1
A addToQueue() 0 3 1
A execSql() 0 13 3
A dumpTables() 0 3 1
A expandQueue() 0 6 4
C getTable() 0 82 12

How to fix   Complexity   

Complex Class

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

1
<?php
2
/*
3
 You may not change or alter any portion of this comment or credits
4
 of supporting developers from this source code or any supporting source code
5
 which is considered copyrighted (c) material of the original comment or credit authors.
6
7
 This program is distributed in the hope that it will be useful,
8
 but WITHOUT ANY WARRANTY; without even the implied warranty of
9
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
10
 */
11
12
namespace Xmf\Database;
13
14
use Doctrine\DBAL\FetchMode;
15
use Doctrine\DBAL\Driver\Statement;
16
use Xmf\Language;
17
use Xoops\Core\Database\Connection;
18
use Xoops\Core\Database\Factory;
19
20
/**
21
 * Xmf\Database\Tables
22
 *
23
 * inspired by Yii CDbMigration
24
 *
25
 * Build a work queue of database changes needed to implement new and
26
 * changed tables. Define table(s) you are dealing with and any desired
27
 * change(s). If the changes are already in place (i.e. the new column
28
 * already exists) no work is added. Then executeQueue() to process the
29
 * whole set.
30
 *
31
 * @category  Xmf\Database\Tables
32
 * @package   Xmf
33
 * @author    Richard Griffith <[email protected]>
34
 * @copyright 2011-2019 XOOPS Project (https://xoops.org)
35
 * @license   GNU GPL 2 or later (https://www.gnu.org/licenses/gpl-2.0.html)
36
 */
37
class Tables
38
{
39
    /**
40
     * @var Connection
41
     */
42
    protected $db;
43
44
    /**
45
     * @var string
46
     */
47
    protected $databaseName;
48
49
    /**
50
     * @var array Tables
51
     */
52
    protected $tables;
53
54
    /**
55
     * @var array Work queue
56
     */
57
    protected $queue;
58
59
    /**
60
     * @var string last error message
61
     */
62
    protected $lastError;
63
64
    /**
65
     * @var int last error number
66
     */
67
    protected $lastErrNo;
68
69
    /**
70
     * Constructor
71
     *
72
     */
73 19
    public function __construct()
74
    {
75 19
        Language::load('xmf');
76
77 19
        $this->db = Factory::getConnection();
78 19
        $this->databaseName = \XoopsBaseConfig::get('db-name');
79 19
        $this->resetQueue();
80 19
    }
81
82
    /**
83
     * Return a table name, prefixed with site table prefix
84
     *
85
     * @param string $table table name to contain prefix
86
     *
87
     * @return string table name with prefix
88
     */
89 15
    protected function name($table)
90
    {
91 15
        return $this->db->prefix($table);
92
    }
93
94
    /**
95
     * Add new column for table to the work queue
96
     *
97
     * @param string $table      table to contain the column
98
     * @param string $column     name of column to add
99
     * @param string $attributes column_definition
100
     *
101
     * @return bool true if no errors, false if errors encountered
102
     */
103 1
    public function addColumn($table, $column, $attributes)
104
    {
105
        $columnDef = array(
106 1
            'name' => $column,
107 1
            'attributes' => $attributes
108
        );
109
110
        // Find table def.
111 1
        if (isset($this->tables[$table])) {
112 1
            $tableDef = &$this->tables[$table];
113
            // Is this on a table we are adding?
114 1
            if (isset($tableDef['create']) && $tableDef['create']) {
115
                array_push($tableDef['columns'], $columnDef);
116
            } else {
117 1
                foreach ($tableDef['columns'] as $col) {
118 1
                    if (strcasecmp($col['name'], $column) == 0) {
119
                        return true;
120
                    }
121
                }
122 1
                $this->queue[] = "ALTER TABLE `{$tableDef['name']}`"
123 1
                    . " ADD COLUMN `{$column}` {$columnDef['attributes']}";
124 1
                array_push($tableDef['columns'], $columnDef);
125
            }
126
        } else {
127
            return $this->tableNotEstablished();
128
        }
129
130 1
        return true; // exists or is added to queue
131
    }
132
133
    /**
134
     * Add new primary key definition for table to work queue
135
     *
136
     * @param string $table  table
137
     * @param string $column column or comma separated list of columns
138
     *                       to use as primary key
139
     *
140
     * @return bool true if no errors, false if errors encountered
141
     */
142 1
    public function addPrimaryKey($table, $column)
143
    {
144 1
        $columns = str_getcsv(str_replace(' ', '', $column));
145 1
        $columnList = '';
146 1
        $firstComma = '';
147 1
        foreach ($columns as $col) {
148 1
            $columnList .= "{$firstComma}`{$col}`";
149 1
            $firstComma = ', ';
150
        }
151 1
        if (isset($this->tables[$table])) {
152 1
            if (isset($this->tables[$table]['create']) && $this->tables[$table]['create']) {
153
                $this->tables[$table]['keys']['PRIMARY']['columns'] = $columnList;
154
            } else {
155 1
                $this->queue[] = "ALTER TABLE `{$this->tables[$table]['name']}` ADD PRIMARY KEY({$columnList})";
156
            }
157
        } else {
158
            return $this->tableNotEstablished();
159
        }
160
161 1
        return true;
162
    }
163
164
    /**
165
     * Add new index definition for index to work queue
166
     *
167
     * @param string $name   name of index to add
168
     * @param string $table  table indexed
169
     * @param string $column column or a comma separated list of columns
170
     *                        to use as the key
171
     * @param bool   $unique true if index is to be unique
172
     *
173
     * @return bool true if no errors, false if errors encountered
174
     */
175 1
    public function addIndex($name, $table, $column, $unique = false)
176
    {
177 1
        $columns = str_getcsv($column);
178 1
        $columnList = '';
179 1
        $firstComma = '';
180 1
        foreach ($columns as $col) {
181 1
            $columnList .= $firstComma . $this->quoteIndexColumnName($col);
182 1
            $firstComma = ', ';
183
        }
184 1
        if (isset($this->tables[$table])) {
185 1
            if (isset($this->tables[$table]['create']) && $this->tables[$table]['create']) {
186
                $this->tables[$table]['keys'][$name]['columns'] = $columnList;
187
                $this->tables[$table]['keys'][$name]['unique'] = (bool) $unique;
188
            } else {
189 1
                $add = ($unique ? 'ADD UNIQUE INDEX' : 'ADD INDEX');
190 1
                $this->queue[] = "ALTER TABLE `{$this->tables[$table]['name']}` {$add} `{$name}` ({$columnList})";
191
            }
192
        } else {
193
            return $this->tableNotEstablished();
194
        }
195
196 1
        return true;
197
    }
198
199
    /**
200
     * Backtick quote the column names used in index creation.
201
     *
202
     * Handles prefix indexed columns specified as name(length) - i.e. name(20).
203
     *
204
     * @param string $columnName column name to quote with optional prefix length
205
     *
206
     * @return string
207
     */
208 1
    protected function quoteIndexColumnName($columnName)
209
    {
210 1
        $column = str_replace(' ', '', $columnName);
211 1
        $length = '';
212
213 1
        $lengthPosition = strpos($column, '(');
214 1
        if ($lengthPosition) {
215
            $length = ' ' . substr($column, $lengthPosition);
216
            $column = substr($column, 0, $lengthPosition);
217
        }
218 1
        $quotedName = "`{$column}`{$length}";
219
220 1
        return $quotedName;
221
    }
222
223
    /**
224
     * Load table schema from database, or starts new empty schema if
225
     * table does not exist
226
     *
227
     * @param string $table table
228
     *
229
     * @return bool true if no errors, false if errors encountered
230
     */
231
    public function addTable($table)
232
    {
233
        if (isset($this->tables[$table])) {
234
            return true;
235
        }
236
        $tableDef = $this->getTable($table);
237
        if (is_array($tableDef)) {
238
            $this->tables[$table] = $tableDef;
239
240
            return true;
241
        } else {
242
            if ($tableDef === true) {
243
                $tableDef = array(
244
                    'name' => $this->name($table),
245
                    'options' => 'ENGINE=InnoDB',
246
                    'columns' => array(),
247
                    'keys' => array(),
248
                    'create' => true,
249
                );
250
                $this->tables[$table] = $tableDef;
251
252
                $this->queue[] = array('createtable' => $table);
253
254
                return true;
255
            } else {
256
                return false;
257
            }
258
        }
259
    }
260
261
    /**
262
     * AddTable only if it exists
263
     *
264
     * @param string $table table
265
     *
266
     * @return bool true if table exists, false otherwise
267
     */
268 15
    public function useTable($table)
269
    {
270 15
        if (isset($this->tables[$table])) {
271
            return true;
272
        }
273 15
        $tableDef = $this->getTable($table);
274 15
        if (is_array($tableDef)) {
275 15
            $this->tables[$table] = $tableDef;
276 15
            return true;
277
        }
278 2
        return false;
279
    }
280
281
    /**
282
     * Get column attributes
283
     *
284
     * @param string $table  table containing the column
285
     * @param string $column column to alter
286
     *
287
     * @return string|bool attribute string, or false if error encountered
288
     */
289 1
    public function getColumnAttributes($table, $column)
290
    {
291
        // Find table def.
292 1
        if (isset($this->tables[$table])) {
293 1
            $tableDef = $this->tables[$table];
294
            // loop thru and find the column
295 1
            foreach ($tableDef['columns'] as $col) {
296 1
                if (strcasecmp($col['name'], $column) === 0) {
297 1
                    return $col['attributes'];
298
                }
299
            }
300
        }
301
302
        return false;
303
    }
304
305
    /**
306
     * Get indexes for a table
307
     *
308
     * @param string $table get indexes for this named table
309
     *
310
     * @return array|bool array of indexes, or false if error encountered
311
     */
312 1
    public function getTableIndexes($table)
313
    {
314
        // Find table def.
315 1
        if (isset($this->tables[$table]) && isset($this->tables[$table]['keys'])) {
316 1
            return $this->tables[$table]['keys'];
317
        }
318
319 1
        return false;
320
    }
321
322
    /**
323
     * Add alter column operation to the work queue
324
     *
325
     * @param string $table      table containing the column
326
     * @param string $column     column to alter
327
     * @param string $attributes new column_definition
328
     * @param string $newName    new name for column, blank to keep same
329
     *
330
     * @return bool true if no errors, false if errors encountered
331
     */
332 1
    public function alterColumn($table, $column, $attributes, $newName = '')
333
    {
334 1
        if (empty($newName)) {
335
            $newName = $column;
336
        }
337
        // Find table def.
338 1
        if (isset($this->tables[$table])) {
339 1
            $tableDef = &$this->tables[$table];
340
            // Is this on a table we are adding?
341 1
            if (isset($tableDef['create']) && $tableDef['create']) {
342
                // loop thru and find the column
343
                foreach ($tableDef['columns'] as &$col) {
344
                    if (strcasecmp($col['name'], $column) == 0) {
345
                        $col['name'] = $newName;
346
                        $col['attributes'] = $attributes;
347
                        break;
348
                    }
349
                }
350
351
                return true;
352
            } else {
353 1
                $this->queue[] = "ALTER TABLE `{$tableDef['name']}` " .
354 1
                    "CHANGE COLUMN `{$column}` `{$newName}` {$attributes} ";
355
                // loop thru and find the column
356 1
                foreach ($tableDef['columns'] as &$col) {
357 1
                    if (strcasecmp($col['name'], $column) == 0) {
358 1
                        $col['name'] = $newName;
359 1
                        $col['attributes'] = $attributes;
360 1
                        break;
361
                    }
362
                }
363
            }
364
        } else {
365
            return $this->tableNotEstablished();
366
        }
367
368 1
        return true;
369
    }
370
371
    /**
372
     * Loads table schema from database, and adds newTable with that
373
     * schema to the queue
374
     *
375
     * @param string $table    existing table
376
     * @param string $newTable new table
377
     * @param bool   $withData true to copy data, false for schema only
378
     *
379
     * @return bool true if no errors, false if errors encountered
380
     */
381 1
    public function copyTable($table, $newTable, $withData = false)
382
    {
383 1
        if (isset($this->tables[$newTable])) {
384
            return true;
385
        }
386 1
        $tableDef = $this->getTable($table);
387 1
        $copy = $this->name($newTable);
388 1
        $original = $this->name($table);
389
390 1
        if (is_array($tableDef)) {
391 1
            $tableDef['name'] = $copy;
392 1
            if ($withData) {
393
                $this->queue[] = "CREATE TABLE `{$copy}` LIKE `{$original}` ;";
394
                $this->queue[] = "INSERT INTO `{$copy}` SELECT * FROM `{$original}` ;";
395
            } else {
396 1
                $tableDef['create'] = true;
397 1
                $this->queue[] = array('createtable' => $newTable);
398
            }
399 1
            $this->tables[$newTable] = $tableDef;
400
401 1
            return true;
402
        } else {
403
            return false;
404
        }
405
    }
406
407
    /**
408
     * Add drop column operation to the work queue
409
     *
410
     * @param string $table  table containing the column
411
     * @param string $column column to drop
412
     *
413
     * @return bool true if no errors, false if errors encountered
414
     */
415 1
    public function dropColumn($table, $column)
416
    {
417
        // Find table def.
418 1
        if (isset($this->tables[$table])) {
419 1
            $tableDef = $this->tables[$table];
420 1
            $this->queue[] = "ALTER TABLE `{$tableDef['name']}` DROP COLUMN `{$column}`";
421
        } else {
422
            return $this->tableNotEstablished();
423
        }
424
425 1
        return true;
426
    }
427
428
    /**
429
     * Add drop index operation to the work queue
430
     *
431
     * @param string $name  name of index to drop
432
     * @param string $table table indexed
433
     *
434
     * @return bool true if no errors, false if errors encountered
435
     */
436 1
    public function dropIndex($name, $table)
437
    {
438 1
        if (isset($this->tables[$table])) {
439 1
            $tableDef = $this->tables[$table];
440 1
            $this->queue[] = "ALTER TABLE `{$tableDef['name']}` DROP INDEX `{$name}`";
441
        } else {
442
            return $this->tableNotEstablished();
443
        }
444
445 1
        return true;
446
    }
447
448
    /**
449
     * Add drop for all (non-PRIMARY) keys for a table to the work
450
     * queue. This can be used to clean up indexes with automatic names.
451
     *
452
     * @param string $table table indexed
453
     *
454
     * @return bool true if no errors, false if errors encountered
455
     */
456
    public function dropIndexes($table)
457
    {
458
        // Find table def.
459
        if (isset($this->tables[$table])) {
460
            $tableDef = &$this->tables[$table];
461
            // Is this on a table we are adding?
462
            if (isset($tableDef['create']) && $tableDef['create']) {
463
                // strip everything but the PRIMARY from definition
464
                foreach ($tableDef['keys'] as $keyName => $key) {
465
                    if ($keyName !== 'PRIMARY') {
466
                        unset($tableDef['keys'][$keyName]);
467
                    }
468
                }
469
            } else {
470
                // build drops to strip everything but the PRIMARY
471
                foreach ($tableDef['keys'] as $keyName => $key) {
472
                    if ($keyName !== 'PRIMARY') {
473
                        $this->queue[] = "ALTER TABLE `{$tableDef['name']}` DROP INDEX {$keyName}";
474
                    }
475
                }
476
            }
477
        } else {
478
            return $this->tableNotEstablished();
479
        }
480
481
        return true;
482
    }
483
484
    /**
485
     * Add drop of PRIMARY key for a table to the work queue
486
     *
487
     * @param string $table table
488
     *
489
     * @return bool true if no errors, false if errors encountered
490
     */
491 1
    public function dropPrimaryKey($table)
492
    {
493 1
        if (isset($this->tables[$table])) {
494 1
            $tableDef = $this->tables[$table];
495 1
            $this->queue[] = "ALTER TABLE `{$tableDef['name']}` DROP PRIMARY KEY ";
496
        } else {
497
            return $this->tableNotEstablished();
498
        }
499
500 1
        return true;
501
    }
502
503
    /**
504
     * Add drop of table to the work queue
505
     *
506
     * @param string $table table
507
     *
508
     * @return bool true if no errors, false if errors encountered
509
     */
510 1
    public function dropTable($table)
511
    {
512 1
        if (isset($this->tables[$table])) {
513 1
            $tableDef = $this->tables[$table];
514 1
            $this->queue[] = "DROP TABLE `{$tableDef['name']}` ";
515 1
            unset($this->tables[$table]);
516
        }
517
        // no table is not an error since we are dropping it anyway
518 1
        return true;
519
    }
520
521
522
    /**
523
     * Add rename table operation to the work queue
524
     *
525
     * @param string $table   table
526
     * @param string $newName new table name
527
     *
528
     * @return bool true if no errors, false if errors encountered
529
     */
530 1
    public function renameTable($table, $newName)
531
    {
532 1
        if (isset($this->tables[$table])) {
533 1
            $tableDef = $this->tables[$table];
534 1
            $newTable = $this->name($newName);
535 1
            $this->queue[] = "ALTER TABLE `{$tableDef['name']}` RENAME TO `{$newTable}`";
536 1
            $tableDef['name'] = $newTable;
537 1
            $this->tables[$newName] = $tableDef;
538 1
            unset($this->tables[$table]);
539
        } else {
540
            return $this->tableNotEstablished();
541
        }
542
543 1
        return true;
544
    }
545
546
    /**
547
     * Add alter table table_options (ENGINE, DEFAULT CHARSET, etc.)
548
     * to work queue
549
     *
550
     * @param string $table   table
551
     * @param string $options table_options
552
     *
553
     * @return bool true if no errors, false if errors encountered
554
     */
555 1
    public function setTableOptions($table, $options)
556
    {
557 1
        if (isset($this->tables[$table])) {
558 1
            $tableDef = &$this->tables[$table];
559
            // Is this on a table we are adding?
560 1
            if (isset($tableDef['create']) && $tableDef['create']) {
561
                $tableDef['options'] = $options;
562
                return true;
563
            } else {
564 1
                $this->queue[] = "ALTER TABLE `{$tableDef['name']}` {$options} ";
565 1
                $tableDef['options'] = $options;
566 1
                return true;
567
            }
568
        } else {
569
            return $this->tableNotEstablished();
570
        }
571
    }
572
573
574
    /**
575
     * Clear the work queue
576
     *
577
     * @return void
578
     */
579 19
    public function resetQueue()
580
    {
581 19
        $this->tables = array();
582 19
        $this->queue  = array();
583 19
    }
584
585
    /**
586
     * Executes the work queue
587
     *
588
     * @param bool $force true to force updates even if this is a 'GET' request
589
     *
590
     * @return bool true if no errors, false if errors encountered
591
     */
592
    public function executeQueue($force = false)
593
    {
594
        $this->expandQueue();
595
        foreach ($this->queue as &$ddl) {
596
            if (is_array($ddl)) {
597
                if (isset($ddl['createtable'])) {
598
                    $ddl = $this->renderTableCreate($ddl['createtable']);
599
                }
600
            }
601
            $result = $this->execSql($ddl, $force);
0 ignored issues
show
Bug introduced by
It seems like $ddl can also be of type false; however, parameter $sql of Xmf\Database\Tables::execSql() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

601
            $result = $this->execSql(/** @scrutinizer ignore-type */ $ddl, $force);
Loading history...
602
            if (!$result) {
603
                $this->lastError = $this->db->errorInfo();
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->db->errorInfo() of type array<mixed,mixed> is incompatible with the declared type string of property $lastError.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
604
                $this->lastErrNo = $this->db->errorCode();
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->db->errorCode() can also be of type string. However, the property $lastErrNo is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
605
606
                return false;
607
            }
608
        }
609
610
        return true;
611
    }
612
613
614
    /**
615
     * Create a DELETE statement and add it to the work queue
616
     *
617
     * @param string                 $table    table
618
     * @param string|CriteriaElement $criteria string where clause or object criteria
0 ignored issues
show
Bug introduced by
The type Xmf\Database\CriteriaElement was not found. Did you mean CriteriaElement? If so, make sure to prefix the type with \.
Loading history...
619
     *
620
     * @return bool true if no errors, false if errors encountered
621
     */
622
    public function delete($table, $criteria)
623
    {
624
        if (isset($this->tables[$table])) {
625
            $tableDef = $this->tables[$table];
626
            $where = '';
627
            if (is_scalar($criteria)) {
628
                $where = $criteria;
629
            } elseif (is_object($criteria)) {
630
                $where = $criteria->renderWhere();
631
            }
632
            $this->queue[] = "DELETE FROM `{$tableDef['name']}` {$where}";
633
        } else {
634
            return $this->tableNotEstablished();
635
        }
636
637
        return true;
638
    }
639
640
    /**
641
     * Create an INSERT SQL statement and add it to the work queue.
642
     *
643
     * @param string  $table      table
644
     * @param array   $columns    array of 'column'=>'value' entries
645
     * @param boolean $quoteValue true to quote values, false if caller handles quoting
646
     *
647
     * @return boolean true if no errors, false if errors encountered
648
     */
649
    public function insert($table, $columns, $quoteValue = true)
650
    {
651
        if (isset($this->tables[$table])) {
652
            $tableDef = $this->tables[$table];
653
            $colSql = '';
654
            $valSql = '';
655
            foreach ($tableDef['columns'] as $col) {
656
                $comma = empty($colSql) ? '' : ', ';
657
                if (isset($columns[$col['name']])) {
658
                    $colSql .= "{$comma}`{$col['name']}`";
659
                    $valSql .= $comma
660
                        . ($quoteValue ? $this->db->quote($columns[$col['name']]) : $columns[$col['name']]);
661
                }
662
            }
663
            $sql = "INSERT INTO `{$tableDef['name']}` ({$colSql}) VALUES({$valSql})";
664
            $this->queue[] = $sql;
665
666
            return true;
667
        } else {
668
            return $this->tableNotEstablished();
669
        }
670
    }
671
672
    /**
673
     * Create an UPDATE SQL statement and add it to the work queue
674
     *
675
     * @param string                 $table      table
676
     * @param array                  $columns    array of 'column'=>'value' entries
677
     * @param string|CriteriaElement $criteria   string where clause or object criteria
678
     * @param boolean                $quoteValue true to quote values, false if caller handles quoting
679
     *
680
     * @return boolean true if no errors, false if errors encountered
681
     */
682
    public function update($table, $columns, $criteria, $quoteValue = true)
683
    {
684
        if (isset($this->tables[$table])) {
685
            $tableDef = $this->tables[$table];
686
            $where = '';
687
            if (is_scalar($criteria)) {
688
                $where = $criteria;
689
            } elseif (is_object($criteria)) {
690
                $where = $criteria->renderWhere();
691
            }
692
            $colSql = '';
693
            foreach ($tableDef['columns'] as $col) {
694
                $comma = empty($colSql) ? '' : ', ';
695
                if (isset($columns[$col['name']])) {
696
                    $colSql .= "{$comma}`{$col['name']}` = "
697
                        . ($quoteValue ? $this->db->quote($columns[$col['name']]) : $columns[$col['name']]);
698
                }
699
            }
700
            $sql = "UPDATE `{$tableDef['name']}` SET {$colSql} {$where}";
701
            $this->queue[] = $sql;
702
703
            return true;
704
        } else {
705
            return $this->tableNotEstablished();
706
        }
707
    }
708
709
    /**
710
     * Add statement to remove all rows from a table to the work queue
711
     *
712
     * @param string $table table
713
     *
714
     * @return bool true if no errors, false if errors encountered
715
     */
716
    public function truncate($table)
717
    {
718
        if (isset($this->tables[$table])) {
719
            $tableDef = $this->tables[$table];
720
            $this->queue[] = "TRUNCATE TABLE `{$tableDef['name']}`";
721
        } else {
722
            return $this->tableNotEstablished();
723
        }
724
725
        return true;
726
    }
727
728
729
730
    /**
731
     * return SQL to create the table
732
     *
733
     * This method does NOT modify the work queue
734
     *
735
     * @param string $table    table
736
     * @param bool   $prefixed true to return with table name prefixed
737
     *
738
     * @return string|false string SQL to create table, or false if errors encountered
739
     */
740
    protected function renderTableCreate($table, $prefixed = false)
741
    {
742
        if (isset($this->tables[$table])) {
743
            $tableDef = $this->tables[$table];
744
            $tableName = ($prefixed ? $tableDef['name'] : $table);
745
            $sql = "CREATE TABLE `{$tableName}` (";
746
            $firstComma = '';
747
            foreach ($tableDef['columns'] as $col) {
748
                $sql .= "{$firstComma}\n    `{$col['name']}`  {$col['attributes']}";
749
                $firstComma = ',';
750
            }
751
            $keySql = '';
752
            foreach ($tableDef['keys'] as $keyName => $key) {
753
                if ($keyName === 'PRIMARY') {
754
                    $keySql .= ",\n  PRIMARY KEY ({$key['columns']})";
755
                } else {
756
                    $unique = $key['unique'] ? 'UNIQUE ' : '';
757
                    $keySql .= ",\n  {$unique}KEY {$keyName} ({$key['columns']})";
758
                }
759
            }
760
            $sql .= $keySql;
761
            $sql .= "\n) {$tableDef['options']}";
762
763
            return $sql;
764
        } else {
765
            return $this->tableNotEstablished();
766
        }
767
    }
768
769
    /**
770
     * execute an SQL statement
771
     *
772
     * @param string $sql   SQL statement to execute
773
     * @param bool   $force true to use force updates even in safe requests
774
     *
775
     * @return Statement|false result Statement, or false on error
776
     *                         Any error message is in $this->lastError;
777
     */
778 15
    protected function execSql($sql, $force = false)
779
    {
780 15
        if ($force) {
781
            $this->db->setForce(true);
782
        }
783 15
        $result = $this->db->query($sql);
784
785 15
        if (!$result) {
786
            $this->lastError = $this->db->errorInfo();
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->db->errorInfo() of type array<mixed,mixed> is incompatible with the declared type string of property $lastError.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
787
            $this->lastErrNo = $this->db->errorCode();
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->db->errorCode() can also be of type string. However, the property $lastErrNo is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
788
        }
789
790 15
        return $result;
791
    }
792
793
    /**
794
     * fetch the next row of a result set
795
     *
796
     * @param Statement $result as returned by query
797
     *
798
     * @return mixed false on error
799
     */
800 15
    protected function fetch($result)
801
    {
802 15
        return $result->fetch(FetchMode::ASSOCIATIVE);
803
    }
804
805
    /**
806
     * get table definition from INFORMATION_SCHEMA
807
     *
808
     * @param string $table table
809
     *
810
     * @return array|bool table definition array if table exists, true if table not defined, or
811
     *                    false on error. Error message in $this->lastError;
812
     */
813 15
    protected function getTable($table)
814
    {
815 15
        $tableDef = array();
816
817 15
        $sql  = 'SELECT TABLE_NAME, ENGINE, CHARACTER_SET_NAME ';
818 15
        $sql .= ' FROM `INFORMATION_SCHEMA`.`TABLES` t, ';
819 15
        $sql .= ' `INFORMATION_SCHEMA`.`COLLATIONS` c ';
820 15
        $sql .= ' WHERE t.TABLE_SCHEMA = \'' . $this->databaseName . '\' ';
821 15
        $sql .= ' AND t.TABLE_NAME = \'' . $this->name($table) . '\' ';
822 15
        $sql .= ' AND t.TABLE_COLLATION  = c.COLLATION_NAME ';
823
824 15
        $result = $this->execSql($sql);
825 15
        if (!$result) {
826
            return false;
827
        }
828 15
        $tableSchema = $this->fetch($result);
829 15
        if (empty($tableSchema)) {
830 2
            return true;
831
        }
832 15
        $tableDef['name'] = $tableSchema['TABLE_NAME'];
833 15
        $tableDef['options'] = 'ENGINE=' . $tableSchema['ENGINE'] . ' '
834 15
            . 'DEFAULT CHARSET=' . $tableSchema['CHARACTER_SET_NAME'];
835
836 15
        $sql  = 'SELECT * ';
837 15
        $sql .= ' FROM `INFORMATION_SCHEMA`.`COLUMNS` ';
838 15
        $sql .= ' WHERE TABLE_SCHEMA = \'' . $this->databaseName . '\' ';
839 15
        $sql .= ' AND TABLE_NAME = \'' . $this->name($table) . '\' ';
840 15
        $sql .= ' ORDER BY `ORDINAL_POSITION` ';
841
842 15
        $result = $this->execSql($sql);
843
844 15
        while ($column = $this->fetch($result)) {
0 ignored issues
show
Bug introduced by
It seems like $result can also be of type false; however, parameter $result of Xmf\Database\Tables::fetch() does only seem to accept Doctrine\DBAL\Driver\Statement, maybe add an additional type check? ( Ignorable by Annotation )

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

844
        while ($column = $this->fetch(/** @scrutinizer ignore-type */ $result)) {
Loading history...
845 15
            $attributes = ' ' . $column['COLUMN_TYPE'] . ' '
846 15
                . (($column['IS_NULLABLE'] === 'NO') ? ' NOT NULL ' : '')
847 15
                . (($column['COLUMN_DEFAULT'] === null) ? '' : " DEFAULT '" . $column['COLUMN_DEFAULT'] . "' ")
848 15
                . $column['EXTRA'];
849
850
            $columnDef = array(
851 15
                'name' => $column['COLUMN_NAME'],
852 15
                'attributes' => $attributes
853
            );
854
855 15
            $tableDef['columns'][] = $columnDef;
856
        };
857
858 15
        $sql  = 'SELECT `INDEX_NAME`, `SEQ_IN_INDEX`, `NON_UNIQUE`, ';
859 15
        $sql .= ' `COLUMN_NAME`, `SUB_PART` ';
860 15
        $sql .= ' FROM `INFORMATION_SCHEMA`.`STATISTICS` ';
861 15
        $sql .= ' WHERE TABLE_SCHEMA = \'' . $this->databaseName . '\' ';
862 15
        $sql .= ' AND TABLE_NAME = \'' . $this->name($table) . '\' ';
863 15
        $sql .= ' ORDER BY `INDEX_NAME`, `SEQ_IN_INDEX` ';
864
865 15
        $result = $this->execSql($sql);
866
867 15
        $lastKey = '';
868 15
        $keyCols = '';
869 15
        $keyUnique = false;
870 15
        while ($key = $this->fetch($result)) {
871 15
            if ($lastKey != $key['INDEX_NAME']) {
872 15
                if (!empty($lastKey)) {
873 15
                    $tableDef['keys'][$lastKey]['columns'] = $keyCols;
874 15
                    $tableDef['keys'][$lastKey]['unique'] = $keyUnique;
875
                }
876 15
                $lastKey = $key['INDEX_NAME'];
877 15
                $keyCols = $key['COLUMN_NAME'];
878 15
                if (!empty($key['SUB_PART'])) {
879
                    $keyCols .= ' (' . $key['SUB_PART'] . ')';
880
                }
881 15
                $keyUnique = !$key['NON_UNIQUE'];
882
            } else {
883 15
                $keyCols .= ', ' . $key['COLUMN_NAME'];
884 15
                if (!empty($key['SUB_PART'])) {
885
                    $keyCols .= ' (' . $key['SUB_PART'] . ')';
886
                }
887
            }
888
        };
889 15
        if (!empty($lastKey)) {
890 15
            $tableDef['keys'][$lastKey]['columns'] = $keyCols;
891 15
            $tableDef['keys'][$lastKey]['unique'] = $keyUnique;
892
        }
893
894 15
        return $tableDef;
895
    }
896
897
    /**
898
     * During processing, tables to be created are put in the queue as
899
     * an array('createtable' => tablename) since the definition is not
900
     * complete. This method will expand those references to the full
901
     * ddl to create the table.
902
     *
903
     * @return void
904
     */
905 11
    protected function expandQueue()
906
    {
907 11
        foreach ($this->queue as &$ddl) {
908 11
            if (is_array($ddl)) {
909
                if (isset($ddl['createtable'])) {
910
                    $ddl = $this->renderTableCreate($ddl['createtable'], true);
911
                }
912
            }
913
        }
914 11
    }
915
916
    /**
917
     * Return message from last error encountered
918
     *
919
     * @return string last error message
920
     */
921 1
    public function getLastError()
922
    {
923 1
        return $this->lastError;
924
    }
925
926
    /**
927
     * Return code from last error encountered
928
     *
929
     * @return int last error number
930
     */
931 1
    public function getLastErrNo()
932
    {
933 1
        return $this->lastErrNo;
934
    }
935
936
    /**
937
     * dumpTables - utility function to dump raw tables array
938
     *
939
     * @return array tables
940
     */
941 2
    public function dumpTables()
942
    {
943 2
        return $this->tables;
944
    }
945
946
    /**
947
     * dumpQueue - utility function to dump the work queue
948
     *
949
     * @return array work queue
950
     */
951 11
    public function dumpQueue()
952
    {
953 11
        $this->expandQueue();
954
955 11
        return $this->queue;
956
    }
957
958
    /**
959
     * addToQueue - utility function to add a statement to the work queue
960
     *
961
     * @param string $sql an SQL/DDL statement to add
962
     *
963
     * @return void
964
     */
965 1
    public function addToQueue($sql)
966
    {
967 1
        $this->queue[] = $sql;
968 1
    }
969
970
    /**
971
     * Set lastError as table not established
972
     *
973
     * @return false
974
     */
975
    protected function tableNotEstablished()
976
    {
977
        $this->lastError = _DB_XMF_TABLE_IS_NOT_DEFINED;
978
        $this->lastErrNo = -1;
979
        return false;
980
    }
981
}
982