Failed Conditions
Push — develop ( b4c2cf...86369f )
by Sergei
18s queued 13s
created

DB2Statement::getIterator()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 3
ccs 0
cts 3
cp 0
rs 10
c 0
b 0
f 0
cc 1
eloc 1
nc 1
nop 0
crap 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\DBAL\Driver\IBMDB2;
6
7
use Doctrine\DBAL\Driver\Statement;
8
use Doctrine\DBAL\Driver\StatementIterator;
9
use Doctrine\DBAL\Exception\InvalidColumnIndex;
10
use Doctrine\DBAL\FetchMode;
11
use Doctrine\DBAL\ParameterType;
12
use IteratorAggregate;
13
use ReflectionClass;
14
use ReflectionObject;
15
use ReflectionProperty;
16
use stdClass;
17
use const CASE_LOWER;
18
use const DB2_BINARY;
19
use const DB2_CHAR;
20
use const DB2_LONG;
21
use const DB2_PARAM_FILE;
22
use const DB2_PARAM_IN;
23
use function array_change_key_case;
24
use function array_key_exists;
25
use function assert;
26
use function count;
27
use function db2_bind_param;
28
use function db2_execute;
29
use function db2_fetch_array;
30
use function db2_fetch_assoc;
31
use function db2_fetch_both;
32
use function db2_fetch_object;
33
use function db2_free_result;
34
use function db2_num_fields;
35
use function db2_num_rows;
36
use function error_get_last;
37
use function fclose;
38
use function fwrite;
39
use function gettype;
40
use function is_int;
41
use function is_object;
42
use function is_resource;
43
use function is_string;
44
use function ksort;
45
use function sprintf;
46
use function stream_copy_to_stream;
47
use function stream_get_meta_data;
48
use function strtolower;
49
use function tmpfile;
50
51
class DB2Statement implements IteratorAggregate, Statement
52
{
53
    /** @var resource */
54
    private $stmt;
55
56
    /** @var mixed[] */
57
    private $bindParam = [];
58
59
    /**
60
     * Map of LOB parameter positions to the tuples containing reference to the variable bound to the driver statement
61
     * and the temporary file handle bound to the underlying statement
62
     *
63
     * @var mixed[][]
64
     */
65
    private $lobs = [];
66
67
    /** @var string Name of the default class to instantiate when fetching class instances. */
68
    private $defaultFetchClass = '\stdClass';
69
70
    /** @var mixed[] Constructor arguments for the default class to instantiate when fetching class instances. */
71
    private $defaultFetchClassCtorArgs = [];
72
73
    /** @var int */
74
    private $defaultFetchMode = FetchMode::MIXED;
75
76
    /**
77
     * Indicates whether the statement is in the state when fetching results is possible
78
     *
79
     * @var bool
80
     */
81
    private $result = false;
82
83
    /**
84
     * @param resource $stmt
85
     */
86
    public function __construct($stmt)
87
    {
88
        $this->stmt = $stmt;
89
    }
90
91
    /**
92
     * {@inheritdoc}
93
     */
94
    public function bindValue($param, $value, int $type = ParameterType::STRING) : void
95
    {
96
        $this->bindParam($param, $value, $type);
97
    }
98
99
    /**
100
     * {@inheritdoc}
101
     */
102
    public function bindParam($param, &$variable, int $type = ParameterType::STRING, ?int $length = null) : void
103
    {
104
        assert(is_int($param));
105
106
        switch ($type) {
107
            case ParameterType::INTEGER:
108
                $this->bind($param, $variable, DB2_PARAM_IN, DB2_LONG);
109
                break;
110
111
            case ParameterType::LARGE_OBJECT:
112
                if (isset($this->lobs[$param])) {
113
                    [, $handle] = $this->lobs[$param];
114
                    fclose($handle);
115
                }
116
117
                $handle = $this->createTemporaryFile();
118
                $path   = stream_get_meta_data($handle)['uri'];
119
120
                $this->bind($param, $path, DB2_PARAM_FILE, DB2_BINARY);
121
122
                $this->lobs[$param] = [&$variable, $handle];
123
                break;
124
125
            default:
126
                $this->bind($param, $variable, DB2_PARAM_IN, DB2_CHAR);
127
                break;
128
        }
129
    }
130
131
    /**
132
     * @param int   $position Parameter position
133
     * @param mixed $variable
134
     *
135
     * @throws DB2Exception
136
     */
137
    private function bind(int $position, &$variable, int $parameterType, int $dataType) : void
138
    {
139
        $this->bindParam[$position] =& $variable;
140
141
        if (! db2_bind_param($this->stmt, $position, 'variable', $parameterType, $dataType)) {
142
            throw DB2Exception::fromStatementError($this->stmt);
143
        }
144
    }
145
146
    /**
147
     * {@inheritdoc}
148
     */
149
    public function closeCursor() : void
150
    {
151
        $this->bindParam = [];
152
153
        if (! $this->result) {
154
            return;
155
        }
156
157
        db2_free_result($this->stmt);
158
159
        $this->result = false;
160
    }
161
162
    /**
163
     * {@inheritdoc}
164
     */
165
    public function columnCount() : int
166
    {
167
        return db2_num_fields($this->stmt) ?: 0;
168
    }
169
170
    /**
171
     * {@inheritdoc}
172
     */
173
    public function execute(?array $params = null) : void
174
    {
175
        if ($params === null) {
176
            ksort($this->bindParam);
177
178
            $params = [];
179
180
            foreach ($this->bindParam as $column => $value) {
181
                $params[] = $value;
182
            }
183
        }
184
185
        foreach ($this->lobs as [$source, $target]) {
186
            if (is_resource($source)) {
187
                $this->copyStreamToStream($source, $target);
188
189
                continue;
190
            }
191
192
            $this->writeStringToStream($source, $target);
193
        }
194
195
        $retval = db2_execute($this->stmt, $params);
196
197
        foreach ($this->lobs as [, $handle]) {
198
            fclose($handle);
199
        }
200
201
        $this->lobs = [];
202
203
        if ($retval === false) {
204
            throw DB2Exception::fromStatementError($this->stmt);
205
        }
206
207
        $this->result = true;
208
    }
209
210
    /**
211
     * {@inheritdoc}
212
     */
213
    public function setFetchMode(int $fetchMode, ...$args) : void
214
    {
215
        $this->defaultFetchMode = $fetchMode;
216
217
        if (isset($args[0])) {
218
            $this->defaultFetchClass = $args[0];
219
        }
220
221
        if (! isset($args[1])) {
222
            return;
223
        }
224
225
        $this->defaultFetchClassCtorArgs = (array) $args[2];
226
    }
227
228
    /**
229
     * {@inheritdoc}
230
     */
231
    public function getIterator()
232
    {
233
        return new StatementIterator($this);
234
    }
235
236
    /**
237
     * {@inheritdoc}
238
     */
239
    public function fetch(?int $fetchMode = null, ...$args)
240
    {
241
        // do not try fetching from the statement if it's not expected to contain result
242
        // in order to prevent exceptional situation
243
        if (! $this->result) {
244
            return false;
245
        }
246
247
        $fetchMode = $fetchMode ?: $this->defaultFetchMode;
248
        switch ($fetchMode) {
249
            case FetchMode::COLUMN:
250
                return $this->fetchColumn();
251
252
            case FetchMode::MIXED:
253
                return db2_fetch_both($this->stmt);
254
255
            case FetchMode::ASSOCIATIVE:
256
                return db2_fetch_assoc($this->stmt);
257
258
            case FetchMode::CUSTOM_OBJECT:
259
                $className = $this->defaultFetchClass;
260
                $ctorArgs  = $this->defaultFetchClassCtorArgs;
261
262
                if (count($args) > 0) {
263
                    $className = $args[0];
264
                    $ctorArgs  = $args[1] ?? [];
265
                }
266
267
                $result = db2_fetch_object($this->stmt);
268
269
                if ($result instanceof stdClass) {
270
                    $result = $this->castObject($result, $className, $ctorArgs);
271
                }
272
273
                return $result;
274
275
            case FetchMode::NUMERIC:
276
                return db2_fetch_array($this->stmt);
277
278
            case FetchMode::STANDARD_OBJECT:
279
                return db2_fetch_object($this->stmt);
280
281
            default:
282
                throw new DB2Exception('Given Fetch-Style ' . $fetchMode . ' is not supported.');
283
        }
284
    }
285
286
    /**
287
     * {@inheritdoc}
288
     */
289
    public function fetchAll(?int $fetchMode = null, ...$args) : array
290
    {
291
        $rows = [];
292
293
        switch ($fetchMode) {
294
            case FetchMode::CUSTOM_OBJECT:
295
                while (($row = $this->fetch($fetchMode, ...$args)) !== false) {
296
                    $rows[] = $row;
297
                }
298
                break;
299
            case FetchMode::COLUMN:
300
                while (($row = $this->fetchColumn()) !== false) {
301
                    $rows[] = $row;
302
                }
303
                break;
304
            default:
305
                while (($row = $this->fetch($fetchMode)) !== false) {
306
                    $rows[] = $row;
307
                }
308
        }
309
310
        return $rows;
311
    }
312
313
    /**
314
     * {@inheritdoc}
315
     */
316
    public function fetchColumn(int $columnIndex = 0)
317
    {
318
        $row = $this->fetch(FetchMode::NUMERIC);
319
320
        if ($row === false) {
321
            return false;
322
        }
323
324
        if (! array_key_exists($columnIndex, $row)) {
0 ignored issues
show
Bug introduced by
It seems like $row can also be of type object; however, parameter $search of array_key_exists() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

324
        if (! array_key_exists($columnIndex, /** @scrutinizer ignore-type */ $row)) {
Loading history...
325
            throw InvalidColumnIndex::new($columnIndex, count($row));
326
        }
327
328
        return $row[$columnIndex];
329
    }
330
331
    /**
332
     * {@inheritdoc}
333
     */
334
    public function rowCount() : int
335
    {
336
        return @db2_num_rows($this->stmt) ? : 0;
337
    }
338
339
    /**
340
     * Casts a stdClass object to the given class name mapping its' properties.
341
     *
342
     * @param stdClass      $sourceObject     Object to cast from.
343
     * @param string|object $destinationClass Name of the class or class instance to cast to.
344
     * @param mixed[]       $ctorArgs         Arguments to use for constructing the destination class instance.
345
     *
346
     * @throws DB2Exception
347
     */
348
    private function castObject(stdClass $sourceObject, $destinationClass, array $ctorArgs = []) : object
349
    {
350
        if (! is_string($destinationClass)) {
351
            if (! is_object($destinationClass)) {
352
                throw new DB2Exception(sprintf(
353
                    'Destination class has to be of type string or object, %s given.',
354
                    gettype($destinationClass)
355
                ));
356
            }
357
        } else {
358
            $destinationClass = new ReflectionClass($destinationClass);
359
            $destinationClass = $destinationClass->newInstanceArgs($ctorArgs);
360
        }
361
362
        $sourceReflection           = new ReflectionObject($sourceObject);
363
        $destinationClassReflection = new ReflectionObject($destinationClass);
364
        /** @var ReflectionProperty[] $destinationProperties */
365
        $destinationProperties = array_change_key_case($destinationClassReflection->getProperties(), CASE_LOWER);
366
367
        foreach ($sourceReflection->getProperties() as $sourceProperty) {
368
            $sourceProperty->setAccessible(true);
369
370
            $name  = $sourceProperty->getName();
371
            $value = $sourceProperty->getValue($sourceObject);
372
373
            // Try to find a case-matching property.
374
            if ($destinationClassReflection->hasProperty($name)) {
375
                $destinationProperty = $destinationClassReflection->getProperty($name);
376
377
                $destinationProperty->setAccessible(true);
378
                $destinationProperty->setValue($destinationClass, $value);
379
380
                continue;
381
            }
382
383
            $name = strtolower($name);
384
385
            // Try to find a property without matching case.
386
            // Fallback for the driver returning either all uppercase or all lowercase column names.
387
            if (isset($destinationProperties[$name])) {
388
                $destinationProperty = $destinationProperties[$name];
389
390
                $destinationProperty->setAccessible(true);
391
                $destinationProperty->setValue($destinationClass, $value);
392
393
                continue;
394
            }
395
396
            $destinationClass->$name = $value;
397
        }
398
399
        return $destinationClass;
400
    }
401
402
    /**
403
     * @return resource
404
     *
405
     * @throws DB2Exception
406
     */
407
    private function createTemporaryFile()
408
    {
409
        $handle = @tmpfile();
410
411
        if ($handle === false) {
412
            throw new DB2Exception('Could not create temporary file: ' . error_get_last()['message']);
413
        }
414
415
        return $handle;
416
    }
417
418
    /**
419
     * @param resource $source
420
     * @param resource $target
421
     *
422
     * @throws DB2Exception
423
     */
424
    private function copyStreamToStream($source, $target) : void
425
    {
426
        if (@stream_copy_to_stream($source, $target) === false) {
427
            throw new DB2Exception('Could not copy source stream to temporary file: ' . error_get_last()['message']);
428
        }
429
    }
430
431
    /**
432
     * @param resource $target
433
     *
434
     * @throws DB2Exception
435
     */
436
    private function writeStringToStream(string $string, $target) : void
437
    {
438
        if (@fwrite($target, $string) === false) {
439
            throw new DB2Exception('Could not write string to temporary file: ' . error_get_last()['message']);
440
        }
441
    }
442
}
443