Passed
Push — master ( 9ad3d5...6c4f98 )
by Sergey
03:18
created

EagerLoadedDataList::eagerLoadHasMany()   A

Complexity

Conditions 5
Paths 9

Size

Total Lines 33
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 21
CRAP Score 5.0023

Importance

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