Failed Conditions
Pull Request — master (#3074)
by Sergei
15:08
created

SQLSrvStatement::trackLastInsertId()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

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

286
                    SQLSRV_SQLTYPE_VARBINARY(/** @scrutinizer ignore-type */ 'max'),
Loading history...
287
                ];
288
            } else {
289
                $params[$column - 1] =& $variable;
290
            }
291
        }
292
293
        $stmt = sqlsrv_prepare($this->conn, $this->sql, $params);
294
295
        if (!$stmt) {
0 ignored issues
show
introduced by
$stmt is of type resource|false, thus it always evaluated to false.
Loading history...
296
            throw SQLSrvException::fromSqlSrvErrors();
297
        }
298
299
        return $stmt;
300
    }
301
302
    /**
303
     * {@inheritdoc}
304
     */
305
    public function setFetchMode($fetchMode, $arg2 = null, $arg3 = null)
306
    {
307
        $this->defaultFetchMode          = $fetchMode;
308
        $this->defaultFetchClass         = $arg2 ?: $this->defaultFetchClass;
309
        $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...
310
311
        return true;
312
    }
313
314
    /**
315
     * {@inheritdoc}
316
     */
317
    public function getIterator()
318
    {
319
        return new StatementIterator($this);
320
    }
321
322
    /**
323
     * {@inheritdoc}
324
     *
325
     * @throws SQLSrvException
326
     */
327
    public function fetch($fetchMode = null, $cursorOrientation = \PDO::FETCH_ORI_NEXT, $cursorOffset = 0)
328
    {
329
        // do not try fetching from the statement if it's not expected to contain result
330
        // in order to prevent exceptional situation
331
        if (!$this->result) {
332
            return false;
333
        }
334
335
        $args      = func_get_args();
336
        $fetchMode = $fetchMode ?: $this->defaultFetchMode;
337
338
        if ($fetchMode === FetchMode::COLUMN) {
339
            return $this->fetchColumn();
340
        }
341
342
        if (isset(self::$fetchMap[$fetchMode])) {
343
            return sqlsrv_fetch_array($this->stmt, self::$fetchMap[$fetchMode]) ?: false;
344
        }
345
346
        if (in_array($fetchMode, [FetchMode::STANDARD_OBJECT, FetchMode::CUSTOM_OBJECT], true)) {
347
            $className = $this->defaultFetchClass;
348
            $ctorArgs  = $this->defaultFetchClassCtorArgs;
349
350
            if (count($args) >= 2) {
351
                $className = $args[1];
352
                $ctorArgs  = $args[2] ?? [];
353
            }
354
355
            return sqlsrv_fetch_object($this->stmt, $className, $ctorArgs) ?: false;
356
        }
357
358
        throw new SQLSrvException('Fetch mode is not supported!');
359
    }
360
361
    /**
362
     * {@inheritdoc}
363
     */
364
    public function fetchAll($fetchMode = null, $fetchArgument = null, $ctorArgs = null)
365
    {
366
        $rows = [];
367
368
        switch ($fetchMode) {
369
            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...
370
                while (($row = $this->fetch(...func_get_args())) !== false) {
371
                    $rows[] = $row;
372
                }
373
                break;
374
375
            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...
376
                while (($row = $this->fetchColumn()) !== false) {
377
                    $rows[] = $row;
378
                }
379
                break;
380
381
            default:
382
                while (($row = $this->fetch($fetchMode)) !== false) {
383
                    $rows[] = $row;
384
                }
385
        }
386
387
        return $rows;
388
    }
389
390
    /**
391
     * {@inheritdoc}
392
     */
393
    public function fetchColumn($columnIndex = 0)
394
    {
395
        $row = $this->fetch(FetchMode::NUMERIC);
396
397
        if (false === $row) {
398
            return false;
399
        }
400
401
        return $row[$columnIndex] ?? null;
402
    }
403
404
    /**
405
     * {@inheritdoc}
406
     */
407
    public function rowCount()
408
    {
409
        return sqlsrv_rows_affected($this->stmt);
410
    }
411
412
    private function trackLastInsertId() : void
413
    {
414
        if (! $this->lastInsertId) {
415
            return;
416
        }
417
418
        $statement = sqlsrv_query($this->conn, 'SELECT @@IDENTITY');
419
420
        if (false !== $statement) {
421
            sqlsrv_fetch($statement);
422
423
            $lastInsertId = sqlsrv_get_field($statement, 0) ?: '0';
424
425
            $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

425
            $this->lastInsertId->setId(/** @scrutinizer ignore-type */ $lastInsertId);
Loading history...
426
        }
427
    }
428
}
429