Filler::getRecordForRoot()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3

Importance

Changes 0
Metric Value
eloc 4
c 0
b 0
f 0
dl 0
loc 8
ccs 5
cts 5
cp 1
rs 10
cc 3
nc 3
nop 0
crap 3
1
<?php
2
3
namespace kalanis\kw_mapper\Search\Connector\Database;
4
5
6
use kalanis\kw_mapper\MapperException;
7
use kalanis\kw_mapper\Records\ARecord;
8
use kalanis\kw_mapper\Records\TFill;
9
use kalanis\kw_mapper\Search\Connector;
10
use kalanis\kw_mapper\Storage\Shared\QueryBuilder\Join;
11
12
13
/**
14
 * Class Filler
15
 * @package kalanis\kw_mapper\Search\Connector\Database
16
 * Filling (Hydrate) both columns and Records
17
 *
18
 * Reconstruct the tree structure which has been defined by relations in the DB. Each join says that there is
19
 * a step between the record levels. The topmost is the base record on which the search has been initialized.
20
 * The rest are just deeper ones based on the relations to the topmost one. It is just another variant of tree.
21
 * But you dig the depths instead of go upwards.
22
 * Start with getColumns() to decide which columns will be returned from DB
23
 * After the data will be obtained pass them through fillResults() to fill records itself
24
 *
25
 * Someone says it shall be Hydrate. But I fill it until there is nothing to get and no space left!
26
 */
27
class Filler
28
{
29
    use TFill;
30
31
    protected string $hashDelimiter = "--::\e::--";
32
    protected string $columnDelimiter = '____';
33
    protected ARecord $basicRecord;
34
    /** @var RecordsInJoin[] */
35
    protected array $recordsInJoin = [];
36
    private bool $fromDatabase = false;
37
38 22
    public function __construct(ARecord $basicRecord)
39
    {
40 22
        $this->basicRecord = $basicRecord;
41
    }
42
43
    /**
44
     * @param RecordsInJoin[] $recordsInJoin
45
     */
46 13
    public function initTreeSolver(array &$recordsInJoin): void
47
    {
48 13
        $this->recordsInJoin = $recordsInJoin;
49
    }
50
51
    /**
52
     * Get all necessary columns which will be added and used in query
53
     * @param Join[] $joins
54
     * @throws MapperException
55
     * @return array<string|int, array<string|int|float|null>>
56
     */
57 7
    public function getColumns(array $joins): array
58
    {
59 7
        $used = [];
60 7
        $columns = [];
61 7
        $join = $this->orderJoinsColumns($joins);
62 7
        foreach ($this->recordsInJoin as &$record) {
63 7
            $alias = $record->getStoreKey();
64 7
            if (in_array($alias, $used)) {
65
                // @codeCoverageIgnoreStart
66
                // if they came here more than once
67
                // usually happens when the circular dependency came - the child has child which is the same record
68
                continue;
69
            }
70
            // @codeCoverageIgnoreEnd
71 7
            foreach ($record->getRecord()->getMapper()->getRelations() as $relation) {
72 7
                $joinAlias = empty($join[$alias]) ? $alias : $join[$alias];
73 7
                $columns[] = [$joinAlias, $relation, implode($this->columnDelimiter, [$joinAlias, $relation])];
74
            }
75 7
            $used[] = $alias;
76
        }
77 7
        return $columns;
78
    }
79
80
    /**
81
     * @param Join[] $joins
82
     * @return string[]
83
     */
84 7
    protected function orderJoinsColumns(array &$joins): array
85
    {
86 7
        $return = [];
87 7
        foreach ($joins as &$join) {
88 3
            $return[$join->getJoinUnderAlias()] = $join->getTableAlias();
89
        }
90 7
        return $return;
91
    }
92
93
    /**
94
     * Fill results from db response in lines to record tree with objects
95
     * Contains huge caching
96
     * Really hideous method which will need to stay this way, because it makes a tree from a flat table
97
     * What can be stored elsewhere is already there
98
     * @param iterable<string|int, array<string|int, string|int|float>> $dataSourceRows
99
     * @param mixed $parent
100
     * @throws MapperException
101
     * @return ARecord[]
102
     */
103 12
    public function fillResults(iterable $dataSourceRows, $parent = null): array
104
    {
105 12
        $this->setAsFromDatabase($parent);
106
        /** @var array<string, array<string, ARecord>> $aliasedRecords */
107 12
        $aliasedRecords = [];
108
        /** @var array<string|int, array<string, string>> $hashedRows */
109 12
        $hashedRows = [];
110
        // parse input data into something processable
111
        // got records with elementary data and hashes of them
112 12
        foreach ($dataSourceRows as $lineNo => &$row) {
113
            // get each table from resulted row
114 12
            $splitRow = $this->splitByTables($row);
115
//print_r(['row', $splitRow]);
116
117
            // now put each table into record
118 12
            foreach ($splitRow as $alias => &$columns) {
119 12
                $hashedRecord = $this->hashRow($columns);
120
                // store info which row is it
121 12
                if (!isset($hashedRows[$lineNo])) {
122 12
                    $hashedRows[$lineNo] = [];
123
                }
124 12
                $hashedRows[$lineNo][$alias] = $hashedRecord;
125 12
                if (is_null($hashedRecord)) {
126
                    // skip for empty content
127 1
                    continue;
128
                }
129
                // store that record
130 12
                if (!isset($aliasedRecords[$alias])) {
131 12
                    $aliasedRecords[$alias] = [];
132
                }
133 12
                if (isset($aliasedRecords[$alias][$hashedRecord])) {
134
                    // skip for existing
135 4
                    continue;
136
                }
137 12
                $aliasedRecords[$alias][$hashedRecord] = $this->fillRecordFromAlias($alias, $columns);
138
            }
139
        }
140
141
//print_r(['hashes rec', $aliasedRecords]); // records of each table in each row keyed to their hash --> $aliasedRecords[table_name][hash] = Record
142
//print_r(['hashes row', $hashedRows]); // line contains --> $hashedRows[line_number][table_name] = hash
143
144
        // tell which alias is parent of another - only by hashes
145 11
        $parentsAliases = $this->getParentsAliases();
146
        /** @var array<string, array<string, array<string, string[]>>> $children */
147 11
        $children = [];
148 11
        foreach ($hashedRows as &$hashedRow) {
149 11
            foreach ($parentsAliases as $currentAlias => &$parentsAlias) {
150 11
                if (empty($hashedRow[$parentsAlias])) { // top parent
151 11
                    continue;
152
                }
153 4
                $currentHash = $hashedRow[$currentAlias];
154 4
                $parentHash = $hashedRow[$parentsAlias];
155
                // from parent aliases which will be called to fill add child aliases with their content
156 4
                if (!isset($children[$parentsAlias])) {
157 4
                    $children[$parentsAlias] = [];
158
                }
159 4
                if (!isset($children[$parentsAlias][$parentHash])) {
160 4
                    $children[$parentsAlias][$parentHash] = [];
161
                }
162 4
                if (!isset($children[$parentsAlias][$parentHash][$currentAlias])) {
163 4
                    $children[$parentsAlias][$parentHash][$currentAlias] = [];
164
                }
165
                // can be more than one child for parent
166 4
                if (!empty($currentHash)) {
167 4
                    $children[$parentsAlias][$parentHash][$currentAlias][] = $currentHash;
168
                }
169
            }
170
        }
171
172
//print_r(['hashes children', $children]);
173
174
        // now put records together as they're defined by their hashes
175 11
        foreach ($children as $parentAlias => &$hashes) {
176 4
            foreach ($hashes as $parentHash => &$childrenHashes) {
177
                /** @var ARecord $record */
178 4
                $record = $aliasedRecords[$parentAlias][$parentHash];
179
180 4
                foreach ($childrenHashes as $childAlias => $childrenHashArr) {
181 4
                    $records = [];
182 4
                    $aliasParams = $this->getRecordForAlias($childAlias);
183 4
                    foreach ($childrenHashArr as $hash) {
184 4
                        $records[] = $aliasedRecords[$childAlias][$hash];
185
                    }
186 4
                    $record->getEntry($aliasParams->getKnownAs())->setData($records, $this->fromDatabase);
187
                }
188
            }
189
        }
190
191 11
        $results = array_values($aliasedRecords[$this->getRecordForRoot()->getStoreKey()]);
192
//print_r(['count res', count($results) ]);
193
194 10
        return $results;
195
    }
196
197
    /**
198
     * @param array<string|int, string|int|float|null> $columns
199
     * @return string|null
200
     */
201 12
    protected function hashRow(array &$columns): ?string
202
    {
203 12
        $cols = implode($this->hashDelimiter, $columns);
204 12
        if (empty(str_replace($this->hashDelimiter, '', $cols))) {
205 1
            return null;
206
        }
207 12
        return md5($cols);
208
    }
209
210
    /**
211
     * @param string $alias
212
     * @param array<string|int, string|int|float|null> $columns
213
     * @throws MapperException
214
     * @return ARecord
215
     */
216 12
    protected function fillRecordFromAlias(string $alias, array &$columns): ARecord
217
    {
218 12
        $original = $this->getRecordForAlias($alias)->getRecord();
219 12
        $record = clone $original;
220 12
        $properties = array_flip($record->getMapper()->getRelations());
221 12
        foreach ($columns as $column => $value) {
222 12
            if (isset($properties[$column])) {
223 12
                $property = strval($properties[$column]);
224 12
                if ($record->offsetExists($property) && ($record->offsetGet($property)) !== $value) {
225 12
                    $record->getEntry($property)->setData($value, $this->fromDatabase);
226
                }
227
            }
228
        }
229 12
        return $record;
230
    }
231
232
    /**
233
     * @param string $alias
234
     * @throws MapperException
235
     * @return RecordsInJoin
236
     */
237 12
    protected function getRecordForAlias(string $alias): RecordsInJoin
238
    {
239 12
        foreach ($this->recordsInJoin as $recordInJoin) {
240 12
            if ($recordInJoin->getStoreKey() == $alias) {
241 12
                return $recordInJoin;
242
            }
243
        }
244 1
        throw new MapperException(sprintf('No record for alias *%s* found.', $alias));
245
    }
246
247
    /**
248
     * @throws MapperException
249
     * @return RecordsInJoin
250
     */
251 11
    protected function getRecordForRoot(): RecordsInJoin
252
    {
253 11
        foreach ($this->recordsInJoin as $recordInJoin) {
254 11
            if (is_null($recordInJoin->getParentAlias())) {
255 10
                return $recordInJoin;
256
            }
257
        }
258 1
        throw new MapperException(sprintf('No root record found.'));
259
    }
260
261
    /**
262
     * @return array<string, string|null>
263
     */
264 11
    protected function getParentsAliases(): array
265
    {
266 11
        $result = [];
267 11
        foreach ($this->recordsInJoin as &$recordInJoin) {
268 11
            $result[$recordInJoin->getStoreKey()] = $recordInJoin->getParentAlias();
269
        }
270 11
        return $result;
271
    }
272
273
    /**
274
     * @param mixed $class
275
     */
276 12
    private function setAsFromDatabase($class): void
277
    {
278 12
        if ($class && is_object($class)) {
279 6
            if ($class instanceof Connector\Database) {
280 6
                $this->fromDatabase = true;
281 6
                return;
282
            }
283
            // another for other possible connectors - probably...
284
        }
285 6
        $this->fromDatabase = false;
286
    }
287
288
    /**
289
     * @param array<string|int, string|int|float> $row
290
     * @throws MapperException
291
     * @return array<string, array<string|int, string|int|float>>
292
     */
293 12
    protected function splitByTables(&$row): array
294
    {
295 12
        $byTables = [];
296 12
        foreach ($row as $column => &$data) {
297 12
            $column = strval($column);
298 12
            $delimiterPoint = strpos($column, '.'); // look for delimiter, not every time is present
299 12
            $delimiterOur = strpos($column, $this->columnDelimiter); // our delimiter, because some databases returns only columns
300 12
            if ((false === $delimiterPoint) && (false === $delimiterOur)) {
301 1
                $table = $this->basicRecord->getMapper()->getAlias();
302 11
            } elseif (false === $delimiterPoint) { // database returns our delimiter
303 10
                $table = substr($column, 0, $delimiterOur);
304 10
                $column = substr($column, $delimiterOur + strlen($this->columnDelimiter));
305
            } else {
306 1
                $table = substr($column, 0, $delimiterPoint);
307 1
                $column = substr($column, $delimiterPoint + 1);
308
            }
309 12
            $byTables[$table][$column] = $data;
310
        }
311 12
        return $byTables;
312
    }
313
}
314