Failed Conditions
Pull Request — master (#4007)
by Sergei
11:47
created

SQLSrvStatement::fetchAllNumeric()   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
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 3
cp 0.6667
crap 1.037
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\DBAL\Driver\SQLSrv;
6
7
use Doctrine\DBAL\Driver\DriverException;
8
use Doctrine\DBAL\Driver\FetchUtils;
9
use Doctrine\DBAL\Driver\Statement;
10
use Doctrine\DBAL\ParameterType;
11
use Traversable;
12
use function assert;
13
use function is_int;
14
use function sqlsrv_execute;
15
use function sqlsrv_fetch;
16
use function sqlsrv_fetch_array;
17
use function sqlsrv_get_field;
18
use function sqlsrv_next_result;
19
use function sqlsrv_num_fields;
20
use function SQLSRV_PHPTYPE_STREAM;
21
use function SQLSRV_PHPTYPE_STRING;
22
use function sqlsrv_prepare;
23
use function sqlsrv_rows_affected;
24
use function SQLSRV_SQLTYPE_VARBINARY;
25
use function stripos;
26
use const SQLSRV_ENC_BINARY;
27
use const SQLSRV_FETCH_ASSOC;
28
use const SQLSRV_FETCH_NUMERIC;
29
use const SQLSRV_PARAM_IN;
30
31
/**
32
 * SQL Server Statement.
33
 */
34
final class SQLSrvStatement implements Statement
35
{
36
    /**
37
     * The SQLSRV Resource.
38
     *
39
     * @var resource
40
     */
41
    private $conn;
42
43
    /**
44
     * The SQL statement to execute.
45
     *
46
     * @var string
47
     */
48
    private $sql;
49
50
    /**
51
     * The SQLSRV statement resource.
52
     *
53
     * @var resource|null
54
     */
55
    private $stmt;
56
57
    /**
58
     * References to the variables bound as statement parameters.
59
     *
60
     * @var mixed
61
     */
62
    private $variables = [];
63
64
    /**
65
     * Bound parameter types.
66
     *
67
     * @var int[]
68
     */
69
    private $types = [];
70
71
    /**
72
     * The last insert ID.
73
     *
74
     * @var LastInsertId|null
75
     */
76
    private $lastInsertId;
77
78
    /**
79
     * Indicates whether the statement is in the state when fetching results is possible
80
     *
81
     * @var bool
82
     */
83
    private $result = false;
84
85
    /**
86
     * Append to any INSERT query to retrieve the last insert id.
87
     */
88
    private const LAST_INSERT_ID_SQL = ';SELECT SCOPE_IDENTITY() AS LastInsertId;';
89
90
    /**
91
     * @param resource $conn
92
     */
93 314
    public function __construct($conn, string $sql, ?LastInsertId $lastInsertId = null)
94
    {
95 314
        $this->conn = $conn;
96 314
        $this->sql  = $sql;
97
98 314
        if (stripos($sql, 'INSERT INTO ') !== 0) {
99 307
            return;
100
        }
101
102 87
        $this->sql         .= self::LAST_INSERT_ID_SQL;
103 87
        $this->lastInsertId = $lastInsertId;
104 87
    }
105
106
    /**
107
     * {@inheritdoc}
108
     */
109 107
    public function bindValue($param, $value, int $type = ParameterType::STRING) : void
110
    {
111 107
        assert(is_int($param));
112
113 107
        $this->variables[$param] = $value;
114 107
        $this->types[$param]     = $type;
115 107
    }
116
117
    /**
118
     * {@inheritdoc}
119
     */
120 23
    public function bindParam($param, &$variable, int $type = ParameterType::STRING, ?int $length = null) : void
121
    {
122 23
        assert(is_int($param));
123
124 23
        $this->variables[$param] =& $variable;
125 23
        $this->types[$param]     = $type;
126
127
        // unset the statement resource if it exists as the new one will need to be bound to the new variable
128 23
        $this->stmt = null;
129 23
    }
130
131 19
    public function closeCursor() : void
132
    {
133
        // not having the result means there's nothing to close
134 19
        if ($this->stmt === null || ! $this->result) {
135 3
            return;
136
        }
137
138
        // emulate it by fetching and discarding rows, similarly to what PDO does in this case
139
        // @link http://php.net/manual/en/pdostatement.closecursor.php
140
        // @link https://github.com/php/php-src/blob/php-7.0.11/ext/pdo/pdo_stmt.c#L2075
141
        // deliberately do not consider multiple result sets, since doctrine/dbal doesn't support them
142 16
        while (sqlsrv_fetch($this->stmt) !== false) {
143
        }
144
145 16
        $this->result = false;
146 16
    }
147
148 3
    public function columnCount() : int
149
    {
150 3
        if ($this->stmt === null) {
151
            return 0;
152
        }
153
154 3
        $count = sqlsrv_num_fields($this->stmt);
155
156 3
        if ($count !== false) {
157 3
            return $count;
158
        }
159
160
        return 0;
161
    }
162
163
    /**
164
     * {@inheritdoc}
165
     */
166 307
    public function execute(?array $params = null) : void
167
    {
168 307
        if ($params !== null) {
169 5
            foreach ($params as $key => $val) {
170 4
                if (is_int($key)) {
171 4
                    $this->bindValue($key + 1, $val);
172
                } else {
173
                    $this->bindValue($key, $val);
174
                }
175
            }
176
        }
177
178 307
        if ($this->stmt === null) {
179 307
            $this->stmt = $this->prepare();
180
        }
181
182 306
        if (! sqlsrv_execute($this->stmt)) {
183
            throw SQLSrvException::fromSqlSrvErrors();
184
        }
185
186 306
        if ($this->lastInsertId !== null) {
187 87
            sqlsrv_next_result($this->stmt);
188 87
            sqlsrv_fetch($this->stmt);
189 87
            $this->lastInsertId->setId(sqlsrv_get_field($this->stmt, 0));
190
        }
191
192 306
        $this->result = true;
193 306
    }
194
195
    /**
196
     * {@inheritdoc}
197
     */
198 204
    public function fetchNumeric()
199
    {
200 204
        return $this->fetch(SQLSRV_FETCH_NUMERIC);
201
    }
202
203
    /**
204
     * {@inheritdoc}
205
     */
206 168
    public function fetchAssociative()
207
    {
208 168
        return $this->fetch(SQLSRV_FETCH_ASSOC);
209
    }
210
211
    /**
212
     * {@inheritdoc}
213
     */
214 197
    public function fetchOne()
215
    {
216 197
        return FetchUtils::fetchOneFromNumeric($this);
217
    }
218
219
    /**
220
     * {@inheritdoc}
221
     */
222 2
    public function fetchAllNumeric() : array
223
    {
224 2
        return FetchUtils::fetchAllNumericByOne($this);
225
    }
226
227
    /**
228
     * {@inheritdoc}
229
     */
230 108
    public function fetchAllAssociative() : array
231
    {
232 108
        return FetchUtils::fetchAllAssociativeByOne($this);
233
    }
234
235
    /**
236
     * {@inheritdoc}
237
     */
238 9
    public function fetchColumn() : array
239
    {
240 9
        return FetchUtils::fetchColumnByOne($this);
241
    }
242
243
    /**
244
     * @return Traversable<int,array<int,mixed>>
245
     *
246
     * @throws DriverException
247
     */
248
    public function iterateNumeric() : Traversable
249
    {
250
        return FetchUtils::iterateNumericByOne($this);
251
    }
252
253
    /**
254
     * @return Traversable<int,array<string,mixed>>
255
     *
256
     * @throws DriverException
257
     */
258 1
    public function iterateAssociative() : Traversable
259
    {
260 1
        return FetchUtils::iterateAssociativeByOne($this);
261
    }
262
263
    /**
264
     * @return Traversable<int,mixed>
265
     *
266
     * @throws DriverException
267
     */
268
    public function iterateColumn() : Traversable
269
    {
270
        return FetchUtils::iterateColumnByOne($this);
271
    }
272
273 86
    public function rowCount() : int
274
    {
275 86
        if ($this->stmt === null) {
276
            return 0;
277
        }
278
279 86
        $count = sqlsrv_rows_affected($this->stmt);
280
281 86
        if ($count !== false) {
282 86
            return $count;
283
        }
284
285
        return 0;
286
    }
287
288
    /**
289
     * Prepares SQL Server statement resource
290
     *
291
     * @return resource
292
     *
293
     * @throws SQLSrvException
294
     */
295 307
    private function prepare()
296
    {
297 307
        $params = [];
298
299 307
        foreach ($this->variables as $column => &$variable) {
300 128
            switch ($this->types[$column]) {
301
                case ParameterType::LARGE_OBJECT:
302 7
                    $params[$column - 1] = [
303 7
                        &$variable,
304
                        SQLSRV_PARAM_IN,
305 7
                        SQLSRV_PHPTYPE_STREAM(SQLSRV_ENC_BINARY),
306 7
                        SQLSRV_SQLTYPE_VARBINARY('max'),
307
                    ];
308 7
                    break;
309
310
                case ParameterType::BINARY:
311 1
                    $params[$column - 1] = [
312 1
                        &$variable,
313
                        SQLSRV_PARAM_IN,
314 1
                        SQLSRV_PHPTYPE_STRING(SQLSRV_ENC_BINARY),
315
                    ];
316 1
                    break;
317
318
                default:
319 125
                    $params[$column - 1] =& $variable;
320 125
                    break;
321
            }
322
        }
323
324 307
        $stmt = sqlsrv_prepare($this->conn, $this->sql, $params);
325
326 307
        if ($stmt === false) {
327 1
            throw SQLSrvException::fromSqlSrvErrors();
328
        }
329
330 306
        return $stmt;
331
    }
332
333
    /**
334
     * @return mixed|false
335
     */
336 300
    private function fetch(int $fetchType)
337
    {
338
        // do not try fetching from the statement if it's not expected to contain result
339
        // in order to prevent exceptional situation
340 300
        if ($this->stmt === null || ! $this->result) {
341 9
            return false;
342
        }
343
344 291
        return sqlsrv_fetch_array($this->stmt, $fetchType) ?? false;
345
    }
346
}
347