Passed
Pull Request — master (#10)
by
unknown
11:06
created

EagerLoadedDataList::eagerLoadManyMany()   C

Complexity

Conditions 13
Paths 30

Size

Total Lines 81
Code Lines 52

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 53
CRAP Score 13.0584

Importance

Changes 6
Bugs 2 Features 2
Metric Value
eloc 52
c 6
b 2
f 2
dl 0
loc 81
ccs 53
cts 57
cp 0.9298
rs 6.6166
cc 13
nc 30
nop 3
crap 13.0584

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 5
            $sort = [];
210
211 5
            if (is_array($depData)) {
212 1
                if (!isset($depData['from']) || !isset($depData['to']) || !isset($depData['through'])) {
213
                    throw new Exception(sprintf('Incompatible "many_many through" configuration for %s.%s', $localClass, $dep));
214
                }
215 1
                $throughClass = $depData['through'];
216
                // determine the target data object
217 1
                $depClass = $schema->hasOneComponent($depData['through'], $depData['to']);
218 1
                if (!$depClass) {
219
                    throw new Exception(sprintf('Class %s does not have a $has_one component named', $depData['through'], $depData['to']));
220
                }
221
222 1
                $table = DataObject::getSchema()->tableName($throughClass);
223
224 1
                $childField = $depData['to']. 'ID';
225 1
                $parentField = $depData['from']. 'ID';
226
227 1
                if ($defaultSort = Config::inst()->get($throughClass, 'default_sort')) {
228 1
                    $sort[] = $defaultSort;
229
                }
230
            } else {
231 4
                $depClass = $depData;
232 4
                $component = $schema->manyManyComponent($localClass, $dep);
233
234 4
                $table = $component['join'];
235 4
                $childField = $component['childField'];
236 4
                $parentField = $component['parentField'];
237
            }
238
239 5
            $descriptor = [
240 5
                'class' => $depClass,
241 5
                'map' => [],
242 5
            ];
243
244 5
            $idsQuery = SQLSelect::create(
245 5
                [
246 5
                    '"' . $childField . '"',
247 5
                    '"' . $parentField . '"',
248 5
                ],
249 5
                '"' . $table . '"',
250 5
                [
251 5
                    '"' . $parentField . '" IN (' . implode(',', $data) . ')'
252 5
                ],
253 5
                $sort,
254 5
            )->execute();
255
256 5
            $collection = [];
257 5
            $relListReverted = [];
258 5
            foreach ($idsQuery as $row) {
259 5
                $relID = $row[$childField];
260 5
                $localID = $row[$parentField];
261 5
                if (!isset($collection[$localID])) {
262 5
                    $collection[$localID] = [];
263
                }
264 5
                $collection[$localID][] = $relID;
265 5
                $relListReverted[$relID] = 1; //use ids as keys to avoid
266
            }
267
268 5
            if (count($relListReverted)) {
269 5
                $result = DataObject::get($depClass)->filter('ID', array_keys($relListReverted));
270 5
                if (count($depSeq)>1) {
271
                    $result = $result
272
                        ->with(implode('.', array_slice($depSeq, 1)));
273
                }
274
275 5
                foreach ($result as $depRecord) {
276 5
                    $this->eagerLoadingRelatedCache[$depClass][$depRecord->ID] = $depRecord;
277
                }
278
            }
279
280 5
            $descriptor['map'] = $collection;
281 5
            $this->eagerLoadingRelatedMaps['many_many'][$dep] = $descriptor;
282
        }
283
    }
284
285 12
    public function fulfillEagerRelations(DataObject $item)
286
    {
287 12
        foreach ($this->eagerLoadingRelatedMaps['has_one'] as $dep => $depInfo) {
288 6
            $depClass = $depInfo['class'];
289 6
            if (isset($depInfo['map'][$item->ID])) {
290 6
                $depID = $depInfo['map'][$item->ID];
291 6
                if (isset($this->eagerLoadingRelatedCache[$depClass][$depID])) {
292 6
                    $depRecord = $this->eagerLoadingRelatedCache[$depClass][$depID];
293 6
                    $item->setComponent($dep, $depRecord);
294
                }
295
            }
296
        }
297
298 12
        foreach ($this->eagerLoadingRelatedMaps['has_many'] as $dep => $depInfo) {
299 3
            $depClass = $depInfo['class'];
300 3
            $collection = [];
301 3
            if (isset($depInfo['map'][$item->ID])) {
302 3
                foreach ($depInfo['map'][$item->ID] as $depID) {
303 3
                    if (isset($this->eagerLoadingRelatedCache[$depClass][$depID])) {
304 3
                        $depRecord = $this->eagerLoadingRelatedCache[$depClass][$depID];
305 3
                        $collection[] = $depRecord;
306
                    }
307
                }
308
            }
309 3
            if (!method_exists($item, 'addEagerRelation')) {
310 1
                throw new EagerLoadingException(
311 1
                    "Model {$item->ClassName} must include " .
312 1
                    EagerLoaderMultiAccessor::class .
313 1
                    " trait to use eager loading for \$has_many"
314 1
                );
315
            }
316 2
            $item->addEagerRelation($dep, $collection);
317
        }
318
319 11
        foreach ($this->eagerLoadingRelatedMaps['many_many'] as $dep => $depInfo) {
320 5
            $depClass = $depInfo['class'];
321 5
            $collection = [];
322 5
            if (isset($depInfo['map'][$item->ID])) {
323 5
                foreach ($depInfo['map'][$item->ID] as $depID) {
324
                    // foreach ($depIDlist as $depID) {
325 5
                        if (isset($this->eagerLoadingRelatedCache[$depClass][$depID])) {
326 5
                            $depRecord = $this->eagerLoadingRelatedCache[$depClass][$depID];
327 5
                            $collection[] = $depRecord;
328
                        }
329
                    // }
330
                }
331
            }
332 5
            if (!method_exists($item, 'addEagerRelation')) {
333
                throw new EagerLoadingException(
334
                    "Model {$item->ClassName} must include " .
335
                    EagerLoaderMultiAccessor::class .
336
                    " trait to use eager loading for \$many_many"
337
                );
338
            }
339 5
            $item->addEagerRelation($dep, $collection);
340
        }
341
    }
342
    /**
343
     * Returns a generator for this DataList
344
     *
345
     * @return \Generator&DataObject[]
346
     */
347
    public function getGenerator()
348
    {
349
        $query = $this->dataQuery()->execute();
350
351
        while ($row = $query->record()) {
352
            yield $this->createDataObject($row);
353
        }
354
    }
355
356 11
    private function eagerLoadingPrepareCache($all, $selected)
357
    {
358 11
        foreach ($selected as $depSeq) {
359 11
            $dep = $depSeq[0];
360 11
            $depClass = $all[$dep];
361 11
            $depClass = $depClass['through'] ?? $depClass;
362 11
            if (!isset($this->eagerLoadingRelatedCache[$depClass])) {
363 11
                $this->eagerLoadingRelatedCache[$depClass] = [];
364
            }
365
        }
366
    }
367
}
368