Completed
Push — 2.11.x ( 0a2e2f...a8544c )
by Grégoire
23s queued 16s
created

SQLSrvStatement::getIterator()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1.037

Importance

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

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