Passed
Push — master ( 6139d1...678a4c )
by Petr
13:22 queued 10:27
created

Filler::getColumns()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 21
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 5.0113

Importance

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