Completed
Push — master ( 8575c2...a53269 )
by Marco
02:31 queued 01:15
created

SQLSrvStatement::fetch()   D

Complexity

Conditions 9
Paths 6

Size

Total Lines 32
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 90

Importance

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

266
                    SQLSRV_SQLTYPE_VARBINARY(/** @scrutinizer ignore-type */ 'max'),
Loading history...
267
                ];
268
            } else {
269
                $params[$column - 1] =& $variable;
270
            }
271
        }
272
273
        $stmt = sqlsrv_prepare($this->conn, $this->sql, $params);
274
275
        if (!$stmt) {
276
            throw SQLSrvException::fromSqlSrvErrors();
277
        }
278
279
        return $stmt;
280
    }
281
282
    /**
283
     * {@inheritdoc}
284
     */
285
    public function setFetchMode($fetchMode, $arg2 = null, $arg3 = null)
286
    {
287
        $this->defaultFetchMode          = $fetchMode;
288
        $this->defaultFetchClass         = $arg2 ?: $this->defaultFetchClass;
289
        $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...
290
291
        return true;
292
    }
293
294
    /**
295
     * {@inheritdoc}
296
     */
297
    public function getIterator()
298
    {
299
        return new StatementIterator($this);
300
    }
301
302
    /**
303
     * {@inheritdoc}
304
     *
305
     * @throws SQLSrvException
306
     */
307
    public function fetch($fetchMode = null, $cursorOrientation = PDO::FETCH_ORI_NEXT, $cursorOffset = 0)
308
    {
309
        // do not try fetching from the statement if it's not expected to contain result
310
        // in order to prevent exceptional situation
311
        if (!$this->result) {
312
            return false;
313
        }
314
315
        $args      = func_get_args();
316
        $fetchMode = $fetchMode ?: $this->defaultFetchMode;
317
318
        if ($fetchMode === PDO::FETCH_COLUMN) {
319
            return $this->fetchColumn();
320
        }
321
322
        if (isset(self::$fetchMap[$fetchMode])) {
323
            return sqlsrv_fetch_array($this->stmt, self::$fetchMap[$fetchMode]) ?: false;
324
        }
325
326
        if (in_array($fetchMode, [PDO::FETCH_OBJ, PDO::FETCH_CLASS], true)) {
327
            $className = $this->defaultFetchClass;
328
            $ctorArgs  = $this->defaultFetchClassCtorArgs;
329
330
            if (count($args) >= 2) {
331
                $className = $args[1];
332
                $ctorArgs  = $args[2] ?? [];
333
            }
334
335
            return sqlsrv_fetch_object($this->stmt, $className, $ctorArgs) ?: false;
0 ignored issues
show
Bug introduced by
It seems like $ctorArgs can also be of type string; however, parameter $ctor_params of sqlsrv_fetch_object() does only seem to accept null|array, 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

335
            return sqlsrv_fetch_object($this->stmt, $className, /** @scrutinizer ignore-type */ $ctorArgs) ?: false;
Loading history...
336
        }
337
338
        throw new SQLSrvException('Fetch mode is not supported!');
339
    }
340
341
    /**
342
     * {@inheritdoc}
343
     */
344
    public function fetchAll($fetchMode = null, $fetchArgument = null, $ctorArgs = null)
345
    {
346
        $rows = [];
347
348
        switch ($fetchMode) {
349
            case PDO::FETCH_CLASS:
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...
350
                while ($row = call_user_func_array([$this, 'fetch'], func_get_args())) {
351
                    $rows[] = $row;
352
                }
353
                break;
354
            case PDO::FETCH_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...
355
                while ($row = $this->fetchColumn()) {
356
                    $rows[] = $row;
357
                }
358
                break;
359
            default:
360
                while ($row = $this->fetch($fetchMode)) {
361
                    $rows[] = $row;
362
                }
363
        }
364
365
        return $rows;
366
    }
367
368
    /**
369
     * {@inheritdoc}
370
     */
371
    public function fetchColumn($columnIndex = 0)
372
    {
373
        $row = $this->fetch(PDO::FETCH_NUM);
374
375
        if (false === $row) {
376
            return false;
377
        }
378
379
        return $row[$columnIndex] ?? null;
380
    }
381
382
    /**
383
     * {@inheritdoc}
384
     */
385
    public function rowCount()
386
    {
387
        return sqlsrv_rows_affected($this->stmt);
388
    }
389
}
390