Passed
Pull Request — 2.x (#431)
by Aleksei
25:07 queued 09:50
created

HasMany::prepare()   C

Complexity

Conditions 12
Paths 51

Size

Total Lines 44
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 25
CRAP Score 12

Importance

Changes 0
Metric Value
cc 12
eloc 29
nc 51
nop 4
dl 0
loc 44
ccs 25
cts 25
cp 1
crap 12
rs 6.9666
c 0
b 0
f 0

How to fix   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
declare(strict_types=1);
4
5
namespace Cycle\ORM\Relation;
6
7
use Cycle\ORM\Exception\Relation\BadRelationValueException;
8
use Cycle\ORM\FactoryInterface;
9
use Cycle\ORM\Heap\Node;
10
use Cycle\ORM\MapperInterface;
11
use Cycle\ORM\ORMInterface;
12
use Cycle\ORM\Reference\EmptyReference;
13
use Cycle\ORM\Reference\Reference;
14
use Cycle\ORM\Reference\ReferenceInterface;
15
use Cycle\ORM\Relation;
16
use Cycle\ORM\Relation\Traits\HasSomeTrait;
17
use Cycle\ORM\Select;
18
use Cycle\ORM\Select\LoaderInterface;
19
use Cycle\ORM\Service\EntityFactoryInterface;
20
use Cycle\ORM\Service\SourceProviderInterface;
21
use Cycle\ORM\Transaction\Pool;
22
use Cycle\ORM\Transaction\Tuple;
23
24
/**
25
 * Provides the ability to own the collection of entities.
26
 *
27
 * @internal
28
 */
29
class HasMany extends AbstractRelation
30
{
31
    use HasSomeTrait;
0 ignored issues
show
introduced by
The trait Cycle\ORM\Relation\Traits\HasSomeTrait requires some properties which are not provided by Cycle\ORM\Relation\HasMany: $state, $entity
Loading history...
32
33 2744
    protected FactoryInterface $factory;
34
    protected Select $select;
35 2744
36 2744
    public function __construct(ORMInterface $orm, string $role, string $name, string $target, array $schema)
37 2744
    {
38
        parent::__construct($orm, $role, $name, $target, $schema);
39
        $sourceProvider = $orm->getService(SourceProviderInterface::class);
40 2744
        $this->factory = $orm->getFactory();
41 2744
42 2744
        // Prepare Select Statement
43
        $this->select = (new Select($orm, $this->target))
44
            ->scope($sourceProvider->getSource($this->target)->getScope())
45 996
            ->orderBy($this->schema[Relation::ORDER_BY] ?? []);
46
    }
47 996
48 996
    public function prepare(Pool $pool, Tuple $tuple, mixed $related, bool $load = true): void
49 996
    {
50
        $node = $tuple->node;
51 996
        $original = $node->getRelation($this->getName());
52 466
        $tuple->state->setRelation($this->getName(), $related);
53 346
54 346
        if ($original instanceof ReferenceInterface) {
55
            if (!$load && $this->compareReferences($original, $related) && !$original->hasValue()) {
56 144
                $tuple->state->setRelationStatus($this->getName(), RelationInterface::STATUS_RESOLVED);
57 144
                return;
58
            }
59
            $original = $this->resolve($original, true);
60 706
            $node->setRelation($this->getName(), $original);
61 8
        }
62 8
63
        if ($related instanceof ReferenceInterface) {
64 706
            $related = $this->resolve($related, true);
65 224
            $tuple->state->setRelation($this->getName(), $related);
66
        } elseif (!\is_iterable($related)) {
67
            if ($related === null) {
68 706
                $related = $this->collect([]);
69 248
            } else {
70 248
                throw new BadRelationValueException(\sprintf(
71
                    'Value for Has Many relation must be of the iterable type, %s given.',
72 610
                    \get_debug_type($related),
73
                ));
74
            }
75
        }
76 610
        foreach ($this->calcDeleted($related, $original ?? []) as $item) {
0 ignored issues
show
Bug introduced by
It seems like $original ?? array() can also be of type null; however, parameter $original of Cycle\ORM\Relation\HasMany::calcDeleted() does only seem to accept iterable, maybe add an additional type check? ( Ignorable by Annotation )

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

76
        foreach ($this->calcDeleted($related, /** @scrutinizer ignore-type */ $original ?? []) as $item) {
Loading history...
Bug introduced by
It seems like $related can also be of type null; however, parameter $related of Cycle\ORM\Relation\HasMany::calcDeleted() does only seem to accept iterable, maybe add an additional type check? ( Ignorable by Annotation )

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

76
        foreach ($this->calcDeleted(/** @scrutinizer ignore-type */ $related, $original ?? []) as $item) {
Loading history...
77 610
            $this->deleteChild($pool, $tuple, $item);
78 610
        }
79 610
80
        if (\count($related) === 0) {
0 ignored issues
show
Bug introduced by
It seems like $related can also be of type null; however, parameter $value of count() does only seem to accept Countable|array, maybe add an additional type check? ( Ignorable by Annotation )

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

80
        if (\count(/** @scrutinizer ignore-type */ $related) === 0) {
Loading history...
81
            $tuple->state->setRelationStatus($this->getName(), RelationInterface::STATUS_RESOLVED);
82
            return;
83
        }
84
        $tuple->state->setRelationStatus($this->getName(), RelationInterface::STATUS_PROCESS);
85
86 610
        // $relationName = $this->getTargetRelationName()
87
        // Store new and existing items
88 610
        foreach ($related as $item) {
89 610
            $rTuple = $pool->attachStore($item, true);
90 610
            $this->assertValid($rTuple->node);
91
            if ($this->isNullable()) {
92 610
                // todo?
93
                // $rNode->setRelationStatus($relationName, RelationInterface::STATUS_DEFERRED);
94 610
            }
95
        }
96
    }
97
98
    public function queue(Pool $pool, Tuple $tuple): void
99 610
    {
100 610
        $node = $tuple->node;
0 ignored issues
show
Unused Code introduced by
The assignment to $node is dead and can be removed.
Loading history...
101
        $related = $tuple->state->getRelation($this->getName());
102 610
        $related = $this->extract($related);
103
104 610
        $tuple->state->setRelationStatus($this->getName(), RelationInterface::STATUS_RESOLVED);
105 136
106
        if ($related instanceof ReferenceInterface && !$related->hasValue()) {
0 ignored issues
show
introduced by
$related is never a sub-type of Cycle\ORM\Reference\ReferenceInterface.
Loading history...
107
            return;
108 104
        }
109
110
        // Fill related
111 136
        $relationName = $this->getTargetRelationName();
112 136
        foreach ($related as $item) {
113
            /** @var Tuple $rTuple */
114
            $rTuple = $pool->offsetGet($item);
115 530
116 530
            if ($this->inversion !== null) {
117
                if ($rTuple->node->getStatus() === Node::NEW) {
118
                    // For existing entities it can be unwanted
119
                    // if Reference to Parent will be rewritten by Parent Entity
120
                    $rTuple->state->setRelation($relationName, $tuple->entity);
121
                }
122
123 1000
                if ($rTuple->state->getRelationStatus($relationName) === RelationInterface::STATUS_PREPARE) {
124
                    continue;
125 1000
                }
126 1000
            }
127 896
            $this->applyChanges($tuple, $rTuple);
128
            $rTuple->state->setRelationStatus($relationName, RelationInterface::STATUS_RESOLVED);
129
        }
130 1000
    }
131 1000
132
    /**
133
     * Init relation state and entity collection.
134 1272
     */
135
    public function init(EntityFactoryInterface $factory, Node $node, array $data): iterable
136 1272
    {
137 504
        $elements = [];
138
        foreach ($data as $item) {
139 1192
            $elements[] = $factory->make($this->target, $item, Node::MANAGED);
140 1192
        }
141
142 1192
        $node->setRelation($this->getName(), $elements);
143 1192
        return $this->collect($elements);
144
    }
145 1192
146
    public function cast(?array $data): array
147
    {
148 1230
        if (!$data) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $data of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
149
            return [];
150 1230
        }
151 1230
152 112
        /** @var array<non-empty-string, MapperInterface> $mappers Mappers cache */
153 1230
        $mappers = [];
154
155
        foreach ($data as $key => $item) {
156 1230
            $role = $item[LoaderInterface::ROLE_KEY] ?? $this->target;
157
            $mappers[$role] ??= $this->mapperProvider->getMapper($role);
158 1230
            // break link
159 1230
            unset($data[$key]);
160 1230
            $data[$key] = $mappers[$role]->cast($item);
161 1230
        }
162 112
        return $data;
163
    }
164 1150
165
    public function initReference(Node $node): ReferenceInterface
166 1150
    {
167
        $scope = $this->getReferenceScope($node);
168
        return $scope === null
169 444
            ? new EmptyReference($node->getRole(), [])
170
            : new Reference($this->target, $scope);
171 444
    }
172 176
173
    protected function getReferenceScope(Node $node): ?array
174 372
    {
175
        $scope = [];
176
        $nodeData = $node->getData();
177
        foreach ($this->innerKeys as $i => $key) {
178
            if (!isset($nodeData[$key])) {
179 372
                return null;
180 152
            }
181
            $scope[$this->outerKeys[$i]] = $nodeData[$key];
182
        }
183 268
        return $scope;
184
    }
185 268
186 268
    public function resolve(ReferenceInterface $reference, bool $load): ?iterable
187
    {
188 268
        if ($reference->hasValue()) {
189
            return $reference->getValue();
190 268
        }
191
        if ($reference->getScope() === []) {
192
            // nothing to proxy to
193 1292
            $reference->setValue([]);
194
            return [];
0 ignored issues
show
Bug Best Practice introduced by
The expression return array() returns the type array which is incompatible with the type-hinted return iterable|null.
Loading history...
195 1292
        }
196
        if ($load === false) {
197
            return null;
198 1292
        }
199 1292
200 1292
        $scope = array_merge($reference->getScope(), $this->schema[Relation::WHERE] ?? []);
201
202
        $iterator = (clone $this->select)->where($scope)->getIterator(findInHeap: true);
203
        $result = \iterator_to_array($iterator, false);
204
205
        $reference->setValue($result);
206 706
207
        return $result;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $result returns the type array which is incompatible with the type-hinted return iterable|null.
Loading history...
208 706
    }
209 592
210
    public function collect(mixed $data): iterable
211 706
    {
212 8
        if (!\is_iterable($data)) {
213
            throw new \InvalidArgumentException('Collected data in the HasMany relation should be iterable.');
214 706
        }
215
        return $this->factory->collection(
216
            $this->schema[Relation::COLLECTION_TYPE] ?? null
217
        )->collect($data);
218
    }
219
220 706
    /**
221
     * Convert entity data into array.
222 706
     */
223 706
    public function extract(mixed $data): array
224 706
    {
225 706
        if ($data instanceof \Doctrine\Common\Collections\Collection) {
226
            return $data->toArray();
227
        }
228 706
        if ($data instanceof \Traversable) {
229
            return \iterator_to_array($data);
230
        }
231
        return \is_array($data) ? $data : [];
232
    }
233
234
    /**
235
     * Return objects which are subject of removal.
236
     */
237
    protected function calcDeleted(iterable $related, iterable $original): array
238
    {
239
        $related = $this->extract($related);
240
        $original = $this->extract($original);
241
        return array_udiff(
242
            $original ?? [],
243
            $related,
244
            // static fn(object $a, object $b): int => strcmp(spl_object_hash($a), spl_object_hash($b))
245
            static fn (object $a, object $b): int => (int)($a === $b) - 1
246
        );
247
    }
248
}
249