Completed
Push — master ( 42e2f4...06fa71 )
by David
15s queued 12s
created

InnerResultIterator::createEmpyIterator()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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