InnerResultIterator   A
last analyzed

Complexity

Total Complexity 33

Size/Duplication

Total Lines 324
Duplicated Lines 0 %

Importance

Changes 4
Bugs 1 Features 1
Metric Value
wmc 33
eloc 100
c 4
b 1
f 1
dl 0
loc 324
rs 9.76

15 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 2 1
A getRowCountViaSqlQuery() 0 8 1
A current() 0 3 1
A count() 0 13 4
A offsetSet() 0 3 1
A getQuery() 0 5 1
A executeQuery() 0 9 1
A createInnerResultIterator() 0 15 1
A key() 0 3 1
A offsetExists() 0 3 1
A valid() 0 3 1
A rewind() 0 5 1
A offsetUnset() 0 3 1
D next() 0 84 16
A offsetGet() 0 3 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace TheCodingMachine\TDBM;
6
7
use Doctrine\DBAL\Platforms\AbstractPlatform;
8
use Doctrine\DBAL\Platforms\MySQLPlatform;
9
use Doctrine\DBAL\Result;
10
use Mouf\Database\MagicQuery;
11
use Psr\Log\LoggerInterface;
12
use TheCodingMachine\TDBM\Utils\DbalUtils;
13
14
/*
15
 Copyright (C) 2006-2017 David Négrier - THE CODING MACHINE
16
17
 This program is free software; you can redistribute it and/or modify
18
 it under the terms of the GNU General Public License as published by
19
 the Free Software Foundation; either version 2 of the License, or
20
 (at your option) any later version.
21
22
 This program is distributed in the hope that it will be useful,
23
 but WITHOUT ANY WARRANTY; without even the implied warranty of
24
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
25
 GNU General Public License for more details.
26
27
 You should have received a copy of the GNU General Public License
28
 along with this program; if not, write to the Free Software
29
 Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
30
 */
31
32
/**
33
 * Iterator used to retrieve results.
34
 */
35
class InnerResultIterator implements \Iterator, InnerResultIteratorInterface
36
{
37
    protected ?Result $result = null;
38
39
    /** @var bool */
40
    protected $fetchStarted = false;
41
    /** @var ObjectStorageInterface */
42
    private $objectStorage;
43
    /** @var string|null */
44
    private $className;
45
46
    /** @var TDBMService */
47
    private $tdbmService;
48
    /** @var string */
49
    private $magicSql;
50
    /** @var mixed[] */
51
    private $parameters;
52
    /** @var int|null */
53
    private $limit;
54
    /** @var int|null */
55
    private $offset;
56
    /** @var array[] */
57
    private $columnDescriptors;
58
    /** @var MagicQuery */
59
    private $magicQuery;
60
61
    /**
62
     * The key of the current retrieved object.
63
     *
64
     * @var int
65
     */
66
    protected $key = -1;
67
68
    /** @var AbstractTDBMObject|null */
69
    protected $current = null;
70
71
    /** @var AbstractPlatform */
72
    private $databasePlatform;
73
74
    /** @var LoggerInterface */
75
    private $logger;
76
77
    /** @var int|null */
78
    protected $count = null;
79
80
    final private function __construct()
81
    {
82
    }
83
84
    /**
85
     * @param mixed[] $parameters
86
     * @param array[] $columnDescriptors
87
     */
88
    public static function createInnerResultIterator(string $magicSql, array $parameters, ?int $limit, ?int $offset, array $columnDescriptors, ObjectStorageInterface $objectStorage, ?string $className, TDBMService $tdbmService, MagicQuery $magicQuery, LoggerInterface $logger): self
89
    {
90
        $iterator = new static();
91
        $iterator->magicSql = $magicSql;
92
        $iterator->objectStorage = $objectStorage;
93
        $iterator->className = $className;
94
        $iterator->tdbmService = $tdbmService;
95
        $iterator->parameters = $parameters;
96
        $iterator->limit = $limit;
97
        $iterator->offset = $offset;
98
        $iterator->columnDescriptors = $columnDescriptors;
99
        $iterator->magicQuery = $magicQuery;
100
        $iterator->databasePlatform = $iterator->tdbmService->getConnection()->getDatabasePlatform();
101
        $iterator->logger = $logger;
102
        return $iterator;
103
    }
104
105
    private function getQuery(): string
106
    {
107
        $sql = $this->magicQuery->buildPreparedStatement($this->magicSql, $this->parameters);
108
        $sql = $this->tdbmService->getConnection()->getDatabasePlatform()->modifyLimitQuery($sql, $this->limit, $this->offset);
109
        return $sql;
110
    }
111
112
    protected function executeQuery(): void
113
    {
114
        $sql = $this->getQuery();
115
116
        $this->logger->debug('Running SQL request: '.$sql);
117
118
        $this->result = $this->tdbmService->getConnection()->executeQuery($sql, $this->parameters, DbalUtils::generateTypes($this->parameters));
119
120
        $this->fetchStarted = true;
121
    }
122
123
    /**
124
     * Counts found records (this is the number of records fetched, taking into account the LIMIT and OFFSET settings).
125
     *
126
     * @return int
127
     */
128
    public function count(): int
129
    {
130
        if ($this->count !== null) {
131
            return $this->count;
132
        }
133
134
        if ($this->fetchStarted && $this->tdbmService->getConnection()->getDatabasePlatform() instanceof MySqlPlatform) {
135
            // Optimisation: we don't need a separate "count" SQL request in MySQL.
136
            assert($this->result instanceof Result);
137
            $this->count = (int)$this->result->rowCount();
138
            return $this->count;
139
        }
140
        return $this->getRowCountViaSqlQuery();
141
    }
142
143
    /**
144
     * Makes a separate SQL query to compute the row count.
145
     * (not needed in MySQL if fetch is already done)
146
     */
147
    private function getRowCountViaSqlQuery(): int
148
    {
149
        $countSql = 'SELECT COUNT(1) FROM ('.$this->getQuery().') c';
150
151
        $this->logger->debug('Running count SQL request: '.$countSql);
152
153
        $this->count = (int) $this->tdbmService->getConnection()->fetchOne($countSql, $this->parameters, DbalUtils::generateTypes($this->parameters));
154
        return $this->count;
155
    }
156
157
    /**
158
     * Fetches record at current cursor.
159
     *
160
     * @return AbstractTDBMObject
161
     */
162
    public function current(): mixed
163
    {
164
        return $this->current;
165
    }
166
167
    /**
168
     * Returns the current result's key.
169
     *
170
     * @return int
171
     */
172
    public function key(): mixed
173
    {
174
        return $this->key;
175
    }
176
177
    /**
178
     * Advances the cursor to the next result.
179
     * Casts the database result into one (or several) beans.
180
     */
181
    public function next(): void
182
    {
183
        $row = $this->result->fetchAssociative();
0 ignored issues
show
Bug introduced by
The method fetchAssociative() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

183
        /** @scrutinizer ignore-call */ 
184
        $row = $this->result->fetchAssociative();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
184
        if ($row) {
185
            /** @var array<string, array<string, array<string, mixed>>> $beansData array<tablegroup, array<table, array<column, value>>>*/
186
            $beansData = [];
187
            $allNull = true;
188
            foreach ($row as $key => $value) {
189
                if (!isset($this->columnDescriptors[$key])) {
190
                    continue;
191
                }
192
                if ($allNull !== false && $value !== null) {
193
                    $allNull = false;
194
                }
195
196
                $columnDescriptor = $this->columnDescriptors[$key];
197
198
                if ($columnDescriptor['tableGroup'] === null) {
199
                    // A column can have no tableGroup (if it comes from an ORDER BY expression)
200
                    continue;
201
                }
202
203
                // Let's cast the value according to its type
204
                $value = $columnDescriptor['type']->convertToPHPValue($value, $this->databasePlatform);
205
206
                $beansData[$columnDescriptor['tableGroup']][$columnDescriptor['table']][$columnDescriptor['column']] = $value;
207
            }
208
            if ($allNull === true) {
209
                $this->next();
210
                return;
211
            }
212
213
            $reflectionClassCache = [];
214
            $firstBean = true;
215
            /** @var array<string, array<string, mixed>> $beanData */
216
            foreach ($beansData as $beanData) {
217
                // Let's find the bean class name associated to the bean.
218
219
                list($actualClassName, $mainBeanTableName, $tablesUsed) = $this->tdbmService->_getClassNameFromBeanData($beanData);
220
221
                // @TODO (gua) this is a weird hack to be able to force a TDBMObject...
222
                // `$this->className` could be used to override `$actualClassName`
223
                if ($this->className !== null && is_a($this->className, TDBMObject::class, true)) {
224
                    $actualClassName = $this->className;
225
                }
226
227
                // Let's filter out the beanData that is not used (because it belongs to a part of the hierarchy that is not fetched:
228
                foreach ($beanData as $tableName => $descriptors) {
229
                    if (!in_array($tableName, $tablesUsed)) {
230
                        unset($beanData[$tableName]);
231
                    }
232
                }
233
234
                // Must we create the bean? Let's see in the cache if we have a mapping DbRow?
235
                // Let's get the first object mapping a row:
236
                // We do this loop only for the first table
237
238
                $primaryKeys = $this->tdbmService->_getPrimaryKeysFromObjectData($mainBeanTableName, $beanData[$mainBeanTableName]);
239
                $hash = $this->tdbmService->getObjectHash($primaryKeys);
240
241
                $dbRow = $this->objectStorage->get($mainBeanTableName, $hash);
242
                if ($dbRow !== null) {
243
                    $bean = $dbRow->getTDBMObject();
244
                } else {
245
                    // Let's construct the bean
246
                    if (!isset($reflectionClassCache[$actualClassName])) {
247
                        $reflectionClassCache[$actualClassName] = new \ReflectionClass($actualClassName);
248
                    }
249
                    // Let's bypass the constructor when creating the bean!
250
                    /** @var AbstractTDBMObject $bean */
251
                    $bean = $reflectionClassCache[$actualClassName]->newInstanceWithoutConstructor();
252
                    $bean->_constructFromData($beanData, $this->tdbmService);
253
                }
254
255
                // The first bean is the one containing the main table.
256
                if ($firstBean) {
257
                    $firstBean = false;
258
                    $this->current = $bean;
259
                }
260
            }
261
262
            ++$this->key;
263
        } else {
264
            $this->current = null;
265
        }
266
    }
267
268
    /**
269
     * Moves the cursor to the beginning of the result set.
270
     */
271
    public function rewind(): void
272
    {
273
        $this->executeQuery();
274
        $this->key = -1;
275
        $this->next();
276
    }
277
    /**
278
     * Checks if the cursor is reading a valid result.
279
     *
280
     * @return bool
281
     */
282
    public function valid(): bool
283
    {
284
        return $this->current !== null;
285
    }
286
287
    /**
288
     * Whether a offset exists.
289
     *
290
     * @link http://php.net/manual/en/arrayaccess.offsetexists.php
291
     *
292
     * @param mixed $offset <p>
293
     *                      An offset to check for.
294
     *                      </p>
295
     *
296
     * @return bool true on success or false on failure.
297
     *              </p>
298
     *              <p>
299
     *              The return value will be casted to boolean if non-boolean was returned
300
     *
301
     * @since 5.0.0
302
     */
303
    public function offsetExists($offset): bool
304
    {
305
        throw new TDBMInvalidOperationException('You cannot access this result set via index because it was fetched in CURSOR mode. Use ARRAY_MODE instead.');
306
    }
307
308
    /**
309
     * Offset to retrieve.
310
     *
311
     * @link http://php.net/manual/en/arrayaccess.offsetget.php
312
     *
313
     * @param mixed $offset <p>
314
     *                      The offset to retrieve.
315
     *                      </p>
316
     *
317
     * @return mixed Can return all value types
318
     *
319
     * @since 5.0.0
320
     */
321
    public function offsetGet($offset): mixed
322
    {
323
        throw new TDBMInvalidOperationException('You cannot access this result set via index because it was fetched in CURSOR mode. Use ARRAY_MODE instead.');
324
    }
325
326
    /**
327
     * Offset to set.
328
     *
329
     * @link http://php.net/manual/en/arrayaccess.offsetset.php
330
     *
331
     * @param mixed $offset <p>
332
     *                      The offset to assign the value to.
333
     *                      </p>
334
     * @param mixed $value  <p>
335
     *                      The value to set.
336
     *                      </p>
337
     *
338
     * @since 5.0.0
339
     */
340
    public function offsetSet($offset, $value): void
341
    {
342
        throw new TDBMInvalidOperationException('You cannot set values in a TDBM result set.');
343
    }
344
345
    /**
346
     * Offset to unset.
347
     *
348
     * @link http://php.net/manual/en/arrayaccess.offsetunset.php
349
     *
350
     * @param mixed $offset <p>
351
     *                      The offset to unset.
352
     *                      </p>
353
     *
354
     * @since 5.0.0
355
     */
356
    public function offsetUnset($offset): void
357
    {
358
        throw new TDBMInvalidOperationException('You cannot unset values in a TDBM result set.');
359
    }
360
}
361