Passed
Push — master ( de2eca...123efd )
by Sergey
03:18
created

EagerLoadedDataList::eagerLoadingPrepareCache()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3

Importance

Changes 0
Metric Value
eloc 5
c 0
b 0
f 0
dl 0
loc 7
ccs 6
cts 6
cp 1
rs 10
cc 3
nc 3
nop 2
crap 3
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 $eagerLoadingRelatedMaps = [
19
        'has_one' => [],
20
        'has_many' => [],
21
        'many_many' => [],
22
    ];
23
24 7
    public function __construct($classOrList)
25
    {
26 7
        if (is_string($classOrList)) {
27
            parent::__construct($classOrList);
28
        } else {
29 7
            parent::__construct($classOrList->dataClass());
30 7
            $this->dataQuery = $classOrList->dataQuery();
31
        }
32 7
    }
33
34
    public $eagerLoadingRelatedCache = [];
35 7
    public static function cloneFrom(DataList $list)
36
    {
37 7
        $clone = new EagerLoadedDataList($list);
38
39 7
        $clone->withList = $list->withList;
40 7
        $clone->eagerLoadingRelatedCache = $list->eagerLoadingRelatedCache;
41 7
        return $clone;
42
    }
43
44
    /**
45
     * Create a DataObject from the given SQL row
46
     *
47
     * @param array $row
48
     * @return DataObject
49
     */
50 7
    public function createDataObject($row)
51
    {
52 7
        $this->prepareEagerRelations();
53 7
        $item = parent::createDataObject($row);
54
55 7
        $this->fulfillEagerRelations($item);
56 7
        return $item;
57
    }
58
59
    private $relationsPrepared = false;
60
61 7
    private function filterWithList($list)
62
    {
63 7
        return array_filter(
64 7
            $this->withList,
65 7
            function ($dep) use ($list) {
66 7
                return array_key_exists($dep[0], $list);
67 7
            }
68
        );
69
    }
70
71 7
    public function prepareEagerRelations()
72
    {
73 7
        if ($this->relationsPrepared) {
74 7
            return;
75
        }
76 7
        $this->relationsPrepared = true;
77 7
        $localClass = $this->dataClass();
78 7
        $config = Config::forClass($localClass);
79 7
        $hasOnes = (array) $config->get('has_one');
80 7
        $hasManys = (array) $config->get('has_many');
81 7
        $manyManys = (array) $config->get('many_many');
82 7
        $belongsManyManys = (array) $config->get('belongs_many_many');
83
84
        //collect has_ones
85 7
        $withHasOnes = $this->filterWithList($hasOnes);
86 7
        $withHasManys = $this->filterWithList($hasManys);
87 7
        $withManyManys = $this->filterWithList($manyManys);
88 7
        $withBelongsManyManys = $this->filterWithList($belongsManyManys);
89
90 7
        if (!count($withHasOnes) && !count($withHasManys) && !count($withManyManys) && !count($withBelongsManyManys)) {
91
            // do nothing if no matches
92
            /** @todo report errors */
93
            return;
94
        }
95
96 7
        $data = $this->column('ID');
97 7
        if (count($withHasOnes)) {
98 4
            $this->eagerLoadingPrepareCache($hasOnes, $withHasOnes);
99 4
            $this->eagerLoadHasOne($data, $hasOnes, $withHasOnes);
100
        }
101 7
        if (count($withHasManys)) {
102 2
            $this->eagerLoadingPrepareCache($hasManys, $withHasManys);
103 2
            $this->eagerLoadHasMany($data, $hasManys, $withHasManys);
104
        }
105 7
        if (count($withManyManys)) {
106 1
            $this->eagerLoadingPrepareCache($manyManys, $withManyManys);
107 1
            $this->eagerLoadManyMany($data, $manyManys, $withManyManys);
108
        }
109 7
        if (count($withBelongsManyManys)) {
110 1
            $this->eagerLoadingPrepareCache($belongsManyManys, $withBelongsManyManys);
111 1
            $this->eagerLoadManyMany($data, $belongsManyManys, $withBelongsManyManys);
112
        }
113 7
    }
114
115 4
    public function eagerLoadHasOne(&$ids, $hasOnes, $withHasOnes)
116
    {
117 4
        $schema = DataObject::getSchema();
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
            $component = $schema->hasOneComponent($this->dataClass, $dep);
0 ignored issues
show
Unused Code introduced by
The assignment to $component is dead and can be removed.
Loading history...
140
141 4
            $descriptor['map'] = Utils::extractField($data, $descriptor['localField']);
142 4
            $uniqueIDs = array_unique($descriptor['map']);
143 4
            while (count($uniqueIDs)) {
144 4
                $IDsubset = array_splice($uniqueIDs, 0, self::ID_LIMIT);
145 4
                $result = DataObject::get($depClass)->filter('ID', $IDsubset);
146 4
                if (count($depSeq)>1) {
147
                    $result = $result
148 2
                        ->with(implode('.', array_slice($depSeq, 1)));
149
                }
150
151 4
                foreach ($result as $depRecord) {
152 4
                    $this->eagerLoadingRelatedCache[$depClass][$depRecord->ID] = $depRecord;
153
                }
154
            }
155
156 4
            $this->eagerLoadingRelatedMaps['has_one'][$dep] = $descriptor;
157
        }
158 4
    }
159
160 2
    public function eagerLoadHasMany($data, $hasManys, $withHasManys)
161
    {
162 2
        $localClass = $this->dataClass();
163 2
        $localClassTail = basename(str_replace('\\', '/', $localClass));
164
165 2
        foreach ($withHasManys as $depSeq) {
166 2
            $dep = $depSeq[0];
167 2
            $depClass = $hasManys[$dep];
168 2
            $localNameInDep = $localClassTail;
169 2
            $depKey = "{$localNameInDep}ID";
170
            $descriptor = [
171 2
                'class' => $depClass,
172 2
                'remoteRelation' => $localNameInDep,
173 2
                'remoteField' => $depKey,
174
                'map' => [],
175
            ];
176 2
            $result = DataObject::get($depClass)->filter($depKey, $data);
177 2
            if (count($depSeq)>1) {
178
                $result = $result
179
                    ->with(implode('.', array_slice($depSeq, 1)));
180
            }
181
182 2
            $collection = [];
183
184 2
            foreach ($data as $localRecordID) {
185 2
                $collection[$localRecordID] = [];
186
            }
187 2
            foreach ($result as $depRecord) {
188 2
                $this->eagerLoadingRelatedCache[$depClass][$depRecord->ID] = $depRecord;
189 2
                $collection[$depRecord->$depKey][] = $depRecord->ID;
190
            }
191 2
            $descriptor['map'] = $collection;
192 2
            $this->eagerLoadingRelatedMaps['has_many'][$dep] = $descriptor;
193
        }
194 2
    }
195
196 2
    public function eagerLoadManyMany(&$data, $manyManys, $withManyManys)
197
    {
198 2
        $localClass = $this->dataClass();
199 2
        $schema = DataObject::getSchema();
200
201 2
        foreach ($withManyManys as $depSeq) {
202 2
            $dep = $depSeq[0];
203 2
            $depClass = $manyManys[$dep];
204
205 2
            $component = $schema->manyManyComponent($localClass, $dep);
206
207
            $descriptor = [
208 2
                'class' => $depClass,
209
                'map' => [],
210
            ];
211
212 2
            $idsQuery = SQLSelect::create(
213 2
                implode(',', [$component['childField'], $component['parentField']]),
214 2
                $component['join'],
215
                [
216 2
                    $component['parentField'] . ' IN (' . implode(',', $data) . ')'
217
                ]
218 2
            )->execute();
219
220 2
            $collection = [];
221 2
            $relListReverted = [];
222 2
            foreach ($idsQuery as $row) {
223 2
                $relID = $row[$component['childField']];
224 2
                $localID = $row[$component['parentField']];
225 2
                if (!isset($collection[$localID])) {
226 2
                    $collection[$localID] = [];
227
                }
228 2
                $collection[$localID][] = $relID;
229 2
                $relListReverted[$relID] = 1; //use ids as keys to avoid
230
            }
231
232 2
            $result = DataObject::get($depClass)->filter('ID', array_keys($relListReverted));
233 2
            if (count($depSeq)>1) {
234
                $result = $result
235
                    ->with(implode('.', array_slice($depSeq, 1)));
236
            }
237
238 2
            foreach ($result as $depRecord) {
239 2
                $this->eagerLoadingRelatedCache[$depClass][$depRecord->ID] = $depRecord;
240
            }
241
242 2
            $descriptor['map'] = $collection;
243 2
            $this->eagerLoadingRelatedMaps['has_many'][$dep] = $descriptor;
244
        }
245 2
    }
246
247 7
    public function fulfillEagerRelations(DataObject $item)
248
    {
249 7
        foreach ($this->eagerLoadingRelatedMaps['has_one'] as $dep => $depInfo) {
250 4
            $depClass = $depInfo['class'];
251 4
            if (isset($depInfo['map'][$item->ID])) {
252 4
                $depID = $depInfo['map'][$item->ID];
253 4
                if (isset($this->eagerLoadingRelatedCache[$depClass][$depID])) {
254 4
                    $depRecord = $this->eagerLoadingRelatedCache[$depClass][$depID];
255 4
                    $item->setComponent($dep, $depRecord);
256
                }
257
            }
258
        }
259
260 7
        foreach ($this->eagerLoadingRelatedMaps['has_many'] as $dep => $depInfo) {
261 4
            $depClass = $depInfo['class'];
262 4
            $collection = [];
263 4
            if (isset($depInfo['map'][$item->ID])) {
264 4
                foreach ($depInfo['map'][$item->ID] as $depID) {
265 4
                    if (isset($this->eagerLoadingRelatedCache[$depClass][$depID])) {
266 4
                        $depRecord = $this->eagerLoadingRelatedCache[$depClass][$depID];
267 4
                        $collection[] = $depRecord;
268
                    }
269
                }
270
            }
271 4
            if (!method_exists($item, 'addEagerRelation')) {
272
                throw new \Exception(
273
                    "Model {$item->ClassName} must include " .
274
                    get_class(EagerLoaderMultiAccessor::class) .
0 ignored issues
show
Bug introduced by
Gurucomkz\EagerLoading\E...derMultiAccessor::class of type string is incompatible with the type object expected by parameter $object of get_class(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

274
                    get_class(/** @scrutinizer ignore-type */ EagerLoaderMultiAccessor::class) .
Loading history...
275
                    " trait to use eager loading for \$has_many"
276
                );
277
            }
278 4
            $item->addEagerRelation($dep, $collection);
279
        }
280
281 7
        foreach ($this->eagerLoadingRelatedMaps['many_many'] as $dep => $depInfo) {
282
            $depClass = $depInfo['class'];
283
            $collection = [];
284
            if (isset($depInfo['map'][$item->ID])) {
285
                foreach ($depInfo['map'][$item->ID] as $depIDlist) {
286
                    foreach ($depIDlist as $depID) {
287
                        if (isset($this->eagerLoadingRelatedCache[$depClass][$depID])) {
288
                            $depRecord = $this->eagerLoadingRelatedCache[$depClass][$depID];
289
                            $collection[] = $depRecord;
290
                        }
291
                    }
292
                }
293
            }
294
            if (!method_exists($item, 'addEagerRelation')) {
295
                throw new \Exception(
296
                    "Model {$item->ClassName} must include " .
297
                    get_class(EagerLoaderMultiAccessor::class) .
298
                    " trait to use eager loading for \$has_many"
299
                );
300
            }
301
            $item->addEagerRelation($dep, $collection);
302
        }
303 7
    }
304
    /**
305
     * Returns a generator for this DataList
306
     *
307
     * @return \Generator&DataObject[]
308
     */
309
    public function getGenerator()
310
    {
311
        $query = $this->query()->execute();
312
313
        while ($row = $query->record()) {
314
            yield $this->createDataObject($row);
315
        }
316
    }
317
318 7
    private function eagerLoadingPrepareCache($all, $selected)
319
    {
320 7
        foreach ($selected as $depSeq) {
321 7
            $dep = $depSeq[0];
322 7
            $depClass = $all[$dep];
323 7
            if (!isset($this->eagerLoadingRelatedCache[$depClass])) {
324 7
                $this->eagerLoadingRelatedCache[$depClass] = [];
325
            }
326
        }
327 7
    }
328
}
329