Passed
Pull Request — master (#172)
by David
06:43
created

InnerResultIterator::createInnerResultIterator()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 16
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 14
c 0
b 0
f 0
dl 0
loc 16
rs 9.7998
cc 1
nc 1
nop 11

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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