Completed
Push — master ( 6d673d...7f4ef4 )
by Sergei
40:47 queued 40:43
created

DB2Statement   C

Complexity

Total Complexity 57

Size/Duplication

Total Lines 389
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
wmc 57
eloc 142
dl 0
loc 389
ccs 0
cts 215
cp 0
rs 5.04
c 0
b 0
f 0

17 Methods

Rating   Name   Duplication   Size   Complexity  
A writeStringToStream() 0 4 2
A copyStreamToStream() 0 4 2
A bindParam() 0 26 4
A createTemporaryFile() 0 9 2
B castObject() 0 52 6
A __construct() 0 3 1
A bindValue() 0 3 1
A fetchColumn() 0 13 3
B execute() 0 35 7
A columnCount() 0 3 2
A bind() 0 6 2
A setFetchMode() 0 13 3
A rowCount() 0 3 2
A closeCursor() 0 11 2
A getIterator() 0 3 1
B fetch() 0 44 11
A fetchAll() 0 22 6

How to fix   Complexity   

Complex Class

Complex classes like DB2Statement often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DB2Statement, and based on these observations, apply Extract Interface, too.

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
final 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
     * {@inheritdoc}
133
     */
134
    public function closeCursor() : void
135
    {
136
        $this->bindParam = [];
137
138
        if (! $this->result) {
139
            return;
140
        }
141
142
        db2_free_result($this->stmt);
143
144
        $this->result = false;
145
    }
146
147
    /**
148
     * {@inheritdoc}
149
     */
150
    public function columnCount() : int
151
    {
152
        return db2_num_fields($this->stmt) ?: 0;
153
    }
154
155
    /**
156
     * {@inheritdoc}
157
     */
158
    public function execute(?array $params = null) : void
159
    {
160
        if ($params === null) {
161
            ksort($this->bindParam);
162
163
            $params = [];
164
165
            foreach ($this->bindParam as $column => $value) {
166
                $params[] = $value;
167
            }
168
        }
169
170
        foreach ($this->lobs as [$source, $target]) {
171
            if (is_resource($source)) {
172
                $this->copyStreamToStream($source, $target);
173
174
                continue;
175
            }
176
177
            $this->writeStringToStream($source, $target);
178
        }
179
180
        $retval = db2_execute($this->stmt, $params);
181
182
        foreach ($this->lobs as [, $handle]) {
183
            fclose($handle);
184
        }
185
186
        $this->lobs = [];
187
188
        if ($retval === false) {
189
            throw DB2Exception::fromStatementError($this->stmt);
190
        }
191
192
        $this->result = true;
193
    }
194
195
    /**
196
     * {@inheritdoc}
197
     */
198
    public function setFetchMode(int $fetchMode, ...$args) : void
199
    {
200
        $this->defaultFetchMode = $fetchMode;
201
202
        if (isset($args[0])) {
203
            $this->defaultFetchClass = $args[0];
204
        }
205
206
        if (! isset($args[1])) {
207
            return;
208
        }
209
210
        $this->defaultFetchClassCtorArgs = (array) $args[2];
211
    }
212
213
    /**
214
     * {@inheritdoc}
215
     */
216
    public function getIterator()
217
    {
218
        return new StatementIterator($this);
219
    }
220
221
    /**
222
     * {@inheritdoc}
223
     */
224
    public function fetch(?int $fetchMode = null, ...$args)
225
    {
226
        // do not try fetching from the statement if it's not expected to contain result
227
        // in order to prevent exceptional situation
228
        if (! $this->result) {
229
            return false;
230
        }
231
232
        $fetchMode = $fetchMode ?: $this->defaultFetchMode;
233
        switch ($fetchMode) {
234
            case FetchMode::COLUMN:
235
                return $this->fetchColumn();
236
237
            case FetchMode::MIXED:
238
                return db2_fetch_both($this->stmt);
239
240
            case FetchMode::ASSOCIATIVE:
241
                return db2_fetch_assoc($this->stmt);
242
243
            case FetchMode::CUSTOM_OBJECT:
244
                $className = $this->defaultFetchClass;
245
                $ctorArgs  = $this->defaultFetchClassCtorArgs;
246
247
                if (count($args) > 0) {
248
                    $className = $args[0];
249
                    $ctorArgs  = $args[1] ?? [];
250
                }
251
252
                $result = db2_fetch_object($this->stmt);
253
254
                if ($result instanceof stdClass) {
255
                    $result = $this->castObject($result, $className, $ctorArgs);
256
                }
257
258
                return $result;
259
260
            case FetchMode::NUMERIC:
261
                return db2_fetch_array($this->stmt);
262
263
            case FetchMode::STANDARD_OBJECT:
264
                return db2_fetch_object($this->stmt);
265
266
            default:
267
                throw new DB2Exception('Given Fetch-Style ' . $fetchMode . ' is not supported.');
268
        }
269
    }
270
271
    /**
272
     * {@inheritdoc}
273
     */
274
    public function fetchAll(?int $fetchMode = null, ...$args) : array
275
    {
276
        $rows = [];
277
278
        switch ($fetchMode) {
279
            case FetchMode::CUSTOM_OBJECT:
280
                while (($row = $this->fetch($fetchMode, ...$args)) !== false) {
281
                    $rows[] = $row;
282
                }
283
                break;
284
            case FetchMode::COLUMN:
285
                while (($row = $this->fetchColumn()) !== false) {
286
                    $rows[] = $row;
287
                }
288
                break;
289
            default:
290
                while (($row = $this->fetch($fetchMode)) !== false) {
291
                    $rows[] = $row;
292
                }
293
        }
294
295
        return $rows;
296
    }
297
298
    /**
299
     * {@inheritdoc}
300
     */
301
    public function fetchColumn(int $columnIndex = 0)
302
    {
303
        $row = $this->fetch(FetchMode::NUMERIC);
304
305
        if ($row === false) {
306
            return false;
307
        }
308
309
        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

309
        if (! array_key_exists($columnIndex, /** @scrutinizer ignore-type */ $row)) {
Loading history...
310
            throw InvalidColumnIndex::new($columnIndex, count($row));
311
        }
312
313
        return $row[$columnIndex];
314
    }
315
316
    /**
317
     * {@inheritdoc}
318
     */
319
    public function rowCount() : int
320
    {
321
        return @db2_num_rows($this->stmt) ? : 0;
322
    }
323
324
    /**
325
     * @param int   $position Parameter position
326
     * @param mixed $variable
327
     *
328
     * @throws DB2Exception
329
     */
330
    private function bind(int $position, &$variable, int $parameterType, int $dataType) : void
331
    {
332
        $this->bindParam[$position] =& $variable;
333
334
        if (! db2_bind_param($this->stmt, $position, 'variable', $parameterType, $dataType)) {
335
            throw DB2Exception::fromStatementError($this->stmt);
336
        }
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