EagerLoad::load()   F
last analyzed

Complexity

Conditions 33
Paths 2105

Size

Total Lines 218
Code Lines 135

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 1122

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 33
eloc 135
c 3
b 0
f 0
nc 2105
nop 0
dl 0
loc 218
ccs 0
cts 135
cp 0
crap 1122
rs 0

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
3
/**
4
 * This file is part of the Zemit Framework.
5
 *
6
 * (c) Zemit Team <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE.txt
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Zemit\Mvc\Model\EagerLoading;
13
14
use Phalcon\Mvc\EntityInterface;
15
use Phalcon\Mvc\Model\Relation;
16
use Phalcon\Mvc\Model\RelationInterface;
17
18
/**
19
 * Represents a level in the relations tree to be eagerly loaded
20
 */
21
final class EagerLoad
22
{
23
    private RelationInterface $relation;
24
    
25
    /** @var null|callable */
26
    private $constraints;
27
    
28
    /** @var Loader|EagerLoad */
29
    private $parent;
30
    
31
    /** @var null|\Phalcon\Mvc\ModelInterface[] */
32
    private ?array $subject = null;
33
    
34
    /**
35
     * @param Relation $relation
36
     * @param null|callable $constraints
37
     * @param Loader|EagerLoad $parent
38
     */
39
    public function __construct(Relation $relation, $constraints, $parent)
40
    {
41
        $this->relation = $relation;
42
        $this->constraints = is_callable($constraints) ? $constraints : null;
43
        $this->parent = $parent;
44
    }
45
    
46
    /**
47
     * @return null|\Phalcon\Mvc\ModelInterface[]
48
     */
49
    public function getSubject()
50
    {
51
        return $this->subject;
52
    }
53
    
54
    /**
55
     * Executes each db query needed
56
     *
57
     * Note: The {$alias} property is set two times because Phalcon Model ignores
58
     * empty arrays when overloading property set.
59
     *
60
     * Also {@see https://github.com/stibiumz/phalcon.eager-loading/issues/1}
61
     *
62
     * @return $this
63
     */
64
    public function load()
65
    {
66
        $parentSubject = $this->parent->getSubject();
67
        
68
        if (empty($parentSubject)) {
69
            return $this;
70
        }
71
        
72
        $relation = $this->relation;
73
        
74
        $options = $relation->getOptions();
75
        $alias = strtolower($options['alias']);
76
        $relField = $relation->getFields();
77
        $relReferencedModel = $relation->getReferencedModel();
78
        $relReferencedField = $relation->getReferencedFields();
79
        $relIrModel = $relation->getIntermediateModel();
80
        $relIrField = $relation->getIntermediateFields();
81
        $relIrReferencedField = $relation->getIntermediateReferencedFields();
82
        
83
        // @todo support multiples fields with eager loading
84
        if (is_array($relField)) {
85
            throw new \RuntimeException('Relation field must be a string, multiple fields are not supported yet.');
86
        }
87
        if (is_array($relReferencedField)) {
88
            throw new \RuntimeException('Relation Referenced field must be a string, multiple fields are not supported yet.');
89
        }
90
        if (is_array($relIrField)) {
91
            throw new \RuntimeException('Relation Intermediate field must be a string, multiple fields are not supported yet.');
92
        }
93
        if (is_array($relIrReferencedField)) {
94
            throw new \RuntimeException('Relation Intermediate Referenced field must be a string, multiple fields are not supported yet.');
95
        }
96
        
97
        // PHQL has problems with this slash
98
        if ($relReferencedModel[0] === '\\') {
99
            $relReferencedModel = ltrim($relReferencedModel, '\\');
100
        }
101
        
102
        $bindValues = [];
103
        foreach ($parentSubject as $record) {
104
            assert($record instanceof EntityInterface);
105
            $relFieldAr = is_array($relField)? $relField : [$relField];
106
            foreach ($relFieldAr as $relField) {
107
                $bindValues[$record->readAttribute($relField)] = true;
108
            }
109
        }
110
        unset($record);
111
        
112
        $bindValues = array_keys($bindValues);
113
        
114
        $subjectCount = count($parentSubject);
115
        $isManyToManyForMany = false;
116
        
117
        $builder = new QueryBuilder();
118
        $builder->from($relReferencedModel);
119
        
120
        if ($isThrough = $relation->isThrough()) {
121
            if ($subjectCount === 1) {
122
                // The query is for a single model
123
                $builder
124
                    ->innerJoin(
125
                        $relIrModel,
126
                        sprintf(
127
                            '[%s].[%s] = [%s].[%s]',
128
                            $relIrModel,
129
                            $relIrReferencedField,
130
                            $relReferencedModel,
131
                            $relReferencedField
132
                        )
133
                    )
134
                    ->where('[' . $relIrModel . '].[deleted] <> 1') // @todo do this correctly
135
                    ->inWhere("[{$relIrModel}].[{$relIrField}]", $bindValues)
136
                ;
137
            }
138
            else {
139
                // The query is for many models, so it's needed to execute an
140
                // extra query
141
                $isManyToManyForMany = true;
142
                
143
                $relIrValues = new QueryBuilder();
144
                $relIrValues = $relIrValues
145
                    ->from($relIrModel)
146
                    ->where('[' . $relIrModel . '].[deleted] <> 1') // @todo do this correctly
147
                    ->inWhere("[{$relIrModel}].[{$relIrField}]", $bindValues)
148
                    ->getQuery()
149
                    ->execute()
150
                ;
151
                
152
                $bindValues = [];
153
                $modelReferencedModelValues = [];
154
                foreach ($relIrValues as $row) {
155
                    $bindValues[$row->readAttribute($relIrReferencedField)] = true;
156
                    $modelReferencedModelValues[$row->readAttribute($relIrField)][$row->readAttribute($relIrReferencedField)] = true;
157
                }
158
                unset($relIrValues);
159
                unset($row);
160
                
161
                $builder->inWhere(
162
                    "[{$relReferencedModel}].[{$relReferencedField}]",
163
                    array_keys($bindValues)
164
                );
165
            }
166
        }
167
        else {
168
            $builder->inWhere(
169
                "[{$relReferencedModel}].[{$relReferencedField}]",
170
                $bindValues
171
            );
172
        }
173
        
174
        $constraint = $this->constraints;
175
        if (is_callable($constraint)) {
176
            $constraint($builder);
177
        }
178
        
179
        $records = [];
180
        
181
        if ($isManyToManyForMany) {
182
            foreach ($builder->getQuery()->execute() as $record) {
183
                $records[$record->readAttribute($relReferencedField)] = $record;
184
            }
185
            unset($record);
186
            
187
            foreach ($parentSubject as $record) {
188
                assert($record instanceof EntityInterface);
189
                $referencedFieldValue = $record->readAttribute($relField);
190
                
191
                if (isset($modelReferencedModelValues[$referencedFieldValue])) {
192
                    $referencedModels = [];
193
                    
194
                    foreach ($modelReferencedModelValues[$referencedFieldValue] as $idx => $_) {
195
                        if (isset($records[$idx])) {
196
                            $referencedModels[] = $records[$idx];
197
                        }
198
                    }
199
                    
200
                    $record->{$alias} = $referencedModels;
201
                    $record->{$alias} = null;
202
                    $record->{$alias} = $referencedModels;
203
                }
204
                else {
205
                    $record->{$alias} = null;
206
                    $record->{$alias} = [];
207
                }
208
            }
209
            unset($record);
210
            
211
            $records = array_values($records);
212
        }
213
        else {
214
            // We expect a single object or a set of it
215
            $isSingle = !$isThrough && (
216
                    $relation->getType() === Relation::HAS_ONE ||
217
                    $relation->getType() === Relation::BELONGS_TO
218
                );
219
            
220
            if ($subjectCount === 1) {
221
                // Keep all records in memory
222
                foreach ($builder->getQuery()->execute() as $record) {
223
                    $records[] = $record;
224
                }
225
                unset($record);
226
                
227
                $record = $parentSubject[0];
228
                if ($isSingle) {
229
                    $record->{$alias} = empty($records) ? null : $records[0];
230
                }
231
                elseif (empty($records)) {
232
                    $record->{$alias} = null;
233
                    $record->{$alias} = [];
234
                }
235
                else {
236
                    $record->{$alias} = $records;
237
                    $record->{$alias} = null;
238
                    $record->{$alias} = $records;
239
                }
240
            }
241
            else {
242
                $indexedRecords = [];
243
                
244
                // Keep all records in memory
245
                foreach ($builder->getQuery()->execute() as $record) {
246
                    $records[] = $record;
247
                    
248
                    if ($isSingle) {
249
                        $indexedRecords[$record->readAttribute($relReferencedField)] = $record;
250
                    }
251
                    else {
252
                        $indexedRecords[$record->readAttribute($relReferencedField)][] = $record;
253
                    }
254
                }
255
                
256
                foreach ($parentSubject as $record) {
257
                    assert($record instanceof EntityInterface);
258
                    $referencedFieldValue = $record->readAttribute($relField);
259
                    
260
                    if (isset($indexedRecords[$referencedFieldValue])) {
261
                        $record->{$alias} = $indexedRecords[$referencedFieldValue];
262
                        
263
                        if (is_array($indexedRecords[$referencedFieldValue])) {
264
                            $record->{$alias} = null;
265
                            $record->{$alias} = $indexedRecords[$referencedFieldValue];
266
                        }
267
                    }
268
                    else {
269
                        $record->{$alias} = null;
270
                        if (!$isSingle) {
271
                            $record->{$alias} = [];
272
                        }
273
                    }
274
                }
275
                unset($record);
276
            }
277
        }
278
        
279
        $this->subject = $records;
280
        
281
        return $this;
282
    }
283
}
284