Failed Conditions
Push — master ( 30b923...92920e )
by Marco
19s queued 13s
created

SQLSrvStatement::prepare()   A

Complexity

Conditions 5
Paths 8

Size

Total Lines 36
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 6.2327

Importance

Changes 0
Metric Value
eloc 23
dl 0
loc 36
ccs 19
cts 30
cp 0.6333
rs 9.2408
c 0
b 0
f 0
cc 5
nc 8
nop 0
crap 6.2327
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 const SQLSRV_ENC_BINARY;
12
use const SQLSRV_ERR_ERRORS;
13
use const SQLSRV_FETCH_ASSOC;
14
use const SQLSRV_FETCH_BOTH;
15
use const SQLSRV_FETCH_NUMERIC;
16
use const SQLSRV_PARAM_IN;
17
use function array_key_exists;
18
use function count;
19
use function func_get_args;
20
use function in_array;
21
use function is_int;
22
use function is_numeric;
23
use function sqlsrv_errors;
24
use function sqlsrv_execute;
25
use function sqlsrv_fetch;
26
use function sqlsrv_fetch_array;
27
use function sqlsrv_fetch_object;
28
use function sqlsrv_get_field;
29
use function sqlsrv_next_result;
30
use function sqlsrv_num_fields;
31
use function SQLSRV_PHPTYPE_STREAM;
32
use function SQLSRV_PHPTYPE_STRING;
33
use function sqlsrv_prepare;
34
use function sqlsrv_rows_affected;
35
use function SQLSRV_SQLTYPE_VARBINARY;
36
use function stripos;
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
    public const LAST_INSERT_ID_SQL = ';SELECT SCOPE_IDENTITY() AS LastInsertId;';
128
129
    /**
130
     * @param resource $conn
131
     * @param string   $sql
132
     */
133 243
    public function __construct($conn, $sql, ?LastInsertId $lastInsertId = null)
134
    {
135 243
        $this->conn = $conn;
136 243
        $this->sql  = $sql;
137
138 243
        if (stripos($sql, 'INSERT INTO ') !== 0) {
139 236
            return;
140
        }
141
142 86
        $this->sql         .= self::LAST_INSERT_ID_SQL;
143 86
        $this->lastInsertId = $lastInsertId;
144 86
    }
145
146
    /**
147
     * {@inheritdoc}
148
     */
149 109
    public function bindValue($param, $value, $type = ParameterType::STRING)
150
    {
151 109
        if (! is_numeric($param)) {
152
            throw new SQLSrvException(
153
                'sqlsrv does not support named parameters to queries, use question mark (?) placeholders instead.'
154
            );
155
        }
156
157 109
        $this->variables[$param] = $value;
158 109
        $this->types[$param]     = $type;
159 109
    }
160
161
    /**
162
     * {@inheritdoc}
163
     */
164 8
    public function bindParam($column, &$variable, $type = ParameterType::STRING, $length = null)
165
    {
166 8
        if (! is_numeric($column)) {
167
            throw new SQLSrvException('sqlsrv does not support named parameters to queries, use question mark (?) placeholders instead.');
168
        }
169
170 8
        $this->variables[$column] =& $variable;
171 8
        $this->types[$column]     = $type;
172
173
        // unset the statement resource if it exists as the new one will need to be bound to the new variable
174 8
        $this->stmt = null;
175 8
    }
176
177
    /**
178
     * {@inheritdoc}
179
     */
180 20
    public function closeCursor()
181
    {
182
        // not having the result means there's nothing to close
183 20
        if ($this->stmt === null || ! $this->result) {
184 4
            return true;
185
        }
186
187
        // emulate it by fetching and discarding rows, similarly to what PDO does in this case
188
        // @link http://php.net/manual/en/pdostatement.closecursor.php
189
        // @link https://github.com/php/php-src/blob/php-7.0.11/ext/pdo/pdo_stmt.c#L2075
190
        // deliberately do not consider multiple result sets, since doctrine/dbal doesn't support them
191 16
        while (sqlsrv_fetch($this->stmt)) {
192
        }
193
194 16
        $this->result = false;
195
196 16
        return true;
197
    }
198
199
    /**
200
     * {@inheritdoc}
201
     */
202 4
    public function columnCount()
203
    {
204 4
        if ($this->stmt === null) {
205
            return 0;
206
        }
207
208 4
        return sqlsrv_num_fields($this->stmt) ?: 0;
209
    }
210
211
    /**
212
     * {@inheritdoc}
213
     */
214
    public function errorCode()
215
    {
216
        $errors = sqlsrv_errors(SQLSRV_ERR_ERRORS);
217
        if ($errors) {
218
            return $errors[0]['code'];
219
        }
220
221
        return false;
222
    }
223
224
    /**
225
     * {@inheritdoc}
226
     */
227
    public function errorInfo()
228
    {
229
        return (array) sqlsrv_errors(SQLSRV_ERR_ERRORS);
230
    }
231
232
    /**
233
     * {@inheritdoc}
234
     */
235 235
    public function execute($params = null)
236
    {
237 235
        if ($params) {
238 78
            $hasZeroIndex = array_key_exists(0, $params);
239
240 78
            foreach ($params as $key => $val) {
241 78
                if ($hasZeroIndex && is_int($key)) {
242 78
                    $this->bindValue($key + 1, $val);
243
                } else {
244
                    $this->bindValue($key, $val);
245
                }
246
            }
247
        }
248
249 235
        if (! $this->stmt) {
250 235
            $this->stmt = $this->prepare();
251
        }
252
253 234
        if (! sqlsrv_execute($this->stmt)) {
254
            throw SQLSrvException::fromSqlSrvErrors();
255
        }
256
257 234
        if ($this->lastInsertId) {
258 86
            sqlsrv_next_result($this->stmt);
259 86
            sqlsrv_fetch($this->stmt);
260 86
            $this->lastInsertId->setId(sqlsrv_get_field($this->stmt, 0));
261
        }
262
263 234
        $this->result = true;
264 234
    }
265
266
    /**
267
     * Prepares SQL Server statement resource
268
     *
269
     * @return resource
270
     *
271
     * @throws SQLSrvException
272
     */
273 235
    private function prepare()
274
    {
275 235
        $params = [];
276
277 235
        foreach ($this->variables as $column => &$variable) {
278 115
            switch ($this->types[$column]) {
279
                case ParameterType::LARGE_OBJECT:
280 7
                    $params[$column - 1] = [
281 7
                        &$variable,
282
                        SQLSRV_PARAM_IN,
283 7
                        SQLSRV_PHPTYPE_STREAM(SQLSRV_ENC_BINARY),
284 7
                        SQLSRV_SQLTYPE_VARBINARY('max'),
0 ignored issues
show
Bug introduced by
'max' of type string is incompatible with the type integer expected by parameter $byteCount of SQLSRV_SQLTYPE_VARBINARY(). ( Ignorable by Annotation )

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

284
                        SQLSRV_SQLTYPE_VARBINARY(/** @scrutinizer ignore-type */ 'max'),
Loading history...
285
                    ];
286 7
                    break;
287
288
                case ParameterType::BINARY:
289 1
                    $params[$column - 1] = [
290 1
                        &$variable,
291
                        SQLSRV_PARAM_IN,
292 1
                        SQLSRV_PHPTYPE_STRING(SQLSRV_ENC_BINARY),
293
                    ];
294 1
                    break;
295
296
                default:
297 112
                    $params[$column - 1] =& $variable;
298 112
                    break;
299
            }
300
        }
301
302 235
        $stmt = sqlsrv_prepare($this->conn, $this->sql, $params);
303
304 235
        if (! $stmt) {
0 ignored issues
show
introduced by
$stmt is of type false|resource, thus it always evaluated to false.
Loading history...
305 1
            throw SQLSrvException::fromSqlSrvErrors();
306
        }
307
308 234
        return $stmt;
309
    }
310
311
    /**
312
     * {@inheritdoc}
313
     */
314 236
    public function setFetchMode($fetchMode, $arg2 = null, $arg3 = null)
315
    {
316 236
        $this->defaultFetchMode          = $fetchMode;
317 236
        $this->defaultFetchClass         = $arg2 ?: $this->defaultFetchClass;
318 236
        $this->defaultFetchClassCtorArgs = $arg3 ? (array) $arg3 : $this->defaultFetchClassCtorArgs;
319
320 236
        return true;
321
    }
322
323
    /**
324
     * {@inheritdoc}
325
     */
326 3
    public function getIterator()
327
    {
328 3
        return new StatementIterator($this);
329
    }
330
331
    /**
332
     * {@inheritdoc}
333
     *
334
     * @throws SQLSrvException
335
     */
336 226
    public function fetch($fetchMode = null, $cursorOrientation = PDO::FETCH_ORI_NEXT, $cursorOffset = 0)
337
    {
338
        // do not try fetching from the statement if it's not expected to contain result
339
        // in order to prevent exceptional situation
340 226
        if ($this->stmt === null || ! $this->result) {
341 9
            return false;
342
        }
343
344 217
        $args      = func_get_args();
345 217
        $fetchMode = $fetchMode ?: $this->defaultFetchMode;
346
347 217
        if ($fetchMode === FetchMode::COLUMN) {
348 1
            return $this->fetchColumn();
349
        }
350
351 217
        if (isset(self::$fetchMap[$fetchMode])) {
352 213
            return sqlsrv_fetch_array($this->stmt, self::$fetchMap[$fetchMode]) ?: false;
353
        }
354
355 4
        if (in_array($fetchMode, [FetchMode::STANDARD_OBJECT, FetchMode::CUSTOM_OBJECT], true)) {
356 4
            $className = $this->defaultFetchClass;
357 4
            $ctorArgs  = $this->defaultFetchClassCtorArgs;
358
359 4
            if (count($args) >= 2) {
360 1
                $className = $args[1];
361 1
                $ctorArgs  = $args[2] ?? [];
362
            }
363
364 4
            return sqlsrv_fetch_object($this->stmt, $className, $ctorArgs) ?: false;
365
        }
366
367
        throw new SQLSrvException('Fetch mode is not supported!');
368
    }
369
370
    /**
371
     * {@inheritdoc}
372
     */
373 110
    public function fetchAll($fetchMode = null, $fetchArgument = null, $ctorArgs = null)
374
    {
375 110
        $rows = [];
376
377 110
        switch ($fetchMode) {
378
            case FetchMode::CUSTOM_OBJECT:
379 1
                while (($row = $this->fetch(...func_get_args())) !== false) {
380 1
                    $rows[] = $row;
381
                }
382 1
                break;
383
384
            case FetchMode::COLUMN:
385 9
                while (($row = $this->fetchColumn()) !== false) {
386 9
                    $rows[] = $row;
387
                }
388 9
                break;
389
390
            default:
391 100
                while (($row = $this->fetch($fetchMode)) !== false) {
392 92
                    $rows[] = $row;
393
                }
394
        }
395
396 110
        return $rows;
397
    }
398
399
    /**
400
     * {@inheritdoc}
401
     */
402 60
    public function fetchColumn($columnIndex = 0)
403
    {
404 60
        $row = $this->fetch(FetchMode::NUMERIC);
405
406 60
        if ($row === false) {
407 15
            return false;
408
        }
409
410 54
        return $row[$columnIndex] ?? null;
411
    }
412
413
    /**
414
     * {@inheritdoc}
415
     */
416 86
    public function rowCount()
417
    {
418 86
        if ($this->stmt === null) {
419
            return 0;
420
        }
421
422 86
        return sqlsrv_rows_affected($this->stmt) ?: 0;
423
    }
424
}
425