1<?php
2
3namespace Doctrine\DBAL\Driver\IBMDB2;
4
5use Doctrine\DBAL\Driver\FetchUtils;
6use Doctrine\DBAL\Driver\IBMDB2\Exception\CannotCopyStreamToStream;
7use Doctrine\DBAL\Driver\IBMDB2\Exception\CannotCreateTemporaryFile;
8use Doctrine\DBAL\Driver\IBMDB2\Exception\CannotWriteToTemporaryFile;
9use Doctrine\DBAL\Driver\IBMDB2\Exception\StatementError;
10use Doctrine\DBAL\Driver\Result;
11use Doctrine\DBAL\Driver\Statement as StatementInterface;
12use Doctrine\DBAL\Driver\StatementIterator;
13use Doctrine\DBAL\FetchMode;
14use Doctrine\DBAL\ParameterType;
15use IteratorAggregate;
16use PDO;
17use ReflectionClass;
18use ReflectionObject;
19use ReflectionProperty;
20use ReturnTypeWillChange;
21use stdClass;
22
23use function array_change_key_case;
24use function assert;
25use function db2_bind_param;
26use function db2_execute;
27use function db2_fetch_array;
28use function db2_fetch_assoc;
29use function db2_fetch_both;
30use function db2_fetch_object;
31use function db2_free_result;
32use function db2_num_fields;
33use function db2_num_rows;
34use function db2_stmt_error;
35use function db2_stmt_errormsg;
36use function error_get_last;
37use function fclose;
38use function func_get_args;
39use function func_num_args;
40use function fwrite;
41use function gettype;
42use function is_int;
43use function is_object;
44use function is_resource;
45use function is_string;
46use function ksort;
47use function sprintf;
48use function stream_copy_to_stream;
49use function stream_get_meta_data;
50use function strtolower;
51use function tmpfile;
52
53use const CASE_LOWER;
54use const DB2_BINARY;
55use const DB2_CHAR;
56use const DB2_LONG;
57use const DB2_PARAM_FILE;
58use const DB2_PARAM_IN;
59
60/**
61 * @deprecated Use {@link Statement} instead
62 */
63class DB2Statement implements IteratorAggregate, StatementInterface, Result
64{
65    /** @var resource */
66    private $stmt;
67
68    /** @var mixed[] */
69    private $bindParam = [];
70
71    /**
72     * Map of LOB parameter positions to the tuples containing reference to the variable bound to the driver statement
73     * and the temporary file handle bound to the underlying statement
74     *
75     * @var mixed[][]
76     */
77    private $lobs = [];
78
79    /** @var string Name of the default class to instantiate when fetching class instances. */
80    private $defaultFetchClass = '\stdClass';
81
82    /** @var mixed[] Constructor arguments for the default class to instantiate when fetching class instances. */
83    private $defaultFetchClassCtorArgs = [];
84
85    /** @var int */
86    private $defaultFetchMode = FetchMode::MIXED;
87
88    /**
89     * Indicates whether the statement is in the state when fetching results is possible
90     *
91     * @var bool
92     */
93    private $result = false;
94
95    /**
96     * @internal The statement can be only instantiated by its driver connection.
97     *
98     * @param resource $stmt
99     */
100    public function __construct($stmt)
101    {
102        $this->stmt = $stmt;
103    }
104
105    /**
106     * {@inheritdoc}
107     */
108    public function bindValue($param, $value, $type = ParameterType::STRING)
109    {
110        assert(is_int($param));
111
112        return $this->bindParam($param, $value, $type);
113    }
114
115    /**
116     * {@inheritdoc}
117     */
118    public function bindParam($param, &$variable, $type = ParameterType::STRING, $length = null)
119    {
120        assert(is_int($param));
121
122        switch ($type) {
123            case ParameterType::INTEGER:
124                $this->bind($param, $variable, DB2_PARAM_IN, DB2_LONG);
125                break;
126
127            case ParameterType::LARGE_OBJECT:
128                if (isset($this->lobs[$param])) {
129                    [, $handle] = $this->lobs[$param];
130                    fclose($handle);
131                }
132
133                $handle = $this->createTemporaryFile();
134                $path   = stream_get_meta_data($handle)['uri'];
135
136                $this->bind($param, $path, DB2_PARAM_FILE, DB2_BINARY);
137
138                $this->lobs[$param] = [&$variable, $handle];
139                break;
140
141            default:
142                $this->bind($param, $variable, DB2_PARAM_IN, DB2_CHAR);
143                break;
144        }
145
146        return true;
147    }
148
149    /**
150     * @param int   $position Parameter position
151     * @param mixed $variable
152     *
153     * @throws DB2Exception
154     */
155    private function bind($position, &$variable, int $parameterType, int $dataType): void
156    {
157        $this->bindParam[$position] =& $variable;
158
159        if (! db2_bind_param($this->stmt, $position, 'variable', $parameterType, $dataType)) {
160            throw StatementError::new($this->stmt);
161        }
162    }
163
164    /**
165     * {@inheritdoc}
166     *
167     * @deprecated Use free() instead.
168     */
169    public function closeCursor()
170    {
171        $this->bindParam = [];
172
173        if (! db2_free_result($this->stmt)) {
174            return false;
175        }
176
177        $this->result = false;
178
179        return true;
180    }
181
182    /**
183     * {@inheritdoc}
184     */
185    public function columnCount()
186    {
187        return db2_num_fields($this->stmt) ?: 0;
188    }
189
190    /**
191     * {@inheritdoc}
192     *
193     * @deprecated The error information is available via exceptions.
194     */
195    public function errorCode()
196    {
197        return db2_stmt_error();
198    }
199
200    /**
201     * {@inheritdoc}
202     *
203     * @deprecated The error information is available via exceptions.
204     */
205    public function errorInfo()
206    {
207        return [
208            db2_stmt_errormsg(),
209            db2_stmt_error(),
210        ];
211    }
212
213    /**
214     * {@inheritdoc}
215     */
216    public function execute($params = null)
217    {
218        if ($params === null) {
219            ksort($this->bindParam);
220
221            $params = [];
222
223            foreach ($this->bindParam as $column => $value) {
224                $params[] = $value;
225            }
226        }
227
228        foreach ($this->lobs as [$source, $target]) {
229            if (is_resource($source)) {
230                $this->copyStreamToStream($source, $target);
231
232                continue;
233            }
234
235            $this->writeStringToStream($source, $target);
236        }
237
238        $retval = db2_execute($this->stmt, $params);
239
240        foreach ($this->lobs as [, $handle]) {
241            fclose($handle);
242        }
243
244        $this->lobs = [];
245
246        if ($retval === false) {
247            throw StatementError::new($this->stmt);
248        }
249
250        $this->result = true;
251
252        return $retval;
253    }
254
255    /**
256     * {@inheritdoc}
257     *
258     * @deprecated Use one of the fetch- or iterate-related methods.
259     */
260    public function setFetchMode($fetchMode, $arg2 = null, $arg3 = null)
261    {
262        $this->defaultFetchMode          = $fetchMode;
263        $this->defaultFetchClass         = $arg2 ?: $this->defaultFetchClass;
264        $this->defaultFetchClassCtorArgs = $arg3 ? (array) $arg3 : $this->defaultFetchClassCtorArgs;
265
266        return true;
267    }
268
269    /**
270     * {@inheritdoc}
271     *
272     * @deprecated Use iterateNumeric(), iterateAssociative() or iterateColumn() instead.
273     */
274    #[ReturnTypeWillChange]
275    public function getIterator()
276    {
277        return new StatementIterator($this);
278    }
279
280    /**
281     * {@inheritdoc}
282     *
283     * @deprecated Use fetchNumeric(), fetchAssociative() or fetchOne() instead.
284     */
285    public function fetch($fetchMode = null, $cursorOrientation = PDO::FETCH_ORI_NEXT, $cursorOffset = 0)
286    {
287        // do not try fetching from the statement if it's not expected to contain result
288        // in order to prevent exceptional situation
289        if (! $this->result) {
290            return false;
291        }
292
293        $fetchMode = $fetchMode ?: $this->defaultFetchMode;
294        switch ($fetchMode) {
295            case FetchMode::COLUMN:
296                return $this->fetchColumn();
297
298            case FetchMode::MIXED:
299                return db2_fetch_both($this->stmt);
300
301            case FetchMode::ASSOCIATIVE:
302                return db2_fetch_assoc($this->stmt);
303
304            case FetchMode::CUSTOM_OBJECT:
305                $className = $this->defaultFetchClass;
306                $ctorArgs  = $this->defaultFetchClassCtorArgs;
307
308                if (func_num_args() >= 2) {
309                    $args      = func_get_args();
310                    $className = $args[1];
311                    $ctorArgs  = $args[2] ?? [];
312                }
313
314                $result = db2_fetch_object($this->stmt);
315
316                if ($result instanceof stdClass) {
317                    $result = $this->castObject($result, $className, $ctorArgs);
318                }
319
320                return $result;
321
322            case FetchMode::NUMERIC:
323                return db2_fetch_array($this->stmt);
324
325            case FetchMode::STANDARD_OBJECT:
326                return db2_fetch_object($this->stmt);
327
328            default:
329                throw new DB2Exception('Given Fetch-Style ' . $fetchMode . ' is not supported.');
330        }
331    }
332
333    /**
334     * {@inheritdoc}
335     *
336     * @deprecated Use fetchAllNumeric(), fetchAllAssociative() or fetchFirstColumn() instead.
337     */
338    public function fetchAll($fetchMode = null, $fetchArgument = null, $ctorArgs = null)
339    {
340        $rows = [];
341
342        switch ($fetchMode) {
343            case FetchMode::CUSTOM_OBJECT:
344                while (($row = $this->fetch(...func_get_args())) !== false) {
345                    $rows[] = $row;
346                }
347
348                break;
349
350            case FetchMode::COLUMN:
351                while (($row = $this->fetchColumn()) !== false) {
352                    $rows[] = $row;
353                }
354
355                break;
356
357            default:
358                while (($row = $this->fetch($fetchMode)) !== false) {
359                    $rows[] = $row;
360                }
361        }
362
363        return $rows;
364    }
365
366    /**
367     * {@inheritdoc}
368     *
369     * @deprecated Use fetchOne() instead.
370     */
371    public function fetchColumn($columnIndex = 0)
372    {
373        $row = $this->fetch(FetchMode::NUMERIC);
374
375        if ($row === false) {
376            return false;
377        }
378
379        return $row[$columnIndex] ?? null;
380    }
381
382    /**
383     * {@inheritDoc}
384     */
385    public function fetchNumeric()
386    {
387        if (! $this->result) {
388            return false;
389        }
390
391        return db2_fetch_array($this->stmt);
392    }
393
394    /**
395     * {@inheritdoc}
396     */
397    public function fetchAssociative()
398    {
399        // do not try fetching from the statement if it's not expected to contain the result
400        // in order to prevent exceptional situation
401        if (! $this->result) {
402            return false;
403        }
404
405        return db2_fetch_assoc($this->stmt);
406    }
407
408    /**
409     * {@inheritdoc}
410     */
411    public function fetchOne()
412    {
413        return FetchUtils::fetchOne($this);
414    }
415
416    /**
417     * {@inheritdoc}
418     */
419    public function fetchAllNumeric(): array
420    {
421        return FetchUtils::fetchAllNumeric($this);
422    }
423
424    /**
425     * {@inheritdoc}
426     */
427    public function fetchAllAssociative(): array
428    {
429        return FetchUtils::fetchAllAssociative($this);
430    }
431
432    /**
433     * {@inheritdoc}
434     */
435    public function fetchFirstColumn(): array
436    {
437        return FetchUtils::fetchFirstColumn($this);
438    }
439
440    /**
441     * {@inheritdoc}
442     */
443    public function rowCount()
444    {
445        return @db2_num_rows($this->stmt) ? : 0;
446    }
447
448    public function free(): void
449    {
450        $this->bindParam = [];
451
452        db2_free_result($this->stmt);
453
454        $this->result = false;
455    }
456
457    /**
458     * Casts a stdClass object to the given class name mapping its' properties.
459     *
460     * @param stdClass            $sourceObject     Object to cast from.
461     * @param class-string|object $destinationClass Name of the class or class instance to cast to.
462     * @param mixed[]             $ctorArgs         Arguments to use for constructing the destination class instance.
463     *
464     * @return object
465     *
466     * @throws DB2Exception
467     */
468    private function castObject(stdClass $sourceObject, $destinationClass, array $ctorArgs = [])
469    {
470        if (! is_string($destinationClass)) {
471            if (! is_object($destinationClass)) {
472                throw new DB2Exception(sprintf(
473                    'Destination class has to be of type string or object, %s given.',
474                    gettype($destinationClass)
475                ));
476            }
477        } else {
478            $destinationClass = new ReflectionClass($destinationClass);
479            $destinationClass = $destinationClass->newInstanceArgs($ctorArgs);
480        }
481
482        $sourceReflection           = new ReflectionObject($sourceObject);
483        $destinationClassReflection = new ReflectionObject($destinationClass);
484        /** @var ReflectionProperty[] $destinationProperties */
485        $destinationProperties = array_change_key_case($destinationClassReflection->getProperties(), CASE_LOWER);
486
487        foreach ($sourceReflection->getProperties() as $sourceProperty) {
488            $sourceProperty->setAccessible(true);
489
490            $name  = $sourceProperty->getName();
491            $value = $sourceProperty->getValue($sourceObject);
492
493            // Try to find a case-matching property.
494            if ($destinationClassReflection->hasProperty($name)) {
495                $destinationProperty = $destinationClassReflection->getProperty($name);
496
497                $destinationProperty->setAccessible(true);
498                $destinationProperty->setValue($destinationClass, $value);
499
500                continue;
501            }
502
503            $name = strtolower($name);
504
505            // Try to find a property without matching case.
506            // Fallback for the driver returning either all uppercase or all lowercase column names.
507            if (isset($destinationProperties[$name])) {
508                $destinationProperty = $destinationProperties[$name];
509
510                $destinationProperty->setAccessible(true);
511                $destinationProperty->setValue($destinationClass, $value);
512
513                continue;
514            }
515
516            $destinationClass->$name = $value;
517        }
518
519        return $destinationClass;
520    }
521
522    /**
523     * @return resource
524     *
525     * @throws DB2Exception
526     */
527    private function createTemporaryFile()
528    {
529        $handle = @tmpfile();
530
531        if ($handle === false) {
532            throw CannotCreateTemporaryFile::new(error_get_last());
533        }
534
535        return $handle;
536    }
537
538    /**
539     * @param resource $source
540     * @param resource $target
541     *
542     * @throws DB2Exception
543     */
544    private function copyStreamToStream($source, $target): void
545    {
546        if (@stream_copy_to_stream($source, $target) === false) {
547            throw CannotCopyStreamToStream::new(error_get_last());
548        }
549    }
550
551    /**
552     * @param resource $target
553     *
554     * @throws DB2Exception
555     */
556    private function writeStringToStream(string $string, $target): void
557    {
558        if (@fwrite($target, $string) === false) {
559            throw CannotWriteToTemporaryFile::new(error_get_last());
560        }
561    }
562}
563