1<?php
2
3namespace Doctrine\DBAL\Driver\IBMDB2;
4
5use Doctrine\DBAL\Driver\Statement;
6use Doctrine\DBAL\Driver\StatementIterator;
7use Doctrine\DBAL\FetchMode;
8use Doctrine\DBAL\ParameterType;
9use IteratorAggregate;
10use PDO;
11use ReflectionClass;
12use ReflectionObject;
13use ReflectionProperty;
14use stdClass;
15
16use function array_change_key_case;
17use function assert;
18use function db2_bind_param;
19use function db2_execute;
20use function db2_fetch_array;
21use function db2_fetch_assoc;
22use function db2_fetch_both;
23use function db2_fetch_object;
24use function db2_free_result;
25use function db2_num_fields;
26use function db2_num_rows;
27use function db2_stmt_error;
28use function db2_stmt_errormsg;
29use function error_get_last;
30use function fclose;
31use function func_get_args;
32use function func_num_args;
33use function fwrite;
34use function gettype;
35use function is_int;
36use function is_object;
37use function is_resource;
38use function is_string;
39use function ksort;
40use function sprintf;
41use function stream_copy_to_stream;
42use function stream_get_meta_data;
43use function strtolower;
44use function tmpfile;
45
46use const CASE_LOWER;
47use const DB2_BINARY;
48use const DB2_CHAR;
49use const DB2_LONG;
50use const DB2_PARAM_FILE;
51use const DB2_PARAM_IN;
52
53class DB2Statement implements IteratorAggregate, Statement
54{
55    /** @var resource */
56    private $stmt;
57
58    /** @var mixed[] */
59    private $bindParam = [];
60
61    /**
62     * Map of LOB parameter positions to the tuples containing reference to the variable bound to the driver statement
63     * and the temporary file handle bound to the underlying statement
64     *
65     * @var mixed[][]
66     */
67    private $lobs = [];
68
69    /** @var string Name of the default class to instantiate when fetching class instances. */
70    private $defaultFetchClass = '\stdClass';
71
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     * Indicates whether the statement is in the state when fetching results is possible
80     *
81     * @var bool
82     */
83    private $result = false;
84
85    /**
86     * @param resource $stmt
87     */
88    public function __construct($stmt)
89    {
90        $this->stmt = $stmt;
91    }
92
93    /**
94     * {@inheritdoc}
95     */
96    public function bindValue($param, $value, $type = ParameterType::STRING)
97    {
98        assert(is_int($param));
99
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        switch ($type) {
111            case ParameterType::INTEGER:
112                $this->bind($param, $variable, DB2_PARAM_IN, DB2_LONG);
113                break;
114
115            case ParameterType::LARGE_OBJECT:
116                if (isset($this->lobs[$param])) {
117                    [, $handle] = $this->lobs[$param];
118                    fclose($handle);
119                }
120
121                $handle = $this->createTemporaryFile();
122                $path   = stream_get_meta_data($handle)['uri'];
123
124                $this->bind($param, $path, DB2_PARAM_FILE, DB2_BINARY);
125
126                $this->lobs[$param] = [&$variable, $handle];
127                break;
128
129            default:
130                $this->bind($param, $variable, DB2_PARAM_IN, DB2_CHAR);
131                break;
132        }
133
134        return true;
135    }
136
137    /**
138     * @param int   $position Parameter position
139     * @param mixed $variable
140     *
141     * @throws DB2Exception
142     */
143    private function bind($position, &$variable, int $parameterType, int $dataType): void
144    {
145        $this->bindParam[$position] =& $variable;
146
147        if (! db2_bind_param($this->stmt, $position, 'variable', $parameterType, $dataType)) {
148            throw new DB2Exception(db2_stmt_errormsg());
149        }
150    }
151
152    /**
153     * {@inheritdoc}
154     */
155    public function closeCursor()
156    {
157        $this->bindParam = [];
158
159        if (! db2_free_result($this->stmt)) {
160            return false;
161        }
162
163        $this->result = false;
164
165        return true;
166    }
167
168    /**
169     * {@inheritdoc}
170     */
171    public function columnCount()
172    {
173        return db2_num_fields($this->stmt) ?: 0;
174    }
175
176    /**
177     * {@inheritdoc}
178     */
179    public function errorCode()
180    {
181        return db2_stmt_error();
182    }
183
184    /**
185     * {@inheritdoc}
186     */
187    public function errorInfo()
188    {
189        return [
190            db2_stmt_errormsg(),
191            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                $params[] = $value;
207            }
208        }
209
210        foreach ($this->lobs as [$source, $target]) {
211            if (is_resource($source)) {
212                $this->copyStreamToStream($source, $target);
213
214                continue;
215            }
216
217            $this->writeStringToStream($source, $target);
218        }
219
220        $retval = db2_execute($this->stmt, $params);
221
222        foreach ($this->lobs as [, $handle]) {
223            fclose($handle);
224        }
225
226        $this->lobs = [];
227
228        if ($retval === false) {
229            throw new DB2Exception(db2_stmt_errormsg());
230        }
231
232        $this->result = true;
233
234        return $retval;
235    }
236
237    /**
238     * {@inheritdoc}
239     */
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;
245
246        return true;
247    }
248
249    /**
250     * {@inheritdoc}
251     */
252    public function getIterator()
253    {
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        // do not try fetching from the statement if it's not expected to contain result
263        // in order to prevent exceptional situation
264        if (! $this->result) {
265            return false;
266        }
267
268        $fetchMode = $fetchMode ?: $this->defaultFetchMode;
269        switch ($fetchMode) {
270            case FetchMode::COLUMN:
271                return $this->fetchColumn();
272
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                $className = $this->defaultFetchClass;
281                $ctorArgs  = $this->defaultFetchClassCtorArgs;
282
283                if (func_num_args() >= 2) {
284                    $args      = func_get_args();
285                    $className = $args[1];
286                    $ctorArgs  = $args[2] ?? [];
287                }
288
289                $result = db2_fetch_object($this->stmt);
290
291                if ($result instanceof stdClass) {
292                    $result = $this->castObject($result, $className, $ctorArgs);
293                }
294
295                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:
317                while (($row = $this->fetch(...func_get_args())) !== false) {
318                    $rows[] = $row;
319                }
320
321                break;
322
323            case FetchMode::COLUMN:
324                while (($row = $this->fetchColumn()) !== false) {
325                    $rows[] = $row;
326                }
327
328                break;
329
330            default:
331                while (($row = $this->fetch($fetchMode)) !== false) {
332                    $rows[] = $row;
333                }
334        }
335
336        return $rows;
337    }
338
339    /**
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        }
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     * Casts a stdClass object to the given class name mapping its' properties.
363     *
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     * @return object
369     *
370     * @throws DB2Exception
371     */
372    private function castObject(stdClass $sourceObject, $destinationClass, array $ctorArgs = [])
373    {
374        if (! is_string($destinationClass)) {
375            if (! is_object($destinationClass)) {
376                throw new DB2Exception(sprintf(
377                    'Destination class has to be of type string or object, %s given.',
378                    gettype($destinationClass)
379                ));
380            }
381        } else {
382            $destinationClass = new ReflectionClass($destinationClass);
383            $destinationClass = $destinationClass->newInstanceArgs($ctorArgs);
384        }
385
386        $sourceReflection           = new ReflectionObject($sourceObject);
387        $destinationClassReflection = new ReflectionObject($destinationClass);
388        /** @var ReflectionProperty[] $destinationProperties */
389        $destinationProperties = array_change_key_case($destinationClassReflection->getProperties(), CASE_LOWER);
390
391        foreach ($sourceReflection->getProperties() as $sourceProperty) {
392            $sourceProperty->setAccessible(true);
393
394            $name  = $sourceProperty->getName();
395            $value = $sourceProperty->getValue($sourceObject);
396
397            // Try to find a case-matching property.
398            if ($destinationClassReflection->hasProperty($name)) {
399                $destinationProperty = $destinationClassReflection->getProperty($name);
400
401                $destinationProperty->setAccessible(true);
402                $destinationProperty->setValue($destinationClass, $value);
403
404                continue;
405            }
406
407            $name = strtolower($name);
408
409            // Try to find a property without matching case.
410            // Fallback for the driver returning either all uppercase or all lowercase column names.
411            if (isset($destinationProperties[$name])) {
412                $destinationProperty = $destinationProperties[$name];
413
414                $destinationProperty->setAccessible(true);
415                $destinationProperty->setValue($destinationClass, $value);
416
417                continue;
418            }
419
420            $destinationClass->$name = $value;
421        }
422
423        return $destinationClass;
424    }
425
426    /**
427     * @return resource
428     *
429     * @throws DB2Exception
430     */
431    private function createTemporaryFile()
432    {
433        $handle = @tmpfile();
434
435        if ($handle === false) {
436            throw new DB2Exception('Could not create temporary file: ' . error_get_last()['message']);
437        }
438
439        return $handle;
440    }
441
442    /**
443     * @param resource $source
444     * @param resource $target
445     *
446     * @throws DB2Exception
447     */
448    private function copyStreamToStream($source, $target): void
449    {
450        if (@stream_copy_to_stream($source, $target) === false) {
451            throw new DB2Exception('Could not copy source stream to temporary file: ' . error_get_last()['message']);
452        }
453    }
454
455    /**
456     * @param resource $target
457     *
458     * @throws DB2Exception
459     */
460    private function writeStringToStream(string $string, $target): void
461    {
462        if (@fwrite($target, $string) === false) {
463            throw new DB2Exception('Could not write string to temporary file: ' . error_get_last()['message']);
464        }
465    }
466}
467