Completed
Pull Request — develop (#3569)
by Jonathan
22:31 queued 18:18
created

DB2Statement::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1.037

Importance

Changes 0
Metric Value
dl 0
loc 3
ccs 2
cts 3
cp 0.6667
rs 10
c 0
b 0
f 0
cc 1
eloc 1
nc 1
nop 1
crap 1.037
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 234
    public function __construct($stmt)
87
    {
88 234
        $this->stmt = $stmt;
89 234
    }
90
91
    /**
92
     * {@inheritdoc}
93
     */
94 232
    public function bindValue($param, $value, int $type = ParameterType::STRING) : void
95
    {
96 232
        $this->bindParam($param, $value, $type);
97 232
    }
98
99
    /**
100
     * {@inheritdoc}
101
     */
102 232
    public function bindParam($param, &$variable, int $type = ParameterType::STRING, ?int $length = null) : void
103
    {
104 232
        assert(is_int($param));
105
106 232
        switch ($type) {
107
            case ParameterType::INTEGER:
108 232
                $this->bind($param, $variable, DB2_PARAM_IN, DB2_LONG);
109 232
                break;
110
111
            case ParameterType::LARGE_OBJECT:
112 232
                if (isset($this->lobs[$param])) {
113
                    [, $handle] = $this->lobs[$param];
114
                    fclose($handle);
115
                }
116
117 232
                $handle = $this->createTemporaryFile();
118 232
                $path   = stream_get_meta_data($handle)['uri'];
119
120 232
                $this->bind($param, $path, DB2_PARAM_FILE, DB2_BINARY);
121
122 232
                $this->lobs[$param] = [&$variable, $handle];
123 232
                break;
124
125
            default:
126 232
                $this->bind($param, $variable, DB2_PARAM_IN, DB2_CHAR);
127 232
                break;
128
        }
129 232
    }
130
131
    /**
132
     * @param int   $position Parameter position
133
     * @param mixed $variable
134
     *
135
     * @throws DB2Exception
136
     */
137 232
    private function bind(int $position, &$variable, int $parameterType, int $dataType) : void
138
    {
139 232
        $this->bindParam[$position] =& $variable;
140
141 232
        if (! db2_bind_param($this->stmt, $position, 'variable', $parameterType, $dataType)) {
142
            throw DB2Exception::fromStatementError($this->stmt);
143
        }
144 232
    }
145
146
    /**
147
     * {@inheritdoc}
148
     */
149 116
    public function closeCursor() : void
150
    {
151 116
        $this->bindParam = [];
152
153 116
        if (! $this->result) {
154 43
            return;
155
        }
156
157 116
        db2_free_result($this->stmt);
158
159 116
        $this->result = false;
160 116
    }
161
162
    /**
163
     * {@inheritdoc}
164
     */
165 116
    public function columnCount() : int
166
    {
167 116
        return db2_num_fields($this->stmt) ?: 0;
168
    }
169
170
    /**
171
     * {@inheritdoc}
172
     */
173 234
    public function execute(?array $params = null) : void
174
    {
175 234
        if ($params === null) {
176 232
            ksort($this->bindParam);
177
178 232
            $params = [];
179
180 232
            foreach ($this->bindParam as $column => $value) {
181 232
                $params[] = $value;
182
            }
183
        }
184
185 234
        foreach ($this->lobs as [$source, $target]) {
186 232
            if (is_resource($source)) {
187 231
                $this->copyStreamToStream($source, $target);
188
189 231
                continue;
190
            }
191
192 232
            $this->writeStringToStream($source, $target);
193
        }
194
195 234
        $retval = db2_execute($this->stmt, $params);
196
197 234
        foreach ($this->lobs as [, $handle]) {
198 232
            fclose($handle);
199
        }
200
201 234
        $this->lobs = [];
202
203 234
        if ($retval === false) {
204
            throw DB2Exception::fromStatementError($this->stmt);
205
        }
206
207 234
        $this->result = true;
208 234
    }
209
210
    /**
211
     * {@inheritdoc}
212
     */
213 234
    public function setFetchMode(int $fetchMode, ...$args) : void
214
    {
215 234
        $this->defaultFetchMode = $fetchMode;
216
217 234
        if (isset($args[0])) {
218 152
            $this->defaultFetchClass = $args[0];
219
        }
220
221 234
        if (! isset($args[1])) {
222 234
            return;
223
        }
224
225
        $this->defaultFetchClassCtorArgs = (array) $args[2];
226
    }
227
228
    /**
229
     * {@inheritdoc}
230
     */
231 234
    public function getIterator()
232
    {
233 234
        return new StatementIterator($this);
234
    }
235
236
    /**
237
     * {@inheritdoc}
238
     */
239 234
    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 234
        if (! $this->result) {
244 46
            return false;
245
        }
246
247 234
        $fetchMode = $fetchMode ?: $this->defaultFetchMode;
248 234
        switch ($fetchMode) {
249
            case FetchMode::COLUMN:
250 37
                return $this->fetchColumn();
251
252
            case FetchMode::MIXED:
253 214
                return db2_fetch_both($this->stmt);
254
255
            case FetchMode::ASSOCIATIVE:
256 234
                return db2_fetch_assoc($this->stmt);
257
258
            case FetchMode::CUSTOM_OBJECT:
259 154
                $className = $this->defaultFetchClass;
260 154
                $ctorArgs  = $this->defaultFetchClassCtorArgs;
261
262 154
                if (count($args) > 0) {
263 154
                    $className = $args[0];
264 154
                    $ctorArgs  = $args[1] ?? [];
265
                }
266
267 154
                $result = db2_fetch_object($this->stmt);
268
269 154
                if ($result instanceof stdClass) {
270 154
                    $result = $this->castObject($result, $className, $ctorArgs);
271
                }
272
273 154
                return $result;
274
275
            case FetchMode::NUMERIC:
276 231
                return db2_fetch_array($this->stmt);
277
278
            case FetchMode::STANDARD_OBJECT:
279 155
                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 234
    public function fetchAll(?int $fetchMode = null, ...$args) : array
290
    {
291 234
        $rows = [];
292
293 234
        switch ($fetchMode) {
294
            case FetchMode::CUSTOM_OBJECT:
295 154
                while (($row = $this->fetch($fetchMode, ...$args)) !== false) {
296 154
                    $rows[] = $row;
297
                }
298 154
                break;
299
            case FetchMode::COLUMN:
300 231
                while (($row = $this->fetchColumn()) !== false) {
301 231
                    $rows[] = $row;
302
                }
303 231
                break;
304
            default:
305 234
                while (($row = $this->fetch($fetchMode)) !== false) {
306 222
                    $rows[] = $row;
307
                }
308
        }
309
310 234
        return $rows;
311
    }
312
313
    /**
314
     * {@inheritdoc}
315
     */
316 231
    public function fetchColumn(int $columnIndex = 0)
317
    {
318 231
        $row = $this->fetch(FetchMode::NUMERIC);
319
320 231
        if ($row === false) {
321 231
            return false;
322
        }
323
324 231
        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 36
            throw InvalidColumnIndex::new($columnIndex, count($row));
326
        }
327
328 231
        return $row[$columnIndex];
329
    }
330
331
    /**
332
     * {@inheritdoc}
333
     */
334 232
    public function rowCount() : int
335
    {
336 232
        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 154
    private function castObject(stdClass $sourceObject, $destinationClass, array $ctorArgs = []) : object
349
    {
350 154
        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 154
            $destinationClass = new ReflectionClass($destinationClass);
359 154
            $destinationClass = $destinationClass->newInstanceArgs($ctorArgs);
360
        }
361
362 154
        $sourceReflection           = new ReflectionObject($sourceObject);
363 154
        $destinationClassReflection = new ReflectionObject($destinationClass);
364
        /** @var ReflectionProperty[] $destinationProperties */
365 154
        $destinationProperties = array_change_key_case($destinationClassReflection->getProperties(), CASE_LOWER);
366
367 154
        foreach ($sourceReflection->getProperties() as $sourceProperty) {
368 154
            $sourceProperty->setAccessible(true);
369
370 154
            $name  = $sourceProperty->getName();
371 154
            $value = $sourceProperty->getValue($sourceObject);
372
373
            // Try to find a case-matching property.
374 154
            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 154
            $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 154
            if (isset($destinationProperties[$name])) {
388
                $destinationProperty = $destinationProperties[$name];
389
390
                $destinationProperty->setAccessible(true);
391
                $destinationProperty->setValue($destinationClass, $value);
392
393
                continue;
394
            }
395
396 154
            $destinationClass->$name = $value;
397
        }
398
399 154
        return $destinationClass;
400
    }
401
402
    /**
403
     * @return resource
404
     *
405
     * @throws DB2Exception
406
     */
407 232
    private function createTemporaryFile()
408
    {
409 232
        $handle = @tmpfile();
410
411 232
        if ($handle === false) {
412
            throw new DB2Exception('Could not create temporary file: ' . error_get_last()['message']);
413
        }
414
415 232
        return $handle;
416
    }
417
418
    /**
419
     * @param resource $source
420
     * @param resource $target
421
     *
422
     * @throws DB2Exception
423
     */
424 231
    private function copyStreamToStream($source, $target) : void
425
    {
426 231
        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 231
    }
430
431
    /**
432
     * @param resource $target
433
     *
434
     * @throws DB2Exception
435
     */
436 232
    private function writeStringToStream(string $string, $target) : void
437
    {
438 232
        if (@fwrite($target, $string) === false) {
439
            throw new DB2Exception('Could not write string to temporary file: ' . error_get_last()['message']);
440
        }
441 232
    }
442
}
443