Completed
Pull Request — master (#3885)
by Grégoire
37:52 queued 34:57
created

DB2Statement::closeCursor()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2.0625

Importance

Changes 0
Metric Value
eloc 5
dl 0
loc 11
ccs 6
cts 8
cp 0.75
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 0
crap 2.0625
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 function array_change_key_case;
18
use function array_key_exists;
19
use function assert;
20
use function count;
21
use function db2_bind_param;
22
use function db2_execute;
23
use function db2_fetch_array;
24
use function db2_fetch_assoc;
25
use function db2_fetch_both;
26
use function db2_fetch_object;
27
use function db2_free_result;
28
use function db2_num_fields;
29
use function db2_num_rows;
30
use function error_get_last;
31
use function fclose;
32
use function fwrite;
33
use function gettype;
34
use function is_int;
35
use function is_object;
36
use function is_resource;
37
use function is_string;
38
use function ksort;
39
use function sprintf;
40
use function stream_copy_to_stream;
41
use function stream_get_meta_data;
42
use function strtolower;
43
use function tmpfile;
44
use const CASE_LOWER;
45
use const DB2_BINARY;
46
use const DB2_CHAR;
47
use const DB2_LONG;
48
use const DB2_PARAM_FILE;
49
use const DB2_PARAM_IN;
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 311
    public function __construct($stmt)
87
    {
88 311
        $this->stmt = $stmt;
89 311
    }
90
91
    /**
92
     * {@inheritdoc}
93
     */
94 35
    public function bindValue($param, $value, int $type = ParameterType::STRING) : void
95
    {
96 35
        $this->bindParam($param, $value, $type);
97 35
    }
98
99
    /**
100
     * {@inheritdoc}
101
     */
102 59
    public function bindParam($param, &$variable, int $type = ParameterType::STRING, ?int $length = null) : void
103
    {
104 59
        assert(is_int($param));
105
106 59
        switch ($type) {
107
            case ParameterType::INTEGER:
108 39
                $this->bind($param, $variable, DB2_PARAM_IN, DB2_LONG);
109 39
                break;
110
111
            case ParameterType::LARGE_OBJECT:
112 7
                if (isset($this->lobs[$param])) {
113
                    [, $handle] = $this->lobs[$param];
114
                    fclose($handle);
115
                }
116
117 7
                $handle = $this->createTemporaryFile();
118 7
                $path   = stream_get_meta_data($handle)['uri'];
119
120 7
                $this->bind($param, $path, DB2_PARAM_FILE, DB2_BINARY);
121
122 7
                $this->lobs[$param] = [&$variable, $handle];
123 7
                break;
124
125
            default:
126 36
                $this->bind($param, $variable, DB2_PARAM_IN, DB2_CHAR);
127 36
                break;
128
        }
129 59
    }
130
131 19
    public function closeCursor() : void
132
    {
133 19
        $this->bindParam = [];
134
135 19
        if (! $this->result) {
136 3
            return;
137
        }
138
139 16
        db2_free_result($this->stmt);
140
141 16
        $this->result = false;
142 16
    }
143
144 4
    public function columnCount() : int
145
    {
146 4
        return db2_num_fields($this->stmt) ?: 0;
147
    }
148
149
    /**
150
     * {@inheritdoc}
151
     */
152 304
    public function execute(?array $params = null) : void
153
    {
154 304
        if ($params === null) {
155 282
            ksort($this->bindParam);
156
157 282
            $params = [];
158
159 282
            foreach ($this->bindParam as $column => $value) {
160 59
                $params[] = $value;
161
            }
162
        }
163
164 304
        foreach ($this->lobs as [$source, $target]) {
165 7
            if (is_resource($source)) {
166 3
                $this->copyStreamToStream($source, $target);
167
168 3
                continue;
169
            }
170
171 5
            $this->writeStringToStream($source, $target);
172
        }
173
174 304
        $retval = db2_execute($this->stmt, $params);
175
176 299
        foreach ($this->lobs as [, $handle]) {
177 7
            fclose($handle);
178
        }
179
180 299
        $this->lobs = [];
181
182 299
        if ($retval === false) {
183
            throw DB2Exception::fromStatementError($this->stmt);
184
        }
185
186 299
        $this->result = true;
187 299
    }
188
189
    /**
190
     * {@inheritdoc}
191
     */
192 302
    public function setFetchMode(int $fetchMode, ...$args) : void
193
    {
194 302
        $this->defaultFetchMode = $fetchMode;
195
196 302
        if (isset($args[0])) {
197 2
            $this->defaultFetchClass = $args[0];
198
        }
199
200 302
        if (! isset($args[1])) {
201 302
            return;
202
        }
203
204
        $this->defaultFetchClassCtorArgs = (array) $args[2];
205
    }
206
207
    /**
208
     * {@inheritdoc}
209
     */
210 1
    public function getIterator()
211
    {
212 1
        return new StatementIterator($this);
213
    }
214
215
    /**
216
     * {@inheritdoc}
217
     */
218 290
    public function fetch(?int $fetchMode = null, ...$args)
219
    {
220
        // do not try fetching from the statement if it's not expected to contain result
221
        // in order to prevent exceptional situation
222 290
        if (! $this->result) {
223 9
            return false;
224
        }
225
226 281
        $fetchMode = $fetchMode ?: $this->defaultFetchMode;
227 281
        switch ($fetchMode) {
228
            case FetchMode::COLUMN:
229 1
                return $this->fetchColumn();
230
231
            case FetchMode::MIXED:
232 2
                return db2_fetch_both($this->stmt);
233
234
            case FetchMode::ASSOCIATIVE:
235 143
                return db2_fetch_assoc($this->stmt);
236
237
            case FetchMode::CUSTOM_OBJECT:
238 3
                $className = $this->defaultFetchClass;
239 3
                $ctorArgs  = $this->defaultFetchClassCtorArgs;
240
241 3
                if (count($args) > 0) {
242 1
                    $className = $args[0];
243 1
                    $ctorArgs  = $args[1] ?? [];
244
                }
245
246 3
                $result = db2_fetch_object($this->stmt);
247
248 3
                if ($result instanceof stdClass) {
249 3
                    $result = $this->castObject($result, $className, $ctorArgs);
250
                }
251
252 3
                return $result;
253
254
            case FetchMode::NUMERIC:
255 189
                return db2_fetch_array($this->stmt);
256
257
            case FetchMode::STANDARD_OBJECT:
258 1
                return db2_fetch_object($this->stmt);
259
260
            default:
261
                throw new DB2Exception('Given Fetch-Style ' . $fetchMode . ' is not supported.');
262
        }
263
    }
264
265
    /**
266
     * {@inheritdoc}
267
     */
268 105
    public function fetchAll(?int $fetchMode = null, ...$args) : array
269
    {
270 105
        $rows = [];
271
272 105
        switch ($fetchMode) {
273
            case FetchMode::CUSTOM_OBJECT:
274 1
                while (($row = $this->fetch($fetchMode, ...$args)) !== false) {
275 1
                    $rows[] = $row;
276
                }
277
278 1
                break;
279
280
            case FetchMode::COLUMN:
281 10
                while (($row = $this->fetchColumn()) !== false) {
282 10
                    $rows[] = $row;
283
                }
284
285 10
                break;
286
287
            default:
288 94
                while (($row = $this->fetch($fetchMode)) !== false) {
289 86
                    $rows[] = $row;
290
                }
291
        }
292
293 105
        return $rows;
294
    }
295
296
    /**
297
     * {@inheritdoc}
298
     */
299 183
    public function fetchColumn(int $columnIndex = 0)
300
    {
301 183
        $row = $this->fetch(FetchMode::NUMERIC);
302
303 183
        if ($row === false) {
304 16
            return false;
305
        }
306
307 177
        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

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