DB2Statement.php 14.4 KB
Newer Older
1 2
<?php

Benjamin Eberlei's avatar
Benjamin Eberlei committed
3
namespace Doctrine\DBAL\Driver\IBMDB2;
4

5
use Doctrine\DBAL\Driver\FetchUtils;
6 7 8
use Doctrine\DBAL\Driver\IBMDB2\Exception\CannotCopyStreamToStream;
use Doctrine\DBAL\Driver\IBMDB2\Exception\CannotCreateTemporaryFile;
use Doctrine\DBAL\Driver\IBMDB2\Exception\CannotWriteToTemporaryFile;
9
use Doctrine\DBAL\Driver\IBMDB2\Exception\StatementError;
10
use Doctrine\DBAL\Driver\Result;
11
use Doctrine\DBAL\Driver\Statement as StatementInterface;
12
use Doctrine\DBAL\Driver\StatementIterator;
13 14
use Doctrine\DBAL\FetchMode;
use Doctrine\DBAL\ParameterType;
15 16 17 18 19 20
use IteratorAggregate;
use PDO;
use ReflectionClass;
use ReflectionObject;
use ReflectionProperty;
use stdClass;
21

22
use function array_change_key_case;
23
use function assert;
24 25 26 27 28 29 30 31 32 33 34
use function db2_bind_param;
use function db2_execute;
use function db2_fetch_array;
use function db2_fetch_assoc;
use function db2_fetch_both;
use function db2_fetch_object;
use function db2_free_result;
use function db2_num_fields;
use function db2_num_rows;
use function db2_stmt_error;
use function db2_stmt_errormsg;
35 36
use function error_get_last;
use function fclose;
37 38
use function func_get_args;
use function func_num_args;
39
use function fwrite;
40
use function gettype;
41
use function is_int;
42
use function is_object;
43
use function is_resource;
44 45 46
use function is_string;
use function ksort;
use function sprintf;
47 48
use function stream_copy_to_stream;
use function stream_get_meta_data;
49
use function strtolower;
50
use function tmpfile;
51

Grégoire Paris's avatar
Grégoire Paris committed
52 53 54 55 56 57
use const CASE_LOWER;
use const DB2_BINARY;
use const DB2_CHAR;
use const DB2_LONG;
use const DB2_PARAM_FILE;
use const DB2_PARAM_IN;
58

59 60 61 62
/**
 * @deprecated Use {@link Statement} instead
 */
class DB2Statement implements IteratorAggregate, StatementInterface, Result
63
{
64
    /** @var resource */
65
    private $stmt;
66

67
    /** @var mixed[] */
68
    private $bindParam = [];
69

70 71 72 73 74 75 76 77
    /**
     * Map of LOB parameter positions to the tuples containing reference to the variable bound to the driver statement
     * and the temporary file handle bound to the underlying statement
     *
     * @var mixed[][]
     */
    private $lobs = [];

78
    /** @var string Name of the default class to instantiate when fetching class instances. */
79 80
    private $defaultFetchClass = '\stdClass';

81
    /** @var mixed[] Constructor arguments for the default class to instantiate when fetching class instances. */
82
    private $defaultFetchClassCtorArgs = [];
83

84
    /** @var int */
85
    private $defaultFetchMode = FetchMode::MIXED;
86

87 88 89 90 91 92 93
    /**
     * Indicates whether the statement is in the state when fetching results is possible
     *
     * @var bool
     */
    private $result = false;

Benjamin Morel's avatar
Benjamin Morel committed
94
    /**
95 96
     * @internal The statement can be only instantiated by its driver connection.
     *
Benjamin Morel's avatar
Benjamin Morel committed
97 98
     * @param resource $stmt
     */
99 100
    public function __construct($stmt)
    {
101
        $this->stmt = $stmt;
102 103 104
    }

    /**
105
     * {@inheritdoc}
106
     */
107
    public function bindValue($param, $value, $type = ParameterType::STRING)
108
    {
109 110
        assert(is_int($param));

111
        return $this->bindParam($param, $value, $type);
112 113 114
    }

    /**
115
     * {@inheritdoc}
116
     */
117
    public function bindParam($param, &$variable, $type = ParameterType::STRING, $length = null)
118
    {
119
        assert(is_int($param));
120

121 122
        switch ($type) {
            case ParameterType::INTEGER:
123
                $this->bind($param, $variable, DB2_PARAM_IN, DB2_LONG);
124
                break;
125

126
            case ParameterType::LARGE_OBJECT:
127 128
                if (isset($this->lobs[$param])) {
                    [, $handle] = $this->lobs[$param];
129 130
                    fclose($handle);
                }
131

132 133 134
                $handle = $this->createTemporaryFile();
                $path   = stream_get_meta_data($handle)['uri'];

135
                $this->bind($param, $path, DB2_PARAM_FILE, DB2_BINARY);
136

137
                $this->lobs[$param] = [&$variable, $handle];
138 139 140
                break;

            default:
141
                $this->bind($param, $variable, DB2_PARAM_IN, DB2_CHAR);
142
                break;
143
        }
Benjamin Morel's avatar
Benjamin Morel committed
144

145
        return true;
146 147
    }

148
    /**
Sergei Morozov's avatar
Sergei Morozov committed
149 150
     * @param int   $position Parameter position
     * @param mixed $variable
151 152 153
     *
     * @throws DB2Exception
     */
154
    private function bind($position, &$variable, int $parameterType, int $dataType): void
155
    {
Sergei Morozov's avatar
Sergei Morozov committed
156
        $this->bindParam[$position] =& $variable;
157

Sergei Morozov's avatar
Sergei Morozov committed
158
        if (! db2_bind_param($this->stmt, $position, 'variable', $parameterType, $dataType)) {
159
            throw StatementError::new($this->stmt);
160 161 162
        }
    }

163
    /**
164
     * {@inheritdoc}
165 166
     *
     * @deprecated Use free() instead.
167
     */
168
    public function closeCursor()
169
    {
170
        $this->bindParam = [];
Benjamin Morel's avatar
Benjamin Morel committed
171

172
        if (! db2_free_result($this->stmt)) {
173 174 175 176 177 178
            return false;
        }

        $this->result = false;

        return true;
179 180 181
    }

    /**
182
     * {@inheritdoc}
183
     */
184
    public function columnCount()
185
    {
Sergei Morozov's avatar
Sergei Morozov committed
186
        return db2_num_fields($this->stmt) ?: 0;
187 188 189
    }

    /**
190
     * {@inheritdoc}
191 192
     *
     * @deprecated The error information is available via exceptions.
193
     */
194
    public function errorCode()
195 196 197 198 199
    {
        return db2_stmt_error();
    }

    /**
200
     * {@inheritdoc}
201 202
     *
     * @deprecated The error information is available via exceptions.
203
     */
204
    public function errorInfo()
205
    {
206
        return [
207 208
            db2_stmt_errormsg(),
            db2_stmt_error(),
209
        ];
210 211 212
    }

    /**
213
     * {@inheritdoc}
214
     */
215
    public function execute($params = null)
216
    {
217
        if ($params === null) {
218
            ksort($this->bindParam);
219

220
            $params = [];
221

222
            foreach ($this->bindParam as $column => $value) {
223 224
                $params[] = $value;
            }
225
        }
226

227 228 229 230 231 232 233 234 235 236
        foreach ($this->lobs as [$source, $target]) {
            if (is_resource($source)) {
                $this->copyStreamToStream($source, $target);

                continue;
            }

            $this->writeStringToStream($source, $target);
        }

237
        $retval = db2_execute($this->stmt, $params);
238

239 240 241 242 243 244
        foreach ($this->lobs as [, $handle]) {
            fclose($handle);
        }

        $this->lobs = [];

245
        if ($retval === false) {
246
            throw StatementError::new($this->stmt);
247
        }
Benjamin Morel's avatar
Benjamin Morel committed
248

249 250
        $this->result = true;

251 252 253
        return $retval;
    }

254 255
    /**
     * {@inheritdoc}
256 257
     *
     * @deprecated Use one of the fetch- or iterate-related methods.
258
     */
259
    public function setFetchMode($fetchMode, $arg2 = null, $arg3 = null)
260
    {
261
        $this->defaultFetchMode          = $fetchMode;
262
        $this->defaultFetchClass         = $arg2 ?: $this->defaultFetchClass;
263
        $this->defaultFetchClassCtorArgs = $arg3 ? (array) $arg3 : $this->defaultFetchClassCtorArgs;
Benjamin Morel's avatar
Benjamin Morel committed
264 265

        return true;
266 267 268 269
    }

    /**
     * {@inheritdoc}
270 271
     *
     * @deprecated Use iterateNumeric(), iterateAssociative() or iterateColumn() instead.
272 273 274
     */
    public function getIterator()
    {
275
        return new StatementIterator($this);
276 277
    }

278
    /**
279
     * {@inheritdoc}
280 281
     *
     * @deprecated Use fetchNumeric(), fetchAssociative() or fetchOne() instead.
282
     */
283
    public function fetch($fetchMode = null, $cursorOrientation = PDO::FETCH_ORI_NEXT, $cursorOffset = 0)
284
    {
285 286
        // do not try fetching from the statement if it's not expected to contain result
        // in order to prevent exceptional situation
287
        if (! $this->result) {
288 289 290
            return false;
        }

291
        $fetchMode = $fetchMode ?: $this->defaultFetchMode;
292
        switch ($fetchMode) {
293
            case FetchMode::COLUMN:
294 295
                return $this->fetchColumn();

296
            case FetchMode::MIXED:
297
                return db2_fetch_both($this->stmt);
298 299

            case FetchMode::ASSOCIATIVE:
300
                return db2_fetch_assoc($this->stmt);
301 302

            case FetchMode::CUSTOM_OBJECT:
303 304 305 306 307 308
                $className = $this->defaultFetchClass;
                $ctorArgs  = $this->defaultFetchClassCtorArgs;

                if (func_num_args() >= 2) {
                    $args      = func_get_args();
                    $className = $args[1];
309
                    $ctorArgs  = $args[2] ?? [];
310 311
                }

312
                $result = db2_fetch_object($this->stmt);
313

314
                if ($result instanceof stdClass) {
315 316 317 318
                    $result = $this->castObject($result, $className, $ctorArgs);
                }

                return $result;
319 320

            case FetchMode::NUMERIC:
321
                return db2_fetch_array($this->stmt);
322 323

            case FetchMode::STANDARD_OBJECT:
324
                return db2_fetch_object($this->stmt);
325

326
            default:
327
                throw new DB2Exception('Given Fetch-Style ' . $fetchMode . ' is not supported.');
328 329 330 331
        }
    }

    /**
332
     * {@inheritdoc}
333
     *
334
     * @deprecated Use fetchAllNumeric(), fetchAllAssociative() or fetchFirstColumn() instead.
335
     */
336
    public function fetchAll($fetchMode = null, $fetchArgument = null, $ctorArgs = null)
337
    {
338
        $rows = [];
339 340

        switch ($fetchMode) {
341
            case FetchMode::CUSTOM_OBJECT:
Sergei Morozov's avatar
Sergei Morozov committed
342
                while (($row = $this->fetch(...func_get_args())) !== false) {
343 344
                    $rows[] = $row;
                }
Grégoire Paris's avatar
Grégoire Paris committed
345

346
                break;
Sergei Morozov's avatar
Sergei Morozov committed
347

348
            case FetchMode::COLUMN:
Sergei Morozov's avatar
Sergei Morozov committed
349
                while (($row = $this->fetchColumn()) !== false) {
350 351
                    $rows[] = $row;
                }
Grégoire Paris's avatar
Grégoire Paris committed
352

353
                break;
Sergei Morozov's avatar
Sergei Morozov committed
354

355
            default:
Sergei Morozov's avatar
Sergei Morozov committed
356
                while (($row = $this->fetch($fetchMode)) !== false) {
357 358
                    $rows[] = $row;
                }
359
        }
Benjamin Morel's avatar
Benjamin Morel committed
360

361 362 363 364
        return $rows;
    }

    /**
365
     * {@inheritdoc}
366 367
     *
     * @deprecated Use fetchOne() instead.
368
     */
369
    public function fetchColumn($columnIndex = 0)
370
    {
371
        $row = $this->fetch(FetchMode::NUMERIC);
372

373
        if ($row === false) {
374
            return false;
375
        }
Benjamin Morel's avatar
Benjamin Morel committed
376

377
        return $row[$columnIndex] ?? null;
378 379
    }

380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416
    /**
     * {@inheritDoc}
     */
    public function fetchNumeric()
    {
        if (! $this->result) {
            return false;
        }

        return db2_fetch_array($this->stmt);
    }

    /**
     * {@inheritdoc}
     */
    public function fetchAssociative()
    {
        // do not try fetching from the statement if it's not expected to contain the result
        // in order to prevent exceptional situation
        if (! $this->result) {
            return false;
        }

        return db2_fetch_assoc($this->stmt);
    }

    /**
     * {@inheritdoc}
     */
    public function fetchOne()
    {
        return FetchUtils::fetchOne($this);
    }

    /**
     * {@inheritdoc}
     */
417
    public function fetchAllNumeric(): array
418 419 420 421 422 423 424
    {
        return FetchUtils::fetchAllNumeric($this);
    }

    /**
     * {@inheritdoc}
     */
425
    public function fetchAllAssociative(): array
426 427 428 429
    {
        return FetchUtils::fetchAllAssociative($this);
    }

430 431 432
    /**
     * {@inheritdoc}
     */
433
    public function fetchFirstColumn(): array
434 435 436 437
    {
        return FetchUtils::fetchFirstColumn($this);
    }

438
    /**
439
     * {@inheritdoc}
440
     */
441
    public function rowCount()
442
    {
443
        return @db2_num_rows($this->stmt) ? : 0;
444
    }
445

446 447 448 449 450 451 452 453 454
    public function free(): void
    {
        $this->bindParam = [];

        db2_free_result($this->stmt);

        $this->result = false;
    }

455 456 457
    /**
     * Casts a stdClass object to the given class name mapping its' properties.
     *
458
     * @param stdClass      $sourceObject     Object to cast from.
459
     * @param string|object $destinationClass Name of the class or class instance to cast to.
460
     * @param mixed[]       $ctorArgs         Arguments to use for constructing the destination class instance.
461 462 463 464 465
     *
     * @return object
     *
     * @throws DB2Exception
     */
466
    private function castObject(stdClass $sourceObject, $destinationClass, array $ctorArgs = [])
467
    {
468 469
        if (! is_string($destinationClass)) {
            if (! is_object($destinationClass)) {
470
                throw new DB2Exception(sprintf(
471 472
                    'Destination class has to be of type string or object, %s given.',
                    gettype($destinationClass)
473 474 475
                ));
            }
        } else {
476
            $destinationClass = new ReflectionClass($destinationClass);
477 478 479
            $destinationClass = $destinationClass->newInstanceArgs($ctorArgs);
        }

480 481 482 483
        $sourceReflection           = new ReflectionObject($sourceObject);
        $destinationClassReflection = new ReflectionObject($destinationClass);
        /** @var ReflectionProperty[] $destinationProperties */
        $destinationProperties = array_change_key_case($destinationClassReflection->getProperties(), CASE_LOWER);
484 485 486 487 488 489 490

        foreach ($sourceReflection->getProperties() as $sourceProperty) {
            $sourceProperty->setAccessible(true);

            $name  = $sourceProperty->getName();
            $value = $sourceProperty->getValue($sourceObject);

491
            // Try to find a case-matching property.
492 493 494 495 496
            if ($destinationClassReflection->hasProperty($name)) {
                $destinationProperty = $destinationClassReflection->getProperty($name);

                $destinationProperty->setAccessible(true);
                $destinationProperty->setValue($destinationClass, $value);
497 498

                continue;
499
            }
500 501 502 503 504 505 506 507 508 509 510 511 512 513 514

            $name = strtolower($name);

            // Try to find a property without matching case.
            // Fallback for the driver returning either all uppercase or all lowercase column names.
            if (isset($destinationProperties[$name])) {
                $destinationProperty = $destinationProperties[$name];

                $destinationProperty->setAccessible(true);
                $destinationProperty->setValue($destinationClass, $value);

                continue;
            }

            $destinationClass->$name = $value;
515 516 517 518
        }

        return $destinationClass;
    }
519 520 521 522 523 524 525 526 527 528 529

    /**
     * @return resource
     *
     * @throws DB2Exception
     */
    private function createTemporaryFile()
    {
        $handle = @tmpfile();

        if ($handle === false) {
530
            throw CannotCreateTemporaryFile::new(error_get_last()['message']);
531 532 533 534 535 536 537 538 539 540 541
        }

        return $handle;
    }

    /**
     * @param resource $source
     * @param resource $target
     *
     * @throws DB2Exception
     */
542
    private function copyStreamToStream($source, $target): void
543 544
    {
        if (@stream_copy_to_stream($source, $target) === false) {
545
            throw CannotCopyStreamToStream::new(error_get_last()['message']);
546 547 548 549 550 551 552 553
        }
    }

    /**
     * @param resource $target
     *
     * @throws DB2Exception
     */
554
    private function writeStringToStream(string $string, $target): void
555 556
    {
        if (@fwrite($target, $string) === false) {
557
            throw CannotWriteToTemporaryFile::new(error_get_last()['message']);
558 559
        }
    }
560
}