SQLSrvStatement.php 10.5 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 PDO;
11 12 13 14 15 16 17 18
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;
use function array_key_exists;
use function count;
19
use function func_get_args;
20
use function in_array;
Sergei Morozov's avatar
Sergei Morozov committed
21
use function is_int;
22 23 24 25 26 27 28 29 30 31
use function is_numeric;
use function sqlsrv_errors;
use function sqlsrv_execute;
use function sqlsrv_fetch;
use function sqlsrv_fetch_array;
use function sqlsrv_fetch_object;
use function sqlsrv_get_field;
use function sqlsrv_next_result;
use function sqlsrv_num_fields;
use function SQLSRV_PHPTYPE_STREAM;
32
use function SQLSRV_PHPTYPE_STRING;
33 34 35 36
use function sqlsrv_prepare;
use function sqlsrv_rows_affected;
use function SQLSRV_SQLTYPE_VARBINARY;
use function stripos;
37

38
/**
Benjamin Morel's avatar
Benjamin Morel committed
39
 * SQL Server Statement.
40
 */
41 42 43
class SQLSrvStatement implements IteratorAggregate, Statement
{
    /**
Benjamin Morel's avatar
Benjamin Morel committed
44
     * The SQLSRV Resource.
45 46 47 48 49 50
     *
     * @var resource
     */
    private $conn;

    /**
Benjamin Morel's avatar
Benjamin Morel committed
51
     * The SQL statement to execute.
52 53 54 55 56 57
     *
     * @var string
     */
    private $sql;

    /**
Benjamin Morel's avatar
Benjamin Morel committed
58
     * The SQLSRV statement resource.
59
     *
60
     * @var resource|null
61 62 63 64
     */
    private $stmt;

    /**
65
     * References to the variables bound as statement parameters.
66
     *
67
     * @var mixed
68
     */
69
    private $variables = [];
70 71 72 73

    /**
     * Bound parameter types.
     *
74
     * @var int[]
75
     */
76
    private $types = [];
77 78

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

89
    /**
90
     * The name of the default class to instantiate when fetching class instances.
91 92 93 94 95 96
     *
     * @var string
     */
    private $defaultFetchClass = '\stdClass';

    /**
97
     * The constructor arguments for the default class to instantiate when fetching class instances.
98
     *
99
     * @var mixed[]
100
     */
101
    private $defaultFetchClassCtorArgs = [];
102

103
    /**
Benjamin Morel's avatar
Benjamin Morel committed
104
     * The fetch style.
105
     *
Sergei Morozov's avatar
Sergei Morozov committed
106
     * @var int
107
     */
108
    private $defaultFetchMode = FetchMode::MIXED;
109

110
    /**
Benjamin Morel's avatar
Benjamin Morel committed
111 112
     * The last insert ID.
     *
113
     * @var LastInsertId|null
114 115 116
     */
    private $lastInsertId;

117 118 119 120 121 122 123
    /**
     * Indicates whether the statement is in the state when fetching results is possible
     *
     * @var bool
     */
    private $result = false;

124 125
    /**
     * Append to any INSERT query to retrieve the last insert id.
126 127
     *
     * @deprecated This constant has been deprecated and will be made private in 3.0
128
     */
129
    public const LAST_INSERT_ID_SQL = ';SELECT SCOPE_IDENTITY() AS LastInsertId;';
130

Benjamin Morel's avatar
Benjamin Morel committed
131
    /**
132 133
     * @param resource $conn
     * @param string   $sql
Benjamin Morel's avatar
Benjamin Morel committed
134
     */
135
    public function __construct($conn, $sql, ?LastInsertId $lastInsertId = null)
136 137
    {
        $this->conn = $conn;
138
        $this->sql  = $sql;
139

140 141
        if (stripos($sql, 'INSERT INTO ') !== 0) {
            return;
142
        }
143 144 145

        $this->sql         .= self::LAST_INSERT_ID_SQL;
        $this->lastInsertId = $lastInsertId;
146 147
    }

Benjamin Morel's avatar
Benjamin Morel committed
148 149 150
    /**
     * {@inheritdoc}
     */
151
    public function bindValue($param, $value, $type = ParameterType::STRING)
152
    {
153
        if (! is_numeric($param)) {
154 155 156 157 158 159
            throw new SQLSrvException(
                'sqlsrv does not support named parameters to queries, use question mark (?) placeholders instead.'
            );
        }

        $this->variables[$param] = $value;
160
        $this->types[$param]     = $type;
161 162

        return true;
163 164 165 166 167
    }

    /**
     * {@inheritdoc}
     */
168
    public function bindParam($column, &$variable, $type = ParameterType::STRING, $length = null)
169
    {
170 171
        if (! is_numeric($column)) {
            throw new SQLSrvException('sqlsrv does not support named parameters to queries, use question mark (?) placeholders instead.');
172 173
        }

174
        $this->variables[$column] =& $variable;
175
        $this->types[$column]     = $type;
176 177 178

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

        return true;
181 182
    }

Benjamin Morel's avatar
Benjamin Morel committed
183 184 185
    /**
     * {@inheritdoc}
     */
186 187
    public function closeCursor()
    {
188
        // not having the result means there's nothing to close
Sergei Morozov's avatar
Sergei Morozov committed
189
        if ($this->stmt === null || ! $this->result) {
190 191 192
            return true;
        }

193 194 195 196
        // 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
197 198
        while (sqlsrv_fetch($this->stmt)) {
        }
199

200 201
        $this->result = false;

202
        return true;
203 204
    }

Benjamin Morel's avatar
Benjamin Morel committed
205 206 207
    /**
     * {@inheritdoc}
     */
208 209
    public function columnCount()
    {
Sergei Morozov's avatar
Sergei Morozov committed
210 211 212 213 214
        if ($this->stmt === null) {
            return 0;
        }

        return sqlsrv_num_fields($this->stmt) ?: 0;
215 216 217
    }

    /**
Benjamin Morel's avatar
Benjamin Morel committed
218
     * {@inheritdoc}
219 220 221 222 223 224 225
     */
    public function errorCode()
    {
        $errors = sqlsrv_errors(SQLSRV_ERR_ERRORS);
        if ($errors) {
            return $errors[0]['code'];
        }
Benjamin Morel's avatar
Benjamin Morel committed
226

227 228 229 230
        return false;
    }

    /**
Benjamin Morel's avatar
Benjamin Morel committed
231
     * {@inheritdoc}
232 233 234
     */
    public function errorInfo()
    {
Sergei Morozov's avatar
Sergei Morozov committed
235
        return (array) sqlsrv_errors(SQLSRV_ERR_ERRORS);
236 237
    }

Benjamin Morel's avatar
Benjamin Morel committed
238 239 240
    /**
     * {@inheritdoc}
     */
241 242 243 244
    public function execute($params = null)
    {
        if ($params) {
            $hasZeroIndex = array_key_exists(0, $params);
Sergei Morozov's avatar
Sergei Morozov committed
245

246
            foreach ($params as $key => $val) {
Sergei Morozov's avatar
Sergei Morozov committed
247 248 249 250 251
                if ($hasZeroIndex && is_int($key)) {
                    $this->bindValue($key + 1, $val);
                } else {
                    $this->bindValue($key, $val);
                }
252 253 254
            }
        }

255
        if (! $this->stmt) {
256
            $this->stmt = $this->prepare();
257 258
        }

259
        if (! sqlsrv_execute($this->stmt)) {
260 261
            throw SQLSrvException::fromSqlSrvErrors();
        }
262 263 264 265

        if ($this->lastInsertId) {
            sqlsrv_next_result($this->stmt);
            sqlsrv_fetch($this->stmt);
Steve Müller's avatar
Steve Müller committed
266
            $this->lastInsertId->setId(sqlsrv_get_field($this->stmt, 0));
267
        }
268 269

        $this->result = true;
270 271

        return true;
272 273
    }

274 275 276 277
    /**
     * Prepares SQL Server statement resource
     *
     * @return resource
278
     *
279 280 281 282
     * @throws SQLSrvException
     */
    private function prepare()
    {
283
        $params = [];
284 285

        foreach ($this->variables as $column => &$variable) {
286 287 288 289 290
            switch ($this->types[$column]) {
                case ParameterType::LARGE_OBJECT:
                    $params[$column - 1] = [
                        &$variable,
                        SQLSRV_PARAM_IN,
291 292
                        SQLSRV_PHPTYPE_STREAM(SQLSRV_ENC_BINARY),
                        SQLSRV_SQLTYPE_VARBINARY('max'),
293 294 295 296 297 298 299
                    ];
                    break;

                case ParameterType::BINARY:
                    $params[$column - 1] = [
                        &$variable,
                        SQLSRV_PARAM_IN,
300
                        SQLSRV_PHPTYPE_STRING(SQLSRV_ENC_BINARY),
301 302 303 304 305 306
                    ];
                    break;

                default:
                    $params[$column - 1] =& $variable;
                    break;
307 308 309 310 311
            }
        }

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

312
        if (! $stmt) {
313 314 315 316 317 318
            throw SQLSrvException::fromSqlSrvErrors();
        }

        return $stmt;
    }

Benjamin Morel's avatar
Benjamin Morel committed
319 320 321
    /**
     * {@inheritdoc}
     */
322
    public function setFetchMode($fetchMode, $arg2 = null, $arg3 = null)
323
    {
324 325 326
        $this->defaultFetchMode          = $fetchMode;
        $this->defaultFetchClass         = $arg2 ?: $this->defaultFetchClass;
        $this->defaultFetchClassCtorArgs = $arg3 ? (array) $arg3 : $this->defaultFetchClassCtorArgs;
Benjamin Morel's avatar
Benjamin Morel committed
327 328

        return true;
329 330 331 332 333 334 335
    }

    /**
     * {@inheritdoc}
     */
    public function getIterator()
    {
336
        return new StatementIterator($this);
337 338 339 340
    }

    /**
     * {@inheritdoc}
341 342
     *
     * @throws SQLSrvException
343
     */
344
    public function fetch($fetchMode = null, $cursorOrientation = PDO::FETCH_ORI_NEXT, $cursorOffset = 0)
345
    {
346 347
        // 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
348
        if ($this->stmt === null || ! $this->result) {
349 350 351
            return false;
        }

Steve Müller's avatar
Steve Müller committed
352
        $args      = func_get_args();
353
        $fetchMode = $fetchMode ?: $this->defaultFetchMode;
Steve Müller's avatar
Steve Müller committed
354

355
        if ($fetchMode === FetchMode::COLUMN) {
356 357 358
            return $this->fetchColumn();
        }

359
        if (isset(self::$fetchMap[$fetchMode])) {
Steve Müller's avatar
Steve Müller committed
360 361
            return sqlsrv_fetch_array($this->stmt, self::$fetchMap[$fetchMode]) ?: false;
        }
362

363
        if (in_array($fetchMode, [FetchMode::STANDARD_OBJECT, FetchMode::CUSTOM_OBJECT], true)) {
364 365
            $className = $this->defaultFetchClass;
            $ctorArgs  = $this->defaultFetchClassCtorArgs;
Steve Müller's avatar
Steve Müller committed
366

367
            if (count($args) >= 2) {
368
                $className = $args[1];
369
                $ctorArgs  = $args[2] ?? [];
370
            }
371

Steve Müller's avatar
Steve Müller committed
372
            return sqlsrv_fetch_object($this->stmt, $className, $ctorArgs) ?: false;
373
        }
374

375
        throw new SQLSrvException('Fetch mode is not supported!');
376 377 378 379 380
    }

    /**
     * {@inheritdoc}
     */
381
    public function fetchAll($fetchMode = null, $fetchArgument = null, $ctorArgs = null)
382
    {
383
        $rows = [];
384 385

        switch ($fetchMode) {
386
            case FetchMode::CUSTOM_OBJECT:
387
                while (($row = $this->fetch(...func_get_args())) !== false) {
388 389 390
                    $rows[] = $row;
                }
                break;
391 392

            case FetchMode::COLUMN:
393
                while (($row = $this->fetchColumn()) !== false) {
394 395 396
                    $rows[] = $row;
                }
                break;
397

398
            default:
399
                while (($row = $this->fetch($fetchMode)) !== false) {
400 401
                    $rows[] = $row;
                }
402
        }
Benjamin Morel's avatar
Benjamin Morel committed
403

404 405 406 407 408 409 410 411
        return $rows;
    }

    /**
     * {@inheritdoc}
     */
    public function fetchColumn($columnIndex = 0)
    {
412
        $row = $this->fetch(FetchMode::NUMERIC);
Benjamin Morel's avatar
Benjamin Morel committed
413

414
        if ($row === false) {
415
            return false;
416 417
        }

418
        return $row[$columnIndex] ?? null;
419 420 421 422 423 424 425
    }

    /**
     * {@inheritdoc}
     */
    public function rowCount()
    {
Sergei Morozov's avatar
Sergei Morozov committed
426 427 428 429 430
        if ($this->stmt === null) {
            return 0;
        }

        return sqlsrv_rows_affected($this->stmt) ?: 0;
431 432
    }
}