Statement::resolveValues()   A
last analyzed

Complexity

Conditions 4
Paths 4

Size

Total Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 16
rs 9.7333
c 0
b 0
f 0
cc 4
nc 4
nop 0
1
<?php
2
3
namespace BenTools\SimpleDBAL\Model\Adapter\Mysqli;
4
5
use BenTools\SimpleDBAL\Contract\AdapterInterface;
6
use BenTools\SimpleDBAL\Contract\ResultInterface;
7
use BenTools\SimpleDBAL\Contract\StatementInterface;
8
use BenTools\SimpleDBAL\Model\Exception\ParamBindingException;
9
use BenTools\SimpleDBAL\Model\StatementTrait;
10
use mysqli_result;
11
use mysqli_stmt;
12
13
class Statement implements StatementInterface
14
{
15
16
    use StatementTrait;
17
18
    /**
19
     * @var MysqliAdapter
20
     */
21
    protected $connection;
22
23
    /**
24
     * @var mysqli_stmt
25
     */
26
    protected $stmt;
27
28
    /**
29
     * @var string
30
     */
31
    protected $queryString;
32
33
    /**
34
     * @var string
35
     */
36
    protected $runnableQueryString;
37
38
    /**
39
     * MysqliStatement constructor.
40
     * @param MysqliAdapter $connection
41
     * @param mysqli_stmt $stmt
42
     * @param array $values
43
     * @param string $queryString
44
     * @param string $runnableQueryString
45
     */
46
    public function __construct(MysqliAdapter $connection, mysqli_stmt $stmt, array $values = null, string $queryString, string $runnableQueryString = null)
47
    {
48
        $this->connection          = $connection;
49
        $this->stmt                = $stmt;
50
        $this->values              = $values;
0 ignored issues
show
Documentation Bug introduced by
It seems like $values can be null. However, the property $values is declared as array. Maybe change the type of the property to array|null or add a type check?

Our type inference engine has found an assignment of a scalar value (like a string, an integer or null) to a property which is an array.

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

To type hint that a parameter can be either an array or null, you can set a type hint of array and a default value of null. The PHP interpreter will then accept both an array or null for that parameter.

function aContainsB(array $needle = null, array  $haystack) {
    if (!$needle) {
        return false;
    }

    return array_intersect($haystack, $needle) == $haystack;
}

The function can be called with either null or an array for the parameter $needle but will only accept an array as $haystack.

Loading history...
51
        $this->queryString         = $queryString;
52
        $this->runnableQueryString = $runnableQueryString;
53
    }
54
55
    /**
56
     * @return MysqliAdapter
57
     */
58
    final public function getConnection(): AdapterInterface
59
    {
60
        return $this->connection;
61
    }
62
63
    /**
64
     * @inheritDoc
65
     */
66
    public function getQueryString(): string
67
    {
68
        return $this->queryString;
69
    }
70
71
    /**
72
     * @inheritDoc
73
     */
74
    public function getWrappedStatement()
75
    {
76
        return $this->stmt;
77
    }
78
79
    /**
80
     * @inheritDoc
81
     */
82
    public function bind(): void
83
    {
84
        if ($this->hasValues()) {
85
            if ($this->hasNamedPlaceholders() && true === $this->getConnection()->getOption(MysqliAdapter::OPT_RESOLVE_NAMED_PARAMS)) {
86
                $values = $this->resolveValues();
87
            } else {
88
                $values = $this->values;
89
            }
90
91
            $values = array_map([$this, 'toScalar'], $values);
92
            $types = $this->getTypesFor($values);
93
            $this->stmt->bind_param($types, ...$values);
94
        }
95
    }
96
97
    /**
98
     * @param array $values
99
     * @return string
100
     */
101
    private function getTypesFor(array $values): string
102
    {
103
        $types = '';
104
        foreach ($values as $key => $value) {
105
            $types .= $this->getMysqliType($value);
106
        }
107
        return $types;
108
    }
109
110
    /**
111
     * @return array
112
     */
113
    private function resolveValues(): array
114
    {
115
        preg_match_all('#:([a-zA-Z0-9_]+)#', $this->queryString, $matches);
116
        $placeholders = $matches[1];
117
        $values       = [];
118
        if (count(array_unique($placeholders)) !== count(array_keys($this->values))) {
119
            throw new ParamBindingException("Placeholders count does not match values count.", 0, null, $this);
120
        }
121
        foreach ($placeholders as $placeholder) {
122
            if (!array_key_exists($placeholder, $this->values)) {
123
                throw new ParamBindingException(sprintf("Unable to find placeholder %s into the list of values.", $placeholder), 0, null, $this);
124
            }
125
            $values[] = $this->values[$placeholder];
126
        }
127
        return $values;
128
    }
129
130
    /**
131
     * Attempt to convert non-scalar values.
132
     *
133
     * @param $value
134
     * @return string
135
     */
136
    protected function toScalar($value)
137
    {
138
        if (is_scalar($value)) {
139
            return $value;
140
        } elseif (null === $value) {
141
            return 'NULL';
142
        } else {
143
            if (is_object($value)) {
144
                if (is_callable([$value, '__toString'])) {
145
                    return (string) $value;
146
                } elseif ($value instanceof \DateTimeInterface) {
147
                    return $value->format('Y-m-d H:i:s');
148
                } else {
149
                    throw new \InvalidArgumentException(sprintf("Cast of class %s is impossible", get_class($value)));
150
                }
151
            } else {
152
                throw new \InvalidArgumentException(sprintf("Cast of type %s is impossible", gettype($value)));
153
            }
154
        }
155
    }
156
157
    /**
158
     * @inheritDoc
159
     */
160
    public function preview(): string
161
    {
162
        if (!$this->hasValues()) {
163
            return $this->queryString;
164
        }
165
166
        $escape = function ($value) {
167
            if (null === $value) {
168
                return 'NULL';
169
            }
170
            $value = $this->toScalar($value);
171
            $type = gettype($value);
172
            switch ($type) {
173
                case 'boolean':
174
                    return (int) $value;
175
                case 'double':
176
                case 'integer':
177
                    return $value;
178
                default:
179
                    return (string) "'" . addslashes($this->getConnection()->getWrappedConnection()->real_escape_string($value)) . "'";
0 ignored issues
show
Bug introduced by
The method real_escape_string does only exist in mysqli, but not in PDO.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
180
            }
181
        };
182
183
        $keywords = [];
184
        $preview  = $this->queryString;
185
186
        # Case of question mark placeholders
187
        if ($this->hasAnonymousPlaceholders()) {
188
            if (count($this->values) !== preg_match_all("/([\?])/", $this->queryString)) {
189
                throw new ParamBindingException("Number of variables doesn't match number of parameters in prepared statement", 0, null, $this);
190
            }
191
192
            foreach ($this->values as $value) {
193
                $preview = preg_replace("/([\?])/", $escape($value), $preview, 1);
194
            }
195
        } # Case of named placeholders
196
        else {
197
            foreach ($this->values as $key => $value) {
198
                if (!in_array($key, $keywords, true)) {
199
                    $keywords[] = $key;
200
                }
201
            }
202
203
            $nbPlaceholders = preg_match_all('#:([a-zA-Z0-9_]+)#', $this->queryString, $placeholders);
204
205
            if ($nbPlaceholders > 0 && count(array_unique($placeholders[1])) !== count($this->values)) {
206
                throw new ParamBindingException("Number of variables doesn't match number of parameters in prepared statement", 0, null, $this);
207
            }
208
209
            foreach ($keywords as $keyword) {
210
                $pattern = "/(\:\b" . $keyword . "\b)/i";
211
                $preview = preg_replace($pattern, $escape($this->values[$keyword]), $preview);
212
            }
213
        }
214
        return $preview;
215
    }
216
217
    /**
218
     * @param $value
219
     * @return string
220
     */
221
    protected function getMysqliType($value)
222
    {
223
        if (!is_scalar($value)) {
224
            throw new \InvalidArgumentException(sprintf("Can only cast scalar variables, %s given.", gettype($value)));
225
        }
226
        $cast = gettype($value);
227
        switch ($cast) {
228
            case 'double':
229
                return 'd';
230
            case 'integer':
231
                return 'i';
232
            default:
233
                return 's';
234
        }
235
    }
236
    /**
237
     * @return Result
238
     */
239
    public function createResult(): ResultInterface
240
    {
241
        $this->bind();
242
        $this->getWrappedStatement()->execute();
243
        $result = $this->getWrappedStatement()->get_result();
244
        $mysqli = $this->getConnection()->getWrappedConnection();
245
        return !$result instanceof mysqli_result ? new Result($mysqli) : new Result($mysqli, $result, $this->getWrappedStatement());
0 ignored issues
show
Bug introduced by
It seems like $mysqli defined by $this->getConnection()->getWrappedConnection() on line 244 can also be of type object<PDO>; however, BenTools\SimpleDBAL\Mode...i\Result::__construct() does only seem to accept object<mysqli>, 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...
246
    }
247
248
    /**
249
     * @inheritDoc
250
     */
251
    public function __toString(): string
252
    {
253
        return $this->queryString;
254
    }
255
}
256