Passed
Push — master ( 6c4f98...5b76f2 )
by Sergey
05:13 queued 01:53
created

EagerLoadedDataList   B

Complexity

Total Complexity 52

Size/Duplication

Total Lines 310
Duplicated Lines 0 %

Test Coverage

Coverage 87.78%

Importance

Changes 5
Bugs 3 Features 1
Metric Value
wmc 52
eloc 181
c 5
b 3
f 1
dl 0
loc 310
ccs 158
cts 180
cp 0.8778
rs 7.44

11 Methods

Rating   Name   Duplication   Size   Complexity  
A createDataObject() 0 7 1
B eagerLoadManyMany() 0 48 6
A __construct() 0 7 2
B eagerLoadHasOne() 0 38 6
A cloneFrom() 0 8 1
A eagerLoadHasMany() 0 33 5
B prepareEagerRelations() 0 41 10
A getGenerator() 0 6 2
A filterWithList() 0 6 1
C fulfillEagerRelations() 0 55 15
A eagerLoadingPrepareCache() 0 7 3

How to fix   Complexity   

Complex Class

Complex classes like EagerLoadedDataList often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use EagerLoadedDataList, and based on these observations, apply Extract Interface, too.

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