Failed Conditions
Pull Request — master (#2765)
by Sergei
34:48
created

SQLSrvStatement::execute()   B

Complexity

Conditions 7
Paths 8

Size

Total Lines 21
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 56

Importance

Changes 0
Metric Value
dl 0
loc 21
ccs 0
cts 17
cp 0
rs 7.551
c 0
b 0
f 0
cc 7
eloc 11
nc 8
nop 1
crap 56
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 function func_get_args;
28
29
/**
30
 * SQL Server Statement.
31
 *
32
 * @since 2.3
33
 * @author Benjamin Eberlei <[email protected]>
34
 */
35
class SQLSrvStatement implements IteratorAggregate, Statement
36
{
37
    /**
38
     * The SQLSRV Resource.
39
     *
40
     * @var resource
41
     */
42
    private $conn;
43
44
    /**
45
     * The SQL statement to execute.
46
     *
47
     * @var string
48
     */
49
    private $sql;
50
51
    /**
52
     * The SQLSRV statement resource.
53
     *
54
     * @var resource
55
     */
56
    private $stmt;
57
58
    /**
59
     * References to the variables bound as statement parameters.
60
     *
61
     * @var array
62
     */
63
    private $variables = [];
64
65
    /**
66
     * Bound parameter types.
67
     *
68
     * @var array
69
     */
70
    private $types = [];
71
72
    /**
73
     * Translations.
74
     *
75
     * @var array
76
     */
77
    private static $fetchMap = [
78
        FetchMode::MIXED       => SQLSRV_FETCH_BOTH,
79
        FetchMode::ASSOCIATIVE => SQLSRV_FETCH_ASSOC,
80
        FetchMode::NUMERIC     => SQLSRV_FETCH_NUMERIC,
81
    ];
82
83
    /**
84
     * The name of the default class to instantiate when fetching class instances.
85
     *
86
     * @var string
87
     */
88
    private $defaultFetchClass = '\stdClass';
89
90
    /**
91
     * The constructor arguments for the default class to instantiate when fetching class instances.
92
     *
93
     * @var string
94
     */
95
    private $defaultFetchClassCtorArgs = [];
96
97
    /**
98
     * The fetch style.
99
     *
100
     * @var int
101
     */
102
    private $defaultFetchMode = FetchMode::MIXED;
103
104
    /**
105
     * The last insert ID.
106
     *
107
     * @var \Doctrine\DBAL\Driver\SQLSrv\LastInsertId|null
108
     */
109
    private $lastInsertId;
110
111
    /**
112
     * Indicates whether the statement is in the state when fetching results is possible
113
     *
114
     * @var bool
115
     */
116
    private $result = false;
117
118
    /**
119
     * Append to any INSERT query to retrieve the last insert id.
120
     *
121
     * @var string
122
     *
123
     * @deprecated do not rely on this constant in the future, as it will be completely removed
124
     * @internal
125
     */
126
    const LAST_INSERT_ID_SQL = ';SELECT SCOPE_IDENTITY() AS LastInsertId;';
127
128
    /**
129
     * @param resource                                       $conn
130
     * @param string                                         $sql
131
     * @param \Doctrine\DBAL\Driver\SQLSrv\LastInsertId|null $lastInsertId
132
     */
133
    public function __construct($conn, $sql, LastInsertId $lastInsertId = null)
134
    {
135
        $this->conn = $conn;
136
        $this->sql = $sql;
137
        $this->lastInsertId = $lastInsertId;
138
    }
139
140
    /**
141
     * {@inheritdoc}
142
     */
143
    public function bindValue($param, $value, $type = ParameterType::STRING)
144
    {
145
        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
        $this->variables[$param] = $value;
152
        $this->types[$param] = $type;
153
    }
154
155
    /**
156
     * {@inheritdoc}
157
     */
158
    public function bindParam($column, &$variable, $type = ParameterType::STRING, $length = null)
159
    {
160
        if (!is_numeric($column)) {
161
            throw new SQLSrvException("sqlsrv does not support named parameters to queries, use question mark (?) placeholders instead.");
162
        }
163
164
        $this->variables[$column] =& $variable;
165
        $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
        $this->stmt = null;
169
    }
170
171
    /**
172
     * {@inheritdoc}
173
     */
174
    public function closeCursor()
175
    {
176
        // not having the result means there's nothing to close
177
        if (!$this->result) {
178
            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
        while (sqlsrv_fetch($this->stmt));
186
187
        $this->result = false;
188
189
        return true;
190
    }
191
192
    /**
193
     * {@inheritdoc}
194
     */
195
    public function columnCount()
196
    {
197
        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
    public function execute($params = null)
225
    {
226
        if ($params) {
227
            $hasZeroIndex = array_key_exists(0, $params);
228
            foreach ($params as $key => $val) {
229
                $key = ($hasZeroIndex && is_numeric($key)) ? $key + 1 : $key;
230
                $this->bindValue($key, $val);
231
            }
232
        }
233
234
        if ( ! $this->stmt) {
235
            $this->stmt = $this->prepare();
236
        }
237
238
        if (!sqlsrv_execute($this->stmt)) {
239
            throw SQLSrvException::fromSqlSrvErrors();
240
        }
241
242
        $this->trackLastInsertId();
243
244
        $this->result = true;
245
    }
246
247
    /**
248
     * Prepares SQL Server statement resource
249
     *
250
     * @return resource
251
     * @throws SQLSrvException
252
     */
253
    private function prepare()
254
    {
255
        $params = [];
256
257
        foreach ($this->variables as $column => &$variable) {
258
            if ($this->types[$column] === ParameterType::LARGE_OBJECT) {
259
                $params[$column - 1] = [
260
                    &$variable,
261
                    SQLSRV_PARAM_IN,
262
                    SQLSRV_PHPTYPE_STREAM(SQLSRV_ENC_BINARY),
263
                    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

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

402
            $this->lastInsertId->setId(/** @scrutinizer ignore-type */ $lastInsertId);
Loading history...
403
        }
404
    }
405
}
406