Completed
Push — master ( 294ce5...3463b0 )
by Ryan
04:18
created

Database::get_schema()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 8
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 5
nc 2
nop 0
1
<?php
0 ignored issues
show
Coding Style Compatibility introduced by
For compatibility and reusability of your code, PSR1 recommends that a file should introduce either new symbols (like classes, functions, etc.) or have side-effects (like outputting something, or including other files), but not both at the same time. The first symbol is defined on line 19 and the first side effect is on line 12.

The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.

The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.

To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.

Loading history...
2
3
/**
4
 *
5
 */
6
namespace Godsgood33\Php_Db;
7
8
use Katzgrau\KLogger\Logger;
9
use Psr\Log\LogLevel;
10
use Exception;
11
use mysqli;
12
require_once 'DBConfig.php';
13
14
/**
15
 * A generic database class
16
 *
17
 * @author Ryan Prather
18
 */
19
class Database
20
{
21
22
    /**
23
     * Constant defining a SELECT query
24
     *
25
     * @var integer
26
     */
27
    const SELECT = 1;
28
29
    /**
30
     * Constant defining a SELECT COUNT query
31
     *
32
     * @var integer
33
     */
34
    const SELECT_COUNT = 2;
35
36
    /**
37
     * Constant defining a CREATE TABLE query
38
     *
39
     * @var integer
40
     */
41
    const CREATE_TABLE = 3;
42
43
    /**
44
     * Constant defining DROP query
45
     *
46
     * @var integer
47
     */
48
    const DROP = 4;
49
50
    /**
51
     * Constant defining DELETE query
52
     *
53
     * @var integer
54
     */
55
    const DELETE = 5;
56
57
    /**
58
     * Constant defining INSERT query
59
     *
60
     * @var integer
61
     */
62
    const INSERT = 6;
63
64
    /**
65
     * Constant defining REPLACE query
66
     *
67
     * @var integer
68
     */
69
    const REPLACE = 7;
70
71
    /**
72
     * Constant defining UPDATE query
73
     *
74
     * @var integer
75
     */
76
    const UPDATE = 8;
77
78
    /**
79
     * Constant defining EXTENDED INSERT query
80
     *
81
     * @var integer
82
     */
83
    const EXTENDED_INSERT = 9;
84
85
    /**
86
     * Constant defining EXTENDED REPLACE query
87
     *
88
     * @var integer
89
     */
90
    const EXTENDED_REPLACE = 10;
91
92
    /**
93
     * Constant defining EXTENDED UPDATE query
94
     *
95
     * @var integer
96
     */
97
    const EXTENDED_UPDATE = 11;
98
99
    /**
100
     * Constant defining ALTER TABLE query
101
     *
102
     * @var integer
103
     */
104
    const ALTER_TABLE = 12;
105
106
    /**
107
     * Constant defining a TRUNCATE TABLE query
108
     *
109
     * @var integer
110
     */
111
    const TRUNCATE = 13;
112
113
    /**
114
     * The mysqli connection
115
     *
116
     * @var \mysqli
117
     */
118
    private $c;
119
120
    /**
121
     * To store the SQL statement
122
     *
123
     * @var string
124
     */
125
    public $sql = null;
126
127
    /**
128
     * A string to store the type of query that is being run
129
     *
130
     * @var int
131
     */
132
    private $query_type = null;
133
134
    /**
135
     * The result of the query
136
     *
137
     * @var mixed
138
     */
139
    private $result = null;
140
141
    /**
142
     * Log level
143
     *
144
     * @var LogLevel
145
     */
146
    public $log_level = LogLevel::DEBUG;
147
148
    /**
149
     * Variable to store the logger
150
     *
151
     * @var \Katzgrau\KLogger\Logger
152
     */
153
    private $logger = null;
154
155
    /**
156
     * Variable to decide if we need to automatically run the queries after generating them
157
     *
158
     * @var boolean
159
     */
160
    public static $autorun = false;
161
162
    /**
163
     * Constructor
164
     *
165
     * @param \mysqli $dbh
166
     *            [optional]
167
     *            [by ref]
168
     *            mysqli object to perform queries.
169
     */
170
    public function __construct(&$dbh = null)
171
    {
172
        if (! is_null($dbh) && is_a($dbh, "mysqli")) {
173
            $this->c = $dbh;
174
        } else {
175
            $this->c = new mysqli(PHP_DB_SERVER, PHP_DB_USER, PHP_DB_PWD, PHP_DB_SCHEMA);
176
        }
177
178
        if ($this->c->connect_errno) {
179
            throw new Exception("Could not create database class due to error {$this->c->error}", E_ERROR);
180
        }
181
182
        $this->logger = new Logger("./", $this->log_level, [
0 ignored issues
show
Documentation introduced by
$this->log_level is of type object<Psr\Log\LogLevel>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
183
            'filename' => 'db.log',
184
            'dateFormat' => 'Y-m-d H:i:s.u',
185
            'logFormat' => "[{date}] {level}{level-padding} {message} {context}"
186
        ]);
187
188
        $this->set_var("time_zone", "+00:00");
189
        $this->set_var("sql_mode", "");
190
    }
191
192
    /**
193
     * Function to make sure that the database is connected
194
     *
195
     * @return boolean
196
     */
197
    public function is_connected()
198
    {
199
        return $this->c->ping();
200
    }
201
202
    /**
203
     * Function to set the log level just in case there needs to be a change to the default log level
204
     *
205
     * @param LogLevel $level
206
     */
207
    public function set_log_level(LogLevel $level)
208
    {
209
        $this->log_level = $level;
210
    }
211
212
    /**
213
     * Function to return the currently selected database schema
214
     *
215
     * @return string
216
     */
217
    public function get_schema()
218
    {
219
        if ($res = $this->c->query("SELECT DATABASE()")) {
220
            $row = $res->fetch_row();
221
            return $row[0];
222
        }
223
        return null;
224
    }
225
226
    /**
227
     * Function to set schema
228
     *
229
     * @param string $schema
230
     */
231
    public function set_schema(string $schema)
232
    {
233
        if (! $this->c->select_db($schema)) {
234
            throw new Exception("Failed to change databases to {$schema}", E_ERROR);
235
        }
236
        return true;
237
    }
238
239
    /**
240
     * Method to set a MYSQL variable
241
     *
242
     * @param string $name
243
     * @param string $val
244
     * @return boolean
245
     */
246
    public function set_var(string $name, string $val)
247
    {
248
        if (! $name || ! $val) {
249
            return false;
250
        }
251
252
        return $this->c->real_query("SET $name = {$this->_escape($val)}");
253
    }
254
255
    /**
256
     * Function to execute the statement
257
     *
258
     * @param mixed $return
259
     *            [optional]
260
     *            MYSQLI constant to control what is returned from the mysqli_result object
261
     * @param string $sql
262
     *            [optional]
263
     *            Optional SQL query
264
     *
265
     * @return mixed
266
     */
267
    public function execute($return = MYSQLI_ASSOC, $class = null, $sql = null)
268
    {
269
        if (! is_null($sql)) {
270
            $this->sql = $sql;
271
        }
272
273
        if (is_a($this->c, 'mysqli')) {
274
            if (! $this->c->ping()) {
275
                $this->c = null;
276
                $this->c = new mysqli(PHP_DB_SERVER, PHP_DB_USER, PHP_DB_PWD, PHP_DB_SCHEMA);
277
            }
278
        } else {
279
            throw new Exception('Database was not connected', E_ERROR);
280
        }
281
282
        $this->logger->debug($this->sql);
283
284
        try {
285
            if (in_array($this->query_type, [
286
                self::SELECT,
287
                self::SELECT_COUNT
288
            ])) {
289
                $this->result = $this->c->query($this->sql);
290 View Code Duplication
                if ($this->c->error) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
291
                    $this->log("There is an error " . $this->c->error, LogLevel::ERROR);
292
                    throw new Exception("There was an error " . $this->c->error, E_ERROR);
293
                }
294
            } else {
295
                $this->result = $this->c->real_query($this->sql);
296 View Code Duplication
                if ($this->c->errno) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
297
                    $this->log("There was an error " . $this->c->error, LogLevel::ERROR);
298
                    throw new Exception("There was an error " . $this->c->error, E_ERROR);
299
                }
300
            }
301
302
            if ($return == MYSQLI_OBJECT && ! is_null($class) && class_exists($class)) {
303
                $this->result = $this->check_results($return, $class);
304
            } elseif ($return == MYSQLI_OBJECT && is_null($class)) {
305
                $this->result = $this->check_results($return, 'stdClass');
306
            } else {
307
                $this->result = $this->check_results(MYSQLI_ASSOC);
308
            }
309
        } catch (Exception $e) {
310
            // die($e->getTraceAsString());
0 ignored issues
show
Unused Code Comprehensibility introduced by
80% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
311
        }
312
313
        return $this->result;
314
    }
315
316
    /**
317
     * Function to check the results and return what is expected
318
     *
319
     * @param mixed $return_type
320
     *            [optional]
321
     *            Optional return mysqli_result return type
322
     *
323
     * @return mixed
324
     */
325
    private function check_results($return_type = MYSQLI_ASSOC, $class = null)
326
    {
327
        $res = null;
328
329
        switch ($this->query_type) {
330
            case self::SELECT_COUNT:
331
                if (! is_a($this->result, 'mysqli_result')) {
332
                    $this->log("Error with return on query", LogLevel::ERROR);
333
                }
334
335
                if ($this->result->num_rows == 1) {
336
                    $row = $this->result->fetch_assoc();
337
                    if (isset($row['count'])) {
338
                        $res = $row['count'];
339
                    }
340
                } elseif ($this->result->num_rows > 1) {
341
                    $res = $this->result->num_rows;
342
                }
343
344
                mysqli_free_result($this->result);
345
346
                return $res;
347
            case self::SELECT:
348
                if (! is_a($this->result, 'mysqli_result')) {
349
                    $this->log("Error with return on query", LogLevel::ERROR);
350
                }
351
352
                if ($return_type == MYSQLI_OBJECT && ! is_null($class) && class_exists($class)) {
353
                    if ($this->result->num_rows == 1) {
354
                        $res = $this->result->fetch_object($class);
355
                    } elseif ($this->result->num_rows > 1) {
356
                        while ($row = $this->result->fetch_object($class)) {
357
                            $res[] = $row;
358
                        }
359
                    }
360
                } else {
361
                    if ($this->result->num_rows == 1) {
362
                        $res = $this->result->fetch_array($return_type);
363
                    } elseif ($this->result->num_rows > 1) {
364
                        $res = $this->fetch_all($return_type);
365
                    }
366
                }
367
368
                mysqli_free_result($this->result);
369
370
                return $res;
371
            case self::INSERT:
372
                if ($this->c->error) {
373
                    $this->log("Database Error " . $this->c->error, LogLevel::ERROR);
374
                    return 0;
375
                }
376
377
                if ($this->c->insert_id) {
378
                    return $this->c->insert_id;
379
                } elseif ($this->c->affected_rows) {
380
                    return $this->c->affected_rows;
381
                }
382
383
                return 1;
384
            case self::EXTENDED_INSERT:
385
            case self::EXTENDED_REPLACE:
386
            case self::EXTENDED_UPDATE:
387
            case self::REPLACE:
388
            case self::UPDATE:
389
            case self::DELETE:
390
            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...
391
                if ($this->c->error && $this->c->errno == 1060) {
392
                    return ($this->c->affected_rows ? $this->c->affected_rows : true);
393
                } elseif ($this->c->error) {
394
                    $this->log("Database Error " . $this->c->error, LogLevel::ERROR);
395
                    return false;
396
                } elseif ($this->c->affected_rows) {
397
                    return $this->c->affected_rows;
398
                } else {
399
                    return true;
400
                }
401
            case self::CREATE_TABLE:
402
            case self::DROP:
403
            case self::TRUNCATE:
404
                return true;
405
        }
406
    }
407
408
    /**
409
     * Function to pass through calling the query function (used for backwards compatibility and for more complex queries that aren't currently supported)
410
     * Nothing is escaped
411
     *
412
     * @param string $sql
413
     *            [optional]
414
     *            Optional query to pass in and execute
415
     *
416
     * @return \mysqli_result
417
     */
418
    public function query($sql = null)
419
    {
420
        if (is_null($sql)) {
421
            return $this->c->query($this->sql);
422
        } else {
423
            return $this->c->query($sql);
424
        }
425
    }
426
427
    /**
428
     * A function to build a select query
429
     *
430
     * @param string $table_name
431
     *            The table to query
432
     * @param array $fields
433
     *            [optional]
434
     *            Optional array of fields to return (defaults to '*')
435
     * @param array $where
436
     *            [optional]
437
     *            Optional 2-dimensional array to build where clause from
438
     * @param array $flags
439
     *            [optional]
440
     *            Optional 2-dimensional array to allow other flags
441
     *
442
     * @see Database::where()
443
     * @see Database::flags()
444
     *
445
     * @return mixed
446
     */
447
    public function select($table_name, $fields = null, $where = null, $flags = null)
448
    {
449
        $this->sql = null;
450
        $this->query_type = self::SELECT;
451
452
        if (! is_null($table_name) && is_string($table_name)) {
453
            $this->sql = "SELECT " . $this->fields($fields) . " FROM $table_name";
454
        } else {
455
            throw new Exception("Table name is invalid", E_ERROR);
456
        }
457
458 View Code Duplication
        if (isset($flags['joins']) && is_array($flags['joins'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
459
            $this->sql .= " " . implode(" ", $flags['joins']);
460
        }
461
462 View Code Duplication
        if (! is_null($where) && is_array($where) && count($where)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
463
            $this->sql .= $this->where($where);
464
        }
465
466
        if (count($flags)) {
467
            $this->sql .= $this->flags($flags);
468
        }
469
470
        if (self::$autorun) {
471
            return $this->execute(MYSQLI_BOTH);
472
        }
473
474
        return $this->sql;
475
    }
476
477
    /**
478
     * Function to build a query to check the number of rows in a table
479
     *
480
     * @param string $table_name
481
     *            The table to query
482
     * @param array $where
483
     *            [optional]
484
     *            Optional 2-dimensional array to build where clause
485
     * @param array $flags
486
     *            [optional]
487
     *            Optional 2-dimensional array to add flags
488
     *
489
     * @see Database::where()
490
     * @see Database::flags()
491
     *
492
     * @return string|NULL
493
     */
494
    public function select_count($table_name, $where = null, $flags = null)
495
    {
496
        $this->sql = null;
497
        $this->query_type = self::SELECT_COUNT;
498
499
        if (! is_null($table_name) && is_string($table_name)) {
500
            $this->sql = "SELECT COUNT(1) AS 'count' FROM $table_name";
501
        } else {
502
            return null;
503
        }
504
505 View Code Duplication
        if (isset($flags['joins']) && is_array($flags['joins'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
506
            $this->sql .= " " . implode(" ", $flags['joins']);
507
        }
508
509 View Code Duplication
        if (! is_null($where) && is_array($where) && count($where)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
510
            $this->sql .= $this->where($where);
511
        }
512
513
        if (count($flags)) {
514
            $this->sql .= $this->flags($flags);
515
        }
516
517
        if (self::$autorun) {
518
            return $this->execute(MYSQLI_BOTH);
519
        }
520
521
        return $this->sql;
522
    }
523
524
    /**
525
     * Function to build an insert query statement
526
     *
527
     * @param string $table_name
528
     * @param array $params
529
     * @param boolean $to_ignore
530
     *
531
     * @return string|NULL
532
     */
533
    public function insert($table_name, $params = null, $to_ignore = false)
534
    {
535
        $this->sql = null;
536
        $this->query_type = self::INSERT;
537
538
        if (! is_null($table_name) && is_string($table_name)) {
539
            $this->sql = "INSERT" . ($to_ignore ? " IGNORE" : "") . " INTO $table_name" . (is_array($params) && count($params) ? " (`" . implode("`,`", array_keys($params)) . "`)" : null);
540
        } else {
541
            throw (new Exception("Missing table name in insert function", E_ERROR));
542
        }
543
544
        if (is_array($params) && count($params)) {
545
            $this->sql .= " VALUES (" . implode(",", array_map([
546
                $this,
547
                '_escape'
548
            ], array_values($params))) . ")";
549
        } elseif (is_string($params) && stripos($params, 'SELECT') !== false) {
550
            $this->sql .= " {$params}";
551
        } else {
552
            throw (new Exception("Invalid type passed to insert " . gettype($params), E_ERROR));
553
        }
554
555
        if (self::$autorun) {
556
            return $this->execute(MYSQLI_BOTH);
557
        }
558
559
        return $this->sql;
560
    }
561
562
    /**
563
     * Function to create an extended insert query statement
564
     *
565
     * @param string $table_name
566
     *            The table name that the data is going to be inserted on
567
     * @param array $fields
568
     *            An array of field names that each value represents
569
     * @param array|string $params
570
     *            An array of array of values or a string with a SELECT statement to populate the insert with
571
     * @param boolean $to_ignore
572
     *            [optional]
573
     *            Boolean to decide if we need to use the INSERT IGNORE INTO syntax
574
     *
575
     * @return NULL|string Returns the SQL if self::$autorun is set to false, else it returns the output from running.
576
     */
577
    public function extended_insert($table_name, $fields, $params, $to_ignore = false)
578
    {
579
        $this->sql = null;
580
        $this->query_type = self::EXTENDED_INSERT;
581
582
        if (! is_null($table_name) && is_string($table_name)) {
583
            $this->sql = "INSERT " . ($to_ignore ? "IGNORE " : "") . "INTO $table_name " . "(`" . implode("`,`", $fields) . "`)";
584
        } else {
585
            throw (new Exception("Missing table name in extended_insert", E_ERROR));
586
        }
587
588
        if (is_array($params) && count($params)) {
589
            $this->sql .= " VALUES ";
590
            if (isset($params[0]) && is_array($params[0])) {
591
                foreach ($params as $p) {
592
                    if (count($p) != count($fields)) {
593
                        throw (new Exception("Inconsistent number of fields in fields and values in extended_insert " . print_r($p, true), E_ERROR));
594
                    }
595
                    $this->sql .= "(" . implode(",", array_map([
596
                        $this,
597
                        '_escape'
598
                    ], array_values($p))) . ")";
599
600
                    if ($p != end($params)) {
601
                        $this->sql .= ",";
602
                    }
603
                }
604
            }
605
        }
606
607
        if (self::$autorun) {
608
            return $this->execute(MYSQLI_BOTH);
609
        }
610
611
        return $this->sql;
612
    }
613
614
    /**
615
     * Build a statement to update a table
616
     *
617
     * @param string $table_name
618
     *            The table name to update
619
     * @param array $params
620
     *            Name/value pairs of the field name and value
621
     * @param array $where
622
     *            [optional]
623
     *            Two-dimensional array to create where clause
624
     * @param array $flags
625
     *            [optional]
626
     *            Two-dimensional array to create other flag options (joins, order, and group)
627
     *
628
     * @see Database::where()
629
     * @see Database::flags()
630
     *
631
     * @return NULL|string
632
     */
633
    public function update($table_name, $params, $where = null, $flags = null)
634
    {
635
        $this->sql = "UPDATE ";
636
        $this->query_type = self::UPDATE;
637
638
        if (! is_null($table_name) && is_string($table_name)) {
639
            $this->sql .= $table_name;
640
641 View Code Duplication
            if (isset($flags['joins'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
642
                $this->sql .= " " . implode(" ", $flags['joins']);
643
                unset($flags['joins']);
644
            }
645
646
            $this->sql .= " SET ";
647
        } else {
648
            throw new Exception("Invalid table name datatype", E_ERROR);
649
        }
650
651
        foreach ($params as $f => $p) {
652
            if ((strpos($f, "`") === false) && (strpos($f, ".") === false) && (strpos($f, "*") === false) && (stripos($f, " as ") === false)) {
653
                $f = "`{$f}`";
654
            }
655
656
            if (! is_null($p)) {
657
                $this->sql .= "$f={$this->_escape($p)},";
658
            } else {
659
                $this->sql .= "$f=NULL,";
660
            }
661
        }
662
663
        $this->sql = substr($this->sql, 0, - 1);
664
665 View Code Duplication
        if (! is_null($where) && is_array($where) && count($where)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
666
            $this->sql .= $this->where($where);
667
        }
668
669 View Code Duplication
        if (! is_null($flags) && is_array($flags) && count($flags)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
670
            $this->sql .= $this->flags($flags);
671
        }
672
673
        if (self::$autorun) {
674
            return $this->execute(MYSQLI_BOTH);
675
        }
676
677
        return $this->sql;
678
    }
679
680
    /**
681
     * Function to offer an extended updated functionality by using two different tables.
682
     *
683
     * @param string $to_be_updated
684
     *            The table that you want to update (alias 'tbu' is automatically added)
685
     * @param string $original
686
     *            The table with the data you want to overwrite to_be_updated table (alias 'o' is automatically added)
687
     * @param string $using
688
     *            The common index value between them that will join the fields
689
     * @param array|string $params
690
     *            If string only a single field is updated (tbu.$params = o.$params)
691
     *            If array each element in the array is a field to be updated (tbu.$param = o.$param)
692
     *
693
     * @return mixed
694
     */
695
    public function extended_update($to_be_updated, $original, $using, $params)
696
    {
697
        $this->sql = "UPDATE ";
698
        $this->query_type = self::EXTENDED_UPDATE;
699
700
        if (! is_null($to_be_updated) && ! is_null($original) && ! is_null($using)) {
701
            $this->sql .= "$to_be_updated tbu INNER JOIN $original o USING ($using) SET ";
702
        }
703
704
        if (is_array($params) && count($params)) {
705
            foreach ($params as $param) {
706
                if ($param != $using) {
707
                    $this->sql .= "tbu.`$param` = o.`$param`,";
708
                }
709
            }
710
            $this->sql = substr($this->sql, 0, - 1);
711
        } elseif (is_string($params)) {
712
            $this->sql .= "tbu.`$params` = o.`$params`";
713
        } else {
714
            throw new Exception("Do not understand datatype " . gettype($params), E_ERROR);
715
        }
716
717
        if (self::$autorun) {
718
            return $this->execute(MYSQL_BOTH);
719
        }
720
721
        return $this->sql;
722
    }
723
724
    /**
725
     * Function to build a replace query
726
     *
727
     * @param string $table_name
728
     *            The table to update
729
     * @param array $params
730
     *            Name/value pair to insert
731
     *
732
     * @return NULL|string
733
     */
734
    public function replace($table_name, $params)
735
    {
736
        $this->sql = null;
737
        $this->query_type = self::REPLACE;
738
739 View Code Duplication
        if (! is_null($table_name) && is_string($table_name)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
740
            $this->sql = "REPLACE INTO $table_name " . "(`" . implode("`,`", array_keys($params)) . "`)";
741
        } else {
742
            throw (new Exception("Table name is not valid", E_ERROR));
743
        }
744
745
        $this->sql .= " VALUES (" . implode(",", array_map([
746
            $this,
747
            '_escape'
748
        ], array_values($params))) . ")";
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 extended replace statement
759
     *
760
     * @param string $table_name
761
     *            Table name to update
762
     * @param array $fields
763
     *            Array of fields
764
     * @param array $params
765
     *            Two-dimensional array of values
766
     *
767
     * @return NULL|string
768
     */
769
    public function extended_replace($table_name, $fields, $params)
770
    {
771
        $this->sql = null;
772
        $this->query_type = self::EXTENDED_REPLACE;
773
774 View Code Duplication
        if (! is_null($table_name) && is_string($table_name)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
775
            $this->sql = "REPLACE INTO $table_name " . "(`" . implode("`,`", $fields) . "`)";
776
        } else {
777
            throw (new Exception("Table name is not valid", E_ERROR));
778
        }
779
780
        if (is_array($params) && count($params)) {
781
            $this->sql .= " VALUES ";
782
            foreach ($params as $p) {
783
                $this->sql .= "(" . implode(",", array_map([
784
                    $this,
785
                    '_escape'
786
                ], array_values($p))) . ")";
787
788
                if ($p != end($params)) {
789
                    $this->sql .= ",";
790
                }
791
            }
792
        }
793
794
        if (self::$autorun) {
795
            return $this->execute(MYSQLI_BOTH);
796
        }
797
798
        return $this->sql;
799
    }
800
801
    /**
802
     * Function to build a delete statement
803
     *
804
     * @param string $table_name
805
     *            Table name to act on
806
     * @param array $fields
807
     *            [optional]
808
     *            Optional list of fields to delete (used when including multiple tables)
809
     * @param array $where
810
     *            [optional]
811
     *            Optional 2-dimensional array to build where clause from
812
     * @param array $joins
813
     *            [optional]
814
     *            Optional 2-dimensional array to add other flags
815
     *
816
     * @see Database::where()
817
     * @see Database::flags()
818
     *
819
     * @return string|NULL
820
     */
821
    public function delete($table_name, $fields = null, $where = null, $joins = null)
822
    {
823
        $this->sql = "DELETE";
824
        $this->query_type = self::DELETE;
825
826
        if (! is_null($fields) && is_array($fields)) {
827
            $this->sql .= " " . implode(",", $fields);
828
        }
829
830 View Code Duplication
        if (! is_null($table_name) && is_string($table_name)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
831
            $this->sql .= " FROM $table_name";
832
        } else {
833
            throw (new Exception("Failed to create delete query, no table name", E_ERROR));
834
        }
835
836
        if (! is_null($joins) && is_array($joins) && count($joins)) {
837
            $this->sql .= " " . implode(" ", $joins);
838
        }
839
840 View Code Duplication
        if (! is_null($where) && is_array($where) && count($where)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
841
            $this->sql .= $this->where($where);
842
        }
843
844
        if (self::$autorun) {
845
            return $this->execute(MYSQLI_BOTH);
846
        }
847
848
        return $this->sql;
849
    }
850
851
    /**
852
     * Function to build a drop table statement (automatically executes)
853
     *
854
     * @param string $name
855
     *            Table to drop
856
     * @param string $type
857
     *            [optional]
858
     *            Type of item to drop ('table', 'view') (defaulted to 'table')
859
     * @param boolean $is_tmp
860
     *            [optional]
861
     *            Optional boolean if this is a temporary table
862
     *
863
     * @return string|NULL
864
     */
865
    public function drop($name, $type = 'table', $is_tmp = false)
866
    {
867
        $this->sql = null;
868
        $this->query_type = self::DROP;
869
870
        switch ($type) {
871
            case 'table':
872
                $type = 'TABLE';
873
                break;
874
            case 'view':
875
                $type = 'VIEW';
876
                break;
877
            default:
878
                throw new Exception("Invalid type " . gettype($type), E_ERROR);
879
        }
880
881
        if (! is_null($name) && is_string($name)) {
882
            $this->sql = "DROP" . ($is_tmp ? " TEMPORARY" : "") . " $type IF EXISTS `$name`";
883
        } else {
884
            throw new Exception("Table name is invalid", E_ERROR);
885
        }
886
887
        if (self::$autorun) {
888
            return $this->execute(MYSQLI_BOTH);
889
        }
890
891
        return $this->sql;
892
    }
893
894
    /**
895
     * Function to build a truncate table statement (automatically executes)
896
     *
897
     * @param string $table_name
898
     *            Table to truncate
899
     *
900
     * @return string|NULL
901
     */
902
    public function truncate($table_name)
903
    {
904
        $this->sql = null;
905
        $this->query_type = self::TRUNCATE;
906
907 View Code Duplication
        if (! is_null($table_name) && is_string($table_name)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
908
            $this->sql = "TRUNCATE TABLE $table_name";
909
        } else {
910
            throw new Exception("Table name is invalid", E_ERROR);
911
        }
912
913
        if (self::$autorun) {
914
            return $this->execute(MYSQLI_BOTH);
915
        }
916
917
        return $this->sql;
918
    }
919
920
    /**
921
     * Function to build a create temporary table statement
922
     *
923
     * @param string $table_name
924
     *            Name to give the table when creating
925
     * @param boolean $is_tmp
926
     *            [optional]
927
     *            Optional boolean to make the table a temporary table
928
     * @param mixed $select
929
     *            [optional]
930
     *            Optional parameter if null uses last built statement
931
     *            If string, will be made the SQL statement executed to create the table
932
     *            If array, 2-dimensional array with "field", "datatype" values to build table fields
933
     *
934
     * @return NULL|string
935
     */
936
    public function create_table($table_name, $is_tmp = false, $select = null)
937
    {
938
        $this->query_type = self::CREATE_TABLE;
939
940
        if (is_null($select) && ! is_null($this->sql) && substr($this->sql, 0, 6) == 'SELECT') {
941
            $this->sql = "CREATE" . ($is_tmp ? " TEMPORARY" : "") . " TABLE IF NOT EXISTS $table_name AS ($this->sql)";
942
        }
943
        if (! is_null($table_name) && is_string($table_name) && is_string($select)) {
944
            $this->sql = "CREATE" . ($is_tmp ? " TEMPORARY" : "") . " TABLE IF NOT EXISTS $table_name AS ($select)";
945
        } elseif (! is_null($table_name) && is_string($table_name) && is_array($select)) {
946
            $this->sql = "CREATE" . ($is_tmp ? " TEMPORARY" : "") . " TABLE IF NOT EXISTS $table_name (";
947
948
            foreach ($select as $field) {
949
                $default = null;
950
                if (isset($field['default'])) {
951
                    $default = (is_null($field['default']) ? "" : " DEFAULT '{$field['default']}'");
952
                }
953
                $this->sql .= "`{$field['field']}` {$field['datatype']}" . $default . (isset($field['option']) ? " {$field['option']}" : '') . ",";
954
            }
955
            $this->sql = substr($this->sql, 0, - 1) . ")";
956
        }
957
958
        if (self::$autorun) {
959
            return $this->execute();
960
        }
961
962
        return $this->sql;
963
    }
964
965
    /**
966
     * Function to create a table using a stdClass object derived from JSON
967
     *
968
     * @param \stdClass $json
969
     *
970
     * @example /examples/create_table_json.json
971
     *
972
     */
973
    public function create_table_json($json)
974
    {
975
        $this->query_type = self::CREATE_TABLE;
976
        $this->c->select_db($json->schema);
977
978
        $this->sql = "CREATE TABLE IF NOT EXISTS `{$json->name}` (";
979
        foreach ($json->fields as $field) {
980
            $this->sql .= "`{$field->name}` {$field->dataType}";
981
982
            if ($field->dataType == 'enum') {
983
                $this->sql .= "('" . implode("','", $field->values) . "')";
984
            }
985
986
            if ($field->ai) {
987
                $this->sql .= " AUTO_INCREMENT";
988
            }
989
990
            if ($field->nn) {
991
                $this->sql .= " NOT NULL";
992
            } else {
993
                if ($field->default === null) {
994
                    $this->sql .= " DEFAULT NULL";
995
                } elseif (strlen($field->default)) {
996
                    $this->sql .= " DEFAULT '{$field->default}'";
997
                }
998
            }
999
1000
            if ($field != end($json->fields)) {
1001
                $this->sql .= ",";
1002
            }
1003
        }
1004
1005
        if (isset($json->index) && count($json->index)) {
1006
            foreach ($json->index as $ind) {
1007
                $this->sql .= ", " . strtoupper($ind->type) . " `{$ind->id}` (`{$ind->ref}`)";
1008
            }
1009
        }
1010
1011
        if (isset($json->constraints) && count($json->constraints)) {
1012
            foreach ($json->constraints as $con) {
1013
                $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));
1014
            }
1015
        }
1016
1017
        if (isset($json->unique) && count($json->unique)) {
1018
            $this->sql .= ", UNIQUE(`" . implode("`,`", $json->unique) . "`)";
1019
        }
1020
1021
        if (isset($json->primary_key) && count($json->primary_key)) {
1022
            $this->sql .= ", PRIMARY KEY(`" . implode("`,`", $json->primary_key) . "`))";
1023
        } else {
1024
            if (substr($this->sql, - 1) == ',') {
1025
                $this->sql = substr($this->sql, 0, - 1);
1026
            }
1027
1028
            $this->sql .= ")";
1029
        }
1030
1031
        $this->execute(MYSQLI_BOTH);
1032
    }
1033
1034
    /**
1035
     * Function to alter a existing table
1036
     *
1037
     * @param string $table_name
1038
     *            Table to alter
1039
     * @param string $action
1040
     *            What action should be taken ('add-column', 'drop-column', 'modify-column')
1041
     * @param mixed $params
1042
     *            For add column this is a stdClass object that has the same elements as the example json
1043
     *
1044
     * @return mixed
1045
     */
1046
    public function alter_table($table_name, $action, $params)
1047
    {
1048
        $this->query_type = self::ALTER_TABLE;
1049
        $this->sql = "ALTER TABLE $table_name";
1050
        if ($action == 'add-column') {
1051
            $nn = ($params->nn ? " NOT NULL" : "");
1052
            $default = null;
1053 View Code Duplication
            if ($params->default === null) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1054
                $default = " DEFAULT NULL";
1055
            } elseif (strlen($params->default)) {
1056
                $default = " DEFAULT {$this->_escape($params->default)}";
1057
            }
1058
            $this->sql .= " ADD COLUMN `{$params->name}` {$params->dataType}" . $nn . $default;
1059
        } elseif ($action == 'drop-column') {
1060
            $this->sql .= " DROP COLUMN ";
1061
            foreach ($params as $col) {
1062
                $this->sql .= "`{$col->name}`";
1063
1064
                if ($col != end($params)) {
1065
                    $this->sql .= ",";
1066
                }
1067
            }
1068
        } elseif ($action == 'modify-column') {
1069
            $this->sql .= " MODIFY COLUMN";
1070
            $nn = ($params->nn ? " NOT NULL" : "");
1071
            $default = null;
1072 View Code Duplication
            if ($params->default === null) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1073
                $default = " DEFAULT NULL";
1074
            } elseif (strlen($params->default)) {
1075
                $default = " DEFAULT {$this->_escape($params->default)}";
1076
            }
1077
            $this->sql .= " `{$params->name}` `{$params->new_name}` {$params->dataType}" . $nn . $default;
1078
        }
1079
1080
        if (self::$autorun) {
1081
            return $this->execute();
1082
        }
1083
1084
        return $this->sql;
1085
    }
1086
1087
    /**
1088
     * Check to see if a field in a table exists
1089
     *
1090
     * @param string $table_name
1091
     *            Table to check
1092
     * @param string $field_name
1093
     *            Field name to find
1094
     *
1095
     * @return boolean Returns TRUE if field is found in that schema and table, otherwise FALSE
1096
     */
1097
    public function field_exists($table_name, $field_name)
1098
    {
1099
        $fdata = $this->field_data($table_name);
1100
1101
        if (is_array($fdata) && count($fdata)) {
1102
            foreach ($fdata as $field) {
1103
                if ($field->name == $field_name) {
1104
                    return true;
1105
                }
1106
            }
1107
        }
1108
1109
        return false;
1110
    }
1111
1112
    /**
1113
     * Function to get the column data (datatype, flags, defaults, etc)
1114
     *
1115
     * @param string $table_name
1116
     *            Table to query
1117
     * @param mixed $field
1118
     *            [optional]
1119
     *            Optional field to retrieve data (if null, returns data from all fields)
1120
     *
1121
     * @return array
1122
     */
1123
    public function field_data($table_name, $field = null)
1124
    {
1125
        if (is_null($field)) {
1126
            $res = $this->c->query("SELECT * FROM $table_name LIMIT 1");
1127
        } elseif (is_array($field)) {
1128
            $res = $this->c->query("SELECT `" . implode("`,`", $field) . "` FROM $table_name LIMIT 1");
1129
        } elseif (is_string($field)) {
1130
            $res = $this->c->query("SELECT $field FROM $table_name LIMIT 1");
1131
        } else {
1132
            return null;
1133
        }
1134
1135
        $fields = null;
1136
        if (is_a($res, 'mysqli_result')) {
1137
            $fields = $res->fetch_fields();
1138
            foreach ($fields as $i => $f) {
1139
                $fields["{$f->name}"] = $f;
1140
                unset($fields[$i]);
1141
            }
1142
        }
1143
1144
        return $fields;
1145
    }
1146
1147
    /**
1148
     * Function to check that all field parameters are set correctly
1149
     *
1150
     * @param object $field_data
1151
     * @param object $check
1152
     * @param object $pks
1153
     * @param object $index
1154
     *
1155
     * @return array
1156
     */
1157
    public function field_check($field_data, $check, $pks, $index)
1158
    {
1159
        $default = null;
1160
        $ret = null;
1161
1162
        $nn = ($check->nn ? " NOT NULL" : null);
1163
        if ($check->default === null) {
1164
            $default = " DEFAULT NULL";
1165
        } elseif (strlen($check->default)) {
1166
            $default = " DEFAULT '{$check->default}'";
1167
        }
1168
1169
        if ($field_data->type != $check->type && $check->type != MYSQLI_TYPE_ENUM) {
1170
            $this->log("{$field_data->name} wrong datatype, changing to {$check->dataType}", LogLevel::NOTICE);
1171
            $ret = " CHANGE COLUMN `{$field_data->name}` `{$check->name}` {$check->dataType}" . "{$nn}{$default}";
1172
        } elseif (! is_null($check->length) && $field_data->length != $check->length) {
1173
            $this->log("{$field_data->name} incorrect size ({$field_data->length} != {$check->length})", LogLevel::NOTICE);
1174
            $ret = " CHANGE COLUMN `{$field_data->name}` `{$check->name}` {$check->dataType}" . "{$nn}{$default}";
1175
        } elseif ($check->type == MYSQLI_TYPE_ENUM && ! ($field_data->flags & MYSQLI_ENUM_FLAG)) {
1176
            $ret = " CHANGE COLUMN `{$field_data->name}` `{$check->name}` {$check->dataType}('" . implode("','", $check->values) . "')" . "{$nn}{$default}";
1177
        }
1178
1179
        if (! is_null($index) && count($index)) {
1180
            foreach ($index as $ind) {
1181
                if ($check->name == $ind->ref && ! ($field_data->flags & MYSQLI_MULTIPLE_KEY_FLAG)) {
1182
                    $this->log("{$field_data->name} is not an index", LogLevel::NOTICE);
1183
                    $ret .= ($ret ? "," : "") . " ADD INDEX `{$ind->id}` (`{$ind->ref}` ASC)";
1184
                }
1185
            }
1186
        }
1187
1188
        if (in_array($check->name, $pks) && ! ($field_data->flags & MYSQLI_PRI_KEY_FLAG)) {
1189
            $ret .= ($ret ? "," : "") . " DROP PRIMARY KEY, ADD PRIMARY KEY(`" . implode("`,`", $pks) . "`)";
1190
        }
1191
1192
        return $ret;
1193
    }
1194
1195
    /**
1196
     * Function to check for the existence of a table within a schema
1197
     *
1198
     * @param string $schema
1199
     *            The schema to search in
1200
     * @param string $table_name
1201
     *            Table to search for
1202
     *
1203
     * @return boolean Returns number of tables that match if table is found in that schema, otherwise FALSE
1204
     */
1205
    public function table_exists(string $schema, string $table_name)
1206
    {
1207
        if (! $this->c->select_db($schema)) {
1208
            fwrite(STDOUT, $this->c->error . PHP_EOL);
1209
        }
1210
        $sql = "SHOW TABLES LIKE '{$table_name}'";
1211
1212
        if ($res = $this->c->query($sql)) {
1213
            if (is_a($res, 'mysqli_result') && $res->num_rows) {
1214
                return $res->num_rows;
1215
            }
1216
        } else {
1217
            if ($this->c->errno) {
1218
                fwrite(STDOUT, $this->c->error . PHP_EOL);
1219
            }
1220
        }
1221
1222
        return false;
1223
    }
1224
1225
    /**
1226
     * Function to detect if string is a JSON object or not
1227
     *
1228
     * @param string $val
1229
     *
1230
     * @return boolean
1231
     */
1232
    private function isJson($val)
1233
    {
1234
        json_decode($val);
1235
        return (json_last_error() == JSON_ERROR_NONE);
1236
    }
1237
1238
    /**
1239
     * Function to escape SQL characters to prevent SQL injection
1240
     *
1241
     * @param mixed $val
1242
     *            Value to escape
1243
     *
1244
     * @return string Escaped value
1245
     */
1246
    private function _escape($val)
1247
    {
1248
        if (is_null($val) || (is_string($val) && $val == 'NULL')) {
1249
            return 'NULL';
1250
        } elseif (is_numeric($val) || is_string($val)) {
1251
            if ($this->isJson($val)) {
1252
                return "'{$this->c->real_escape_string($val)}'";
1253
            } elseif (strtolower($val) == 'now()') {
1254
                return $val;
1255
            } elseif (preg_match("/\.`\w+`/", $val)) {
1256
                return $val;
1257
            }
1258
            return "'{$this->c->real_escape_string($val)}'";
1259
        } elseif (is_a($val, 'DateTime')) {
1260
            return "'{$val->format(MYSQL_DATETIME)}'";
1261
        } elseif (is_bool($val)) {
1262
            return $val ? "'1'" : "'0'";
1263
        } elseif (gettype($val) == 'object' && method_exists($val, '_escape')) {
1264
            $ret = call_user_func([
1265
                $val,
1266
                '_escape'
1267
            ]);
1268
            if ($ret !== false) {
1269
                return $ret;
1270
            } else {
1271
                throw new Exception("Error in return from _escape method in " . get_class($val), E_ERROR);
1272
            }
1273
        } elseif (gettype($val) == 'object') {
1274
            $this->log("Unknown object to escape " . get_class($val) . " in SQL string {$this->sql}", LogLevel::ERROR);
1275
        }
1276
1277
        throw new Exception("Unknown datatype to escape in SQL string {$this->sql} " . gettype($val), E_ERROR);
1278
    }
1279
1280
    /**
1281
     * Function to retrieve all results
1282
     *
1283
     * @param string $resulttype
1284
     *
1285
     * @return mixed
1286
     */
1287
    private function fetch_all($resulttype = MYSQLI_ASSOC)
1288
    {
1289
        $res = [];
1290
        if (method_exists('mysqli_result', 'fetch_all')) { // Compatibility layer with PHP < 5.3
1291
            $res = $this->result->fetch_all($resulttype);
1292
        } else {
1293
            while ($tmp = $this->result->fetch_array($resulttype)) {
1294
                $res[] = $tmp;
1295
            }
1296
        }
1297
1298
        return $res;
1299
    }
1300
1301
    /**
1302
     * Function to populate the fields for the SQL
1303
     *
1304
     * @param array $fields
1305
     *            [optional]
1306
     *            Optional array of fields to string together to create a field list
1307
     *
1308
     * @return string
1309
     */
1310
    private function fields($fields = null)
1311
    {
1312
        $str_fields = null;
1313
1314
        if (is_array($fields) && count($fields)) {
1315
            foreach ($fields as $field) {
1316
                if ((strpos($field, '`') === false) && (strpos($field, '.') === false) && (strpos($field, '*') === false) && (strpos($field, 'JSON_') === false) && (stripos($field, ' as ') === false)) {
1317
                    $str_fields .= "`$field`,";
1318
                } else {
1319
                    $str_fields .= "$field,";
1320
                }
1321
            }
1322
            $str_fields = substr($str_fields, 0, - 1);
1323
        } elseif (is_string($fields)) {
1324
            $str_fields = $fields;
1325
        } elseif (is_null($fields)) {
1326
            $str_fields = "*";
1327
        }
1328
1329
        return $str_fields;
1330
    }
1331
1332
    /**
1333
     * Function to create the where statement for the SQL
1334
     *
1335
     * @param array $where
1336
     *            Two-dimensional array to use to build the where clause
1337
     *
1338
     *            <code>
1339
     *            [<br />
1340
     *            &nbsp;&nbsp;[<br />
1341
     *            &nbsp;&nbsp;&nbsp;&nbsp;'field' => 'field_name',<br />
1342
     *            &nbsp;&nbsp;&nbsp;&nbsp;'op' => '=', // (defaults to '=', common operations or IN, NOT_IN, BETWEEN, LIKE, NOT_LIKE, IS, & IS_NOT constants)<br />
1343
     *            &nbsp;&nbsp;&nbsp;&nbsp;'value' => 'field_value',<br />
1344
     *            &nbsp;&nbsp;&nbsp;&nbsp;'sql_op' => 'AND', // NOT required for first element (common SQL operators AND, OR, NOR)<br />
1345
     *            &nbsp;&nbsp;&nbsp;&nbsp;'open-paren' => true, // optional to add a paren '(' BEFORE clause<br />
1346
     *            &nbsp;&nbsp;&nbsp;&nbsp;'close-paren' => true, // optional to add a paren ')' AFTER clause<br />
1347
     *            &nbsp;&nbsp;&nbsp;&nbsp;'low' => '1', // LOW value only used in BETWEEN clause<br />
1348
     *            &nbsp;&nbsp;&nbsp;&nbsp;'high' => '100', // HIGH value only used in BETWEEN clause<br />
1349
     *            &nbsp;&nbsp;&nbsp;&nbsp;'case_insensitive' => true // optional boolean to set the parameters to LOWER to do case insenstive comparison
1350
     *            &nbsp;&nbsp;],<br />
1351
     *            &nbsp;&nbsp;[<br />
1352
     *            &nbsp;&nbsp;&nbsp;&nbsp;...<br />
1353
     *            &nbsp;&nbsp;],<br />
1354
     *            &nbsp;&nbsp;...<br />
1355
     *            ]
1356
     *            </code>
1357
     *
1358
     * @return string
1359
     */
1360
    private function where($where)
1361
    {
1362
        $ret = " WHERE";
1363
        if (! is_array($where) || ! count($where) || ! isset($where[0])) {
1364
            $this->log("Invalid where array clause", LogLevel::WARNING);
1365
            return;
1366
        }
1367
1368
        foreach ($where as $x => $w) {
1369
            $ret .= $this->parse_clause($w, $x);
1370
        }
1371
1372
        if ($ret == " WHERE") {
1373
            $ret = '';
1374
        }
1375
1376
        return $ret;
1377
    }
1378
1379
    /**
1380
     * Function to parse the flags
1381
     *
1382
     * @param array $flags
1383
     *            Two-dimensional array to added flags
1384
     *
1385
     *            <code>
1386
     *            [
1387
     *            &nbsp;&nbsp;'joins' => [
1388
     *            &nbsp;&nbsp;&nbsp;&nbsp;"JOIN table2 t2 ON t2.id=t1.id"
1389
     *            &nbsp;&nbsp;],
1390
     *            &nbsp;&nbsp;'group' => 'field',
1391
     *            &nbsp;&nbsp;'having' => 'field',
1392
     *            &nbsp;&nbsp;'order' => 'field',
1393
     *            &nbsp;&nbsp;'start' => 0,
1394
     *            &nbsp;&nbsp;'limit' => 0
1395
     *            ]
1396
     *            </code>
1397
     *
1398
     * @see Database::groups()
1399
     * @see Database::having()
1400
     * @see Database::order()
1401
     *
1402
     * @return string
1403
     */
1404
    private function flags($flags)
1405
    {
1406
        $ret = '';
1407
1408
        if (isset($flags['group'])) {
1409
            $ret .= $this->groups($flags['group']);
1410
        }
1411
1412
        if (isset($flags['having']) && is_array($flags['having'])) {
1413
            $ret .= $this->having($flags['having']);
1414
        }
1415
1416
        if (isset($flags['order'])) {
1417
            $ret .= $this->order($flags['order']);
1418
        }
1419
1420
        if (isset($flags['limit']) && (is_string($flags['limit']) || is_numeric($flags['limit']))) {
1421
            $ret .= " LIMIT ";
1422
            if (isset($flags['start']) && (is_string($flags['start']) || is_numeric($flags['start']))) {
1423
                $ret .= "{$flags['start']},";
1424
            }
1425
            $ret .= "{$flags['limit']}";
1426
        }
1427
1428
        return $ret;
1429
    }
1430
1431
    /**
1432
     * Function to parse SQL GROUP BY statements
1433
     *
1434
     * @param mixed $groups
1435
     *
1436
     * @return string
1437
     */
1438
    private function groups($groups)
1439
    {
1440
        $ret = '';
1441
        if (is_array($groups) && count($groups)) {
1442
            $ret .= " GROUP BY";
1443
1444
            foreach ($groups as $grp) {
1445
                $ret .= " $grp";
1446
1447
                if ($grp != end($groups)) {
1448
                    $ret .= ",";
1449
                }
1450
            }
1451
        } elseif (is_string($groups)) {
1452
            $ret .= " GROUP BY {$groups}";
1453
        } else {
1454
            throw (new Exception("Error in datatype for groups " . gettype($groups), E_ERROR));
1455
        }
1456
1457
        return $ret;
1458
    }
1459
1460
    /**
1461
     * Function to parse SQL HAVING statements (same format as WHERE)
1462
     *
1463
     * @param mixed $having
1464
     *
1465
     * @return string
1466
     *
1467
     * @see Database::where()
1468
     */
1469
    private function having($having)
1470
    {
1471
        if (! is_array($having) || ! count($having) || ! isset($having[0]) || ! is_array($having[0])) {
1472
            $this->log("Invalid having parameter", LogLevel::WARNING, $having);
1473
            return;
1474
        }
1475
1476
        $ret = " HAVING";
1477
        foreach ($having as $x => $h) {
1478
            $ret .= $this->parse_clause($h, $x);
1479
        }
1480
1481
        if ($ret == " HAVING") {
1482
            $ret = '';
1483
        }
1484
1485
        return $ret;
1486
    }
1487
1488
    /**
1489
     * Function to parse SQL ORDER BY statements
1490
     *
1491
     * @param mixed $order
1492
     *
1493
     * @return string
1494
     */
1495
    private function order($order)
1496
    {
1497
        $ret = '';
1498
        if (is_array($order)) {
1499
            $ret .= " ORDER BY";
1500
1501
            foreach ($order as $ord) {
1502
                $ret .= " {$ord['field']} {$ord['sort']}";
1503
1504
                if ($ord != end($order)) {
1505
                    $ret .= ",";
1506
                }
1507
            }
1508
        } elseif (is_string($order)) {
1509
            $ret .= " ORDER BY {$order}";
1510
        }
1511
1512
        return $ret;
1513
    }
1514
1515
    /**
1516
     * Function to see if a constraint exists
1517
     *
1518
     * @param string $con_id
1519
     *
1520
     * @return boolean
1521
     */
1522
    private function is_constraint($con_id)
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
1523
    {
1524
        $res = $this->c->query("SELECT * FROM information_schema.TABLE_CONSTRAINTS WHERE CONSTRAINT_NAME = '$con_id'");
1525
1526
        if ($res->num_rows) {
1527
            return true;
1528
        }
1529
1530
        return false;
1531
    }
1532
1533
    /**
1534
     * Function to call logger and log activity
1535
     *
1536
     * @param string $msg
1537
     * @param LogLevel $level
1538
     *            [optional]
1539
     * @param array $context
1540
     *            [optional]
1541
     */
1542
    public function log($msg, $level = LogLevel::ERROR, $context = [])
1543
    {
1544
        if ($level == LogLevel::INFO) {
1545
            $this->logger->info($msg, $context);
1546
        } elseif ($level == LogLevel::WARNING) {
1547
            $this->logger->warning($msg, $context);
1548
        } elseif ($level == LogLevel::ERROR) {
1549
            $this->logger->error($msg, $context);
1550
        } elseif ($level == LogLevel::NOTICE) {
1551
            $this->logger->notice($msg, $context);
1552
        } elseif ($level == LogLevel::DEBUG) {
1553
            $this->logger->debug($msg, $context);
1554
        }
1555
    }
1556
1557
    /**
1558
     * Function to parse where and having clauses
1559
     *
1560
     * @param mixed $clause
1561
     * @param int $index
1562
     */
1563
    private function parse_clause($clause, $index)
1564
    {
1565
        $ret = null;
1566
1567
        if (! isset($clause['field']) && isset($clause['close-paren']) && $clause['close-paren']) {
1568
            $ret .= ")";
1569
            return $ret;
1570
        } elseif ($index > 0 && ! isset($clause['sql_op'])) {
1571
            $this->log("Missing sql_op field to identify how current and previous WHERE clause statements should be linked ('AND', 'OR', 'XOR', etc), skipped", LogLevel::WARNING, $clause);
1572
            return;
1573
        }
1574
1575
        $op = '=';
1576
        if (isset($clause['op'])) {
1577
            $op = $clause['op'];
1578
        }
1579
1580
        switch ($op) {
1581 View Code Duplication
            case BETWEEN:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1582
                if (! isset($clause['field']) || ! isset($clause['low']) || ! isset($clause['high'])) {
1583
                    $this->log("Missing field, low, or high for BETWEEN where clause, skipping", LogLevel::WARNING, $clause);
1584
                    return;
1585
                }
1586
                break;
1587 View Code Duplication
            default:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1588
                if (! isset($clause['field']) || ! isset($clause['value'])) {
1589
                    $this->log("Missing field or value for WHERE clause, skipping", LogLevel::WARNING, $clause);
1590
                    return;
1591
                }
1592
        }
1593
1594
        if ($index > 0) {
1595
            $ret .= " {$clause['sql_op']}";
1596
        }
1597
1598
        if (isset($clause['open-paren']) && $clause['open-paren']) {
1599
            $ret .= " (";
1600
        }
1601
1602
        if (isset($clause['backticks']) && ! $clause['backticks']) {
1603
            $field = $clause['field'];
1604
        } else {
1605
            $field = "`{$clause['field']}`";
1606
        }
1607
1608
        if ($op == IN || $op == NOT_IN) {
1609
            if (is_string($clause['value'])) {
1610
                $ret .= " {$field} {$op} " . (strpos($clause['value'], '(') !== false ? $clause['value'] : "({$clause['value']})");
1611
            } elseif (is_array($clause['value'])) {
1612
                $ret .= " {$field} {$op} (" . implode(",", array_map([
1613
                    $this,
1614
                    '_escape'
1615
                ], $clause['value'])) . ")";
1616
            } else {
1617
                $this->log("Invalid datatype for IN WHERE clause, only string and array allowed " . gettype($clause['value']), LogLevel::ERROR, $clause);
1618
                throw new Exception("Invalid datatype for IN WHERE clause", E_ERROR);
1619
            }
1620
        } elseif ($op == BETWEEN) {
1621
            $ret .= " {$field} BETWEEN {$this->_escape($clause['low'])} AND {$this->_escape($clause['high'])}";
1622
        } else {
1623
            if (isset($clause['escape']) && ! $clause['escape']) {
1624
                $value = $clause['value'];
1625
            } else {
1626
                $value = $this->_escape($clause['value']);
1627
            }
1628
1629
            if (isset($clause['case_insensitive']) && $clause['case_insensitive']) {
1630
                $ret .= " LOWER({$field}) {$op} LOWER({$this->_escape($clause['value'])})";
1631
            } elseif (preg_match("/\(SELECT/", $clause['value'])) {
1632
                $ret .= " {$field} {$op} {$clause['value']}";
1633
            } else {
1634
                $ret .= " {$field} {$op} {$value}";
1635
            }
1636
        }
1637
1638
        if (isset($clause['close-paren']) && $clause['close-paren']) {
1639
            $ret .= ")";
1640
        }
1641
1642
        return $ret;
1643
    }
1644
}