Completed
Pull Request — master (#3065)
by Laurence
16:45
created

SQLSrvStatement   B

Complexity

Total Complexity 49

Size/Duplication

Total Lines 378
Duplicated Lines 0 %

Test Coverage

Coverage 61.88%

Importance

Changes 0
Metric Value
wmc 49
eloc 125
dl 0
loc 378
rs 8.48
c 0
b 0
f 0
ccs 112
cts 181
cp 0.6188

15 Methods

Rating   Name   Duplication   Size   Complexity  
A errorInfo() 0 3 1
A rowCount() 0 3 1
A setFetchMode() 0 7 3
B fetch() 0 32 9
A fetchColumn() 0 9 2
A bindParam() 0 11 2
A bindValue() 0 10 2
A columnCount() 0 3 1
B prepare() 0 45 6
A fetchAll() 0 24 6
A errorCode() 0 8 2
A closeCursor() 0 16 3
A getIterator() 0 3 1
A __construct() 0 8 2
B execute() 0 25 8

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
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the MIT license. For more information, see
17
 * <http://www.doctrine-project.org>.
18
 */
19
20
namespace Doctrine\DBAL\Driver\SQLSrv;
21
22
use Doctrine\DBAL\Driver\StatementIterator;
23
use Doctrine\DBAL\FetchMode;
24
use Doctrine\DBAL\ParameterType;
25
use IteratorAggregate;
26
use Doctrine\DBAL\Driver\Statement;
27
use const SQLSRV_ENC_BINARY;
28
use const SQLSRV_ERR_ERRORS;
29
use const SQLSRV_FETCH_ASSOC;
30
use const SQLSRV_FETCH_BOTH;
31
use const SQLSRV_FETCH_NUMERIC;
32
use const SQLSRV_PARAM_IN;
33
use const SQLSRV_ENC_CHAR;
34
use function array_key_exists;
35
use function count;
36
use function func_get_args;
37
use function in_array;
38
use function is_numeric;
39
use function sqlsrv_errors;
40
use function sqlsrv_execute;
41
use function sqlsrv_fetch;
42
use function sqlsrv_fetch_array;
43
use function sqlsrv_fetch_object;
44
use function sqlsrv_get_field;
45
use function sqlsrv_next_result;
46
use function sqlsrv_num_fields;
47
use function SQLSRV_PHPTYPE_STREAM;
48
use function SQLSRV_PHPTYPE_STRING;
49
use function sqlsrv_prepare;
50
use function sqlsrv_rows_affected;
51
use function SQLSRV_SQLTYPE_VARBINARY;
52
use function stripos;
53
use function SQLSRV_SQLTYPE_VARCHAR;
54
55
/**
56
 * SQL Server Statement.
57
 *
58
 * @since 2.3
59
 * @author Benjamin Eberlei <[email protected]>
60
 */
61
class SQLSrvStatement implements IteratorAggregate, Statement
62
{
63
    /**
64
     * The SQLSRV Resource.
65
     *
66
     * @var resource
67
     */
68
    private $conn;
69
70
    /**
71
     * The SQL statement to execute.
72
     *
73
     * @var string
74
     */
75
    private $sql;
76
77
    /**
78
     * The SQLSRV statement resource.
79
     *
80
     * @var resource|null
81
     */
82
    private $stmt;
83
84
    /**
85
     * References to the variables bound as statement parameters.
86
     *
87
     * @var array
88
     */
89
    private $variables = [];
90
91
    /**
92
     * Bound parameter types.
93
     *
94
     * @var array
95
     */
96
    private $types = [];
97
98
    /**
99
     * Translations.
100
     *
101
     * @var array
102
     */
103
    private static $fetchMap = [
104
        FetchMode::MIXED       => SQLSRV_FETCH_BOTH,
105
        FetchMode::ASSOCIATIVE => SQLSRV_FETCH_ASSOC,
106
        FetchMode::NUMERIC     => SQLSRV_FETCH_NUMERIC,
107
    ];
108
109
    /**
110
     * The name of the default class to instantiate when fetching class instances.
111
     *
112
     * @var string
113
     */
114
    private $defaultFetchClass = '\stdClass';
115
116
    /**
117
     * The constructor arguments for the default class to instantiate when fetching class instances.
118
     *
119
     * @var mixed[]
120
     */
121
    private $defaultFetchClassCtorArgs = [];
122
123
    /**
124
     * The fetch style.
125
     *
126
     * @var int
127
     */
128
    private $defaultFetchMode = FetchMode::MIXED;
129
130
    /**
131
     * The last insert ID.
132
     *
133
     * @var \Doctrine\DBAL\Driver\SQLSrv\LastInsertId|null
134
     */
135
    private $lastInsertId;
136
137
    /**
138
     * Indicates whether the statement is in the state when fetching results is possible
139
     *
140
     * @var bool
141
     */
142
    private $result = false;
143
144
    /**
145
     * Append to any INSERT query to retrieve the last insert id.
146
     *
147
     * @var string
148
     */
149
    const LAST_INSERT_ID_SQL = ';SELECT SCOPE_IDENTITY() AS LastInsertId;';
150
151
    /**
152
     * @param resource                                       $conn
153
     * @param string                                         $sql
154
     * @param \Doctrine\DBAL\Driver\SQLSrv\LastInsertId|null $lastInsertId
155
     */
156 240
    public function __construct($conn, $sql, LastInsertId $lastInsertId = null)
157
    {
158 240
        $this->conn = $conn;
159 240
        $this->sql = $sql;
160
161 240
        if (stripos($sql, 'INSERT INTO ') === 0) {
162 83
            $this->sql .= self::LAST_INSERT_ID_SQL;
163 83
            $this->lastInsertId = $lastInsertId;
164
        }
165 240
    }
166
167
    /**
168
     * {@inheritdoc}
169
     */
170 111
    public function bindValue($param, $value, $type = ParameterType::STRING)
171
    {
172 111
        if (!is_numeric($param)) {
173
            throw new SQLSrvException(
174
                'sqlsrv does not support named parameters to queries, use question mark (?) placeholders instead.'
175
            );
176
        }
177
178 111
        $this->variables[$param] = $value;
179 111
        $this->types[$param] = $type;
180 111
    }
181
182
    /**
183
     * {@inheritdoc}
184
     */
185 7
    public function bindParam($column, &$variable, $type = ParameterType::STRING, $length = null)
186
    {
187 7
        if (!is_numeric($column)) {
188
            throw new SQLSrvException("sqlsrv does not support named parameters to queries, use question mark (?) placeholders instead.");
189
        }
190
191 7
        $this->variables[$column] =& $variable;
192 7
        $this->types[$column] = $type;
193
194
        // unset the statement resource if it exists as the new one will need to be bound to the new variable
195 7
        $this->stmt = null;
196 7
    }
197
198
    /**
199
     * {@inheritdoc}
200
     */
201 19
    public function closeCursor()
202
    {
203
        // not having the result means there's nothing to close
204 19
        if (!$this->result) {
205 4
            return true;
206
        }
207
208
        // emulate it by fetching and discarding rows, similarly to what PDO does in this case
209
        // @link http://php.net/manual/en/pdostatement.closecursor.php
210
        // @link https://github.com/php/php-src/blob/php-7.0.11/ext/pdo/pdo_stmt.c#L2075
211
        // deliberately do not consider multiple result sets, since doctrine/dbal doesn't support them
212 15
        while (sqlsrv_fetch($this->stmt));
213
214 15
        $this->result = false;
215
216 15
        return true;
217
    }
218
219
    /**
220
     * {@inheritdoc}
221
     */
222 4
    public function columnCount()
223
    {
224 4
        return sqlsrv_num_fields($this->stmt);
225
    }
226
227
    /**
228
     * {@inheritdoc}
229
     */
230
    public function errorCode()
231
    {
232
        $errors = sqlsrv_errors(SQLSRV_ERR_ERRORS);
233
        if ($errors) {
234
            return $errors[0]['code'];
235
        }
236
237
        return false;
238
    }
239
240
    /**
241
     * {@inheritdoc}
242
     */
243
    public function errorInfo()
244
    {
245
        return sqlsrv_errors(SQLSRV_ERR_ERRORS);
246
    }
247
248
    /**
249
     * {@inheritdoc}
250
     */
251 232
    public function execute($params = null)
252
    {
253 232
        if ($params) {
254 78
            $hasZeroIndex = array_key_exists(0, $params);
255 78
            foreach ($params as $key => $val) {
256 78
                $key = ($hasZeroIndex && is_numeric($key)) ? $key + 1 : $key;
257 78
                $this->bindValue($key, $val);
258
            }
259
        }
260
261 232
        if ( ! $this->stmt) {
262 232
            $this->stmt = $this->prepare();
263
        }
264
265 231
        if (!sqlsrv_execute($this->stmt)) {
266 1
            throw SQLSrvException::fromSqlSrvErrors();
267
        }
268
269 231
        if ($this->lastInsertId) {
270 83
            sqlsrv_next_result($this->stmt);
271 83
            sqlsrv_fetch($this->stmt);
272 83
            $this->lastInsertId->setId(sqlsrv_get_field($this->stmt, 0));
273
        }
274
275 231
        $this->result = true;
276 231
    }
277
278
    /**
279
     * Prepares SQL Server statement resource
280
     *
281
     * @return resource
282
     * @throws SQLSrvException
283
     */
284 232
    private function prepare()
285
    {
286 232
        $params = [];
287
288 232
        foreach ($this->variables as $column => &$variable) {
289 116
            switch ($this->types[$column]) {
290 116
                case ParameterType::LARGE_OBJECT:
291 4
                    $params[$column - 1] = [
292 4
                        &$variable,
293
                        SQLSRV_PARAM_IN,
294 4
                        SQLSRV_PHPTYPE_STREAM(SQLSRV_ENC_BINARY),
295 4
                        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

295
                        SQLSRV_SQLTYPE_VARBINARY(/** @scrutinizer ignore-type */ 'max'),
Loading history...
296
                    ];
297 4
                    break;
298
299 115
                case ParameterType::STRING:
300 108
                    $params[$column - 1] = [
301 108
                        &$variable,
302
                        SQLSRV_PARAM_IN,
303 108
                        SQLSRV_PHPTYPE_STRING(SQLSRV_ENC_CHAR),
304 108
                        SQLSRV_SQLTYPE_VARCHAR('max'),
0 ignored issues
show
Bug introduced by
'max' of type string is incompatible with the type integer expected by parameter $charCount of SQLSRV_SQLTYPE_VARCHAR(). ( Ignorable by Annotation )

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

304
                        SQLSRV_SQLTYPE_VARCHAR(/** @scrutinizer ignore-type */ 'max'),
Loading history...
305
                    ];
306 108
                    break;
307
308 26
                case ParameterType::BINARY:
309 1
                    $params[$column - 1] = [
310 1
                        &$variable,
311
                        SQLSRV_PARAM_IN,
312 1
                        SQLSRV_PHPTYPE_STRING(SQLSRV_ENC_BINARY),
313
                    ];
314 1
                    break;
315
316
                default:
317 25
                    $params[$column - 1] =& $variable;
318 116
                    break;
319
            }
320
        }
321
322 232
        $stmt = sqlsrv_prepare($this->conn, $this->sql, $params);
323
324 232
        if (!$stmt) {
0 ignored issues
show
introduced by
$stmt is of type resource|false, thus it always evaluated to false.
Loading history...
325 1
            throw SQLSrvException::fromSqlSrvErrors();
326
        }
327
328 231
        return $stmt;
329
    }
330
331
    /**
332
     * {@inheritdoc}
333
     */
334 233
    public function setFetchMode($fetchMode, $arg2 = null, $arg3 = null)
335
    {
336 233
        $this->defaultFetchMode          = $fetchMode;
337 233
        $this->defaultFetchClass         = $arg2 ?: $this->defaultFetchClass;
338 233
        $this->defaultFetchClassCtorArgs = $arg3 ? (array) $arg3 : $this->defaultFetchClassCtorArgs;
339
340 233
        return true;
341
    }
342
343
    /**
344
     * {@inheritdoc}
345
     */
346 3
    public function getIterator()
347
    {
348 3
        return new StatementIterator($this);
349
    }
350
351
    /**
352
     * {@inheritdoc}
353
     *
354
     * @throws SQLSrvException
355
     */
356 223
    public function fetch($fetchMode = null, $cursorOrientation = \PDO::FETCH_ORI_NEXT, $cursorOffset = 0)
357
    {
358
        // do not try fetching from the statement if it's not expected to contain result
359
        // in order to prevent exceptional situation
360 223
        if (!$this->result) {
361 9
            return false;
362
        }
363
364 214
        $args      = func_get_args();
365 214
        $fetchMode = $fetchMode ?: $this->defaultFetchMode;
366
367 214
        if ($fetchMode === FetchMode::COLUMN) {
368 1
            return $this->fetchColumn();
369
        }
370
371 214
        if (isset(self::$fetchMap[$fetchMode])) {
372 210
            return sqlsrv_fetch_array($this->stmt, self::$fetchMap[$fetchMode]) ?: false;
373
        }
374
375 4
        if (in_array($fetchMode, [FetchMode::STANDARD_OBJECT, FetchMode::CUSTOM_OBJECT], true)) {
376 4
            $className = $this->defaultFetchClass;
377 4
            $ctorArgs  = $this->defaultFetchClassCtorArgs;
378
379 4
            if (count($args) >= 2) {
380 1
                $className = $args[1];
381 1
                $ctorArgs  = $args[2] ?? [];
382
            }
383
384 4
            return sqlsrv_fetch_object($this->stmt, $className, $ctorArgs) ?: false;
385
        }
386
387
        throw new SQLSrvException('Fetch mode is not supported!');
388
    }
389
390
    /**
391
     * {@inheritdoc}
392
     */
393 108
    public function fetchAll($fetchMode = null, $fetchArgument = null, $ctorArgs = null)
394
    {
395 108
        $rows = [];
396
397
        switch ($fetchMode) {
398 108
            case FetchMode::CUSTOM_OBJECT:
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
399 1
                while (($row = $this->fetch(...func_get_args())) !== false) {
400 1
                    $rows[] = $row;
401
                }
402 1
                break;
403
404 107
            case FetchMode::COLUMN:
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
405 4
                while (($row = $this->fetchColumn()) !== false) {
406 4
                    $rows[] = $row;
407
                }
408 4
                break;
409
410
            default:
411 103
                while (($row = $this->fetch($fetchMode)) !== false) {
412 96
                    $rows[] = $row;
413
                }
414
        }
415
416 108
        return $rows;
417
    }
418
419
    /**
420
     * {@inheritdoc}
421
     */
422 54
    public function fetchColumn($columnIndex = 0)
423
    {
424 54
        $row = $this->fetch(FetchMode::NUMERIC);
425
426 54
        if (false === $row) {
427 10
            return false;
428
        }
429
430 48
        return $row[$columnIndex] ?? null;
431
    }
432
433
    /**
434
     * {@inheritdoc}
435
     */
436 84
    public function rowCount()
437
    {
438 84
        return sqlsrv_rows_affected($this->stmt);
439
    }
440
}
441