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

SQLSrvStatement::errorInfo()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 3
ccs 0
cts 3
cp 0
rs 10
c 0
b 0
f 0
cc 1
eloc 1
nc 1
nop 0
crap 2
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_query;
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
     * @deprecated do not rely on this constant in the future, as it will be completely removed
148
     * @internal
149
     */
150
    const LAST_INSERT_ID_SQL = ';SELECT SCOPE_IDENTITY() AS LastInsertId;';
151
152
    /**
153
     * @param resource                                       $conn
154
     * @param string                                         $sql
155
     * @param \Doctrine\DBAL\Driver\SQLSrv\LastInsertId|null $lastInsertId
156
     */
157
    public function __construct($conn, $sql, LastInsertId $lastInsertId = null)
158
    {
159
        $this->conn = $conn;
160
        $this->sql = $sql;
161
        $this->lastInsertId = $lastInsertId;
162
    }
163
164
    /**
165
     * {@inheritdoc}
166
     */
167
    public function bindValue($param, $value, $type = ParameterType::STRING)
168
    {
169
        if (!is_numeric($param)) {
170
            throw new SQLSrvException(
171
                'sqlsrv does not support named parameters to queries, use question mark (?) placeholders instead.'
172
            );
173
        }
174
175
        $this->variables[$param] = $value;
176
        $this->types[$param] = $type;
177
    }
178
179
    /**
180
     * {@inheritdoc}
181
     */
182
    public function bindParam($column, &$variable, $type = ParameterType::STRING, $length = null)
183
    {
184
        if (!is_numeric($column)) {
185
            throw new SQLSrvException("sqlsrv does not support named parameters to queries, use question mark (?) placeholders instead.");
186
        }
187
188
        $this->variables[$column] =& $variable;
189
        $this->types[$column] = $type;
190
191
        // unset the statement resource if it exists as the new one will need to be bound to the new variable
192
        $this->stmt = null;
193
    }
194
195
    /**
196
     * {@inheritdoc}
197
     */
198
    public function closeCursor()
199
    {
200
        // not having the result means there's nothing to close
201
        if (!$this->result) {
202
            return true;
203
        }
204
205
        // emulate it by fetching and discarding rows, similarly to what PDO does in this case
206
        // @link http://php.net/manual/en/pdostatement.closecursor.php
207
        // @link https://github.com/php/php-src/blob/php-7.0.11/ext/pdo/pdo_stmt.c#L2075
208
        // deliberately do not consider multiple result sets, since doctrine/dbal doesn't support them
209
        while (sqlsrv_fetch($this->stmt));
210
211
        $this->result = false;
212
213
        return true;
214
    }
215
216
    /**
217
     * {@inheritdoc}
218
     */
219
    public function columnCount()
220
    {
221
        return sqlsrv_num_fields($this->stmt);
222
    }
223
224
    /**
225
     * {@inheritdoc}
226
     */
227
    public function errorCode()
228
    {
229
        $errors = sqlsrv_errors(SQLSRV_ERR_ERRORS);
230
        if ($errors) {
231
            return $errors[0]['code'];
232
        }
233
234
        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...
235
    }
236
237
    /**
238
     * {@inheritdoc}
239
     */
240
    public function errorInfo()
241
    {
242
        return sqlsrv_errors(SQLSRV_ERR_ERRORS);
243
    }
244
245
    /**
246
     * {@inheritdoc}
247
     */
248
    public function execute($params = null)
249
    {
250
        if ($params) {
251
            $hasZeroIndex = array_key_exists(0, $params);
252
            foreach ($params as $key => $val) {
253
                $key = ($hasZeroIndex && is_numeric($key)) ? $key + 1 : $key;
254
                $this->bindValue($key, $val);
255
            }
256
        }
257
258
        if ( ! $this->stmt) {
259
            $this->stmt = $this->prepare();
260
        }
261
262
        if (!sqlsrv_execute($this->stmt)) {
263
            throw SQLSrvException::fromSqlSrvErrors();
264
        }
265
266
        $this->trackLastInsertId();
267
268
        $this->result = true;
269
    }
270
271
    /**
272
     * Prepares SQL Server statement resource
273
     *
274
     * @return resource
275
     * @throws SQLSrvException
276
     */
277
    private function prepare()
278
    {
279
        $params = [];
280
281
        foreach ($this->variables as $column => &$variable) {
282
            if ($this->types[$column] === ParameterType::LARGE_OBJECT) {
283
                $params[$column - 1] = [
284
                    &$variable,
285
                    SQLSRV_PARAM_IN,
286
                    SQLSRV_PHPTYPE_STREAM(SQLSRV_ENC_BINARY),
287
                    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

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