Passed
Push — main ( 954f0f...3cf1f8 )
by Sergey
04:03
created

EagerLoadedDataList::eagerLoadManyMany()   B

Complexity

Conditions 6
Paths 13

Size

Total Lines 48
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 28
CRAP Score 6.0014

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 31
c 2
b 0
f 0
dl 0
loc 48
ccs 28
cts 29
cp 0.9655
rs 8.8017
cc 6
nc 13
nop 3
crap 6.0014
1
<?php
2
namespace Gurucomkz\EagerLoading;
3
4
use SilverStripe\Core\Config\Config;
5
use SilverStripe\ORM\DataList;
6
use SilverStripe\ORM\DataObject;
7
use SilverStripe\ORM\Queries\SQLSelect;
8
9
/**
10
 * Replaces DataList when EagerLoading is used. Fetches data when the main query is actually executed.
11
 * Appends related objects when a DataObject is actually created.
12
 */
13
class EagerLoadedDataList extends DataList
14
{
15
16
    const ID_LIMIT = 5000;
17
    public $withList = [];
18
    public $_relatedMaps = [
19
        'has_one' => [],
20
        'has_many' => [],
21
        'many_many' => [],
22
    ];
23
24 7
    public function __construct($classOrList)
25
    {
26 7
        if (is_string($classOrList)) {
27
            parent::__construct($classOrList);
28
        } else {
29 7
            parent::__construct($classOrList->dataClass());
30 7
            $this->dataQuery = $classOrList->dataQuery();
31
        }
32 7
    }
33
34
    public $_relatedCache = [];
35 7
    public static function cloneFrom(DataList $list)
36
    {
37 7
        $clone = new EagerLoadedDataList($list);
38
39 7
        $clone->withList = $list->withList;
40 7
        $clone->_relatedCache = $list->_relatedCache;
41 7
        return $clone;
42
    }
43
44
    /**
45
     * Create a DataObject from the given SQL row
46
     *
47
     * @param array $row
48
     * @return DataObject
49
     */
50 7
    public function createDataObject($row)
51
    {
52 7
        $this->prepareEagerRelations();
53 7
        $item = parent::createDataObject($row);
54
55 7
        $this->fulfillEagerRelations($item);
56 7
        return $item;
57
    }
58
59
    private $relationsPrepared = false;
60
61 7
    private function filterWithList($list)
62
    {
63 7
        return array_filter(
64 7
            $this->withList,
65 7
            function ($dep) use ($list) {
66 7
                return array_key_exists($dep[0], $list);
67 7
            }
68
        );
69
    }
70
71 7
    public function prepareEagerRelations()
72
    {
73 7
        if ($this->relationsPrepared) {
74 7
            return;
75
        }
76 7
        $this->relationsPrepared = true;
77 7
        $localClass = $this->dataClass();
78 7
        $config = Config::forClass($localClass);
79 7
        $hasOnes = (array) $config->get('has_one');
80 7
        $hasManys = (array) $config->get('has_many');
81 7
        $manyManys = (array) $config->get('many_many');
82 7
        $belongsManyManys = (array) $config->get('belongs_many_many');
83
84
        //collect has_ones
85 7
        $withHasOnes = $this->filterWithList($hasOnes);
86 7
        $withHasManys = $this->filterWithList($hasManys);
87 7
        $withManyManys = $this->filterWithList($manyManys);
88 7
        $withBelongsManyManys = $this->filterWithList($belongsManyManys);
89
90 7
        if (!count($withHasOnes) && !count($withHasManys) && !count($withManyManys) && !count($withBelongsManyManys)) {
91
            // do nothing if no matches
92
            /** @todo report errors */
93
            return;
94
        }
95
96 7
        $data = $this->column('ID');
97 7
        if (count($withHasOnes)) {
98 4
            $this->_prepareCache($hasOnes, $withHasOnes);
99 4
            $this->eagerLoadHasOne($data, $hasOnes, $withHasOnes);
100
        }
101 7
        if (count($withHasManys)) {
102 2
            $this->_prepareCache($hasManys, $withHasManys);
103 2
            $this->eagerLoadHasMany($data, $hasManys, $withHasManys);
104
        }
105 7
        if (count($withManyManys)) {
106 1
            $this->_prepareCache($manyManys, $withManyManys);
107 1
            $this->eagerLoadManyMany($data, $manyManys, $withManyManys);
108
        }
109 7
        if (count($withBelongsManyManys)) {
110 1
            $this->_prepareCache($belongsManyManys, $withBelongsManyManys);
111 1
            $this->eagerLoadManyMany($data, $belongsManyManys, $withBelongsManyManys);
112
        }
113 7
    }
114
115 4
    public function eagerLoadHasOne(&$ids, $hasOnes, $withHasOnes)
116
    {
117 4
        $schema = DataObject::getSchema();
118
119
        //collect required IDS
120 4
        $fields = ['ID'];
121 4
        foreach ($withHasOnes as $depSeq) {
122 4
            $dep = $depSeq[0];
123 4
            $fields[] = "{$dep}ID";
124
        }
125 4
        $table = Config::forClass($this->dataClass)->get('table_name');
126 4
        $data = new SQLSelect(implode(',', $fields), [$table], ["ID IN (" . implode(',', $ids) . ")"]);
127 4
        $data = Utils::EnsureArray($data->execute(), 'ID');
128
129 4
        foreach ($withHasOnes as $depSeq) {
130 4
            $dep = $depSeq[0];
131 4
            $depClass = $hasOnes[$dep];
132
133
            $descriptor = [
134 4
                'class' => $depClass,
135 4
                'localField' => "{$dep}ID",
136
                'map' => [],
137
            ];
138
139 4
            $component = $schema->hasOneComponent($this->dataClass, $dep);
0 ignored issues
show
Unused Code introduced by
The assignment to $component is dead and can be removed.
Loading history...
140
141 4
            $descriptor['map'] = Utils::extractField($data, $descriptor['localField']);
142 4
            $uniqueIDs = array_unique($descriptor['map']);
143 4
            while (count($uniqueIDs)) {
144 4
                $IDsubset = array_splice($uniqueIDs, 0, self::ID_LIMIT);
145 4
                $result = DataObject::get($depClass)->filter('ID', $IDsubset);
146 4
                if (count($depSeq)>1) {
147
                    $result = $result
148 2
                        ->with(implode('.', array_slice($depSeq, 1)));
149
                }
150
151 4
                foreach ($result as $depRecord) {
152 4
                    $this->_relatedCache[$depClass][$depRecord->ID] = $depRecord;
153
                }
154
            }
155
156 4
            $this->_relatedMaps['has_one'][$dep] = $descriptor;
157
        }
158 4
    }
159
160 2
    public function eagerLoadHasMany($data, $hasManys, $withHasManys)
161
    {
162 2
        $localClass = $this->dataClass();
163 2
        $localClassTail = basename(str_replace('\\', '/', $localClass));
164
165 2
        foreach ($withHasManys as $depSeq) {
166 2
            $dep = $depSeq[0];
167 2
            $depClass = $hasManys[$dep];
168 2
            $localNameInDep = $localClassTail;
169 2
            $depKey = "{$localNameInDep}ID";
170
            $descriptor = [
171 2
                'class' => $depClass,
172 2
                'remoteRelation' => $localNameInDep,
173 2
                'remoteField' => $depKey,
174
                'map' => [],
175
            ];
176 2
            $result = DataObject::get($depClass)->filter($depKey, $data);
177 2
            if (count($depSeq)>1) {
178
                $result = $result
179
                    ->with(implode('.', array_slice($depSeq, 1)));
180
            }
181
182 2
            $collection = [];
183
184 2
            foreach ($data as $localRecordID) {
185 2
                $collection[$localRecordID] = [];
186
            }
187 2
            foreach ($result as $depRecord) {
188 2
                $this->_relatedCache[$depClass][$depRecord->ID] = $depRecord;
189 2
                $collection[$depRecord->$depKey][] = $depRecord->ID;
190
            }
191 2
            $descriptor['map'] = $collection;
192 2
            $this->_relatedMaps['has_many'][$dep] = $descriptor;
193
        }
194 2
    }
195
196 2
    public function eagerLoadManyMany(&$data, $manyManys, $withManyManys)
197
    {
198 2
        $localClass = $this->dataClass();
199 2
        $schema = DataObject::getSchema();
200
201 2
        foreach ($withManyManys as $depSeq) {
202 2
            $dep = $depSeq[0];
203 2
            $depClass = $manyManys[$dep];
204
205 2
            $component = $schema->manyManyComponent($localClass, $dep);
206
207
            $descriptor = [
208 2
                'class' => $depClass,
209
                'map' => [],
210
            ];
211
212 2
            $idsQuery = SQLSelect::create(
213 2
                implode(',', [$component['childField'], $component['parentField']]),
214 2
                $component['join'],
215
                [
216 2
                    $component['parentField'] . ' IN (' . implode(',', $data) . ')'
217
                ]
218 2
            )->execute();
219
220 2
            $collection = [];
221 2
            $relListReverted = [];
222 2
            foreach ($idsQuery as $row) {
223 2
                $relID = $row[$component['childField']];
224 2
                $localID = $row[$component['parentField']];
225 2
                if (!isset($collection[$localID])) {
226 2
                    $collection[$localID] = [];
227
                }
228 2
                $collection[$localID][] = $relID;
229 2
                $relListReverted[$relID] = 1; //use ids as keys to avoid
230
            }
231
232 2
            $result = DataObject::get($depClass)->filter('ID', array_keys($relListReverted));
233 2
            if (count($depSeq)>1) {
234
                $result = $result
235
                    ->with(implode('.', array_slice($depSeq, 1)));
236
            }
237
238 2
            foreach ($result as $depRecord) {
239 2
                $this->_relatedCache[$depClass][$depRecord->ID] = $depRecord;
240
            }
241
242 2
            $descriptor['map'] = $collection;
243 2
            $this->_relatedMaps['has_many'][$dep] = $descriptor;
244
        }
245 2
    }
246
247 7
    public function fulfillEagerRelations(DataObject $item)
248
    {
249 7
        foreach ($this->_relatedMaps['has_one'] as $dep => $depInfo) {
250 4
            $depClass = $depInfo['class'];
251 4
            if (isset($depInfo['map'][$item->ID])) {
252 4
                $depID = $depInfo['map'][$item->ID];
253 4
                if (isset($this->_relatedCache[$depClass][$depID])) {
254 4
                    $depRecord = $this->_relatedCache[$depClass][$depID];
255 4
                    $item->setComponent($dep, $depRecord);
256
                }
257
            }
258
        }
259
260 7
        foreach ($this->_relatedMaps['has_many'] as $dep => $depInfo) {
261 4
            $depClass = $depInfo['class'];
262 4
            $collection = [];
263 4
            if (isset($depInfo['map'][$item->ID])) {
264 4
                foreach ($depInfo['map'][$item->ID] as $depID) {
265 4
                    if (isset($this->_relatedCache[$depClass][$depID])) {
266 4
                        $depRecord = $this->_relatedCache[$depClass][$depID];
267 4
                        $collection[] = $depRecord;
268
                    }
269
                }
270
            }
271 4
            if (!method_exists($item, 'addEagerRelation')) {
272
                throw new \Exception("Model {$item->ClassName} must include Gurucomkz\EagerLoading\EagerLoaderMultiAccessor trait to use eager loading for \$has_many");
273
            }
274 4
            $item->addEagerRelation($dep, $collection);
275
        }
276
277 7
        foreach ($this->_relatedMaps['many_many'] as $dep => $depInfo) {
278
            $depClass = $depInfo['class'];
279
            $collection = [];
280
            if (isset($depInfo['map'][$item->ID])) {
281
                foreach ($depInfo['map'][$item->ID] as $depIDlist) {
282
                    foreach ($depIDlist as $depID) {
283
                        if (isset($this->_relatedCache[$depClass][$depID])) {
284
                            $depRecord = $this->_relatedCache[$depClass][$depID];
285
                            $collection[] = $depRecord;
286
                        }
287
                    }
288
                }
289
            }
290
            if (!method_exists($item, 'addEagerRelation')) {
291
                throw new \Exception("Model {$item->ClassName} must include Gurucomkz\EagerLoading\EagerLoaderMultiAccessor trait to use eager loading for \$many_many");
292
            }
293
            $item->addEagerRelation($dep, $collection);
294
        }
295 7
    }
296
    /**
297
     * Returns a generator for this DataList
298
     *
299
     * @return \Generator&DataObject[]
300
     */
301
    public function getGenerator()
302
    {
303
        $query = $this->query()->execute();
304
305
        while ($row = $query->record()) {
306
            yield $this->createDataObject($row);
307
        }
308
    }
309
310 7
    private function _prepareCache($all, $selected)
311
    {
312 7
        foreach ($selected as $depSeq) {
313 7
            $dep = $depSeq[0];
314 7
            $depClass = $all[$dep];
315 7
            if (!isset($this->_relatedCache[$depClass])) {
316 7
                $this->_relatedCache[$depClass] = [];
317
            }
318
        }
319 7
    }
320
}
321