Failed Conditions
Push — master ( 01c22b...e42c1f )
by Marco
79:13 queued 10s
created

SQLSrvStatement   B

Complexity

Total Complexity 50

Size/Duplication

Total Lines 363
Duplicated Lines 0 %

Test Coverage

Coverage 57.06%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 50
eloc 119
dl 0
loc 363
ccs 97
cts 170
cp 0.5706
rs 8.4
c 2
b 0
f 0

13 Methods

Rating   Name   Duplication   Size   Complexity  
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 bindParam() 0 9 1
A bindValue() 0 6 1
A columnCount() 0 7 3
A prepare() 0 36 5
A closeCursor() 0 15 4
A getIterator() 0 3 1
A __construct() 0 11 2
A fetchAll() 0 24 6

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\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
     * Append to any INSERT query to retrieve the last insert id.
125
     *
126
     * @deprecated This constant has been deprecated and will be made private in 3.0
127
     */
128
    public const LAST_INSERT_ID_SQL = ';SELECT SCOPE_IDENTITY() AS LastInsertId;';
129
130
    /**
131
     * @param resource $conn
132
     */
133
    public function __construct($conn, string $sql, ?LastInsertId $lastInsertId = null)
134
    {
135 228
        $this->conn = $conn;
136
        $this->sql  = $sql;
137 228
138 228
        if (stripos($sql, 'INSERT INTO ') !== 0) {
139
            return;
140 228
        }
141 227
142
        $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

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

386
        if (! array_key_exists($columnIndex, /** @scrutinizer ignore-type */ $row)) {
Loading history...
387 227
            throw InvalidColumnIndex::new($columnIndex, count($row));
388 227
        }
389
390 227
        return $row[$columnIndex];
391
    }
392
393 216
    /**
394 216
     * {@inheritdoc}
395
     */
396
    public function rowCount() : int
397
    {
398 227
        if ($this->stmt === null) {
399
            return 0;
400
        }
401
402
        return sqlsrv_rows_affected($this->stmt) ?: 0;
403
    }
404
}
405