Tables   F
last analyzed

Complexity

Total Complexity 131

Size/Duplication

Total Lines 975
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 367
dl 0
loc 975
rs 2
c 0
b 0
f 0
wmc 131

37 Methods

Rating   Name   Duplication   Size   Complexity  
A name() 0 3 1
A addPrimaryKey() 0 20 5
A addColumn() 0 28 6
A __construct() 0 7 1
A addIndex() 0 22 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 delete() 0 17 4
A resetQueue() 0 4 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
B update() 0 25 8
A addTable() 0 26 4
A useTable() 0 11 3
A getTableIndexes() 0 8 3
A fetch() 0 3 1
A execSql() 0 14 3
A expandQueue() 0 6 4
A getLastErrNo() 0 3 1
A tableNotEstablished() 0 5 1
A getLastError() 0 3 1
A dumpQueue() 0 5 1
C getTable() 0 83 11
A quoteDefaultClause() 0 17 3
A addToQueue() 0 3 1
A dumpTables() 0 3 1

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

598
            $result = $this->execSql(/** @scrutinizer ignore-type */ $ddl, $force);
Loading history...
599
            if (!$result) {
600
                $this->lastError = $this->db->error();
0 ignored issues
show
Bug introduced by
The method error() does not exist on XoopsDatabase. Since it exists in all sub-types, consider adding an abstract or default implementation to XoopsDatabase. ( Ignorable by Annotation )

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

600
                /** @scrutinizer ignore-call */ 
601
                $this->lastError = $this->db->error();
Loading history...
601
                $this->lastErrNo = $this->db->errno();
0 ignored issues
show
Bug introduced by
The method errno() does not exist on XoopsDatabase. Since it exists in all sub-types, consider adding an abstract or default implementation to XoopsDatabase. ( Ignorable by Annotation )

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

601
                /** @scrutinizer ignore-call */ 
602
                $this->lastErrNo = $this->db->errno();
Loading history...
602
603
                return false;
604
            }
605
        }
606
607
        return true;
608
    }
609
610
611
    /**
612
     * Create a DELETE statement and add it to the work queue
613
     *
614
     * @param string                 $table    table
615
     * @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...
616
     *
617
     * @return bool true if no errors, false if errors encountered
618
     */
619
    public function delete($table, $criteria)
620
    {
621
        if (isset($this->tables[$table])) {
622
            $tableDef = $this->tables[$table];
623
            $where = '';
624
            if (is_scalar($criteria)) {
625
                $where = $criteria;
626
            } elseif (is_object($criteria)) {
627
                /** @var  \CriteriaCompo $criteria */
628
                $where = $criteria->renderWhere();
629
            }
630
            $this->queue[] = "DELETE FROM `{$tableDef['name']}` {$where}";
631
        } else {
632
            return $this->tableNotEstablished();
633
        }
634
635
        return true;
636
    }
637
638
    /**
639
     * Create an INSERT SQL statement and add it to the work queue.
640
     *
641
     * @param string  $table      table
642
     * @param array   $columns    array of 'column'=>'value' entries
643
     * @param boolean $quoteValue true to quote values, false if caller handles quoting
644
     *
645
     * @return boolean true if no errors, false if errors encountered
646
     */
647
    public function insert($table, $columns, $quoteValue = true)
648
    {
649
        if (isset($this->tables[$table])) {
650
            $tableDef = $this->tables[$table];
651
            $colSql = '';
652
            $valSql = '';
653
            foreach ($tableDef['columns'] as $col) {
654
                $comma = empty($colSql) ? '' : ', ';
655
                if (isset($columns[$col['name']])) {
656
                    $colSql .= "{$comma}`{$col['name']}`";
657
                    $valSql .= $comma
658
                        . ($quoteValue ? $this->db->quote($columns[$col['name']]) : $columns[$col['name']]);
0 ignored issues
show
Bug introduced by
The method quote() does not exist on XoopsDatabase. Since it exists in all sub-types, consider adding an abstract or default implementation to XoopsDatabase. ( Ignorable by Annotation )

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

658
                        . ($quoteValue ? $this->db->/** @scrutinizer ignore-call */ quote($columns[$col['name']]) : $columns[$col['name']]);
Loading history...
659
                }
660
            }
661
            $sql = "INSERT INTO `{$tableDef['name']}` ({$colSql}) VALUES({$valSql})";
662
            $this->queue[] = $sql;
663
664
            return true;
665
        } else {
666
            return $this->tableNotEstablished();
667
        }
668
    }
669
670
    /**
671
     * Create an UPDATE SQL statement and add it to the work queue
672
     *
673
     * @param string                 $table      table
674
     * @param array                  $columns    array of 'column'=>'value' entries
675
     * @param string|CriteriaElement $criteria   string where clause or object criteria
676
     * @param boolean                $quoteValue true to quote values, false if caller handles quoting
677
     *
678
     * @return boolean true if no errors, false if errors encountered
679
     */
680
    public function update($table, $columns, $criteria, $quoteValue = true)
681
    {
682
        if (isset($this->tables[$table])) {
683
            $tableDef = $this->tables[$table];
684
            $where = '';
685
            if (is_scalar($criteria)) {
686
                $where = $criteria;
687
            } elseif (is_object($criteria)) {
688
                /** @var  \CriteriaCompo $criteria */
689
                $where = $criteria->renderWhere();
690
            }
691
            $colSql = '';
692
            foreach ($tableDef['columns'] as $col) {
693
                $comma = empty($colSql) ? '' : ', ';
694
                if (isset($columns[$col['name']])) {
695
                    $colSql .= "{$comma}`{$col['name']}` = "
696
                        . ($quoteValue ? $this->db->quote($columns[$col['name']]) : $columns[$col['name']]);
697
                }
698
            }
699
            $sql = "UPDATE `{$tableDef['name']}` SET {$colSql} {$where}";
700
            $this->queue[] = $sql;
701
702
            return true;
703
        } else {
704
            return $this->tableNotEstablished();
705
        }
706
    }
707
708
    /**
709
     * Add statement to remove all rows from a table to the work queue
710
     *
711
     * @param string $table table
712
     *
713
     * @return bool true if no errors, false if errors encountered
714
     */
715
    public function truncate($table)
716
    {
717
        if (isset($this->tables[$table])) {
718
            $tableDef = $this->tables[$table];
719
            $this->queue[] = "TRUNCATE TABLE `{$tableDef['name']}`";
720
        } else {
721
            return $this->tableNotEstablished();
722
        }
723
724
        return true;
725
    }
726
727
728
729
    /**
730
     * return SQL to create the table
731
     *
732
     * This method does NOT modify the work queue
733
     *
734
     * @param string $table    table
735
     * @param bool   $prefixed true to return with table name prefixed
736
     *
737
     * @return string|false string SQL to create table, or false if errors encountered
738
     */
739
    protected function renderTableCreate($table, $prefixed = false)
740
    {
741
        if (isset($this->tables[$table])) {
742
            $tableDef = $this->tables[$table];
743
            $tableName = ($prefixed ? $tableDef['name'] : $table);
744
            $sql = "CREATE TABLE `{$tableName}` (";
745
            $firstComma = '';
746
            foreach ($tableDef['columns'] as $col) {
747
                $sql .= "{$firstComma}\n    `{$col['name']}`  {$col['attributes']}";
748
                $firstComma = ',';
749
            }
750
            $keySql = '';
751
            foreach ($tableDef['keys'] as $keyName => $key) {
752
                if ($keyName === 'PRIMARY') {
753
                    $keySql .= ",\n  PRIMARY KEY ({$key['columns']})";
754
                } else {
755
                    $unique = $key['unique'] ? 'UNIQUE ' : '';
756
                    $keySql .= ",\n  {$unique}KEY {$keyName} ({$key['columns']})";
757
                }
758
            }
759
            $sql .= $keySql;
760
            $sql .= "\n) {$tableDef['options']}";
761
762
            return $sql;
763
        } else {
764
            return $this->tableNotEstablished();
765
        }
766
    }
767
768
    /**
769
     * execute an SQL statement
770
     *
771
     * @param string $sql   SQL statement to execute
772
     * @param bool   $force true to use force updates even in safe requests
773
     *
774
     * @return mixed result resource if no error,
775
     *               true if no error but no result
776
     *               false if error encountered.
777
     *               Any error message is in $this->lastError;
778
     */
779
    protected function execSql($sql, $force = false)
780
    {
781
        if ($force) {
782
            $result = $this->db->queryF($sql);
0 ignored issues
show
Bug introduced by
The method queryF() does not exist on XoopsDatabase. Since it exists in all sub-types, consider adding an abstract or default implementation to XoopsDatabase. ( Ignorable by Annotation )

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

782
            /** @scrutinizer ignore-call */ 
783
            $result = $this->db->queryF($sql);
Loading history...
783
        } else {
784
            $result = $this->db->query($sql);
0 ignored issues
show
Bug introduced by
The method query() does not exist on XoopsDatabase. Since it exists in all sub-types, consider adding an abstract or default implementation to XoopsDatabase. ( Ignorable by Annotation )

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

784
            /** @scrutinizer ignore-call */ 
785
            $result = $this->db->query($sql);
Loading history...
785
        }
786
787
        if (!$result) {
788
            $this->lastError = $this->db->error();
789
            $this->lastErrNo = $this->db->errno();
790
        }
791
792
        return $result;
793
    }
794
795
    /**
796
     * fetch the next row of a result set
797
     *
798
     * @param resource $result as returned by query
799
     *
800
     * @return mixed false on error
801
     */
802
    protected function fetch($result)
803
    {
804
        return $this->db->fetchArray($result);
0 ignored issues
show
Bug introduced by
The method fetchArray() does not exist on XoopsDatabase. Since it exists in all sub-types, consider adding an abstract or default implementation to XoopsDatabase. ( Ignorable by Annotation )

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

804
        return $this->db->/** @scrutinizer ignore-call */ fetchArray($result);
Loading history...
805
    }
806
807
    /**
808
     * create default value clause for DDL
809
     *
810
     * @param string|null $default the default value to be quoted
811
     *
812
     * @return string the correctly quoted default value
813
     */
814
    protected function quoteDefaultClause($default)
815
    {
816
        // . (($column['COLUMN_DEFAULT'] === null) ? '' : " DEFAULT '" . $column['COLUMN_DEFAULT'] . "' ")
817
        // no default specified
818
        if (null===$default) {
819
            return '';
820
        }
821
822
        // functions should not be quoted
823
        // this section will need expanded when XOOPS minimum is no longer a mysql 5 version
824
        // Until mysql 8, only allowed function is CURRENT_TIMESTAMP
825
        if ($default === 'CURRENT_TIMESTAMP') {
826
            return ' DEFAULT CURRENT_TIMESTAMP ';
827
        }
828
829
        // surround default with quotes
830
        return " DEFAULT '{$default}' ";
831
    }
832
833
    /**
834
     * get table definition from INFORMATION_SCHEMA
835
     *
836
     * @param string $table table
837
     *
838
     * @return array|bool table definition array if table exists, true if table not defined, or
839
     *                    false on error. Error message in $this->lastError;
840
     */
841
    protected function getTable($table)
842
    {
843
        $tableDef = array();
844
845
        $sql  = 'SELECT TABLE_NAME, ENGINE, CHARACTER_SET_NAME ';
846
        $sql .= ' FROM `INFORMATION_SCHEMA`.`TABLES` t, ';
847
        $sql .= ' `INFORMATION_SCHEMA`.`COLLATIONS` c ';
848
        $sql .= ' WHERE t.TABLE_SCHEMA = \'' . $this->databaseName . '\' ';
849
        $sql .= ' AND t.TABLE_NAME = \'' . $this->name($table) . '\' ';
850
        $sql .= ' AND t.TABLE_COLLATION  = c.COLLATION_NAME ';
851
852
        $result = $this->execSql($sql);
853
        if (!$result) {
854
            return false;
855
        }
856
        $tableSchema = $this->fetch($result);
857
        if (empty($tableSchema)) {
858
            return true;
859
        }
860
        $tableDef['name'] = $tableSchema['TABLE_NAME'];
861
        $tableDef['options'] = 'ENGINE=' . $tableSchema['ENGINE'] . ' '
862
            . 'DEFAULT CHARSET=' . $tableSchema['CHARACTER_SET_NAME'];
863
864
        $sql  = 'SELECT * ';
865
        $sql .= ' FROM `INFORMATION_SCHEMA`.`COLUMNS` ';
866
        $sql .= ' WHERE TABLE_SCHEMA = \'' . $this->databaseName . '\' ';
867
        $sql .= ' AND TABLE_NAME = \'' . $this->name($table) . '\' ';
868
        $sql .= ' ORDER BY `ORDINAL_POSITION` ';
869
870
        $result = $this->execSql($sql);
871
872
        while ($column = $this->fetch($result)) {
873
            $attributes = ' ' . $column['COLUMN_TYPE'] . ' '
874
                . (($column['IS_NULLABLE'] === 'NO') ? ' NOT NULL ' : '')
875
                . $this->quoteDefaultClause($column['COLUMN_DEFAULT'])
876
                //. $column['EXTRA'];
877
                . str_replace('DEFAULT_GENERATED ', '', $column['EXTRA']);
878
879
            $columnDef = array(
880
                'name' => $column['COLUMN_NAME'],
881
                'attributes' => $attributes
882
            );
883
884
            $tableDef['columns'][] = $columnDef;
885
        };
886
887
        $sql  = 'SELECT `INDEX_NAME`, `SEQ_IN_INDEX`, `NON_UNIQUE`, ';
888
        $sql .= ' `COLUMN_NAME`, `SUB_PART` ';
889
        $sql .= ' FROM `INFORMATION_SCHEMA`.`STATISTICS` ';
890
        $sql .= ' WHERE TABLE_SCHEMA = \'' . $this->databaseName . '\' ';
891
        $sql .= ' AND TABLE_NAME = \'' . $this->name($table) . '\' ';
892
        $sql .= ' ORDER BY `INDEX_NAME`, `SEQ_IN_INDEX` ';
893
894
        $result = $this->execSql($sql);
895
896
        $lastKey = '';
897
        $keyCols = '';
898
        $keyUnique = false;
899
        while ($key = $this->fetch($result)) {
900
            if ($lastKey != $key['INDEX_NAME']) {
901
                if (!empty($lastKey)) {
902
                    $tableDef['keys'][$lastKey]['columns'] = $keyCols;
903
                    $tableDef['keys'][$lastKey]['unique'] = $keyUnique;
904
                }
905
                $lastKey = $key['INDEX_NAME'];
906
                $keyCols = $key['COLUMN_NAME'];
907
                if (!empty($key['SUB_PART'])) {
908
                    $keyCols .= ' (' . $key['SUB_PART'] . ')';
909
                }
910
                $keyUnique = !$key['NON_UNIQUE'];
911
            } else {
912
                $keyCols .= ', ' . $key['COLUMN_NAME'];
913
                if (!empty($key['SUB_PART'])) {
914
                    $keyCols .= ' (' . $key['SUB_PART'] . ')';
915
                }
916
            }
917
        };
918
        if (!empty($lastKey)) {
919
            $tableDef['keys'][$lastKey]['columns'] = $keyCols;
920
            $tableDef['keys'][$lastKey]['unique'] = $keyUnique;
921
        }
922
923
        return $tableDef;
924
    }
925
926
    /**
927
     * During processing, tables to be created are put in the queue as
928
     * an array('createtable' => tablename) since the definition is not
929
     * complete. This method will expand those references to the full
930
     * ddl to create the table.
931
     *
932
     * @return void
933
     */
934
    protected function expandQueue()
935
    {
936
        foreach ($this->queue as &$ddl) {
937
            if (is_array($ddl)) {
938
                if (isset($ddl['createtable'])) {
939
                    $ddl = $this->renderTableCreate($ddl['createtable'], true);
940
                }
941
            }
942
        }
943
    }
944
945
    /**
946
     * Return message from last error encountered
947
     *
948
     * @return string last error message
949
     */
950
    public function getLastError()
951
    {
952
        return $this->lastError;
953
    }
954
955
    /**
956
     * Return code from last error encountered
957
     *
958
     * @return int last error number
959
     */
960
    public function getLastErrNo()
961
    {
962
        return $this->lastErrNo;
963
    }
964
965
    /**
966
     * dumpTables - utility function to dump raw tables array
967
     *
968
     * @return array tables
969
     */
970
    public function dumpTables()
971
    {
972
        return $this->tables;
973
    }
974
975
    /**
976
     * dumpQueue - utility function to dump the work queue
977
     *
978
     * @return array work queue
979
     */
980
    public function dumpQueue()
981
    {
982
        $this->expandQueue();
983
984
        return $this->queue;
985
    }
986
987
    /**
988
     * addToQueue - utility function to add a statement to the work queue
989
     *
990
     * @param string $sql an SQL/DDL statement to add
991
     *
992
     * @return void
993
     */
994
    public function addToQueue($sql)
995
    {
996
        $this->queue[] = $sql;
997
    }
998
999
    /**
1000
     * Set lastError as table not established
1001
     *
1002
     * @return false
1003
     */
1004
    protected function tableNotEstablished()
1005
    {
1006
        $this->lastError = _DB_XMF_TABLE_IS_NOT_DEFINED;
1007
        $this->lastErrNo = -1;
1008
        return false;
1009
    }
1010
}
1011