Loader::__construct()   F
last analyzed

Complexity

Conditions 20
Paths 138

Size

Total Lines 88
Code Lines 50

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 420

Importance

Changes 2
Bugs 1 Features 0
Metric Value
cc 20
eloc 50
c 2
b 1
f 0
nc 138
nop 2
dl 0
loc 88
ccs 0
cts 47
cp 0
crap 420
rs 3.85

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\Di\Di;
15
use Phalcon\Mvc\Model\Relation;
16
use Phalcon\Mvc\Model\Resultset\Complex;
17
use Phalcon\Mvc\Model\Resultset\Simple;
18
use Phalcon\Mvc\Model\ResultsetInterface;
19
use Phalcon\Mvc\Model\Row;
20
use Phalcon\Mvc\ModelInterface;
21
22
final class Loader
23
{
24
    public ?array $subject;
25
    
26
    public string $className;
27
    
28
    public array $eagerLoads;
29
    
30
    public bool $singleModel;
31
    
32
    public array $options = [];
33
    
34
    private const string E_INVALID_SUBJECT = 'Expected value of `subject` to be either a ModelInterface object, a Simple object or an array of ModelInterface objects.';
35
    private const string E_INVALID_CLASSNAME = 'Expected value of `className` to be either an existing class name.';
36
    
37
    /**
38
     * Constructs a new instance of the class.
39
     *
40
     * @param mixed $from The data source from which to load the data. Can be an instance of ModelInterface,
41
     *                    Simple, array, null, or boolean.
42
     * @param array ...$arguments Optional arguments for eager loading. Each argument should be an array
43
     *                            specifying the relationships to eager load.
44
     *
45
     * @throws \InvalidArgumentException If the supplied data source is invalid.
46
     */
47
    public function __construct(mixed $from, array ...$arguments)
48
    {
49
        $error = false;
50
        $className = null;
51
        $this->singleModel = false;
52
        
53
        // Handle Model Interface
54
        if ($from instanceof ModelInterface) {
55
            $className = get_class($from);
56
            $from = [$from];
57
            $this->singleModel = true;
58
        }
59
        
60
        // Handle Simple Resultset
61
        elseif ($from instanceof Simple) {
62
            $from = iterator_to_array($from);
63
            if (isset($from[0])) {
64
                $className = get_class($from[0]);
65
            }
66
            else {
67
                $from = null;
68
            }
69
        }
70
        
71
        // Handle Complex Resultset
72
        elseif ($from instanceof Complex) {
73
            // we will consider the first model to be the main model
74
            $tmp = [];
75
            foreach ($from as $row) {
76
                assert($row instanceof Row);
77
                $array = $row->toArray();
78
                $firstModel = reset($array);
79
                $tmp []= $firstModel;
80
            }
81
            $from = $tmp;
82
            if (isset($from[0])) {
83
                $className = get_class($from[0]);
84
            }
85
            else {
86
                $from = null;
87
            }
88
        }
89
        
90
        // Handle array
91
        elseif (is_array($from)) {
92
            $from = array_filter($from);
93
            if (isset($from[0])) {
94
                $className = get_class($from[0]);
95
            }
96
            foreach ($from as $el) {
97
                if ($el instanceof ModelInterface) {
98
                    // elements must be all the same model class
99
                    if ($className !== get_class($el)) {
100
                        $error = true;
101
                        break;
102
                    }
103
                }
104
                else {
105
                    // element must be a ModelInterface
106
                    $error = true;
107
                    break;
108
                }
109
            }
110
            if (empty($from)) {
111
                $from = null;
112
            }
113
        }
114
        
115
        // Handle null or empty
116
        elseif (is_null($from) || is_bool($from)) {
117
            $from = null;
118
        }
119
        
120
        // error
121
        else {
122
            $error = true;
123
        }
124
        
125
        if ($error) {
126
            throw new \InvalidArgumentException(self::E_INVALID_SUBJECT);
127
        }
128
        if (!isset($className) || !class_exists($className)) {
129
            throw new \InvalidArgumentException(self::E_INVALID_CLASSNAME);
130
        }
131
        
132
        $this->subject = $from;
133
        $this->className = $className;
134
        $this->eagerLoads = ($from === null || empty($arguments)) ? [] : self::parseArguments($arguments);
135
    }
136
    
137
    /**
138
     * Sets the options for the current object instance.
139
     *
140
     * @param array $options An array of options for the current object.
141
     * @return $this The current object instance after setting the options.
142
     */
143
    public function setOptions(array $options = []): self
144
    {
145
        $this->options = $options;
146
        return $this;
147
    }
148
    
149
    /**
150
     * Sets the subject of the object.
151
     *
152
     * @param array|null $subject The subject data array or null to clear the subject.
153
     *
154
     * @return $this The current object instance with the subject set.
155
     */
156
    public function setSubject(?array $subject): self
157
    {
158
        $this->subject = $subject;
159
        return $this;
160
    }
161
    
162
    /**
163
     * Gets the subject
164
     *
165
     * @return ModelInterface[]|null The subject, or null if it has not been set.
166
     */
167
    public function getSubject(): ?array
168
    {
169
        return $this->subject;
170
    }
171
    
172
    /**
173
     * Retrieves the first element from the subject array and returns it.
174
     *
175
     * @return ModelInterface|null The first element from the subject array, or null if the array is empty.
176
     */
177
    public function getFirstSubject(): ?ModelInterface
178
    {
179
        return $this->subject[0] ?? null;
180
    }
181
    
182
    /**
183
     * Creates an instance of the current object from various input types and returns it.
184
     *
185
     * @param mixed $subject The input object or array to create the instance from.
186
     * @param mixed ...$arguments Additional arguments that can be passed to the creation process.
187
     * 
188
     * @return array|ModelInterface|null The current object instance created from the input.
189
     */
190
    public static function from(mixed $subject, mixed ...$arguments): array|ModelInterface|null
191
    {
192
        if ($subject instanceof ModelInterface) {
193
            return self::fromModel($subject, ...$arguments);
194
        }
195
        
196
        else if ($subject instanceof ResultsetInterface) {
197
            return self::fromResultset($subject, ...$arguments);
198
        }
199
        
200
        else if (is_array($subject)) {
201
            return self::fromArray($subject, ...$arguments);
202
        }
203
        
204
        throw new \InvalidArgumentException(Loader::E_INVALID_SUBJECT);
205
    }
206
    
207
    /**
208
     * Create and get from a Model
209
     *
210
     * @param ModelInterface $subject
211
     * @param mixed ...$arguments
212
     * @return ?ModelInterface
213
     */
214
    public static function fromModel(ModelInterface $subject, mixed ...$arguments): ?ModelInterface
215
    {
216
        return (new self($subject, ...$arguments))->execute()->getFirstSubject();
217
    }
218
    
219
    /**
220
     * Create and get from an array
221
     *
222
     * @param ModelInterface[] $subject
223
     * @param mixed ...$arguments
224
     * @return array
225
     */
226
    public static function fromArray(array $subject, mixed ...$arguments): array
227
    {
228
        return (new self($subject, ...$arguments))->execute()->getSubject() ?? [];
229
    }
230
    
231
    /**
232
     * Create and get from a Model without soft deleted records
233
     *
234
     * @param ModelInterface $subject
235
     * @param mixed ...$arguments
236
     * @return ?ModelInterface
237
     */
238
    public static function fromModelWithoutSoftDelete(ModelInterface $subject, mixed ...$arguments): ?ModelInterface
239
    {
240
        $options = ['softDelete' => 'softDelete'];
241
        $obj = new self($subject, ...$arguments);
242
        return $obj->setOptions($options)->execute()->getFirstSubject();
243
    }
244
    
245
    /**
246
     * Create and get from an array without soft deleted records
247
     *
248
     * @param ModelInterface[] $subject
249
     * @param mixed ...$arguments
250
     * @return array
251
     */
252
    public static function fromArrayWithoutSoftDelete(array $subject, mixed ...$arguments): array
253
    {
254
        $options = ['softDelete' => 'softDelete'];
255
        $obj = new self($subject, ...$arguments);
256
        return $obj->setOptions($options)->execute()->getSubject() ?? [];
257
    }
258
    
259
    /**
260
     * Create and get from a Resultset
261
     *
262
     * @param ResultsetInterface $subject
263
     * @param mixed ...$arguments
264
     * @return array
265
     */
266
    public static function fromResultset(ResultsetInterface $subject, mixed ...$arguments): array
267
    {
268
        return (new self($subject, ...$arguments))->execute()->getSubject() ?? [];
269
    }
270
    
271
    /**
272
     * Parses the arguments that will be resolved to Relation instances
273
     *
274
     * @param array $arguments
275
     * @return array
276
     * @throws \InvalidArgumentException
277
     */
278
    private static function parseArguments(array $arguments): array
279
    {
280
        if (empty($arguments)) {
281
            throw new \InvalidArgumentException('Arguments can not be empty');
282
        }
283
        
284
        $relations = [];
285
        if (count($arguments) === 1 && isset($arguments[0]) && is_array($arguments[0])) {
286
            foreach ($arguments[0] as $relationAlias => $queryConstraints) {
287
                if (is_string($relationAlias)) {
288
                    $relations[$relationAlias] = is_callable($queryConstraints) ? $queryConstraints : null;
289
                }
290
                elseif (is_string($queryConstraints)) {
291
                    $relations[$queryConstraints] = null;
292
                }
293
            }
294
        }
295
        else {
296
            foreach ($arguments as $relationAlias) {
297
                if (is_string($relationAlias)) {
298
                    $relations[$relationAlias] = null;
299
                }
300
            }
301
        }
302
        
303
        return $relations;
304
    }
305
    
306
    /**
307
     * Adds an eager load for a given relation alias and optional constraints and returns an instance of the current object.
308
     *
309
     * @param string $relationAlias The alias of the relation to be eagerly loaded.
310
     * @param callable|null $constraints Optional. The callback function that applies constraints on the eager loaded relation. Default is null.
311
     * 
312
     * @return $this The current object instance after adding the eager load.
313
     */
314
    public function addEagerLoad(string $relationAlias, ?callable $constraints = null): self
315
    {
316
        $this->eagerLoads[$relationAlias] = $constraints;
317
        return $this;
318
    }
319
    
320
    /**
321
     * Resolves the relations
322
     *
323
     * @return EagerLoad[]
324
     * @throws \RuntimeException
325
     */
326
    private function buildTree(): array
327
    {
328
        uksort($this->eagerLoads, 'strcmp');
329
        
330
        $di = Di::getDefault();
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $di is correct as Phalcon\Di\Di::getDefault() targeting Phalcon\Di\Di::getDefault() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
331
        $mM = $di['modelsManager'];
332
        
333
        $eagerLoads = $resolvedRelations = [];
334
        
335
        foreach ($this->eagerLoads as $relationAliases => $queryConstraints) {
336
            $nestingLevel = 0;
337
            $relationAliases = explode('.', $relationAliases);
338
            $nestingLevels = count($relationAliases);
339
            
340
            do {
341
                do {
342
                    $alias = $relationAliases[$nestingLevel];
343
                    $name = implode('.', array_slice($relationAliases, 0, $nestingLevel + 1));
344
                    if (isset($eagerLoads[$name])) {
345
                        $nestingLevel++;
346
                    }
347
                }
348
                while (isset($eagerLoads[$name]));
349
                
350
                if ($nestingLevel === 0) {
351
                    $parentClassName = $this->className;
352
                }
353
                else {
354
                    $parentName = implode('.', array_slice($relationAliases, 0, $nestingLevel));
355
                    $parentClassName = $resolvedRelations[$parentName]->getReferencedModel();
356
                    
357
                    if ($parentClassName[0] === '\\') {
358
                        $parentClassName = ltrim($parentClassName, '\\');
359
                    }
360
                }
361
                
362
                if (!isset($resolvedRelations[$name])) {
363
                    $mM->load($parentClassName);
364
                    $relation = $mM->getRelationByAlias($parentClassName, $alias);
365
                    
366
                    if (!$relation instanceof Relation) {
367
                        throw new \RuntimeException(sprintf(
368
                            'There is no defined relation for the model `%s` using alias `%s`',
369
                            $parentClassName,
370
                            $alias
371
                        ));
372
                    }
373
                    
374
                    $resolvedRelations[$name] = $relation;
375
                }
376
                else {
377
                    $relation = $resolvedRelations[$name];
378
                }
379
                
380
                $relType = $relation->getType();
381
                
382
                if ($relType !== Relation::BELONGS_TO &&
383
                    $relType !== Relation::HAS_ONE &&
384
                    $relType !== Relation::HAS_MANY &&
385
                    $relType !== Relation::HAS_MANY_THROUGH
386
                ) {
387
                    
388
                    throw new \RuntimeException(sprintf('Unknown relation type `%s`', $relType));
389
                }
390
                
391
                // @todo allow composite keys
392
//                if (is_array($relation->getFields()) ||
393
//                    is_array($relation->getReferencedFields())
394
//                ) {
395
//                    throw new \RuntimeException('Relations with composite keys are not supported');
396
//                }
397
                
398
                $parent = $nestingLevel > 0 && isset($parentName)? $eagerLoads[$parentName] : $this;
399
                $constraints = $nestingLevel + 1 === $nestingLevels ? $queryConstraints : null;
400
                
401
                $eagerLoads[$name] = new EagerLoad($relation, $constraints, $parent);
402
            }
403
            while (++$nestingLevel < $nestingLevels);
404
        }
405
        
406
        return $eagerLoads;
407
    }
408
    
409
    /**
410
     * Execute the eager loading of related models.
411
     *
412
     * This method iterates through the result of the buildTree method and loads the related models
413
     * using the load method of each eager load instance.
414
     *
415
     * @return self The instance of the class executing the method.
416
     */
417
    public function execute(): self
418
    {
419
        foreach ($this->buildTree() as $eagerLoad) {
420
            // @todo option to enable or disable soft delete etc.
421
//            $eagerLoad->load($this->options); 
422
            $eagerLoad->load();
423
        }
424
        
425
        return $this;
426
    }
427
    
428
    /**
429
     * Loads the data from a data source and returns an instance of the current object.
430
     *
431
     * @return $this The current object instance after loading the data.
432
     */
433
    public function load(): self
434
    {
435
        return $this->execute();
436
    }
437
}
438