Test Failed
Pull Request — master (#2765)
by Marco
04:16
created

SQLSrvStatement::bindValue()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 11
ccs 0
cts 10
cp 0
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 6
nc 2
nop 3
crap 6
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 PDO;
23
use IteratorAggregate;
24
use Doctrine\DBAL\Driver\Statement;
25
26
/**
27
 * SQL Server Statement.
28
 *
29
 * @since 2.3
30
 * @author Benjamin Eberlei <[email protected]>
31
 */
32
class SQLSrvStatement implements IteratorAggregate, Statement
33
{
34
    /**
35
     * The SQLSRV Resource.
36
     *
37
     * @var resource
38
     */
39
    private $conn;
40
41
    /**
42
     * The SQL statement to execute.
43
     *
44
     * @var string
45
     */
46
    private $sql;
47
48
    /**
49
     * The SQLSRV statement resource.
50
     *
51
     * @var resource
52
     */
53
    private $stmt;
54
55
    /**
56
     * References to the variables bound as statement parameters.
57
     *
58
     * @var array
59
     */
60
    private $variables = [];
61
62
    /**
63
     * Bound parameter types.
64
     *
65
     * @var array
66
     */
67
    private $types = [];
68
69
    /**
70
     * Translations.
71
     *
72
     * @var array
73
     */
74
    private static $fetchMap = [
75
        PDO::FETCH_BOTH => SQLSRV_FETCH_BOTH,
76
        PDO::FETCH_ASSOC => SQLSRV_FETCH_ASSOC,
77
        PDO::FETCH_NUM => SQLSRV_FETCH_NUMERIC,
78
    ];
79
80
    /**
81
     * The name of the default class to instantiate when fetch mode is \PDO::FETCH_CLASS.
82
     *
83
     * @var string
84
     */
85
    private $defaultFetchClass = '\stdClass';
86
87
    /**
88
     * The constructor arguments for the default class to instantiate when fetch mode is \PDO::FETCH_CLASS.
89
     *
90
     * @var string
91
     */
92
    private $defaultFetchClassCtorArgs = [];
93
94
    /**
95
     * The fetch style.
96
     *
97
     * @param integer
98
     */
99
    private $defaultFetchMode = PDO::FETCH_BOTH;
100
101
    /**
102
     * The last insert ID.
103
     *
104
     * @var \Doctrine\DBAL\Driver\SQLSrv\LastInsertId|null
105
     */
106
    private $lastInsertId;
107
108
    /**
109
     * Indicates whether the statement is in the state when fetching results is possible
110
     *
111
     * @var bool
112
     */
113
    private $result = false;
114
115
    /**
116
     * Append to any INSERT query to retrieve the last insert id.
117
     *
118
     * @var string
119
     *
120
     * @deprecated do not rely on this constant in the future, as it will be completely removed
121
     * @internal
122
     */
123
    const LAST_INSERT_ID_SQL = ';SELECT SCOPE_IDENTITY() AS LastInsertId;';
124
125
    /**
126
     * @param resource                                       $conn
127
     * @param string                                         $sql
128
     * @param \Doctrine\DBAL\Driver\SQLSrv\LastInsertId|null $lastInsertId
129
     */
130
    public function __construct($conn, $sql, LastInsertId $lastInsertId = null)
131
    {
132
        $this->conn = $conn;
133
        $this->sql = $sql;
134
        $this->lastInsertId = $lastInsertId;
135
    }
136
137
    /**
138
     * {@inheritdoc}
139
     */
140
    public function bindValue($param, $value, $type = null)
141
    {
142
        if (!is_numeric($param)) {
143
            throw new SQLSrvException(
144
                'sqlsrv does not support named parameters to queries, use question mark (?) placeholders instead.'
145
            );
146
        }
147
148
        $this->variables[$param] = $value;
149
        $this->types[$param] = $type;
150
    }
151
152
    /**
153
     * {@inheritdoc}
154
     */
155
    public function bindParam($column, &$variable, $type = null, $length = null)
156
    {
157
        if (!is_numeric($column)) {
158
            throw new SQLSrvException("sqlsrv does not support named parameters to queries, use question mark (?) placeholders instead.");
159
        }
160
161
        $this->variables[$column] =& $variable;
162
        $this->types[$column] = $type;
163
164
        // unset the statement resource if it exists as the new one will need to be bound to the new variable
165
        $this->stmt = null;
166
    }
167
168
    /**
169
     * {@inheritdoc}
170
     */
171
    public function closeCursor()
172
    {
173
        // not having the result means there's nothing to close
174
        if (!$this->result) {
175
            return true;
176
        }
177
178
        // emulate it by fetching and discarding rows, similarly to what PDO does in this case
179
        // @link http://php.net/manual/en/pdostatement.closecursor.php
180
        // @link https://github.com/php/php-src/blob/php-7.0.11/ext/pdo/pdo_stmt.c#L2075
181
        // deliberately do not consider multiple result sets, since doctrine/dbal doesn't support them
182
        while (sqlsrv_fetch($this->stmt));
183
184
        $this->result = false;
185
186
        return true;
187
    }
188
189
    /**
190
     * {@inheritdoc}
191
     */
192
    public function columnCount()
193
    {
194
        return sqlsrv_num_fields($this->stmt);
195
    }
196
197
    /**
198
     * {@inheritdoc}
199
     */
200 View Code Duplication
    public function errorCode()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
201
    {
202
        $errors = sqlsrv_errors(SQLSRV_ERR_ERRORS);
203
        if ($errors) {
204
            return $errors[0]['code'];
205
        }
206
207
        return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type declared by the interface Doctrine\DBAL\Driver\Statement::errorCode of type string.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
208
    }
209
210
    /**
211
     * {@inheritdoc}
212
     */
213
    public function errorInfo()
214
    {
215
        return sqlsrv_errors(SQLSRV_ERR_ERRORS);
216
    }
217
218
    /**
219
     * {@inheritdoc}
220
     */
221
    public function execute($params = null)
222
    {
223 View Code Duplication
        if ($params) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
224
            $hasZeroIndex = array_key_exists(0, $params);
225
            foreach ($params as $key => $val) {
226
                $key = ($hasZeroIndex && is_numeric($key)) ? $key + 1 : $key;
227
                $this->bindValue($key, $val);
228
            }
229
        }
230
231
        if ( ! $this->stmt) {
232
            $this->stmt = $this->prepare();
233
        }
234
235
        if (!sqlsrv_execute($this->stmt)) {
236
            throw SQLSrvException::fromSqlSrvErrors();
237
        }
238
239
        $this->trackLastInsertId();
240
241
        $this->result = true;
242
    }
243
244
    /**
245
     * Prepares SQL Server statement resource
246
     *
247
     * @return resource
248
     * @throws SQLSrvException
249
     */
250
    private function prepare()
251
    {
252
        $params = [];
253
254
        foreach ($this->variables as $column => &$variable) {
255
            if (PDO::PARAM_LOB === $this->types[$column]) {
256
                $params[$column - 1] = [
257
                    &$variable,
258
                    SQLSRV_PARAM_IN,
259
                    SQLSRV_PHPTYPE_STREAM(SQLSRV_ENC_BINARY),
260
                    SQLSRV_SQLTYPE_VARBINARY('max'),
261
                ];
262
            } else {
263
                $params[$column - 1] =& $variable;
264
            }
265
        }
266
267
        $stmt = sqlsrv_prepare($this->conn, $this->sql, $params);
268
269
        if (!$stmt) {
270
            throw SQLSrvException::fromSqlSrvErrors();
271
        }
272
273
        return $stmt;
274
    }
275
276
    /**
277
     * {@inheritdoc}
278
     */
279 View Code Duplication
    public function setFetchMode($fetchMode, $arg2 = null, $arg3 = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
280
    {
281
        $this->defaultFetchMode          = $fetchMode;
282
        $this->defaultFetchClass         = $arg2 ?: $this->defaultFetchClass;
283
        $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...
284
285
        return true;
286
    }
287
288
    /**
289
     * {@inheritdoc}
290
     */
291
    public function getIterator()
292
    {
293
        $data = $this->fetchAll();
294
295
        return new \ArrayIterator($data);
296
    }
297
298
    /**
299
     * {@inheritdoc}
300
     *
301
     * @throws SQLSrvException
302
     */
303
    public function fetch($fetchMode = null, $cursorOrientation = PDO::FETCH_ORI_NEXT, $cursorOffset = 0)
304
    {
305
        // do not try fetching from the statement if it's not expected to contain result
306
        // in order to prevent exceptional situation
307
        if (!$this->result) {
308
            return false;
309
        }
310
311
        $args      = func_get_args();
312
        $fetchMode = $fetchMode ?: $this->defaultFetchMode;
313
314
        if (isset(self::$fetchMap[$fetchMode])) {
315
            return sqlsrv_fetch_array($this->stmt, self::$fetchMap[$fetchMode]) ?: false;
316
        }
317
318
        if (in_array($fetchMode, [PDO::FETCH_OBJ, PDO::FETCH_CLASS], true)) {
319
            $className = $this->defaultFetchClass;
320
            $ctorArgs  = $this->defaultFetchClassCtorArgs;
321
322
            if (count($args) >= 2) {
323
                $className = $args[1];
324
                $ctorArgs  = isset($args[2]) ? $args[2] : [];
325
            }
326
327
            return sqlsrv_fetch_object($this->stmt, $className, $ctorArgs) ?: false;
328
        }
329
330
        throw new SQLSrvException('Fetch mode is not supported!');
331
    }
332
333
    /**
334
     * {@inheritdoc}
335
     */
336 View Code Duplication
    public function fetchAll($fetchMode = null, $fetchArgument = null, $ctorArgs = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
337
    {
338
        $rows = [];
339
340
        switch ($fetchMode) {
341
            case PDO::FETCH_CLASS:
342
                while ($row = call_user_func_array([$this, 'fetch'], func_get_args())) {
343
                    $rows[] = $row;
344
                }
345
                break;
346
            case PDO::FETCH_COLUMN:
347
                while ($row = $this->fetchColumn()) {
348
                    $rows[] = $row;
349
                }
350
                break;
351
            default:
352
                while ($row = $this->fetch($fetchMode)) {
353
                    $rows[] = $row;
354
                }
355
        }
356
357
        return $rows;
358
    }
359
360
    /**
361
     * {@inheritdoc}
362
     */
363 View Code Duplication
    public function fetchColumn($columnIndex = 0)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
364
    {
365
        $row = $this->fetch(PDO::FETCH_NUM);
366
367
        if (false === $row) {
368
            return false;
369
        }
370
371
        return isset($row[$columnIndex]) ? $row[$columnIndex] : null;
372
    }
373
374
    /**
375
     * {@inheritdoc}
376
     */
377
    public function rowCount()
378
    {
379
        return sqlsrv_rows_affected($this->stmt);
380
    }
381
382
    private function trackLastInsertId() : void
383
    {
384
        if (! $this->lastInsertId) {
385
            return;
386
        }
387
388
        $statement = sqlsrv_query($this->conn, 'SELECT @@IDENTITY');
389
390 View Code Duplication
        if (false !== $statement) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
391
            sqlsrv_fetch($statement);
392
393
            $lastInsertId = sqlsrv_get_field($statement, 0) ?: '0';
394
395
            $this->lastInsertId->setId($lastInsertId);
396
        }
397
    }
398
}
399