Passed
Push — main ( 43c69a...a0bf98 )
by Sergey
02:27
created

EagerLoadedDataList::createDataObject()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

292
        $query = $this->/** @scrutinizer ignore-call */ query()->execute();
Loading history...
293
294
        while ($row = $query->record()) {
295
            yield $this->createDataObject($row);
296
        }
297
    }
298
299
    private function _prepareCache($all,$selected)
300
    {
301
        foreach($selected as $depSeq) {
302
            $dep = $depSeq[0];
303
            $depClass = $all[$dep];
304
            if(!isset($this->_relatedCache[$depClass])) { $this->_relatedCache[$depClass] = []; }
305
        }
306
    }
307
308
309
310
}
311