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 15
use IteratorAggregate;
use PDO;
use ReflectionClass;
use ReflectionObject;
use ReflectionProperty;
use stdClass;
use const CASE_LOWER;
16
use const DB2_BINARY;
17 18
use const DB2_CHAR;
use const DB2_LONG;
19
use const DB2_PARAM_FILE;
20 21 22 23 24 25 26 27 28 29 30 31 32
use const DB2_PARAM_IN;
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;
33 34
use function error_get_last;
use function fclose;
35 36
use function func_get_args;
use function func_num_args;
37
use function fwrite;
38 39
use function gettype;
use function is_object;
40
use function is_resource;
41 42 43
use function is_string;
use function ksort;
use function sprintf;
44 45
use function stream_copy_to_stream;
use function stream_get_meta_data;
46
use function strtolower;
47
use function tmpfile;
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 312
                    $rows[] = $row;
                }
                break;
313
            case FetchMode::COLUMN:
Sergei Morozov's avatar
Sergei Morozov committed
314
                while (($row = $this->fetchColumn()) !== false) {
315 316 317 318
                    $rows[] = $row;
                }
                break;
            default:
Sergei Morozov's avatar
Sergei Morozov committed
319
                while (($row = $this->fetch($fetchMode)) !== false) {
320 321
                    $rows[] = $row;
                }
322
        }
Benjamin Morel's avatar
Benjamin Morel committed
323

324 325 326 327
        return $rows;
    }

    /**
328
     * {@inheritdoc}
329
     */
330
    public function fetchColumn($columnIndex = 0)
331
    {
332
        $row = $this->fetch(FetchMode::NUMERIC);
333

334
        if ($row === false) {
335
            return false;
336
        }
Benjamin Morel's avatar
Benjamin Morel committed
337

338
        return $row[$columnIndex] ?? null;
339 340 341
    }

    /**
342
     * {@inheritdoc}
343
     */
344
    public function rowCount()
345
    {
346
        return @db2_num_rows($this->stmt) ? : 0;
347
    }
348 349 350 351

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

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

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

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

385
            // Try to find a case-matching property.
386 387 388 389 390
            if ($destinationClassReflection->hasProperty($name)) {
                $destinationProperty = $destinationClassReflection->getProperty($name);

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

                continue;
393
            }
394 395 396 397 398 399 400 401 402 403 404 405 406 407 408

            $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;
409 410 411 412
        }

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

    /**
     * @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']);
        }
    }
454
}