EagerLoadedDataList   D
last analyzed

Complexity

Total Complexity 58

Size/Duplication

Total Lines 344
Duplicated Lines 0 %

Test Coverage

Coverage 91%

Importance

Changes 9
Bugs 6 Features 2
Metric Value
wmc 58
eloc 202
c 9
b 6
f 2
dl 0
loc 344
rs 4.5599
ccs 192
cts 211
cp 0.91

11 Methods

Rating   Name   Duplication   Size   Complexity  
A createDataObject() 0 7 1
C eagerLoadManyMany() 0 75 12
A __construct() 0 7 2
B eagerLoadHasOne() 0 38 6
A cloneFrom() 0 8 1
B eagerLoadHasMany() 0 39 6
B prepareEagerRelations() 0 41 10
A getGenerator() 0 6 2
A filterWithList() 0 6 1
C fulfillEagerRelations() 0 55 14
A eagerLoadingPrepareCache() 0 8 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 Exception;
5
use SilverStripe\Core\Config\Config;
6
use SilverStripe\ORM\DataList;
7
use SilverStripe\ORM\DataObject;
8
use SilverStripe\ORM\Queries\SQLSelect;
9
10
/**
11
 * Replaces DataList when EagerLoading is used. Fetches data when the main query is actually executed.
12
 * Appends related objects when a DataObject is actually created.
13
 */
14
class EagerLoadedDataList extends DataList
15
{
16
17
    const ID_LIMIT = 5000;
18
    public $withList = [];
19
    public $withListOriginal = [];
20
    public $eagerLoadingRelatedMaps = [
21
        'has_one' => [],
22
        'has_many' => [],
23
        'many_many' => [],
24
    ];
25
26 12
    public function __construct($classOrList)
27
    {
28 12
        if (is_string($classOrList)) {
29
            parent::__construct($classOrList);
30
        } else {
31 12
            parent::__construct($classOrList->dataClass());
32 12
            $this->dataQuery = $classOrList->dataQuery();
33
        }
34
    }
35
36
    public $eagerLoadingRelatedCache = [];
37 12
    public static function cloneFrom(DataList $list)
38
    {
39 12
        $clone = new EagerLoadedDataList($list);
40
41 12
        $clone->withList = $list->withList;
42 12
        $clone->withListOriginal = $list->withListOriginal;
43 12
        $clone->eagerLoadingRelatedCache = $list->eagerLoadingRelatedCache;
44 12
        return $clone;
45
    }
46
47
    /**
48
     * Create a DataObject from the given SQL row
49
     *
50
     * @param array $row
51
     * @return DataObject
52
     */
53 12
    public function createDataObject($row)
54
    {
55 12
        $this->prepareEagerRelations();
56 12
        $item = parent::createDataObject($row);
57
58 12
        $this->fulfillEagerRelations($item);
59 11
        return $item;
60
    }
61
62
    private $relationsPrepared = false;
63
64 12
    private function filterWithList($list)
65
    {
66 12
        return array_filter(
67 12
            $this->withList,
68 12
            function ($dep) use ($list) {
69 12
                return array_key_exists($dep[0], $list);
70 12
            }
71 12
        );
72
    }
73
74 12
    public function prepareEagerRelations()
75
    {
76 12
        if ($this->relationsPrepared) {
77 10
            return;
78
        }
79 12
        $this->relationsPrepared = true;
80 12
        $localClass = $this->dataClass();
81 12
        $config = Config::forClass($localClass);
82 12
        $hasOnes = (array) $config->get('has_one');
83 12
        $hasManys = (array) $config->get('has_many');
84 12
        $manyManys = (array) $config->get('many_many');
85 12
        $belongsManyManys = (array) $config->get('belongs_many_many');
86
87
        //collect has_ones
88 12
        $withHasOnes = $this->filterWithList($hasOnes);
89 12
        $withHasManys = $this->filterWithList($hasManys);
90 12
        $withManyManys = $this->filterWithList($manyManys);
91 12
        $withBelongsManyManys = $this->filterWithList($belongsManyManys);
92
93 12
        if (!count($withHasOnes) && !count($withHasManys) && !count($withManyManys) && !count($withBelongsManyManys)) {
94
            // Injector::inst()->get(LoggerInterface::class)
95
            // ->debug("Invalid names supplied for ->with(" . implode(', ', $this->withListOriginal) . ")");
96 1
            return;
97
        }
98
99 11
        $data = $this->column('ID');
100 11
        if (count($withHasOnes)) {
101 6
            $this->eagerLoadingPrepareCache($hasOnes, $withHasOnes);
102 6
            $this->eagerLoadHasOne($data, $hasOnes, $withHasOnes);
103
        }
104 11
        if (count($withHasManys)) {
105 3
            $this->eagerLoadingPrepareCache($hasManys, $withHasManys);
106 3
            $this->eagerLoadHasMany($data, $hasManys, $withHasManys);
107
        }
108 11
        if (count($withManyManys)) {
109 4
            $this->eagerLoadingPrepareCache($manyManys, $withManyManys);
110 4
            $this->eagerLoadManyMany($data, $manyManys, $withManyManys);
111
        }
112 11
        if (count($withBelongsManyManys)) {
113 1
            $this->eagerLoadingPrepareCache($belongsManyManys, $withBelongsManyManys);
114 1
            $this->eagerLoadManyMany($data, $belongsManyManys, $withBelongsManyManys);
115
        }
116
    }
117
118 6
    public function eagerLoadHasOne(&$ids, $hasOnes, $withHasOnes)
119
    {
120
        //collect required IDS
121 6
        $fields = ['ID'];
122 6
        foreach ($withHasOnes as $depSeq) {
123 6
            $dep = $depSeq[0];
124 6
            $fields[] = "\"{$dep}ID\"";
125
        }
126 6
        $table = DataObject::getSchema()->tableName($this->dataClass);
127 6
        $data = new SQLSelect($fields, '"' . $table . '"', ['"ID" IN (' . implode(',', $ids) . ')']);
128 6
        $data = Utils::EnsureArray($data->execute(), 'ID');
129
130 6
        foreach ($withHasOnes as $depSeq) {
131 6
            $dep = $depSeq[0];
132 6
            $depClass = $hasOnes[$dep];
133
134 6
            $descriptor = [
135 6
                'class' => $depClass,
136 6
                'localField' => "{$dep}ID",
137 6
                'map' => [],
138 6
            ];
139
140 6
            $descriptor['map'] = Utils::extractField($data, $descriptor['localField']);
141 6
            $uniqueIDs = array_unique($descriptor['map']);
142 6
            while (count($uniqueIDs)) {
143 6
                $IDsubset = array_splice($uniqueIDs, 0, self::ID_LIMIT);
144 6
                $result = DataObject::get($depClass)->filter('ID', $IDsubset);
145 6
                if (count($depSeq)>1) {
146 2
                    $result = $result
147 2
                        ->with(implode('.', array_slice($depSeq, 1)));
148
                }
149
150 6
                foreach ($result as $depRecord) {
151 6
                    $this->eagerLoadingRelatedCache[$depClass][$depRecord->ID] = $depRecord;
152
                }
153
            }
154
155 6
            $this->eagerLoadingRelatedMaps['has_one'][$dep] = $descriptor;
156
        }
157
    }
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
            if (false !== strpos($depClass, '.')) {
168
                $dcSplit = explode('.', $depClass, 2);
169
                $depClass = $dcSplit[0];
170
                $localNameInDep = $dcSplit[1];
171
            } else {
172 3
                $localNameInDep = $localClassTail;
173
            }
174 3
            $depKey = "{$localNameInDep}ID";
175 3
            $descriptor = [
176 3
                'class' => $depClass,
177 3
                'remoteRelation' => $localNameInDep,
178 3
                'remoteField' => $depKey,
179 3
                'map' => [],
180 3
            ];
181 3
            $result = DataObject::get($depClass)->filter($depKey, $data);
182 3
            if (count($depSeq)>1) {
183
                $result = $result
184
                    ->with(implode('.', array_slice($depSeq, 1)));
185
            }
186
187 3
            $collection = [];
188
189 3
            foreach ($data as $localRecordID) {
190 3
                $collection[$localRecordID] = [];
191
            }
192 3
            foreach ($result as $depRecord) {
193 3
                $this->eagerLoadingRelatedCache[$depClass][$depRecord->ID] = $depRecord;
194 3
                $collection[$depRecord->$depKey][] = $depRecord->ID;
195
            }
196 3
            $descriptor['map'] = $collection;
197 3
            $this->eagerLoadingRelatedMaps['has_many'][$dep] = $descriptor;
198
        }
199
    }
200
201 5
    public function eagerLoadManyMany(&$data, $manyManys, $withManyManys)
202
    {
203 5
        $localClass = $this->dataClass();
204 5
        $schema = DataObject::getSchema();
205
206 5
        foreach ($withManyManys as $depSeq) {
207 5
            $dep = $depSeq[0];
208 5
            $depData = $manyManys[$dep];
209
210 5
            if (is_array($depData)) {
211 1
                if (!isset($depData['from']) || !isset($depData['to']) || !isset($depData['through'])) {
212
                    throw new Exception(sprintf('Incompatible "many_many through" configuration for %s.%s', $localClass, $dep));
213
                }
214 1
                $throughClass = $depData['through'];
215
                // determine the target data object
216 1
                $depClass = $schema->hasOneComponent($depData['through'], $depData['to']);
217 1
                if (!$depClass) {
218
                    throw new Exception(sprintf('Class %s does not have a $has_one component named', $depData['through'], $depData['to']));
219
                }
220
221 1
                $table = DataObject::getSchema()->tableName($throughClass);
222
223 1
                $childField = $depData['to']. 'ID';
224 1
                $parentField = $depData['from']. 'ID';
225
            } else {
226 4
                $depClass = $depData;
227 4
                $component = $schema->manyManyComponent($localClass, $dep);
228
229 4
                $table = $component['join'];
230 4
                $childField = $component['childField'];
231 4
                $parentField = $component['parentField'];
232
            }
233
234 5
            $descriptor = [
235 5
                'class' => $depClass,
236 5
                'map' => [],
237 5
            ];
238
239 5
            $idsQuery = SQLSelect::create(
240 5
                [
241 5
                    '"' . $childField . '"',
242 5
                    '"' . $parentField . '"',
243 5
                ],
244 5
                '"' . $table . '"',
245 5
                [
246 5
                    '"' . $parentField . '" IN (' . implode(',', $data) . ')'
247 5
                ]
248 5
            )->execute();
249
250 5
            $collection = [];
251 5
            $relListReverted = [];
252 5
            foreach ($idsQuery as $row) {
253 5
                $relID = $row[$childField];
254 5
                $localID = $row[$parentField];
255 5
                if (!isset($collection[$localID])) {
256 5
                    $collection[$localID] = [];
257
                }
258 5
                $collection[$localID][] = $relID;
259 5
                $relListReverted[$relID] = 1; //use ids as keys to avoid
260
            }
261
262 5
            if (count($relListReverted)) {
263 5
                $result = DataObject::get($depClass)->filter('ID', array_keys($relListReverted));
264 5
                if (count($depSeq)>1) {
265
                    $result = $result
266
                        ->with(implode('.', array_slice($depSeq, 1)));
267
                }
268
269 5
                foreach ($result as $depRecord) {
270 5
                    $this->eagerLoadingRelatedCache[$depClass][$depRecord->ID] = $depRecord;
271
                }
272
            }
273
274 5
            $descriptor['map'] = $collection;
275 5
            $this->eagerLoadingRelatedMaps['many_many'][$dep] = $descriptor;
276
        }
277
    }
278
279 12
    public function fulfillEagerRelations(DataObject $item)
280
    {
281 12
        foreach ($this->eagerLoadingRelatedMaps['has_one'] as $dep => $depInfo) {
282 6
            $depClass = $depInfo['class'];
283 6
            if (isset($depInfo['map'][$item->ID])) {
284 6
                $depID = $depInfo['map'][$item->ID];
285 6
                if (isset($this->eagerLoadingRelatedCache[$depClass][$depID])) {
286 6
                    $depRecord = $this->eagerLoadingRelatedCache[$depClass][$depID];
287 6
                    $item->setComponent($dep, $depRecord);
288
                }
289
            }
290
        }
291
292 12
        foreach ($this->eagerLoadingRelatedMaps['has_many'] as $dep => $depInfo) {
293 3
            $depClass = $depInfo['class'];
294 3
            $collection = [];
295 3
            if (isset($depInfo['map'][$item->ID])) {
296 3
                foreach ($depInfo['map'][$item->ID] as $depID) {
297 3
                    if (isset($this->eagerLoadingRelatedCache[$depClass][$depID])) {
298 3
                        $depRecord = $this->eagerLoadingRelatedCache[$depClass][$depID];
299 3
                        $collection[] = $depRecord;
300
                    }
301
                }
302
            }
303 3
            if (!method_exists($item, 'addEagerRelation')) {
304 1
                throw new EagerLoadingException(
305 1
                    "Model {$item->ClassName} must include " .
306 1
                    EagerLoaderMultiAccessor::class .
307 1
                    " trait to use eager loading for \$has_many"
308 1
                );
309
            }
310 2
            $item->addEagerRelation($dep, $collection);
311
        }
312
313 11
        foreach ($this->eagerLoadingRelatedMaps['many_many'] as $dep => $depInfo) {
314 5
            $depClass = $depInfo['class'];
315 5
            $collection = [];
316 5
            if (isset($depInfo['map'][$item->ID])) {
317 5
                foreach ($depInfo['map'][$item->ID] as $depID) {
318
                    // foreach ($depIDlist as $depID) {
319 5
                        if (isset($this->eagerLoadingRelatedCache[$depClass][$depID])) {
320 5
                            $depRecord = $this->eagerLoadingRelatedCache[$depClass][$depID];
321 5
                            $collection[] = $depRecord;
322
                        }
323
                    // }
324
                }
325
            }
326 5
            if (!method_exists($item, 'addEagerRelation')) {
327
                throw new EagerLoadingException(
328
                    "Model {$item->ClassName} must include " .
329
                    EagerLoaderMultiAccessor::class .
330
                    " trait to use eager loading for \$many_many"
331
                );
332
            }
333 5
            $item->addEagerRelation($dep, $collection);
334
        }
335
    }
336
    /**
337
     * Returns a generator for this DataList
338
     *
339
     * @return \Generator&DataObject[]
340
     */
341
    public function getGenerator()
342
    {
343
        $query = $this->dataQuery()->execute();
344
345
        while ($row = $query->record()) {
346
            yield $this->createDataObject($row);
347
        }
348
    }
349
350 11
    private function eagerLoadingPrepareCache($all, $selected)
351
    {
352 11
        foreach ($selected as $depSeq) {
353 11
            $dep = $depSeq[0];
354 11
            $depClass = $all[$dep];
355 11
            $depClass = $depClass['through'] ?? $depClass;
356 11
            if (!isset($this->eagerLoadingRelatedCache[$depClass])) {
357 11
                $this->eagerLoadingRelatedCache[$depClass] = [];
358
            }
359
        }
360
    }
361
}
362