Completed
Push — develop ( 361a2b...a96e4b )
by Marco
20s queued 14s
created

DB2Statement   C

Complexity

Total Complexity 57

Size/Duplication

Total Lines 389
Duplicated Lines 0 %

Test Coverage

Coverage 54.88%

Importance

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

17 Methods

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

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

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