Passed
Branch master (3dddd2)
by Sergey
04:58 queued 01:12
created

EagerLoadedDataList::eagerLoadHasMany()   B

Complexity

Conditions 6
Paths 17

Size

Total Lines 39
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 22
CRAP Score 6.1308

Importance

Changes 2
Bugs 1 Features 0
Metric Value
eloc 29
c 2
b 1
f 0
dl 0
loc 39
ccs 22
cts 26
cp 0.8462
rs 8.8337
cc 6
nc 17
nop 3
crap 6.1308
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($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
            if (false !== strpos($depClass, '.')) {
167
                $dcSplit = explode('.', $depClass, 2);
168
                $depClass = $dcSplit[0];
169
                $localNameInDep = $dcSplit[1];
170
            } else {
171 3
                $localNameInDep = $localClassTail;
172
            }
173 3
            $depKey = "{$localNameInDep}ID";
174
            $descriptor = [
175 3
                'class' => $depClass,
176 3
                'remoteRelation' => $localNameInDep,
177 3
                'remoteField' => $depKey,
178
                'map' => [],
179
            ];
180 3
            $result = DataObject::get($depClass)->filter($depKey, $data);
181 3
            if (count($depSeq)>1) {
182
                $result = $result
183
                    ->with(implode('.', array_slice($depSeq, 1)));
184
            }
185
186 3
            $collection = [];
187
188 3
            foreach ($data as $localRecordID) {
189 3
                $collection[$localRecordID] = [];
190
            }
191 3
            foreach ($result as $depRecord) {
192 3
                $this->eagerLoadingRelatedCache[$depClass][$depRecord->ID] = $depRecord;
193 3
                $collection[$depRecord->$depKey][] = $depRecord->ID;
194
            }
195 3
            $descriptor['map'] = $collection;
196 3
            $this->eagerLoadingRelatedMaps['has_many'][$dep] = $descriptor;
197
        }
198 3
    }
199
200 4
    public function eagerLoadManyMany(&$data, $manyManys, $withManyManys)
201
    {
202 4
        $localClass = $this->dataClass();
203 4
        $schema = DataObject::getSchema();
204
205 4
        foreach ($withManyManys as $depSeq) {
206 4
            $dep = $depSeq[0];
207 4
            $depClass = $manyManys[$dep];
208
209 4
            $component = $schema->manyManyComponent($localClass, $dep);
210
211
            $descriptor = [
212 4
                'class' => $depClass,
213
                'map' => [],
214
            ];
215
216 4
            $idsQuery = SQLSelect::create(
217
                [
218 4
                    '"' . $component['childField'] . '"',
219 4
                    '"' . $component['parentField'] . '"',
220
                ],
221 4
                '"' . $component['join'] . '"',
222
                [
223 4
                    '"' . $component['parentField'] . '" IN (' . implode(',', $data) . ')'
224
                ]
225 4
            )->execute();
226
227 4
            $collection = [];
228 4
            $relListReverted = [];
229 4
            foreach ($idsQuery as $row) {
230 4
                $relID = $row[$component['childField']];
231 4
                $localID = $row[$component['parentField']];
232 4
                if (!isset($collection[$localID])) {
233 4
                    $collection[$localID] = [];
234
                }
235 4
                $collection[$localID][] = $relID;
236 4
                $relListReverted[$relID] = 1; //use ids as keys to avoid
237
            }
238
239 4
            $result = DataObject::get($depClass)->filter('ID', array_keys($relListReverted));
240 4
            if (count($depSeq)>1) {
241
                $result = $result
242
                    ->with(implode('.', array_slice($depSeq, 1)));
243
            }
244
245 4
            foreach ($result as $depRecord) {
246 4
                $this->eagerLoadingRelatedCache[$depClass][$depRecord->ID] = $depRecord;
247
            }
248
249 4
            $descriptor['map'] = $collection;
250 4
            $this->eagerLoadingRelatedMaps['has_many'][$dep] = $descriptor;
251
        }
252 4
    }
253
254 11
    public function fulfillEagerRelations(DataObject $item)
255
    {
256 11
        foreach ($this->eagerLoadingRelatedMaps['has_one'] as $dep => $depInfo) {
257 6
            $depClass = $depInfo['class'];
258 6
            if (isset($depInfo['map'][$item->ID])) {
259 6
                $depID = $depInfo['map'][$item->ID];
260 6
                if (isset($this->eagerLoadingRelatedCache[$depClass][$depID])) {
261 6
                    $depRecord = $this->eagerLoadingRelatedCache[$depClass][$depID];
262 6
                    $item->setComponent($dep, $depRecord);
263
                }
264
            }
265
        }
266
267 11
        foreach ($this->eagerLoadingRelatedMaps['has_many'] as $dep => $depInfo) {
268 7
            $depClass = $depInfo['class'];
269 7
            $collection = [];
270 7
            if (isset($depInfo['map'][$item->ID])) {
271 7
                foreach ($depInfo['map'][$item->ID] as $depID) {
272 7
                    if (isset($this->eagerLoadingRelatedCache[$depClass][$depID])) {
273 7
                        $depRecord = $this->eagerLoadingRelatedCache[$depClass][$depID];
274 7
                        $collection[] = $depRecord;
275
                    }
276
                }
277
            }
278 7
            if (!method_exists($item, 'addEagerRelation')) {
279 1
                throw new EagerLoadingException(
280 1
                    "Model {$item->ClassName} must include " .
281 1
                    EagerLoaderMultiAccessor::class .
282 1
                    " trait to use eager loading for \$has_many"
283
                );
284
            }
285 6
            $item->addEagerRelation($dep, $collection);
286
        }
287
288 10
        foreach ($this->eagerLoadingRelatedMaps['many_many'] as $dep => $depInfo) {
289
            $depClass = $depInfo['class'];
290
            $collection = [];
291
            if (isset($depInfo['map'][$item->ID])) {
292
                foreach ($depInfo['map'][$item->ID] as $depIDlist) {
293
                    foreach ($depIDlist as $depID) {
294
                        if (isset($this->eagerLoadingRelatedCache[$depClass][$depID])) {
295
                            $depRecord = $this->eagerLoadingRelatedCache[$depClass][$depID];
296
                            $collection[] = $depRecord;
297
                        }
298
                    }
299
                }
300
            }
301
            if (!method_exists($item, 'addEagerRelation')) {
302
                throw new EagerLoadingException(
303
                    "Model {$item->ClassName} must include " .
304
                    EagerLoaderMultiAccessor::class .
305
                    " trait to use eager loading for \$many_many"
306
                );
307
            }
308
            $item->addEagerRelation($dep, $collection);
309
        }
310 10
    }
311
    /**
312
     * Returns a generator for this DataList
313
     *
314
     * @return \Generator&DataObject[]
315
     */
316
    public function getGenerator()
317
    {
318
        $query = $this->query()->execute();
319
320
        while ($row = $query->record()) {
321
            yield $this->createDataObject($row);
322
        }
323
    }
324
325 10
    private function eagerLoadingPrepareCache($all, $selected)
326
    {
327 10
        foreach ($selected as $depSeq) {
328 10
            $dep = $depSeq[0];
329 10
            $depClass = $all[$dep];
330 10
            if (!isset($this->eagerLoadingRelatedCache[$depClass])) {
331 10
                $this->eagerLoadingRelatedCache[$depClass] = [];
332
            }
333
        }
334 10
    }
335
}
336