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

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

Steve Müller's avatar
Steve Müller committed
5
use Doctrine\DBAL\Driver\Statement;
6
use Doctrine\DBAL\Driver\StatementIterator;
7 8
use Doctrine\DBAL\FetchMode;
use Doctrine\DBAL\ParameterType;
9 10 11 12 13 14
use IteratorAggregate;
use PDO;
use ReflectionClass;
use ReflectionObject;
use ReflectionProperty;
use stdClass;
15

16
use function array_change_key_case;
17
use function assert;
18 19 20 21 22 23 24 25 26 27 28
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;
29 30
use function error_get_last;
use function fclose;
31 32
use function func_get_args;
use function func_num_args;
33
use function fwrite;
34
use function gettype;
35
use function is_int;
36
use function is_object;
37
use function is_resource;
38 39 40
use function is_string;
use function ksort;
use function sprintf;
41 42
use function stream_copy_to_stream;
use function stream_get_meta_data;
43
use function strtolower;
44
use function tmpfile;
45

Grégoire Paris's avatar
Grégoire Paris committed
46 47 48 49 50 51
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;
52

53
class DB2Statement implements IteratorAggregate, Statement
54
{
55
    /** @var resource */
56
    private $stmt;
57

58
    /** @var mixed[] */
59
    private $bindParam = [];
60

61 62 63 64 65 66 67 68
    /**
     * 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 = [];

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

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

75
    /** @var int */
76
    private $defaultFetchMode = FetchMode::MIXED;
77

78 79 80 81 82 83 84
    /**
     * 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
85 86 87
    /**
     * @param resource $stmt
     */
88 89
    public function __construct($stmt)
    {
90
        $this->stmt = $stmt;
91 92 93
    }

    /**
94
     * {@inheritdoc}
95
     */
96
    public function bindValue($param, $value, $type = ParameterType::STRING)
97
    {
98 99
        assert(is_int($param));

100
        return $this->bindParam($param, $value, $type);
101 102 103
    }

    /**
104
     * {@inheritdoc}
105
     */
106
    public function bindParam($param, &$variable, $type = ParameterType::STRING, $length = null)
107
    {
108
        assert(is_int($param));
109

110 111
        switch ($type) {
            case ParameterType::INTEGER:
112
                $this->bind($param, $variable, DB2_PARAM_IN, DB2_LONG);
113
                break;
114

115
            case ParameterType::LARGE_OBJECT:
116 117
                if (isset($this->lobs[$param])) {
                    [, $handle] = $this->lobs[$param];
118 119
                    fclose($handle);
                }
120

121 122 123
                $handle = $this->createTemporaryFile();
                $path   = stream_get_meta_data($handle)['uri'];

124
                $this->bind($param, $path, DB2_PARAM_FILE, DB2_BINARY);
125

126
                $this->lobs[$param] = [&$variable, $handle];
127 128 129
                break;

            default:
130
                $this->bind($param, $variable, DB2_PARAM_IN, DB2_CHAR);
131
                break;
132
        }
Benjamin Morel's avatar
Benjamin Morel committed
133

134
        return true;
135 136
    }

137
    /**
Sergei Morozov's avatar
Sergei Morozov committed
138 139
     * @param int   $position Parameter position
     * @param mixed $variable
140 141 142
     *
     * @throws DB2Exception
     */
143
    private function bind($position, &$variable, int $parameterType, int $dataType): void
144
    {
Sergei Morozov's avatar
Sergei Morozov committed
145
        $this->bindParam[$position] =& $variable;
146

Sergei Morozov's avatar
Sergei Morozov committed
147
        if (! db2_bind_param($this->stmt, $position, 'variable', $parameterType, $dataType)) {
148 149 150 151
            throw new DB2Exception(db2_stmt_errormsg());
        }
    }

152
    /**
153
     * {@inheritdoc}
154
     */
155
    public function closeCursor()
156
    {
157
        $this->bindParam = [];
Benjamin Morel's avatar
Benjamin Morel committed
158

159
        if (! db2_free_result($this->stmt)) {
160 161 162 163 164 165
            return false;
        }

        $this->result = false;

        return true;
166 167 168
    }

    /**
169
     * {@inheritdoc}
170
     */
171
    public function columnCount()
172
    {
Sergei Morozov's avatar
Sergei Morozov committed
173
        return db2_num_fields($this->stmt) ?: 0;
174 175 176
    }

    /**
177
     * {@inheritdoc}
178
     */
179
    public function errorCode()
180 181 182 183 184
    {
        return db2_stmt_error();
    }

    /**
185
     * {@inheritdoc}
186
     */
187
    public function errorInfo()
188
    {
189
        return [
190 191
            db2_stmt_errormsg(),
            db2_stmt_error(),
192
        ];
193 194 195
    }

    /**
196
     * {@inheritdoc}
197
     */
198
    public function execute($params = null)
199
    {
200
        if ($params === null) {
201
            ksort($this->bindParam);
202

203
            $params = [];
204

205
            foreach ($this->bindParam as $column => $value) {
206 207
                $params[] = $value;
            }
208
        }
209

210 211 212 213 214 215 216 217 218 219
        foreach ($this->lobs as [$source, $target]) {
            if (is_resource($source)) {
                $this->copyStreamToStream($source, $target);

                continue;
            }

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

220
        $retval = db2_execute($this->stmt, $params);
221

222 223 224 225 226 227
        foreach ($this->lobs as [, $handle]) {
            fclose($handle);
        }

        $this->lobs = [];

228
        if ($retval === false) {
Benjamin Eberlei's avatar
Benjamin Eberlei committed
229
            throw new DB2Exception(db2_stmt_errormsg());
230
        }
Benjamin Morel's avatar
Benjamin Morel committed
231

232 233
        $this->result = true;

234 235 236
        return $retval;
    }

237 238 239
    /**
     * {@inheritdoc}
     */
240
    public function setFetchMode($fetchMode, $arg2 = null, $arg3 = null)
241
    {
242
        $this->defaultFetchMode          = $fetchMode;
243
        $this->defaultFetchClass         = $arg2 ?: $this->defaultFetchClass;
244
        $this->defaultFetchClassCtorArgs = $arg3 ? (array) $arg3 : $this->defaultFetchClassCtorArgs;
Benjamin Morel's avatar
Benjamin Morel committed
245 246

        return true;
247 248 249 250 251 252 253
    }

    /**
     * {@inheritdoc}
     */
    public function getIterator()
    {
254
        return new StatementIterator($this);
255 256
    }

257
    /**
258
     * {@inheritdoc}
259
     */
260
    public function fetch($fetchMode = null, $cursorOrientation = PDO::FETCH_ORI_NEXT, $cursorOffset = 0)
261
    {
262 263
        // do not try fetching from the statement if it's not expected to contain result
        // in order to prevent exceptional situation
264
        if (! $this->result) {
265 266 267
            return false;
        }

268
        $fetchMode = $fetchMode ?: $this->defaultFetchMode;
269
        switch ($fetchMode) {
270
            case FetchMode::COLUMN:
271 272
                return $this->fetchColumn();

273
            case FetchMode::MIXED:
274
                return db2_fetch_both($this->stmt);
275 276

            case FetchMode::ASSOCIATIVE:
277
                return db2_fetch_assoc($this->stmt);
278 279

            case FetchMode::CUSTOM_OBJECT:
280 281 282 283 284 285
                $className = $this->defaultFetchClass;
                $ctorArgs  = $this->defaultFetchClassCtorArgs;

                if (func_num_args() >= 2) {
                    $args      = func_get_args();
                    $className = $args[1];
286
                    $ctorArgs  = $args[2] ?? [];
287 288
                }

289
                $result = db2_fetch_object($this->stmt);
290

291
                if ($result instanceof stdClass) {
292 293 294 295
                    $result = $this->castObject($result, $className, $ctorArgs);
                }

                return $result;
296 297

            case FetchMode::NUMERIC:
298
                return db2_fetch_array($this->stmt);
299 300

            case FetchMode::STANDARD_OBJECT:
301
                return db2_fetch_object($this->stmt);
302

303
            default:
304
                throw new DB2Exception('Given Fetch-Style ' . $fetchMode . ' is not supported.');
305 306 307 308
        }
    }

    /**
309
     * {@inheritdoc}
310
     */
311
    public function fetchAll($fetchMode = null, $fetchArgument = null, $ctorArgs = null)
312
    {
313
        $rows = [];
314 315

        switch ($fetchMode) {
316
            case FetchMode::CUSTOM_OBJECT:
Sergei Morozov's avatar
Sergei Morozov committed
317
                while (($row = $this->fetch(...func_get_args())) !== false) {
318 319
                    $rows[] = $row;
                }
Grégoire Paris's avatar
Grégoire Paris committed
320

321
                break;
Sergei Morozov's avatar
Sergei Morozov committed
322

323
            case FetchMode::COLUMN:
Sergei Morozov's avatar
Sergei Morozov committed
324
                while (($row = $this->fetchColumn()) !== false) {
325 326
                    $rows[] = $row;
                }
Grégoire Paris's avatar
Grégoire Paris committed
327

328
                break;
Sergei Morozov's avatar
Sergei Morozov committed
329

330
            default:
Sergei Morozov's avatar
Sergei Morozov committed
331
                while (($row = $this->fetch($fetchMode)) !== false) {
332 333
                    $rows[] = $row;
                }
334
        }
Benjamin Morel's avatar
Benjamin Morel committed
335

336 337 338 339
        return $rows;
    }

    /**
340
     * {@inheritdoc}
341
     */
342
    public function fetchColumn($columnIndex = 0)
343
    {
344
        $row = $this->fetch(FetchMode::NUMERIC);
345

346
        if ($row === false) {
347
            return false;
348
        }
Benjamin Morel's avatar
Benjamin Morel committed
349

350
        return $row[$columnIndex] ?? null;
351 352 353
    }

    /**
354
     * {@inheritdoc}
355
     */
356
    public function rowCount()
357
    {
358
        return @db2_num_rows($this->stmt) ? : 0;
359
    }
360 361 362 363

    /**
     * Casts a stdClass object to the given class name mapping its' properties.
     *
364
     * @param stdClass      $sourceObject     Object to cast from.
365
     * @param string|object $destinationClass Name of the class or class instance to cast to.
366
     * @param mixed[]       $ctorArgs         Arguments to use for constructing the destination class instance.
367 368 369 370 371
     *
     * @return object
     *
     * @throws DB2Exception
     */
372
    private function castObject(stdClass $sourceObject, $destinationClass, array $ctorArgs = [])
373
    {
374 375
        if (! is_string($destinationClass)) {
            if (! is_object($destinationClass)) {
376
                throw new DB2Exception(sprintf(
377 378
                    'Destination class has to be of type string or object, %s given.',
                    gettype($destinationClass)
379 380 381
                ));
            }
        } else {
382
            $destinationClass = new ReflectionClass($destinationClass);
383 384 385
            $destinationClass = $destinationClass->newInstanceArgs($ctorArgs);
        }

386 387 388 389
        $sourceReflection           = new ReflectionObject($sourceObject);
        $destinationClassReflection = new ReflectionObject($destinationClass);
        /** @var ReflectionProperty[] $destinationProperties */
        $destinationProperties = array_change_key_case($destinationClassReflection->getProperties(), CASE_LOWER);
390 391 392 393 394 395 396

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

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

397
            // Try to find a case-matching property.
398 399 400 401 402
            if ($destinationClassReflection->hasProperty($name)) {
                $destinationProperty = $destinationClassReflection->getProperty($name);

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

                continue;
405
            }
406 407 408 409 410 411 412 413 414 415 416 417 418 419 420

            $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;
421 422 423 424
        }

        return $destinationClass;
    }
425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447

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

        if ($handle === false) {
            throw new DB2Exception('Could not create temporary file: ' . error_get_last()['message']);
        }

        return $handle;
    }

    /**
     * @param resource $source
     * @param resource $target
     *
     * @throws DB2Exception
     */
448
    private function copyStreamToStream($source, $target): void
449 450 451 452 453 454 455 456 457 458 459
    {
        if (@stream_copy_to_stream($source, $target) === false) {
            throw new DB2Exception('Could not copy source stream to temporary file: ' . error_get_last()['message']);
        }
    }

    /**
     * @param resource $target
     *
     * @throws DB2Exception
     */
460
    private function writeStringToStream(string $string, $target): void
461 462 463 464 465
    {
        if (@fwrite($target, $string) === false) {
            throw new DB2Exception('Could not write string to temporary file: ' . error_get_last()['message']);
        }
    }
466
}