Failed Conditions
Pull Request — master (#3138)
by Michael
53:05 queued 49:36
created

SQLSrvStatement   B

Complexity

Total Complexity 47

Size/Duplication

Total Lines 357
Duplicated Lines 0 %

Test Coverage

Coverage 60.12%

Importance

Changes 0
Metric Value
wmc 47
dl 0
loc 357
ccs 98
cts 163
cp 0.6012
rs 8.439
c 0
b 0
f 0

15 Methods

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

267
                    SQLSRV_SQLTYPE_VARBINARY(/** @scrutinizer ignore-type */ 'max'),
Loading history...
268
                ];
269
            } else {
270 110
                $params[$column - 1] =& $variable;
271
            }
272
        }
273
274 225
        $stmt = sqlsrv_prepare($this->conn, $this->sql, $params);
275
276 225
        if (!$stmt) {
0 ignored issues
show
introduced by
$stmt is of type resource|false, thus it always evaluated to false.
Loading history...
277 1
            throw SQLSrvException::fromSqlSrvErrors();
278
        }
279
280 224
        return $stmt;
281
    }
282
283
    /**
284
     * {@inheritdoc}
285
     */
286 226
    public function setFetchMode($fetchMode, $arg2 = null, $arg3 = null)
287
    {
288 226
        $this->defaultFetchMode          = $fetchMode;
289 226
        $this->defaultFetchClass         = $arg2 ?: $this->defaultFetchClass;
290 226
        $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...
291
292 226
        return true;
293
    }
294
295
    /**
296
     * {@inheritdoc}
297
     */
298 3
    public function getIterator()
299
    {
300 3
        return new StatementIterator($this);
301
    }
302
303
    /**
304
     * {@inheritdoc}
305
     *
306
     * @throws SQLSrvException
307
     */
308 216
    public function fetch($fetchMode = null, $cursorOrientation = \PDO::FETCH_ORI_NEXT, $cursorOffset = 0)
309
    {
310
        // do not try fetching from the statement if it's not expected to contain result
311
        // in order to prevent exceptional situation
312 216
        if (!$this->result) {
313 9
            return false;
314
        }
315
316 207
        $args      = func_get_args();
317 207
        $fetchMode = $fetchMode ?: $this->defaultFetchMode;
318
319 207
        if ($fetchMode === FetchMode::COLUMN) {
320 1
            return $this->fetchColumn();
321
        }
322
323 207
        if (isset(self::$fetchMap[$fetchMode])) {
324 203
            return sqlsrv_fetch_array($this->stmt, self::$fetchMap[$fetchMode]) ?: false;
325
        }
326
327 4
        if (in_array($fetchMode, [FetchMode::STANDARD_OBJECT, FetchMode::CUSTOM_OBJECT], true)) {
328 4
            $className = $this->defaultFetchClass;
329 4
            $ctorArgs  = $this->defaultFetchClassCtorArgs;
330
331 4
            if (count($args) >= 2) {
332 1
                $className = $args[1];
333 1
                $ctorArgs  = $args[2] ?? [];
334
            }
335
336 4
            return sqlsrv_fetch_object($this->stmt, $className, $ctorArgs) ?: false;
337
        }
338
339
        throw new SQLSrvException('Fetch mode is not supported!');
340
    }
341
342
    /**
343
     * {@inheritdoc}
344
     */
345 103
    public function fetchAll($fetchMode = null, $fetchArgument = null, $ctorArgs = null)
346
    {
347 103
        $rows = [];
348
349
        switch ($fetchMode) {
350 103
            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...
351 1
                while (($row = $this->fetch(...func_get_args())) !== false) {
352 1
                    $rows[] = $row;
353
                }
354 1
                break;
355
356 102
            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...
357 4
                while (($row = $this->fetchColumn()) !== false) {
358 4
                    $rows[] = $row;
359
                }
360 4
                break;
361
362
            default:
363 98
                while (($row = $this->fetch($fetchMode)) !== false) {
364 91
                    $rows[] = $row;
365
                }
366
        }
367
368 103
        return $rows;
369
    }
370
371
    /**
372
     * {@inheritdoc}
373
     */
374 51
    public function fetchColumn($columnIndex = 0)
375
    {
376 51
        $row = $this->fetch(FetchMode::NUMERIC);
377
378 51
        if (false === $row) {
379 10
            return false;
380
        }
381
382 45
        return $row[$columnIndex] ?? null;
383
    }
384
385
    /**
386
     * {@inheritdoc}
387
     */
388 81
    public function rowCount()
389
    {
390 81
        return sqlsrv_rows_affected($this->stmt);
391
    }
392
}
393