Failed Conditions
Pull Request — develop (#3523)
by
unknown
62:01
created

SQLSrvStatement::rowCount()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
eloc 4
dl 0
loc 9
ccs 0
cts 0
cp 0
rs 10
c 0
b 0
f 0
cc 4
nc 3
nop 0
crap 20
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
     * The affected number of rows
125
     *
126
     * @var int|false
127
     */
128
    private $rowCount;
129
130
    /**
131
     * Append to any INSERT query to retrieve the last insert id.
132
     *
133
     * @deprecated This constant has been deprecated and will be made private in 3.0
134
     */
135
    public const LAST_INSERT_ID_SQL = ';SELECT SCOPE_IDENTITY() AS LastInsertId;';
136
137
    /**
138
     * @param resource $conn
139
     */
140
    public function __construct($conn, string $sql, ?LastInsertId $lastInsertId = null)
141
    {
142
        $this->conn = $conn;
143
        $this->sql  = $sql;
144
145
        if (stripos($sql, 'INSERT INTO ') !== 0) {
146
            return;
147
        }
148
149
        $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

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

404
        if (! array_key_exists($columnIndex, /** @scrutinizer ignore-type */ $row)) {
Loading history...
405
            throw InvalidColumnIndex::new($columnIndex, count($row));
406
        }
407
408
        return $row[$columnIndex];
409
    }
410
411
    /**
412
     * {@inheritdoc}
413
     */
414
    public function rowCount() : int
415
    {
416
        if ($this->stmt === null) {
417
            return 0;
418
        }
419
420
        $count = $this->rowCount ?: sqlsrv_rows_affected($this->stmt);
421
422
        return is_int($count) ? $count : 0;
423
    }
424
}
425