Completed
Push — develop ( fa42c1...0ef7d4 )
by Sergei
22:52
created

DB2Statement   F

Complexity

Total Complexity 60

Size/Duplication

Total Lines 420
Duplicated Lines 0 %

Test Coverage

Coverage 51.93%

Importance

Changes 0
Metric Value
wmc 60
eloc 151
dl 0
loc 420
ccs 121
cts 233
cp 0.5193
rs 3.6
c 0
b 0
f 0

19 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
A bindValue() 0 3 1
B execute() 0 41 8
A columnCount() 0 7 2
A bind() 0 6 2
A setFetchMode() 0 13 3
A fetchColumn() 0 9 2
A bindParam() 0 27 4
A rowCount() 0 3 2
A closeCursor() 0 15 3
A writeStringToStream() 0 4 2
A errorCode() 0 3 1
A createTemporaryFile() 0 9 2
B castObject() 0 52 6
A getIterator() 0 3 1
B fetch() 0 44 11
A errorInfo() 0 5 1
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
namespace Doctrine\DBAL\Driver\IBMDB2;
4
5
use Doctrine\DBAL\Driver\Statement;
6
use Doctrine\DBAL\Driver\StatementIterator;
7
use Doctrine\DBAL\FetchMode;
8
use Doctrine\DBAL\ParameterType;
9
use IteratorAggregate;
10
use ReflectionClass;
11
use ReflectionObject;
12
use ReflectionProperty;
13
use stdClass;
14
use const CASE_LOWER;
15
use const DB2_BINARY;
16
use const DB2_CHAR;
17
use const DB2_LONG;
18
use const DB2_PARAM_FILE;
19
use const DB2_PARAM_IN;
20
use function array_change_key_case;
21
use function count;
22
use function db2_bind_param;
23
use function db2_execute;
24
use function db2_fetch_array;
25
use function db2_fetch_assoc;
26
use function db2_fetch_both;
27
use function db2_fetch_object;
28
use function db2_free_result;
29
use function db2_num_fields;
30
use function db2_num_rows;
31
use function db2_stmt_error;
32
use function db2_stmt_errormsg;
33
use function error_get_last;
34
use function fclose;
35
use function fwrite;
36
use function gettype;
37
use function is_object;
38
use function is_resource;
39
use function is_string;
40
use function ksort;
41
use function sprintf;
42
use function stream_copy_to_stream;
43
use function stream_get_meta_data;
44
use function strtolower;
45
use function tmpfile;
46
47
class DB2Statement implements IteratorAggregate, Statement
48
{
49
    /** @var resource */
50
    private $stmt;
51
52
    /** @var mixed[] */
53
    private $bindParam = [];
54
55
    /**
56
     * Map of LOB parameter positions to the tuples containing reference to the variable bound to the driver statement
57
     * and the temporary file handle bound to the underlying statement
58
     *
59
     * @var mixed[][]
60
     */
61
    private $lobs = [];
62
63
    /** @var string Name of the default class to instantiate when fetching class instances. */
64
    private $defaultFetchClass = '\stdClass';
65
66
    /** @var mixed[] Constructor arguments for the default class to instantiate when fetching class instances. */
67
    private $defaultFetchClassCtorArgs = [];
68
69
    /** @var int */
70
    private $defaultFetchMode = FetchMode::MIXED;
71
72
    /**
73
     * Indicates whether the statement is in the state when fetching results is possible
74
     *
75
     * @var bool
76
     */
77
    private $result = false;
78
79
    /**
80
     * @param resource $stmt
81
     */
82 229
    public function __construct($stmt)
83
    {
84 229
        $this->stmt = $stmt;
85 229
    }
86
87
    /**
88
     * {@inheritdoc}
89
     */
90 36
    public function bindValue($param, $value, $type = ParameterType::STRING)
91
    {
92 36
        return $this->bindParam($param, $value, $type);
93
    }
94
95
    /**
96
     * {@inheritdoc}
97
     */
98 44
    public function bindParam($column, &$variable, $type = ParameterType::STRING, $length = null)
99
    {
100 44
        switch ($type) {
101
            case ParameterType::INTEGER:
102 24
                $this->bind($column, $variable, DB2_PARAM_IN, DB2_LONG);
103 24
                break;
104
105
            case ParameterType::LARGE_OBJECT:
106 7
                if (isset($this->lobs[$column])) {
107
                    [, $handle] = $this->lobs[$column];
108
                    fclose($handle);
109
                }
110
111 7
                $handle = $this->createTemporaryFile();
112 7
                $path   = stream_get_meta_data($handle)['uri'];
113
114 7
                $this->bind($column, $path, DB2_PARAM_FILE, DB2_BINARY);
115
116 7
                $this->lobs[$column] = [&$variable, $handle];
117 7
                break;
118
119
            default:
120 37
                $this->bind($column, $variable, DB2_PARAM_IN, DB2_CHAR);
121 37
                break;
122
        }
123
124 44
        return true;
125
    }
126
127
    /**
128
     * @param int|string $parameter Parameter position or name
129
     * @param mixed      $variable
130
     *
131
     * @throws DB2Exception
132
     */
133 44
    private function bind($parameter, &$variable, int $parameterType, int $dataType) : void
134
    {
135 44
        $this->bindParam[$parameter] =& $variable;
136
137 44
        if (! db2_bind_param($this->stmt, $parameter, 'variable', $parameterType, $dataType)) {
0 ignored issues
show
Bug introduced by
It seems like $parameter can also be of type string; however, parameter $parameter_number of db2_bind_param() does only seem to accept integer, 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

137
        if (! db2_bind_param($this->stmt, /** @scrutinizer ignore-type */ $parameter, 'variable', $parameterType, $dataType)) {
Loading history...
138
            throw new DB2Exception(db2_stmt_errormsg());
139
        }
140 44
    }
141
142
    /**
143
     * {@inheritdoc}
144
     */
145 19
    public function closeCursor()
146
    {
147 19
        if (! $this->stmt) {
148
            return false;
149
        }
150
151 19
        $this->bindParam = [];
152
153 19
        if (! db2_free_result($this->stmt)) {
154
            return false;
155
        }
156
157 19
        $this->result = false;
158
159 19
        return true;
160
    }
161
162
    /**
163
     * {@inheritdoc}
164
     */
165 4
    public function columnCount()
166
    {
167 4
        if (! $this->stmt) {
168
            return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the return type mandated by Doctrine\DBAL\Driver\Res...tatement::columnCount() of integer.

In the issue above, the returned value is violating the contract defined by the mentioned interface.

Let's take a look at an example:

interface HasName {
    /** @return string */
    public function getName();
}

class Name {
    public $name;
}

class User implements HasName {
    /** @return string|Name */
    public function getName() {
        return new Name('foo'); // This is a violation of the ``HasName`` interface
                                // which only allows a string value to be returned.
    }
}
Loading history...
169
        }
170
171 4
        return db2_num_fields($this->stmt);
172
    }
173
174
    /**
175
     * {@inheritdoc}
176
     */
177
    public function errorCode()
178
    {
179
        return db2_stmt_error();
180
    }
181
182
    /**
183
     * {@inheritdoc}
184
     */
185
    public function errorInfo()
186
    {
187
        return [
188
            db2_stmt_errormsg(),
189
            db2_stmt_error(),
190
        ];
191
    }
192
193
    /**
194
     * {@inheritdoc}
195
     */
196 221
    public function execute($params = null)
197
    {
198 221
        if (! $this->stmt) {
199
            return false;
200
        }
201
202 221
        if ($params === null) {
203 198
            ksort($this->bindParam);
204
205 198
            $params = [];
206
207 198
            foreach ($this->bindParam as $column => $value) {
208 44
                $params[] = $value;
209
            }
210
        }
211
212 221
        foreach ($this->lobs as [$source, $target]) {
213 7
            if (is_resource($source)) {
214 3
                $this->copyStreamToStream($source, $target);
215
216 3
                continue;
217
            }
218
219 5
            $this->writeStringToStream($source, $target);
220
        }
221
222 221
        $retval = db2_execute($this->stmt, $params);
223
224 216
        foreach ($this->lobs as [, $handle]) {
225 7
            fclose($handle);
226
        }
227
228 216
        $this->lobs = [];
229
230 216
        if ($retval === false) {
231
            throw new DB2Exception(db2_stmt_errormsg());
232
        }
233
234 216
        $this->result = true;
235
236 216
        return $retval;
237
    }
238
239
    /**
240
     * {@inheritdoc}
241
     */
242 220
    public function setFetchMode($fetchMode, ...$args)
243
    {
244 220
        $this->defaultFetchMode = $fetchMode;
245
246 220
        if (isset($args[0])) {
247 2
            $this->defaultFetchClass = $args[0];
248
        }
249
250 220
        if (isset($args[1])) {
251
            $this->defaultFetchClassCtorArgs = (array) $args[2];
252
        }
253
254 220
        return true;
255
    }
256
257
    /**
258
     * {@inheritdoc}
259
     */
260 3
    public function getIterator()
261
    {
262 3
        return new StatementIterator($this);
263
    }
264
265
    /**
266
     * {@inheritdoc}
267
     */
268 207
    public function fetch($fetchMode = null, ...$args)
269
    {
270
        // do not try fetching from the statement if it's not expected to contain result
271
        // in order to prevent exceptional situation
272 207
        if (! $this->result) {
273 9
            return false;
274
        }
275
276 198
        $fetchMode = $fetchMode ?: $this->defaultFetchMode;
277 198
        switch ($fetchMode) {
278
            case FetchMode::COLUMN:
279 1
                return $this->fetchColumn();
280
281
            case FetchMode::MIXED:
282 2
                return db2_fetch_both($this->stmt);
283
284
            case FetchMode::ASSOCIATIVE:
285 134
                return db2_fetch_assoc($this->stmt);
286
287
            case FetchMode::CUSTOM_OBJECT:
288 3
                $className = $this->defaultFetchClass;
289 3
                $ctorArgs  = $this->defaultFetchClassCtorArgs;
290
291 3
                if (count($args) > 0) {
292 1
                    $className = $args[0];
293 1
                    $ctorArgs  = $args[1] ?? [];
294
                }
295
296 3
                $result = db2_fetch_object($this->stmt);
297
298 3
                if ($result instanceof stdClass) {
299 3
                    $result = $this->castObject($result, $className, $ctorArgs);
300
                }
301
302 3
                return $result;
303
304
            case FetchMode::NUMERIC:
305 60
                return db2_fetch_array($this->stmt);
306
307
            case FetchMode::STANDARD_OBJECT:
308 1
                return db2_fetch_object($this->stmt);
309
310
            default:
311
                throw new DB2Exception('Given Fetch-Style ' . $fetchMode . ' is not supported.');
312
        }
313
    }
314
315
    /**
316
     * {@inheritdoc}
317
     */
318 93
    public function fetchAll($fetchMode = null, ...$args)
319
    {
320 93
        $rows = [];
321
322 93
        switch ($fetchMode) {
323
            case FetchMode::CUSTOM_OBJECT:
324 1
                while (($row = $this->fetch($fetchMode, ...$args)) !== false) {
325 1
                    $rows[] = $row;
326
                }
327 1
                break;
328
            case FetchMode::COLUMN:
329 9
                while (($row = $this->fetchColumn()) !== false) {
330 9
                    $rows[] = $row;
331
                }
332 9
                break;
333
            default:
334 83
                while (($row = $this->fetch($fetchMode)) !== false) {
335 76
                    $rows[] = $row;
336
                }
337
        }
338
339 93
        return $rows;
340
    }
341
342
    /**
343
     * {@inheritdoc}
344
     */
345 57
    public function fetchColumn($columnIndex = 0)
346
    {
347 57
        $row = $this->fetch(FetchMode::NUMERIC);
348
349 57
        if ($row === false) {
350 15
            return false;
351
        }
352
353 51
        return $row[$columnIndex] ?? null;
354
    }
355
356
    /**
357
     * {@inheritdoc}
358
     */
359 85
    public function rowCount() : int
360
    {
361 85
        return @db2_num_rows($this->stmt) ? : 0;
362
    }
363
364
    /**
365
     * Casts a stdClass object to the given class name mapping its' properties.
366
     *
367
     * @param stdClass      $sourceObject     Object to cast from.
368
     * @param string|object $destinationClass Name of the class or class instance to cast to.
369
     * @param mixed[]       $ctorArgs         Arguments to use for constructing the destination class instance.
370
     *
371
     * @return object
372
     *
373
     * @throws DB2Exception
374
     */
375 3
    private function castObject(stdClass $sourceObject, $destinationClass, array $ctorArgs = [])
376
    {
377 3
        if (! is_string($destinationClass)) {
378
            if (! is_object($destinationClass)) {
379
                throw new DB2Exception(sprintf(
380
                    'Destination class has to be of type string or object, %s given.',
381
                    gettype($destinationClass)
382
                ));
383
            }
384
        } else {
385 3
            $destinationClass = new ReflectionClass($destinationClass);
386 3
            $destinationClass = $destinationClass->newInstanceArgs($ctorArgs);
387
        }
388
389 3
        $sourceReflection           = new ReflectionObject($sourceObject);
390 3
        $destinationClassReflection = new ReflectionObject($destinationClass);
391
        /** @var ReflectionProperty[] $destinationProperties */
392 3
        $destinationProperties = array_change_key_case($destinationClassReflection->getProperties(), CASE_LOWER);
393
394 3
        foreach ($sourceReflection->getProperties() as $sourceProperty) {
395 3
            $sourceProperty->setAccessible(true);
396
397 3
            $name  = $sourceProperty->getName();
398 3
            $value = $sourceProperty->getValue($sourceObject);
399
400
            // Try to find a case-matching property.
401 3
            if ($destinationClassReflection->hasProperty($name)) {
402
                $destinationProperty = $destinationClassReflection->getProperty($name);
403
404
                $destinationProperty->setAccessible(true);
405
                $destinationProperty->setValue($destinationClass, $value);
406
407
                continue;
408
            }
409
410 3
            $name = strtolower($name);
411
412
            // Try to find a property without matching case.
413
            // Fallback for the driver returning either all uppercase or all lowercase column names.
414 3
            if (isset($destinationProperties[$name])) {
415
                $destinationProperty = $destinationProperties[$name];
416
417
                $destinationProperty->setAccessible(true);
418
                $destinationProperty->setValue($destinationClass, $value);
419
420
                continue;
421
            }
422
423 3
            $destinationClass->$name = $value;
424
        }
425
426 3
        return $destinationClass;
427
    }
428
429
    /**
430
     * @return resource
431
     *
432
     * @throws DB2Exception
433
     */
434 7
    private function createTemporaryFile()
435
    {
436 7
        $handle = @tmpfile();
437
438 7
        if ($handle === false) {
439
            throw new DB2Exception('Could not create temporary file: ' . error_get_last()['message']);
440
        }
441
442 7
        return $handle;
443
    }
444
445
    /**
446
     * @param resource $source
447
     * @param resource $target
448
     *
449
     * @throws DB2Exception
450
     */
451 3
    private function copyStreamToStream($source, $target) : void
452
    {
453 3
        if (@stream_copy_to_stream($source, $target) === false) {
454
            throw new DB2Exception('Could not copy source stream to temporary file: ' . error_get_last()['message']);
455
        }
456 3
    }
457
458
    /**
459
     * @param resource $target
460
     *
461
     * @throws DB2Exception
462
     */
463 5
    private function writeStringToStream(string $string, $target) : void
464
    {
465 5
        if (@fwrite($target, $string) === false) {
466
            throw new DB2Exception('Could not write string to temporary file: ' . error_get_last()['message']);
467
        }
468 5
    }
469
}
470