Completed
Pull Request — 2.10.x (#3994)
by Grégoire
11:52
created

DB2Statement::getIterator()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1.037

Importance

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