Failed Conditions
Pull Request — develop (#3348)
by Sergei
38:24 queued 35:29
created

DB2Statement::createTemporaryFile()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
eloc 4
dl 0
loc 9
ccs 0
cts 0
cp 0
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 0
crap 6
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
    public function __construct($stmt)
83
    {
84
        $this->stmt = $stmt;
85
    }
86
87
    /**
88
     * {@inheritdoc}
89
     */
90
    public function bindValue($param, $value, $type = ParameterType::STRING) : bool
91
    {
92
        return $this->bindParam($param, $value, $type);
93
    }
94
95
    /**
96
     * {@inheritdoc}
97
     */
98
    public function bindParam($column, &$variable, $type = ParameterType::STRING, $length = null) : bool
99
    {
100
        switch ($type) {
101
            case ParameterType::INTEGER:
102
                $this->bind($column, $variable, DB2_PARAM_IN, DB2_LONG);
103
                break;
104
105
            case ParameterType::LARGE_OBJECT:
106
                if (isset($this->lobs[$column])) {
107
                    [, $handle] = $this->lobs[$column];
108
                    fclose($handle);
109
                }
110
111
                $handle = $this->createTemporaryFile();
112
                $path   = stream_get_meta_data($handle)['uri'];
113
114
                $this->bind($column, $path, DB2_PARAM_FILE, DB2_BINARY);
115
116
                $this->lobs[$column] = [&$variable, $handle];
117
                break;
118
119
            default:
120
                $this->bind($column, $variable, DB2_PARAM_IN, DB2_CHAR);
121
                break;
122
        }
123
124
        return true;
125
    }
126
127
    /**
128
     * @param int|string $parameter Parameter position or name
129
     * @param mixed      $variable
130
     *
131
     * @throws DB2Exception
132
     */
133
    private function bind($parameter, &$variable, int $parameterType, int $dataType) : void
134
    {
135
        $this->bindParam[$parameter] =& $variable;
136
137
        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
    }
141
142
    /**
143
     * {@inheritdoc}
144
     */
145
    public function closeCursor() : void
146
    {
147
        if (! $this->stmt) {
148
            return;
149
        }
150
151
        $this->bindParam = [];
152
        $this->result    = false;
153
154
        db2_free_result($this->stmt);
155
    }
156
157
    /**
158
     * {@inheritdoc}
159
     */
160
    public function columnCount() : int
161
    {
162
        return db2_num_fields($this->stmt);
163
    }
164
165
    /**
166
     * {@inheritdoc}
167
     */
168
    public function errorCode()
169
    {
170
        return db2_stmt_error();
171
    }
172
173
    /**
174
     * {@inheritdoc}
175
     */
176
    public function errorInfo() : array
177
    {
178
        return [
179
            db2_stmt_errormsg(),
180
            db2_stmt_error(),
181
        ];
182
    }
183
184
    /**
185
     * {@inheritdoc}
186
     */
187
    public function execute(?array $params = null) : bool
188
    {
189
        if (! $this->stmt) {
190
            return false;
191
        }
192
193
        if ($params === null) {
194
            ksort($this->bindParam);
195
196
            $params = [];
197
198
            foreach ($this->bindParam as $column => $value) {
199
                $params[] = $value;
200
            }
201
        }
202
203
        foreach ($this->lobs as [$source, $target]) {
204
            if (is_resource($source)) {
205
                $this->copyStreamToStream($source, $target);
206
207
                continue;
208
            }
209
210
            $this->writeStringToStream($source, $target);
211
        }
212
213
        $retval = db2_execute($this->stmt, $params);
214
215
        foreach ($this->lobs as [, $handle]) {
216
            fclose($handle);
217
        }
218
219
        $this->lobs = [];
220
221
        if ($retval === false) {
222
            throw new DB2Exception(db2_stmt_errormsg());
223
        }
224
225
        $this->result = true;
226
227
        return $retval;
228
    }
229
230
    /**
231
     * {@inheritdoc}
232
     */
233
    public function setFetchMode(int $fetchMode, ...$args) : bool
234
    {
235
        $this->defaultFetchMode = $fetchMode;
236
237
        if (isset($args[0])) {
238
            $this->defaultFetchClass = $args[0];
239
        }
240
241
        if (isset($args[1])) {
242
            $this->defaultFetchClassCtorArgs = (array) $args[2];
243
        }
244
245
        return true;
246
    }
247
248
    /**
249
     * {@inheritdoc}
250
     */
251
    public function getIterator()
252
    {
253
        return new StatementIterator($this);
254
    }
255
256
    /**
257
     * {@inheritdoc}
258
     */
259
    public function fetch(?int $fetchMode = null, ...$args)
260
    {
261
        // do not try fetching from the statement if it's not expected to contain result
262
        // in order to prevent exceptional situation
263
        if (! $this->result) {
264
            return false;
265
        }
266
267
        $fetchMode = $fetchMode ?: $this->defaultFetchMode;
268
        switch ($fetchMode) {
269
            case FetchMode::COLUMN:
270
                return $this->fetchColumn();
271
272
            case FetchMode::MIXED:
273
                return db2_fetch_both($this->stmt);
274
275
            case FetchMode::ASSOCIATIVE:
276
                return db2_fetch_assoc($this->stmt);
277
278
            case FetchMode::CUSTOM_OBJECT:
279
                $className = $this->defaultFetchClass;
280
                $ctorArgs  = $this->defaultFetchClassCtorArgs;
281
282
                if (count($args) > 0) {
283
                    $className = $args[0];
284
                    $ctorArgs  = $args[1] ?? [];
285
                }
286
287
                $result = db2_fetch_object($this->stmt);
288
289
                if ($result instanceof stdClass) {
290
                    $result = $this->castObject($result, $className, $ctorArgs);
291
                }
292
293
                return $result;
294
295
            case FetchMode::NUMERIC:
296
                return db2_fetch_array($this->stmt);
297
298
            case FetchMode::STANDARD_OBJECT:
299
                return db2_fetch_object($this->stmt);
300
301
            default:
302
                throw new DB2Exception('Given Fetch-Style ' . $fetchMode . ' is not supported.');
303
        }
304
    }
305
306
    /**
307
     * {@inheritdoc}
308
     */
309
    public function fetchAll(?int $fetchMode = null, ...$args) : array
310
    {
311
        $rows = [];
312
313
        switch ($fetchMode) {
314
            case FetchMode::CUSTOM_OBJECT:
315
                while (($row = $this->fetch($fetchMode, ...$args)) !== false) {
316
                    $rows[] = $row;
317
                }
318
                break;
319
            case FetchMode::COLUMN:
320
                while (($row = $this->fetchColumn()) !== false) {
321
                    $rows[] = $row;
322
                }
323
                break;
324
            default:
325
                while (($row = $this->fetch($fetchMode)) !== false) {
326
                    $rows[] = $row;
327
                }
328
        }
329
330
        return $rows;
331
    }
332
333
    /**
334
     * {@inheritdoc}
335
     */
336
    public function fetchColumn($columnIndex = 0)
337
    {
338
        $row = $this->fetch(FetchMode::NUMERIC);
339
340
        if ($row === false) {
341
            return false;
342
        }
343
344
        return $row[$columnIndex] ?? null;
345
    }
346
347
    /**
348
     * {@inheritdoc}
349
     */
350
    public function rowCount() : int
351
    {
352
        return @db2_num_rows($this->stmt) ? : 0;
353
    }
354
355
    /**
356
     * Casts a stdClass object to the given class name mapping its' properties.
357
     *
358
     * @param stdClass      $sourceObject     Object to cast from.
359
     * @param string|object $destinationClass Name of the class or class instance to cast to.
360
     * @param mixed[]       $ctorArgs         Arguments to use for constructing the destination class instance.
361
     *
362
     * @throws DB2Exception
363
     */
364
    private function castObject(stdClass $sourceObject, $destinationClass, array $ctorArgs = []) : object
365
    {
366
        if (! is_string($destinationClass)) {
367
            if (! is_object($destinationClass)) {
368
                throw new DB2Exception(sprintf(
369
                    'Destination class has to be of type string or object, %s given.',
370
                    gettype($destinationClass)
371
                ));
372
            }
373
        } else {
374
            $destinationClass = new ReflectionClass($destinationClass);
375
            $destinationClass = $destinationClass->newInstanceArgs($ctorArgs);
376
        }
377
378
        $sourceReflection           = new ReflectionObject($sourceObject);
379
        $destinationClassReflection = new ReflectionObject($destinationClass);
380
        /** @var ReflectionProperty[] $destinationProperties */
381
        $destinationProperties = array_change_key_case($destinationClassReflection->getProperties(), CASE_LOWER);
382
383
        foreach ($sourceReflection->getProperties() as $sourceProperty) {
384
            $sourceProperty->setAccessible(true);
385
386
            $name  = $sourceProperty->getName();
387
            $value = $sourceProperty->getValue($sourceObject);
388
389
            // Try to find a case-matching property.
390
            if ($destinationClassReflection->hasProperty($name)) {
391
                $destinationProperty = $destinationClassReflection->getProperty($name);
392
393
                $destinationProperty->setAccessible(true);
394
                $destinationProperty->setValue($destinationClass, $value);
395
396
                continue;
397
            }
398
399
            $name = strtolower($name);
400
401
            // Try to find a property without matching case.
402
            // Fallback for the driver returning either all uppercase or all lowercase column names.
403
            if (isset($destinationProperties[$name])) {
404
                $destinationProperty = $destinationProperties[$name];
405
406
                $destinationProperty->setAccessible(true);
407
                $destinationProperty->setValue($destinationClass, $value);
408
409
                continue;
410
            }
411
412
            $destinationClass->$name = $value;
413
        }
414
415
        return $destinationClass;
416
    }
417
418
    /**
419
     * @return resource
420
     *
421
     * @throws DB2Exception
422
     */
423
    private function createTemporaryFile()
424
    {
425
        $handle = @tmpfile();
426
427
        if ($handle === false) {
428
            throw new DB2Exception('Could not create temporary file: ' . error_get_last()['message']);
429
        }
430
431
        return $handle;
432
    }
433
434
    /**
435
     * @param resource $source
436
     * @param resource $target
437
     *
438
     * @throws DB2Exception
439
     */
440
    private function copyStreamToStream($source, $target) : void
441
    {
442
        if (@stream_copy_to_stream($source, $target) === false) {
443
            throw new DB2Exception('Could not copy source stream to temporary file: ' . error_get_last()['message']);
444
        }
445
    }
446
447
    /**
448
     * @param resource $target
449
     *
450
     * @throws DB2Exception
451
     */
452
    private function writeStringToStream(string $string, $target) : void
453
    {
454
        if (@fwrite($target, $string) === false) {
455
            throw new DB2Exception('Could not write string to temporary file: ' . error_get_last()['message']);
456
        }
457
    }
458
}
459