Completed
Push — develop ( 361a2b...a96e4b )
by Marco
20s queued 14s
created

SQLSrvStatement   B

Complexity

Total Complexity 52

Size/Duplication

Total Lines 368
Duplicated Lines 0 %

Test Coverage

Coverage 56.57%

Importance

Changes 0
Metric Value
wmc 52
eloc 122
dl 0
loc 368
ccs 99
cts 175
cp 0.5657
rs 7.44
c 0
b 0
f 0

13 Methods

Rating   Name   Duplication   Size   Complexity  
A bindParam() 0 11 2
A bindValue() 0 10 2
A __construct() 0 11 2
A columnCount() 0 7 3
A closeCursor() 0 15 4
A rowCount() 0 7 3
A setFetchMode() 0 13 3
B fetch() 0 31 10
B execute() 0 29 8
A fetchColumn() 0 13 3
A prepare() 0 36 5
A fetchAll() 0 24 6
A getIterator() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like SQLSrvStatement often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SQLSrvStatement, and based on these observations, apply Extract Interface, too.

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

391
        if (! array_key_exists($columnIndex, /** @scrutinizer ignore-type */ $row)) {
Loading history...
392
            throw DBALException::invalidColumnIndex($columnIndex, count($row));
393 215
        }
394 215
395
        return $row[$columnIndex];
396
    }
397
398 225
    /**
399
     * {@inheritdoc}
400
     */
401
    public function rowCount() : int
402
    {
403
        if ($this->stmt === null) {
404 225
            return 0;
405
        }
406 225
407
        return sqlsrv_rows_affected($this->stmt) ?: 0;
408 225
    }
409
}
410