Completed
Push — 2.x ( 0b6590...78009d )
by Aleksei
20s queued 15s
created

HasMany   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 223
Duplicated Lines 0 %

Test Coverage

Coverage 96.19%

Importance

Changes 0
Metric Value
eloc 112
dl 0
loc 223
ccs 101
cts 105
cp 0.9619
rs 8.96
c 0
b 0
f 0
wmc 43

11 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 10 1
A init() 0 9 2
A collect() 0 8 2
A cast() 0 17 3
A calcDeleted() 0 9 1
B queue() 0 31 7
A resolve() 0 22 4
A initReference() 0 6 2
C prepare() 0 50 14
A extract() 0 9 4
A getReferenceScope() 0 11 3

How to fix   Complexity   

Complex Class

Complex classes like HasMany often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use HasMany, and based on these observations, apply Extract Interface, too.

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
60 706
            $original = $this->resolve($original, true);
61 8
            $node->setRelation($this->getName(), $original);
62 8
        }
63
64 706
        if ($related instanceof ReferenceInterface) {
65 224
            $related = $this->resolve($related, true);
66
            $tuple->state->setRelation($this->getName(), $related);
67
        } elseif (SpecialValue::isNotSet($related)) {
68 706
            $tuple->state->setRelationStatus($this->getName(), RelationInterface::STATUS_RESOLVED);
69 248
            return;
70 248
        } elseif (!\is_iterable($related)) {
71
            if ($related === null) {
72 610
                $related = $this->collect([]);
73
            } else {
74
                throw new BadRelationValueException(\sprintf(
75
                    'Value for Has Many relation must be of the iterable type, %s given.',
76 610
                    \get_debug_type($related),
77 610
                ));
78 610
            }
79 610
        }
80
        if (!SpecialValue::isEmpty($original)) {
81
            foreach ($this->calcDeleted($related, $original) as $item) {
0 ignored issues
show
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

81
            foreach ($this->calcDeleted(/** @scrutinizer ignore-type */ $related, $original) as $item) {
Loading history...
Bug introduced by
It seems like $original 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

81
            foreach ($this->calcDeleted($related, /** @scrutinizer ignore-type */ $original) as $item) {
Loading history...
82
                $this->deleteChild($pool, $tuple, $item);
83
            }
84
        }
85
86 610
        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

86
        if (\count(/** @scrutinizer ignore-type */ $related) === 0) {
Loading history...
87
            $tuple->state->setRelationStatus($this->getName(), RelationInterface::STATUS_RESOLVED);
88 610
            return;
89 610
        }
90 610
        $tuple->state->setRelationStatus($this->getName(), RelationInterface::STATUS_PROCESS);
91
92 610
        // $relationName = $this->getTargetRelationName()
93
        // Store new and existing items
94 610
        foreach ($related as $item) {
95
            $rTuple = $pool->attachStore($item, true);
96
            $this->assertValid($rTuple->node);
97
            if ($this->isNullable()) {
98
                // todo?
99 610
                // $rNode->setRelationStatus($relationName, RelationInterface::STATUS_DEFERRED);
100 610
            }
101
        }
102 610
    }
103
104 610
    public function queue(Pool $pool, Tuple $tuple): void
105 136
    {
106
        $node = $tuple->node;
0 ignored issues
show
Unused Code introduced by
The assignment to $node is dead and can be removed.
Loading history...
107
        $related = $tuple->state->getRelation($this->getName());
108 104
        $related = $this->extract($related);
109
110
        $tuple->state->setRelationStatus($this->getName(), RelationInterface::STATUS_RESOLVED);
111 136
112 136
        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...
113
            return;
114
        }
115 530
116 530
        // Fill related
117
        $relationName = $this->getTargetRelationName();
118
        foreach ($related as $item) {
119
            /** @var Tuple $rTuple */
120
            $rTuple = $pool->offsetGet($item);
121
122
            if ($this->inversion !== null) {
123 1000
                if ($rTuple->node->getStatus() === Node::NEW) {
124
                    // For existing entities it can be unwanted
125 1000
                    // if Reference to Parent will be rewritten by Parent Entity
126 1000
                    $rTuple->state->setRelation($relationName, $tuple->entity);
127 896
                }
128
129
                if ($rTuple->state->getRelationStatus($relationName) === RelationInterface::STATUS_PREPARE) {
130 1000
                    continue;
131 1000
                }
132
            }
133
            $this->applyChanges($tuple, $rTuple);
134 1272
            $rTuple->state->setRelationStatus($relationName, RelationInterface::STATUS_RESOLVED);
135
        }
136 1272
    }
137 504
138
    /**
139 1192
     * Init relation state and entity collection.
140 1192
     */
141
    public function init(EntityFactoryInterface $factory, Node $node, array $data): iterable
142 1192
    {
143 1192
        $elements = [];
144
        foreach ($data as $item) {
145 1192
            $elements[] = $factory->make($this->target, $item, Node::MANAGED);
146
        }
147
148 1230
        $node->setRelation($this->getName(), $elements);
149
        return $this->collect($elements);
150 1230
    }
151 1230
152 112
    public function cast(?array $data): array
153 1230
    {
154
        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...
155
            return [];
156 1230
        }
157
158 1230
        /** @var array<non-empty-string, MapperInterface> $mappers Mappers cache */
159 1230
        $mappers = [];
160 1230
161 1230
        foreach ($data as $key => $item) {
162 112
            $role = $item[LoaderInterface::ROLE_KEY] ?? $this->target;
163
            $mappers[$role] ??= $this->mapperProvider->getMapper($role);
164 1150
            // break link
165
            unset($data[$key]);
166 1150
            $data[$key] = $mappers[$role]->cast($item);
167
        }
168
        return $data;
169 444
    }
170
171 444
    public function initReference(Node $node): ReferenceInterface
172 176
    {
173
        $scope = $this->getReferenceScope($node);
174 372
        return $scope === null
175
            ? new EmptyReference($node->getRole(), [])
176
            : new Reference($this->target, $scope);
177
    }
178
179 372
    public function resolve(ReferenceInterface $reference, bool $load): ?iterable
180 152
    {
181
        if ($reference->hasValue()) {
182
            return $reference->getValue();
183 268
        }
184
        if ($reference->getScope() === []) {
185 268
            // nothing to proxy to
186 268
            $reference->setValue([]);
187
            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...
188 268
        }
189
        if ($load === false) {
190 268
            return null;
191
        }
192
193 1292
        $scope = \array_merge($reference->getScope(), $this->schema[Relation::WHERE] ?? []);
194
195 1292
        $iterator = (clone $this->select)->where($scope)->getIterator(findInHeap: true);
196
        $result = \iterator_to_array($iterator, false);
197
198 1292
        $reference->setValue($result);
199 1292
200 1292
        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...
201
    }
202
203
    public function collect(mixed $data): iterable
204
    {
205
        if (!\is_iterable($data)) {
206 706
            throw new \InvalidArgumentException('Collected data in the HasMany relation should be iterable.');
207
        }
208 706
        return $this->factory->collection(
209 592
            $this->schema[Relation::COLLECTION_TYPE] ?? null,
210
        )->collect($data);
211 706
    }
212 8
213
    /**
214 706
     * Convert entity data into array.
215
     */
216
    public function extract(mixed $data): array
217
    {
218
        if ($data instanceof \Doctrine\Common\Collections\Collection) {
219
            return $data->toArray();
220 706
        }
221
        if ($data instanceof \Traversable) {
222 706
            return \iterator_to_array($data);
223 706
        }
224 706
        return \is_array($data) ? $data : [];
225 706
    }
226
227
    protected function getReferenceScope(Node $node): ?array
228 706
    {
229
        $scope = [];
230
        $nodeData = $node->getData();
231
        foreach ($this->innerKeys as $i => $key) {
232
            if (!isset($nodeData[$key])) {
233
                return null;
234
            }
235
            $scope[$this->outerKeys[$i]] = $nodeData[$key];
236
        }
237
        return $scope;
238
    }
239
240
    /**
241
     * Return objects which are subject of removal.
242
     */
243
    protected function calcDeleted(iterable $related, iterable $original): array
244
    {
245
        $related = $this->extract($related);
246
        $original = $this->extract($original);
247
        return \array_udiff(
248
            $original ?? [],
249
            $related,
250
            // static fn(object $a, object $b): int => strcmp(spl_object_hash($a), spl_object_hash($b))
251
            static fn(object $a, object $b): int => (int) ($a === $b) - 1,
252
        );
253
    }
254
}
255