Passed
Push — fix-branch-aliases ( 626e10 )
by Michael
26:54
created

SQLSrvStatement   B

Complexity

Total Complexity 48

Size/Duplication

Total Lines 369
Duplicated Lines 0 %

Test Coverage

Coverage 60.69%

Importance

Changes 0
Metric Value
wmc 48
dl 0
loc 369
ccs 105
cts 173
cp 0.6069
rs 8.4864
c 0
b 0
f 0

15 Methods

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

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
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 string
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 235
    public function __construct($conn, $sql, LastInsertId $lastInsertId = null)
155
    {
156 235
        $this->conn = $conn;
157 235
        $this->sql = $sql;
158
159 235
        if (stripos($sql, 'INSERT INTO ') === 0) {
160 82
            $this->sql .= self::LAST_INSERT_ID_SQL;
161 82
            $this->lastInsertId = $lastInsertId;
162
        }
163 235
    }
164
165
    /**
166
     * {@inheritdoc}
167
     */
168 107
    public function bindValue($param, $value, $type = ParameterType::STRING)
169
    {
170 107
        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 107
        $this->variables[$param] = $value;
177 107
        $this->types[$param] = $type;
178 107
    }
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;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the return type mandated by Doctrine\DBAL\Driver\Statement::errorCode() of string.

In the issue above, the returned value is violating the contract defined by the mentioned interface.

Let's take a look at an example:

interface HasName {
    /** @return string */
    public function getName();
}

class Name {
    public $name;
}

class User implements HasName {
    /** @return string|Name */
    public function getName() {
        return new Name('foo'); // This is a violation of the ``HasName`` interface
                                // which only allows a string value to be returned.
    }
}
Loading history...
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 227
    public function execute($params = null)
250
    {
251 227
        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 227
        if ( ! $this->stmt) {
260 227
            $this->stmt = $this->prepare();
261
        }
262
263 226
        if (!sqlsrv_execute($this->stmt)) {
264
            throw SQLSrvException::fromSqlSrvErrors();
265
        }
266
267 226
        if ($this->lastInsertId) {
268 82
            sqlsrv_next_result($this->stmt);
269 82
            sqlsrv_fetch($this->stmt);
270 82
            $this->lastInsertId->setId(sqlsrv_get_field($this->stmt, 0));
271
        }
272
273 226
        $this->result = true;
274 226
    }
275
276
    /**
277
     * Prepares SQL Server statement resource
278
     *
279
     * @return resource
280
     * @throws SQLSrvException
281
     */
282 227
    private function prepare()
283
    {
284 227
        $params = [];
285
286 227
        foreach ($this->variables as $column => &$variable) {
287 112
            switch ($this->types[$column]) {
288 112
                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 111
                case ParameterType::BINARY:
298 1
                    $params[$column - 1] = [
299 1
                        &$variable,
300
                        SQLSRV_PARAM_IN,
301 1
                        SQLSRV_PHPTYPE_STRING(SQLSRV_ENC_BINARY),
302
                    ];
303 1
                    break;
304
305
                default:
306 110
                    $params[$column - 1] =& $variable;
307 112
                    break;
308
            }
309
        }
310
311 227
        $stmt = sqlsrv_prepare($this->conn, $this->sql, $params);
312
313 227
        if (!$stmt) {
0 ignored issues
show
introduced by
$stmt is of type resource|false, thus it always evaluated to false.
Loading history...
314 1
            throw SQLSrvException::fromSqlSrvErrors();
315
        }
316
317 226
        return $stmt;
318
    }
319
320
    /**
321
     * {@inheritdoc}
322
     */
323 228
    public function setFetchMode($fetchMode, $arg2 = null, $arg3 = null)
324
    {
325 228
        $this->defaultFetchMode          = $fetchMode;
326 228
        $this->defaultFetchClass         = $arg2 ?: $this->defaultFetchClass;
327 228
        $this->defaultFetchClassCtorArgs = $arg3 ? (array) $arg3 : $this->defaultFetchClassCtorArgs;
0 ignored issues
show
Documentation Bug introduced by
It seems like $arg3 ? (array)$arg3 : $...faultFetchClassCtorArgs can also be of type array. However, the property $defaultFetchClassCtorArgs is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
328
329 228
        return true;
330
    }
331
332
    /**
333
     * {@inheritdoc}
334
     */
335 3
    public function getIterator()
336
    {
337 3
        return new StatementIterator($this);
338
    }
339
340
    /**
341
     * {@inheritdoc}
342
     *
343
     * @throws SQLSrvException
344
     */
345 218
    public function fetch($fetchMode = null, $cursorOrientation = \PDO::FETCH_ORI_NEXT, $cursorOffset = 0)
346
    {
347
        // do not try fetching from the statement if it's not expected to contain result
348
        // in order to prevent exceptional situation
349 218
        if (!$this->result) {
350 9
            return false;
351
        }
352
353 209
        $args      = func_get_args();
354 209
        $fetchMode = $fetchMode ?: $this->defaultFetchMode;
355
356 209
        if ($fetchMode === FetchMode::COLUMN) {
357 1
            return $this->fetchColumn();
358
        }
359
360 209
        if (isset(self::$fetchMap[$fetchMode])) {
361 205
            return sqlsrv_fetch_array($this->stmt, self::$fetchMap[$fetchMode]) ?: false;
362
        }
363
364 4
        if (in_array($fetchMode, [FetchMode::STANDARD_OBJECT, FetchMode::CUSTOM_OBJECT], true)) {
365 4
            $className = $this->defaultFetchClass;
366 4
            $ctorArgs  = $this->defaultFetchClassCtorArgs;
367
368 4
            if (count($args) >= 2) {
369 1
                $className = $args[1];
370 1
                $ctorArgs  = $args[2] ?? [];
371
            }
372
373 4
            return sqlsrv_fetch_object($this->stmt, $className, $ctorArgs) ?: false;
374
        }
375
376
        throw new SQLSrvException('Fetch mode is not supported!');
377
    }
378
379
    /**
380
     * {@inheritdoc}
381
     */
382 104
    public function fetchAll($fetchMode = null, $fetchArgument = null, $ctorArgs = null)
383
    {
384 104
        $rows = [];
385
386
        switch ($fetchMode) {
387 104
            case FetchMode::CUSTOM_OBJECT:
388 1
                while (($row = $this->fetch(...func_get_args())) !== false) {
389 1
                    $rows[] = $row;
390
                }
391 1
                break;
392
393 103
            case FetchMode::COLUMN:
394 4
                while (($row = $this->fetchColumn()) !== false) {
395 4
                    $rows[] = $row;
396
                }
397 4
                break;
398
399
            default:
400 99
                while (($row = $this->fetch($fetchMode)) !== false) {
401 92
                    $rows[] = $row;
402
                }
403
        }
404
405 104
        return $rows;
406
    }
407
408
    /**
409
     * {@inheritdoc}
410
     */
411 53
    public function fetchColumn($columnIndex = 0)
412
    {
413 53
        $row = $this->fetch(FetchMode::NUMERIC);
414
415 53
        if (false === $row) {
416 10
            return false;
417
        }
418
419 47
        return $row[$columnIndex] ?? null;
420
    }
421
422
    /**
423
     * {@inheritdoc}
424
     */
425 83
    public function rowCount()
426
    {
427 83
        return sqlsrv_rows_affected($this->stmt);
428
    }
429
}
430