Failed Conditions
Pull Request — master (#4007)
by Sergei
62:58
created

SQLSrvStatement::iterateColumn()   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
    public function __construct($conn, string $sql, ?LastInsertId $lastInsertId = null)
94
    {
95
        $this->conn = $conn;
96
        $this->sql  = $sql;
97
98
        if (stripos($sql, 'INSERT INTO ') !== 0) {
99
            return;
100
        }
101
102
        $this->sql         .= self::LAST_INSERT_ID_SQL;
103
        $this->lastInsertId = $lastInsertId;
104
    }
105
106
    /**
107
     * {@inheritdoc}
108
     */
109
    public function bindValue($param, $value, int $type = ParameterType::STRING) : void
110
    {
111
        assert(is_int($param));
112 318
113
        $this->variables[$param] = $value;
114 318
        $this->types[$param]     = $type;
115 318
    }
116
117 318
    /**
118 311
     * {@inheritdoc}
119
     */
120
    public function bindParam($param, &$variable, int $type = ParameterType::STRING, ?int $length = null) : void
121 87
    {
122 87
        assert(is_int($param));
123 87
124
        $this->variables[$param] =& $variable;
125
        $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 109
        $this->stmt = null;
129
    }
130 109
131
    public function closeCursor() : void
132 109
    {
133 109
        // not having the result means there's nothing to close
134 109
        if ($this->stmt === null || ! $this->result) {
135
            return;
136
        }
137
138
        // emulate it by fetching and discarding rows, similarly to what PDO does in this case
139 24
        // @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 24
        // deliberately do not consider multiple result sets, since doctrine/dbal doesn't support them
142
        while (sqlsrv_fetch($this->stmt) !== false) {
143 24
        }
144 24
145
        $this->result = false;
146
    }
147 24
148 24
    public function columnCount() : int
149
    {
150 19
        if ($this->stmt === null) {
151
            return 0;
152
        }
153 19
154 3
        $count = sqlsrv_num_fields($this->stmt);
155
156
        if ($count !== false) {
157
            return $count;
158
        }
159
160
        return 0;
161 16
    }
162
163
    /**
164 16
     * {@inheritdoc}
165 16
     */
166
    public function execute(?array $params = null) : void
167 4
    {
168
        if ($params !== null) {
169 4
            foreach ($params as $key => $val) {
170
                if (is_int($key)) {
171
                    $this->bindValue($key + 1, $val);
172
                } else {
173 4
                    $this->bindValue($key, $val);
174
                }
175 4
            }
176 4
        }
177
178
        if ($this->stmt === null) {
179
            $this->stmt = $this->prepare();
180
        }
181
182
        if (! sqlsrv_execute($this->stmt)) {
183
            throw SQLSrvException::fromSqlSrvErrors();
184
        }
185 311
186
        if ($this->lastInsertId !== null) {
187 311
            sqlsrv_next_result($this->stmt);
188 5
            sqlsrv_fetch($this->stmt);
189 4
            $this->lastInsertId->setId(sqlsrv_get_field($this->stmt, 0));
190 4
        }
191
192
        $this->result = true;
193
    }
194
195
    /**
196
     * {@inheritdoc}
197 311
     */
198 311
    public function fetchNumeric()
199
    {
200
        return $this->fetch(SQLSRV_FETCH_NUMERIC);
201 310
    }
202
203
    /**
204
     * {@inheritdoc}
205 310
     */
206 87
    public function fetchAssociative()
207 87
    {
208 87
        return $this->fetch(SQLSRV_FETCH_ASSOC);
209
    }
210
211 310
    /**
212 310
     * {@inheritdoc}
213
     */
214 311
    public function fetchOne()
215
    {
216 311
        return FetchUtils::fetchOne($this);
217 311
    }
218
219
    /**
220
     * {@inheritdoc}
221
     */
222 1
    public function fetchAllNumeric() : array
223
    {
224 1
        return FetchUtils::fetchAllNumeric($this);
225
    }
226
227
    /**
228
     * {@inheritdoc}
229
     */
230
    public function fetchAllAssociative() : array
231
    {
232 304
        return FetchUtils::fetchAllAssociative($this);
233
    }
234
235
    /**
236 304
     * {@inheritdoc}
237 9
     */
238
    public function fetchColumn() : array
239
    {
240 295
        return FetchUtils::fetchColumn($this);
241
    }
242 295
243 1
    /**
244
     * @return Traversable<int,array<int,mixed>>
245
     *
246 295
     * @throws DriverException
247 295
     */
248
    public function iterateNumeric() : Traversable
249
    {
250
        return FetchUtils::iterateNumeric($this);
251
    }
252
253
    /**
254
     * @return Traversable<int,array<string,mixed>>
255
     *
256 120
     * @throws DriverException
257
     */
258 120
    public function iterateAssociative() : Traversable
259
    {
260 120
        return FetchUtils::iterateAssociative($this);
261
    }
262 10
263 10
    /**
264
     * @return Traversable<int,mixed>
265
     *
266 10
     * @throws DriverException
267
     */
268
    public function iterateColumn() : Traversable
269 110
    {
270 102
        return FetchUtils::iterateColumn($this);
271
    }
272
273
    public function rowCount() : int
274 120
    {
275
        if ($this->stmt === null) {
276
            return 0;
277
        }
278
279
        $count = sqlsrv_rows_affected($this->stmt);
280 198
281
        if ($count !== false) {
282 198
            return $count;
283
        }
284 198
285 16
        return 0;
286
    }
287
288 192
    /**
289
     * Prepares SQL Server statement resource
290
     *
291 86
     * @return resource
292
     *
293 86
     * @throws SQLSrvException
294
     */
295
    private function prepare()
296
    {
297 86
        $params = [];
298
299 86
        foreach ($this->variables as $column => &$variable) {
300 86
            switch ($this->types[$column]) {
301
                case ParameterType::LARGE_OBJECT:
302
                    $params[$column - 1] = [
303
                        &$variable,
304
                        SQLSRV_PARAM_IN,
305
                        SQLSRV_PHPTYPE_STREAM(SQLSRV_ENC_BINARY),
306
                        SQLSRV_SQLTYPE_VARBINARY('max'),
307
                    ];
308
                    break;
309
310
                case ParameterType::BINARY:
311
                    $params[$column - 1] = [
312
                        &$variable,
313 311
                        SQLSRV_PARAM_IN,
314
                        SQLSRV_PHPTYPE_STRING(SQLSRV_ENC_BINARY),
315 311
                    ];
316
                    break;
317 311
318 131
                default:
319
                    $params[$column - 1] =& $variable;
320 7
                    break;
321 7
            }
322
        }
323 7
324 7
        $stmt = sqlsrv_prepare($this->conn, $this->sql, $params);
325
326 7
        if ($stmt === false) {
327
            throw SQLSrvException::fromSqlSrvErrors();
328
        }
329 1
330 1
        return $stmt;
331
    }
332 1
333
    /**
334 1
     * @return mixed|false
335
     */
336
    private function fetch(int $fetchType)
337 128
    {
338 128
        // do not try fetching from the statement if it's not expected to contain the result
339
        // in order to prevent exceptional situation
340
        if ($this->stmt === null || ! $this->result) {
341
            return false;
342 311
        }
343
344 311
        return sqlsrv_fetch_array($this->stmt, $fetchType) ?? false;
345 1
    }
346
}
347