Completed
Push — master ( cc3868...bfc8bb )
by Marco
21s queued 15s
created

SQLSrvStatement::fetchColumn()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3.243

Importance

Changes 0
Metric Value
eloc 6
dl 0
loc 13
ccs 7
cts 10
cp 0.7
rs 10
c 0
b 0
f 0
cc 3
nc 3
nop 1
crap 3.243
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
final 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
    private const LAST_INSERT_ID_SQL = ';SELECT SCOPE_IDENTITY() AS LastInsertId;';
127
128
    /**
129
     * @param resource $conn
130
     */
131 324
    public function __construct($conn, string $sql, ?LastInsertId $lastInsertId = null)
132
    {
133 324
        $this->conn = $conn;
134 324
        $this->sql  = $sql;
135
136 324
        if (stripos($sql, 'INSERT INTO ') !== 0) {
137 317
            return;
138
        }
139
140 92
        $this->sql         .= self::LAST_INSERT_ID_SQL;
141 92
        $this->lastInsertId = $lastInsertId;
142 92
    }
143
144
    /**
145
     * {@inheritdoc}
146
     */
147 114
    public function bindValue($param, $value, int $type = ParameterType::STRING) : void
148
    {
149 114
        assert(is_int($param));
150
151 114
        $this->variables[$param] = $value;
152 114
        $this->types[$param]     = $type;
153 114
    }
154
155
    /**
156
     * {@inheritdoc}
157
     */
158 24
    public function bindParam($param, &$variable, int $type = ParameterType::STRING, ?int $length = null) : void
159
    {
160 24
        assert(is_int($param));
161
162 24
        $this->variables[$param] =& $variable;
163 24
        $this->types[$param]     = $type;
164
165
        // unset the statement resource if it exists as the new one will need to be bound to the new variable
166 24
        $this->stmt = null;
167 24
    }
168
169
    /**
170
     * {@inheritdoc}
171
     */
172 19
    public function closeCursor() : void
173
    {
174
        // not having the result means there's nothing to close
175 19
        if ($this->stmt === null || ! $this->result) {
176 3
            return;
177
        }
178
179
        // emulate it by fetching and discarding rows, similarly to what PDO does in this case
180
        // @link http://php.net/manual/en/pdostatement.closecursor.php
181
        // @link https://github.com/php/php-src/blob/php-7.0.11/ext/pdo/pdo_stmt.c#L2075
182
        // deliberately do not consider multiple result sets, since doctrine/dbal doesn't support them
183 16
        while (sqlsrv_fetch($this->stmt) !== false) {
184
        }
185
186 16
        $this->result = false;
187 16
    }
188
189
    /**
190
     * {@inheritdoc}
191
     */
192 4
    public function columnCount() : int
193
    {
194 4
        if ($this->stmt === null) {
195
            return 0;
196
        }
197
198 4
        return sqlsrv_num_fields($this->stmt) ?: 0;
199
    }
200
201
    /**
202
     * {@inheritdoc}
203
     */
204 317
    public function execute(?array $params = null) : void
205
    {
206 317
        if ($params) {
207 83
            $hasZeroIndex = array_key_exists(0, $params);
208
209 83
            foreach ($params as $key => $val) {
210 83
                if ($hasZeroIndex && is_int($key)) {
211 83
                    $this->bindValue($key + 1, $val);
212
                } else {
213
                    $this->bindValue($key, $val);
214
                }
215
            }
216
        }
217
218 317
        if (! $this->stmt) {
219 317
            $this->stmt = $this->prepare();
220
        }
221
222 316
        if (! sqlsrv_execute($this->stmt)) {
223
            throw SQLSrvException::fromSqlSrvErrors();
224
        }
225
226 316
        if ($this->lastInsertId) {
227 92
            sqlsrv_next_result($this->stmt);
228 92
            sqlsrv_fetch($this->stmt);
229 92
            $this->lastInsertId->setId(sqlsrv_get_field($this->stmt, 0));
230
        }
231
232 316
        $this->result = true;
233 316
    }
234
235
    /**
236
     * {@inheritdoc}
237
     */
238 317
    public function setFetchMode(int $fetchMode, ...$args) : void
239
    {
240 317
        $this->defaultFetchMode = $fetchMode;
241
242 317
        if (isset($args[0])) {
243 2
            $this->defaultFetchClass = $args[0];
244
        }
245
246 317
        if (! isset($args[1])) {
247 317
            return;
248
        }
249
250
        $this->defaultFetchClassCtorArgs = (array) $args[1];
251
    }
252
253
    /**
254
     * {@inheritdoc}
255
     */
256 1
    public function getIterator()
257
    {
258 1
        return new StatementIterator($this);
259
    }
260
261
    /**
262
     * {@inheritdoc}
263
     *
264
     * @throws SQLSrvException
265
     */
266 308
    public function fetch(?int $fetchMode = null, ...$args)
267
    {
268
        // do not try fetching from the statement if it's not expected to contain result
269
        // in order to prevent exceptional situation
270 308
        if ($this->stmt === null || ! $this->result) {
271 9
            return false;
272
        }
273
274 299
        $fetchMode = $fetchMode ?: $this->defaultFetchMode;
275
276 299
        if ($fetchMode === FetchMode::COLUMN) {
277 1
            return $this->fetchColumn();
278
        }
279
280 299
        if (isset(self::$fetchMap[$fetchMode])) {
281 295
            return sqlsrv_fetch_array($this->stmt, self::$fetchMap[$fetchMode]) ?: false;
282
        }
283
284 4
        if (in_array($fetchMode, [FetchMode::STANDARD_OBJECT, FetchMode::CUSTOM_OBJECT], true)) {
285 4
            $className = $this->defaultFetchClass;
286 4
            $ctorArgs  = $this->defaultFetchClassCtorArgs;
287
288 4
            if (count($args) > 0) {
289 1
                $className = $args[0];
290 1
                $ctorArgs  = $args[1] ?? [];
291
            }
292
293 4
            return sqlsrv_fetch_object($this->stmt, $className, $ctorArgs) ?: false;
294
        }
295
296
        throw new SQLSrvException('Fetch mode is not supported.');
297
    }
298
299
    /**
300
     * {@inheritdoc}
301
     */
302 120
    public function fetchAll(?int $fetchMode = null, ...$args) : array
303
    {
304 120
        $rows = [];
305
306 120
        switch ($fetchMode) {
307
            case FetchMode::CUSTOM_OBJECT:
308 1
                while (($row = $this->fetch($fetchMode, ...$args)) !== false) {
309 1
                    $rows[] = $row;
310
                }
311 1
                break;
312
313
            case FetchMode::COLUMN:
314 10
                while (($row = $this->fetchColumn()) !== false) {
315 10
                    $rows[] = $row;
316
                }
317 10
                break;
318
319
            default:
320 109
                while (($row = $this->fetch($fetchMode)) !== false) {
321 101
                    $rows[] = $row;
322
                }
323
        }
324
325 120
        return $rows;
326
    }
327
328
    /**
329
     * {@inheritdoc}
330
     */
331 198
    public function fetchColumn(int $columnIndex = 0)
332
    {
333 198
        $row = $this->fetch(FetchMode::NUMERIC);
334
335 198
        if ($row === false) {
336 16
            return false;
337
        }
338
339 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

339
        if (! array_key_exists($columnIndex, /** @scrutinizer ignore-type */ $row)) {
Loading history...
340 2
            throw InvalidColumnIndex::new($columnIndex, count($row));
341
        }
342
343 190
        return $row[$columnIndex];
344
    }
345
346
    /**
347
     * {@inheritdoc}
348
     */
349 91
    public function rowCount() : int
350
    {
351 91
        if ($this->stmt === null) {
352
            return 0;
353
        }
354
355 91
        return sqlsrv_rows_affected($this->stmt) ?: 0;
356
    }
357
358
    /**
359
     * Prepares SQL Server statement resource
360
     *
361
     * @return resource
362
     *
363
     * @throws SQLSrvException
364
     */
365 317
    private function prepare()
366
    {
367 317
        $params = [];
368
369 317
        foreach ($this->variables as $column => &$variable) {
370 136
            switch ($this->types[$column]) {
371
                case ParameterType::LARGE_OBJECT:
372 7
                    $params[$column - 1] = [
373 7
                        &$variable,
374
                        SQLSRV_PARAM_IN,
375 7
                        SQLSRV_PHPTYPE_STREAM(SQLSRV_ENC_BINARY),
376 7
                        SQLSRV_SQLTYPE_VARBINARY('max'),
377
                    ];
378 7
                    break;
379
380
                case ParameterType::BINARY:
381 1
                    $params[$column - 1] = [
382 1
                        &$variable,
383
                        SQLSRV_PARAM_IN,
384 1
                        SQLSRV_PHPTYPE_STRING(SQLSRV_ENC_BINARY),
385
                    ];
386 1
                    break;
387
388
                default:
389 133
                    $params[$column - 1] =& $variable;
390 133
                    break;
391
            }
392
        }
393
394 317
        $stmt = sqlsrv_prepare($this->conn, $this->sql, $params);
395
396 317
        if (! $stmt) {
0 ignored issues
show
introduced by
$stmt is of type false|resource, thus it always evaluated to false.
Loading history...
397 1
            throw SQLSrvException::fromSqlSrvErrors();
398
        }
399
400 316
        return $stmt;
401
    }
402
}
403