Failed Conditions
Push — develop ( 152bc9...e39bc0 )
by Sergei
102:42 queued 37:39
created

DB2Statement::createTemporaryFile()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 2.5

Importance

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