Passed
Pull Request — master (#172)
by David
02:16
created

InnerResultIterator::next()   D

Complexity

Conditions 14
Paths 297

Size

Total Lines 80
Code Lines 39

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 39
c 0
b 0
f 0
dl 0
loc 80
rs 4.2208
cc 14
nc 297
nop 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
declare(strict_types=1);
3
4
namespace TheCodingMachine\TDBM;
5
6
use Doctrine\DBAL\Platforms\MySqlPlatform;
7
use Doctrine\DBAL\Statement;
8
use Mouf\Database\MagicQuery;
9
use Psr\Log\LoggerInterface;
10
use Psr\Log\NullLogger;
11
use TheCodingMachine\TDBM\QueryFactory\SmartEagerLoad\PartialQueryFactory;
12
use TheCodingMachine\TDBM\QueryFactory\SmartEagerLoad\StorageNode;
13
use TheCodingMachine\TDBM\Utils\DbalUtils;
14
15
/*
16
 Copyright (C) 2006-2017 David Négrier - THE CODING MACHINE
17
18
 This program is free software; you can redistribute it and/or modify
19
 it under the terms of the GNU General Public License as published by
20
 the Free Software Foundation; either version 2 of the License, or
21
 (at your option) any later version.
22
23
 This program is distributed in the hope that it will be useful,
24
 but WITHOUT ANY WARRANTY; without even the implied warranty of
25
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
26
 GNU General Public License for more details.
27
28
 You should have received a copy of the GNU General Public License
29
 along with this program; if not, write to the Free Software
30
 Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
31
 */
32
33
/**
34
 * Iterator used to retrieve results.
35
 */
36
class InnerResultIterator implements \Iterator, \Countable, \ArrayAccess
37
{
38
    /**
39
     * @var Statement
40
     */
41
    protected $statement;
42
43
    protected $fetchStarted = false;
44
    private $objectStorage;
45
    private $className;
46
47
    private $tdbmService;
48
    private $magicSql;
49
    private $parameters;
50
    private $limit;
51
    private $offset;
52
    private $columnDescriptors;
53
    private $magicQuery;
54
55
    /**
56
     * The key of the current retrieved object.
57
     *
58
     * @var int
59
     */
60
    protected $key = -1;
61
62
    protected $current = null;
63
64
    private $databasePlatform;
65
66
    /**
67
     * @var LoggerInterface
68
     */
69
    private $logger;
70
    /**
71
     * @var PartialQueryFactory|null
72
     */
73
    private $partialQueryFactory;
74
75
    protected $count = null;
76
77
    private function __construct()
78
    {
79
    }
80
81
    /**
82
     * @param mixed[] $parameters
83
     * @param array[] $columnDescriptors
84
     */
85
    public static function createInnerResultIterator(string $magicSql, array $parameters, ?int $limit, ?int $offset, array $columnDescriptors, ObjectStorageInterface $objectStorage, ?string $className, TDBMService $tdbmService, MagicQuery $magicQuery, LoggerInterface $logger, ?PartialQueryFactory $partialQueryFactory): self
86
    {
87
        $iterator =  new static();
88
        $iterator->magicSql = $magicSql;
89
        $iterator->objectStorage = $objectStorage;
90
        $iterator->className = $className;
91
        $iterator->tdbmService = $tdbmService;
92
        $iterator->parameters = $parameters;
93
        $iterator->limit = $limit;
94
        $iterator->offset = $offset;
95
        $iterator->columnDescriptors = $columnDescriptors;
96
        $iterator->magicQuery = $magicQuery;
97
        $iterator->databasePlatform = $iterator->tdbmService->getConnection()->getDatabasePlatform();
98
        $iterator->logger = $logger;
99
        $iterator->partialQueryFactory = $partialQueryFactory;
100
        return $iterator;
101
    }
102
103
    public static function createEmpyIterator(): self
104
    {
105
        $iterator = new static();
106
        $iterator->count = 0;
107
        $iterator->logger = new NullLogger();
108
        return $iterator;
109
    }
110
111
    private function getQuery(): string
112
    {
113
        $sql = $this->magicQuery->buildPreparedStatement($this->magicSql, $this->parameters);
114
        $sql = $this->tdbmService->getConnection()->getDatabasePlatform()->modifyLimitQuery($sql, $this->limit, $this->offset);
115
        return $sql;
116
    }
117
118
    protected function executeQuery(): void
119
    {
120
        $sql = $this->getQuery();
121
122
        $this->logger->debug('Running SQL request: '.$sql);
123
124
        $this->statement = $this->tdbmService->getConnection()->executeQuery($sql, $this->parameters, DbalUtils::generateArrayTypes($this->parameters));
125
126
        $this->fetchStarted = true;
127
    }
128
129
    /**
130
     * Counts found records (this is the number of records fetched, taking into account the LIMIT and OFFSET settings).
131
     *
132
     * @return int
133
     */
134
    public function count()
135
    {
136
        if ($this->count !== null) {
137
            return $this->count;
138
        }
139
140
        if ($this->fetchStarted && $this->tdbmService->getConnection()->getDatabasePlatform() instanceof MySqlPlatform) {
141
            // Optimisation: we don't need a separate "count" SQL request in MySQL.
142
            $this->count = $this->statement->rowCount();
143
            return $this->count;
144
        }
145
        return $this->getRowCountViaSqlQuery();
146
    }
147
148
    /**
149
     * Makes a separate SQL query to compute the row count.
150
     * (not needed in MySQL if fetch is already done)
151
     */
152
    private function getRowCountViaSqlQuery(): int
153
    {
154
        $countSql = 'SELECT COUNT(1) FROM ('.$this->getQuery().') c';
155
156
        $this->logger->debug('Running count SQL request: '.$countSql);
157
158
        $this->count = (int) $this->tdbmService->getConnection()->fetchColumn($countSql, $this->parameters, 0, DbalUtils::generateArrayTypes($this->parameters));
159
        return $this->count;
160
    }
161
162
    /**
163
     * Fetches record at current cursor.
164
     *
165
     * @return AbstractTDBMObject
166
     */
167
    public function current()
168
    {
169
        return $this->current;
170
    }
171
172
    /**
173
     * Returns the current result's key.
174
     *
175
     * @return int
176
     */
177
    public function key()
178
    {
179
        return $this->key;
180
    }
181
182
    /**
183
     * Advances the cursor to the next result.
184
     * Casts the database result into one (or several) beans.
185
     */
186
    public function next()
187
    {
188
        $row = $this->statement->fetch(\PDO::FETCH_ASSOC);
189
        if ($row) {
190
191
            // array<tablegroup, array<table, array<column, value>>>
192
            $beansData = [];
193
            foreach ($row as $key => $value) {
194
                if (!isset($this->columnDescriptors[$key])) {
195
                    continue;
196
                }
197
198
                $columnDescriptor = $this->columnDescriptors[$key];
199
200
                if ($columnDescriptor['tableGroup'] === null) {
201
                    // A column can have no tableGroup (if it comes from an ORDER BY expression)
202
                    continue;
203
                }
204
205
                // Let's cast the value according to its type
206
                $value = $columnDescriptor['type']->convertToPHPValue($value, $this->databasePlatform);
207
208
                $beansData[$columnDescriptor['tableGroup']][$columnDescriptor['table']][$columnDescriptor['column']] = $value;
209
            }
210
211
            $partialQuery = null;
212
            if ($this instanceof StorageNode && $this->partialQueryFactory !== null) {
213
                $partialQuery = $this->partialQueryFactory->getPartialQuery($this, $this->magicQuery, $this->parameters);
214
            }
215
216
            $reflectionClassCache = [];
217
            $firstBean = true;
218
            foreach ($beansData as $beanData) {
219
220
                // Let's find the bean class name associated to the bean.
221
222
                list($actualClassName, $mainBeanTableName, $tablesUsed) = $this->tdbmService->_getClassNameFromBeanData($beanData);
223
224
                if ($this->className !== null) {
225
                    $actualClassName = $this->className;
226
                }
227
228
                // Let's filter out the beanData that is not used (because it belongs to a part of the hierarchy that is not fetched:
229
                foreach ($beanData as $tableName => $descriptors) {
230
                    if (!in_array($tableName, $tablesUsed)) {
231
                        unset($beanData[$tableName]);
232
                    }
233
                }
234
235
                // Must we create the bean? Let's see in the cache if we have a mapping DbRow?
236
                // Let's get the first object mapping a row:
237
                // We do this loop only for the first table
238
239
                $primaryKeys = $this->tdbmService->_getPrimaryKeysFromObjectData($mainBeanTableName, $beanData[$mainBeanTableName]);
240
                $hash = $this->tdbmService->getObjectHash($primaryKeys);
241
242
                $dbRow = $this->objectStorage->get($mainBeanTableName, $hash);
243
                if ($dbRow !== null) {
244
                    $bean = $dbRow->getTDBMObject();
245
                } else {
246
                    // Let's construct the bean
247
                    if (!isset($reflectionClassCache[$actualClassName])) {
248
                        $reflectionClassCache[$actualClassName] = new \ReflectionClass($actualClassName);
249
                    }
250
                    // Let's bypass the constructor when creating the bean!
251
                    /** @var AbstractTDBMObject $bean */
252
                    $bean = $reflectionClassCache[$actualClassName]->newInstanceWithoutConstructor();
253
                    $bean->_constructFromData($beanData, $this->tdbmService, $partialQuery);
254
                }
255
256
                // The first bean is the one containing the main table.
257
                if ($firstBean) {
258
                    $firstBean = false;
259
                    $this->current = $bean;
260
                }
261
            }
262
263
            ++$this->key;
264
        } else {
265
            $this->current = null;
266
        }
267
    }
268
269
    /**
270
     * Moves the cursor to the beginning of the result set.
271
     */
272
    public function rewind()
273
    {
274
        if ($this->count === 0) {
275
            return;
276
        }
277
        $this->executeQuery();
278
        $this->key = -1;
279
        $this->next();
280
    }
281
    /**
282
     * Checks if the cursor is reading a valid result.
283
     *
284
     * @return bool
285
     */
286
    public function valid()
287
    {
288
        if ($this->count === 0) {
289
            return false;
290
        }
291
        return $this->current !== null;
292
    }
293
294
    /**
295
     * Whether a offset exists.
296
     *
297
     * @link http://php.net/manual/en/arrayaccess.offsetexists.php
298
     *
299
     * @param mixed $offset <p>
300
     *                      An offset to check for.
301
     *                      </p>
302
     *
303
     * @return bool true on success or false on failure.
304
     *              </p>
305
     *              <p>
306
     *              The return value will be casted to boolean if non-boolean was returned
307
     *
308
     * @since 5.0.0
309
     */
310
    public function offsetExists($offset)
311
    {
312
        throw new TDBMInvalidOperationException('You cannot access this result set via index because it was fetched in CURSOR mode. Use ARRAY_MODE instead.');
313
    }
314
315
    /**
316
     * Offset to retrieve.
317
     *
318
     * @link http://php.net/manual/en/arrayaccess.offsetget.php
319
     *
320
     * @param mixed $offset <p>
321
     *                      The offset to retrieve.
322
     *                      </p>
323
     *
324
     * @return mixed Can return all value types
325
     *
326
     * @since 5.0.0
327
     */
328
    public function offsetGet($offset)
329
    {
330
        throw new TDBMInvalidOperationException('You cannot access this result set via index because it was fetched in CURSOR mode. Use ARRAY_MODE instead.');
331
    }
332
333
    /**
334
     * Offset to set.
335
     *
336
     * @link http://php.net/manual/en/arrayaccess.offsetset.php
337
     *
338
     * @param mixed $offset <p>
339
     *                      The offset to assign the value to.
340
     *                      </p>
341
     * @param mixed $value  <p>
342
     *                      The value to set.
343
     *                      </p>
344
     *
345
     * @since 5.0.0
346
     */
347
    public function offsetSet($offset, $value)
348
    {
349
        throw new TDBMInvalidOperationException('You cannot set values in a TDBM result set.');
350
    }
351
352
    /**
353
     * Offset to unset.
354
     *
355
     * @link http://php.net/manual/en/arrayaccess.offsetunset.php
356
     *
357
     * @param mixed $offset <p>
358
     *                      The offset to unset.
359
     *                      </p>
360
     *
361
     * @since 5.0.0
362
     */
363
    public function offsetUnset($offset)
364
    {
365
        throw new TDBMInvalidOperationException('You cannot unset values in a TDBM result set.');
366
    }
367
}
368