Passed
Push — master ( 80d253...827863 )
by Sergey
03:57
created

EagerLoadedDataList   B

Complexity

Total Complexity 52

Size/Duplication

Total Lines 310
Duplicated Lines 0 %

Test Coverage

Coverage 87.71%

Importance

Changes 5
Bugs 3 Features 1
Metric Value
wmc 52
eloc 180
c 5
b 3
f 1
dl 0
loc 310
ccs 157
cts 179
cp 0.8771
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
A getGenerator() 0 6 2
A filterWithList() 0 6 1
C fulfillEagerRelations() 0 55 15
A eagerLoadingPrepareCache() 0 7 3
B prepareEagerRelations() 0 41 10

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 11
    public function __construct($classOrList)
26
    {
27 11
        if (is_string($classOrList)) {
28
            parent::__construct($classOrList);
29
        } else {
30 11
            parent::__construct($classOrList->dataClass());
31 11
            $this->dataQuery = $classOrList->dataQuery();
32
        }
33 11
    }
34
35
    public $eagerLoadingRelatedCache = [];
36 11
    public static function cloneFrom(DataList $list)
37
    {
38 11
        $clone = new EagerLoadedDataList($list);
39
40 11
        $clone->withList = $list->withList;
41 11
        $clone->withListOriginal = $list->withListOriginal;
42 11
        $clone->eagerLoadingRelatedCache = $list->eagerLoadingRelatedCache;
43 11
        return $clone;
44
    }
45
46
    /**
47
     * Create a DataObject from the given SQL row
48
     *
49
     * @param array $row
50
     * @return DataObject
51
     */
52 11
    public function createDataObject($row)
53
    {
54 11
        $this->prepareEagerRelations();
55 11
        $item = parent::createDataObject($row);
56
57 11
        $this->fulfillEagerRelations($item);
58 10
        return $item;
59
    }
60
61
    private $relationsPrepared = false;
62
63 11
    private function filterWithList($list)
64
    {
65 11
        return array_filter(
66 11
            $this->withList,
67 11
            function ($dep) use ($list) {
68 11
                return array_key_exists($dep[0], $list);
69 11
            }
70
        );
71
    }
72
73 11
    public function prepareEagerRelations()
74
    {
75 11
        if ($this->relationsPrepared) {
76 9
            return;
77
        }
78 11
        $this->relationsPrepared = true;
79 11
        $localClass = $this->dataClass();
80 11
        $config = Config::forClass($localClass);
81 11
        $hasOnes = (array) $config->get('has_one');
82 11
        $hasManys = (array) $config->get('has_many');
83 11
        $manyManys = (array) $config->get('many_many');
84 11
        $belongsManyManys = (array) $config->get('belongs_many_many');
85
86
        //collect has_ones
87 11
        $withHasOnes = $this->filterWithList($hasOnes);
88 11
        $withHasManys = $this->filterWithList($hasManys);
89 11
        $withManyManys = $this->filterWithList($manyManys);
90 11
        $withBelongsManyManys = $this->filterWithList($belongsManyManys);
91
92 11
        if (!count($withHasOnes) && !count($withHasManys) && !count($withManyManys) && !count($withBelongsManyManys)) {
93
            // Injector::inst()->get(LoggerInterface::class)
94
            // ->debug("Invalid names supplied for ->with(" . implode(', ', $this->withListOriginal) . ")");
95 1
            return;
96
        }
97
98 10
        $data = $this->column('ID');
99 10
        if (count($withHasOnes)) {
100 6
            $this->eagerLoadingPrepareCache($hasOnes, $withHasOnes);
101 6
            $this->eagerLoadHasOne($data, $hasOnes, $withHasOnes);
102
        }
103 10
        if (count($withHasManys)) {
104 3
            $this->eagerLoadingPrepareCache($hasManys, $withHasManys);
105 3
            $this->eagerLoadHasMany($data, $hasManys, $withHasManys);
106
        }
107 10
        if (count($withManyManys)) {
108 3
            $this->eagerLoadingPrepareCache($manyManys, $withManyManys);
109 3
            $this->eagerLoadManyMany($data, $manyManys, $withManyManys);
110
        }
111 10
        if (count($withBelongsManyManys)) {
112 1
            $this->eagerLoadingPrepareCache($belongsManyManys, $withBelongsManyManys);
113 1
            $this->eagerLoadManyMany($data, $belongsManyManys, $withBelongsManyManys);
114
        }
115 10
    }
116
117 6
    public function eagerLoadHasOne(&$ids, $hasOnes, $withHasOnes)
118
    {
119
        //collect required IDS
120 6
        $fields = ['ID'];
121 6
        foreach ($withHasOnes as $depSeq) {
122 6
            $dep = $depSeq[0];
123 6
            $fields[] = "{$dep}ID";
124
        }
125 6
        $table = Config::forClass($this->dataClass)->get('table_name');
126 6
        $data = new SQLSelect(implode(',', $fields), [$table], ["ID IN (" . implode(',', $ids) . ")"]);
127 6
        $data = Utils::EnsureArray($data->execute(), 'ID');
128
129 6
        foreach ($withHasOnes as $depSeq) {
130 6
            $dep = $depSeq[0];
131 6
            $depClass = $hasOnes[$dep];
132
133
            $descriptor = [
134 6
                'class' => $depClass,
135 6
                'localField' => "{$dep}ID",
136
                'map' => [],
137
            ];
138
139 6
            $descriptor['map'] = Utils::extractField($data, $descriptor['localField']);
140 6
            $uniqueIDs = array_unique($descriptor['map']);
141 6
            while (count($uniqueIDs)) {
142 6
                $IDsubset = array_splice($uniqueIDs, 0, self::ID_LIMIT);
143 6
                $result = DataObject::get($depClass)->filter('ID', $IDsubset);
144 6
                if (count($depSeq)>1) {
145
                    $result = $result
146 2
                        ->with(implode('.', array_slice($depSeq, 1)));
147
                }
148
149 6
                foreach ($result as $depRecord) {
150 6
                    $this->eagerLoadingRelatedCache[$depClass][$depRecord->ID] = $depRecord;
151
                }
152
            }
153
154 6
            $this->eagerLoadingRelatedMaps['has_one'][$dep] = $descriptor;
155
        }
156 6
    }
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 4
    public function eagerLoadManyMany(&$data, $manyManys, $withManyManys)
195
    {
196 4
        $localClass = $this->dataClass();
197 4
        $schema = DataObject::getSchema();
198
199 4
        foreach ($withManyManys as $depSeq) {
200 4
            $dep = $depSeq[0];
201 4
            $depClass = $manyManys[$dep];
202
203 4
            $component = $schema->manyManyComponent($localClass, $dep);
204
205
            $descriptor = [
206 4
                'class' => $depClass,
207
                'map' => [],
208
            ];
209
210 4
            $idsQuery = SQLSelect::create(
211 4
                implode(',', [$component['childField'], $component['parentField']]),
212 4
                $component['join'],
213
                [
214 4
                    $component['parentField'] . ' IN (' . implode(',', $data) . ')'
215
                ]
216 4
            )->execute();
217
218 4
            $collection = [];
219 4
            $relListReverted = [];
220 4
            foreach ($idsQuery as $row) {
221 4
                $relID = $row[$component['childField']];
222 4
                $localID = $row[$component['parentField']];
223 4
                if (!isset($collection[$localID])) {
224 4
                    $collection[$localID] = [];
225
                }
226 4
                $collection[$localID][] = $relID;
227 4
                $relListReverted[$relID] = 1; //use ids as keys to avoid
228
            }
229
230 4
            $result = DataObject::get($depClass)->filter('ID', array_keys($relListReverted));
231 4
            if (count($depSeq)>1) {
232
                $result = $result
233
                    ->with(implode('.', array_slice($depSeq, 1)));
234
            }
235
236 4
            foreach ($result as $depRecord) {
237 4
                $this->eagerLoadingRelatedCache[$depClass][$depRecord->ID] = $depRecord;
238
            }
239
240 4
            $descriptor['map'] = $collection;
241 4
            $this->eagerLoadingRelatedMaps['has_many'][$dep] = $descriptor;
242
        }
243 4
    }
244
245 11
    public function fulfillEagerRelations(DataObject $item)
246
    {
247 11
        foreach ($this->eagerLoadingRelatedMaps['has_one'] as $dep => $depInfo) {
248 6
            $depClass = $depInfo['class'];
249 6
            if (isset($depInfo['map'][$item->ID])) {
250 6
                $depID = $depInfo['map'][$item->ID];
251 6
                if (isset($this->eagerLoadingRelatedCache[$depClass][$depID])) {
252 6
                    $depRecord = $this->eagerLoadingRelatedCache[$depClass][$depID];
253 6
                    $item->setComponent($dep, $depRecord);
254
                }
255
            }
256
        }
257
258 11
        foreach ($this->eagerLoadingRelatedMaps['has_many'] as $dep => $depInfo) {
259 7
            $depClass = $depInfo['class'];
260 7
            $collection = [];
261 7
            if (isset($depInfo['map'][$item->ID])) {
262 7
                foreach ($depInfo['map'][$item->ID] as $depID) {
263 7
                    if (isset($this->eagerLoadingRelatedCache[$depClass][$depID])) {
264 7
                        $depRecord = $this->eagerLoadingRelatedCache[$depClass][$depID];
265 7
                        $collection[] = $depRecord;
266
                    }
267
                }
268
            }
269 7
            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 6
            $item->addEagerRelation($dep, $collection);
277
        }
278
279 10
        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 10
    }
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 10
    private function eagerLoadingPrepareCache($all, $selected)
317
    {
318 10
        foreach ($selected as $depSeq) {
319 10
            $dep = $depSeq[0];
320 10
            $depClass = $all[$dep];
321 10
            if (!isset($this->eagerLoadingRelatedCache[$depClass])) {
322 10
                $this->eagerLoadingRelatedCache[$depClass] = [];
323
            }
324
        }
325 10
    }
326
}
327