Passed
Pull Request — master (#1)
by Dmytro
04:37
created

DBCore::selectSingleValue()   B

Complexity

Conditions 5
Paths 10

Size

Total Lines 24
Code Lines 15

Duplication

Lines 24
Ratio 100 %

Importance

Changes 5
Bugs 1 Features 2
Metric Value
cc 5
eloc 15
c 5
b 1
f 2
nc 10
nop 3
dl 24
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
 *
12
 * @author Dmytro Zarezenko <[email protected]>
13
 * @copyright (c) 2009 - 2016, Dmytro Zarezenko
14
 *
15
 * @git https://github.com/Asymptix/Framework
16
 *
17
 * @license http://opensource.org/licenses/MIT
18
 */
19
class DBCore
20
{
21
    /**
22
     * An array containing all the opened connections.
23
     *
24
     * @var array
25
     */
26
    protected $connections = [];
27
28
    /**
29
     * The incremented index of connections.
30
     *
31
     * @var int
32
     */
33
    protected $index = 0;
34
35
    /**
36
     * Current connection index.
37
     *
38
     * @var int
39
     */
40
    protected $currIndex = 0;
41
42
    /**
43
     * Instance of a class.
44
     *
45
     * @var DBCore
46
     */
47
    protected static $instance;
48
49
    /**
50
     * Returns an instance of this class.
51
     *
52
     * @return DBCore
53
     */
54
    public static function getInstance()
55
    {
56
        if (!isset(self::$instance)) {
57
            self::$instance = new self();
58
        }
59
60
        return self::$instance;
61
    }
62
63
    /**
64
     * Reset the internal static instance.
65
     *
66
     * @return void
67
     */
68
    public static function resetInstance()
69
    {
70
        if (self::$instance) {
71
            self::$instance->reset();
72
            self::$instance = null;
73
        }
74
    }
75
76
    /**
77
     * Reset this instance of the manager.
78
     *
79
     * @return void
80
     */
81
    public function reset()
82
    {
83
        foreach ($this->connections as $conn) {
84
            $conn->close();
85
        }
86
        $this->connections = [];
87
        $this->index = 0;
88
        $this->currIndex = 0;
89
    }
90
91
    /**
92
     * Seves a new connection to DBCore->connections.
93
     *
94
     * @param mysqli Object $connResource An object which represents the connection to a MySQL Server.
95
     * @param string        $connName     Name of the connection, if empty numeric key is used.
96
     *
97
     * @throws DBCoreException If trying to save a connection with an existing name.
98
     */
99
    public static function connection($connResource = null, $connName = null)
100
    {
101
        if ($connResource == null) {
102
            return self::getInstance()->getCurrentConnection();
103
        }
104
        self::getInstance()->openConnection($connResource, $connName);
105
    }
106
107
    /**
108
     * Seves a new connection to DBCore->connections.
109
     *
110
     * @param mysqli Object $connResource An object which represents the connection to a MySQL Server.
111
     * @param string        $connName     Name of the connection, if empty numeric key is used.
112
     *
113
     * @throws DBCoreException If trying to save a connection with an existing name.
114
     */
115
    public function openConnection($connResource, $connName = null)
116
    {
117
        if ($connName !== null) {
118
            $connName = (string) $connName;
119
            if (isset($this->connections[$connName])) {
120
                throw new DBCoreException('You trying to save a connection with an existing name');
121
            }
122
        } else {
123
            $connName = $this->index;
124
            $this->index++;
125
        }
126
127
        $this->connections[$connName] = $connResource;
128
    }
129
130
    /**
131
     * Get the connection instance for the passed name.
132
     *
133
     * @param string $connName Name of the connection, if empty numeric key is used.
134
     *
135
     * @throws DBCoreException If trying to get a non-existent connection.
136
     *
137
     * @return mysqli Object
138
     */
139
    public function getConnection($connName)
140
    {
141
        if (!isset($this->connections[$connName])) {
142
            throw new DBCoreException('Unknown connection: '.$connName);
143
        }
144
145
        return $this->connections[$connName];
146
    }
147
148
    /**
149
     * Get the name of the passed connection instance.
150
     *
151
     * @param mysqli Object $connResource Connection object to be searched for.
152
     *
153
     * @return string The name of the connection.
154
     */
155
    public function getConnectionName($connResource)
156
    {
157
        return array_search($connResource, $this->connections, true);
158
    }
159
160
    /**
161
     * Closes the specified connection.
162
     *
163
     * @param mixed $connection Connection object or its name.
164
     */
165
    public function closeConnection($connection)
166
    {
167
        $key = false;
168
        if (Tools::isObject($connection)) {
169
            $connection->close();
170
            $key = $this->getConnectionName($connection);
171
        } elseif (is_string($connection)) {
172
            $key = $connection;
173
        }
174
175
        if ($key !== false) {
176
            unset($this->connections[$key]);
177
178
            if ($key === $this->currIndex) {
179
                $key = key($this->connections);
180
                $this->currIndex = ($key !== null) ? $key : 0;
181
            }
182
        }
183
184
        unset($connection);
185
    }
186
187
    /**
188
     * Returns all opened connections.
189
     *
190
     * @return array
191
     */
192
    public function getConnections()
193
    {
194
        return $this->connections;
195
    }
196
197
    /**
198
     * Sets the current connection to $key.
199
     *
200
     * @param mixed $key The connection key
201
     *
202
     * @throws DBCoreException
203
     */
204
    public function setCurrentConnection($key)
205
    {
206
        if (!$this->contains($key)) {
207
            throw new DBCoreException("Connection key '$key' does not exist.");
208
        }
209
        $this->currIndex = $key;
210
    }
211
212
    /**
213
     * Whether or not the DBCore contains specified connection.
214
     *
215
     * @param mixed $key The connection key
216
     *
217
     * @return bool
218
     */
219
    public function contains($key)
220
    {
221
        return isset($this->connections[$key]);
222
    }
223
224
    /**
225
     * Returns the number of opened connections.
226
     *
227
     * @return int
228
     */
229
    public function count()
230
    {
231
        return count($this->connections);
232
    }
233
234
    /**
235
     * Returns an ArrayIterator that iterates through all connections.
236
     *
237
     * @return ArrayIterator
238
     */
239
    public function getIterator()
240
    {
241
        return new ArrayIterator($this->connections);
242
    }
243
244
    /**
245
     * Get the current connection instance.
246
     *
247
     * @throws DBCoreException If there are no open connections
248
     *
249
     * @return mysqli Object
250
     */
251
    public function getCurrentConnection()
252
    {
253
        $key = $this->currIndex;
254
        if (!isset($this->connections[$key])) {
255
            throw new DBCoreException('There is no open connection');
256
        }
257
258
        return $this->connections[$key];
259
    }
260
261
    /**
262
     * Check database errors.
263
     *
264
     * @param object $dbObj
265
     */
266
    private static function checkDbError($dbObj)
267
    {
268
        if ($dbObj->error != '') {
269
            throw new DBCoreException($dbObj->error);
270
        }
271
    }
272
273
    /**
274
     * Bind parameters to the statment with dynamic number of parameters.
275
     *
276
     * @param resource $stmt   Statement.
277
     * @param string   $types  Types string.
278
     * @param array    $params Parameters.
279
     */
280
    private static function bindParameters($stmt, $types, $params)
281
    {
282
        $args = [];
283
        $args[] = $types;
284
285
        foreach ($params as &$param) {
286
            $args[] = &$param;
287
        }
288
        call_user_func_array([$stmt, 'bind_param'], $args);
289
    }
290
291
    /**
292
     * Return parameters from the statment with dynamic number of parameters.
293
     *
294
     * @param resource $stmt   Statement.
295
     * @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...
296
     */
297
    public static function bindResults($stmt)
298
    {
299
        $resultSet = [];
300
        $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...
301
        $fieldsCounter = 0;
302
        while ($field = $metaData->fetch_field()) {
303
            if (!isset($resultSet[$field->table])) {
304
                $resultSet[$field->table] = [];
305
            }
306
            $resultSet[$field->table][$field->name] = $fieldsCounter++;
307
            $parameterName = 'variable'.$fieldsCounter; //$field->name;
308
            $$parameterName = null;
309
            $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...
310
        }
311
        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...
312
        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...
313
            foreach ($resultSet as &$tableResult) {
314
                foreach ($tableResult as &$fieldValue) {
315
                    $fieldValue = $parameters[$fieldValue];
316
                }
317
            }
318
319
            return $resultSet;
320
        }
321
        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...
322
    }
323
324
    /**
325
     * Execute DB SQL queries using Prepared Statements.
326
     *
327
     * @param mixed  $query  SQL query template string or DBPreparedQuery object
328
     *                       if single parameter.
329
     * @param string $types  Types string (ex: "isdb").
330
     * @param array  $params Parameters in the same order like types string.
331
     *
332
     * @return mixed Statement object or FALSE if an error occurred.
333
     */
334
    private static function doQuery($query, $types = '', $params = [])
335
    {
336
        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...
337
            $dbQuery = new DBPreparedQuery($query, $types, $params);
338
        } else {
339
            $dbQuery = $query;
340
        }
341
342
        $stmt = self::connection()->prepare($dbQuery->query);
343
        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...
344
345
        if ($dbQuery->isBindable()) {
346
            if ($dbQuery->isValid()) {
347
                self::bindParameters($stmt, $dbQuery->types, $dbQuery->params);
348
            } else {
349
                throw new DBCoreException('Number of types is not equal parameters number or types string is invalid');
350
            }
351
        }
352
353
        $stmt->execute();
354
        self::checkDbError($stmt);
355
356
        return $stmt;
357
    }
358
359
    /**
360
     * Execute update DB SQL queries using Prepared Statements.
361
     *
362
     * @param string $query  SQL query template string or DBPreparedQuery object
363
     *                       if single parameter.
364
     * @param string $types  Types string.
365
     * @param array  $params Parameters.
366
     *
367
     * @return int Returns the number of affected rows on success, and -1 if the last query failed.
368
     */
369
    public static function doUpdateQuery($query, $types = '', $params = [])
370
    {
371
        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...
372
            $dbQuery = new DBPreparedQuery($query, $types, $params);
373
        } else {
374
            $dbQuery = $query;
375
        }
376
        $stmt = self::doQuery($dbQuery);
377
378
        switch ($dbQuery->getType()) {
379
            case DBQueryType::INSERT:
380
                $result = self::connection()->insert_id;
381
                break;
382
            case DBQueryType::UPDATE:
383
                $result = self::connection()->affected_rows;
384
                break;
385
            default:
386
                $result = self::connection()->affected_rows;
387
        }
388
        $stmt->close();
389
390
        return $result;
391
    }
392
393
    /**
394
     * Execute select DB SQL queries using Prepared Statements.
395
     *
396
     * @param mixed  $query  SQL query template string or DBPreparedQuery object
397
     *                       if single parameter.
398
     * @param string $types  Types string (ex: "isdb").
399
     * @param array  $params Parameters in the same order like types string.
400
     *
401
     * @return mixed Statement object or FALSE if an error occurred.
402
     */
403
    public static function doSelectQuery($query, $types = '', $params = [])
404
    {
405
        $stmt = self::doQuery($query, $types, $params);
406
407
        $stmt->store_result();
408
        self::checkDbError($stmt);
409
410
        return $stmt;
411
    }
412
413
    /**
414
     * Returns list of database table fields.
415
     *
416
     * @param string $tableName Name of the table.
417
     *
418
     * @return array<string> List of the database table fields (syntax: array[fieldName] = fieldType)
419
     */
420
    public static function getTableFieldsList($tableName)
421
    {
422
        if (!empty($tableName)) {
423
            $query = 'SHOW FULL COLUMNS FROM '.$tableName;
424
            $stmt = self::doSelectQuery($query);
425
            if ($stmt !== false) {
426
                $stmt->bind_result(
427
                    $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...
428
                );
429
430
                $fieldsList = [];
431
                while ($stmt->fetch()) {
432
                    $fieldsList[$field] = [
433
                        'type'       => $type,
434
                        'collation'  => $collation,
435
                        'null'       => $null,
436
                        'key'        => $key,
437
                        'default'    => $default,
438
                        'extra'      => $extra,
439
                        'privileges' => $privileges,
440
                        'comment'    => $comment,
441
                    ];
442
                }
443
                $stmt->close();
444
445
                return $fieldsList;
446
            }
447
        }
448
449
        return [];
450
    }
451
452
    /**
453
     * Returns printable SQL field value for table fields list generator.
454
     *
455
     * @param string $type  SQL type of the field.
456
     * @param mixed  $value Field value.
457
     *
458
     * @return string
459
     */
460
    private static function getPrintableSQLValue($type, $value)
461
    {
462
        if (strpos($type, 'varchar') === 0
463
         || strpos($type, 'text') === 0
464
         || strpos($type, 'longtext') === 0
465
         || strpos($type, 'enum') === 0
466
         || strpos($type, 'char') === 0
467
         || strpos($type, 'datetime') === 0
468
         || strpos($type, 'timestamp') === 0
469
         || strpos($type, 'date') === 0) {
470
            return '"'.$value.'"';
471
        } elseif (strpos($type, 'int') === 0
472
         || strpos($type, 'tinyint') === 0
473
         || strpos($type, 'smallint') === 0
474
         || strpos($type, 'mediumint') === 0
475
         || strpos($type, 'bigint') === 0) {
476
            if (!empty($value)) {
477
                return $value;
478
            }
479
480
            return '0';
481
        } elseif (strpos($type, 'float') === 0
482
         || strpos($type, 'double') === 0
483
         || strpos($type, 'decimal') === 0) {
484
            if (!empty($value)) {
485
                return $value;
486
            }
487
488
            return '0.0';
489
        }
490
491
        return $value;
492
    }
493
494
    /**
495
     * Returns printable field description string for table fields list generator.
496
     *
497
     * @param string $field      Field name.
498
     * @param array  $attributes List of field attributes.
499
     *
500
     * @return string
501
     */
502
    public static function getPrintableFieldString($field, $attributes)
503
    {
504
        $extra = trim($attributes['extra']);
505
        $comment = trim($attributes['comment']);
506
507
        $fieldStr = "'".$field."' => ";
508
        if ($attributes['null'] === 'YES' && is_null($attributes['default'])) {
509
            $fieldStr .= 'null';
510
        } else {
511
            $fieldStr .= self::getPrintableSQLValue($attributes['type'], $attributes['default']);
512
        }
513
        $fieldStr .= ', // '.$attributes['type'].
514
            ', '.(($attributes['null'] == 'NO') ? 'not null' : 'null')
515
            .", default '".$attributes['default']."'".
516
            ($extra ? ', '.$extra : '').
517
            ($comment ? ' ('.$comment.')' : '')."\n";
518
519
        return $fieldStr;
520
    }
521
522
    /**
523
     * Outputs comfortable for Bean Class creation list of table fields.
524
     *
525
     * @param string $tableName Name of the Db table.
526
     */
527
    public static function displayTableFieldsList($tableName)
528
    {
529
        echo '<pre>';
530
        if (!empty($tableName)) {
531
            $fieldsList = self::getTableFieldsList($tableName);
532
            if (!empty($fieldsList)) {
533
                foreach ($fieldsList as $field => $attributes) {
534
                    echo self::getPrintableFieldString($field, $attributes);
0 ignored issues
show
Documentation introduced by
$attributes 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...
535
                }
536
            }
537
        }
538
        echo '</pre>';
539
    }
540
541
    /**
542
     * Returns list of fields values with default indexes.
543
     *
544
     * @param array<mixed> $fieldsList  List of the table fields (syntax: array[fieldName] = fieldValue)
545
     * @param string       $idFieldName Name of the primary key field.
546
     *
547
     * @return array<mixed>
548
     */
549
    private static function createValuesList($fieldsList, $idFieldName = '')
550
    {
551
        $valuesList = [];
552
        foreach ($fieldsList as $fieldName => $fieldValue) {
553
            if ($fieldName != $idFieldName) {
554
                $valuesList[] = $fieldValue;
555
            }
556
        }
557
558
        return $valuesList;
559
    }
560
561
    /**
562
     * Executes SQL INSERT query to the database.
563
     *
564
     * @param DBObject $dbObject DBObject to insert.
565
     * @param bool     $debug    Debug mode flag.
566
     *
567
     * @return int Insertion ID (primary key value) or null on debug.
568
     */
569
    public static function insertDBObject($dbObject, $debug = false)
570
    {
571
        $fieldsList = $dbObject->getFieldsList();
572
        $idFieldName = $dbObject->getIdFieldName();
573
574
        if (Tools::isInteger($fieldsList[$idFieldName])) {
575
            $query = 'INSERT INTO '.$dbObject->getTableName().'
576
                          SET '.DBPreparedQuery::sqlQMValuesString($fieldsList, $idFieldName);
577
            $typesString = DBPreparedQuery::sqlTypesString($fieldsList, $idFieldName);
578
            $valuesList = self::createValuesList($fieldsList, $idFieldName);
579
        } else {
580
            $query = 'INSERT INTO '.$dbObject->getTableName().'
581
                          SET '.DBPreparedQuery::sqlQMValuesString($fieldsList);
582
            $typesString = DBPreparedQuery::sqlTypesString($fieldsList);
583
            $valuesList = self::createValuesList($fieldsList);
584
        }
585
586
        if ($debug) {
587
            DBQuery::showQueryDebugInfo($query, $typesString, $valuesList);
588
589
            return;
590
        }
591
        self::doUpdateQuery($query, $typesString, $valuesList);
592
593
        return self::connection()->insert_id;
594
    }
595
596
    /**
597
     * Executes SQL UPDATE query to the database.
598
     *
599
     * @param DBObject $dbObject DBObject to update.
600
     * @param bool     $debug    Debug mode flag.
601
     *
602
     * @return int Returns the number of affected rows on success, and -1 if
603
     *             the last query failed.
604
     */
605
    public static function updateDBObject($dbObject, $debug = false)
606
    {
607
        $fieldsList = $dbObject->getFieldsList();
608
        $idFieldName = $dbObject->getIdFieldName();
609
610
        $query = 'UPDATE '.$dbObject->getTableName().'
611
                  SET '.DBPreparedQuery::sqlQMValuesString($fieldsList, $idFieldName).'
612
                  WHERE '.$idFieldName.' = ?
613
                  LIMIT 1';
614
        $typesString = DBPreparedQuery::sqlTypesString($fieldsList, $idFieldName);
615
        if (Tools::isInteger($fieldsList[$idFieldName])) {
616
            $typesString .= 'i';
617
        } else {
618
            $typesString .= 's';
619
        }
620
        $valuesList = self::createValuesList($fieldsList, $idFieldName);
621
        $valuesList[] = $dbObject->getId();
622
623
        if ($debug) {
624
            DBQuery::showQueryDebugInfo($query, $typesString, $valuesList);
625
        } else {
626
            return self::doUpdateQuery($query, $typesString, $valuesList);
627
        }
628
    }
629
630
    /**
631
     * Executes SQL DELETE query to the database.
632
     *
633
     * @param DBObject $dbObject DBObject to delete.
634
     *
635
     * @return int Returns the number of affected rows on success, and -1 if
636
     *             the last query failed.
637
     */
638
    public static function deleteDBObject($dbObject)
639
    {
640
        if (!empty($dbObject) && is_object($dbObject)) {
641
            $query = 'DELETE FROM '.$dbObject->getTableName().
642
                     ' WHERE '.$dbObject->getIdFieldName().' = ? LIMIT 1';
643
            if (Tools::isInteger($dbObject->getId())) {
644
                $typesString = 'i';
645
            } else {
646
                $typesString = 's';
647
            }
648
            self::doUpdateQuery($query, $typesString, [$dbObject->getId()]);
649
650
            return self::connection()->affected_rows;
651
        } else {
652
            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...
653
        }
654
    }
655
656
    /**
657
     * Returns DBObject from ResultSet.
658
     *
659
     * @param DBObject $dbObject
660
     * @param array    $resultSet Associated by table names arrays of selected
661
     *                            fields.
662
     *
663
     * @return DBObject
664
     */
665
    public static function selectDBObjectFromResultSet($dbObject, $resultSet)
666
    {
667
        $dbObject->setFieldsValues($resultSet[$dbObject->getTableName()]);
668
669
        return $dbObject;
670
    }
671
672
    /**
673
     * Returns DB object by database query statement.
674
     *
675
     * @param resource $stmt      Database query statement.
676
     * @param string   $className Name of the DB object class.
677
     *
678
     * @return DBObject
679
     */
680
    public static function selectDBObjectFromStatement($stmt, $className)
681
    {
682
        if (is_object($className)) {
683
            $className = get_class($className);
684
        }
685
686
        if ($stmt->num_rows == 1) {
687
            $resultSet = self::bindResults($stmt);
688
            $dbObject = new $className();
689
            self::selectDBObjectFromResultSet($dbObject, $resultSet);
0 ignored issues
show
Bug introduced by
It seems like $resultSet defined by self::bindResults($stmt) on line 687 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...
690
691
            if (!is_null($dbObject) && is_object($dbObject) && $dbObject->getId()) {
692
                return $dbObject;
693
            } else {
694
                return;
695
            }
696
        } elseif ($stmt->num_rows > 1) {
697
            throw new DBCoreException("More than single record of '".$className."' entity selected");
698
        }
699
    }
700
701
    /**
702
     * Selects DBObject from database.
703
     *
704
     * @param string $query    SQL query.
705
     * @param string $types    Types string (ex: "isdb").
706
     * @param array  $params   Parameters in the same order like types string.
707
     * @param mixed  $instance Instance of the some DBObject class or it's class name.
708
     *
709
     * @return DBObject Selected DBObject or NULL otherwise.
710
     */
711 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...
712
    {
713
        $stmt = self::doSelectQuery($query, $types, $params);
714
        $obj = null;
715
        if ($stmt) {
716
            $obj = self::selectDBObjectFromStatement($stmt, $instance);
717
718
            $stmt->close();
719
        }
720
721
        return $obj;
722
    }
723
724
    /**
725
     * Returns list of DB objects by database query statement.
726
     *
727
     * @param resource $stmt      Database query statement.
728
     * @param mixed    $className Instance of the some DBObject class or it's class name.
729
     *
730
     * @return array<DBObject>
731
     */
732
    public static function selectDBObjectsFromStatement($stmt, $className)
733
    {
734
        if (is_object($className)) {
735
            $className = get_class($className);
736
        }
737
738
        if ($stmt->num_rows > 0) {
739
            $objectsList = [];
740
            while ($resultSet = self::bindResults($stmt)) {
741
                $dbObject = new $className();
742
                self::selectDBObjectFromResultSet($dbObject, $resultSet);
743
744
                $recordId = $dbObject->getId();
745
                if (!is_null($recordId)) {
746
                    $objectsList[$recordId] = $dbObject;
747
                } else {
748
                    $objectsList[] = $dbObject;
749
                }
750
            }
751
752
            return $objectsList;
753
        }
754
755
        return [];
756
    }
757
758
    /**
759
     * Selects DBObject list from database.
760
     *
761
     * @param string $query    SQL query.
762
     * @param string $types    Types string (ex: "isdb").
763
     * @param array  $params   Parameters in the same order like types string.
764
     * @param mixed  $instance Instance of the some DBObject class or it's class name.
765
     *
766
     * @return DBObject Selected DBObject or NULL otherwise.
767
     */
768 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...
769
    {
770
        $stmt = self::doSelectQuery($query, $types, $params);
771
        $obj = null;
772
        if ($stmt) {
773
            $obj = self::selectDBObjectsFromStatement($stmt, $instance);
774
775
            $stmt->close();
776
        }
777
778
        return $obj;
779
    }
780
781
    /**
782
     * Executes SQL query with single record and return this record.
783
     *
784
     * @param mixed  $query  SQL query template string or DBPreparedQuery object
785
     *                       if single parameter.
786
     * @param string $types  Types string (ex: "isdb").
787
     * @param array  $params Parameters in the same order like types string.
788
     *
789
     * @throws DBCoreException If no one or more than one records selected.
790
     *
791
     * @return array Selected record with table names as keys or NULL if no
792
     *               data selected.
793
     */
794 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...
795
    {
796
        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...
797
            $dbQuery = new DBPreparedQuery($query, $types, $params);
798
        } else {
799
            $dbQuery = $query;
800
        }
801
        $stmt = $dbQuery->go();
802
803
        if ($stmt !== false) {
804
            $record = null;
805
            if ($stmt->num_rows === 1) {
806
                $record = self::bindResults($stmt);
807
            }
808
            $stmt->close();
809
810
            if (is_null($record)) {
811
                throw new DBCoreException('No one or more than one records selected.');
812
            }
813
814
            return $record;
815
        }
816
    }
817
818
    /**
819
     * Executes SQL query with single record and value result and return this value.
820
     *
821
     * @param mixed  $query  SQL query template string or DBPreparedQuery object
822
     *                       if single parameter.
823
     * @param string $types  Types string (ex: "isdb").
824
     * @param array  $params Parameters in the same order like types string.
825
     *
826
     * @throws DBCoreException If no one or more than one records selected.
827
     *
828
     * @return mixed
829
     */
830 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...
831
    {
832
        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...
833
            $dbQuery = new DBPreparedQuery($query, $types, $params);
834
        } else {
835
            $dbQuery = $query;
836
        }
837
        $stmt = $dbQuery->go();
838
839
        if ($stmt !== false) {
840
            $value = null;
841
            if ($stmt->num_rows === 1) {
842
                $stmt->bind_result($value);
843
                $stmt->fetch();
844
            }
845
            $stmt->close();
846
847
            if (is_null($value)) {
848
                throw new DBCoreException('No one or more than one records selected.');
849
            }
850
851
            return $value;
852
        }
853
    }
854
855
    /**
856
     * Calls DBCore magic methods like:
857
     *    get[User]By[Id]($userId)
858
     *    get[User]By[Email]($email)
859
     *    get[Users]()
860
     *    delete[Users]($ids)
861
     *    delete[User]($userId)
862
     *    set[User]Activation($activationFieldName, $flagValue).
863
     *
864
     * @param string $methodName   Name of the magic method.
865
     * @param array  $methodParams List of dynamic parameters.
866
     *
867
     * @throws DBCoreException
868
     *
869
     * @return mixed
870
     */
871
    public static function __callStatic($methodName, $methodParams)
872
    {
873
        if (strrpos($methodName, 'ies') == strlen($methodName) - 3) {
874
            $methodName = substr($methodName, 0, strlen($methodName) - 3).'ys';
875
        }
876
877
        /*
878
         * Get database record object by Id
879
         */
880
        if (preg_match('#get([a-zA-Z]+)ById#', $methodName, $matches)) {
881
            $dbSelector = new DBSelector($matches[1]);
882
883
            return $dbSelector->selectDBObjectById($methodParams[0]);
884
        }
885
886
        /*
887
         * Get database record object by some field value
888
         */
889
        if (preg_match('#get([a-zA-Z]+)By([a-zA-Z]+)#', $methodName, $matches)) {
890
            if (empty($methodParams[0])) {
891
                return;
892
            }
893
            $dbSelector = new DBSelector($matches[1]);
894
895
            $fieldName = substr(strtolower(preg_replace('#([A-Z]{1})#', '_$1', $matches[2])), 1);
896
897
            return $dbSelector->selectDBObjectByField($fieldName, $methodParams[0]);
898
        }
899
900
        /*
901
         * Get all database records
902
         */
903
        if (preg_match('#get([a-zA-Z]+)s#', $methodName, $matches)) {
904
            return self::Selector()->selectDBObjects();
905
        }
906
907
        /*
908
         * Delete selected records from the database
909
         */
910
        if (preg_match('#delete([a-zA-Z]+)s#', $methodName, $matches)) {
911
            $className = $matches[1];
912
            $idsList = $methodParams[0];
913
914
            $idsList = array_filter($idsList, 'isInteger');
915
            if (!empty($idsList)) {
916
                $itemsNumber = count($idsList);
917
                $types = DBPreparedQuery::sqlSingleTypeString('i', $itemsNumber);
918
                $dbObject = new $className();
919
920
                if (!isInstanceOf($dbObject, $className)) {
921
                    throw new DBCoreException("Class with name '".$className."' is not exists");
922
                }
923
924
                $query = 'DELETE FROM '.$dbObject->getTableName().'
925
                          WHERE '.$dbObject->getIdFieldName().'
926
                             IN ('.DBPreparedQuery::sqlQMString($itemsNumber).')';
927
928
                return self::doUpdateQuery($query, $types, $idsList);
929
            }
930
931
            return 0;
932
        }
933
934
        /*
935
         * Delete selected record from the database
936
         */
937
        if (preg_match('#delete([a-zA-Z]+)#', $methodName, $matches)) {
938
            return call_user_func(
939
                [self::getInstance(), $methodName.'s'],
940
                [$methodParams[0]]
941
            );
942
        }
943
944
        /*
945
         * Set activation value of selected records
946
         */
947
        if (preg_match('#set([a-zA-Z]+)Activation#', $methodName, $matches)) {
948
            $className = $matches[1];
949
            if (strrpos($className, 'ies') == strlen($className) - 3) {
950
                $className = substr($className, 0, strlen($className) - 3).'y';
951
            } else {
952
                $className = substr($className, 0, strlen($className) - 1);
953
            }
954
955
            $idsList = $methodParams[0];
956
            $activationFieldName = $methodParams[1];
957
            $activationValue = $methodParams[2];
958
959
            if (empty($activationFieldName)) {
960
                throw new DBCoreException('Invalid activation field name');
961
            }
962
963
            $idsList = array_filter($idsList, 'isInteger');
964
            if (!empty($idsList)) {
965
                $itemsNumber = count($idsList);
966
                $types = DBPreparedQuery::sqlSingleTypeString('i', $itemsNumber);
967
                $dbObject = new $className();
968
969
                if (!isInstanceOf($dbObject, $className)) {
970
                    throw new DBCoreException("Class with name '".$className."' is not exists");
971
                }
972
973
                $query = 'UPDATE '.$dbObject->getTableName().' SET `'.$activationFieldName."` = '".$activationValue."'
974
                          WHERE ".$dbObject->getIdFieldName().' IN ('.DBPreparedQuery::sqlQMString($itemsNumber).')';
975
976
                return self::doUpdateQuery($query, $types, $idsList);
977
            }
978
        }
979
980
        throw new DBCoreException('No such method "'.$methodName.'"');
981
    }
982
}
983
984
/**
985
 * Service exception class.
986
 */
987
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...
988
{
989
}
990