SQLSrvStatement.php 9.22 KB
Newer Older
1 2 3 4
<?php

namespace Doctrine\DBAL\Driver\SQLSrv;

5
use Doctrine\DBAL\Driver\Statement;
6
use Doctrine\DBAL\Driver\StatementIterator;
7 8
use Doctrine\DBAL\FetchMode;
use Doctrine\DBAL\ParameterType;
9
use IteratorAggregate;
10
use function array_key_exists;
Sergei Morozov's avatar
Sergei Morozov committed
11
use function is_int;
12 13 14 15 16 17 18 19 20
use function is_numeric;
use function sqlsrv_errors;
use function sqlsrv_execute;
use function sqlsrv_fetch;
use function sqlsrv_fetch_array;
use function sqlsrv_get_field;
use function sqlsrv_next_result;
use function sqlsrv_num_fields;
use function SQLSRV_PHPTYPE_STREAM;
21
use function SQLSRV_PHPTYPE_STRING;
22 23 24 25
use function sqlsrv_prepare;
use function sqlsrv_rows_affected;
use function SQLSRV_SQLTYPE_VARBINARY;
use function stripos;
Grégoire Paris's avatar
Grégoire Paris committed
26 27 28 29 30 31
use const SQLSRV_ENC_BINARY;
use const SQLSRV_ERR_ERRORS;
use const SQLSRV_FETCH_ASSOC;
use const SQLSRV_FETCH_BOTH;
use const SQLSRV_FETCH_NUMERIC;
use const SQLSRV_PARAM_IN;
32

33
/**
Benjamin Morel's avatar
Benjamin Morel committed
34
 * SQL Server Statement.
35
 */
36 37 38
class SQLSrvStatement implements IteratorAggregate, Statement
{
    /**
Benjamin Morel's avatar
Benjamin Morel committed
39
     * The SQLSRV Resource.
40 41 42 43 44 45
     *
     * @var resource
     */
    private $conn;

    /**
Benjamin Morel's avatar
Benjamin Morel committed
46
     * The SQL statement to execute.
47 48 49 50 51 52
     *
     * @var string
     */
    private $sql;

    /**
Benjamin Morel's avatar
Benjamin Morel committed
53
     * The SQLSRV statement resource.
54
     *
55
     * @var resource|null
56 57 58 59
     */
    private $stmt;

    /**
60
     * References to the variables bound as statement parameters.
61
     *
62
     * @var mixed
63
     */
64
    private $variables = [];
65 66 67 68

    /**
     * Bound parameter types.
     *
69
     * @var int[]
70
     */
71
    private $types = [];
72 73

    /**
Benjamin Morel's avatar
Benjamin Morel committed
74
     * Translations.
75
     *
76
     * @var int[]
77
     */
78
    private static $fetchMap = [
Sergei Morozov's avatar
Sergei Morozov committed
79
        FetchMode::MIXED       => SQLSRV_FETCH_BOTH,
80
        FetchMode::ASSOCIATIVE => SQLSRV_FETCH_ASSOC,
Sergei Morozov's avatar
Sergei Morozov committed
81
        FetchMode::NUMERIC     => SQLSRV_FETCH_NUMERIC,
82
    ];
83 84

    /**
Benjamin Morel's avatar
Benjamin Morel committed
85
     * The fetch style.
86
     *
Sergei Morozov's avatar
Sergei Morozov committed
87
     * @var int
88
     */
89
    private $defaultFetchMode = FetchMode::MIXED;
90

91
    /**
Benjamin Morel's avatar
Benjamin Morel committed
92 93
     * The last insert ID.
     *
94
     * @var LastInsertId|null
95 96 97
     */
    private $lastInsertId;

98 99 100 101 102 103 104
    /**
     * Indicates whether the statement is in the state when fetching results is possible
     *
     * @var bool
     */
    private $result = false;

105 106
    /**
     * Append to any INSERT query to retrieve the last insert id.
107 108
     *
     * @deprecated This constant has been deprecated and will be made private in 3.0
109
     */
110
    public const LAST_INSERT_ID_SQL = ';SELECT SCOPE_IDENTITY() AS LastInsertId;';
111

Benjamin Morel's avatar
Benjamin Morel committed
112
    /**
113 114
     * @param resource $conn
     * @param string   $sql
Benjamin Morel's avatar
Benjamin Morel committed
115
     */
116
    public function __construct($conn, $sql, ?LastInsertId $lastInsertId = null)
117 118
    {
        $this->conn = $conn;
119
        $this->sql  = $sql;
120

121 122
        if (stripos($sql, 'INSERT INTO ') !== 0) {
            return;
123
        }
124 125 126

        $this->sql         .= self::LAST_INSERT_ID_SQL;
        $this->lastInsertId = $lastInsertId;
127 128
    }

Benjamin Morel's avatar
Benjamin Morel committed
129 130 131
    /**
     * {@inheritdoc}
     */
132
    public function bindValue($param, $value, $type = ParameterType::STRING)
133
    {
134
        if (! is_numeric($param)) {
135 136 137 138 139 140
            throw new SQLSrvException(
                'sqlsrv does not support named parameters to queries, use question mark (?) placeholders instead.'
            );
        }

        $this->variables[$param] = $value;
141
        $this->types[$param]     = $type;
142 143

        return true;
144 145 146 147 148
    }

    /**
     * {@inheritdoc}
     */
149
    public function bindParam($column, &$variable, $type = ParameterType::STRING, $length = null)
150
    {
151 152
        if (! is_numeric($column)) {
            throw new SQLSrvException('sqlsrv does not support named parameters to queries, use question mark (?) placeholders instead.');
153 154
        }

155
        $this->variables[$column] =& $variable;
156
        $this->types[$column]     = $type;
157 158 159

        // unset the statement resource if it exists as the new one will need to be bound to the new variable
        $this->stmt = null;
160 161

        return true;
162 163
    }

Benjamin Morel's avatar
Benjamin Morel committed
164 165 166
    /**
     * {@inheritdoc}
     */
167 168
    public function closeCursor()
    {
169
        // not having the result means there's nothing to close
Sergei Morozov's avatar
Sergei Morozov committed
170
        if ($this->stmt === null || ! $this->result) {
171 172 173
            return true;
        }

174 175 176 177
        // emulate it by fetching and discarding rows, similarly to what PDO does in this case
        // @link http://php.net/manual/en/pdostatement.closecursor.php
        // @link https://github.com/php/php-src/blob/php-7.0.11/ext/pdo/pdo_stmt.c#L2075
        // deliberately do not consider multiple result sets, since doctrine/dbal doesn't support them
178 179
        while (sqlsrv_fetch($this->stmt)) {
        }
180

181 182
        $this->result = false;

183
        return true;
184 185
    }

Benjamin Morel's avatar
Benjamin Morel committed
186 187 188
    /**
     * {@inheritdoc}
     */
189 190
    public function columnCount()
    {
Sergei Morozov's avatar
Sergei Morozov committed
191 192 193 194
        if ($this->stmt === null) {
            return 0;
        }

195 196 197 198 199 200 201
        $count = sqlsrv_num_fields($this->stmt);

        if ($count !== false) {
            return $count;
        }

        return 0;
202 203 204
    }

    /**
Benjamin Morel's avatar
Benjamin Morel committed
205
     * {@inheritdoc}
206 207 208 209
     */
    public function errorCode()
    {
        $errors = sqlsrv_errors(SQLSRV_ERR_ERRORS);
210
        if ($errors !== null) {
211 212
            return $errors[0]['code'];
        }
Benjamin Morel's avatar
Benjamin Morel committed
213

214 215 216 217
        return false;
    }

    /**
Benjamin Morel's avatar
Benjamin Morel committed
218
     * {@inheritdoc}
219 220 221
     */
    public function errorInfo()
    {
Sergei Morozov's avatar
Sergei Morozov committed
222
        return (array) sqlsrv_errors(SQLSRV_ERR_ERRORS);
223 224
    }

Benjamin Morel's avatar
Benjamin Morel committed
225 226 227
    /**
     * {@inheritdoc}
     */
228 229
    public function execute($params = null)
    {
230
        if ($params !== null) {
231 232
            $hasZeroIndex = array_key_exists(0, $params);
            foreach ($params as $key => $val) {
Sergei Morozov's avatar
Sergei Morozov committed
233 234 235 236 237
                if ($hasZeroIndex && is_int($key)) {
                    $this->bindValue($key + 1, $val);
                } else {
                    $this->bindValue($key, $val);
                }
238 239 240
            }
        }

241
        if ($this->stmt === null) {
242
            $this->stmt = $this->prepare();
243 244
        }

245
        if (! sqlsrv_execute($this->stmt)) {
246 247
            throw SQLSrvException::fromSqlSrvErrors();
        }
248

249
        if ($this->lastInsertId !== null) {
250 251
            sqlsrv_next_result($this->stmt);
            sqlsrv_fetch($this->stmt);
Steve Müller's avatar
Steve Müller committed
252
            $this->lastInsertId->setId(sqlsrv_get_field($this->stmt, 0));
253
        }
254 255

        $this->result = true;
256 257

        return true;
258 259
    }

260 261 262 263
    /**
     * Prepares SQL Server statement resource
     *
     * @return resource
264
     *
265 266 267 268
     * @throws SQLSrvException
     */
    private function prepare()
    {
269
        $params = [];
270 271

        foreach ($this->variables as $column => &$variable) {
272 273 274 275 276
            switch ($this->types[$column]) {
                case ParameterType::LARGE_OBJECT:
                    $params[$column - 1] = [
                        &$variable,
                        SQLSRV_PARAM_IN,
277 278
                        SQLSRV_PHPTYPE_STREAM(SQLSRV_ENC_BINARY),
                        SQLSRV_SQLTYPE_VARBINARY('max'),
279 280 281 282 283 284 285
                    ];
                    break;

                case ParameterType::BINARY:
                    $params[$column - 1] = [
                        &$variable,
                        SQLSRV_PARAM_IN,
286
                        SQLSRV_PHPTYPE_STRING(SQLSRV_ENC_BINARY),
287 288 289 290 291 292
                    ];
                    break;

                default:
                    $params[$column - 1] =& $variable;
                    break;
293 294 295 296 297
            }
        }

        $stmt = sqlsrv_prepare($this->conn, $this->sql, $params);

298
        if ($stmt === false) {
299 300 301 302 303 304
            throw SQLSrvException::fromSqlSrvErrors();
        }

        return $stmt;
    }

Benjamin Morel's avatar
Benjamin Morel committed
305 306 307
    /**
     * {@inheritdoc}
     */
308
    public function setFetchMode($fetchMode)
309
    {
310 311
        $this->defaultFetchMode = $fetchMode;

Benjamin Morel's avatar
Benjamin Morel committed
312
        return true;
313 314 315 316 317 318 319
    }

    /**
     * {@inheritdoc}
     */
    public function getIterator()
    {
320
        return new StatementIterator($this);
321 322 323 324
    }

    /**
     * {@inheritdoc}
325 326
     *
     * @throws SQLSrvException
327
     */
328
    public function fetch($fetchMode = null)
329
    {
330 331
        // do not try fetching from the statement if it's not expected to contain result
        // in order to prevent exceptional situation
Sergei Morozov's avatar
Sergei Morozov committed
332
        if ($this->stmt === null || ! $this->result) {
333 334 335
            return false;
        }

336
        $fetchMode = $fetchMode ?? $this->defaultFetchMode;
Steve Müller's avatar
Steve Müller committed
337

338
        if ($fetchMode === FetchMode::COLUMN) {
339 340 341
            return $this->fetchColumn();
        }

342
        if (isset(self::$fetchMap[$fetchMode])) {
343
            return sqlsrv_fetch_array($this->stmt, self::$fetchMap[$fetchMode]) ?? false;
Steve Müller's avatar
Steve Müller committed
344
        }
345

346
        throw new SQLSrvException('Fetch mode is not supported!');
347 348 349 350 351
    }

    /**
     * {@inheritdoc}
     */
352
    public function fetchAll($fetchMode = null)
353
    {
354
        $rows = [];
355 356

        switch ($fetchMode) {
357
            case FetchMode::COLUMN:
358
                while (($row = $this->fetchColumn()) !== false) {
359 360
                    $rows[] = $row;
                }
Grégoire Paris's avatar
Grégoire Paris committed
361

362
                break;
363

364
            default:
365
                while (($row = $this->fetch($fetchMode)) !== false) {
366 367
                    $rows[] = $row;
                }
368
        }
Benjamin Morel's avatar
Benjamin Morel committed
369

370 371 372 373 374 375
        return $rows;
    }

    /**
     * {@inheritdoc}
     */
376
    public function fetchColumn()
377
    {
378
        $row = $this->fetch(FetchMode::NUMERIC);
Benjamin Morel's avatar
Benjamin Morel committed
379

380
        if ($row === false) {
381
            return false;
382 383
        }

384
        return $row[0] ?? null;
385 386
    }

387
    public function rowCount() : int
388
    {
Sergei Morozov's avatar
Sergei Morozov committed
389 390 391 392
        if ($this->stmt === null) {
            return 0;
        }

393 394 395 396 397 398 399
        $count = sqlsrv_rows_affected($this->stmt);

        if ($count !== false) {
            return $count;
        }

        return 0;
400 401
    }
}