Failed Conditions
Push — master ( 764644...3e4eb0 )
by Ryan
03:35
created

Database::alterTable()   C

Complexity

Conditions 13
Paths 28

Size

Total Lines 39
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 13
eloc 28
nc 28
nop 3
dl 0
loc 39
rs 5.1234
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 *
5
 */
6
namespace Godsgood33\Php_Db;
7
8
use Monolog\Logger;
9
use Monolog\Formatter\LineFormatter;
10
use Monolog\Handler\StreamHandler;
11
use Error;
12
use Exception;
13
use mysqli;
14
15
/**
16
 * A generic database class
17
 *
18
 * @author Ryan Prather
19
 */
20
class Database
21
{
22
23
    /**
24
     * Constant defining a SELECT query
25
     *
26
     * @var integer
27
     */
28
    const SELECT = 1;
29
30
    /**
31
     * Constant defining a SELECT COUNT query
32
     *
33
     * @var integer
34
     */
35
    const SELECT_COUNT = 2;
36
37
    /**
38
     * Constant defining a CREATE TABLE query
39
     *
40
     * @var integer
41
     */
42
    const CREATE_TABLE = 3;
43
44
    /**
45
     * Constant defining DROP query
46
     *
47
     * @var integer
48
     */
49
    const DROP = 4;
50
51
    /**
52
     * Constant defining DELETE query
53
     *
54
     * @var integer
55
     */
56
    const DELETE = 5;
57
58
    /**
59
     * Constant defining INSERT query
60
     *
61
     * @var integer
62
     */
63
    const INSERT = 6;
64
65
    /**
66
     * Constant defining REPLACE query
67
     *
68
     * @var integer
69
     */
70
    const REPLACE = 7;
71
72
    /**
73
     * Constant defining UPDATE query
74
     *
75
     * @var integer
76
     */
77
    const UPDATE = 8;
78
79
    /**
80
     * Constant defining EXTENDED INSERT query
81
     *
82
     * @var integer
83
     */
84
    const EXTENDED_INSERT = 9;
85
86
    /**
87
     * Constant defining EXTENDED REPLACE query
88
     *
89
     * @var integer
90
     */
91
    const EXTENDED_REPLACE = 10;
92
93
    /**
94
     * Constant defining EXTENDED UPDATE query
95
     *
96
     * @var integer
97
     */
98
    const EXTENDED_UPDATE = 11;
99
100
    /**
101
     * Constant defining ALTER TABLE query
102
     *
103
     * @var integer
104
     */
105
    const ALTER_TABLE = 12;
106
107
    /**
108
     * Constant defining action for alter table statement
109
     *
110
     * @var integer
111
     */
112
    const ADD_COLUMN = 1;
113
114
    /**
115
     * Constant defining action for alter table statement
116
     *
117
     * @var integer
118
     */
119
    const DROP_COLUMN = 2;
120
121
    /**
122
     * Constant defining action for alter table statement
123
     *
124
     * @var integer
125
     */
126
    const MODIFY_COLUMN = 3;
127
128
    /**
129
     * Constant defining a TRUNCATE TABLE query
130
     *
131
     * @var integer
132
     */
133
    const TRUNCATE = 13;
134
135
    /**
136
     * Global to represent an IN statement (e.g.
137
     * WHERE field IN (1,2))
138
     *
139
     * @var string
140
     */
141
    const IN = 'IN';
142
143
    /**
144
     * Global to represent a NOT IN statement (e.g.
145
     * WHERE field NOT IN (1,2))
146
     *
147
     * @var string
148
     */
149
    const NOT_IN = 'NOT IN';
150
151
    /**
152
     * Global to represent a BETWEEN statement (e.g.
153
     * WHERE field BETWEEN 1 and 2)
154
     *
155
     * @var string
156
     */
157
    const BETWEEN = 'BETWEEN';
158
159
    /**
160
     * Global to represent a LIKE statement (e.g.
161
     * WHERE field LIKE '%value%')
162
     *
163
     * @var string
164
     */
165
    const LIKE = 'LIKE';
166
167
    /**
168
     * Global to represent a NOT LIKE statement (e.g.
169
     * WHERE field NOT LIKE '%value%')
170
     *
171
     * @var string
172
     */
173
    const NOT_LIKE = 'NOT LIKE';
174
175
    /**
176
     * Global to represent an IS statement (e.g.
177
     * WHERE field IS NULL)
178
     *
179
     * @var string
180
     */
181
    const IS = 'IS';
182
183
    /**
184
     * Global to represent an IS NOT statement (e.g.
185
     * WHERE field IS NOT NULL)
186
     *
187
     * @var string
188
     */
189
    const IS_NOT = 'IS NOT';
190
191
    /**
192
     * The mysqli connection
193
     *
194
     * @var \mysqli
195
     */
196
    private $_c;
197
198
    /**
199
     * To store the SQL statement
200
     *
201
     * @var string
202
     */
203
    private $_sql = null;
204
205
    /**
206
     * A string to store the type of query that is being run
207
     *
208
     * @var string
209
     */
210
    private $_queryType = null;
211
212
    /**
213
     * The result of the query
214
     *
215
     * @var mixed
216
     */
217
    private $_result = null;
218
219
    /**
220
     * Log level
221
     *
222
     * @var string
223
     */
224
    private $_logLevel = Logger::ERROR;
225
226
    /**
227
     * Variable to store the logger
228
     *
229
     * @var \Monolog\Logger
230
     */
231
    private $_logger = null;
232
233
    /**
234
     * Path for the logger to log the file
235
     *
236
     * @var string
237
     */
238
    private $_logPath = null;
239
240
    /**
241
     * Variable to decide if we need to automatically run the queries after generating them
242
     *
243
     * @var boolean
244
     */
245
    public static $autorun = false;
246
247
    /**
248
     * Constructor
249
     *
250
     * @param string $strLogPath
251
     *            [optional]
252
     * @param \mysqli $dbh
253
     *            [optional]
254
     *            [by ref]
255
     *            mysqli object to perform queries.
256
     */
257
    public function __construct(string $strLogPath = __DIR__, mysqli &$dbh = null)
258
    {
259
        require_once 'DBConfig.php';
260
        if (! is_null($dbh) && is_a($dbh, "mysqli")) {
261
            $this->_c = $dbh;
262
        } else {
263
            if (PHP_DB_SERVER == '{IP|hostname}' || PHP_DB_USER == '{username}' || PHP_DB_PWD == '{password}' || PHP_DB_SCHEMA == '{schema}') {
0 ignored issues
show
introduced by
The condition Godsgood33\Php_Db\PHP_DB...DB_SCHEMA == '{schema}' can never be false.
Loading history...
264
                throw new Error("Need to update DBConfig.php", E_ERROR);
265
            }
266
            $this->_c = new mysqli(PHP_DB_SERVER, PHP_DB_USER, PHP_DB_PWD, PHP_DB_SCHEMA);
267
        }
268
269
        if ($this->_c->connect_errno) {
270
            throw new Error("Could not create database class due to error {$this->_c->error}", E_ERROR);
271
        }
272
273
        $this->_logPath = $strLogPath;
274
        touch($this->_logPath . "/db.log");
275
276
        $this->_logger = new Logger('db', [
277
            new StreamHandler(realpath($this->_logPath . "/db.log"), $this->_logLevel)
0 ignored issues
show
Bug introduced by
$this->_logLevel of type string is incompatible with the type integer expected by parameter $level of Monolog\Handler\StreamHandler::__construct(). ( Ignorable by Annotation )

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

277
            new StreamHandler(realpath($this->_logPath . "/db.log"), /** @scrutinizer ignore-type */ $this->_logLevel)
Loading history...
278
        ]);
279
280
        if (PHP_SAPI == 'cli') {
281
            $stream = new StreamHandler("php://output", $this->_logLevel);
282
            $stream->setFormatter(new LineFormatter("%datetime% %level_name% %message%" . PHP_EOL, "H:i:s.u"));
283
            $this->_logger->pushHandler($stream);
284
        }
285
286
        $this->_logger->info("Database connected");
287
        $this->_logger->debug("Connection details:", [
288
            'Server' => PHP_DB_SERVER,
289
            'User' => PHP_DB_USER,
290
            'Schema' => PHP_DB_SCHEMA
291
        ]);
292
293
        $this->setVar("time_zone", "+00:00");
294
        $this->setVar("sql_mode", "");
295
    }
296
297
    /**
298
     * Function to make sure that the database is connected
299
     *
300
     * @return boolean
301
     */
302
    public function isConnected()
303
    {
304
        $this->_logger->debug("Pinging server");
305
        return $this->_c->ping();
306
    }
307
308
    /**
309
     * Setter function for _logger
310
     *
311
     * @param Logger $log
312
     */
313
    public function setLogger(Logger $log)
314
    {
315
        $this->_logger->debug("Setting logger");
316
        $this->_logger = $log;
317
    }
318
319
    /**
320
     * Getter function for _Logger
321
     *
322
     * @return string
323
     */
324
    public function getLogLevel()
325
    {
326
        $this->_logger->debug("Getting log level ({$this->_logLevel})");
327
        return $this->_logLevel;
328
    }
329
330
    /**
331
     * Function to set the log level just in case there needs to be a change to the default log level
332
     *
333
     * @param string $strLevel
334
     */
335
    public function setLogLevel(string $strLevel)
336
    {
337
        $this->_logger->debug("Setting log level to {$strLevel}");
338
        $this->_logLevel = $strLevel;
339
340
        $handles = [];
341
342
        foreach ($this->_logger->getHandlers() as $h) {
343
            $h->setLevel($strLevel);
0 ignored issues
show
Bug introduced by
The method setLevel() does not exist on Monolog\Handler\HandlerInterface. It seems like you code against a sub-type of Monolog\Handler\HandlerInterface such as Monolog\Handler\AbstractHandler. ( Ignorable by Annotation )

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

343
            $h->/** @scrutinizer ignore-call */ 
344
                setLevel($strLevel);
Loading history...
344
            $handles[] = $h;
345
        }
346
347
        $this->_logger->setHandlers($handles);
348
    }
349
350
    /**
351
     * Getter function for _sql
352
     *
353
     * @return string
354
     */
355
    public function getSql()
356
    {
357
        return $this->_sql;
358
    }
359
360
    /**
361
     * Function to return the currently selected database schema
362
     *
363
     * @return string
364
     */
365
    public function getSchema()
366
    {
367
        if ($res = $this->_c->query("SELECT DATABASE()")) {
368
            $row = $res->fetch_row();
369
370
            $this->_logger->debug("Getting schema {$row[0]}");
371
            return $row[0];
372
        }
373
        return null;
374
    }
375
376
    /**
377
     * Function to set schema
378
     *
379
     * @param string $strSchema
380
     */
381
    public function setSchema(string $strSchema)
382
    {
383
        $this->_logger->debug("Setting schema to {$strSchema}");
384
        if (! $this->_c->select_db($strSchema)) {
385
            $this->_logger->emergency("Unknown schema {$strSchema}");
386
            return false;
387
        }
388
        return true;
389
    }
390
391
    /**
392
     * Method to set a MYSQL variable
393
     *
394
     * @param string $strName
395
     * @param string $strVal
396
     * @return boolean
397
     */
398
    public function setVar(string $strName, string $strVal)
399
    {
400
        if (! $strName || ! $strVal) {
401
            $this->_logger->debug("name or value are blank", [
402
                'name' => $strName,
403
                'value' => $strVal
404
            ]);
405
            return false;
406
        }
407
408
        $this->_logger->debug("Setting {$strName} = '{$strVal}'");
409
410
        if ($this->_c->real_query("SET $strName = {$this->_escape($strVal)}")) {
411
            return true;
412
        } else {
413
            $this->_logger->error("Failed to set variable {$this->_c->error}");
414
            return false;
415
        }
416
    }
417
418
    /**
419
     * Function to execute the statement
420
     *
421
     * @param mixed $return
422
     *            [optional]
423
     *            MYSQLI constant to control what is returned from the mysqli_result object
424
     * @param string $class
425
     *            [optional]
426
     *            Class to use when returning object
427
     * @param string $strSql
428
     *            [optional]
429
     *            Optional SQL query
430
     *
431
     * @throws \Exception
432
     * @throws \InvalidArgumentException
433
     *
434
     * @return mixed
435
     */
436
    public function execute($return = MYSQLI_ASSOC, $class = null, $strSql = null)
437
    {
438
        if (! is_null($strSql)) {
439
            $this->_sql = $strSql;
440
        }
441
442
        if (is_a($this->_c, 'mysqli')) {
443
            if (! $this->_c->ping()) {
444
                require_once 'DBConfig.php';
445
                $this->_c = null;
446
                $this->_c = new mysqli(PHP_DB_SERVER, PHP_DB_USER, PHP_DB_PWD, PHP_DB_SCHEMA);
447
            }
448
        } else {
449
            throw new \Error('Database was not connected', E_ERROR);
450
        }
451
452
        $this->_logger->info("Executing {$this->_queryType} query");
453
        $this->_logger->debug($this->_sql);
454
455
        try {
456
            if (in_array($this->_queryType, [
457
                self::SELECT,
458
                self::SELECT_COUNT
459
            ])) {
460
                $this->_result = $this->_c->query($this->_sql);
461
                if ($this->_c->error) {
462
                    $this->_logger->error("There is an error {$this->_c->error}");
463
                    throw new Exception("There was an error {$this->_c->error}", E_ERROR);
464
                }
465
            } else {
466
                $this->_result = $this->_c->real_query($this->_sql);
467
                if ($this->_c->errno) {
468
                    $this->_logger->error("There was an error {$this->_c->error}");
469
                    throw new Exception("There was an error {$this->_c->error}", E_ERROR);
470
                }
471
            }
472
473
            if ($return == MYSQLI_OBJECT && ! is_null($class) && class_exists($class)) {
474
                $this->_logger->debug("Checking results for query", [
475
                    'class' => get_class($class)
0 ignored issues
show
Bug introduced by
$class of type string is incompatible with the type object expected by parameter $object of get_class(). ( Ignorable by Annotation )

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

475
                    'class' => get_class(/** @scrutinizer ignore-type */ $class)
Loading history...
476
                ]);
477
                $this->_result = $this->checkResults($return, $class);
478
            } elseif ($return == MYSQLI_OBJECT && is_null($class)) {
479
                $this->_logger->debug("Checking results for query", [
480
                    'class' => 'stdClass'
481
                ]);
482
                $this->_result = $this->checkResults($return, 'stdClass');
483
            } else {
484
                $this->_logger->debug("Checking results for query and returning associative array");
485
                $this->_result = $this->checkResults(MYSQLI_ASSOC);
486
            }
487
        } catch (Exception $e) {}
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
488
489
        return $this->_result;
490
    }
491
492
    /**
493
     * Function to check the results and return what is expected
494
     *
495
     * @param mixed $returnType
496
     *            [optional]
497
     *            Optional return mysqli_result return type
498
     *
499
     * @return mixed
500
     */
501
    public function checkResults($returnType = MYSQLI_ASSOC, $class = null)
502
    {
503
        $res = null;
504
505
        switch ($this->_queryType) {
506
            case self::SELECT_COUNT:
507
                if (! is_a($this->_result, 'mysqli_result')) {
508
                    die($this->_logger->error("Error with return on query"));
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
509
                }
510
511
                if ($this->_result->num_rows == 1) {
512
                    $row = $this->_result->fetch_assoc();
513
                    if (isset($row['count'])) {
514
                        $this->_logger->debug("Returning SELECT_COUNT query", [
515
                            'count' => $row['count']
516
                        ]);
517
                        $res = $row['count'];
518
                    }
519
                } elseif ($this->_result->num_rows > 1) {
520
                    $this->_logger->debug("Returning SELECT_COUNT query", [
521
                        'count' => $this->_result->num_rows
522
                    ]);
523
                    $res = $this->_result->num_rows;
524
                }
525
526
                mysqli_free_result($this->_result);
527
528
                return $res;
529
            case self::SELECT:
530
                if (! is_a($this->_result, 'mysqli_result')) {
531
                    die($this->_logger->error("Error with return on query"));
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
532
                }
533
534
                if ($returnType == MYSQLI_OBJECT && ! is_null($class) && class_exists($class)) {
535
                    if ($this->_result->num_rows == 1) {
536
                        $this->_logger->debug("Returning object from SELECT query", [
537
                            'type' => get_class($class)
538
                        ]);
539
                        $res = $this->_result->fetch_object($class);
540
                    } elseif ($this->_result->num_rows > 1) {
541
                        $this->_logger->debug("Returning object array from SELECT query", [
542
                            'type' => get_class($class)
543
                        ]);
544
                        while ($row = $this->_result->fetch_object($class)) {
545
                            $res[] = $row;
546
                        }
547
                    }
548
                } else {
549
                    if ($this->_result->num_rows == 1) {
550
                        $this->_logger->debug("Fetching results");
551
                        $res = $this->_result->fetch_array($returnType);
552
                    } elseif ($this->_result->num_rows > 1) {
553
                        $this->_logger->debug("Fetching results array");
554
                        $res = $this->fetchAll($returnType);
555
                    }
556
                }
557
558
                mysqli_free_result($this->_result);
559
560
                return $res;
561
            case self::INSERT:
562
                if ($this->_c->error) {
563
                    $this->_logger->error("Database Error {$this->_c->error}");
564
                    return 0;
565
                }
566
567
                if ($this->_c->insert_id) {
568
                    $this->_logger->debug("Insert successful returning insert_id", [
569
                        'id' => $this->_c->insert_id
570
                    ]);
571
                    return $this->_c->insert_id;
572
                } elseif ($this->_c->affected_rows) {
573
                    $this->_logger->debug("Insert successful return affected row count", [
574
                        'count' => $this->_c->affected_rows
575
                    ]);
576
                    return $this->_c->affected_rows;
577
                }
578
579
                $this->_logger->debug("Insert successful, but no ID so returning 1 for success");
580
581
                return 1;
582
            // intentional fall through
583
            case self::EXTENDED_INSERT:
584
            // intentional fall through
585
            case self::EXTENDED_REPLACE:
586
            // intentional fall through
587
            case self::EXTENDED_UPDATE:
588
            // intentional fall through
589
            case self::REPLACE:
590
            // intentional fall through
591
            case self::UPDATE:
592
            // intentional fall through
593
            case self::DELETE:
594
            // intentional fall through
595
            case self::ALTER_TABLE:
0 ignored issues
show
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
596
                if ($this->_c->error) {
597
                    $this->_logger->error("Database Error {$this->_c->error}");
598
                    return false;
599
                } elseif ($this->_c->affected_rows) {
600
                    $this->_logger->debug("Returning affected row count for {$this->_queryType}", [
601
                        'count' => $this->_c->affected_rows
602
                    ]);
603
                    return $this->_c->affected_rows;
604
                } else {
605
                    return true;
606
                }
607
            case self::CREATE_TABLE:
608
            case self::DROP:
609
            case self::TRUNCATE:
610
                $this->_logger->debug("Returning from {$this->_queryType}");
611
                return true;
612
        }
613
    }
614
615
    /**
616
     * Function to pass through calling the query function (used for backwards compatibility and for more complex queries that aren't currently supported)
617
     * Nothing is escaped
618
     *
619
     * @param string $strSql
620
     *            [optional]
621
     *            Optional query to pass in and execute
622
     *
623
     * @return \mysqli_result|boolean
624
     */
625
    public function query($strSql = null)
626
    {
627
        if (is_null($strSql)) {
628
            return $this->_c->query($this->_sql);
629
        } else {
630
            return $this->_c->query($strSql);
631
        }
632
    }
633
634
    /**
635
     * A function to build a select query
636
     *
637
     * @param string $strTableName
638
     *            The table to query
639
     * @param array|string $fields
640
     *            [optional]
641
     *            Optional array of fields to return (defaults to '*')
642
     * @param array $arrWhere
643
     *            [optional]
644
     *            Optional 2-dimensional array to build where clause from
645
     * @param array $arrFlags
646
     *            [optional]
647
     *            Optional 2-dimensional array to allow other flags
648
     *
649
     * @see Database::where()
650
     * @see Database::flags()
651
     *
652
     * @throws \InvalidArgumentException
653
     *
654
     * @return mixed
655
     */
656
    public function select(string $strTableName, $fields = null, array $arrWhere = [], array $arrFlags = [])
657
    {
658
        $this->_sql = null;
659
        $this->_queryType = self::SELECT;
660
661
        if (! is_null($strTableName) && is_string($strTableName)) {
0 ignored issues
show
introduced by
The condition ! is_null($strTableName)...s_string($strTableName) can never be false.
Loading history...
662
            $this->_logger->debug("Starting SELECT query of {$strTableName}", [
663
                'fields' => $this->fields($fields)
664
            ]);
665
            $this->_sql = "SELECT " . $this->fields($fields) . " FROM $strTableName";
666
        } else {
667
            $this->_logger->emergency("Table name is invalid or wrong type");
668
            throw new Error("Table name is invalid");
669
        }
670
671
        if (isset($arrFlags['joins']) && is_array($arrFlags['joins']) && count($arrFlags['joins'])) {
672
            $this->_logger->debug("Adding joins", [
673
                'joins' => implode(' ', $arrFlags['joins'])
674
            ]);
675
            $this->_sql .= " " . implode(" ", $arrFlags['joins']);
676
        } else {
677
            $this->_logger->debug("No joins");
678
        }
679
680
        if (! is_null($arrWhere) && is_array($arrWhere) && count($arrWhere)) {
681
            $where_str = " WHERE";
682
            $this->_logger->debug("Parsing where clause and adding to query");
683
            foreach ($arrWhere as $x => $w) {
684
                $where_str .= $this->parseClause($w, $x);
685
            }
686
            if (strlen($where_str) > strlen(" WHERE")) {
687
                $this->_sql .= $where_str;
688
            }
689
        }
690
691
        if (is_array($arrFlags) && count($arrFlags)) {
692
            $this->_logger->debug("Parsing flags and adding to query", $arrFlags);
693
            $this->_sql .= $this->flags($arrFlags);
694
        }
695
696
        if (self::$autorun) {
697
            return $this->execute(MYSQLI_BOTH);
698
        }
699
700
        return $this->_sql;
701
    }
702
703
    /**
704
     * Function to build a query to check the number of rows in a table
705
     *
706
     * @param string $strTableName
707
     *            The table to query
708
     * @param array $where
709
     *            [optional]
710
     *            Optional 2-dimensional array to build where clause
711
     * @param array $flags
712
     *            [optional]
713
     *            Optional 2-dimensional array to add flags
714
     *
715
     * @see Database::where()
716
     * @see Database::flags()
717
     *
718
     * @return string|NULL
719
     */
720
    public function selectCount(string $strTableName, array $arrWhere = [], array $arrFlags = [])
721
    {
722
        $this->_sql = null;
723
        $this->_queryType = self::SELECT_COUNT;
724
725
        if (! is_null($strTableName) && is_string($strTableName)) {
0 ignored issues
show
introduced by
The condition ! is_null($strTableName)...s_string($strTableName) can never be false.
Loading history...
726
            $this->_sql = "SELECT COUNT(1) AS 'count' FROM $strTableName";
727
        } else {
728
            $this->_logger->emergency("Table name is invalid or wrong type");
729
            throw new Error("Table name is invalid");
730
        }
731
732
        if (isset($arrFlags['joins']) && is_array($arrFlags['joins'])) {
733
            $this->_sql .= " " . implode(" ", $arrFlags['joins']);
734
        }
735
736
        if (! is_null($arrWhere) && is_array($arrWhere) && count($arrWhere)) {
737
            $where_str = " WHERE";
738
            foreach ($arrWhere as $x => $w) {
739
                $where_str .= $this->parseClause($w, $x);
740
            }
741
            if (strlen($where_str) > strlen(" WHERE")) {
742
                $this->_sql .= $where_str;
743
            }
744
        }
745
746
        if (is_array($arrFlags) && count($arrFlags)) {
747
            $this->_sql .= $this->flags($arrFlags);
748
        }
749
750
        if (self::$autorun) {
751
            return $this->execute(MYSQLI_BOTH);
752
        }
753
754
        return $this->_sql;
755
    }
756
757
    /**
758
     * Function to build an insert query statement
759
     *
760
     * @param string $strTableName
761
     * @param array|string $params
762
     * @param boolean $to_ignore
763
     *
764
     * @return string|NULL
765
     */
766
    public function insert(string $strTableName, $params = null, bool $blnToIgnore = false)
767
    {
768
        $this->_sql = null;
769
        $this->_queryType = self::INSERT;
770
771
        if (! is_null($strTableName) && is_string($strTableName)) {
0 ignored issues
show
introduced by
The condition ! is_null($strTableName)...s_string($strTableName) can never be false.
Loading history...
772
            $this->_sql = "INSERT" . ($blnToIgnore ? " IGNORE" : "") . " INTO $strTableName" . (is_array($params) && count($params) ? " (`" . implode("`,`", array_keys($params)) . "`)" : null);
773
        } else {
774
            throw new Error("Table name is invalid");
775
        }
776
777
        if (is_array($params) && count($params)) {
778
            $this->_sql .= " VALUES (" . implode(",", array_map([
779
                $this,
780
                '_escape'
781
            ], array_values($params))) . ")";
782
        } elseif (is_string($params) && stripos($params, 'SELECT') !== false) {
783
            $this->_sql .= " {$params}";
784
        } else {
785
            throw new Error("Invalid type passed to insert " . gettype($params));
786
        }
787
788
        if (self::$autorun) {
789
            return $this->execute(MYSQLI_BOTH);
790
        }
791
792
        return $this->_sql;
793
    }
794
795
    /**
796
     * Function to create an extended insert query statement
797
     *
798
     * @param string $strTableName
799
     *            The table name that the data is going to be inserted on
800
     * @param array $arrFields
801
     *            An array of field names that each value represents
802
     * @param array|string $params
803
     *            An array of array of values or a string with a SELECT statement to populate the insert with
804
     * @param boolean $blnToIgnore
805
     *            [optional]
806
     *            Boolean to decide if we need to use the INSERT IGNORE INTO syntax
807
     *
808
     * @return NULL|string Returns the SQL if self::$autorun is set to false, else it returns the output from running.
809
     */
810
    public function extendedInsert(string $strTableName, array $arrFields, $params, bool $blnToIgnore = false)
811
    {
812
        $this->_sql = null;
813
        $this->_queryType = self::EXTENDED_INSERT;
814
815
        if (! is_null($strTableName) && is_string($strTableName)) {
0 ignored issues
show
introduced by
The condition ! is_null($strTableName)...s_string($strTableName) can never be false.
Loading history...
816
            $this->_sql = "INSERT " . ($blnToIgnore ? "IGNORE " : "") . "INTO $strTableName " . "(`" . implode("`,`", $arrFields) . "`)";
817
        } else {
818
            throw new Error("Table name is invalid");
819
        }
820
821
        if (is_array($params) && count($params)) {
822
            $this->_sql .= " VALUES ";
823
            if (isset($params[0]) && is_array($params[0])) {
824
                foreach ($params as $p) {
825
                    if (count($p) != count($arrFields)) {
826
                        $this->_logger->emergency("Inconsistent number of fields to values in extended_insert", [
827
                            $p
828
                        ]);
829
                        throw new Error("Inconsistent number of fields in fields and values in extended_insert " . print_r($p, true));
830
                    }
831
                    $this->_sql .= "(" . implode(",", array_map([
832
                        $this,
833
                        '_escape'
834
                    ], array_values($p))) . ")";
835
836
                    if ($p != end($params)) {
837
                        $this->_sql .= ",";
838
                    }
839
                }
840
            }
841
        }
842
843
        if (self::$autorun) {
844
            return $this->execute(MYSQLI_BOTH);
845
        }
846
847
        return $this->_sql;
848
    }
849
850
    /**
851
     * Build a statement to update a table
852
     *
853
     * @param string $strTableName
854
     *            The table name to update
855
     * @param array $arrParams
856
     *            Name/value pairs of the field name and value
857
     * @param array $arrWhere
858
     *            [optional]
859
     *            Two-dimensional array to create where clause
860
     * @param array $arrFlags
861
     *            [optional]
862
     *            Two-dimensional array to create other flag options (joins, order, and group)
863
     *
864
     * @see Database::where()
865
     * @see Database::flags()
866
     *
867
     * @return NULL|string
868
     */
869
    public function update(string $strTableName, array $arrParams, array $arrWhere = [], array $arrFlags = [])
870
    {
871
        $this->_sql = "UPDATE ";
872
        $this->_queryType = self::UPDATE;
873
874
        if (! is_null($strTableName) && is_string($strTableName)) {
0 ignored issues
show
introduced by
The condition ! is_null($strTableName)...s_string($strTableName) can never be false.
Loading history...
875
            $this->_sql .= $strTableName;
876
877
            if (isset($arrFlags['joins']) && is_array($arrFlags['joins'])) {
878
                $this->_sql .= " " . implode(" ", $arrFlags['joins']);
879
                unset($arrFlags['joins']);
880
            }
881
882
            $this->_sql .= " SET ";
883
        } else {
884
            throw new Error("Table name is invalid");
885
        }
886
887
        if (is_array($arrParams) && count($arrParams)) {
888
            foreach ($arrParams as $f => $p) {
889
                if ((strpos($f, "`") === false) && (strpos($f, ".") === false) && (strpos($f, "*") === false) && (stripos($f, " as ") === false)) {
890
                    $f = "`{$f}`";
891
                }
892
893
                if (! is_null($p)) {
894
                    $this->_sql .= "$f={$this->_escape($p)},";
895
                } else {
896
                    $this->_sql .= "$f=NULL,";
897
                }
898
            }
899
        } else {
900
            throw new Error("No fields to update");
901
        }
902
903
        $this->_sql = substr($this->_sql, 0, - 1);
904
905
        if (! is_null($arrWhere) && is_array($arrWhere) && count($arrWhere)) {
906
            $where_str = " WHERE";
907
            foreach ($arrWhere as $x => $w) {
908
                $where_str .= $this->parseClause($w, $x);
909
            }
910
            if (strlen($where_str) > strlen(" WHERE")) {
911
                $this->_sql .= $where_str;
912
            }
913
        }
914
915
        if (! is_null($arrFlags) && is_array($arrFlags) && count($arrFlags)) {
916
            $this->_sql .= $this->flags($arrFlags);
917
        }
918
919
        if (self::$autorun) {
920
            return $this->execute(MYSQLI_BOTH);
921
        }
922
923
        return $this->_sql;
924
    }
925
926
    /**
927
     * Function to offer an extended updated functionality by using two different tables.
928
     *
929
     * @param string $to_be_updated
930
     *            The table that you want to update (alias 'tbu' is automatically added)
931
     * @param string $original
932
     *            The table with the data you want to overwrite to_be_updated table (alias 'o' is automatically added)
933
     * @param string $using
934
     *            The common index value between them that will join the fields
935
     * @param array|string $params
936
     *            If string only a single field is updated (tbu.$params = o.$params)
937
     *            If array each element in the array is a field to be updated (tbu.$param = o.$param)
938
     *
939
     * @return mixed
940
     */
941
    public function extendedUpdate(string $strTableToUpdate, string $strOriginalTable, string $strLinkField, $params)
942
    {
943
        $this->_sql = "UPDATE ";
944
        $this->_queryType = self::EXTENDED_UPDATE;
945
946
        if (! is_null($strTableToUpdate) && ! is_null($strOriginalTable) && ! is_null($strLinkField)) {
0 ignored issues
show
introduced by
The condition ! is_null($strTableToUpd... is_null($strLinkField) can never be false.
Loading history...
947
            $this->_sql .= "$strTableToUpdate tbu INNER JOIN $strOriginalTable o USING ($strLinkField) SET ";
948
        } else {
949
            throw new Error("Missing necessary fields");
950
        }
951
952
        if (is_array($params) && count($params)) {
953
            foreach ($params as $param) {
954
                if ($param != $strLinkField) {
955
                    $this->_sql .= "tbu.`$param` = o.`$param`,";
956
                }
957
            }
958
            $this->_sql = substr($this->_sql, 0, - 1);
959
        } elseif (is_string($params)) {
960
            $this->_sql .= "tbu.`$params` = o.`$params`";
961
        } else {
962
            throw new Exception("Do not understand datatype " . gettype($params), E_ERROR);
963
        }
964
965
        if (self::$autorun) {
966
            return $this->execute(MYSQLI_BOTH);
967
        }
968
969
        return $this->_sql;
970
    }
971
972
    /**
973
     * Function to build a replace query
974
     *
975
     * @param string $strTableName
976
     *            The table to update
977
     * @param array $arrParams
978
     *            Name/value pair to insert
979
     *
980
     * @return NULL|string
981
     */
982
    public function replace(string $strTableName, array $arrParams)
983
    {
984
        $this->_sql = null;
985
        $this->_queryType = self::REPLACE;
986
987
        if (! is_null($strTableName) && is_string($strTableName)) {
0 ignored issues
show
introduced by
The condition ! is_null($strTableName)...s_string($strTableName) can never be false.
Loading history...
988
            $this->_sql = "REPLACE INTO $strTableName " . "(`" . implode("`,`", array_keys($arrParams)) . "`)";
989
        } else {
990
            throw new Error("Table name is invalid");
991
        }
992
993
        $this->_sql .= " VALUES (" . implode(",", array_map([
994
            $this,
995
            '_escape'
996
        ], array_values($arrParams))) . ")";
997
998
        if (self::$autorun) {
999
            return $this->execute(MYSQLI_BOTH);
1000
        }
1001
1002
        return $this->_sql;
1003
    }
1004
1005
    /**
1006
     * Function to build an extended replace statement
1007
     *
1008
     * @param string $strTableName
1009
     *            Table name to update
1010
     * @param array $arrFields
1011
     *            Array of fields
1012
     * @param array $arrParams
1013
     *            Two-dimensional array of values
1014
     *
1015
     * @return NULL|string
1016
     */
1017
    public function extendedReplace(string $strTableName, array $arrFields, array $arrParams)
1018
    {
1019
        $this->_sql = null;
1020
        $this->_queryType = self::EXTENDED_REPLACE;
1021
1022
        if (! is_array($arrFields) || ! count($arrFields)) {
1023
            throw new Exception("Error with the field type");
1024
        }
1025
1026
        if (! is_null($strTableName) && is_string($strTableName)) {
0 ignored issues
show
introduced by
The condition ! is_null($strTableName)...s_string($strTableName) can never be false.
Loading history...
1027
            $this->_sql = "REPLACE INTO $strTableName " . "(`" . implode("`,`", $arrFields) . "`)";
1028
        } else {
1029
            throw new Error("Table name is invalid");
1030
        }
1031
1032
        if (is_array($arrParams) && count($arrParams)) {
1033
            $this->_sql .= " VALUES ";
1034
            foreach ($arrParams as $p) {
1035
                $this->_sql .= "(" . implode(",", array_map([
1036
                    $this,
1037
                    '_escape'
1038
                ], array_values($p))) . ")";
1039
1040
                if ($p != end($arrParams)) {
1041
                    $this->_sql .= ",";
1042
                }
1043
            }
1044
        }
1045
1046
        if (self::$autorun) {
1047
            return $this->execute(MYSQLI_BOTH);
1048
        }
1049
1050
        return $this->_sql;
1051
    }
1052
1053
    /**
1054
     * Function to build a delete statement
1055
     *
1056
     * @param string $strTableName
1057
     *            Table name to act on
1058
     * @param array $arrFields
1059
     *            [optional]
1060
     *            Optional list of fields to delete (used when including multiple tables)
1061
     * @param array $arrWhere
1062
     *            [optional]
1063
     *            Optional 2-dimensional array to build where clause from
1064
     * @param array $arrJoins
1065
     *            [optional]
1066
     *            Optional 2-dimensional array to add other flags
1067
     *
1068
     * @see Database::where()
1069
     * @see Database::flags()
1070
     *
1071
     * @return string|NULL
1072
     */
1073
    public function delete(string $strTableName, array $arrFields = [], array $arrWhere = [], array $arrJoins = [])
1074
    {
1075
        $this->_sql = "DELETE";
1076
        $this->_queryType = self::DELETE;
1077
1078
        $this->_logger->debug("Deleting table data");
1079
1080
        if (! is_null($arrFields) && is_array($arrFields) && count($arrFields)) {
1081
            $this->_sql .= " " . implode(",", $arrFields);
1082
        }
1083
1084
        if (! is_null($strTableName) && is_string($strTableName)) {
0 ignored issues
show
introduced by
The condition ! is_null($strTableName)...s_string($strTableName) can never be false.
Loading history...
1085
            $this->_sql .= " FROM $strTableName";
1086
        } else {
1087
            throw new Error("Table name is invalid");
1088
        }
1089
1090
        if (! is_null($arrJoins) && is_array($arrJoins) && count($arrJoins)) {
1091
            $this->_sql .= " " . implode(" ", $arrJoins);
1092
        }
1093
1094
        if (! is_null($arrWhere) && is_array($arrWhere) && count($arrWhere)) {
1095
            $where_str = " WHERE";
1096
            foreach ($arrWhere as $x => $w) {
1097
                $where_str .= $this->parseClause($w, $x);
1098
            }
1099
            if (strlen($where_str) > strlen(" WHERE")) {
1100
                $this->_sql .= $where_str;
1101
            }
1102
        }
1103
1104
        if (self::$autorun) {
1105
            return $this->execute(MYSQLI_BOTH);
1106
        }
1107
1108
        return $this->_sql;
1109
    }
1110
1111
    /**
1112
     * Function to build a drop table statement (automatically executes)
1113
     *
1114
     * @param string $strTableName
1115
     *            Table to drop
1116
     * @param string $strType
1117
     *            [optional]
1118
     *            Type of item to drop ('table', 'view') (defaulted to 'table')
1119
     * @param boolean $blnIsTemp
1120
     *            [optional]
1121
     *            Optional boolean if this is a temporary table
1122
     *
1123
     * @return string|NULL
1124
     */
1125
    public function drop(string $strTableName, string $strType = 'table', bool $blnIsTemp = false)
1126
    {
1127
        $this->_sql = null;
1128
        $this->_queryType = self::DROP;
1129
1130
        switch ($strType) {
1131
            case 'table':
1132
                $strType = 'TABLE';
1133
                break;
1134
            case 'view':
1135
                $strType = 'VIEW';
1136
                break;
1137
            default:
1138
                throw new Error("Invalid type " . gettype($strType), E_ERROR);
1139
        }
1140
1141
        if (! is_null($strTableName) && is_string($strTableName)) {
0 ignored issues
show
introduced by
The condition ! is_null($strTableName)...s_string($strTableName) can never be false.
Loading history...
1142
            $this->_sql = "DROP" . ($blnIsTemp ? " TEMPORARY" : "") . " $strType IF EXISTS `{$strTableName}`";
1143
        } else {
1144
            throw new Error("Table name is invalid");
1145
        }
1146
1147
        if (self::$autorun) {
1148
            return $this->execute(MYSQLI_BOTH);
1149
        }
1150
1151
        return $this->_sql;
1152
    }
1153
1154
    /**
1155
     * Function to build a truncate table statement (automatically executes)
1156
     *
1157
     * @param string $strTableName
1158
     *            Table to truncate
1159
     *
1160
     * @throws \Error
1161
     *
1162
     * @return string|NULL
1163
     */
1164
    public function truncate(string $strTableName)
1165
    {
1166
        $this->_sql = null;
1167
        $this->_queryType = self::TRUNCATE;
1168
1169
        if (! is_null($strTableName) && is_string($strTableName)) {
0 ignored issues
show
introduced by
The condition ! is_null($strTableName)...s_string($strTableName) can never be false.
Loading history...
1170
            $this->_sql = "TRUNCATE TABLE $strTableName";
1171
        } else {
1172
            throw new Error("Table name is invalid");
1173
        }
1174
1175
        if (self::$autorun) {
1176
            return $this->execute(MYSQLI_BOTH);
1177
        }
1178
1179
        return $this->_sql;
1180
    }
1181
1182
    /**
1183
     * Function to build a create temporary table statement
1184
     *
1185
     * @param string $strTableName
1186
     *            Name to give the table when creating
1187
     * @param boolean $blnIsTemp
1188
     *            [optional]
1189
     *            Optional boolean to make the table a temporary table
1190
     * @param mixed $strSelect
1191
     *            [optional]
1192
     *            Optional parameter if null uses last built statement
1193
     *            If string, will be made the SQL statement executed to create the table
1194
     *            If array, 2-dimensional array with "field", "datatype" values to build table fields
1195
     *
1196
     * @return NULL|string
1197
     */
1198
    public function createTable(string $strTableName, bool $blnIsTemp = false, $strSelect = null)
1199
    {
1200
        $this->_queryType = self::CREATE_TABLE;
1201
1202
        if (is_null($strSelect) && ! is_null($this->_sql) && substr($this->_sql, 0, 6) == 'SELECT') {
1203
            $this->_sql = "CREATE" . ($blnIsTemp ? " TEMPORARY" : "") . " TABLE IF NOT EXISTS $strTableName AS ($this->_sql)";
1204
        } elseif (! is_null($strTableName) && is_string($strTableName) && is_string($strSelect)) {
1205
            $this->_sql = "CREATE" . ($blnIsTemp ? " TEMPORARY" : "") . " TABLE IF NOT EXISTS $strTableName AS ($strSelect)";
1206
        } elseif (! is_null($strTableName) && is_string($strTableName) && is_array($strSelect)) {
1207
            $this->_sql = "CREATE" . ($blnIsTemp ? " TEMPORARY" : "") . " TABLE IF NOT EXISTS $strTableName (";
1208
1209
            foreach ($strSelect as $field) {
1210
                $default = null;
1211
                if (isset($field['default'])) {
1212
                    $default = (is_null($field['default']) ? "" : " DEFAULT '{$field['default']}'");
1213
                }
1214
                $this->_sql .= "`{$field['field']}` {$field['datatype']}" . $default . (isset($field['option']) ? " {$field['option']}" : '') . ",";
1215
            }
1216
            $this->_sql = substr($this->_sql, 0, - 1) . ")";
1217
        }
1218
1219
        if (self::$autorun) {
1220
            return $this->execute();
1221
        }
1222
1223
        return $this->_sql;
1224
    }
1225
1226
    /**
1227
     * Function to create a table using a stdClass object derived from JSON
1228
     *
1229
     * @param \stdClass $json
1230
     *
1231
     * @example /examples/create_table_json.json
1232
     *
1233
     */
1234
    public function createTableJson($json)
1235
    {
1236
        $this->_queryType = self::CREATE_TABLE;
1237
        $this->_c->select_db($json->schema);
1238
1239
        $this->_sql = "CREATE TABLE IF NOT EXISTS `{$json->name}` (";
1240
        foreach ($json->fields as $field) {
1241
            $this->_sql .= "`{$field->name}` {$field->dataType}";
1242
1243
            if ($field->dataType == 'enum') {
1244
                $this->_sql .= "('" . implode("','", $field->values) . "')";
1245
            }
1246
1247
            if ($field->ai) {
1248
                $this->_sql .= " AUTO_INCREMENT";
1249
            }
1250
1251
            if ($field->nn) {
1252
                $this->_sql .= " NOT NULL";
1253
            } else {
1254
                if ($field->default === null) {
1255
                    $this->_sql .= " DEFAULT NULL";
1256
                } elseif (strlen($field->default)) {
1257
                    $this->_sql .= " DEFAULT '{$field->default}'";
1258
                }
1259
            }
1260
1261
            if ($field != end($json->fields)) {
1262
                $this->_sql .= ",";
1263
            }
1264
        }
1265
1266
        if (isset($json->index) && count($json->index)) {
1267
            foreach ($json->index as $ind) {
1268
                $this->_sql .= ", " . strtoupper($ind->type) . " `{$ind->id}` (`{$ind->ref}`)";
1269
            }
1270
        }
1271
1272
        if (isset($json->constraints) && count($json->constraints)) {
1273
            foreach ($json->constraints as $con) {
1274
                $this->_sql .= ", CONSTRAINT `{$con->id}` " . "FOREIGN KEY (`{$con->local}`) " . "REFERENCES `{$con->schema}`.`{$con->table}` (`{$con->field}`) " . "ON DELETE " . (is_null($con->delete) ? "NO ACTION" : strtoupper($con->delete)) . " " . "ON UPDATE " . (is_null($con->update) ? "NO ACTION" : strtoupper($con->update));
1275
            }
1276
        }
1277
1278
        if (isset($json->unique) && count($json->unique)) {
1279
            $this->_sql .= ", UNIQUE(`" . implode("`,`", $json->unique) . "`)";
1280
        }
1281
1282
        if (isset($json->primary_key) && count($json->primary_key)) {
1283
            $this->_sql .= ", PRIMARY KEY(`" . implode("`,`", $json->primary_key) . "`))";
1284
        } else {
1285
            if (substr($this->_sql, - 1) == ',') {
1286
                $this->_sql = substr($this->_sql, 0, - 1);
1287
            }
1288
1289
            $this->_sql .= ")";
1290
        }
1291
1292
        $this->execute(MYSQLI_BOTH);
1293
    }
1294
1295
    /**
1296
     * Function to alter a existing table
1297
     *
1298
     * @param string $strTableName
1299
     *            Table to alter
1300
     * @param int $intAction
1301
     *            What action should be taken ('add-column', 'drop-column', 'modify-column')
1302
     * @param mixed $params
1303
     *            For add column this is a stdClass object that has the same elements as the example json
1304
     *
1305
     * @return mixed
1306
     */
1307
    public function alterTable(string $strTableName, int $intAction, $params)
1308
    {
1309
        $this->_queryType = self::ALTER_TABLE;
1310
        $this->_sql = "ALTER TABLE $strTableName";
1311
        if ($intAction == self::ADD_COLUMN) {
1312
            $nn = ($params->nn ? " NOT NULL" : "");
1313
            $default = null;
1314
            if ($params->default === null) {
1315
                $default = " DEFAULT NULL";
1316
            } elseif (strlen($params->default)) {
1317
                $default = " DEFAULT {$this->_escape($params->default)}";
1318
            }
1319
            $this->_sql .= " ADD COLUMN `{$params->name}` {$params->dataType}" . $nn . $default;
1320
        } elseif ($intAction == self::DROP_COLUMN) {
1321
            $this->_sql .= " DROP COLUMN ";
1322
            foreach ($params as $col) {
1323
                $this->_sql .= "`{$col->name}`";
1324
1325
                if ($col != end($params)) {
1326
                    $this->_sql .= ",";
1327
                }
1328
            }
1329
        } elseif ($intAction == self::MODIFY_COLUMN) {
1330
            $this->_sql .= " MODIFY COLUMN";
1331
            $nn = ($params->nn ? " NOT NULL" : "");
1332
            $default = null;
1333
            if ($params->default === null) {
1334
                $default = " DEFAULT NULL";
1335
            } elseif (strlen($params->default)) {
1336
                $default = " DEFAULT {$this->_escape($params->default)}";
1337
            }
1338
            $this->_sql .= " `{$params->name}` `{$params->new_name}` {$params->dataType}" . $nn . $default;
1339
        }
1340
1341
        if (self::$autorun) {
1342
            return $this->execute();
1343
        }
1344
1345
        return $this->_sql;
1346
    }
1347
1348
    /**
1349
     * Check to see if a field in a table exists
1350
     *
1351
     * @param string $strTableName
1352
     *            Table to check
1353
     * @param string $strFieldName
1354
     *            Field name to find
1355
     *
1356
     * @return boolean Returns TRUE if field is found in that schema and table, otherwise FALSE
1357
     */
1358
    public function fieldExists(string $strTableName, string $strFieldName)
1359
    {
1360
        $fdata = $this->fieldData($strTableName);
1361
1362
        if (is_array($fdata) && count($fdata)) {
1363
            foreach ($fdata as $field) {
1364
                if ($field->name == $strFieldName) {
1365
                    return true;
1366
                }
1367
            }
1368
        }
1369
1370
        return false;
1371
    }
1372
1373
    /**
1374
     * Function to get the column data (datatype, flags, defaults, etc)
1375
     *
1376
     * @param string $strTableName
1377
     *            Table to query
1378
     * @param mixed $field
1379
     *            [optional]
1380
     *            Optional field to retrieve data (if null, returns data from all fields)
1381
     *
1382
     * @return array
1383
     */
1384
    public function fieldData(string $strTableName, $field = null)
1385
    {
1386
        if (is_null($field)) {
1387
            $res = $this->_c->query("SELECT * FROM $strTableName LIMIT 1");
1388
        } elseif (is_array($field)) {
1389
            $res = $this->_c->query("SELECT `" . implode("`,`", $field) . "` FROM $strTableName LIMIT 1");
1390
        } elseif (is_string($field)) {
1391
            $res = $this->_c->query("SELECT $field FROM $strTableName LIMIT 1");
1392
        } else {
1393
            return null;
1394
        }
1395
1396
        $fields = null;
1397
        if (is_a($res, 'mysqli_result')) {
1398
            $fields = $res->fetch_fields();
1399
            foreach ($fields as $i => $f) {
1400
                $fields["{$f->name}"] = $f;
1401
                unset($fields[$i]);
1402
            }
1403
        }
1404
1405
        return $fields;
1406
    }
1407
1408
    /**
1409
     * Function to check that all field parameters are set correctly
1410
     *
1411
     * @param object $field_data
1412
     * @param object $check
1413
     * @param array $pks
1414
     * @param object $index
1415
     *
1416
     * @return array|string
1417
     */
1418
    public function fieldCheck($field_data, $check, $pks, $index)
1419
    {
1420
        $default = null;
1421
        $ret = null;
1422
1423
        $nn = ($check->nn ? " NOT NULL" : null);
1424
        if ($check->default === null) {
1425
            $default = " DEFAULT NULL";
1426
        } elseif (strlen($check->default)) {
1427
            $default = " DEFAULT '{$check->default}'";
1428
        }
1429
1430
        if ($field_data->type != $check->type && $check->type != MYSQLI_TYPE_ENUM) {
1431
            $this->_logger->notice("Wrong datatype", [
1432
                'name' => $field_data->name,
1433
                'datatype' => $check->dataType
1434
            ]);
1435
            $ret = " CHANGE COLUMN `{$field_data->name}` `{$check->name}` {$check->dataType}" . "{$nn}{$default}";
1436
        } elseif (! is_null($check->length) && $field_data->length != $check->length) {
1437
            $this->_logger->notice("Incorrect size", [
1438
                'name' => $field_data->name,
1439
                'current' => $field_data->length,
1440
                'new_size' => $check->length
1441
            ]);
1442
            $ret = " CHANGE COLUMN `{$field_data->name}` `{$check->name}` {$check->dataType}" . "{$nn}{$default}";
1443
        } elseif ($check->type == MYSQLI_TYPE_ENUM && ! ($field_data->flags & MYSQLI_ENUM_FLAG)) {
1444
            $this->_logger->notice("Setting ENUM type", [
1445
                'name' => $field_data->name,
1446
                'values' => implode(",", $check->values)
1447
            ]);
1448
            $ret = " CHANGE COLUMN `{$field_data->name}` `{$check->name}` {$check->dataType}('" . implode("','", $check->values) . "')" . "{$nn}{$default}";
1449
        }
1450
1451
        if (! is_null($index) && count($index)) {
1452
            foreach ($index as $ind) {
1453
                if ($check->name == $ind->ref && ! ($field_data->flags & MYSQLI_MULTIPLE_KEY_FLAG)) {
1454
                    $this->_logger->name("Missing index", [
0 ignored issues
show
Bug introduced by
The method name() does not exist on Monolog\Logger. ( Ignorable by Annotation )

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

1454
                    $this->_logger->/** @scrutinizer ignore-call */ 
1455
                                    name("Missing index", [

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
1455
                        'name' => $field_data->name
1456
                    ]);
1457
                    $ret .= ($ret ? "," : "") . " ADD INDEX `{$ind->id}` (`{$ind->ref}` ASC)";
1458
                }
1459
            }
1460
        }
1461
1462
        if (in_array($check->name, $pks) && ! ($field_data->flags & MYSQLI_PRI_KEY_FLAG)) {
1463
            $this->_logger->debug("Setting PKs", [
1464
                'keys' => implode(',', $pks)
1465
            ]);
1466
            $ret .= ($ret ? "," : "") . " DROP PRIMARY KEY, ADD PRIMARY KEY(`" . implode("`,`", $pks) . "`)";
1467
        }
1468
1469
        return $ret;
1470
    }
1471
1472
    /**
1473
     * Function to check for the existence of a table within a schema
1474
     *
1475
     * @param string $strSchema
1476
     *            The schema to search in
1477
     * @param string $strTableName
1478
     *            Table to search for
1479
     *
1480
     * @return integer|boolean Returns number of tables that match if table is found in that schema, otherwise FALSE
1481
     */
1482
    public function tableExists(string $strSchema, string $strTableName)
1483
    {
1484
        if (! $this->_c->select_db($strSchema)) {
1485
            fwrite(STDOUT, $this->_c->error . PHP_EOL);
1486
        }
1487
        $sql = "SHOW TABLES LIKE '{$strTableName}'";
1488
1489
        if ($res = $this->_c->query($sql)) {
1490
            if (gettype($res) == 'object' && is_a($res, 'mysqli_result') && $res->num_rows) {
1491
                return $res->num_rows;
1492
            }
1493
        } else {
1494
            if ($this->_c->errno) {
1495
                fwrite(STDOUT, $this->_c->error . PHP_EOL);
1496
            }
1497
        }
1498
1499
        return false;
1500
    }
1501
1502
    /**
1503
     * Function to detect if string is a JSON object or not
1504
     *
1505
     * @param string $strVal
1506
     *
1507
     * @return boolean
1508
     */
1509
    public function isJson(string $strVal)
1510
    {
1511
        json_decode($strVal);
1512
        return (json_last_error() == JSON_ERROR_NONE);
1513
    }
1514
1515
    /**
1516
     * Function to escape SQL characters to prevent SQL injection
1517
     *
1518
     * @param mixed $val
1519
     *            Value to escape
1520
     * @param boolean $blnEscape
1521
     *            Decide if we should escape or not
1522
     *
1523
     * @return string Escaped value
1524
     */
1525
    public function _escape($val, bool $blnEscape = true)
1526
    {
1527
        if (is_null($val) || (is_string($val) && strtolower($val) == 'null')) {
1528
            return 'NULL';
1529
        } elseif (is_numeric($val) || is_string($val)) {
1530
            if ($blnEscape) {
1531
                return "'{$this->_c->real_escape_string($val)}'";
1532
            }
1533
            return $val;
1534
        } elseif (is_a($val, 'DateTime')) {
1535
            return "'{$val->format(MYSQL_DATETIME)}'";
1536
        } elseif (is_bool($val)) {
1537
            return $val ? "'1'" : "'0'";
1538
        } elseif (gettype($val) == 'object' && method_exists($val, '_escape')) {
1539
            $ret = call_user_func([
1540
                $val,
1541
                '_escape'
1542
            ]);
1543
            if ($ret !== false) {
1544
                return $ret;
1545
            } else {
1546
                throw new Exception("Error in return from _escape method in " . get_class($val), E_ERROR);
1547
            }
1548
        } elseif (gettype($val) == 'object') {
1549
            $this->_logger->error("Unknown object to escape " . get_class($val) . " in SQL string {$this->_sql}");
1550
            return;
1551
        }
1552
1553
        throw new Exception("Unknown datatype to escape in SQL string {$this->_sql} " . gettype($val), E_ERROR);
1554
    }
1555
1556
    /**
1557
     * Function to retrieve all results
1558
     *
1559
     * @param int $resulttype
1560
     *
1561
     * @return mixed
1562
     */
1563
    public function fetchAll(int $intResultType = MYSQLI_ASSOC)
1564
    {
1565
        $res = [];
1566
        if (method_exists('mysqli_result', 'fetch_all')) { // Compatibility layer with PHP < 5.3
1567
            $res = $this->_result->fetch_all($intResultType);
1568
        } else {
1569
            while ($tmp = $this->_result->fetch_array($intResultType)) {
1570
                $res[] = $tmp;
1571
            }
1572
        }
1573
1574
        return $res;
1575
    }
1576
1577
    /**
1578
     * Function to populate the fields for the SQL
1579
     *
1580
     * @param array|string $fields
1581
     *            [optional]
1582
     *            Optional array of fields to string together to create a field list
1583
     *
1584
     * @return string
1585
     */
1586
    public function fields($fields = null)
1587
    {
1588
        $ret = null;
1589
1590
        if (is_array($fields) && count($fields) && isset($fields[0]) && is_string($fields[0])) {
1591
            foreach ($fields as $field) {
1592
                if ((strpos($field, '`') === false) && (strpos($field, '.') === false) && (strpos($field, '*') === false) && (strpos($field, 'JSON_') === false) && (stripos($field, ' as ') === false)) {
1593
                    $ret .= "`$field`,";
1594
                } else {
1595
                    $ret .= "$field,";
1596
                }
1597
            }
1598
            $ret = substr($ret, - 1) == ',' ? substr($ret, 0, - 1) : $ret;
1599
        } elseif (is_string($fields)) {
1600
            $ret = $fields;
1601
        } elseif (is_null($fields)) {
1602
            $ret = "*";
1603
        } else {
1604
            throw new \InvalidArgumentException("Invalid field type");
1605
        }
1606
1607
        return $ret;
1608
    }
1609
1610
    /**
1611
     * Function to parse the flags
1612
     *
1613
     * @param array $flags
1614
     *            Two-dimensional array to added flags
1615
     *
1616
     *            <code>
1617
     *            [
1618
     *            &nbsp;&nbsp;'joins' => [
1619
     *            &nbsp;&nbsp;&nbsp;&nbsp;"JOIN table2 t2 ON t2.id=t1.id"
1620
     *            &nbsp;&nbsp;],
1621
     *            &nbsp;&nbsp;'group' => 'field',
1622
     *            &nbsp;&nbsp;'having' => 'field',
1623
     *            &nbsp;&nbsp;'order' => 'field',
1624
     *            &nbsp;&nbsp;'start' => 0,
1625
     *            &nbsp;&nbsp;'limit' => 0
1626
     *            ]
1627
     *            </code>
1628
     *
1629
     * @see Database::groups()
1630
     * @see Database::having()
1631
     * @see Database::order()
1632
     *
1633
     * @return string
1634
     */
1635
    public function flags(array $arrFlags)
1636
    {
1637
        $ret = '';
1638
1639
        if (isset($arrFlags['group'])) {
1640
            $ret .= $this->groups($arrFlags['group']);
1641
        }
1642
1643
        if (isset($arrFlags['having']) && is_array($arrFlags['having'])) {
1644
            $having = " HAVING";
1645
            foreach ($arrFlags['having'] as $x => $h) {
1646
                $having .= $this->parseClause($h, $x);
1647
            }
1648
            if (strlen($having) > strlen(" HAVING")) {
1649
                $ret .= $having;
1650
            }
1651
        }
1652
1653
        if (isset($arrFlags['order'])) {
1654
            $ret .= $this->order($arrFlags['order']);
1655
        }
1656
1657
        if (isset($arrFlags['limit']) && (is_string($arrFlags['limit']) || is_numeric($arrFlags['limit']))) {
1658
            $ret .= " LIMIT ";
1659
            if (isset($arrFlags['start']) && (is_string($arrFlags['start']) || is_numeric($arrFlags['start']))) {
1660
                $ret .= "{$arrFlags['start']},";
1661
            }
1662
            $ret .= "{$arrFlags['limit']}";
1663
        }
1664
1665
        return $ret;
1666
    }
1667
1668
    /**
1669
     * Function to parse SQL GROUP BY statements
1670
     *
1671
     * @param mixed $groups
1672
     *
1673
     * @return string
1674
     */
1675
    public function groups($groups)
1676
    {
1677
        $ret = '';
1678
        if (is_array($groups) && count($groups)) {
1679
            $ret .= " GROUP BY";
1680
1681
            foreach ($groups as $grp) {
1682
                $ret .= " $grp";
1683
1684
                if ($grp != end($groups)) {
1685
                    $ret .= ",";
1686
                }
1687
            }
1688
        } elseif (is_string($groups)) {
1689
            $ret .= " GROUP BY {$groups}";
1690
        } else {
1691
            throw (new Exception("Error in datatype for groups " . gettype($groups), E_ERROR));
1692
        }
1693
1694
        return $ret;
1695
    }
1696
1697
    /**
1698
     * Function to parse SQL ORDER BY statements
1699
     *
1700
     * @param mixed $order
1701
     *
1702
     * @return string
1703
     */
1704
    public function order($order)
1705
    {
1706
        $ret = '';
1707
        if (is_array($order)) {
1708
            $ret .= " ORDER BY";
1709
1710
            foreach ($order as $ord) {
1711
                $ret .= " {$ord['field']} {$ord['sort']}";
1712
1713
                if ($ord != end($order)) {
1714
                    $ret .= ",";
1715
                }
1716
            }
1717
        } elseif (is_string($order)) {
1718
            $ret .= " ORDER BY {$order}";
1719
        }
1720
1721
        return $ret;
1722
    }
1723
1724
    /**
1725
     * Function to see if a constraint exists
1726
     *
1727
     * @param string $strConstraintId
1728
     *
1729
     * @return boolean
1730
     */
1731
    public function isConstraint(string $strConstraintId)
1732
    {
1733
        $res = $this->_c->query("SELECT * FROM information_schema.TABLE_CONSTRAINTS WHERE CONSTRAINT_NAME = '{$strConstraintId}'");
1734
1735
        if ($res->num_rows) {
1736
            return true;
1737
        }
1738
1739
        return false;
1740
    }
1741
1742
    /**
1743
     * Function to parse where and having clauses
1744
     *
1745
     * @param array $arrClause
1746
     * @param int $index
1747
     */
1748
    public function parseClause(array $arrClause, int $intIndex)
1749
    {
1750
        $ret = null;
1751
1752
        $this->_logger->debug("Parsing clause", $arrClause);
1753
1754
        if (! isset($arrClause['field']) && isset($arrClause['close-paren']) && $arrClause['close-paren']) {
1755
            $ret .= ")";
1756
            return $ret;
1757
        } elseif ($intIndex > 0 && ! isset($arrClause['sql_op'])) {
1758
            $this->_logger->warning("Missing sql_op field to identify how current and previous WHERE clause statements should be linked ('AND', 'OR', 'XOR', etc), skipped", [
1759
                'clause' => implode(",", $arrClause)
1760
            ]);
1761
            return;
1762
        }
1763
1764
        $op = '=';
1765
        if (isset($arrClause['op'])) {
1766
            $op = $arrClause['op'];
1767
        }
1768
1769
        switch ($op) {
1770
            case self::BETWEEN:
1771
                if (! isset($arrClause['field']) || ! isset($arrClause['low']) || ! isset($arrClause['high'])) {
1772
                    $this->_logger->warning("Missing field, low, or high for BETWEEN where clause, skipping");
1773
                    return;
1774
                }
1775
                break;
1776
            default:
1777
                if (! isset($arrClause['field']) || ! isset($arrClause['value'])) {
1778
                    $this->_logger->warning("Missing field or value for WHERE clause, skipping", $arrClause);
1779
                    return;
1780
                }
1781
        }
1782
1783
        if ($intIndex > 0) {
1784
            $ret .= " {$arrClause['sql_op']}";
1785
        }
1786
1787
        if (isset($arrClause['open-paren']) && $arrClause['open-paren']) {
1788
            $ret .= " (";
1789
        }
1790
1791
        if (isset($arrClause['backticks']) && ! $arrClause['backticks']) {
1792
            $field = $arrClause['field'];
1793
        } else {
1794
            $field = "`{$arrClause['field']}`";
1795
        }
1796
1797
        if ($op == self::IN || $op == self::NOT_IN) {
1798
            if (is_string($arrClause['value'])) {
1799
                $ret .= " {$field} {$op} " . (strpos($arrClause['value'], '(') !== false ? $arrClause['value'] : "({$arrClause['value']})");
1800
            } elseif (is_array($arrClause['value'])) {
1801
                $ret .= " {$field} {$op} (" . implode(",", array_map([
1802
                    $this,
1803
                    '_escape'
1804
                ], $arrClause['value'])) . ")";
1805
            } else {
1806
                $this->_logger->error("Invalid datatype for IN WHERE clause, only string and array allowed " . gettype($arrClause['value']), $arrClause);
1807
                throw new Exception("Invalid datatype for IN WHERE clause", E_ERROR);
1808
            }
1809
        } elseif ($op == self::BETWEEN) {
1810
            $ret .= " {$field} BETWEEN {$this->_escape($arrClause['low'])} AND {$this->_escape($arrClause['high'])}";
1811
        } else {
1812
            if (isset($arrClause['escape']) && ! $arrClause['escape']) {
1813
                $value = $arrClause['value'];
1814
            } else {
1815
                $value = $this->_escape($arrClause['value']);
1816
            }
1817
1818
            if (isset($arrClause['case_insensitive']) && $arrClause['case_insensitive']) {
1819
                $ret .= " LOWER({$field}) {$op} LOWER({$this->_escape($arrClause['value'])})";
1820
            } elseif (preg_match("/\(SELECT/", $arrClause['value'])) {
1821
                $ret .= " {$field} {$op} {$arrClause['value']}";
1822
            } else {
1823
                $ret .= " {$field} {$op} {$value}";
1824
            }
1825
        }
1826
1827
        if (isset($arrClause['close-paren']) && $arrClause['close-paren']) {
1828
            $ret .= ")";
1829
        }
1830
1831
        return $ret;
1832
    }
1833
}