Passed
Push — master ( 42b575...2cff8f )
by Dmytro
02:57
created

DBCore::selectSingleRecord()   B

Complexity

Conditions 5
Paths 10

Size

Total Lines 24
Code Lines 15

Duplication

Lines 23
Ratio 95.83 %

Importance

Changes 3
Bugs 1 Features 2
Metric Value
cc 5
eloc 15
c 3
b 1
f 2
nc 10
nop 3
dl 23
loc 24
rs 8.5125
1
<?php
2
3
namespace Asymptix\db;
4
5
use Asymptix\core\Tools;
6
7
/**
8
 * Core database functionality.
9
 *
10
 * @category Asymptix PHP Framework
11
 * @author Dmytro Zarezenko <[email protected]>
12
 * @copyright (c) 2009 - 2016, Dmytro Zarezenko
13
 *
14
 * @git https://github.com/Asymptix/Framework
15
 * @license http://opensource.org/licenses/MIT
16
 */
17
class DBCore {
18
    /**
19
     * An array containing all the opened connections.
20
     *
21
     * @var array
22
     */
23
    protected $connections = [];
24
25
    /**
26
     * The incremented index of connections.
27
     *
28
     * @var int
29
     */
30
    protected $index = 0;
31
32
    /**
33
     * Current connection index.
34
     *
35
     * @var int
36
     */
37
    protected $currIndex = 0;
38
39
    /**
40
     * Instance of a class.
41
     *
42
     * @var DBCore
43
     */
44
    protected static $instance;
45
46
    /**
47
     * Returns an instance of this class.
48
     *
49
     * @return DBCore
50
     */
51
    public static function getInstance() {
52
        if (!isset(self::$instance)) {
53
            self::$instance = new self();
54
        }
55
56
        return self::$instance;
57
    }
58
59
    /**
60
     * Reset the internal static instance.
61
     */
62
    public static function resetInstance() {
63
        if (self::$instance) {
64
            self::$instance->reset();
65
            self::$instance = null;
66
        }
67
    }
68
69
    /**
70
     * Reset this instance of the manager.
71
     */
72
    public function reset() {
73
        foreach ($this->connections as $conn) {
74
            $conn->close();
75
        }
76
        $this->connections = [];
77
        $this->index = 0;
78
        $this->currIndex = 0;
79
    }
80
81
    /**
82
     * Seves a new connection to DBCore->connections.
83
     *
84
     * @param mysqli Object $connResource An object which represents the connection to a MySQL Server.
85
     * @param string $connName Name of the connection, if empty numeric key is used.
86
     *
87
     * @throws DBCoreException If trying to save a connection with an existing name.
88
     */
89
    public static function connection($connResource = null, $connName = null) {
90
        if ($connResource == null) {
91
            return self::getInstance()->getCurrentConnection();
92
        }
93
        self::getInstance()->openConnection($connResource, $connName);
94
    }
95
96
    /**
97
     * Seves a new connection to DBCore->connections.
98
     *
99
     * @param mysqli Object $connResource An object which represents the connection to a MySQL Server.
100
     * @param string $connName Name of the connection, if empty numeric key is used.
101
     *
102
     * @throws DBCoreException If trying to save a connection with an existing name.
103
     */
104
    public function openConnection($connResource, $connName = null) {
105
        if ($connName !== null) {
106
            $connName = (string)$connName;
107
            if (isset($this->connections[$connName])) {
108
                throw new DBCoreException("You trying to save a connection with an existing name");
109
            }
110
        } else {
111
            $connName = $this->index;
112
            $this->index++;
113
        }
114
115
        $this->connections[$connName] = $connResource;
116
    }
117
118
    /**
119
     * Get the connection instance for the passed name.
120
     *
121
     * @param string $connName Name of the connection, if empty numeric key is used.
122
     *
123
     * @return mysqli Object
124
     *
125
     * @throws DBCoreException If trying to get a non-existent connection.
126
     */
127
    public function getConnection($connName) {
128
        if (!isset($this->connections[$connName])) {
129
            throw new DBCoreException('Unknown connection: ' . $connName);
130
        }
131
132
        return $this->connections[$connName];
133
    }
134
135
    /**
136
     * Get the name of the passed connection instance.
137
     *
138
     * @param mysqli Object $connResource Connection object to be searched for.
139
     *
140
     * @return string The name of the connection.
141
     */
142
    public function getConnectionName($connResource) {
143
        return array_search($connResource, $this->connections, true);
144
    }
145
146
    /**
147
     * Closes the specified connection.
148
     *
149
     * @param mixed $connection Connection object or its name.
150
     */
151
    public function closeConnection($connection) {
152
        $key = false;
153
        if (Tools::isObject($connection)) {
154
            $connection->close();
155
            $key = $this->getConnectionName($connection);
156
        } elseif (is_string($connection)) {
157
            $key = $connection;
158
        }
159
160
        if ($key !== false) {
161
            unset($this->connections[$key]);
162
163
            if ($key === $this->currIndex) {
164
                $key = key($this->connections);
165
                $this->currIndex = ($key !== null) ? $key : 0;
166
            }
167
        }
168
169
        unset($connection);
170
    }
171
172
    /**
173
     * Returns all opened connections.
174
     *
175
     * @return array
176
     */
177
    public function getConnections() {
178
        return $this->connections;
179
    }
180
181
    /**
182
     * Sets the current connection to $key.
183
     *
184
     * @param mixed $key The connection key
185
     *
186
     * @throws DBCoreException
187
     */
188
    public function setCurrentConnection($key) {
189
        if (!$this->contains($key)) {
190
            throw new DBCoreException("Connection key '$key' does not exist.");
191
        }
192
        $this->currIndex = $key;
193
    }
194
195
    /**
196
     * Whether or not the DBCore contains specified connection.
197
     *
198
     * @param mixed $key The connection key
199
     *
200
     * @return bool
201
     */
202
    public function contains($key) {
203
        return isset($this->connections[$key]);
204
    }
205
206
    /**
207
     * Returns the number of opened connections.
208
     *
209
     * @return int
210
     */
211
    public function count() {
212
        return count($this->connections);
213
    }
214
215
    /**
216
     * Returns an ArrayIterator that iterates through all connections.
217
     *
218
     * @return ArrayIterator
219
     */
220
    public function getIterator() {
221
        return new ArrayIterator($this->connections);
222
    }
223
224
    /**
225
     * Get the current connection instance.
226
     *
227
     * @throws DBCoreException If there are no open connections
228
     *
229
     * @return mysqli Object
230
     */
231
    public function getCurrentConnection() {
232
        $key = $this->currIndex;
233
        if (!isset($this->connections[$key])) {
234
            throw new DBCoreException('There is no open connection');
235
        }
236
237
        return $this->connections[$key];
238
    }
239
240
    /**
241
     * Check database errors.
242
     *
243
     * @param object $dbObj
244
     */
245
    private static function checkDbError($dbObj) {
246
        if ($dbObj->error != "") {
247
            throw new DBCoreException($dbObj->error);
248
        }
249
    }
250
251
    /**
252
     * Bind parameters to the statment with dynamic number of parameters.
253
     *
254
     * @param resource $stmt Statement.
255
     * @param string $types Types string.
256
     * @param array $params Parameters.
257
     */
258
    private static function bindParameters($stmt, $types, $params) {
259
        $args   = [];
260
        $args[] = $types;
261
262
        foreach ($params as &$param) {
263
            $args[] = &$param;
264
        }
265
        call_user_func_array([$stmt, 'bind_param'], $args);
266
    }
267
268
    /**
269
     * Return parameters from the statment with dynamic number of parameters.
270
     *
271
     * @param resource $stmt Statement.
272
     * @param array $params Parameters.
0 ignored issues
show
Bug introduced by
There is no parameter named $params. 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...
273
     */
274
    public static function bindResults($stmt) {
275
        $resultSet = [];
276
        $metaData = $stmt->result_metadata();
0 ignored issues
show
Bug introduced by
The method result_metadata cannot be called on $stmt (of type resource).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
277
        $fieldsCounter = 0;
278
        while ($field = $metaData->fetch_field()) {
279
            if (!isset($resultSet[$field->table])) {
280
                $resultSet[$field->table] = [];
281
            }
282
            $resultSet[$field->table][$field->name] = $fieldsCounter++;
283
            $parameterName = "variable" . $fieldsCounter; //$field->name;
284
            $$parameterName = null;
285
            $parameters[] = &$$parameterName;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$parameters was never initialized. Although not strictly required by PHP, it is generally a good practice to add $parameters = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
286
        }
287
        call_user_func_array([$stmt, 'bind_result'], $parameters);
0 ignored issues
show
Bug introduced by
The variable $parameters does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
288
        if ($stmt->fetch()) {
0 ignored issues
show
Bug introduced by
The method fetch cannot be called on $stmt (of type resource).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
289
            foreach ($resultSet as &$tableResult) {
290
                foreach ($tableResult as &$fieldValue) {
291
                    $fieldValue = $parameters[$fieldValue];
292
                }
293
            }
294
295
            return $resultSet;
296
        }
297
        self::checkDbError($stmt);
0 ignored issues
show
Documentation introduced by
$stmt is of type resource, but the function expects a object.

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...
298
299
        return null;
300
    }
301
302
    /**
303
     * Execute DB SQL queries using Prepared Statements.
304
     *
305
     * @param mixed $query SQL query template string or DBPreparedQuery object
306
     *           if single parameter.
307
     * @param string $types Types string (ex: "isdb").
308
     * @param array $params Parameters in the same order like types string.
309
     *
310
     * @return mixed Statement object or FALSE if an error occurred.
311
     */
312
    private static function doQuery($query, $types = "", $params = []) {
313
        if (!Tools::isInstanceOf($query, new DBPreparedQuery())) {
0 ignored issues
show
Documentation introduced by
new \Asymptix\db\DBPreparedQuery() is of type object<Asymptix\db\DBPreparedQuery>, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
314
            $dbQuery = new DBPreparedQuery($query, $types, $params);
315
        } else {
316
            $dbQuery = $query;
317
        }
318
319
        $stmt = self::connection()->prepare($dbQuery->query);
320
        self::checkDbError(self::connection());
0 ignored issues
show
Bug introduced by
It seems like self::connection() can be null; however, checkDbError() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
321
322
        if ($dbQuery->isBindable()) {
323
            if ($dbQuery->isValid()) {
324
                self::bindParameters($stmt, $dbQuery->types, $dbQuery->params);
325
            } else {
326
                throw new DBCoreException(
327
                    "Number of types is not equal parameters number or types string is invalid"
328
                );
329
            }
330
        }
331
332
        $stmt->execute();
333
        self::checkDbError($stmt);
334
335
        return $stmt;
336
    }
337
338
339
    /**
340
     * Execute update DB SQL queries using Prepared Statements.
341
     *
342
     * @param string $query SQL query template string or DBPreparedQuery object
343
     *           if single parameter.
344
     * @param string $types Types string.
345
     * @param array $params Parameters.
346
     *
347
     * @return int Returns the number of affected rows on success and
348
     *           -1 if the last query failed.
349
     */
350
    public static function doUpdateQuery($query, $types = "", $params = []) {
351
        if (!Tools::isInstanceOf($query, new DBPreparedQuery())) {
0 ignored issues
show
Documentation introduced by
new \Asymptix\db\DBPreparedQuery() is of type object<Asymptix\db\DBPreparedQuery>, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
352
            $dbQuery = new DBPreparedQuery($query, $types, $params);
353
        } else {
354
            $dbQuery = $query;
355
        }
356
        $stmt = self::doQuery($dbQuery);
357
358
        switch ($dbQuery->getType()) {
359
            case (DBQueryType::INSERT):
360
                $result = self::connection()->insert_id;
361
                break;
362
            case (DBQueryType::UPDATE):
363
                $result = self::connection()->affected_rows;
364
                break;
365
            default:
366
                $result = self::connection()->affected_rows;
367
        }
368
        $stmt->close();
369
370
        return $result;
371
    }
372
373
    /**
374
     * Execute select DB SQL queries using Prepared Statements.
375
     *
376
     * @param mixed $query SQL query template string or DBPreparedQuery object
377
     *           if single parameter.
378
     * @param string $types Types string (ex: "isdb").
379
     * @param array $params Parameters in the same order like types string.
380
     *
381
     * @return mixed Statement object or FALSE if an error occurred.
382
     */
383
    public static function doSelectQuery($query, $types = "", $params = []) {
384
        $stmt = self::doQuery($query, $types, $params);
385
386
        $stmt->store_result();
387
        self::checkDbError($stmt);
388
389
        return $stmt;
390
    }
391
392
    /**
393
     * Returns list of database table fields.
394
     *
395
     * @param string $tableName Name of the table.
396
     * @return array<string> List of the database table fields (syntax: array[fieldName] = fieldType)
397
     */
398
    public static function getTableFieldsList($tableName) {
399
        if (!empty($tableName)) {
400
            $query = "SHOW FULL COLUMNS FROM " . $tableName;
401
            $stmt = self::doSelectQuery($query);
402
            if ($stmt !== false) {
403
                $stmt->bind_result(
404
                    $field, $type, $collation, $null, $key, $default, $extra, $privileges, $comment
0 ignored issues
show
Bug introduced by
The variable $field does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $type does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $collation does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $null does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $key does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $default does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $extra does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $privileges does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $comment does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
405
                );
406
407
                $fieldsList = [];
408
                while ($stmt->fetch()) {
409
                    $fieldsList[$field] = [
410
                        'type' => $type,
411
                        'collation' => $collation,
412
                        'null' => $null,
413
                        'key' => $key,
414
                        'default' => $default,
415
                        'extra' => $extra,
416
                        'privileges' => $privileges,
417
                        'comment' => $comment
418
                    ];
419
                }
420
                $stmt->close();
421
422
                return $fieldsList;
423
            }
424
        }
425
426
        return [];
427
    }
428
429
    /**
430
     * Returns printable SQL field value for table fields list generator.
431
     *
432
     * @param string $type SQL type of the field.
433
     * @param mixed $value Field value.
434
     *
435
     * @return string
436
     */
437
    private static function getPrintableSQLValue($type, $value) {
438
        if (strpos($type, "varchar") === 0
439
         || strpos($type, "text") === 0
440
         || strpos($type, "longtext") === 0
441
         || strpos($type, "enum") === 0
442
         || strpos($type, "char") === 0
443
         || strpos($type, "datetime") === 0
444
         || strpos($type, "timestamp") === 0
445
         || strpos($type, "date") === 0) {
446
            return ('"' . $value . '"');
447
        } elseif (strpos($type, "int") === 0
448
         || strpos($type, "tinyint") === 0
449
         || strpos($type, "smallint") === 0
450
         || strpos($type, "mediumint") === 0
451
         || strpos($type, "bigint") === 0) {
452
            if (!empty($value)) {
453
                return $value;
454
            }
455
456
            return "0";
457
        } elseif (strpos($type, "float") === 0
458
         || strpos($type, "double") === 0
459
         || strpos($type, "decimal") === 0) {
460
            if (!empty($value)) {
461
                return $value;
462
            }
463
464
            return "0.0";
465
        }
466
467
        return $value;
468
    }
469
470
    /**
471
     * Returns printable field description string for table fields list generator.
472
     *
473
     * @param string $field Field name.
474
     * @param array $attributes List of field attributes.
475
     *
476
     * @return string
477
     */
478
    public static function getPrintableFieldString($field, $attributes) {
479
        $extra = trim($attributes['extra']);
480
        $comment = trim($attributes['comment']);
481
482
        $fieldStr = "'" . $field . "' => ";
483
        if ($attributes['null'] === 'YES' && is_null($attributes['default'])) {
484
            $fieldStr.= "null";
485
        } else {
486
            $fieldStr.= self::getPrintableSQLValue($attributes['type'], $attributes['default']);
487
        }
488
        $fieldStr.= ", // " . $attributes['type'] .
489
            ", " . (($attributes['null'] == "NO") ? "not null" : "null")
490
            . ", default '" . $attributes['default'] . "'" .
491
            ($extra ? ", " . $extra : "") .
492
            ($comment ? " (" . $comment . ")" : "") . "\n";
493
494
        return $fieldStr;
495
    }
496
497
    /**
498
     * Outputs comfortable for Bean Class creation list of table fields.
499
     *
500
     * @param string $tableName Name of the Db table.
501
     */
502
    public static function displayTableFieldsList($tableName) {
503
        print("<pre>");
504
        if (!empty($tableName)) {
505
            $fieldsList = self::getTableFieldsList($tableName);
506
            if (!empty($fieldsList)) {
507
                foreach ($fieldsList as $field => $attributes) {
508
                    print(self::getPrintableFieldString($field, $attributes));
509
                }
510
            }
511
        }
512
        print("</pre>");
513
    }
514
515
    /**
516
     * Returns list of fields values with default indexes.
517
     *
518
     * @param array<mixed> $fieldsList List of the table fields (syntax: array[fieldName] = fieldValue)
519
     * @param string $idFieldName Name of the primary key field.
520
     * @return array<mixed>
521
     */
522
    private static function createValuesList($fieldsList, $idFieldName = "") {
523
        $valuesList = [];
524
        foreach ($fieldsList as $fieldName => $fieldValue) {
525
            if ($fieldName != $idFieldName) {
526
                $valuesList[] = $fieldValue;
527
            }
528
        }
529
530
        return $valuesList;
531
    }
532
533
    /**
534
     * Executes SQL INSERT query to the database.
535
     *
536
     * @param DBObject $dbObject DBObject to insert.
537
     * @param bool $debug Debug mode flag.
538
     *
539
     * @return int Insertion ID (primary key value) or null on debug.
540
     */
541
    public static function insertDBObject($dbObject, $debug = false) {
542
        $fieldsList = $dbObject->getFieldsList();
543
        $idFieldName = $dbObject->getIdFieldName();
544
545
        if (Tools::isInteger($fieldsList[$idFieldName])) {
546
            $query = "INSERT INTO " . $dbObject->getTableName() . "
547
                          SET " . DBPreparedQuery::sqlQMValuesString($fieldsList, $idFieldName);
548
            $typesString = DBPreparedQuery::sqlTypesString($fieldsList, $idFieldName);
549
            $valuesList = self::createValuesList($fieldsList, $idFieldName);
550
        } else {
551
            $query = "INSERT INTO " . $dbObject->getTableName() . "
552
                          SET " . DBPreparedQuery::sqlQMValuesString($fieldsList);
553
            $typesString = DBPreparedQuery::sqlTypesString($fieldsList);
554
            $valuesList = self::createValuesList($fieldsList);
555
        }
556
557
        if ($debug) {
558
            DBQuery::showQueryDebugInfo($query, $typesString, $valuesList);
559
560
            return null;
561
        }
562
        self::doUpdateQuery($query, $typesString, $valuesList);
563
564
        return (self::connection()->insert_id);
565
    }
566
567
    /**
568
     * Executes SQL UPDATE query to the database.
569
     *
570
     * @param DBObject $dbObject DBObject to update.
571
     * @param bool $debug Debug mode flag.
572
     *
573
     * @return int Returns the number of affected rows on success, and -1 if
574
     *           the last query failed.
575
     */
576
    public static function updateDBObject($dbObject, $debug = false) {
577
        $fieldsList = $dbObject->getFieldsList();
578
        $idFieldName = $dbObject->getIdFieldName();
579
580
        $query = "UPDATE " . $dbObject->getTableName() . "
581
                  SET " . DBPreparedQuery::sqlQMValuesString($fieldsList, $idFieldName) . "
582
                  WHERE " . $idFieldName . " = ?
583
                  LIMIT 1";
584
        $typesString = DBPreparedQuery::sqlTypesString($fieldsList, $idFieldName);
585
        if (Tools::isInteger($fieldsList[$idFieldName])) {
586
            $typesString.= "i";
587
        } else {
588
            $typesString.= "s";
589
        }
590
        $valuesList = self::createValuesList($fieldsList, $idFieldName);
591
        $valuesList[] = $dbObject->getId();
592
593
        if ($debug) {
594
            DBQuery::showQueryDebugInfo($query, $typesString, $valuesList);
595
        } else {
596
            return self::doUpdateQuery($query, $typesString, $valuesList);
597
        }
598
    }
599
600
    /**
601
     * Executes SQL DELETE query to the database.
602
     *
603
     * @param DBObject $dbObject DBObject to delete.
604
     *
605
     * @return int Returns the number of affected rows on success, and -1 if
606
     *           the last query failed.
607
     */
608
    public static function deleteDBObject($dbObject) {
609
        if (!empty($dbObject) && is_object($dbObject)) {
610
            $query = "DELETE FROM " . $dbObject->getTableName() .
611
                     " WHERE " . $dbObject->getIdFieldName() . " = ? LIMIT 1";
612
            if (Tools::isInteger($dbObject->getId())) {
613
                $typesString = "i";
614
            } else {
615
                $typesString = "s";
616
            }
617
            self::doUpdateQuery($query, $typesString, [$dbObject->getId()]);
618
619
            return (self::connection()->affected_rows);
620
        } else {
621
            return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type documented by Asymptix\db\DBCore::deleteDBObject of type integer.

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...
622
        }
623
    }
624
625
    /**
626
     * Returns DBObject from ResultSet.
627
     *
628
     * @param DBObject $dbObject
629
     * @param array $resultSet Associated by table names arrays of selected
630
     *           fields.
631
     *
632
     * @return DBObject
633
     */
634
    public static function selectDBObjectFromResultSet($dbObject, $resultSet) {
635
        $dbObject->setFieldsValues($resultSet[$dbObject->getTableName()]);
636
637
        return $dbObject;
638
    }
639
640
    /**
641
     * Returns DB object by database query statement.
642
     *
643
     * @param resource $stmt Database query statement.
644
     * @param string $className Name of the DB object class.
645
     * @return DBObject
646
     */
647
    public static function selectDBObjectFromStatement($stmt, $className) {
648
        if (is_object($className)) {
649
            $className = get_class($className);
650
        }
651
652
        if ($stmt->num_rows == 1) {
653
            $resultSet = self::bindResults($stmt);
654
            $dbObject = new $className();
655
            self::selectDBObjectFromResultSet($dbObject, $resultSet);
0 ignored issues
show
Bug introduced by
It seems like $resultSet defined by self::bindResults($stmt) on line 653 can also be of type null; however, Asymptix\db\DBCore::selectDBObjectFromResultSet() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
656
657
            if (!is_null($dbObject) && is_object($dbObject) && $dbObject->getId()) {
658
                return $dbObject;
659
            } else {
660
                return null;
661
            }
662
        } elseif ($stmt->num_rows > 1) {
663
            throw new DBCoreException("More than single record of '" . $className . "' entity selected");
664
        }
665
666
        return null;
667
    }
668
669
    /**
670
     * Selects DBObject from database.
671
     *
672
     * @param string $query SQL query.
673
     * @param string $types Types string (ex: "isdb").
674
     * @param array $params Parameters in the same order like types string.
675
     * @param mixed $instance Instance of the some DBObject class or it's class name.
676
     *
677
     * @return DBObject Selected DBObject or NULL otherwise.
678
     */
679 View Code Duplication
    public static function selectDBObject($query, $types, $params, $instance) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
680
        $stmt = self::doSelectQuery($query, $types, $params);
681
        $obj = null;
682
        if ($stmt) {
683
            $obj = self::selectDBObjectFromStatement($stmt, $instance);
684
685
            $stmt->close();
686
        }
687
688
        return $obj;
689
    }
690
691
    /**
692
     * Returns list of DB objects by database query statement.
693
     *
694
     * @param resource $stmt Database query statement.
695
     * @param mixed $className Instance of the some DBObject class or it's class name.
696
     *
697
     * @return array<DBObject>
698
     */
699
    public static function selectDBObjectsFromStatement($stmt, $className) {
700
        if (is_object($className)) {
701
            $className = get_class($className);
702
        }
703
704
        if ($stmt->num_rows > 0) {
705
            $objectsList = [];
706
            while ($resultSet = self::bindResults($stmt)) {
707
                $dbObject = new $className();
708
                self::selectDBObjectFromResultSet($dbObject, $resultSet);
709
710
                $recordId = $dbObject->getId();
711
                if (!is_null($recordId)) {
712
                    $objectsList[$recordId] = $dbObject;
713
                } else {
714
                    $objectsList[] = $dbObject;
715
                }
716
            }
717
718
            return $objectsList;
719
        }
720
721
        return [];
722
    }
723
724
    /**
725
     * Selects DBObject list from database.
726
     *
727
     * @param string $query SQL query.
728
     * @param string $types Types string (ex: "isdb").
729
     * @param array $params Parameters in the same order like types string.
730
     * @param mixed $instance Instance of the some DBObject class or it's class name.
731
     *
732
     * @return DBObject Selected DBObject or NULL otherwise.
733
     */
734 View Code Duplication
    public static function selectDBObjects($query, $types, $params, $instance) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
735
        $stmt = self::doSelectQuery($query, $types, $params);
736
        $obj = null;
737
        if ($stmt) {
738
            $obj = self::selectDBObjectsFromStatement($stmt, $instance);
739
740
            $stmt->close();
741
        }
742
743
        return $obj;
744
    }
745
746
    /**
747
     * Executes SQL query with single record and return this record.
748
     *
749
     * @param mixed $query SQL query template string or DBPreparedQuery object
750
     *           if single parameter.
751
     * @param string $types Types string (ex: "isdb").
752
     * @param array $params Parameters in the same order like types string.
753
     *
754
     * @return array Selected record with table names as keys or NULL if no
755
     *           data selected.
756
     * @throws DBCoreException If no one or more than one records selected.
757
     */
758 View Code Duplication
    public static function selectSingleRecord($query, $types = "", $params = []) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
759
        if (!Tools::isInstanceOf($query, new DBPreparedQuery())) {
0 ignored issues
show
Documentation introduced by
new \Asymptix\db\DBPreparedQuery() is of type object<Asymptix\db\DBPreparedQuery>, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
760
            $dbQuery = new DBPreparedQuery($query, $types, $params);
761
        } else {
762
            $dbQuery = $query;
763
        }
764
        $stmt = $dbQuery->go();
765
766
        if ($stmt !== false) {
767
            $record = null;
768
            if ($stmt->num_rows === 1) {
769
                $record = self::bindResults($stmt);
770
            }
771
            $stmt->close();
772
773
            if (is_null($record)) {
774
                throw new DBCoreException("No one or more than one records selected.");
775
            }
776
777
            return $record;
778
        }
779
780
        return null;
781
    }
782
783
    /**
784
     * Executes SQL query with single record and value result and return this value.
785
     *
786
     * @param mixed $query SQL query template string or DBPreparedQuery object
787
     *           if single parameter.
788
     * @param string $types Types string (ex: "isdb").
789
     * @param array $params Parameters in the same order like types string.
790
     *
791
     * @return mixed
792
     * @throws DBCoreException If no one or more than one records selected.
793
     */
794 View Code Duplication
    public static function selectSingleValue($query, $types = "", $params = []) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
795
        if (!Tools::isInstanceOf($query, new DBPreparedQuery())) {
0 ignored issues
show
Documentation introduced by
new \Asymptix\db\DBPreparedQuery() is of type object<Asymptix\db\DBPreparedQuery>, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
796
            $dbQuery = new DBPreparedQuery($query, $types, $params);
797
        } else {
798
            $dbQuery = $query;
799
        }
800
        $stmt = $dbQuery->go();
801
802
        if ($stmt !== false) {
803
            $value = null;
804
            if ($stmt->num_rows === 1) {
805
                $stmt->bind_result($value);
806
                $stmt->fetch();
807
            }
808
            $stmt->close();
809
810
            if (is_null($value)) {
811
                throw new DBCoreException("No one or more than one records selected.");
812
            }
813
814
            return $value;
815
        }
816
817
        return null;
818
    }
819
820
    /**
821
     * Calls DBCore magic methods like:
822
     *    get[User]By[Id]($userId)
823
     *    get[User]By[Email]($email)
824
     *    get[Users]()
825
     *    delete[Users]($ids)
826
     *    delete[User]($userId)
827
     *    set[User]Activation($activationFieldName, $flagValue).
828
     *
829
     * @param string $methodName Name of the magic method.
830
     * @param array $methodParams List of dynamic parameters.
831
     *
832
     * @return mixed
833
     * @throws DBCoreException
834
     */
835
    public static function __callStatic($methodName, $methodParams) {
836
        if (strrpos($methodName, "ies") == strlen($methodName) - 3) {
837
            $methodName = substr($methodName, 0, strlen($methodName) - 3) . "ys";
838
        }
839
840
        /*
841
         * Get database record object by Id
842
         */
843
        if (preg_match("#get([a-zA-Z]+)ById#", $methodName, $matches)) {
844
            $dbSelector = new DBSelector($matches[1]);
845
846
            return $dbSelector->selectDBObjectById($methodParams[0]);
847
        }
848
849
        /*
850
         * Get database record object by some field value
851
         */
852
        if (preg_match("#get([a-zA-Z]+)By([a-zA-Z]+)#", $methodName, $matches)) {
853
            if (empty($methodParams[0])) {
854
                return null;
855
            }
856
            $dbSelector = new DBSelector($matches[1]);
857
858
            $fieldName = substr(strtolower(preg_replace("#([A-Z]{1})#", "_$1", $matches[2])), 1);
859
860
            return $dbSelector->selectDBObjectByField($fieldName, $methodParams[0]);
861
        }
862
863
        /*
864
         * Get all database records
865
         */
866
        if (preg_match("#get([a-zA-Z]+)s#", $methodName, $matches)) {
867
            return self::Selector()->selectDBObjects();
868
        }
869
870
        /*
871
         * Delete selected records from the database
872
         */
873
        if (preg_match("#delete([a-zA-Z]+)s#", $methodName, $matches)) {
874
            $className = $matches[1];
875
            $idsList = $methodParams[0];
876
877
            $idsList = array_filter($idsList, "isInteger");
878
            if (!empty($idsList)) {
879
                $itemsNumber = count($idsList);
880
                $types = DBPreparedQuery::sqlSingleTypeString("i", $itemsNumber);
881
                $dbObject = new $className();
882
883
                if (!isInstanceOf($dbObject, $className)) {
884
                    throw new DBCoreException("Class with name '" . $className . "' is not exists");
885
                }
886
887
                $query = "DELETE FROM " . $dbObject->getTableName() . "
888
                          WHERE " . $dbObject->getIdFieldName() . "
889
                             IN (" . DBPreparedQuery::sqlQMString($itemsNumber) . ")";
890
891
                return self::doUpdateQuery($query, $types, $idsList);
892
            }
893
894
            return 0;
895
        }
896
897
        /*
898
         * Delete selected record from the database
899
         */
900
        if (preg_match("#delete([a-zA-Z]+)#", $methodName, $matches)) {
901
            return call_user_func(
902
                [self::getInstance(), $methodName . "s"],
903
                [$methodParams[0]]
904
            );
905
        }
906
907
        /*
908
         * Set activation value of selected records
909
         */
910
        if (preg_match("#set([a-zA-Z]+)Activation#", $methodName, $matches)) {
911
            $className = $matches[1];
912
            if (strrpos($className, "ies") == strlen($className) - 3) {
913
                $className = substr($className, 0, strlen($className) - 3) . "y";
914
            } else {
915
                $className = substr($className, 0, strlen($className) - 1);
916
            }
917
918
            $idsList = $methodParams[0];
919
            $activationFieldName = $methodParams[1];
920
            $activationValue = $methodParams[2];
921
922
            if (empty($activationFieldName)) {
923
                throw new DBCoreException("Invalid activation field name");
924
            }
925
926
            $idsList = array_filter($idsList, "isInteger");
927
            if (!empty($idsList)) {
928
                $itemsNumber = count($idsList);
929
                $types = DBPreparedQuery::sqlSingleTypeString("i", $itemsNumber);
930
                $dbObject = new $className();
931
932
                if (!isInstanceOf($dbObject, $className)) {
933
                    throw new DBCoreException("Class with name '" . $className . "' is not exists");
934
                }
935
936
                $query = "UPDATE " . $dbObject->getTableName() . " SET `" . $activationFieldName . "` = '" . $activationValue . "'
937
                          WHERE " . $dbObject->getIdFieldName() . " IN (" . DBPreparedQuery::sqlQMString($itemsNumber) . ")";
938
939
                return self::doUpdateQuery($query, $types, $idsList);
940
            }
941
        }
942
943
        throw new DBCoreException('No such method "' . $methodName . '"');
944
    }
945
946
}
947
948
/**
949
 * Service exception class.
950
 */
951
class DBCoreException extends \Exception {}
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class should be in its own file to aid autoloaders.

Having each class in a dedicated file usually plays nice with PSR autoloaders and is therefore a well established practice. If you use other autoloaders, you might not want to follow this rule.

Loading history...
952