Failed Conditions
Push — master ( 3e4eb0...ac3a1b )
by Ryan
12:47
created

src/Database.php (1 issue)

Labels
Severity
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 variable to store the type of query that is being run
207
     *
208
     * @var int
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}') {
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)
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->/** @scrutinizer ignore-call */
344
                setLevel($strLevel);
345
            $handles[] = $h;
346
        }
347
348
        $this->_logger->setHandlers($handles);
349
    }
350
351
    /**
352
     * Getter function for _queryType
353
     *
354
     * @return int
355
     */
356
    public function getQueryType()
357
    {
358
        return $this->_queryType;
359
    }
360
361
    /**
362
     * Setter function for _queryType
363
     *
364
     * @param int $qt
365
     */
366
    public function setQueryType(int $qt)
367
    {
368
        $this->_queryType = $qt;
369
    }
370
371
    /**
372
     * Getter function for _sql
373
     *
374
     * @return string
375
     */
376
    public function getSql()
377
    {
378
        return $this->_sql;
379
    }
380
381
    /**
382
     * Function to return the currently selected database schema
383
     *
384
     * @return string
385
     */
386
    public function getSchema()
387
    {
388
        if ($res = $this->_c->query("SELECT DATABASE()")) {
389
            $row = $res->fetch_row();
390
391
            $this->_logger->debug("Getting schema {$row[0]}");
392
            return $row[0];
393
        }
394
        return null;
395
    }
396
397
    /**
398
     * Function to set schema
399
     *
400
     * @param string $strSchema
401
     */
402
    public function setSchema(string $strSchema)
403
    {
404
        $this->_logger->debug("Setting schema to {$strSchema}");
405
        if (! $this->_c->select_db($strSchema)) {
406
            $this->_logger->emergency("Unknown schema {$strSchema}");
407
            return false;
408
        }
409
        return true;
410
    }
411
412
    /**
413
     * Method to set a MYSQL variable
414
     *
415
     * @param string $strName
416
     * @param string $strVal
417
     * @return boolean
418
     */
419
    public function setVar(string $strName, string $strVal)
420
    {
421
        if (! $strName || ! $strVal) {
422
            $this->_logger->debug("name or value are blank", [
423
                'name' => $strName,
424
                'value' => $strVal
425
            ]);
426
            return false;
427
        }
428
429
        $this->_logger->debug("Setting {$strName} = '{$strVal}'");
430
431
        if ($this->_c->real_query("SET $strName = {$this->_escape($strVal)}")) {
432
            return true;
433
        } else {
434
            $this->_logger->error("Failed to set variable {$this->_c->error}");
435
            return false;
436
        }
437
    }
438
439
    /**
440
     * Function to execute the statement
441
     *
442
     * @param mixed $return
443
     *            [optional]
444
     *            MYSQLI constant to control what is returned from the mysqli_result object
445
     * @param string $class
446
     *            [optional]
447
     *            Class to use when returning object
448
     * @param string $strSql
449
     *            [optional]
450
     *            Optional SQL query
451
     *
452
     * @throws \Exception
453
     * @throws \InvalidArgumentException
454
     *
455
     * @return mixed
456
     */
457
    public function execute($return = MYSQLI_ASSOC, $class = null, $strSql = null)
458
    {
459
        if (! is_null($strSql)) {
460
            $this->_sql = $strSql;
461
        }
462
463
        if (is_a($this->_c, 'mysqli')) {
464
            if (! $this->_c->ping()) {
465
                require_once 'DBConfig.php';
466
                $this->_c = null;
467
                $this->_c = new mysqli(PHP_DB_SERVER, PHP_DB_USER, PHP_DB_PWD, PHP_DB_SCHEMA);
468
            }
469
        } else {
470
            throw new \Error('Database was not connected', E_ERROR);
471
        }
472
473
        $this->_logger->info("Executing {$this->_queryType} query");
474
        $this->_logger->debug($this->_sql);
475
476
        try {
477
            if (in_array($this->_queryType, [
478
                self::SELECT,
479
                self::SELECT_COUNT
480
            ])) {
481
                $this->_result = $this->_c->query($this->_sql);
482
                if ($this->_c->error) {
483
                    $this->_logger->error("There is an error {$this->_c->error}");
484
                    throw new Exception("There was an error {$this->_c->error}", E_ERROR);
485
                }
486
            } else {
487
                $this->_result = $this->_c->real_query($this->_sql);
488
                if ($this->_c->errno) {
489
                    $this->_logger->error("There was an error {$this->_c->error}");
490
                    throw new Exception("There was an error {$this->_c->error}", E_ERROR);
491
                }
492
            }
493
494
            if ($return == MYSQLI_OBJECT && ! is_null($class) && class_exists(/** @scrutinizer ignore-type */$class)) {
495
                $this->_logger->debug("Checking results for query", [
496
                    'class' => get_class($class)
497
                ]);
498
                $this->_result = $this->checkResults($return, $class);
499
            } elseif ($return == MYSQLI_OBJECT && is_null($class)) {
500
                $this->_logger->debug("Checking results for query", [
501
                    'class' => 'stdClass'
502
                ]);
503
                $this->_result = $this->checkResults($return, 'stdClass');
504
            } else {
505
                $this->_logger->debug("Checking results for query and returning associative array");
506
                $this->_result = $this->checkResults(MYSQLI_ASSOC);
507
            }
508
        } catch (Exception $e) {}
509
510
        return $this->_result;
511
    }
512
513
    /**
514
     * Function to check the results and return what is expected
515
     *
516
     * @param mixed $returnType
517
     *            [optional]
518
     *            Optional return mysqli_result return type
519
     *
520
     * @return mixed
521
     */
522
    public function checkResults($returnType = MYSQLI_ASSOC, $class = null)
523
    {
524
        $res = null;
525
526
        switch ($this->_queryType) {
527
            case self::SELECT_COUNT:
528
                if (! is_a($this->_result, 'mysqli_result')) {
529
                    $this->_logger->error("Error with return on query");
530
                    return;
531
                }
532
533
                if ($this->_result->num_rows == 1) {
534
                    $row = $this->_result->fetch_assoc();
535
                    if (isset($row['count'])) {
536
                        $this->_logger->debug("Returning SELECT_COUNT query", [
537
                            'count' => $row['count']
538
                        ]);
539
                        $res = $row['count'];
540
                    }
541
                } elseif ($this->_result->num_rows > 1) {
542
                    $this->_logger->debug("Returning SELECT_COUNT query", [
543
                        'count' => $this->_result->num_rows
544
                    ]);
545
                    $res = $this->_result->num_rows;
546
                }
547
548
                mysqli_free_result($this->_result);
549
550
                return $res;
551
            case self::SELECT:
552
                if (! is_a($this->_result, 'mysqli_result')) {
553
                    $this->_logger->error("Error with return on query");
554
                    return;
555
                }
556
557
                if ($returnType == MYSQLI_OBJECT && ! is_null($class) && class_exists($class)) {
558
                    if ($this->_result->num_rows == 1) {
559
                        $this->_logger->debug("Returning object from SELECT query", [
560
                            'type' => get_class($class)
561
                        ]);
562
                        $res = $this->_result->fetch_object($class);
563
                    } elseif ($this->_result->num_rows > 1) {
564
                        $this->_logger->debug("Returning object array from SELECT query", [
565
                            'type' => get_class($class)
566
                        ]);
567
                        while ($row = $this->_result->fetch_object($class)) {
568
                            $res[] = $row;
569
                        }
570
                    }
571
                } else {
572
                    if ($this->_result->num_rows == 1) {
573
                        $this->_logger->debug("Fetching results");
574
                        $res = $this->_result->fetch_array($returnType);
575
                    } elseif ($this->_result->num_rows > 1) {
576
                        $this->_logger->debug("Fetching results array");
577
                        $res = $this->fetchAll($returnType);
578
                    }
579
                }
580
581
                mysqli_free_result($this->_result);
582
583
                return $res;
584
            case self::INSERT:
585
                if ($this->_c->error) {
586
                    $this->_logger->error("Database Error {$this->_c->error}");
587
                    return 0;
588
                }
589
590
                if ($this->_c->insert_id) {
591
                    $this->_logger->debug("Insert successful returning insert_id", [
592
                        'id' => $this->_c->insert_id
593
                    ]);
594
                    return $this->_c->insert_id;
595
                } elseif ($this->_c->affected_rows) {
596
                    $this->_logger->debug("Insert successful return affected row count", [
597
                        'count' => $this->_c->affected_rows
598
                    ]);
599
                    return $this->_c->affected_rows;
600
                }
601
602
                $this->_logger->debug("Insert successful, but no ID so returning 1 for success");
603
604
                return 1;
605
            // intentional fall through
606
            case self::EXTENDED_INSERT:
607
            // intentional fall through
608
            case self::EXTENDED_REPLACE:
609
            // intentional fall through
610
            case self::EXTENDED_UPDATE:
611
            // intentional fall through
612
            case self::REPLACE:
613
            // intentional fall through
614
            case self::UPDATE:
615
            // intentional fall through
616
            case self::DELETE:
617
            // intentional fall through
618
            case self::ALTER_TABLE:
619
                if ($this->_c->error) {
620
                    $this->_logger->error("Database Error {$this->_c->error}");
621
                    return false;
622
                } elseif ($this->_c->affected_rows) {
623
                    $this->_logger->debug("Returning affected row count for {$this->_queryType}", [
624
                        'count' => $this->_c->affected_rows
625
                    ]);
626
                    return $this->_c->affected_rows;
627
                } else {
628
                    return true;
629
                }
630
                break;
631
            case self::CREATE_TABLE:
632
            // intentional fall through
633
            case self::DROP:
634
            // intentional fall through
635
            case self::TRUNCATE:
636
                $this->_logger->debug("Returning from {$this->_queryType}");
637
                return true;
638
        }
639
    }
640
641
    /**
642
     * Function to pass through calling the query function (used for backwards compatibility and for more complex queries that aren't currently supported)
643
     * Nothing is escaped
644
     *
645
     * @param string $strSql
646
     *            [optional]
647
     *            Optional query to pass in and execute
648
     *
649
     * @return \mysqli_result|boolean
650
     */
651
    public function query($strSql = null)
652
    {
653
        if (is_null($strSql)) {
654
            return $this->_c->query($this->_sql);
655
        } else {
656
            return $this->_c->query($strSql);
657
        }
658
    }
659
660
    /**
661
     * A function to build a select query
662
     *
663
     * @param string $strTableName
664
     *            The table to query
665
     * @param array|string $fields
666
     *            [optional]
667
     *            Optional array of fields to return (defaults to '*')
668
     * @param array $arrWhere
669
     *            [optional]
670
     *            Optional 2-dimensional array to build where clause from
671
     * @param array $arrFlags
672
     *            [optional]
673
     *            Optional 2-dimensional array to allow other flags
674
     *
675
     * @see Database::where()
676
     * @see Database::flags()
677
     *
678
     * @throws \InvalidArgumentException
679
     *
680
     * @return mixed
681
     */
682
    public function select(string $strTableName, $fields = null, array $arrWhere = [], array $arrFlags = [])
683
    {
684
        $this->_sql = null;
685
        $this->_queryType = self::SELECT;
686
687
        if (! is_null($strTableName)) {
688
            $this->_logger->debug("Starting SELECT query of {$strTableName}", [
689
                'fields' => $this->fields($fields)
690
            ]);
691
            $this->_sql = "SELECT " . $this->fields($fields) . " FROM $strTableName";
692
        } else {
693
            $this->_logger->emergency("Table name is invalid or wrong type");
694
            throw new Error("Table name is invalid");
695
        }
696
697
        if (isset($arrFlags['joins']) && is_array($arrFlags['joins']) && count($arrFlags['joins'])) {
698
            $this->_logger->debug("Adding joins", [
699
                'joins' => implode(' ', $arrFlags['joins'])
700
            ]);
701
            $this->_sql .= " " . implode(" ", $arrFlags['joins']);
702
        } else {
703
            $this->_logger->debug("No joins");
704
        }
705
706
        if (! is_null($arrWhere) && is_array($arrWhere) && count($arrWhere)) {
707
            $where_str = " WHERE";
708
            $this->_logger->debug("Parsing where clause and adding to query");
709
            foreach ($arrWhere as $x => $w) {
710
                $where_str .= $this->parseClause($w, $x);
711
            }
712
            if (strlen($where_str) > strlen(" WHERE")) {
713
                $this->_sql .= $where_str;
714
            }
715
        }
716
717
        if (is_array($arrFlags) && count($arrFlags)) {
718
            $this->_logger->debug("Parsing flags and adding to query", $arrFlags);
719
            $this->_sql .= $this->flags($arrFlags);
720
        }
721
722
        if (self::$autorun) {
723
            return $this->execute(MYSQLI_BOTH);
724
        }
725
726
        return $this->_sql;
727
    }
728
729
    /**
730
     * Function to build a query to check the number of rows in a table
731
     *
732
     * @param string $strTableName
733
     *            The table to query
734
     * @param array $where
735
     *            [optional]
736
     *            Optional 2-dimensional array to build where clause
737
     * @param array $flags
738
     *            [optional]
739
     *            Optional 2-dimensional array to add flags
740
     *
741
     * @see Database::where()
742
     * @see Database::flags()
743
     *
744
     * @return string|NULL
745
     */
746
    public function selectCount(string $strTableName, array $arrWhere = [], array $arrFlags = [])
747
    {
748
        $this->_sql = null;
749
        $this->_queryType = self::SELECT_COUNT;
750
751
        if (! is_null($strTableName)) {
752
            $this->_sql = "SELECT COUNT(1) AS 'count' FROM $strTableName";
753
        } else {
754
            $this->_logger->emergency("Table name is invalid or wrong type");
755
            throw new Error("Table name is invalid");
756
        }
757
758
        if (isset($arrFlags['joins']) && is_array($arrFlags['joins'])) {
759
            $this->_sql .= " " . implode(" ", $arrFlags['joins']);
760
        }
761
762
        if (! is_null($arrWhere) && is_array($arrWhere) && count($arrWhere)) {
763
            $where_str = " WHERE";
764
            foreach ($arrWhere as $x => $w) {
765
                $where_str .= $this->parseClause($w, $x);
766
            }
767
            if (strlen($where_str) > strlen(" WHERE")) {
768
                $this->_sql .= $where_str;
769
            }
770
        }
771
772
        if (is_array($arrFlags) && count($arrFlags)) {
773
            $this->_sql .= $this->flags($arrFlags);
774
        }
775
776
        if (self::$autorun) {
777
            return $this->execute(MYSQLI_BOTH);
778
        }
779
780
        return $this->_sql;
781
    }
782
783
    /**
784
     * Function to build an insert query statement
785
     *
786
     * @param string $strTableName
787
     * @param array|string $params
788
     * @param boolean $to_ignore
789
     *
790
     * @return string|NULL
791
     */
792
    public function insert(string $strTableName, $params = null, bool $blnToIgnore = false)
793
    {
794
        $this->_sql = null;
795
        $this->_queryType = self::INSERT;
796
797
        if (! is_null($strTableName)) {
798
            $this->_sql = "INSERT" . ($blnToIgnore ? " IGNORE" : "") . " INTO $strTableName" . (is_array($params) && count($params) ? " (`" . implode("`,`", array_keys($params)) . "`)" : null);
799
        } else {
800
            throw new Error("Table name is invalid");
801
        }
802
803
        if (is_array($params) && count($params)) {
804
            $this->_sql .= " VALUES (" . implode(",", array_map([
805
                $this,
806
                '_escape'
807
            ], array_values($params))) . ")";
808
        } elseif (is_string($params) && stripos($params, 'SELECT') !== false) {
809
            $this->_sql .= " {$params}";
810
        } else {
811
            throw new Error("Invalid type passed to insert " . gettype($params));
812
        }
813
814
        if (self::$autorun) {
815
            return $this->execute(MYSQLI_BOTH);
816
        }
817
818
        return $this->_sql;
819
    }
820
821
    /**
822
     * Function to create an extended insert query statement
823
     *
824
     * @param string $strTableName
825
     *            The table name that the data is going to be inserted on
826
     * @param array $arrFields
827
     *            An array of field names that each value represents
828
     * @param array|string $params
829
     *            An array of array of values or a string with a SELECT statement to populate the insert with
830
     * @param boolean $blnToIgnore
831
     *            [optional]
832
     *            Boolean to decide if we need to use the INSERT IGNORE INTO syntax
833
     *
834
     * @return NULL|string Returns the SQL if self::$autorun is set to false, else it returns the output from running.
835
     */
836
    public function extendedInsert(string $strTableName, array $arrFields, $params, bool $blnToIgnore = false)
837
    {
838
        $this->_sql = null;
839
        $this->_queryType = self::EXTENDED_INSERT;
840
841
        if (! is_null($strTableName) && is_string($strTableName)) {
842
            $this->_sql = "INSERT " . ($blnToIgnore ? "IGNORE " : "") . "INTO $strTableName " . "(`" . implode("`,`", $arrFields) . "`)";
843
        } else {
844
            throw new Error("Table name is invalid");
845
        }
846
847
        if (is_array($params) && count($params)) {
848
            $this->_sql .= " VALUES ";
849
            if (isset($params[0]) && is_array($params[0])) {
850
                foreach ($params as $p) {
851
                    if (count($p) != count($arrFields)) {
852
                        $this->_logger->emergency("Inconsistent number of fields to values in extended_insert", [
853
                            $p
854
                        ]);
855
                        throw new Error("Inconsistent number of fields in fields and values in extended_insert " . print_r($p, true));
856
                    }
857
                    $this->sql .= "(" . implode(",", array_map([$this, '_escape'], array_values($p))) . "),";
0 ignored issues
show
The property sql does not exist on Godsgood33\Php_Db\Database. Did you mean _sql?
Loading history...
858
859
                    if ($p != end($params)) {
860
                        $this->_sql .= ",";
861
                    }
862
                }
863
            } else {
864
                $this->sql .= "(" . implode("),(", array_map([$this, '_escape'], array_values($params))) . ")";
865
            }
866
        }
867
868
        if (self::$autorun) {
869
            return $this->execute(MYSQLI_BOTH);
870
        }
871
872
        return $this->_sql;
873
    }
874
875
    /**
876
     * Build a statement to update a table
877
     *
878
     * @param string $strTableName
879
     *            The table name to update
880
     * @param array $arrParams
881
     *            Name/value pairs of the field name and value
882
     * @param array $arrWhere
883
     *            [optional]
884
     *            Two-dimensional array to create where clause
885
     * @param array $arrFlags
886
     *            [optional]
887
     *            Two-dimensional array to create other flag options (joins, order, and group)
888
     *
889
     * @see Database::where()
890
     * @see Database::flags()
891
     *
892
     * @return NULL|string
893
     */
894
    public function update(string $strTableName, array $arrParams, array $arrWhere = [], array $arrFlags = [])
895
    {
896
        $this->_sql = "UPDATE ";
897
        $this->_queryType = self::UPDATE;
898
899
        if (! is_null($strTableName) && is_string($strTableName)) {
900
            $this->_sql .= $strTableName;
901
902
            if (isset($arrFlags['joins']) && is_array($arrFlags['joins'])) {
903
                $this->_sql .= " " . implode(" ", $arrFlags['joins']);
904
                unset($arrFlags['joins']);
905
            }
906
907
            $this->_sql .= " SET ";
908
        } else {
909
            throw new Error("Table name is invalid");
910
        }
911
912
        if (is_array($arrParams) && count($arrParams)) {
913
            foreach ($arrParams as $f => $p) {
914
                if ((strpos($f, "`") === false) && (strpos($f, ".") === false) && (strpos($f, "*") === false) && (stripos($f, " as ") === false)) {
915
                    $f = "`{$f}`";
916
                }
917
918
                if (! is_null($p)) {
919
                    $this->_sql .= "$f={$this->_escape($p)},";
920
                } else {
921
                    $this->_sql .= "$f=NULL,";
922
                }
923
            }
924
        } else {
925
            throw new Error("No fields to update");
926
        }
927
928
        $this->_sql = substr($this->_sql, 0, - 1);
929
930
        if (! is_null($arrWhere) && is_array($arrWhere) && count($arrWhere)) {
931
            $where_str = " WHERE";
932
            foreach ($arrWhere as $x => $w) {
933
                $where_str .= $this->parseClause($w, $x);
934
            }
935
            if (strlen($where_str) > strlen(" WHERE")) {
936
                $this->_sql .= $where_str;
937
            }
938
        }
939
940
        if (! is_null($arrFlags) && is_array($arrFlags) && count($arrFlags)) {
941
            $this->_sql .= $this->flags($arrFlags);
942
        }
943
944
        if (self::$autorun) {
945
            return $this->execute(MYSQLI_BOTH);
946
        }
947
948
        return $this->_sql;
949
    }
950
951
    /**
952
     * Function to offer an extended updated functionality by using two different tables.
953
     *
954
     * @param string $to_be_updated
955
     *            The table that you want to update (alias 'tbu' is automatically added)
956
     * @param string $original
957
     *            The table with the data you want to overwrite to_be_updated table (alias 'o' is automatically added)
958
     * @param string $using
959
     *            The common index value between them that will join the fields
960
     * @param array|string $params
961
     *            If string only a single field is updated (tbu.$params = o.$params)
962
     *            If array each element in the array is a field to be updated (tbu.$param = o.$param)
963
     *
964
     * @return mixed
965
     */
966
    public function extendedUpdate(string $strTableToUpdate, string $strOriginalTable, string $strLinkField, $params)
967
    {
968
        $this->_sql = "UPDATE ";
969
        $this->_queryType = self::EXTENDED_UPDATE;
970
971
        if (! is_null($strTableToUpdate) && ! is_null($strOriginalTable) && ! is_null($strLinkField)) {
972
            $this->_sql .= "$strTableToUpdate tbu INNER JOIN $strOriginalTable o USING ($strLinkField) SET ";
973
        } else {
974
            throw new Error("Missing necessary fields");
975
        }
976
977
        if (is_array($params) && count($params)) {
978
            foreach ($params as $param) {
979
                if ($param != $strLinkField) {
980
                    $this->_sql .= "tbu.`$param` = o.`$param`,";
981
                }
982
            }
983
            $this->_sql = substr($this->_sql, 0, - 1);
984
        } elseif (is_string($params)) {
985
            $this->_sql .= "tbu.`$params` = o.`$params`";
986
        } else {
987
            throw new Exception("Do not understand datatype " . gettype($params), E_ERROR);
988
        }
989
990
        if (self::$autorun) {
991
            return $this->execute(MYSQLI_BOTH);
992
        }
993
994
        return $this->_sql;
995
    }
996
997
    /**
998
     * Function to build a replace query
999
     *
1000
     * @param string $strTableName
1001
     *            The table to update
1002
     * @param array $arrParams
1003
     *            Name/value pair to insert
1004
     *
1005
     * @return NULL|string
1006
     */
1007
    public function replace(string $strTableName, array $arrParams)
1008
    {
1009
        $this->_sql = null;
1010
        $this->_queryType = self::REPLACE;
1011
1012
        if (! is_null($strTableName) && is_string($strTableName)) {
1013
            $this->_sql = "REPLACE INTO $strTableName " . "(`" . implode("`,`", array_keys($arrParams)) . "`)";
1014
        } else {
1015
            throw new Error("Table name is invalid");
1016
        }
1017
1018
        $this->_sql .= " VALUES (" . implode(",", array_map([
1019
            $this,
1020
            '_escape'
1021
        ], array_values($arrParams))) . ")";
1022
1023
        if (self::$autorun) {
1024
            return $this->execute(MYSQLI_BOTH);
1025
        }
1026
1027
        return $this->_sql;
1028
    }
1029
1030
    /**
1031
     * Function to build an extended replace statement
1032
     *
1033
     * @param string $strTableName
1034
     *            Table name to update
1035
     * @param array $arrFields
1036
     *            Array of fields
1037
     * @param array $arrParams
1038
     *            Two-dimensional array of values
1039
     *
1040
     * @return NULL|string
1041
     */
1042
    public function extendedReplace(string $strTableName, array $arrFields, array $arrParams)
1043
    {
1044
        $this->_sql = null;
1045
        $this->_queryType = self::EXTENDED_REPLACE;
1046
1047
        if (! is_array($arrFields) || ! count($arrFields)) {
1048
            throw new Exception("Error with the field type");
1049
        }
1050
1051
        if (! is_null($strTableName) && is_string($strTableName)) {
1052
            $this->_sql = "REPLACE INTO $strTableName " . "(`" . implode("`,`", $arrFields) . "`)";
1053
        } else {
1054
            throw new Error("Table name is invalid");
1055
        }
1056
1057
        if (is_array($arrParams) && count($arrParams)) {
1058
            $this->_sql .= " VALUES ";
1059
            foreach ($arrParams as $p) {
1060
                $this->_sql .= "(" . implode(",", array_map([
1061
                    $this,
1062
                    '_escape'
1063
                ], array_values($p))) . ")";
1064
1065
                if ($p != end($arrParams)) {
1066
                    $this->_sql .= ",";
1067
                }
1068
            }
1069
        }
1070
1071
        if (self::$autorun) {
1072
            return $this->execute(MYSQLI_BOTH);
1073
        }
1074
1075
        return $this->_sql;
1076
    }
1077
1078
    /**
1079
     * Function to build a delete statement
1080
     *
1081
     * @param string $strTableName
1082
     *            Table name to act on
1083
     * @param array $arrFields
1084
     *            [optional]
1085
     *            Optional list of fields to delete (used when including multiple tables)
1086
     * @param array $arrWhere
1087
     *            [optional]
1088
     *            Optional 2-dimensional array to build where clause from
1089
     * @param array $arrJoins
1090
     *            [optional]
1091
     *            Optional 2-dimensional array to add other flags
1092
     *
1093
     * @see Database::where()
1094
     * @see Database::flags()
1095
     *
1096
     * @return string|NULL
1097
     */
1098
    public function delete(string $strTableName, array $arrFields = [], array $arrWhere = [], array $arrJoins = [])
1099
    {
1100
        $this->_sql = "DELETE";
1101
        $this->_queryType = self::DELETE;
1102
1103
        $this->_logger->debug("Deleting table data");
1104
1105
        if (! is_null($arrFields) && is_array($arrFields) && count($arrFields)) {
1106
            $this->_sql .= " " . implode(",", $arrFields);
1107
        }
1108
1109
        if (! is_null($strTableName) && is_string($strTableName)) {
1110
            $this->_sql .= " FROM $strTableName";
1111
        } else {
1112
            throw new Error("Table name is invalid");
1113
        }
1114
1115
        if (! is_null($arrJoins) && is_array($arrJoins) && count($arrJoins)) {
1116
            $this->_sql .= " " . implode(" ", $arrJoins);
1117
        }
1118
1119
        if (! is_null($arrWhere) && is_array($arrWhere) && count($arrWhere)) {
1120
            $where_str = " WHERE";
1121
            foreach ($arrWhere as $x => $w) {
1122
                $where_str .= $this->parseClause($w, $x);
1123
            }
1124
            if (strlen($where_str) > strlen(" WHERE")) {
1125
                $this->_sql .= $where_str;
1126
            }
1127
        }
1128
1129
        if (self::$autorun) {
1130
            return $this->execute(MYSQLI_BOTH);
1131
        }
1132
1133
        return $this->_sql;
1134
    }
1135
1136
    /**
1137
     * Function to build a drop table statement (automatically executes)
1138
     *
1139
     * @param string $strTableName
1140
     *            Table to drop
1141
     * @param string $strType
1142
     *            [optional]
1143
     *            Type of item to drop ('table', 'view') (defaulted to 'table')
1144
     * @param boolean $blnIsTemp
1145
     *            [optional]
1146
     *            Optional boolean if this is a temporary table
1147
     *
1148
     * @return string|NULL
1149
     */
1150
    public function drop(string $strTableName, string $strType = 'table', bool $blnIsTemp = false)
1151
    {
1152
        $this->_sql = null;
1153
        $this->_queryType = self::DROP;
1154
1155
        switch ($strType) {
1156
            case 'table':
1157
                $strType = 'TABLE';
1158
                break;
1159
            case 'view':
1160
                $strType = 'VIEW';
1161
                break;
1162
            default:
1163
                throw new Error("Invalid type " . gettype($strType), E_ERROR);
1164
        }
1165
1166
        if (! is_null($strTableName) && is_string($strTableName)) {
1167
            $this->_sql = "DROP" . ($blnIsTemp ? " TEMPORARY" : "") . " $strType IF EXISTS `{$strTableName}`";
1168
        } else {
1169
            throw new Error("Table name is invalid");
1170
        }
1171
1172
        if (self::$autorun) {
1173
            return $this->execute(MYSQLI_BOTH);
1174
        }
1175
1176
        return $this->_sql;
1177
    }
1178
1179
    /**
1180
     * Function to build a truncate table statement (automatically executes)
1181
     *
1182
     * @param string $strTableName
1183
     *            Table to truncate
1184
     *
1185
     * @throws \Error
1186
     *
1187
     * @return string|NULL
1188
     */
1189
    public function truncate(string $strTableName)
1190
    {
1191
        $this->_sql = null;
1192
        $this->_queryType = self::TRUNCATE;
1193
1194
        if (! is_null($strTableName) && is_string($strTableName)) {
1195
            $this->_sql = "TRUNCATE TABLE $strTableName";
1196
        } else {
1197
            throw new Error("Table name is invalid");
1198
        }
1199
1200
        if (self::$autorun) {
1201
            return $this->execute(MYSQLI_BOTH);
1202
        }
1203
1204
        return $this->_sql;
1205
    }
1206
1207
    /**
1208
     * Function to build a create temporary table statement
1209
     *
1210
     * @param string $strTableName
1211
     *            Name to give the table when creating
1212
     * @param boolean $blnIsTemp
1213
     *            [optional]
1214
     *            Optional boolean to make the table a temporary table
1215
     * @param mixed $strSelect
1216
     *            [optional]
1217
     *            Optional parameter if null uses last built statement
1218
     *            If string, will be made the SQL statement executed to create the table
1219
     *            If array, 2-dimensional array with "field", "datatype" values to build table fields
1220
     *
1221
     * @return NULL|string
1222
     */
1223
    public function createTable(string $strTableName, bool $blnIsTemp = false, $strSelect = null)
1224
    {
1225
        $this->_queryType = self::CREATE_TABLE;
1226
1227
        if (is_null($strSelect) && ! is_null($this->_sql) && substr($this->_sql, 0, 6) == 'SELECT') {
1228
            $this->_sql = "CREATE" . ($blnIsTemp ? " TEMPORARY" : "") . " TABLE IF NOT EXISTS $strTableName AS ($this->_sql)";
1229
        } elseif (! is_null($strTableName) && is_string($strTableName) && is_string($strSelect)) {
1230
            $this->_sql = "CREATE" . ($blnIsTemp ? " TEMPORARY" : "") . " TABLE IF NOT EXISTS $strTableName AS ($strSelect)";
1231
        } elseif (! is_null($strTableName) && is_string($strTableName) && is_array($strSelect)) {
1232
            $this->_sql = "CREATE" . ($blnIsTemp ? " TEMPORARY" : "") . " TABLE IF NOT EXISTS $strTableName (";
1233
1234
            foreach ($strSelect as $field) {
1235
                $default = null;
1236
                if (isset($field['default'])) {
1237
                    $default = (is_null($field['default']) ? "" : " DEFAULT '{$field['default']}'");
1238
                }
1239
                $this->_sql .= "`{$field['field']}` {$field['datatype']}" . $default . (isset($field['option']) ? " {$field['option']}" : '') . ",";
1240
            }
1241
            $this->_sql = substr($this->_sql, 0, - 1) . ")";
1242
        }
1243
1244
        if (self::$autorun) {
1245
            return $this->execute();
1246
        }
1247
1248
        return $this->_sql;
1249
    }
1250
1251
    /**
1252
     * Function to create a table using a stdClass object derived from JSON
1253
     *
1254
     * @param \stdClass $json
1255
     *
1256
     * @example /examples/create_table_json.json
1257
     *
1258
     */
1259
    public function createTableJson($json)
1260
    {
1261
        $this->_queryType = self::CREATE_TABLE;
1262
        $this->_c->select_db($json->schema);
1263
1264
        $this->_sql = "CREATE TABLE IF NOT EXISTS `{$json->name}` (";
1265
        foreach ($json->fields as $field) {
1266
            $this->_sql .= "`{$field->name}` {$field->dataType}";
1267
1268
            if ($field->dataType == 'enum') {
1269
                $this->_sql .= "('" . implode("','", $field->values) . "')";
1270
            }
1271
1272
            if ($field->ai) {
1273
                $this->_sql .= " AUTO_INCREMENT";
1274
            }
1275
1276
            if ($field->nn) {
1277
                $this->_sql .= " NOT NULL";
1278
            } else {
1279
                if ($field->default === null) {
1280
                    $this->_sql .= " DEFAULT NULL";
1281
                } elseif (strlen($field->default)) {
1282
                    $this->_sql .= " DEFAULT '{$field->default}'";
1283
                }
1284
            }
1285
1286
            if ($field != end($json->fields)) {
1287
                $this->_sql .= ",";
1288
            }
1289
        }
1290
1291
        if (isset($json->index) && count($json->index)) {
1292
            foreach ($json->index as $ind) {
1293
                $this->_sql .= ", " . strtoupper($ind->type) . " `{$ind->id}` (`{$ind->ref}`)";
1294
            }
1295
        }
1296
1297
        if (isset($json->constraints) && count($json->constraints)) {
1298
            foreach ($json->constraints as $con) {
1299
                $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));
1300
            }
1301
        }
1302
1303
        if (isset($json->unique) && count($json->unique)) {
1304
            $this->_sql .= ", UNIQUE(`" . implode("`,`", $json->unique) . "`)";
1305
        }
1306
1307
        if (isset($json->primary_key) && count($json->primary_key)) {
1308
            $this->_sql .= ", PRIMARY KEY(`" . implode("`,`", $json->primary_key) . "`))";
1309
        } else {
1310
            if (substr($this->_sql, - 1) == ',') {
1311
                $this->_sql = substr($this->_sql, 0, - 1);
1312
            }
1313
1314
            $this->_sql .= ")";
1315
        }
1316
1317
        $this->execute(MYSQLI_BOTH);
1318
    }
1319
1320
    /**
1321
     * Function to alter a existing table
1322
     *
1323
     * @param string $strTableName
1324
     *            Table to alter
1325
     * @param int $intAction
1326
     *            What action should be taken ('add-column', 'drop-column', 'modify-column')
1327
     * @param mixed $params
1328
     *            For add column this is a stdClass object that has the same elements as the example json
1329
     *
1330
     * @return mixed
1331
     */
1332
    public function alterTable(string $strTableName, int $intAction, $params)
1333
    {
1334
        $this->_queryType = self::ALTER_TABLE;
1335
        $this->_sql = "ALTER TABLE $strTableName";
1336
        if ($intAction == self::ADD_COLUMN) {
1337
            $nn = ($params->nn ? " NOT NULL" : "");
1338
            $default = null;
1339
            if ($params->default === null) {
1340
                $default = " DEFAULT NULL";
1341
            } elseif (strlen($params->default)) {
1342
                $default = " DEFAULT {$this->_escape($params->default)}";
1343
            }
1344
            $this->_sql .= " ADD COLUMN `{$params->name}` {$params->dataType}" . $nn . $default;
1345
        } elseif ($intAction == self::DROP_COLUMN) {
1346
            $this->_sql .= " DROP COLUMN ";
1347
            foreach ($params as $col) {
1348
                $this->_sql .= "`{$col->name}`";
1349
1350
                if ($col != end($params)) {
1351
                    $this->_sql .= ",";
1352
                }
1353
            }
1354
        } elseif ($intAction == self::MODIFY_COLUMN) {
1355
            $this->_sql .= " MODIFY COLUMN";
1356
            $nn = ($params->nn ? " NOT NULL" : "");
1357
            $default = null;
1358
            if ($params->default === null) {
1359
                $default = " DEFAULT NULL";
1360
            } elseif (strlen($params->default)) {
1361
                $default = " DEFAULT {$this->_escape($params->default)}";
1362
            }
1363
            $this->_sql .= " `{$params->name}` `{$params->new_name}` {$params->dataType}" . $nn . $default;
1364
        }
1365
1366
        if (self::$autorun) {
1367
            return $this->execute();
1368
        }
1369
1370
        return $this->_sql;
1371
    }
1372
1373
    /**
1374
     * Check to see if a field in a table exists
1375
     *
1376
     * @param string $strTableName
1377
     *            Table to check
1378
     * @param string $strFieldName
1379
     *            Field name to find
1380
     *
1381
     * @return boolean Returns TRUE if field is found in that schema and table, otherwise FALSE
1382
     */
1383
    public function fieldExists(string $strTableName, string $strFieldName)
1384
    {
1385
        $fdata = $this->fieldData($strTableName);
1386
1387
        if (is_array($fdata) && count($fdata)) {
1388
            foreach ($fdata as $field) {
1389
                if ($field->name == $strFieldName) {
1390
                    return true;
1391
                }
1392
            }
1393
        }
1394
1395
        return false;
1396
    }
1397
1398
    /**
1399
     * Function to get the column data (datatype, flags, defaults, etc)
1400
     *
1401
     * @param string $strTableName
1402
     *            Table to query
1403
     * @param mixed $field
1404
     *            [optional]
1405
     *            Optional field to retrieve data (if null, returns data from all fields)
1406
     *
1407
     * @return array
1408
     */
1409
    public function fieldData(string $strTableName, $field = null)
1410
    {
1411
        if (is_null($field)) {
1412
            $res = $this->_c->query("SELECT * FROM $strTableName LIMIT 1");
1413
        } elseif (is_array($field)) {
1414
            $res = $this->_c->query("SELECT `" . implode("`,`", $field) . "` FROM $strTableName LIMIT 1");
1415
        } elseif (is_string($field)) {
1416
            $res = $this->_c->query("SELECT $field FROM $strTableName LIMIT 1");
1417
        } else {
1418
            return null;
1419
        }
1420
1421
        $fields = null;
1422
        if (is_a($res, 'mysqli_result')) {
1423
            $fields = $res->fetch_fields();
1424
            foreach ($fields as $i => $f) {
1425
                $fields["{$f->name}"] = $f;
1426
                unset($fields[$i]);
1427
            }
1428
        }
1429
1430
        return $fields;
1431
    }
1432
1433
    /**
1434
     * Function to check that all field parameters are set correctly
1435
     *
1436
     * @param object $field_data
1437
     * @param object $check
1438
     * @param array $pks
1439
     * @param object $index
1440
     *
1441
     * @return array|string
1442
     */
1443
    public function fieldCheck($field_data, $check, $pks, $index)
1444
    {
1445
        $default = null;
1446
        $ret = null;
1447
1448
        $nn = ($check->nn ? " NOT NULL" : null);
1449
        if ($check->default === null) {
1450
            $default = " DEFAULT NULL";
1451
        } elseif (strlen($check->default)) {
1452
            $default = " DEFAULT '{$check->default}'";
1453
        }
1454
1455
        if ($field_data->type != $check->type && $check->type != MYSQLI_TYPE_ENUM) {
1456
            $this->_logger->notice("Wrong datatype", [
1457
                'name' => $field_data->name,
1458
                'datatype' => $check->dataType
1459
            ]);
1460
            $ret = " CHANGE COLUMN `{$field_data->name}` `{$check->name}` {$check->dataType}" . "{$nn}{$default}";
1461
        } elseif (! is_null($check->length) && $field_data->length != $check->length) {
1462
            $this->_logger->notice("Incorrect size", [
1463
                'name' => $field_data->name,
1464
                'current' => $field_data->length,
1465
                'new_size' => $check->length
1466
            ]);
1467
            $ret = " CHANGE COLUMN `{$field_data->name}` `{$check->name}` {$check->dataType}" . "{$nn}{$default}";
1468
        } elseif ($check->type == MYSQLI_TYPE_ENUM && ! ($field_data->flags & MYSQLI_ENUM_FLAG)) {
1469
            $this->_logger->notice("Setting ENUM type", [
1470
                'name' => $field_data->name,
1471
                'values' => implode(",", $check->values)
1472
            ]);
1473
            $ret = " CHANGE COLUMN `{$field_data->name}` `{$check->name}` {$check->dataType}('" . implode("','", $check->values) . "')" . "{$nn}{$default}";
1474
        }
1475
1476
        if (! is_null($index) && count($index)) {
1477
            foreach ($index as $ind) {
1478
                if ($check->name == $ind->ref && ! ($field_data->flags & MYSQLI_MULTIPLE_KEY_FLAG)) {
1479
                    $this->_logger->debug("Missing index", [
1480
                        'name' => $field_data->name
1481
                    ]);
1482
                    $ret .= ($ret ? "," : "") . " ADD INDEX `{$ind->id}` (`{$ind->ref}` ASC)";
1483
                }
1484
            }
1485
        }
1486
1487
        if (in_array($check->name, $pks) && ! ($field_data->flags & MYSQLI_PRI_KEY_FLAG)) {
1488
            $this->_logger->debug("Setting PKs", [
1489
                'keys' => implode(',', $pks)
1490
            ]);
1491
            $ret .= ($ret ? "," : "") . " DROP PRIMARY KEY, ADD PRIMARY KEY(`" . implode("`,`", $pks) . "`)";
1492
        }
1493
1494
        return $ret;
1495
    }
1496
1497
    /**
1498
     * Function to check for the existence of a table within a schema
1499
     *
1500
     * @param string $strSchema
1501
     *            The schema to search in
1502
     * @param string $strTableName
1503
     *            Table to search for
1504
     *
1505
     * @return integer|boolean Returns number of tables that match if table is found in that schema, otherwise FALSE
1506
     */
1507
    public function tableExists(string $strSchema, string $strTableName)
1508
    {
1509
        if (! $this->_c->select_db($strSchema)) {
1510
            fwrite(STDOUT, $this->_c->error . PHP_EOL);
1511
        }
1512
        $sql = "SHOW TABLES LIKE '{$strTableName}'";
1513
1514
        if ($res = $this->_c->query($sql)) {
1515
            if (gettype($res) == 'object' && is_a($res, 'mysqli_result') && $res->num_rows) {
1516
                return $res->num_rows;
1517
            }
1518
        } else {
1519
            if ($this->_c->errno) {
1520
                fwrite(STDOUT, $this->_c->error . PHP_EOL);
1521
            }
1522
        }
1523
1524
        return false;
1525
    }
1526
1527
    /**
1528
     * Function to detect if string is a JSON object or not
1529
     *
1530
     * @param string $strVal
1531
     *
1532
     * @return boolean
1533
     */
1534
    public function isJson(string $strVal)
1535
    {
1536
        json_decode($strVal);
1537
        return (json_last_error() == JSON_ERROR_NONE);
1538
    }
1539
1540
    /**
1541
     * Function to escape SQL characters to prevent SQL injection
1542
     *
1543
     * @param mixed $val
1544
     *            Value to escape
1545
     * @param boolean $blnEscape
1546
     *            Decide if we should escape or not
1547
     *
1548
     * @return string Escaped value
1549
     */
1550
    public function _escape($val, bool $blnEscape = true)
1551
    {
1552
        if (is_null($val) || (is_string($val) && strtolower($val) == 'null')) {
1553
            return 'NULL';
1554
        } elseif (is_numeric($val) || is_string($val)) {
1555
            if ($blnEscape) {
1556
                return "'{$this->_c->real_escape_string($val)}'";
1557
            }
1558
            return $val;
1559
        } elseif (is_a($val, 'DateTime')) {
1560
            return "'{$val->format(MYSQL_DATETIME)}'";
1561
        } elseif (is_bool($val)) {
1562
            return $val ? "'1'" : "'0'";
1563
        } elseif (gettype($val) == 'object' && method_exists($val, '_escape')) {
1564
            $ret = call_user_func([
1565
                $val,
1566
                '_escape'
1567
            ]);
1568
            if ($ret !== false) {
1569
                return $ret;
1570
            } else {
1571
                throw new Exception("Error in return from _escape method in " . get_class($val), E_ERROR);
1572
            }
1573
        } elseif (gettype($val) == 'object') {
1574
            $this->_logger->error("Unknown object to escape " . get_class($val) . " in SQL string {$this->_sql}");
1575
            return;
1576
        }
1577
1578
        throw new Exception("Unknown datatype to escape in SQL string {$this->_sql} " . gettype($val), E_ERROR);
1579
    }
1580
1581
    /**
1582
     * Function to retrieve all results
1583
     *
1584
     * @param int $resulttype
1585
     *
1586
     * @return mixed
1587
     */
1588
    public function fetchAll(int $intResultType = MYSQLI_ASSOC)
1589
    {
1590
        $res = [];
1591
        if (method_exists('mysqli_result', 'fetch_all')) { // Compatibility layer with PHP < 5.3
1592
            $res = $this->_result->fetch_all($intResultType);
1593
        } else {
1594
            while ($tmp = $this->_result->fetch_array($intResultType)) {
1595
                $res[] = $tmp;
1596
            }
1597
        }
1598
1599
        return $res;
1600
    }
1601
1602
    /**
1603
     * Function to populate the fields for the SQL
1604
     *
1605
     * @param array|string $fields
1606
     *            [optional]
1607
     *            Optional array of fields to string together to create a field list
1608
     *
1609
     * @return string
1610
     */
1611
    public function fields($fields = null)
1612
    {
1613
        $ret = null;
1614
1615
        if (is_array($fields) && count($fields) && isset($fields[0]) && is_string($fields[0])) {
1616
            foreach ($fields as $field) {
1617
                if ((strpos($field, '`') === false) && (strpos($field, '.') === false) && (strpos($field, '*') === false) && (strpos($field, 'JSON_') === false) && (stripos($field, ' as ') === false)) {
1618
                    $ret .= "`$field`,";
1619
                } else {
1620
                    $ret .= "$field,";
1621
                }
1622
            }
1623
            $ret = substr($ret, - 1) == ',' ? substr($ret, 0, - 1) : $ret;
1624
        } elseif (is_string($fields)) {
1625
            $ret = $fields;
1626
        } elseif (is_null($fields)) {
1627
            $ret = "*";
1628
        } else {
1629
            throw new \InvalidArgumentException("Invalid field type");
1630
        }
1631
1632
        return $ret;
1633
    }
1634
1635
    /**
1636
     * Function to parse the flags
1637
     *
1638
     * @param array $flags
1639
     *            Two-dimensional array to added flags
1640
     *
1641
     *            <code>
1642
     *            [
1643
     *            &nbsp;&nbsp;'joins' => [
1644
     *            &nbsp;&nbsp;&nbsp;&nbsp;"JOIN table2 t2 ON t2.id=t1.id"
1645
     *            &nbsp;&nbsp;],
1646
     *            &nbsp;&nbsp;'group' => 'field',
1647
     *            &nbsp;&nbsp;'having' => 'field',
1648
     *            &nbsp;&nbsp;'order' => 'field',
1649
     *            &nbsp;&nbsp;'start' => 0,
1650
     *            &nbsp;&nbsp;'limit' => 0
1651
     *            ]
1652
     *            </code>
1653
     *
1654
     * @see Database::groups()
1655
     * @see Database::having()
1656
     * @see Database::order()
1657
     *
1658
     * @return string
1659
     */
1660
    public function flags(array $arrFlags)
1661
    {
1662
        $ret = '';
1663
1664
        if (isset($arrFlags['group'])) {
1665
            $ret .= $this->groups($arrFlags['group']);
1666
        }
1667
1668
        if (isset($arrFlags['having']) && is_array($arrFlags['having'])) {
1669
            $having = " HAVING";
1670
            foreach ($arrFlags['having'] as $x => $h) {
1671
                $having .= $this->parseClause($h, $x);
1672
            }
1673
            if (strlen($having) > strlen(" HAVING")) {
1674
                $ret .= $having;
1675
            }
1676
        }
1677
1678
        if (isset($arrFlags['order'])) {
1679
            $ret .= $this->order($arrFlags['order']);
1680
        }
1681
1682
        if (isset($arrFlags['limit']) && (is_string($arrFlags['limit']) || is_numeric($arrFlags['limit']))) {
1683
            $ret .= " LIMIT ";
1684
            if (isset($arrFlags['start']) && (is_string($arrFlags['start']) || is_numeric($arrFlags['start']))) {
1685
                $ret .= "{$arrFlags['start']},";
1686
            }
1687
            $ret .= "{$arrFlags['limit']}";
1688
        }
1689
1690
        return $ret;
1691
    }
1692
1693
    /**
1694
     * Function to parse SQL GROUP BY statements
1695
     *
1696
     * @param mixed $groups
1697
     *
1698
     * @return string
1699
     */
1700
    public function groups($groups)
1701
    {
1702
        $ret = '';
1703
        if (is_array($groups) && count($groups)) {
1704
            $ret .= " GROUP BY";
1705
1706
            foreach ($groups as $grp) {
1707
                $ret .= " $grp";
1708
1709
                if ($grp != end($groups)) {
1710
                    $ret .= ",";
1711
                }
1712
            }
1713
        } elseif (is_string($groups)) {
1714
            $ret .= " GROUP BY {$groups}";
1715
        } else {
1716
            throw (new Exception("Error in datatype for groups " . gettype($groups), E_ERROR));
1717
        }
1718
1719
        return $ret;
1720
    }
1721
1722
    /**
1723
     * Function to parse SQL ORDER BY statements
1724
     *
1725
     * @param mixed $order
1726
     *
1727
     * @return string
1728
     */
1729
    public function order($order)
1730
    {
1731
        $ret = '';
1732
        if (is_array($order)) {
1733
            $ret .= " ORDER BY";
1734
1735
            foreach ($order as $ord) {
1736
                $ret .= " {$ord['field']} {$ord['sort']}";
1737
1738
                if ($ord != end($order)) {
1739
                    $ret .= ",";
1740
                }
1741
            }
1742
        } elseif (is_string($order)) {
1743
            $ret .= " ORDER BY {$order}";
1744
        }
1745
1746
        return $ret;
1747
    }
1748
1749
    /**
1750
     * Function to see if a constraint exists
1751
     *
1752
     * @param string $strConstraintId
1753
     *
1754
     * @return boolean
1755
     */
1756
    public function isConstraint(string $strConstraintId)
1757
    {
1758
        $res = $this->_c->query("SELECT * FROM information_schema.TABLE_CONSTRAINTS WHERE CONSTRAINT_NAME = '{$strConstraintId}'");
1759
1760
        if ($res->num_rows) {
1761
            return true;
1762
        }
1763
1764
        return false;
1765
    }
1766
1767
    /**
1768
     * Function to parse where and having clauses
1769
     *
1770
     * @param array $arrClause
1771
     * @param int $index
1772
     */
1773
    public function parseClause(array $arrClause, int $intIndex)
1774
    {
1775
        $ret = null;
1776
1777
        $this->_logger->debug("Parsing clause", $arrClause);
1778
1779
        if (! isset($arrClause['field']) && isset($arrClause['close-paren']) && $arrClause['close-paren']) {
1780
            $ret .= ")";
1781
            return $ret;
1782
        } elseif ($intIndex > 0 && ! isset($arrClause['sql_op'])) {
1783
            $this->_logger->warning("Missing sql_op field to identify how current and previous WHERE clause statements should be linked ('AND', 'OR', 'XOR', etc), skipped", [
1784
                'clause' => implode(",", $arrClause)
1785
            ]);
1786
            return;
1787
        }
1788
1789
        $op = '=';
1790
        if (isset($arrClause['op'])) {
1791
            $op = $arrClause['op'];
1792
        }
1793
1794
        switch ($op) {
1795
            case self::BETWEEN:
1796
                if (! isset($arrClause['field']) || ! isset($arrClause['low']) || ! isset($arrClause['high'])) {
1797
                    $this->_logger->warning("Missing field, low, or high for BETWEEN where clause, skipping");
1798
                    return;
1799
                }
1800
                break;
1801
            default:
1802
                if (! isset($arrClause['field']) || ! isset($arrClause['value'])) {
1803
                    $this->_logger->warning("Missing field or value for WHERE clause, skipping", $arrClause);
1804
                    return;
1805
                }
1806
        }
1807
1808
        if ($intIndex > 0) {
1809
            $ret .= " {$arrClause['sql_op']}";
1810
        }
1811
1812
        if (isset($arrClause['open-paren']) && $arrClause['open-paren']) {
1813
            $ret .= " (";
1814
        }
1815
1816
        if (isset($arrClause['backticks']) && ! $arrClause['backticks']) {
1817
            $field = $arrClause['field'];
1818
        } else {
1819
            $field = "`{$arrClause['field']}`";
1820
        }
1821
1822
        if ($op == self::IN || $op == self::NOT_IN) {
1823
            if (is_string($arrClause['value'])) {
1824
                $ret .= " {$field} {$op} " . (strpos($arrClause['value'], '(') !== false ? $arrClause['value'] : "({$arrClause['value']})");
1825
            } elseif (is_array($arrClause['value'])) {
1826
                $ret .= " {$field} {$op} (" . implode(",", array_map([
1827
                    $this,
1828
                    '_escape'
1829
                ], $arrClause['value'])) . ")";
1830
            } else {
1831
                $this->_logger->error("Invalid datatype for IN WHERE clause, only string and array allowed " . gettype($arrClause['value']), $arrClause);
1832
                throw new Exception("Invalid datatype for IN WHERE clause", E_ERROR);
1833
            }
1834
        } elseif ($op == self::BETWEEN) {
1835
            $ret .= " {$field} BETWEEN {$this->_escape($arrClause['low'])} AND {$this->_escape($arrClause['high'])}";
1836
        } else {
1837
            if (isset($arrClause['escape']) && ! $arrClause['escape']) {
1838
                $value = $arrClause['value'];
1839
            } else {
1840
                $value = $this->_escape($arrClause['value']);
1841
            }
1842
1843
            if (isset($arrClause['case_insensitive']) && $arrClause['case_insensitive']) {
1844
                $ret .= " LOWER({$field}) {$op} LOWER({$this->_escape($arrClause['value'])})";
1845
            } elseif (preg_match("/\(SELECT/", $arrClause['value'])) {
1846
                $ret .= " {$field} {$op} {$arrClause['value']}";
1847
            } else {
1848
                $ret .= " {$field} {$op} {$value}";
1849
            }
1850
        }
1851
1852
        if (isset($arrClause['close-paren']) && $arrClause['close-paren']) {
1853
            $ret .= ")";
1854
        }
1855
1856
        return $ret;
1857
    }
1858
}