Passed
Push — tests-better-coverage ( 54ea4a...911183 )
by Michael
24:49
created

SQLSrvStatement::getIterator()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1.037

Importance

Changes 0
Metric Value
dl 0
loc 3
ccs 2
cts 3
cp 0.6667
rs 10
c 0
b 0
f 0
cc 1
eloc 1
nc 1
nop 0
crap 1.037
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
    const LAST_INSERT_ID_SQL = ';SELECT SCOPE_IDENTITY() AS LastInsertId;';
147
148
    /**
149
     * @param resource                                       $conn
150
     * @param string                                         $sql
151
     * @param \Doctrine\DBAL\Driver\SQLSrv\LastInsertId|null $lastInsertId
152
     */
153 229
    public function __construct($conn, $sql, LastInsertId $lastInsertId = null)
154
    {
155 229
        $this->conn = $conn;
156 229
        $this->sql = $sql;
157
158 229
        if (stripos($sql, 'INSERT INTO ') === 0) {
159 77
            $this->sql .= self::LAST_INSERT_ID_SQL;
160 77
            $this->lastInsertId = $lastInsertId;
161
        }
162 229
    }
163
164
    /**
165
     * {@inheritdoc}
166
     */
167 102
    public function bindValue($param, $value, $type = ParameterType::STRING)
168
    {
169 102
        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 102
        $this->variables[$param] = $value;
176 102
        $this->types[$param] = $type;
177 102
    }
178
179
    /**
180
     * {@inheritdoc}
181
     */
182 7
    public function bindParam($column, &$variable, $type = ParameterType::STRING, $length = null)
183
    {
184 7
        if (!is_numeric($column)) {
185
            throw new SQLSrvException("sqlsrv does not support named parameters to queries, use question mark (?) placeholders instead.");
186
        }
187
188 7
        $this->variables[$column] =& $variable;
189 7
        $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 7
        $this->stmt = null;
193 7
    }
194
195
    /**
196
     * {@inheritdoc}
197
     */
198 19
    public function closeCursor()
199
    {
200
        // not having the result means there's nothing to close
201 19
        if (!$this->result) {
202 4
            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 15
        while (sqlsrv_fetch($this->stmt));
210
211 15
        $this->result = false;
212
213 15
        return true;
214
    }
215
216
    /**
217
     * {@inheritdoc}
218
     */
219 4
    public function columnCount()
220
    {
221 4
        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 221
    public function execute($params = null)
249
    {
250 221
        if ($params) {
251 74
            $hasZeroIndex = array_key_exists(0, $params);
252 74
            foreach ($params as $key => $val) {
253 74
                $key = ($hasZeroIndex && is_numeric($key)) ? $key + 1 : $key;
254 74
                $this->bindValue($key, $val);
255
            }
256
        }
257
258 221
        if ( ! $this->stmt) {
259 221
            $this->stmt = $this->prepare();
260
        }
261
262 220
        if (!sqlsrv_execute($this->stmt)) {
263
            throw SQLSrvException::fromSqlSrvErrors();
264
        }
265
266 220
        if ($this->lastInsertId) {
267 77
            sqlsrv_next_result($this->stmt);
268 77
            sqlsrv_fetch($this->stmt);
269 77
            $this->lastInsertId->setId(sqlsrv_get_field($this->stmt, 0));
270
        }
271
272 220
        $this->result = true;
273 220
    }
274
275
    /**
276
     * Prepares SQL Server statement resource
277
     *
278
     * @return resource
279
     * @throws SQLSrvException
280
     */
281 221
    private function prepare()
282
    {
283 221
        $params = [];
284
285 221
        foreach ($this->variables as $column => &$variable) {
286 107
            if ($this->types[$column] === ParameterType::LARGE_OBJECT) {
287 4
                $params[$column - 1] = [
288 4
                    &$variable,
289
                    SQLSRV_PARAM_IN,
290 4
                    SQLSRV_PHPTYPE_STREAM(SQLSRV_ENC_BINARY),
291 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

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