Mysql::_buildJoin()   C
last analyzed

Complexity

Conditions 7
Paths 8

Size

Total Lines 24
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 56

Importance

Changes 0
Metric Value
cc 7
eloc 15
nc 8
nop 0
dl 0
loc 24
ccs 0
cts 19
cp 0
crap 56
rs 6.7272
c 0
b 0
f 0
1
<?php
2
/**
3
 * Created by IntelliJ IDEA.
4
 * User: rozbo
5
 * Date: 2017/2/27
6
 * Time: 下午2:45
7
 */
8
9
namespace puck;
10
11
use Exception;
12
use mysqli;
13
14
class Mysql {
15
16
17
    /**
18
     * Static instance of self
19
     * @var MysqliDb
20
     */
21
    protected static $_instance;
22
    /**
23
     * Variable which holds an amount of returned rows during get/getOne/select queries
24
     * @var string
25
     */
26
    public $count = 0;
27
    /**
28
     * Variable which holds an amount of returned rows during get/getOne/select queries with withTotalCount()
29
     * @var string
30
     */
31
    public $totalCount = 0;
32
    /**
33
     * Return type: 'array' to return results as array, 'object' as object
34
     * 'json' as json string
35
     * @var string
36
     */
37
    public $returnType = 'array';
38
    public $trace = array();
39
    /**
40
     * Per page limit for pagination
41
     *
42
     * @var int
43
     */
44
45
    public $pageLimit;
46
    /**
47
     * Variable that holds total pages count of last paginate() query
48
     *
49
     * @var int
50
     */
51
    public $totalPages = 0;
52
    /**
53
     * MySQLi instance
54
     * @var mysqli
55
     */
56
    protected $_mysqli;
57
    /**
58
     * The SQL query to be prepared and executed
59
     * @var string
60
     */
61
    protected $_query;
62
    /**
63
     * The previously executed SQL query
64
     * @var string
65
     */
66
    protected $_lastQuery;
67
    /**
68
     * The SQL query options required after SELECT, INSERT, UPDATE or DELETE
69
     * @var string
70
     */
71
    protected $_queryOptions = array();
72
    /**
73
     * An array that holds where joins
74
     * @var array
75
     */
76
    protected $_join = array();
77
    /**
78
     * An array that holds where conditions
79
     * @var array
80
     */
81
    protected $_where = array();
82
    /**
83
     * An array that holds where join ands
84
     *
85
     * @var array
86
     */
87
    protected $_joinAnd = array();
88
    /**
89
     * An array that holds having conditions
90
     * @var array
91
     */
92
    protected $_having = array();
93
    /**
94
     * Dynamic type list for order by condition value
95
     * @var array
96
     */
97
    protected $_orderBy = array(); // Create the empty 0 index
98
    /**
99
     * Dynamic type list for group by condition value
100
     * @var array
101
     */
102
    protected $_groupBy = array();
103
    /**
104
     * Dynamic type list for tempromary locking tables.
105
     * @var array
106
     */
107
    protected $_tableLocks = array();
108
    /**
109
     * Variable which holds the current table lock method.
110
     * @var string
111
     */
112
    protected $_tableLockMethod = "READ";
113
    /**
114
     * Dynamic array that holds a combination of where condition/table data value types and parameter references
115
     * @var array
116
     */
117
    protected $_bindParams = array('');
118
    /**
119
     * Variable which holds last statement error
120
     * @var string
121
     */
122
    protected $_stmtError;
123
    /**
124
     * Variable which holds last statement error code
125
     * @var int
126
     */
127
    protected $_stmtErrno;
128
    /**
129
     * Database credentials
130
     * @var string
131
     */
132
    protected $host;
133
    protected $_username;
134
    protected $_password;
135
    protected $db;
136
    protected $port;
137
    protected $charset;
138
    /**
139
     * Is Subquery object
140
     * @var bool
141
     */
142
    protected $isSubQuery = false;
143
    /**
144
     * Name of the auto increment column
145
     * @var int
146
     */
147
    protected $_lastInsertId = null;
148
    /**
149
     * Column names for update when using onDuplicate method
150
     * @var array
151
     */
152
    protected $_updateColumns = null;
153
    /**
154
     * Should join() results be nested by table
155
     * @var bool
156
     */
157
    protected $_nestJoin = false;
158
    /**
159
     * FOR UPDATE flag
160
     * @var bool
161
     */
162
    protected $_forUpdate = false;
163
164
    /**
165
     * LOCK IN SHARE MODE flag
166
     * @var bool
167
     */
168
    protected $_lockInShareMode = false;
169
170
    /**
171
     * Key field for Map()'ed result array
172
     * @var string
173
     */
174
    protected $_mapKey = null;
175
176
    /**
177
     * Variables for query execution tracing
178
     */
179
    protected $traceStartQ;
180
    protected $traceEnabled;
181
    protected $traceStripPrefix;
182
    /**
183
     * Table prefix
184
     * @var string
185
     */
186
    private $prefix = '';
187
    /**
188
     * 字段列表
189
     * @var string
190
     */
191
    private $field;
192
    /**
193
     * 含有表前缀的表名
194
     * @var string
195
     */
196
    private $tableName = '';
197
198
    /**
199
     * @param string $host
200
     * @param string $username
201
     * @param string $password
202
     * @param string $db
203
     * @param int $port
204
     * @param string $charset
205
     */
206
    public function __construct($host = null, $username = null, $password = null, $db = null, $port = null, $charset = 'utf8') {
207
        $isSubQuery = false;
208
209
        // if params were passed as array
210
        if (is_array($host)) {
211
            foreach ($host as $key => $val) {
212
                $$key = $val;
213
            }
214
        }
215
        // if host were set as mysqli socket
216
        if (is_object($host)) {
217
            $this->_mysqli = $host;
218
        } else {
219
            $this->host = $host;
0 ignored issues
show
Documentation Bug introduced by
It seems like $host can also be of type array. However, the property $host is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
220
        }
221
222
        $this->_username = $username;
223
        $this->_password = $password;
224
        $this->db = $db;
225
        $this->port = $port;
226
        $this->charset = $charset;
227
228
        if ($isSubQuery) {
229
            $this->isSubQuery = true;
230
            return;
231
        }
232
233
        if (isset($prefix)) {
0 ignored issues
show
Bug introduced by
The variable $prefix seems to never exist, and therefore isset should always return false. Did you maybe rename this variable?

This check looks for calls to isset(...) or empty() on variables that are yet undefined. These calls will always produce the same result and can be removed.

This is most likely caused by the renaming of a variable or the removal of a function/method parameter.

Loading history...
234
            $this->setPrefix($prefix);
235
        }
236
237
        self::$_instance = $this;
0 ignored issues
show
Documentation Bug introduced by
It seems like $this of type this<puck\Mysql> is incompatible with the declared type object<puck\MysqliDb> of property $_instance.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
238
    }
239
240
    /**
241
     * A method of returning the static instance to allow access to the
242
     * instantiated object from within another class.
243
     * Inheriting this class would require reloading connection info.
244
     *
245
     * @uses $db = MySqliDb::getInstance();
246
     *
247
     * @return MysqliDb Returns the current instance.
248
     */
249
    public static function getInstance() {
250
        return self::$_instance;
251
    }
252
253
    /**
254
     * Method creates new mysqlidb object for a subquery generation
255
     *
256
     * @param string $subQueryAlias
257
     *
258
     * @return MysqliDb
259
     */
260
    public static function subQuery($subQueryAlias = "") {
261
        return new self(array('host' => $subQueryAlias, 'isSubQuery' => true));
0 ignored issues
show
Documentation introduced by
array('host' => $subQuer..., 'isSubQuery' => true) is of type array<string,string|bool...isSubQuery":"boolean"}>, but the function expects a string|null.

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...
262
    }
263
264
    /**
265
     * A method to disconnect from the database
266
     *
267
     * @throws Exception
268
     * @return void
269
     */
270
    public function disconnect() {
271
        if (!$this->_mysqli)
272
            return;
273
        $this->_mysqli->close();
274
        $this->_mysqli = null;
275
    }
276
277
    /**
278
     * Helper function to create dbObject with JSON return type
279
     *
280
     * @return MysqliDb
281
     */
282
    public function jsonBuilder() {
283
        $this->returnType = 'json';
284
        return $this;
285
    }
286
287
    /**
288
     * Helper function to create dbObject with object return type.
289
     *
290
     * @return MysqliDb
291
     */
292
    public function objectBuilder() {
293
        $this->returnType = 'object';
294
        return $this;
295
    }
296
297
    /**
298
     * Helper function to execute raw SQL query and return only 1 row of results.
299
     * Note that function do not add 'limit 1' to the query by itself
300
     * Same idea as getOne()
301
     *
302
     * @param string $query User-provided query to execute.
303
     * @param array $bindParams Variables array to bind to the SQL statement.
304
     *
305
     * @return array|null Contains the returned row from the query.
306
     */
307
    public function rawQueryOne($query, $bindParams = null) {
308
        $res = $this->rawQuery($query, $bindParams);
309
        if (is_array($res) && isset($res[0])) {
310
            return $res[0];
311
        }
312
313
        return null;
314
    }
315
316
    /**
317
     * Execute raw SQL query.
318
     *
319
     * @param string $query User-provided query to execute.
320
     * @param array $bindParams Variables array to bind to the SQL statement.
321
     *
322
     * @return array Contains the returned rows from the query.
323
     */
324
    public function rawQuery($query, $bindParams = null) {
325
        $params = array(''); // Create the empty 0 index
326
        $this->_query = $query;
327
        $stmt = $this->_prepareQuery();
328
329
        if (is_array($bindParams) === true) {
330
            foreach ($bindParams as $prop => $val) {
331
                $params[0] .= $this->_determineType($val);
332
                array_push($params, $bindParams[$prop]);
333
            }
334
335
            call_user_func_array(array($stmt, 'bind_param'), $this->refValues($params));
336
        }
337
338
        $stmt->execute();
339
        $this->count = $stmt->affected_rows;
340
        $this->_stmtError = $stmt->error;
341
        $this->_stmtErrno = $stmt->errno;
342
        $this->_lastQuery = $this->replacePlaceHolders($this->_query, $params);
343
        $res = $this->_dynamicBindResults($stmt);
344
        $this->reset();
345
346
        return $res;
347
    }
348
349
    /**
350
     * Method attempts to prepare the SQL query
351
     * and throws an error if there was a problem.
352
     *
353
     * @return mysqli_stmt
354
     */
355
    protected function _prepareQuery() {
356
        if (!$stmt = $this->mysqli()->prepare($this->_query)) {
357
            $msg = $this->mysqli()->error . " query: " . $this->_query;
358
            $num = $this->mysqli()->errno;
359
            $this->reset();
360
            throw new Exception($msg, $num);
361
        }
362
363
        if ($this->traceEnabled) {
364
            $this->traceStartQ = microtime(true);
365
        }
366
367
        return $stmt;
368
    }
369
370
    /**
371
     * A method to get mysqli object or create it in case needed
372
     *
373
     * @return mysqli
374
     */
375
    public function mysqli() {
376
        if (!$this->_mysqli) {
377
            $this->connect();
378
        }
379
        return $this->_mysqli;
380
    }
381
382
    /**
383
     * A method to connect to the database
384
     *
385
     * @throws Exception
386
     * @return void
387
     */
388
    public function connect() {
389
        if ($this->isSubQuery) {
390
            return;
391
        }
392
393
        if (empty($this->host)) {
394
            throw new Exception('MySQL host is not set');
395
        }
396
397
        $this->_mysqli = new mysqli($this->host, $this->_username, $this->_password, $this->db, $this->port);
398
399
        if ($this->_mysqli->connect_error) {
400
            throw new Exception('Connect Error ' . $this->_mysqli->connect_errno . ': ' . $this->_mysqli->connect_error, $this->_mysqli->connect_errno);
401
        }
402
403
        if ($this->charset) {
404
            $this->_mysqli->set_charset($this->charset);
405
        }
406
    }
407
408
    /**
409
     * Reset states after an execution
410
     *
411
     * @return MysqliDb Returns the current instance.
412
     */
413
    protected function reset() {
414
        if ($this->traceEnabled) {
415
            $this->trace[] = array($this->_lastQuery, (microtime(true) - $this->traceStartQ), $this->_traceGetCaller());
416
        }
417
418
        $this->_where = array();
419
        $this->_having = array();
420
        $this->_join = array();
421
        $this->_joinAnd = array();
422
        $this->_orderBy = array();
423
        $this->_groupBy = array();
424
        $this->_bindParams = array(''); // Create the empty 0 index
425
        $this->_query = null;
426
        $this->_queryOptions = array();
0 ignored issues
show
Documentation Bug introduced by
It seems like array() of type array is incompatible with the declared type string of property $_queryOptions.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
427
        $this->returnType = 'array';
428
        $this->_nestJoin = false;
429
        $this->_forUpdate = false;
430
        $this->_lockInShareMode = false;
431
        $this->tableName = '';
432
        $this->_lastInsertId = null;
433
        $this->_updateColumns = null;
0 ignored issues
show
Documentation Bug introduced by
It seems like null of type null is incompatible with the declared type array of property $_updateColumns.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
434
        $this->_mapKey = null;
435
    }
436
437
    /**
438
     * Get where and what function was called for query stored in MysqliDB->trace
439
     *
440
     * @return string with information
441
     */
442
    private function _traceGetCaller() {
443
        $dd = debug_backtrace();
444
        $caller = next($dd);
445
        while (isset($caller) && $caller["file"] == __FILE__) {
446
            $caller = next($dd);
447
        }
448
449
        return __CLASS__ . "->" . $caller["function"] . "() >>  file \"" .
450
            str_replace($this->traceStripPrefix, '', $caller["file"]) . "\" line #" . $caller["line"] . " ";
451
    }
452
453
    /**
454
     * This method is needed for prepared statements. They require
455
     * the data type of the field to be bound with "i" s", etc.
456
     * This function takes the input, determines what type it is,
457
     * and then updates the param_type.
458
     *
459
     * @param mixed $item Input to determine the type.
460
     *
461
     * @return string The joined parameter types.
462
     */
463
    protected function _determineType($item) {
464
        switch (gettype($item)) {
465
            case 'NULL':
466
            case 'string':
467
                return 's';
468
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
469
470
            case 'boolean':
471
            case 'integer':
472
                return 'i';
473
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
474
475
            case 'blob':
476
                return 'b';
477
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
478
479
            case 'double':
480
                return 'd';
481
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
482
        }
483
        return '';
484
    }
485
486
    /**
487
     * Referenced data array is required by mysqli since PHP 5.3+
488
     *
489
     * @param array $arr
490
     *
491
     * @return array
492
     */
493
    protected function refValues(array &$arr) {
494
        //Reference in the function arguments are required for HHVM to work
495
        //https://github.com/facebook/hhvm/issues/5155
496
        //Referenced data array is required by mysqli since PHP 5.3+
497
        if (strnatcmp(phpversion(), '5.3') >= 0) {
498
            $refs = array();
499
            foreach ($arr as $key => $value) {
500
                $refs[$key] = &$arr[$key];
501
            }
502
            return $refs;
503
        }
504
        return $arr;
505
    }
506
507
    /**
508
     * Function to replace ? with variables from bind variable
509
     *
510
     * @param string $str
511
     * @param array $vals
512
     *
513
     * @return string
514
     */
515
    protected function replacePlaceHolders($str, $vals) {
516
        $i = 1;
517
        $newStr = "";
518
519
        if (empty($vals)) {
520
            return $str;
521
        }
522
523
        while ($pos = strpos($str, "?")) {
524
            $val = $vals[$i++];
525
            if (is_object($val)) {
526
                $val = '[object]';
527
            }
528
            if ($val === null) {
529
                $val = 'NULL';
530
            }
531
            $newStr .= substr($str, 0, $pos) . "'" . $val . "'";
532
            $str = substr($str, $pos + 1);
533
        }
534
        $newStr .= $str;
535
        return $newStr;
536
    }
537
538
    /**
539
     * This helper method takes care of prepared statements' "bind_result method
540
     * , when the number of variables to pass is unknown.
541
     *
542
     * @param mysqli_stmt $stmt Equal to the prepared statement object.
543
     *
544
     * @return array The results of the SQL fetch.
545
     */
546
    protected function _dynamicBindResults(\mysqli_stmt $stmt) {
547
        $parameters = array();
548
        $results = array();
549
        /**
550
         * @see http://php.net/manual/en/mysqli-result.fetch-fields.php
551
         */
552
        $mysqlLongType = 252;
553
        $shouldStoreResult = false;
554
555
        $meta = $stmt->result_metadata();
556
557
        // if $meta is false yet sqlstate is true, there's no sql error but the query is
558
        // most likely an update/insert/delete which doesn't produce any results
559
        if (!$meta && $stmt->sqlstate)
560
            return array();
561
562
        $row = array();
563
        while ($field = $meta->fetch_field()) {
564
            if ($field->type == $mysqlLongType) {
565
                $shouldStoreResult = true;
566
            }
567
568
            if ($this->_nestJoin && $field->table != $this->tableName) {
569
                $field->table = substr($field->table, strlen($this->prefix));
570
                $row[$field->table][$field->name] = null;
571
                $parameters[] = &$row[$field->table][$field->name];
572
            } else {
573
                $row[$field->name] = null;
574
                $parameters[] = &$row[$field->name];
575
            }
576
        }
577
578
        // avoid out of memory bug in php 5.2 and 5.3. Mysqli allocates lot of memory for long*
579
        // and blob* types. So to avoid out of memory issues store_result is used
580
        // https://github.com/joshcam/PHP-MySQLi-Database-Class/pull/119
581
        if ($shouldStoreResult) {
582
            $stmt->store_result();
583
        }
584
585
        call_user_func_array(array($stmt, 'bind_result'), $parameters);
586
587
        $this->totalCount = 0;
0 ignored issues
show
Documentation Bug introduced by
The property $totalCount was declared of type string, but 0 is of type integer. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
588
        $this->count = 0;
0 ignored issues
show
Documentation Bug introduced by
The property $count was declared of type string, but 0 is of type integer. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
589
590
        while ($stmt->fetch()) {
591
            if ($this->returnType == 'object') {
592
                $result = new stdClass ();
593
                foreach ($row as $key => $val) {
594
                    if (is_array($val)) {
595
                        $result->$key = new stdClass ();
596
                        foreach ($val as $k => $v) {
597
                            $result->$key->$k = $v;
598
                        }
599
                    } else {
600
                        $result->$key = $val;
601
                    }
602
                }
603
            } else {
604
                $result = array();
605
                foreach ($row as $key => $val) {
606
                    if (is_array($val)) {
607
                        foreach ($val as $k => $v) {
608
                            $result[$key][$k] = $v;
609
                        }
610
                    } else {
611
                        $result[$key] = $val;
612
                    }
613
                }
614
            }
615
            $this->count++;
616
            if ($this->_mapKey) {
617
                $results[$row[$this->_mapKey]] = count($row) > 2 ? $result : end($result);
618
            } else {
619
                array_push($results, $result);
620
            }
621
        }
622
623
        if ($shouldStoreResult) {
624
            $stmt->free_result();
625
        }
626
627
        $stmt->close();
628
629
        // stored procedures sometimes can return more then 1 resultset
630
        if ($this->mysqli()->more_results()) {
631
            $this->mysqli()->next_result();
632
        }
633
634
        if (in_array('SQL_CALC_FOUND_ROWS', $this->_queryOptions)) {
635
            $stmt = $this->mysqli()->query('SELECT FOUND_ROWS()');
636
            $totalCount = $stmt->fetch_row();
637
            $this->totalCount = $totalCount[0];
638
        }
639
640
        if ($this->returnType == 'json') {
641
            return json_encode($results);
642
        }
643
644
        return $results;
645
    }
646
647
    /**
648
     * Helper function to execute raw SQL query and return only 1 column of results.
649
     * If 'limit 1' will be found, then string will be returned instead of array
650
     * Same idea as getValue()
651
     *
652
     * @param string $query User-provided query to execute.
653
     * @param array $bindParams Variables array to bind to the SQL statement.
654
     *
655
     * @return mixed Contains the returned rows from the query.
656
     */
657
    public function rawQueryValue($query, $bindParams = null) {
658
        $res = $this->rawQuery($query, $bindParams);
659
        if (!$res) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $res of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
660
            return null;
661
        }
662
663
        $limit = preg_match('/limit\s+1;?$/i', $query);
664
        $key = key($res[0]);
665
        if (isset($res[0][$key]) && $limit == true) {
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $limit of type integer to the boolean true. If you are specifically checking for non-zero, consider using something more explicit like > 0 or !== 0 instead.
Loading history...
666
            return $res[0][$key];
667
        }
668
669
        $newRes = Array();
670
        for ($i = 0; $i < $this->count; $i++) {
671
            $newRes[] = $res[$i][$key];
672
        }
673
        return $newRes;
674
    }
675
676
    /**
677
     * A method to perform select query
678
     *
679
     * @param string $query Contains a user-provided select query.
680
     * @param int|array $numRows Array to define SQL limit in format Array ($count, $offset)
681
     *
682
     * @return array Contains the returned rows from the query.
683
     */
684
    public function query($query, $numRows = null) {
685
        $this->_query = $query;
686
        $stmt = $this->_buildQuery($numRows);
687
        $stmt->execute();
688
        $this->_stmtError = $stmt->error;
689
        $this->_stmtErrno = $stmt->errno;
690
        $res = $this->_dynamicBindResults($stmt);
691
        $this->reset();
692
693
        return $res;
694
    }
695
696
    /**
697
     * Abstraction method that will compile the WHERE statement,
698
     * any passed update data, and the desired rows.
699
     * It then builds the SQL query.
700
     *
701
     * @param int|array $numRows Array to define SQL limit in format Array ($count, $offset)
702
     *                               or only $count
703
     * @param array $tableData Should contain an array of data for updating the database.
704
     *
705
     * @return mysqli_stmt Returns the $stmt object.
706
     */
707
    protected function _buildQuery($numRows = null, $tableData = null) {
708
        // $this->_buildJoinOld();
0 ignored issues
show
Unused Code Comprehensibility introduced by
72% 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...
709
        $this->_buildJoin();
710
        $this->_buildInsertQuery($tableData);
0 ignored issues
show
Bug introduced by
It seems like $tableData defined by parameter $tableData on line 707 can also be of type null; however, puck\Mysql::_buildInsertQuery() does only seem to accept array, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
711
        $this->_buildCondition('WHERE', $this->_where);
712
        $this->_buildGroupBy();
713
        $this->_buildCondition('HAVING', $this->_having);
714
        $this->_buildOrderBy();
715
        $this->_buildLimit($numRows);
0 ignored issues
show
Bug introduced by
It seems like $numRows defined by parameter $numRows on line 707 can also be of type null; however, puck\Mysql::_buildLimit() does only seem to accept integer|array, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
716
        $this->_buildOnDuplicate($tableData);
0 ignored issues
show
Bug introduced by
It seems like $tableData defined by parameter $tableData on line 707 can also be of type null; however, puck\Mysql::_buildOnDuplicate() does only seem to accept array, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
717
718
        if ($this->_forUpdate) {
719
            $this->_query .= ' FOR UPDATE';
720
        }
721
        if ($this->_lockInShareMode) {
722
            $this->_query .= ' LOCK IN SHARE MODE';
723
        }
724
725
        $this->_lastQuery = $this->replacePlaceHolders($this->_query, $this->_bindParams);
726
727
        if ($this->isSubQuery) {
728
            return;
729
        }
730
731
        // Prepare query
732
        $stmt = $this->_prepareQuery();
733
734
        // Bind parameters to statement if any
735
        if (count($this->_bindParams) > 1) {
736
            call_user_func_array(array($stmt, 'bind_param'), $this->refValues($this->_bindParams));
737
        }
738
739
        return $stmt;
740
    }
741
742
    /**
743
     * Abstraction method that will build an JOIN part of the query
744
     */
745
    protected function _buildJoin() {
746
        if (empty ($this->_join))
747
            return;
748
749
        foreach ($this->_join as $data) {
750
            list ($joinType, $joinTable, $joinCondition) = $data;
751
752
            if (is_object($joinTable))
753
                $joinStr = $this->_buildPair("", $joinTable);
754
            else
755
                $joinStr = $joinTable;
756
757
            $this->_query .= " " . $joinType . " JOIN " . $joinStr . " on " . $joinCondition;
758
759
            // Add join and query
760
            if (!empty($this->_joinAnd) && isset($this->_joinAnd[$joinStr])) {
761
                foreach ($this->_joinAnd[$joinStr] as $join_and_cond) {
762
                    list ($concat, $varName, $operator, $val) = $join_and_cond;
763
                    $this->_query .= " " . $concat . " " . $varName;
764
                    $this->conditionToSql($operator, $val);
765
                }
766
            }
767
        }
768
    }
769
770
    /**
771
     * Helper function to add variables into bind parameters array and will return
772
     * its SQL part of the query according to operator in ' $operator ?' or
773
     * ' $operator ($subquery) ' formats
774
     *
775
     * @param string $operator
776
     * @param mixed $value Variable with values
777
     *
778
     * @return string
779
     */
780
    protected function _buildPair($operator, $value) {
781
        if (!is_object($value)) {
782
            $this->_bindParam($value);
783
            return ' ' . $operator . ' ? ';
784
        }
785
786
        $subQuery = $value->getSubQuery();
787
        $this->_bindParams($subQuery['params']);
788
789
        return " " . $operator . " (" . $subQuery['query'] . ") " . $subQuery['alias'];
790
    }
791
792
    /**
793
     * Helper function to add variables into bind parameters array
794
     *
795
     * @param string Variable value
796
     */
797
    protected function _bindParam($value) {
798
        $this->_bindParams[0] .= $this->_determineType($value);
799
        array_push($this->_bindParams, $value);
800
    }
801
802
    /**
803
     * Helper function to add variables into bind parameters array in bulk
804
     *
805
     * @param array $values Variable with values
806
     */
807
    protected function _bindParams($values) {
808
        foreach ($values as $value) {
809
            $this->_bindParam($value);
810
        }
811
    }
812
813
    /**
814
     * Convert a condition and value into the sql string
815
     * @param  String $operator The where constraint operator
816
     * @param  String $val The where constraint value
817
     */
818
    private function conditionToSql($operator, $val) {
819
        switch (strtolower($operator)) {
820
            case 'not in':
821
            case 'in':
822
                $comparison = ' ' . $operator . ' (';
823
                if (is_object($val)) {
824
                    $comparison .= $this->_buildPair("", $val);
825
                } else {
826
                    foreach ($val as $v) {
0 ignored issues
show
Bug introduced by
The expression $val of type string is not traversable.
Loading history...
827
                        $comparison .= ' ?,';
828
                        $this->_bindParam($v);
829
                    }
830
                }
831
                $this->_query .= rtrim($comparison, ',') . ' ) ';
832
                break;
833
            case 'not between':
834
            case 'between':
835
                $this->_query .= " $operator ? AND ? ";
836
                $this->_bindParams($val);
0 ignored issues
show
Documentation introduced by
$val is of type string, but the function expects a array.

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...
837
                break;
838
            case 'not exists':
839
            case 'exists':
840
                $this->_query .= $operator . $this->_buildPair("", $val);
841
                break;
842
            default:
843
                if (is_array($val))
844
                    $this->_bindParams($val);
845
                else if ($val === null)
846
                    $this->_query .= $operator . " NULL";
847
                else if ($val != 'DBNULL' || $val == '0')
848
                    $this->_query .= $this->_buildPair($operator, $val);
849
        }
850
    }
851
852
    /**
853
     * Abstraction method that will build an INSERT or UPDATE part of the query
854
     *
855
     * @param array $tableData
856
     */
857
    protected function _buildInsertQuery($tableData) {
858
        if (!is_array($tableData)) {
859
            return;
860
        }
861
862
        $isInsert = preg_match('/^[INSERT|REPLACE]/', $this->_query);
863
        $dataColumns = array_keys($tableData);
864
        if ($isInsert) {
865
            if (isset ($dataColumns[0]))
866
                $this->_query .= ' (`' . implode($dataColumns, '`, `') . '`) ';
867
            $this->_query .= ' VALUES (';
868
        } else {
869
            $this->_query .= " SET ";
870
        }
871
872
        $this->_buildDataPairs($tableData, $dataColumns, $isInsert);
0 ignored issues
show
Documentation introduced by
$isInsert is of type integer, but the function expects a boolean.

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...
873
874
        if ($isInsert) {
875
            $this->_query .= ')';
876
        }
877
    }
878
879
    /**
880
     * Insert/Update query helper
881
     *
882
     * @param array $tableData
883
     * @param array $tableColumns
884
     * @param bool $isInsert INSERT operation flag
885
     *
886
     * @throws Exception
887
     */
888
    public function _buildDataPairs($tableData, $tableColumns, $isInsert) {
889
        foreach ($tableColumns as $column) {
890
            $value = $tableData[$column];
891
892
            if (!$isInsert) {
893
                if (strpos($column, '.') === false) {
894
                    $this->_query .= "`" . $column . "` = ";
895
                } else {
896
                    $this->_query .= str_replace('.', '.`', $column) . "` = ";
897
                }
898
            }
899
900
            // Subquery value
901
            if ($value instanceof MysqliDb) {
0 ignored issues
show
Bug introduced by
The class puck\MysqliDb does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
902
                $this->_query .= $this->_buildPair("", $value) . ", ";
903
                continue;
904
            }
905
906
            // Simple value
907
            if (!is_array($value)) {
908
                $this->_bindParam($value);
909
                $this->_query .= '?, ';
910
                continue;
911
            }
912
913
            // Function value
914
            $key = key($value);
915
            $val = $value[$key];
916
            switch ($key) {
917
                case '[I]':
918
                    $this->_query .= $column . $val . ", ";
919
                    break;
920
                case '[F]':
921
                    $this->_query .= $val[0] . ", ";
922
                    if (!empty($val[1])) {
923
                        $this->_bindParams($val[1]);
924
                    }
925
                    break;
926
                case '[N]':
927
                    if ($val == null) {
928
                        $this->_query .= "!" . $column . ", ";
929
                    } else {
930
                        $this->_query .= "!" . $val . ", ";
931
                    }
932
                    break;
933
                default:
934
                    throw new Exception("Wrong operation");
935
            }
936
        }
937
        $this->_query = rtrim($this->_query, ', ');
938
    }
939
940
    /**
941
     * Abstraction method that will build the part of the WHERE conditions
942
     *
943
     * @param string $operator
944
     * @param array $conditions
945
     */
946
    protected function _buildCondition($operator, &$conditions) {
947
        if (empty($conditions)) {
948
            return;
949
        }
950
951
        //Prepare the where portion of the query
952
        $this->_query .= ' ' . $operator;
953
954
        foreach ($conditions as $cond) {
955
            list ($concat, $varName, $operator, $val) = $cond;
956
            $this->_query .= " " . $concat . " " . $varName;
957
958
            switch (strtolower($operator)) {
959
                case 'not in':
960
                case 'in':
961
                    $comparison = ' ' . $operator . ' (';
962
                    if (is_object($val)) {
963
                        $comparison .= $this->_buildPair("", $val);
964
                    } else {
965
                        foreach ($val as $v) {
966
                            $comparison .= ' ?,';
967
                            $this->_bindParam($v);
968
                        }
969
                    }
970
                    $this->_query .= rtrim($comparison, ',') . ' ) ';
971
                    break;
972
                case 'not between':
973
                case 'between':
974
                    $this->_query .= " $operator ? AND ? ";
975
                    $this->_bindParams($val);
976
                    break;
977
                case 'not exists':
978
                case 'exists':
979
                    $this->_query .= $operator . $this->_buildPair("", $val);
980
                    break;
981
                default:
982
                    if (is_array($val)) {
983
                        $this->_bindParams($val);
984
                    } elseif ($val === null) {
985
                        $this->_query .= ' ' . $operator . " NULL";
986
                    } elseif ($val != 'DBNULL' || $val == '0') {
987
                        $this->_query .= $this->_buildPair($operator, $val);
988
                    }
989
            }
990
        }
991
    }
992
993
    /**
994
     * Abstraction method that will build the GROUP BY part of the WHERE statement
995
     *
996
     * @return void
997
     */
998
    protected function _buildGroupBy() {
999
        if (empty($this->_groupBy)) {
1000
            return;
1001
        }
1002
1003
        $this->_query .= " GROUP BY ";
1004
1005
        foreach ($this->_groupBy as $key => $value) {
1006
            $this->_query .= $value . ", ";
1007
        }
1008
1009
        $this->_query = rtrim($this->_query, ', ') . " ";
1010
    }
1011
1012
    /**
1013
     * Abstraction method that will build the LIMIT part of the WHERE statement
1014
     *
1015
     * @return void
1016
     */
1017
    protected function _buildOrderBy() {
1018
        if (empty($this->_orderBy)) {
1019
            return;
1020
        }
1021
1022
        $this->_query .= " ORDER BY ";
1023
        foreach ($this->_orderBy as $prop => $value) {
1024
            if (strtolower(str_replace(" ", "", $prop)) == 'rand()') {
1025
                $this->_query .= "rand(), ";
1026
            } else {
1027
                $this->_query .= $prop . " " . $value . ", ";
1028
            }
1029
        }
1030
1031
        $this->_query = rtrim($this->_query, ', ') . " ";
1032
    }
1033
1034
    /**
1035
     * Abstraction method that will build the LIMIT part of the WHERE statement
1036
     *
1037
     * @param int|array $numRows Array to define SQL limit in format Array ($count, $offset)
1038
     *                               or only $count
1039
     *
1040
     * @return void
1041
     */
1042
    protected function _buildLimit($numRows) {
1043
        if (!isset($numRows)) {
1044
            return;
1045
        }
1046
1047
        if (is_array($numRows)) {
1048
            $this->_query .= ' LIMIT ' . (int)$numRows[0] . ', ' . (int)$numRows[1];
1049
        } else {
1050
            $this->_query .= ' LIMIT ' . (int)$numRows;
1051
        }
1052
    }
1053
1054
    /**
1055
     * Helper function to add variables into the query statement
1056
     *
1057
     * @param array $tableData Variable with values
1058
     */
1059
    protected function _buildOnDuplicate($tableData) {
1060
        if (is_array($this->_updateColumns) && !empty($this->_updateColumns)) {
1061
            $this->_query .= " ON DUPLICATE KEY UPDATE ";
1062
            if ($this->_lastInsertId) {
1063
                $this->_query .= $this->_lastInsertId . "=LAST_INSERT_ID (" . $this->_lastInsertId . "), ";
1064
            }
1065
1066
            foreach ($this->_updateColumns as $key => $val) {
1067
                // skip all params without a value
1068
                if (is_numeric($key)) {
1069
                    $this->_updateColumns[$val] = '';
1070
                    unset($this->_updateColumns[$key]);
1071
                } else {
1072
                    $tableData[$key] = $val;
1073
                }
1074
            }
1075
            $this->_buildDataPairs($tableData, array_keys($this->_updateColumns), false);
1076
        }
1077
    }
1078
1079
    /**
1080
     * Insert method to add several rows at once
1081
     *
1082
     * @param string $tableName The name of the table.
1083
     * @param array $multiInsertData Two-dimensinal Data-array containing information for inserting into the DB.
1084
     * @param array $dataKeys Optinal Table Key names, if not set in insertDataSet.
1085
     *
1086
     * @return bool|array Boolean indicating the insertion failed (false), else return id-array ([int])
1087
     */
1088
    public function insertMulti($tableName, array $multiInsertData, array $dataKeys = null) {
1089
        // only auto-commit our inserts, if no transaction is currently running
1090
        $autoCommit = (isset($this->_transaction_in_progress) ? !$this->_transaction_in_progress : true);
0 ignored issues
show
Bug introduced by
The property _transaction_in_progress does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
1091
        $ids = array();
1092
1093
        if ($autoCommit) {
1094
            $this->startTransaction();
1095
        }
1096
1097
        foreach ($multiInsertData as $insertData) {
1098
            if ($dataKeys !== null) {
1099
                // apply column-names if given, else assume they're already given in the data
1100
                $insertData = array_combine($dataKeys, $insertData);
1101
            }
1102
1103
            $id = $this->insert($tableName, $insertData);
1104
            if (!$id) {
1105
                if ($autoCommit) {
1106
                    $this->rollback();
1107
                }
1108
                return false;
1109
            }
1110
            $ids[] = $id;
1111
        }
1112
1113
        if ($autoCommit) {
1114
            $this->commit();
1115
        }
1116
1117
        return $ids;
1118
    }
1119
1120
    /**
1121
     * Begin a transaction
1122
     *
1123
     * @uses mysqli->autocommit(false)
1124
     * @uses register_shutdown_function(array($this, "_transaction_shutdown_check"))
1125
     */
1126
    public function startTransaction() {
1127
        $this->mysqli()->autocommit(false);
1128
        $this->_transaction_in_progress = true;
1129
        register_shutdown_function(array($this, "_transaction_status_check"));
1130
    }
1131
1132
    /**
1133
     * Insert method to add new row
1134
     *
1135
     * @param string $tableName The name of the table.
1136
     * @param array $insertData Data containing information for inserting into the DB.
1137
     *
1138
     * @return bool Boolean indicating whether the insert query was completed succesfully.
1139
     */
1140
    public function insert($tableName, $insertData) {
1141
        return $this->_buildInsert($tableName, $insertData, 'INSERT');
1142
    }
1143
1144
    /**
1145
     * Internal function to build and execute INSERT/REPLACE calls
1146
     *
1147
     * @param string $tableName The name of the table.
1148
     * @param array $insertData Data containing information for inserting into the DB.
1149
     * @param string $operation Type of operation (INSERT, REPLACE)
1150
     *
1151
     * @return bool Boolean indicating whether the insert query was completed succesfully.
1152
     */
1153
    private function _buildInsert($tableName, $insertData, $operation) {
1154
        if ($this->isSubQuery) {
1155
            return;
1156
        }
1157
1158
        $this->_query = $operation . " " . implode(' ', $this->_queryOptions) . " INTO " . $this->prefix . $tableName;
1159
        $stmt = $this->_buildQuery(null, $insertData);
1160
        $status = $stmt->execute();
1161
        $this->_stmtError = $stmt->error;
1162
        $this->_stmtErrno = $stmt->errno;
1163
        $haveOnDuplicate = !empty ($this->_updateColumns);
1164
        $this->reset();
1165
        $this->count = $stmt->affected_rows;
1166
1167
        if ($stmt->affected_rows < 1) {
1168
            // in case of onDuplicate() usage, if no rows were inserted
1169
            if ($status && $haveOnDuplicate) {
1170
                return true;
1171
            }
1172
            return false;
1173
        }
1174
1175
        if ($stmt->insert_id > 0) {
1176
            return $stmt->insert_id;
1177
        }
1178
1179
        return true;
1180
    }
1181
1182
    /**
1183
     * Transaction rollback function
1184
     *
1185
     * @uses mysqli->rollback();
1186
     * @uses mysqli->autocommit(true);
1187
     */
1188
    public function rollback() {
1189
        $result = $this->mysqli()->rollback();
1190
        $this->_transaction_in_progress = false;
1191
        $this->mysqli()->autocommit(true);
1192
        return $result;
1193
    }
1194
1195
    /**
1196
     * Transaction commit
1197
     *
1198
     * @uses mysqli->commit();
1199
     * @uses mysqli->autocommit(true);
1200
     */
1201
    public function commit() {
1202
        $result = $this->mysqli()->commit();
1203
        $this->_transaction_in_progress = false;
1204
        $this->mysqli()->autocommit(true);
1205
        return $result;
1206
    }
1207
1208
    /**
1209
     * A convenient function that returns TRUE if exists at least an element that
1210
     * satisfy the where condition specified calling the "where" method before this one.
1211
     *
1212
     * @param string $tableName The name of the database table to work with.
0 ignored issues
show
Bug introduced by
There is no parameter named $tableName. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
1213
     *
1214
     * @return array Contains the returned rows from the select query.
1215
     */
1216
    public function has() {
1217
        $this->getOne($this->tableName, '1');
1218
        return $this->count >= 1;
1219
    }
1220
1221
    /**
1222
     * A convenient SELECT * function to get one record.
1223
     *
1224
     * @param string $tableName The name of the database table to work with.
1225
     * @param string $columns Desired columns
1226
     *
1227
     * @return array Contains the returned rows from the select query.
1228
     */
1229
    public function getOne($tableName, $columns = '*') {
1230
        $res = $this->get($tableName, 1, $columns);
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->get($tableName, 1, $columns); of type puck\Mysql|array adds the type puck\Mysql to the return on line 1237 which is incompatible with the return type documented by puck\Mysql::getOne of type array.
Loading history...
1231
1232
        if ($res instanceof MysqliDb) {
0 ignored issues
show
Bug introduced by
The class puck\MysqliDb does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
1233
            return $res;
1234
        } elseif (is_array($res) && isset($res[0])) {
1235
            return $res[0];
1236
        } elseif ($res) {
1237
            return $res;
1238
        }
1239
1240
        return null;
1241
    }
1242
1243
    /**
1244
     * A convenient SELECT * function.
1245
     *
1246
     * @param string $tableName The name of the database table to work with.
1247
     * @param int|array $numRows Array to define SQL limit in format Array ($count, $offset)
1248
     *                               or only $count
1249
     * @param string $columns Desired columns
1250
     *
1251
     * @return array Contains the returned rows from the select query.
1252
     */
1253
    public function get($tableName, $numRows = null, $columns = '*') {
1254
        if (empty($columns)) {
1255
            $columns = '*';
1256
        }
1257
1258
        $column = is_array($columns) ? implode(', ', $columns) : $columns;
1259
1260
        if (strpos($tableName, '.') === false) {
1261
            $this->tableName = $this->prefix . $tableName;
1262
        } else {
1263
            $this->tableName = $tableName;
1264
        }
1265
1266
        $this->_query = 'SELECT ' . implode(' ', $this->_queryOptions) . ' ' .
1267
            $column . " FROM " . $this->tableName;
1268
        $stmt = $this->_buildQuery($numRows);
1269
1270
        if ($this->isSubQuery) {
1271
            return $this;
1272
        }
1273
1274
        $stmt->execute();
1275
        $this->_stmtError = $stmt->error;
1276
        $this->_stmtErrno = $stmt->errno;
1277
        $res = $this->_dynamicBindResults($stmt);
1278
        $this->reset();
1279
1280
        return $res;
1281
    }
1282
1283
    /**
1284
     * 更新语句,在更新前请确定调用了where语句。
1285
     *
1286
     * @param array $tableData Array of data to update the desired row.
1287
     * @param int $numRows Limit on the number of rows that can be updated.
1288
     *
1289
     * @return bool
1290
     */
1291
    public function update($tableData, $numRows = null) {
1292
        if ($this->isSubQuery) {
1293
            return;
1294
        }
1295
1296
        $this->_query = "UPDATE " . $this->tableName;
1297
1298
        $stmt = $this->_buildQuery($numRows, $tableData);
1299
        $status = $stmt->execute();
1300
        $this->reset();
1301
        $this->_stmtError = $stmt->error;
1302
        $this->_stmtErrno = $stmt->errno;
1303
        $this->count = $stmt->affected_rows;
1304
1305
        return $status;
1306
    }
1307
1308
    /**
1309
     * 删除语句,请务必确定操作之前调用where语句,否则数据可能会全被干掉。
1310
     *
1311
     * @param int|array $numRows Array to define SQL limit in format Array ($count, $offset)
1312
     *                               or only $count
1313
     *
1314
     * @return bool Indicates success. 0 or 1.
1315
     */
1316
    public function delete($numRows = null) {
1317
        if ($this->isSubQuery) {
1318
            return;
1319
        }
1320
1321
        $table = $this->tableName;
1322
1323
        if (count($this->_join)) {
1324
            $this->_query = "DELETE " . preg_replace('/.* (.*)/', '$1', $table) . " FROM " . $table;
1325
        } else {
1326
            $this->_query = "DELETE FROM " . $table;
1327
        }
1328
1329
        $stmt = $this->_buildQuery($numRows);
1330
        $stmt->execute();
1331
        $this->_stmtError = $stmt->error;
1332
        $this->_stmtErrno = $stmt->errno;
1333
        $this->reset();
1334
1335
        return ($stmt->affected_rows > -1);    //	affected_rows returns 0 if nothing matched where statement, or required updating, -1 if error
1336
    }
1337
1338
    /**
1339
     * This function store update column's name and column name of the
1340
     * autoincrement column
1341
     *
1342
     * @param array $updateColumns Variable with values
1343
     * @param string $lastInsertId Variable value
1344
     *
1345
     * @return MysqliDb
1346
     */
1347
    public function onDuplicate($updateColumns, $lastInsertId = null) {
1348
        $this->_lastInsertId = $lastInsertId;
0 ignored issues
show
Documentation Bug introduced by
It seems like $lastInsertId can also be of type string. However, the property $_lastInsertId is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
1349
        $this->_updateColumns = $updateColumns;
1350
        return $this;
1351
    }
1352
1353
    /**
1354
     * This method allows you to specify multiple (method chaining optional) OR WHERE statements for SQL queries.
1355
     *
1356
     * @uses $MySqliDb->orWhere('id', 7)->orWhere('title', 'MyTitle');
1357
     *
1358
     * @param string $whereProp The name of the database field.
1359
     * @param mixed $whereValue The value of the database field.
1360
     * @param string $operator Comparison operator. Default is =
1361
     *
1362
     * @return MysqliDb
1363
     */
1364
    public function orWhere($whereProp, $whereValue = 'DBNULL', $operator = '=') {
1365
        return $this->where($whereProp, $whereValue, $operator, 'OR');
1366
    }
1367
1368
    /**
1369
     * This method allows you to specify multiple (method chaining optional) AND WHERE statements for SQL queries.
1370
     *
1371
     * @uses $MySqliDb->where('id', 7)->where('title', 'MyTitle');
1372
     *
1373
     * @param string $whereProp The name of the database field.
1374
     * @param mixed $whereValue The value of the database field.
1375
     * @param string $operator Comparison operator. Default is =
1376
     * @param string $cond Condition of where statement (OR, AND)
1377
     *
1378
     * @return MysqliDb
1379
     */
1380
    public function where($whereProp, $whereValue = 'DBNULL', $operator = '=', $cond = 'AND') {
1381
        // forkaround for an old operation api
1382
        if (is_array($whereValue) && ($key = key($whereValue)) != "0") {
1383
            $operator = $key;
1384
            $whereValue = $whereValue[$key];
1385
        }
1386
1387
        if (count($this->_where) == 0) {
1388
            $cond = '';
1389
        }
1390
1391
        $this->_where[] = array($cond, $whereProp, $operator, $whereValue);
1392
        return $this;
1393
    }
1394
1395
    /**
1396
     * This method allows you to specify multiple (method chaining optional) OR HAVING statements for SQL queries.
1397
     *
1398
     * @uses $MySqliDb->orHaving('SUM(tags) > 10')
1399
     *
1400
     * @param string $havingProp The name of the database field.
1401
     * @param mixed $havingValue The value of the database field.
1402
     * @param string $operator Comparison operator. Default is =
1403
     *
1404
     * @return MysqliDb
1405
     */
1406
    public function orHaving($havingProp, $havingValue = null, $operator = null) {
1407
        return $this->having($havingProp, $havingValue, $operator, 'OR');
1408
    }
1409
1410
    /**
1411
     * This method allows you to specify multiple (method chaining optional) AND HAVING statements for SQL queries.
1412
     *
1413
     * @uses $MySqliDb->having('SUM(tags) > 10')
1414
     *
1415
     * @param string $havingProp The name of the database field.
1416
     * @param mixed $havingValue The value of the database field.
1417
     * @param string $operator Comparison operator. Default is =
1418
     *
1419
     * @return MysqliDb
1420
     */
1421
1422
    public function having($havingProp, $havingValue = 'DBNULL', $operator = '=', $cond = 'AND') {
1423
        // forkaround for an old operation api
1424
        if (is_array($havingValue) && ($key = key($havingValue)) != "0") {
1425
            $operator = $key;
1426
            $havingValue = $havingValue[$key];
1427
        }
1428
1429
        if (count($this->_having) == 0) {
1430
            $cond = '';
1431
        }
1432
1433
        $this->_having[] = array($cond, $havingProp, $operator, $havingValue);
1434
        return $this;
1435
    }
1436
1437
    /**
1438
     * This method allows you to concatenate joins for the final SQL statement.
1439
     *
1440
     * @uses $MySqliDb->join('table1', 'field1 <> field2', 'LEFT')
1441
     *
1442
     * @param string $joinTable The name of the table.
1443
     * @param string $joinCondition the condition.
1444
     * @param string $joinType 'LEFT', 'INNER' etc.
1445
     *
1446
     * @throws Exception
1447
     * @return MysqliDb
1448
     */
1449
    public function join($joinTable, $joinCondition, $joinType = '') {
1450
        $allowedTypes = array('LEFT', 'RIGHT', 'OUTER', 'INNER', 'LEFT OUTER', 'RIGHT OUTER');
1451
        $joinType = strtoupper(trim($joinType));
1452
1453
        if ($joinType && !in_array($joinType, $allowedTypes)) {
1454
            throw new Exception('Wrong JOIN type: ' . $joinType);
1455
        }
1456
1457
        if (!is_object($joinTable)) {
1458
            $joinTable = $this->prefix . $joinTable;
1459
        }
1460
1461
        $this->_join[] = Array($joinType, $joinTable, $joinCondition);
1462
1463
        return $this;
1464
    }
1465
1466
    /**
1467
     * This is a basic method which allows you to import raw .CSV data into a table
1468
     * Please check out http://dev.mysql.com/doc/refman/5.7/en/load-data.html for a valid .csv file.
1469
     * @author Jonas Barascu (Noneatme)
1470
     * @param string $importTable The database table where the data will be imported into.
1471
     * @param string $importFile The file to be imported. Please use double backslashes \\ and make sure you
1472
     * @param string $importSettings An Array defining the import settings as described in the README.md
1473
     * @return boolean
1474
     */
1475
    public function loadData($importTable, $importFile, $importSettings = null) {
1476
        // We have to check if the file exists
1477
        if (!file_exists($importFile)) {
1478
            // Throw an exception
1479
            throw new Exception("importCSV -> importFile " . $importFile . " does not exists!");
1480
            return;
0 ignored issues
show
Unused Code introduced by
return; does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
1481
        }
1482
1483
        // Define the default values
1484
        // We will merge it later
1485
        $settings = Array("fieldChar" => ';', "lineChar" => PHP_EOL, "linesToIgnore" => 1);
1486
1487
        // Check the import settings
1488
        if (gettype($importSettings) == "array") {
1489
            // Merge the default array with the custom one
1490
            $settings = array_merge($settings, $importSettings);
1491
        }
1492
1493
        // Add the prefix to the import table
1494
        $table = $this->prefix . $importTable;
1495
1496
        // Add 1 more slash to every slash so maria will interpret it as a path
1497
        $importFile = str_replace("\\", "\\\\", $importFile);
1498
1499
        // Build SQL Syntax
1500
        $sqlSyntax = sprintf('LOAD DATA INFILE \'%s\' INTO TABLE %s',
1501
            $importFile, $table);
1502
1503
        // FIELDS
1504
        $sqlSyntax .= sprintf(' FIELDS TERMINATED BY \'%s\'', $settings["fieldChar"]);
1505
        if (isset($settings["fieldEnclosure"])) {
1506
            $sqlSyntax .= sprintf(' ENCLOSED BY \'%s\'', $settings["fieldEnclosure"]);
1507
        }
1508
1509
        // LINES
1510
        $sqlSyntax .= sprintf(' LINES TERMINATED BY \'%s\'', $settings["lineChar"]);
1511
        if (isset($settings["lineStarting"])) {
1512
            $sqlSyntax .= sprintf(' STARTING BY \'%s\'', $settings["lineStarting"]);
1513
        }
1514
1515
        // IGNORE LINES
1516
        $sqlSyntax .= sprintf(' IGNORE %d LINES', $settings["linesToIgnore"]);
1517
1518
        // Exceute the query unprepared because LOAD DATA only works with unprepared statements.
1519
        $result = $this->queryUnprepared($sqlSyntax);
1520
1521
        // Are there rows modified?
1522
        // Let the user know if the import failed / succeeded
1523
        return (bool)$result;
1524
    }
1525
1526
    /**
1527
     * Pushes a unprepared statement to the mysqli stack.
1528
     * WARNING: Use with caution.
1529
     * This method does not escape strings by default so make sure you'll never use it in production.
1530
     *
1531
     * @author Jonas Barascu
1532
     * @param [[Type]] $query [[Description]]
0 ignored issues
show
Documentation introduced by
The doc-type [[Type]] could not be parsed: Unknown type name "" at position 0. [(view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
1533
     */
1534
    private function queryUnprepared($query) {
1535
        // Execute query
1536
        $stmt = $this->mysqli()->query($query);
1537
1538
        // Failed?
1539
        if (!$stmt) {
1540
            throw new Exception("Unprepared Query Failed, ERRNO: " . $this->mysqli()->errno . " (" . $this->mysqli()->error . ")", $this->mysqli()->errno);
1541
        };
1542
1543
        // return stmt for future use
1544
        return $stmt;
1545
    }
1546
1547
    /**
1548
     * This method is usefull for importing XML files into a specific table.
1549
     * Check out the LOAD XML syntax for your MySQL server.
1550
     *
1551
     * @author Jonas Barascu
1552
     * @param  string $importTable The table in which the data will be imported to.
1553
     * @param  string $importFile The file which contains the .XML data.
1554
     * @param  string $importSettings An Array defining the import settings as described in the README.md
1555
     *
1556
     * @return boolean Returns true if the import succeeded, false if it failed.
1557
     */
1558
    public function loadXml($importTable, $importFile, $importSettings = null) {
1559
        // We have to check if the file exists
1560
        if (!file_exists($importFile)) {
1561
            // Does not exists
1562
            throw new Exception("loadXml: Import file does not exists");
1563
            return;
0 ignored issues
show
Unused Code introduced by
return; does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
1564
        }
1565
1566
        // Create default values
1567
        $settings = Array("linesToIgnore" => 0);
1568
1569
        // Check the import settings
1570
        if (gettype($importSettings) == "array") {
1571
            $settings = array_merge($settings, $importSettings);
1572
        }
1573
1574
        // Add the prefix to the import table
1575
        $table = $this->prefix . $importTable;
1576
1577
        // Add 1 more slash to every slash so maria will interpret it as a path
1578
        $importFile = str_replace("\\", "\\\\", $importFile);
1579
1580
        // Build SQL Syntax
1581
        $sqlSyntax = sprintf('LOAD XML INFILE \'%s\' INTO TABLE %s',
1582
            $importFile, $table);
1583
1584
        // FIELDS
1585
        if (isset($settings["rowTag"])) {
1586
            $sqlSyntax .= sprintf(' ROWS IDENTIFIED BY \'%s\'', $settings["rowTag"]);
1587
        }
1588
1589
        // IGNORE LINES
1590
        $sqlSyntax .= sprintf(' IGNORE %d LINES', $settings["linesToIgnore"]);
1591
1592
        // Exceute the query unprepared because LOAD XML only works with unprepared statements.
1593
        $result = $this->queryUnprepared($sqlSyntax);
1594
1595
        // Are there rows modified?
1596
        // Let the user know if the import failed / succeeded
1597
        return (bool)$result;
1598
    }
1599
1600
    /**
1601
     * This method allows you to specify multiple (method chaining optional) ORDER BY statements for SQL queries.
1602
     *
1603
     * @uses $MySqliDb->orderBy('id', 'desc')->orderBy('name', 'desc');
1604
     *
1605
     * @param string $orderByField The name of the database field.
1606
     * @param string $orderByDirection Order direction.
0 ignored issues
show
Documentation introduced by
There is no parameter named $orderByDirection. Did you maybe mean $orderbyDirection?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

Consider the following example. The parameter $ireland is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $ireland
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was changed, but the annotation was not.

Loading history...
1607
     * @param array $customFields Fieldset for ORDER BY FIELD() ordering
1608
     *
1609
     * @throws Exception
1610
     * @return MysqliDb
1611
     */
1612
    public function orderBy($orderByField, $orderbyDirection = "DESC", $customFields = null) {
1613
        $allowedDirection = Array("ASC", "DESC");
1614
        $orderbyDirection = strtoupper(trim($orderbyDirection));
1615
        $orderByField = preg_replace("/[^-a-z0-9\.\(\),_`\*\'\"]+/i", '', $orderByField);
1616
1617
        // Add table prefix to orderByField if needed.
1618
        //FIXME: We are adding prefix only if table is enclosed into `` to distinguish aliases
1619
        // from table names
1620
        $orderByField = preg_replace('/(\`)([`a-zA-Z0-9_]*\.)/', '\1' . $this->prefix . '\2', $orderByField);
1621
1622
1623
        if (empty($orderbyDirection) || !in_array($orderbyDirection, $allowedDirection)) {
1624
            throw new Exception('Wrong order direction: ' . $orderbyDirection);
1625
        }
1626
1627
        if (is_array($customFields)) {
1628
            foreach ($customFields as $key => $value) {
1629
                $customFields[$key] = preg_replace("/[^-a-z0-9\.\(\),_` ]+/i", '', $value);
1630
            }
1631
1632
            $orderByField = 'FIELD (' . $orderByField . ', "' . implode('","', $customFields) . '")';
1633
        }
1634
1635
        $this->_orderBy[$orderByField] = $orderbyDirection;
1636
        return $this;
1637
    }
1638
1639
    /**
1640
     * This method allows you to specify multiple (method chaining optional) GROUP BY statements for SQL queries.
1641
     *
1642
     * @uses $MySqliDb->groupBy('name');
1643
     *
1644
     * @param string $groupByField The name of the database field.
1645
     *
1646
     * @return MysqliDb
1647
     */
1648
    public function groupBy($groupByField) {
1649
        $groupByField = preg_replace("/[^-a-z0-9\.\(\),_\*]+/i", '', $groupByField);
1650
1651
        $this->_groupBy[] = $groupByField;
1652
        return $this;
1653
    }
1654
1655
    /**
1656
     * This method sets the current table lock method.
1657
     *
1658
     * @author Jonas Barascu
1659
     * @param  string $method The table lock method. Can be READ or WRITE.
1660
     *
1661
     * @throws Exception
1662
     * @return MysqliDb
1663
     */
1664
    public function setLockMethod($method) {
1665
        // Switch the uppercase string
1666
        switch (strtoupper($method)) {
1667
            // Is it READ or WRITE?
1668
            case "READ" || "WRITE":
1669
                // Succeed
1670
                $this->_tableLockMethod = $method;
1671
                break;
1672
            default:
1673
                // Else throw an exception
1674
                throw new Exception("Bad lock type: Can be either READ or WRITE");
1675
                break;
0 ignored issues
show
Unused Code introduced by
break; does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
1676
        }
1677
        return $this;
1678
    }
1679
1680
    /**
1681
     * Locks a table for R/W action.
1682
     *
1683
     * @author Jonas Barascu
1684
     * @param string $table The table to be locked. Can be a table or a view.
1685
     *
1686
     * @throws Exception
1687
     * @return MysqliDb if succeeeded;
1688
     */
1689
    public function lock($table) {
1690
        // Main Query
1691
        $this->_query = "LOCK TABLES";
1692
1693
        // Is the table an array?
1694
        if (gettype($table) == "array") {
1695
            // Loop trough it and attach it to the query
1696
            foreach ($table as $key => $value) {
0 ignored issues
show
Bug introduced by
The expression $table of type string is not traversable.
Loading history...
1697
                if (gettype($value) == "string") {
1698
                    if ($key > 0) {
1699
                        $this->_query .= ",";
1700
                    }
1701
                    $this->_query .= " " . $this->prefix . $value . " " . $this->_tableLockMethod;
1702
                }
1703
            }
1704
        } else {
1705
            // Build the table prefix
1706
            $table = $this->prefix . $table;
1707
1708
            // Build the query
1709
            $this->_query = "LOCK TABLES " . $table . " " . $this->_tableLockMethod;
1710
        }
1711
1712
        // Exceute the query unprepared because LOCK only works with unprepared statements.
1713
        $result = $this->queryUnprepared($this->_query);
1714
        $errno = $this->mysqli()->errno;
1715
1716
        // Reset the query
1717
        $this->reset();
1718
1719
        // Are there rows modified?
1720
        if ($result) {
1721
            // Return true
1722
            // We can't return ourself because if one table gets locked, all other ones get unlocked!
1723
            return true;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return true; (boolean) is incompatible with the return type documented by puck\Mysql::lock of type puck\MysqliDb.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
1724
        } // Something went wrong
1725
        else {
1726
            throw new Exception("Locking of table " . $table . " failed", $errno);
1727
        }
1728
1729
        // Return the success value
1730
        return false;
0 ignored issues
show
Unused Code introduced by
// Return the success value return false; does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
1731
    }
1732
1733
    /**
1734
     * Unlocks all tables in a database.
1735
     * Also commits transactions.
1736
     *
1737
     * @author Jonas Barascu
1738
     * @return MysqliDb
1739
     */
1740
    public function unlock() {
1741
        // Build the query
1742
        $this->_query = "UNLOCK TABLES";
1743
1744
        // Exceute the query unprepared because UNLOCK and LOCK only works with unprepared statements.
1745
        $result = $this->queryUnprepared($this->_query);
1746
        $errno = $this->mysqli()->errno;
1747
1748
        // Reset the query
1749
        $this->reset();
1750
1751
        // Are there rows modified?
1752
        if ($result) {
1753
            // return self
1754
            return $this;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this; (puck\Mysql) is incompatible with the return type documented by puck\Mysql::unlock of type puck\MysqliDb.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
1755
        } // Something went wrong
1756
        else {
1757
            throw new Exception("Unlocking of tables failed", $errno);
1758
        }
1759
1760
1761
        // Return self
1762
        return $this;
0 ignored issues
show
Unused Code introduced by
// Return self return $this; does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
1763
    }
1764
1765
    /**
1766
     * This methods returns the ID of the last inserted item
1767
     *
1768
     * @return int The last inserted item ID.
1769
     */
1770
    public function getInsertId() {
1771
        return $this->mysqli()->insert_id;
1772
    }
1773
1774
    /**
1775
     * Escape harmful characters which might affect a query.
1776
     *
1777
     * @param string $str The string to escape.
1778
     *
1779
     * @return string The escaped string.
1780
     */
1781
    public function escape($str) {
1782
        return $this->mysqli()->real_escape_string($str);
1783
    }
1784
1785
    /**
1786
     * Method to call mysqli->ping() to keep unused connections open on
1787
     * long-running scripts, or to reconnect timed out connections (if php.ini has
1788
     * global mysqli.reconnect set to true). Can't do this directly using object
1789
     * since _mysqli is protected.
1790
     *
1791
     * @return bool True if connection is up
1792
     */
1793
    public function ping() {
1794
        return $this->mysqli()->ping();
1795
    }
1796
1797
    /**
1798
     * @return string
1799
     */
1800
    public function getPrefix(): string {
1801
        return $this->prefix;
1802
    }
1803
1804
    /**
1805
     * Method to set a prefix
1806
     *
1807
     * @param string $prefix Contains a tableprefix
1808
     *
1809
     * @return MysqliDb
1810
     */
1811
    public function setPrefix($prefix = '') {
1812
        $this->prefix = $prefix;
1813
        return $this;
1814
    }
1815
1816
    /**
1817
     * Close connection
1818
     *
1819
     * @return void
1820
     */
1821
    public function __destruct() {
1822
        if ($this->isSubQuery) {
1823
            return;
1824
        }
1825
1826
        if ($this->_mysqli) {
1827
            $this->_mysqli->close();
1828
            $this->_mysqli = null;
1829
        }
1830
    }
1831
1832
    /**
1833
     * Method returns last executed query
1834
     *
1835
     * @return string
1836
     */
1837
    public function getLastQuery() {
1838
        return $this->_lastQuery;
1839
    }
1840
1841
    /**
1842
     * Method returns mysql error
1843
     *
1844
     * @return string
1845
     */
1846
    public function getLastError() {
1847
        if (!$this->_mysqli) {
1848
            return "mysqli is null";
1849
        }
1850
        return trim($this->_stmtError . " " . $this->mysqli()->error);
1851
    }
1852
1853
    /* Helper functions */
1854
1855
    /**
1856
     * Method returns mysql error code
1857
     * @return int
1858
     */
1859
    public function getLastErrno() {
1860
        return $this->_stmtErrno;
1861
    }
1862
1863
    /**
1864
     * Mostly internal method to get query and its params out of subquery object
1865
     * after get() and getAll()
1866
     *
1867
     * @return array
1868
     */
1869
    public function getSubQuery() {
1870
        if (!$this->isSubQuery) {
1871
            return null;
1872
        }
1873
1874
        array_shift($this->_bindParams);
1875
        $val = Array('query' => $this->_query,
1876
            'params' => $this->_bindParams,
1877
            'alias' => $this->host
1878
        );
1879
        $this->reset();
1880
        return $val;
1881
    }
1882
1883
    /**
1884
     * Method returns generated interval function as an insert/update function
1885
     *
1886
     * @param string $diff interval in the formats:
1887
     *        "1", "-1d" or "- 1 day" -- For interval - 1 day
1888
     *        Supported intervals [s]econd, [m]inute, [h]hour, [d]day, [M]onth, [Y]ear
1889
     *        Default null;
1890
     * @param string $func Initial date
1891
     *
1892
     * @return array
1893
     */
1894
    public function now($diff = null, $func = "NOW()") {
1895
        return array("[F]" => Array($this->interval($diff, $func)));
1896
    }
1897
1898
    /**
1899
     * Method returns generated interval function as a string
1900
     *
1901
     * @param string $diff interval in the formats:
1902
     *        "1", "-1d" or "- 1 day" -- For interval - 1 day
1903
     *        Supported intervals [s]econd, [m]inute, [h]hour, [d]day, [M]onth, [Y]ear
1904
     *        Default null;
1905
     * @param string $func Initial date
1906
     *
1907
     * @return string
1908
     */
1909
    public function interval($diff, $func = "NOW()") {
1910
        $types = Array("s" => "second", "m" => "minute", "h" => "hour", "d" => "day", "M" => "month", "Y" => "year");
1911
        $incr = '+';
1912
        $items = '';
1913
        $type = 'd';
1914
1915
        if ($diff && preg_match('/([+-]?) ?([0-9]+) ?([a-zA-Z]?)/', $diff, $matches)) {
1916
            if (!empty($matches[1])) {
1917
                $incr = $matches[1];
1918
            }
1919
1920
            if (!empty($matches[2])) {
1921
                $items = $matches[2];
1922
            }
1923
1924
            if (!empty($matches[3])) {
1925
                $type = $matches[3];
1926
            }
1927
1928
            if (!in_array($type, array_keys($types))) {
1929
                throw new Exception("invalid interval type in '{$diff}'");
1930
            }
1931
1932
            $func .= " " . $incr . " interval " . $items . " " . $types[$type] . " ";
1933
        }
1934
        return $func;
1935
    }
1936
1937
    /**
1938
     * Method generates incremental function call
1939
     *
1940
     * @param int $num increment by int or float. 1 by default
1941
     *
1942
     * @throws Exception
1943
     * @return array
1944
     */
1945
    public function inc($num = 1) {
1946
        if (!is_numeric($num)) {
1947
            throw new Exception('Argument supplied to inc must be a number');
1948
        }
1949
        return array("[I]" => "+" . $num);
1950
    }
1951
1952
    /**
1953
     * Method generates decrimental function call
1954
     *
1955
     * @param int $num increment by int or float. 1 by default
1956
     *
1957
     * @return array
1958
     */
1959
    public function dec($num = 1) {
1960
        if (!is_numeric($num)) {
1961
            throw new Exception('Argument supplied to dec must be a number');
1962
        }
1963
        return array("[I]" => "-" . $num);
1964
    }
1965
1966
    /**
1967
     * Method generates change boolean function call
1968
     *
1969
     * @param string $col column name. null by default
1970
     *
1971
     * @return array
1972
     */
1973
    public function not($col = null) {
1974
        return array("[N]" => (string)$col);
1975
    }
1976
1977
    /**
1978
     * Method generates user defined function call
1979
     *
1980
     * @param string $expr user function body
1981
     * @param array $bindParams
1982
     *
1983
     * @return array
1984
     */
1985
    public function func($expr, $bindParams = null) {
1986
        return array("[F]" => array($expr, $bindParams));
1987
    }
1988
1989
    /**
1990
     * Method returns a copy of a mysqlidb subquery object
1991
     *
1992
     * @return MysqliDb new mysqlidb object
1993
     */
1994
    public function copy() {
1995
        $copy = unserialize(serialize($this));
1996
        $copy->_mysqli = null;
1997
        return $copy;
1998
    }
1999
2000
    /**
2001
     * Shutdown handler to rollback uncommited operations in order to keep
2002
     * atomic operations sane.
2003
     *
2004
     * @uses mysqli->rollback();
2005
     */
2006
    public function _transaction_status_check() {
2007
        if (!$this->_transaction_in_progress) {
2008
            return;
2009
        }
2010
        $this->rollback();
2011
    }
2012
2013
    /**
2014
     * Query exection time tracking switch
2015
     *
2016
     * @param bool $enabled Enable execution time tracking
2017
     * @param string $stripPrefix Prefix to strip from the path in exec log
2018
     *
2019
     * @return MysqliDb
2020
     */
2021
    public function setTrace($enabled, $stripPrefix = null) {
2022
        $this->traceEnabled = $enabled;
2023
        $this->traceStripPrefix = $stripPrefix;
2024
        return $this;
2025
    }
2026
2027
    /**
2028
     * Method to check if needed table is created
2029
     *
2030
     * @param array $tables Table name or an Array of table names to check
2031
     *
2032
     * @return bool True if table exists
2033
     */
2034
    public function tableExists($tables) {
2035
        $tables = !is_array($tables) ? Array($tables) : $tables;
2036
        $count = count($tables);
2037
        if ($count == 0) {
2038
            return false;
2039
        }
2040
2041
        foreach ($tables as $i => $value)
2042
            $tables[$i] = $this->prefix . $value;
2043
        $this->where('table_schema', $this->db);
2044
        $this->where('table_name', $tables, 'in');
2045
        $this->get('information_schema.tables', $count);
2046
        return $this->count == $count;
2047
    }
2048
2049
    /**
2050
     * Return result as an associative array with $idField field value used as a record key
2051
     *
2052
     * Array Returns an array($k => $v) if get(.."param1, param2"), array ($k => array ($v, $v)) otherwise
2053
     *
2054
     * @param string $idField field name to use for a mapped element key
2055
     *
2056
     * @return MysqliDb
2057
     */
2058
    public function map($idField) {
2059
        $this->_mapKey = $idField;
2060
        return $this;
2061
    }
2062
2063
    /**
2064
     * This method allows you to specify multiple (method chaining optional) OR WHERE statements for the join table on part of the SQL query.
2065
     *
2066
     * @uses $dbWrapper->joinWhere('user u', 'u.id', 7)->where('user u', 'u.title', 'MyTitle');
2067
     *
2068
     * @param string $whereJoin The name of the table followed by its prefix.
2069
     * @param string $whereProp The name of the database field.
2070
     * @param mixed $whereValue The value of the database field.
2071
     *
2072
     * @return dbWrapper
2073
     */
2074
    public function joinOrWhere($whereJoin, $whereProp, $whereValue = 'DBNULL', $operator = '=', $cond = 'AND') {
0 ignored issues
show
Unused Code introduced by
The parameter $cond is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
2075
        return $this->joinWhere($whereJoin, $whereProp, $whereValue, $operator, 'OR');
2076
    }
2077
2078
    /**
2079
     * This method allows you to specify multiple (method chaining optional) AND WHERE statements for the join table on part of the SQL query.
2080
     *
2081
     * @uses $dbWrapper->joinWhere('user u', 'u.id', 7)->where('user u', 'u.title', 'MyTitle');
2082
     *
2083
     * @param string $whereJoin The name of the table followed by its prefix.
2084
     * @param string $whereProp The name of the database field.
2085
     * @param mixed $whereValue The value of the database field.
2086
     *
2087
     * @return dbWrapper
2088
     */
2089
    public function joinWhere($whereJoin, $whereProp, $whereValue = 'DBNULL', $operator = '=', $cond = 'AND') {
2090
        $this->_joinAnd[$whereJoin][] = Array($cond, $whereProp, $operator, $whereValue);
2091
        return $this;
2092
    }
2093
2094
    /**
2095
     * 设置当前要操作的数据库完整表名,当然,包含前缀。
2096
     * @param $table
2097
     * @return $this
2098
     */
2099
    public function table($table) {
2100
        $this->tableName = $table;
2101
        return $this;
2102
    }
2103
2104
    /**
2105
     * 设置当前要操作的数据表的名字,这个不包含前缀
2106
     * @param $name
2107
     * @return $this
2108
     */
2109
    public function name($name) {
2110
        $this->tableName = $this->prefix . $name;
2111
        return $this;
2112
    }
2113
2114
    public function max($field) {
2115
        return $this->value("MAX($field)");
2116
    }
2117
2118
    /**
2119
     * value操作,获取某列的查询结果,快捷操作
2120
     * @param $col string 列名
2121
     * @param null $default 默认值
2122
     * @return mixed|null
2123
     */
2124
    public function value($col, $default = null) {
2125
        $result = $this->getValue($this->tableName, $col);
2126
        return false !== $result ? $result : $default;
2127
    }
2128
2129
    /**
2130
     * A convenient SELECT COLUMN function to get a single column value from one row
2131
     *
2132
     * @param string $tableName The name of the database table to work with.
2133
     * @param string $column The desired column
2134
     * @param int $limit Limit of rows to select. Use null for unlimited..1 by default
2135
     *
2136
     * @return mixed Contains the value of a returned column / array of values
2137
     */
2138
    public function getValue($tableName, $column, $limit = 1) {
2139
        $res = $this->ArrayBuilder()->get($tableName, $limit, "{$column} AS retval");
2140
2141
        if (!$res) {
2142
            return null;
2143
        }
2144
2145
        if ($limit == 1) {
2146
            if (isset($res[0]["retval"])) {
2147
                return $res[0]["retval"];
2148
            }
2149
            return null;
2150
        }
2151
2152
        $newRes = Array();
2153
        for ($i = 0; $i < $this->count; $i++) {
2154
            $newRes[] = $res[$i]['retval'];
2155
        }
2156
        return $newRes;
2157
    }
2158
2159
    /**
2160
     * Helper function to create dbObject with array return type
2161
     * Added for consistency as thats default output type
2162
     *
2163
     * @return MysqliDb
2164
     */
2165
    public function arrayBuilder() {
2166
        $this->returnType = 'array';
2167
        return $this;
2168
    }
2169
2170
    public function min($field) {
2171
        return $this->value("MIN($field)");
2172
    }
2173
2174
    public function avg($field) {
2175
        return $this->value("MIN($field)");
2176
    }
2177
2178
    public function sum($field) {
2179
        return $this->value("SUM($field)");
2180
    }
2181
2182
    public function count($field = '*') {
2183
        return $this->value("COUNT($field)");
2184
    }
2185
2186
    public function limit($limit) {
2187
        $this->pageLimit = $limit;
2188
        return $this;
2189
    }
2190
2191
    public function find() {
2192
        return $this->getOne($this->tableName, $this->field);
2193
    }
2194
2195
    public function select() {
2196
        return $this->get($this->tableName, $this->pageLimit ? $this->pageLimit : null, $this->field);
2197
    }
2198
2199
    public function field($field) {
2200
        $this->field = $field;
2201
        return $this;
2202
    }
2203
2204
    public function add($data) {
2205
        return $this->insert($this->tableName, $data);
2206
    }
2207
2208
    /**
2209
     * 在遇到数据冲突时,删掉了旧记录,再写入新记录,这是使用 REPLACE INTO 时最大的一个误区.
2210
     * 问题在此时出现了,写入新记录时不会将你期望的没有修改的写进去,而是不去管他.而这个字段就相当于丢失了.
2211
     * 这可能并非你的逻辑上需要的.
2212
     * @param $data 要替换的数据
2213
     * @return bool 替换操作是否成功
2214
     */
2215
    public function replace($data) {
2216
        return $this->_buildInsert($this->tableName, $data, 'REPLACE');
2217
    }
2218
2219
    public function page($page, $pageLimit = '10') {
2220
        $this->pageLimit = $pageLimit;
0 ignored issues
show
Documentation Bug introduced by
The property $pageLimit was declared of type integer, but $pageLimit is of type string. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
2221
        $info = $this->paginate($this->tableName, $page, $this->field);
2222
        return $info;
2223
    }
2224
2225
    /**
2226
     * Pagination wraper to get()
2227
     *
2228
     * @access public
2229
     * @param string $table The name of the database table to work with
2230
     * @param int $page Page number
2231
     * @param array|string $fields Array or coma separated list of fields to fetch
2232
     * @return array
2233
     */
2234
    public function paginate($table, $page, $fields = null) {
2235
        $offset = $this->pageLimit * ($page - 1);
2236
        $res = $this->withTotalCount()->get($table, Array($offset, $this->pageLimit), $fields);
0 ignored issues
show
Bug introduced by
It seems like $fields defined by parameter $fields on line 2234 can also be of type array or null; however, puck\Mysql::get() does only seem to accept string, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
2237
        $this->totalPages = ceil($this->totalCount / $this->pageLimit);
0 ignored issues
show
Documentation Bug introduced by
The property $totalPages was declared of type integer, but ceil($this->totalCount / $this->pageLimit) is of type double. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
2238
        return $res;
2239
    }
2240
2241
    /**
2242
     * Function to enable SQL_CALC_FOUND_ROWS in the get queries
2243
     *
2244
     * @return MysqliDb
2245
     */
2246
    public function withTotalCount() {
2247
        $this->setQueryOption('SQL_CALC_FOUND_ROWS');
2248
        return $this;
2249
    }
2250
2251
    /**
2252
     * This method allows you to specify multiple (method chaining optional) options for SQL queries.
2253
     *
2254
     * @uses $MySqliDb->setQueryOption('name');
2255
     *
2256
     * @param string|array $options The optons name of the query.
2257
     *
2258
     * @throws Exception
2259
     * @return MysqliDb
2260
     */
2261
    public function setQueryOption($options) {
2262
        $allowedOptions = Array('ALL', 'DISTINCT', 'DISTINCTROW', 'HIGH_PRIORITY', 'STRAIGHT_JOIN', 'SQL_SMALL_RESULT',
2263
            'SQL_BIG_RESULT', 'SQL_BUFFER_RESULT', 'SQL_CACHE', 'SQL_NO_CACHE', 'SQL_CALC_FOUND_ROWS',
2264
            'LOW_PRIORITY', 'IGNORE', 'QUICK', 'MYSQLI_NESTJOIN', 'FOR UPDATE', 'LOCK IN SHARE MODE');
2265
2266
        if (!is_array($options)) {
2267
            $options = Array($options);
2268
        }
2269
2270
        foreach ($options as $option) {
2271
            $option = strtoupper($option);
2272
            if (!in_array($option, $allowedOptions)) {
2273
                throw new Exception('Wrong query option: ' . $option);
2274
            }
2275
2276
            if ($option == 'MYSQLI_NESTJOIN') {
2277
                $this->_nestJoin = true;
2278
            } elseif ($option == 'FOR UPDATE') {
2279
                $this->_forUpdate = true;
2280
            } elseif ($option == 'LOCK IN SHARE MODE') {
2281
                $this->_lockInShareMode = true;
2282
            } else {
2283
                $this->_queryOptions[] = $option;
2284
            }
2285
        }
2286
2287
        return $this;
2288
    }
2289
2290
    /**
2291
     * Abstraction method that will build an JOIN part of the query
2292
     *
2293
     * @return void
2294
     */
2295
    protected function _buildJoinOld() {
2296
        if (empty($this->_join)) {
2297
            return;
2298
        }
2299
2300
        foreach ($this->_join as $data) {
2301
            list ($joinType, $joinTable, $joinCondition) = $data;
2302
2303
            if (is_object($joinTable)) {
2304
                $joinStr = $this->_buildPair("", $joinTable);
2305
            } else {
2306
                $joinStr = $joinTable;
2307
            }
2308
2309
            $this->_query .= " " . $joinType . " JOIN " . $joinStr .
2310
                (false !== stripos($joinCondition, 'using') ? " " : " on ")
2311
                . $joinCondition;
2312
        }
2313
    }
2314
2315
2316
}