Completed
Push — master ( 1eba78...18908c )
by Sergei
62:05 queued 10s
created

src/Driver/IBMDB2/DB2Statement.php (1 issue)

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

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