Completed
Push — master ( 1a9812...b70610 )
by Sergei
19s queued 14s
created

SQLSrvStatement::bindParam()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1.0046

Importance

Changes 0
Metric Value
eloc 4
dl 0
loc 9
ccs 5
cts 6
cp 0.8333
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 4
crap 1.0046
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\DBAL\Driver\SQLSrv;
6
7
use Doctrine\DBAL\Driver\Statement;
8
use Doctrine\DBAL\Driver\StatementIterator;
9
use Doctrine\DBAL\Exception\InvalidColumnIndex;
10
use Doctrine\DBAL\FetchMode;
11
use Doctrine\DBAL\ParameterType;
12
use IteratorAggregate;
13
use const SQLSRV_ENC_BINARY;
14
use const SQLSRV_FETCH_ASSOC;
15
use const SQLSRV_FETCH_BOTH;
16
use const SQLSRV_FETCH_NUMERIC;
17
use const SQLSRV_PARAM_IN;
18
use function array_key_exists;
19
use function assert;
20
use function count;
21
use function in_array;
22
use function is_int;
23
use function sqlsrv_execute;
24
use function sqlsrv_fetch;
25
use function sqlsrv_fetch_array;
26
use function sqlsrv_fetch_object;
27
use function sqlsrv_get_field;
28
use function sqlsrv_next_result;
29
use function sqlsrv_num_fields;
30
use function SQLSRV_PHPTYPE_STREAM;
31
use function SQLSRV_PHPTYPE_STRING;
32
use function sqlsrv_prepare;
33
use function sqlsrv_rows_affected;
34
use function SQLSRV_SQLTYPE_VARBINARY;
35
use function stripos;
36
37
/**
38
 * SQL Server Statement.
39
 */
40
class SQLSrvStatement implements IteratorAggregate, Statement
41
{
42
    /**
43
     * The SQLSRV Resource.
44
     *
45
     * @var resource
46
     */
47
    private $conn;
48
49
    /**
50
     * The SQL statement to execute.
51
     *
52
     * @var string
53
     */
54
    private $sql;
55
56
    /**
57
     * The SQLSRV statement resource.
58
     *
59
     * @var resource|null
60
     */
61
    private $stmt;
62
63
    /**
64
     * References to the variables bound as statement parameters.
65
     *
66
     * @var mixed
67
     */
68
    private $variables = [];
69
70
    /**
71
     * Bound parameter types.
72
     *
73
     * @var int[]
74
     */
75
    private $types = [];
76
77
    /**
78
     * Translations.
79
     *
80
     * @var int[]
81
     */
82
    private static $fetchMap = [
83
        FetchMode::MIXED       => SQLSRV_FETCH_BOTH,
84
        FetchMode::ASSOCIATIVE => SQLSRV_FETCH_ASSOC,
85
        FetchMode::NUMERIC     => SQLSRV_FETCH_NUMERIC,
86
    ];
87
88
    /**
89
     * The name of the default class to instantiate when fetching class instances.
90
     *
91
     * @var string
92
     */
93
    private $defaultFetchClass = '\stdClass';
94
95
    /**
96
     * The constructor arguments for the default class to instantiate when fetching class instances.
97
     *
98
     * @var mixed[]
99
     */
100
    private $defaultFetchClassCtorArgs = [];
101
102
    /**
103
     * The fetch style.
104
     *
105
     * @var int
106
     */
107
    private $defaultFetchMode = FetchMode::MIXED;
108
109
    /**
110
     * The last insert ID.
111
     *
112
     * @var LastInsertId|null
113
     */
114
    private $lastInsertId;
115
116
    /**
117
     * Indicates whether the statement is in the state when fetching results is possible
118
     *
119
     * @var bool
120
     */
121
    private $result = false;
122
123
    /**
124
     * Append to any INSERT query to retrieve the last insert id.
125
     *
126
     * @deprecated This constant has been deprecated and will be made private in 3.0
127
     */
128
    public const LAST_INSERT_ID_SQL = ';SELECT SCOPE_IDENTITY() AS LastInsertId;';
129
130
    /**
131
     * @param resource $conn
132
     */
133 324
    public function __construct($conn, string $sql, ?LastInsertId $lastInsertId = null)
134
    {
135 324
        $this->conn = $conn;
136 324
        $this->sql  = $sql;
137
138 324
        if (stripos($sql, 'INSERT INTO ') !== 0) {
139 317
            return;
140
        }
141
142 92
        $this->sql         .= self::LAST_INSERT_ID_SQL;
0 ignored issues
show
Deprecated Code introduced by
The constant Doctrine\DBAL\Driver\SQL...ent::LAST_INSERT_ID_SQL has been deprecated: This constant has been deprecated and will be made private in 3.0 ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

142
        $this->sql         .= /** @scrutinizer ignore-deprecated */ self::LAST_INSERT_ID_SQL;

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
143 92
        $this->lastInsertId = $lastInsertId;
144 92
    }
145
146
    /**
147
     * {@inheritdoc}
148
     */
149 114
    public function bindValue($param, $value, int $type = ParameterType::STRING) : void
150
    {
151 114
        assert(is_int($param));
152
153 114
        $this->variables[$param] = $value;
154 114
        $this->types[$param]     = $type;
155 114
    }
156
157
    /**
158
     * {@inheritdoc}
159
     */
160 24
    public function bindParam($param, &$variable, int $type = ParameterType::STRING, ?int $length = null) : void
161
    {
162 24
        assert(is_int($param));
163
164 24
        $this->variables[$param] =& $variable;
165 24
        $this->types[$param]     = $type;
166
167
        // unset the statement resource if it exists as the new one will need to be bound to the new variable
168 24
        $this->stmt = null;
169 24
    }
170
171
    /**
172
     * {@inheritdoc}
173
     */
174 19
    public function closeCursor() : void
175
    {
176
        // not having the result means there's nothing to close
177 19
        if ($this->stmt === null || ! $this->result) {
178 3
            return;
179
        }
180
181
        // emulate it by fetching and discarding rows, similarly to what PDO does in this case
182
        // @link http://php.net/manual/en/pdostatement.closecursor.php
183
        // @link https://github.com/php/php-src/blob/php-7.0.11/ext/pdo/pdo_stmt.c#L2075
184
        // deliberately do not consider multiple result sets, since doctrine/dbal doesn't support them
185 16
        while (sqlsrv_fetch($this->stmt) !== false) {
186
        }
187
188 16
        $this->result = false;
189 16
    }
190
191
    /**
192
     * {@inheritdoc}
193
     */
194 4
    public function columnCount() : int
195
    {
196 4
        if ($this->stmt === null) {
197
            return 0;
198
        }
199
200 4
        return sqlsrv_num_fields($this->stmt) ?: 0;
201
    }
202
203
    /**
204
     * {@inheritdoc}
205
     */
206 317
    public function execute(?array $params = null) : void
207
    {
208 317
        if ($params) {
209 83
            $hasZeroIndex = array_key_exists(0, $params);
210
211 83
            foreach ($params as $key => $val) {
212 83
                if ($hasZeroIndex && is_int($key)) {
213 83
                    $this->bindValue($key + 1, $val);
214
                } else {
215
                    $this->bindValue($key, $val);
216
                }
217
            }
218
        }
219
220 317
        if (! $this->stmt) {
221 317
            $this->stmt = $this->prepare();
222
        }
223
224 316
        if (! sqlsrv_execute($this->stmt)) {
225
            throw SQLSrvException::fromSqlSrvErrors();
226
        }
227
228 316
        if ($this->lastInsertId) {
229 92
            sqlsrv_next_result($this->stmt);
230 92
            sqlsrv_fetch($this->stmt);
231 92
            $this->lastInsertId->setId(sqlsrv_get_field($this->stmt, 0));
232
        }
233
234 316
        $this->result = true;
235 316
    }
236
237
    /**
238
     * Prepares SQL Server statement resource
239
     *
240
     * @return resource
241
     *
242
     * @throws SQLSrvException
243
     */
244 317
    private function prepare()
245
    {
246 317
        $params = [];
247
248 317
        foreach ($this->variables as $column => &$variable) {
249 136
            switch ($this->types[$column]) {
250
                case ParameterType::LARGE_OBJECT:
251 7
                    $params[$column - 1] = [
252 7
                        &$variable,
253
                        SQLSRV_PARAM_IN,
254 7
                        SQLSRV_PHPTYPE_STREAM(SQLSRV_ENC_BINARY),
255 7
                        SQLSRV_SQLTYPE_VARBINARY('max'),
256
                    ];
257 7
                    break;
258
259
                case ParameterType::BINARY:
260 1
                    $params[$column - 1] = [
261 1
                        &$variable,
262
                        SQLSRV_PARAM_IN,
263 1
                        SQLSRV_PHPTYPE_STRING(SQLSRV_ENC_BINARY),
264
                    ];
265 1
                    break;
266
267
                default:
268 133
                    $params[$column - 1] =& $variable;
269 133
                    break;
270
            }
271
        }
272
273 317
        $stmt = sqlsrv_prepare($this->conn, $this->sql, $params);
274
275 317
        if (! $stmt) {
0 ignored issues
show
introduced by
$stmt is of type false|resource, thus it always evaluated to false.
Loading history...
276 1
            throw SQLSrvException::fromSqlSrvErrors();
277
        }
278
279 316
        return $stmt;
280
    }
281
282
    /**
283
     * {@inheritdoc}
284
     */
285 317
    public function setFetchMode(int $fetchMode, ...$args) : void
286
    {
287 317
        $this->defaultFetchMode = $fetchMode;
288
289 317
        if (isset($args[0])) {
290 2
            $this->defaultFetchClass = $args[0];
291
        }
292
293 317
        if (! isset($args[1])) {
294 317
            return;
295
        }
296
297
        $this->defaultFetchClassCtorArgs = (array) $args[1];
298
    }
299
300
    /**
301
     * {@inheritdoc}
302
     */
303 3
    public function getIterator()
304
    {
305 3
        return new StatementIterator($this);
306
    }
307
308
    /**
309
     * {@inheritdoc}
310
     *
311
     * @throws SQLSrvException
312
     */
313 308
    public function fetch(?int $fetchMode = null, ...$args)
314
    {
315
        // do not try fetching from the statement if it's not expected to contain result
316
        // in order to prevent exceptional situation
317 308
        if ($this->stmt === null || ! $this->result) {
318 9
            return false;
319
        }
320
321 299
        $fetchMode = $fetchMode ?: $this->defaultFetchMode;
322
323 299
        if ($fetchMode === FetchMode::COLUMN) {
324 1
            return $this->fetchColumn();
325
        }
326
327 299
        if (isset(self::$fetchMap[$fetchMode])) {
328 295
            return sqlsrv_fetch_array($this->stmt, self::$fetchMap[$fetchMode]) ?: false;
329
        }
330
331 4
        if (in_array($fetchMode, [FetchMode::STANDARD_OBJECT, FetchMode::CUSTOM_OBJECT], true)) {
332 4
            $className = $this->defaultFetchClass;
333 4
            $ctorArgs  = $this->defaultFetchClassCtorArgs;
334
335 4
            if (count($args) > 0) {
336 1
                $className = $args[0];
337 1
                $ctorArgs  = $args[1] ?? [];
338
            }
339
340 4
            return sqlsrv_fetch_object($this->stmt, $className, $ctorArgs) ?: false;
341
        }
342
343
        throw new SQLSrvException('Fetch mode is not supported.');
344
    }
345
346
    /**
347
     * {@inheritdoc}
348
     */
349 120
    public function fetchAll(?int $fetchMode = null, ...$args) : array
350
    {
351 120
        $rows = [];
352
353 120
        switch ($fetchMode) {
354
            case FetchMode::CUSTOM_OBJECT:
355 1
                while (($row = $this->fetch($fetchMode, ...$args)) !== false) {
356 1
                    $rows[] = $row;
357
                }
358 1
                break;
359
360
            case FetchMode::COLUMN:
361 10
                while (($row = $this->fetchColumn()) !== false) {
362 10
                    $rows[] = $row;
363
                }
364 10
                break;
365
366
            default:
367 109
                while (($row = $this->fetch($fetchMode)) !== false) {
368 101
                    $rows[] = $row;
369
                }
370
        }
371
372 120
        return $rows;
373
    }
374
375
    /**
376
     * {@inheritdoc}
377
     */
378 198
    public function fetchColumn(int $columnIndex = 0)
379
    {
380 198
        $row = $this->fetch(FetchMode::NUMERIC);
381
382 198
        if ($row === false) {
383 16
            return false;
384
        }
385
386 192
        if (! array_key_exists($columnIndex, $row)) {
0 ignored issues
show
Bug introduced by
It seems like $row can also be of type object; however, parameter $search of array_key_exists() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

386
        if (! array_key_exists($columnIndex, /** @scrutinizer ignore-type */ $row)) {
Loading history...
387 2
            throw InvalidColumnIndex::new($columnIndex, count($row));
388
        }
389
390 190
        return $row[$columnIndex];
391
    }
392
393
    /**
394
     * {@inheritdoc}
395
     */
396 91
    public function rowCount() : int
397
    {
398 91
        if ($this->stmt === null) {
399
            return 0;
400
        }
401
402 91
        return sqlsrv_rows_affected($this->stmt) ?: 0;
403
    }
404
}
405