Completed
Push — master ( 54db38...e6245f )
by Lars
01:29
created

Prepare::prepare()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 6.0131

Importance

Changes 0
Metric Value
dl 0
loc 27
ccs 13
cts 14
cp 0.9286
rs 8.8657
c 0
b 0
f 0
cc 6
nc 5
nop 1
crap 6.0131
1
<?php
2
3
declare(strict_types=1);
4
5
namespace voku\db;
6
7
use voku\db\exceptions\DBGoneAwayException;
8
use voku\db\exceptions\QueryException;
9
10
/**
11
 * Prepare: This class can handle the prepare-statement from the "DB"-class.
12
 */
13
final class Prepare extends \mysqli_stmt
14
{
15
    /**
16
     * @var string - the unchanged query string provided to the constructor
17
     */
18
    private $_sql = '';
19
20
    /**
21
     * @var string - the query string with bound parameters interpolated
22
     */
23
    private $_sql_with_bound_parameters = '';
24
25
    /**
26
     * @var bool
27
     */
28
    private $_use_bound_parameters_interpolated = false;
29
30
    /**
31
     * @var array - array of arrays containing values that have been bound to the query as parameters
32
     */
33
    private $_boundParams = [];
34
35
    /**
36
     * @var DB
37
     */
38
    private $_db;
39
40
    /**
41
     * @var Debug
42
     */
43
    private $_debug;
44
45
    /**
46
     * Prepare constructor.
47
     *
48
     * @param DB     $db
49
     * @param string $query
50
     */
51 10
    public function __construct(DB $db, string $query)
52
    {
53 10
        $this->_db = $db;
54 10
        $this->_debug = $db->getDebugger();
55
56 10
        parent::__construct($db->getLink(), $query);
57
58 10
        $this->prepare($query);
59 10
    }
60
61
    /**
62
     * Prepare destructor.
63
     */
64 10
    public function __destruct()
65
    {
66 10
        $this->close();
67 10
    }
68
69
    /**
70
     * Combines the values stored in $this->boundParams into one array suitable for pushing as the input arguments to
71
     * parent::bind_param when used with call_user_func_array
72
     *
73
     * @return array
74
     */
75 6
    private function _buildArguments(): array
76
    {
77 6
        $arguments = [];
78 6
        $arguments[0] = '';
79
80 6
        foreach ($this->_boundParams as $param) {
81 6
            $arguments[0] .= $param['type'];
82 6
            $arguments[] = &$param['value'];
83
        }
84
85 6
        return $arguments;
86
    }
87
88
    /**
89
     * Escapes the supplied value.
90
     *
91
     * @param array $param
92
     *
93
     * @return array 0 => "$value" escaped<br />
94
     *               1 => "$valueForSqlWithBoundParameters" for insertion into the interpolated query string
95
     */
96 6
    private function _prepareValue(array &$param): array
97
    {
98 6
        $type = $param['type']; // 'i', 'b', 's', 'd'
99 6
        $value = $param['value'];
100
101 6
        $value = $this->_db->escape($value);
102
103 6
        if ($type === 's') {
104 4
            $valueForSqlWithBoundParameters = "'" . $value . "'";
105 2
        } elseif ($type === 'i') {
106 2
            $valueForSqlWithBoundParameters = (int) $value;
107 1
        } elseif ($type === 'd') {
108 1
            $valueForSqlWithBoundParameters = (float) $value;
109
        } else {
110
            $valueForSqlWithBoundParameters = $value;
111
        }
112
113 6
        return [$value, $valueForSqlWithBoundParameters];
114
    }
115
116
    /**
117
     * @return int
118
     */
119
    public function affected_rows(): int
120
    {
121
        return $this->affected_rows;
122
    }
123
124
    /**
125
     * This is a wrapper for "bind_param" what binds variables to a prepared statement as parameters. If you use this
126
     * wrapper, you can debug your query with e.g. "$this->get_sql_with_bound_parameters()".
127
     *
128
     * @param string $types <strong>i<strong> corresponding variable has type integer<br />
129
     *                      <strong>d</strong> corresponding variable has type double<br />
130
     *                      <strong>s</strong> corresponding variable has type string<br />
131
     *                      <strong>b</strong> corresponding variable is a blob and will be sent in packets
132
     *
133
     * INFO: We have to explicitly declare all parameters as references, otherwise it does not seem possible to pass
134
     * them on without losing the reference property
135
     * @param mixed  ...$v
136
     *
137
     * @return bool
138
     */
139 6
    public function bind_param_debug(string $types, &...$v): bool
0 ignored issues
show
Unused Code introduced by
The parameter $v is not used and could be removed.

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

Loading history...
140
    {
141 6
        $this->_use_bound_parameters_interpolated = true;
142
143
        // debug_backtrace returns arguments by reference, see comments at http://php.net/manual/de/function.func-get-args.php
144 6
        $trace = \debug_backtrace(\DEBUG_BACKTRACE_PROVIDE_OBJECT, 1);
145
146 6
        $args = &$trace[0]['args'];
147 6
        $typesArray = \str_split($types);
148
149 6
        $args_count = \count($args) - 1;
150 6
        $types_count = \count($typesArray);
151
152 6
        if ($args_count !== $types_count) {
153
            \trigger_error('Number of variables do not match number of parameters in prepared statement', \E_WARNING);
154
155
            return false;
156
        }
157
158 6
        $arg = 1;
159 6
        foreach ($typesArray as $typeInner) {
160 6
            $val = &$args[$arg];
161 6
            $this->_boundParams[] = [
162 6
                'type'  => $typeInner,
163 6
                'value' => &$val,
164
            ];
165 6
            $arg++;
166
        }
167
168 6
        return true;
169
    }
170
171
    /**
172
     * {@inheritdoc}
173
     *
174
     * @return bool
175
     */
176
    public function execute_raw(): bool
177
    {
178
        return parent::execute();
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (execute() instead of execute_raw()). Are you sure this is correct? If so, you might want to change this to $this->execute().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
179
    }
180
181
    /**
182
     * Executes a prepared Query
183
     *
184
     * @see http://php.net/manual/en/mysqli-stmt.execute.php
185
     *
186
     * @return bool|int|Result|string   "Result" by "<b>SELECT</b>"-queries<br />
187
     *                           "int|string" (insert_id) by "<b>INSERT / REPLACE</b>"-queries<br />
188
     *                           "int" (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
189
     *                           "true" by e.g. "DROP"-queries<br />
190
     *                           "false" on error
191
     */
192 9
    public function execute()
193
    {
194 9
        if ($this->_use_bound_parameters_interpolated === true) {
195 6
            $this->interpolateQuery();
196 6
            \call_user_func_array(['parent', 'bind_param'], $this->_buildArguments());
197
        }
198
199 9
        $query_start_time = \microtime(true);
200 9
        $result = parent::execute();
201 9
        $query_duration = \microtime(true) - $query_start_time;
202
203 9
        if ($result === true) {
204
205
            // "INSERT" || "REPLACE"
206 7 View Code Duplication
            if (\preg_match('/^\s*"?(INSERT|REPLACE)\s+/i', $this->_sql)) {
207 3
                $insert_id = (int) $this->insert_id;
208 3
                $this->_debug->logQuery($this->_sql_with_bound_parameters, $query_duration, $insert_id);
209
210 3
                return $insert_id;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $insert_id; (integer) is incompatible with the return type of the parent method mysqli_stmt::execute of type boolean.

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...
211
            }
212
213
            // "UPDATE" || "DELETE"
214 4 View Code Duplication
            if (\preg_match('/^\s*"?(UPDATE|DELETE)\s+/i', $this->_sql)) {
215 2
                $affected_rows = (int) $this->affected_rows;
216 2
                $this->_debug->logQuery($this->_sql_with_bound_parameters, $query_duration, $affected_rows);
217
218 2
                return $affected_rows;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $affected_rows; (integer) is incompatible with the return type of the parent method mysqli_stmt::execute of type boolean.

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...
219
            }
220
221
            // "SELECT"
222 2
            if (\preg_match('/^\s*"?(SELECT)\s+/i', $this->_sql)) {
223 2
                $select_result = $this->get_result();
0 ignored issues
show
Bug introduced by
The method get_result() does not seem to exist on object<voku\db\Prepare>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
224
225 2
                if ($select_result === false) {
226
                    // log the error query
227
                    $this->_debug->logQuery($this->_sql_with_bound_parameters, $query_duration, 0, true);
228
229
                    return $this->queryErrorHandling($this->error, $this->_sql_with_bound_parameters);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->queryError...with_bound_parameters); (boolean|integer|voku\db\Result|string) is incompatible with the return type of the parent method mysqli_stmt::execute of type boolean.

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...
230
                }
231
232 2
                $num_rows = (int) $select_result->num_rows;
233 2
                $this->_debug->logQuery($this->_sql_with_bound_parameters, $query_duration, $num_rows);
234
235 2
                return new Result($this->_sql_with_bound_parameters, $select_result);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return new \voku\db\Resu...eters, $select_result); (voku\db\Result) is incompatible with the return type of the parent method mysqli_stmt::execute of type boolean.

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...
236
            }
237
238
            // log the ? query
239
            $this->_debug->logQuery($this->_sql_with_bound_parameters, $query_duration, 0);
240
241
            return true;
242
        }
243
244
        // log the error query
245 2
        $this->_debug->logQuery($this->_sql_with_bound_parameters, $query_duration, 0, true);
246
247 2
        return $this->queryErrorHandling($this->error, $this->_sql_with_bound_parameters);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->queryError...with_bound_parameters); (boolean|integer|voku\db\Result|string) is incompatible with the return type of the parent method mysqli_stmt::execute of type boolean.

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...
248
    }
249
250
    /**
251
     * Prepare an SQL statement for execution
252
     *
253
     * @see   http://php.net/manual/en/mysqli-stmt.prepare.php
254
     *
255
     * @param string $query <p>
256
     *                      The query, as a string. It must consist of a single SQL statement.
257
     *                      </p>
258
     *                      <p>
259
     *                      You can include one or more parameter markers in the SQL statement by
260
     *                      embedding question mark (?) characters at the
261
     *                      appropriate positions.
262
     *                      </p>
263
     *                      <p>
264
     *                      You should not add a terminating semicolon or \g
265
     *                      to the statement.
266
     *                      </p>
267
     *                      <p>
268
     *                      The markers are legal only in certain places in SQL statements.
269
     *                      For example, they are allowed in the VALUES() list of an INSERT statement
270
     *                      (to specify column values for a row), or in a comparison with a column in
271
     *                      a WHERE clause to specify a comparison value.
272
     *                      </p>
273
     *                      <p>
274
     *                      However, they are not allowed for identifiers (such as table or column names),
275
     *                      in the select list that names the columns to be returned by a SELECT statement),
276
     *                      or to specify both operands of a binary operator such as the =
277
     *                      equal sign. The latter restriction is necessary because it would be impossible
278
     *                      to determine the parameter type. In general, parameters are legal only in Data
279
     *                      Manipulation Language (DML) statements, and not in Data Definition Language
280
     *                      (DDL) statements.
281
     *                      </p>
282
     *
283
     * @return bool
284
     *              <p>false on error</p>
285
     *
286
     * @since 5.0
287
     */
288 10
    public function prepare($query): bool
289
    {
290 10
        if (!is_string($query)) {
291 1
            throw new \InvalidArgumentException('$query was no string: ' . \gettype($query));
292
        }
293
294 10
        $this->_sql = $query;
295 10
        $this->_sql_with_bound_parameters = $query;
296
297 10
        if (!$this->_db->isReady()) {
298
            return false;
299
        }
300
301 10
        if (!$query || $query === '') {
302 1
            $this->_debug->displayError('Can not prepare an empty query.', false);
303
304 1
            return false;
305
        }
306
307 9
        $bool = parent::prepare($query);
308
309 9
        if ($bool === false) {
310 2
            $this->_debug->displayError('Can not prepare query: ' . $query . ' | ' . $this->error, false);
311
        }
312
313 9
        return $bool;
314
    }
315
316
    /**
317
     * Ger the bound parameters from sql-query as array, if you use the "$this->bind_param_debug()" method.
318
     *
319
     * @return array
320
     */
321
    public function get_bound_params(): array
322
    {
323
        return $this->_boundParams;
324
    }
325
326
    /**
327
     * @return string
328
     */
329
    public function get_sql(): string
330
    {
331
        return $this->_sql;
332
    }
333
334
    /**
335
     * Get the sql-query with bound parameters, if you use the "$this->bind_param_debug()" method.
336
     *
337
     * @return string
338
     */
339 4
    public function get_sql_with_bound_parameters(): string
340
    {
341 4
        return $this->_sql_with_bound_parameters;
342
    }
343
344
    /**
345
     * @return int
346
     */
347
    public function insert_id(): int
348
    {
349
        return $this->insert_id;
350
    }
351
352
    /**
353
     * Copies $this->_sql then replaces bound markers with associated values ($this->_sql is not modified
354
     * but the resulting query string is assigned to $this->sql_bound_parameters)
355
     *
356
     * @return string $testQuery - interpolated db query string
357
     */
358 6
    private function interpolateQuery(): string
359
    {
360 6
        $testQuery = $this->_sql;
361 6
        if ($this->_boundParams) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->_boundParams of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
362
            /** @noinspection AlterInForeachInspection */
363 6
            foreach ($this->_boundParams as &$param) {
364 6
                $values = $this->_prepareValue($param);
365
366
                // set new values
367 6
                $param['value'] = $values[0];
368
                // we need to replace the question mark "?" here
369 6
                $values[1] = \str_replace('?', '###simple_mysqli__prepare_question_mark###', $values[1]);
370
                // build the query (only for debugging)
371 6
                $testQuery = (string) \preg_replace("/\?/", $values[1], $testQuery, 1);
372
            }
373 6
            $testQuery = \str_replace('###simple_mysqli__prepare_question_mark###', '?', $testQuery);
374
        }
375 6
        $this->_sql_with_bound_parameters = $testQuery;
376
377 6
        return $testQuery;
378
    }
379
380
    /**
381
     * Error-handling for the sql-query.
382
     *
383
     * @param string $errorMsg
384
     * @param string $sql
385
     *
386
     * @throws DBGoneAwayException
387
     * @throws QueryException
388
     *
389
     * @return bool|int|Result|string   "Result" by "<b>SELECT</b>"-queries<br />
390
     *                           "int|string" (insert_id) by "<b>INSERT / REPLACE</b>"-queries<br />
391
     *                           "int" (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
392
     *                           "true" by e.g. "DROP"-queries<br />
393
     *                           "false" on error
394
     */
395 2
    private function queryErrorHandling(string $errorMsg, string $sql)
396
    {
397 2
        if ($errorMsg === 'DB server has gone away' || $errorMsg === 'MySQL server has gone away') {
398
            static $RECONNECT_COUNTER;
399
400
            // exit if we have more then 3 "DB server has gone away"-errors
401
            if ($RECONNECT_COUNTER > 3) {
402
                $this->_debug->mailToAdmin('DB-Fatal-Error', $errorMsg . ":\n<br />" . $sql, 5);
403
404
                throw new DBGoneAwayException($errorMsg);
405
            }
406
407
            $this->_debug->mailToAdmin('DB-Error', $errorMsg . ":\n<br />" . $sql);
408
409
            // reconnect
410
            $RECONNECT_COUNTER++;
411
            $this->_db->reconnect(true);
412
413
            // re-run the current query
414
            return $this->execute();
415
        }
416
417 2
        $this->_debug->mailToAdmin('SQL-Error', $errorMsg . ":\n<br />" . $sql);
418
419
        // this query returned an error, we must display it (only for dev) !!!
420 2
        $this->_debug->displayError($errorMsg . ' | ' . $sql);
421
422 2
        return false;
423
    }
424
}
425