DB2Statement.php 12.1 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 17 18 19 20 21 22 23 24 25 26
use function array_change_key_case;
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;
27 28
use function error_get_last;
use function fclose;
29 30
use function func_get_args;
use function func_num_args;
31
use function fwrite;
32 33
use function gettype;
use function is_object;
34
use function is_resource;
35 36 37
use function is_string;
use function ksort;
use function sprintf;
38 39
use function stream_copy_to_stream;
use function stream_get_meta_data;
40
use function strtolower;
41
use function tmpfile;
Grégoire Paris's avatar
Grégoire Paris committed
42 43 44 45 46 47
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;
48

49
class DB2Statement implements IteratorAggregate, Statement
50
{
51
    /** @var resource */
52
    private $stmt;
53

54
    /** @var mixed[] */
55
    private $bindParam = [];
56

57 58 59 60 61 62 63 64
    /**
     * 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 = [];

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

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

71
    /** @var int */
72
    private $defaultFetchMode = FetchMode::MIXED;
73

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

    /**
90
     * {@inheritdoc}
91
     */
92
    public function bindValue($param, $value, $type = ParameterType::STRING)
93
    {
94
        return $this->bindParam($param, $value, $type);
95 96 97
    }

    /**
98
     * {@inheritdoc}
99
     */
100
    public function bindParam($column, &$variable, $type = ParameterType::STRING, $length = null)
101
    {
102 103 104 105
        switch ($type) {
            case ParameterType::INTEGER:
                $this->bind($column, $variable, DB2_PARAM_IN, DB2_LONG);
                break;
106

107 108 109 110 111
            case ParameterType::LARGE_OBJECT:
                if (isset($this->lobs[$column])) {
                    [, $handle] = $this->lobs[$column];
                    fclose($handle);
                }
112

113 114 115 116 117 118 119 120 121 122 123
                $handle = $this->createTemporaryFile();
                $path   = stream_get_meta_data($handle)['uri'];

                $this->bind($column, $path, DB2_PARAM_FILE, DB2_BINARY);

                $this->lobs[$column] = [&$variable, $handle];
                break;

            default:
                $this->bind($column, $variable, DB2_PARAM_IN, DB2_CHAR);
                break;
124
        }
Benjamin Morel's avatar
Benjamin Morel committed
125

126
        return true;
127 128
    }

129
    /**
Sergei Morozov's avatar
Sergei Morozov committed
130 131
     * @param int   $position Parameter position
     * @param mixed $variable
132 133 134
     *
     * @throws DB2Exception
     */
Sergei Morozov's avatar
Sergei Morozov committed
135
    private function bind($position, &$variable, int $parameterType, int $dataType) : void
136
    {
Sergei Morozov's avatar
Sergei Morozov committed
137
        $this->bindParam[$position] =& $variable;
138

Sergei Morozov's avatar
Sergei Morozov committed
139
        if (! db2_bind_param($this->stmt, $position, 'variable', $parameterType, $dataType)) {
140 141 142 143
            throw new DB2Exception(db2_stmt_errormsg());
        }
    }

144
    /**
145
     * {@inheritdoc}
146
     */
147
    public function closeCursor()
148
    {
149
        $this->bindParam = [];
Benjamin Morel's avatar
Benjamin Morel committed
150

151
        if (! db2_free_result($this->stmt)) {
152 153 154 155 156 157
            return false;
        }

        $this->result = false;

        return true;
158 159 160
    }

    /**
161
     * {@inheritdoc}
162
     */
163
    public function columnCount()
164
    {
Sergei Morozov's avatar
Sergei Morozov committed
165
        return db2_num_fields($this->stmt) ?: 0;
166 167 168
    }

    /**
169
     * {@inheritdoc}
170
     */
171
    public function errorCode()
172 173 174 175 176
    {
        return db2_stmt_error();
    }

    /**
177
     * {@inheritdoc}
178
     */
179
    public function errorInfo()
180
    {
181
        return [
182 183
            db2_stmt_errormsg(),
            db2_stmt_error(),
184
        ];
185 186 187
    }

    /**
188
     * {@inheritdoc}
189
     */
190
    public function execute($params = null)
191
    {
192
        if ($params === null) {
193
            ksort($this->bindParam);
194

195
            $params = [];
196

197
            foreach ($this->bindParam as $column => $value) {
198 199
                $params[] = $value;
            }
200
        }
201

202 203 204 205 206 207 208 209 210 211
        foreach ($this->lobs as [$source, $target]) {
            if (is_resource($source)) {
                $this->copyStreamToStream($source, $target);

                continue;
            }

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

212
        $retval = db2_execute($this->stmt, $params);
213

214 215 216 217 218 219
        foreach ($this->lobs as [, $handle]) {
            fclose($handle);
        }

        $this->lobs = [];

220
        if ($retval === false) {
Benjamin Eberlei's avatar
Benjamin Eberlei committed
221
            throw new DB2Exception(db2_stmt_errormsg());
222
        }
Benjamin Morel's avatar
Benjamin Morel committed
223

224 225
        $this->result = true;

226 227 228
        return $retval;
    }

229 230 231
    /**
     * {@inheritdoc}
     */
232
    public function setFetchMode($fetchMode, $arg2 = null, $arg3 = null)
233
    {
234
        $this->defaultFetchMode          = $fetchMode;
235
        $this->defaultFetchClass         = $arg2 ?: $this->defaultFetchClass;
236
        $this->defaultFetchClassCtorArgs = $arg3 ? (array) $arg3 : $this->defaultFetchClassCtorArgs;
Benjamin Morel's avatar
Benjamin Morel committed
237 238

        return true;
239 240 241 242 243 244 245
    }

    /**
     * {@inheritdoc}
     */
    public function getIterator()
    {
246
        return new StatementIterator($this);
247 248
    }

249
    /**
250
     * {@inheritdoc}
251
     */
252
    public function fetch($fetchMode = null, $cursorOrientation = PDO::FETCH_ORI_NEXT, $cursorOffset = 0)
253
    {
254 255
        // do not try fetching from the statement if it's not expected to contain result
        // in order to prevent exceptional situation
256
        if (! $this->result) {
257 258 259
            return false;
        }

260
        $fetchMode = $fetchMode ?: $this->defaultFetchMode;
261
        switch ($fetchMode) {
262
            case FetchMode::COLUMN:
263 264
                return $this->fetchColumn();

265
            case FetchMode::MIXED:
266
                return db2_fetch_both($this->stmt);
267 268

            case FetchMode::ASSOCIATIVE:
269
                return db2_fetch_assoc($this->stmt);
270 271

            case FetchMode::CUSTOM_OBJECT:
272 273 274 275 276 277
                $className = $this->defaultFetchClass;
                $ctorArgs  = $this->defaultFetchClassCtorArgs;

                if (func_num_args() >= 2) {
                    $args      = func_get_args();
                    $className = $args[1];
278
                    $ctorArgs  = $args[2] ?? [];
279 280
                }

281
                $result = db2_fetch_object($this->stmt);
282

283
                if ($result instanceof stdClass) {
284 285 286 287
                    $result = $this->castObject($result, $className, $ctorArgs);
                }

                return $result;
288 289

            case FetchMode::NUMERIC:
290
                return db2_fetch_array($this->stmt);
291 292

            case FetchMode::STANDARD_OBJECT:
293
                return db2_fetch_object($this->stmt);
294

295
            default:
296
                throw new DB2Exception('Given Fetch-Style ' . $fetchMode . ' is not supported.');
297 298 299 300
        }
    }

    /**
301
     * {@inheritdoc}
302
     */
303
    public function fetchAll($fetchMode = null, $fetchArgument = null, $ctorArgs = null)
304
    {
305
        $rows = [];
306 307

        switch ($fetchMode) {
308
            case FetchMode::CUSTOM_OBJECT:
Sergei Morozov's avatar
Sergei Morozov committed
309
                while (($row = $this->fetch(...func_get_args())) !== false) {
310 311
                    $rows[] = $row;
                }
Grégoire Paris's avatar
Grégoire Paris committed
312

313
                break;
Sergei Morozov's avatar
Sergei Morozov committed
314

315
            case FetchMode::COLUMN:
Sergei Morozov's avatar
Sergei Morozov committed
316
                while (($row = $this->fetchColumn()) !== false) {
317 318
                    $rows[] = $row;
                }
Grégoire Paris's avatar
Grégoire Paris committed
319

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

322
            default:
Sergei Morozov's avatar
Sergei Morozov committed
323
                while (($row = $this->fetch($fetchMode)) !== false) {
324 325
                    $rows[] = $row;
                }
326
        }
Benjamin Morel's avatar
Benjamin Morel committed
327

328 329 330 331
        return $rows;
    }

    /**
332
     * {@inheritdoc}
333
     */
334
    public function fetchColumn($columnIndex = 0)
335
    {
336
        $row = $this->fetch(FetchMode::NUMERIC);
337

338
        if ($row === false) {
339
            return false;
340
        }
Benjamin Morel's avatar
Benjamin Morel committed
341

342
        return $row[$columnIndex] ?? null;
343 344 345
    }

    /**
346
     * {@inheritdoc}
347
     */
348
    public function rowCount()
349
    {
350
        return @db2_num_rows($this->stmt) ? : 0;
351
    }
352 353 354 355

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

378 379 380 381
        $sourceReflection           = new ReflectionObject($sourceObject);
        $destinationClassReflection = new ReflectionObject($destinationClass);
        /** @var ReflectionProperty[] $destinationProperties */
        $destinationProperties = array_change_key_case($destinationClassReflection->getProperties(), CASE_LOWER);
382 383 384 385 386 387 388

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

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

389
            // Try to find a case-matching property.
390 391 392 393 394
            if ($destinationClassReflection->hasProperty($name)) {
                $destinationProperty = $destinationClassReflection->getProperty($name);

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

                continue;
397
            }
398 399 400 401 402 403 404 405 406 407 408 409 410 411 412

            $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;
413 414 415 416
        }

        return $destinationClass;
    }
417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457

    /**
     * @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
     */
    private function copyStreamToStream($source, $target) : void
    {
        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
     */
    private function writeStringToStream(string $string, $target) : void
    {
        if (@fwrite($target, $string) === false) {
            throw new DB2Exception('Could not write string to temporary file: ' . error_get_last()['message']);
        }
    }
458
}