Passed
Pull Request — 5.1 (#197)
by ARP
06:50
created

InnerResultIterator::valid()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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