Passed
Pull Request — master (#584)
by Richard
17:37
created

Tables::quoteIndexColumnName()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2.0438

Importance

Changes 0
Metric Value
cc 2
eloc 8
nc 2
nop 1
dl 0
loc 13
ccs 7
cts 9
cp 0.7778
crap 2.0438
rs 10
c 0
b 0
f 0
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\Driver\Statement;
15
use Xmf\Language;
16
use Xoops\Core\Database\Connection;
17
use Xoops\Core\Database\Factory;
18
19
/**
20
 * Xmf\Database\Tables
21
 *
22
 * inspired by Yii CDbMigration
23
 *
24
 * Build a work queue of database changes needed to implement new and
25
 * changed tables. Define table(s) you are dealing with and any desired
26
 * change(s). If the changes are already in place (i.e. the new column
27
 * already exists) no work is added. Then executeQueue() to process the
28
 * whole set.
29
 *
30
 * @category  Xmf\Database\Tables
31
 * @package   Xmf
32
 * @author    Richard Griffith <[email protected]>
33
 * @copyright 2011-2018 XOOPS Project (https://xoops.org)
34
 * @license   GNU GPL 2 or later (http://www.gnu.org/licenses/gpl-2.0.html)
35
 * @link      https://xoops.org
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 1
                        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 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();
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 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();
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(\PDO::FETCH_ASSOC);
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 15
                    $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 11
                    $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