Completed
Pull Request — master (#3065)
by Laurence
19:52
created

SQLSrvStatement   B

Complexity

Total Complexity 49

Size/Duplication

Total Lines 378
Duplicated Lines 0 %

Test Coverage

Coverage 61.33%

Importance

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

15 Methods

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

293
                        SQLSRV_SQLTYPE_VARBINARY(/** @scrutinizer ignore-type */ 'max'),
Loading history...
294
                    ];
295 4
                    break;
296
297 114
                case ParameterType::STRING:
298 109
                    $params[$column - 1] = [
299 109
                        &$variable,
300
                        SQLSRV_PARAM_IN,
301 109
                        SQLSRV_PHPTYPE_STRING(SQLSRV_ENC_CHAR),
302 109
                        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

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