Completed
Push — 2.x ( 983411...a8d8e2 )
by Aleksei
24s queued 12s
created

ManyToMany::resolve()   B

Complexity

Conditions 6
Paths 7

Size

Total Lines 87
Code Lines 58

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 49
CRAP Score 6.0069

Importance

Changes 0
Metric Value
cc 6
eloc 58
c 0
b 0
f 0
nc 7
nop 2
dl 0
loc 87
ccs 49
cts 52
cp 0.9423
crap 6.0069
rs 8.2941

How to fix   Long Method   

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\Collection\Pivoted\PivotedCollectionInterface;
8
use Cycle\ORM\Collection\Pivoted\PivotedStorage;
9
use Cycle\ORM\FactoryInterface;
10
use Cycle\ORM\Heap\HeapInterface;
11
use Cycle\ORM\Heap\Node;
12
use Cycle\ORM\Heap\State;
13
use Cycle\ORM\Iterator;
14
use Cycle\ORM\ORMInterface;
15
use Cycle\ORM\Parser\RootNode;
16
use Cycle\ORM\Reference\EmptyReference;
17
use Cycle\ORM\Reference\Reference;
18
use Cycle\ORM\Reference\ReferenceInterface;
19
use Cycle\ORM\Relation;
20
use Cycle\ORM\Select\JoinableLoader;
21
use Cycle\ORM\Select\Loader\ManyToManyLoader;
22
use Cycle\ORM\Select\RootLoader;
23
use Cycle\ORM\Service\EntityFactoryInterface;
24
use Cycle\ORM\Service\SourceProviderInterface;
25
use Cycle\ORM\Transaction\Pool;
26
use Cycle\ORM\Transaction\Tuple;
27
use SplObjectStorage;
28
use Traversable;
29
30
/**
31
 * @internal
32
 */
33
class ManyToMany extends Relation\AbstractRelation
34
{
35
    /** @var string[] */
36
    protected array $throughInnerKeys;
37
38
    /** @var string[] */
39
    protected array $throughOuterKeys;
40
41
    protected string $pivotRole;
42
43
    protected EntityFactoryInterface $entityFactory;
44
    protected SourceProviderInterface $sourceProvider;
45
    protected FactoryInterface $factory;
46
    private HeapInterface $heap;
47
48 942
    public function __construct(ORMInterface $orm, string $role, string $name, string $target, array $schema)
49
    {
50 942
        parent::__construct($orm, $role, $name, $target, $schema);
51 942
        $this->heap = $orm->getHeap();
52 942
        $this->sourceProvider = $orm->getService(SourceProviderInterface::class);
53 942
        $this->entityFactory = $orm->getService(EntityFactoryInterface::class);
54 942
        $this->factory = $orm->getFactory();
55 942
        $this->pivotRole = $this->schema[Relation::THROUGH_ENTITY];
56
57 942
        $this->throughInnerKeys = (array)$this->schema[Relation::THROUGH_INNER_KEY];
58 942
        $this->throughOuterKeys = (array)$this->schema[Relation::THROUGH_OUTER_KEY];
59
    }
60
61 346
    public function prepare(Pool $pool, Tuple $tuple, mixed $related, bool $load = true): void
62
    {
63 346
        $node = $tuple->node;
64
65
        /** @var PivotedStorage|ReferenceInterface|null $original */
66 346
        $original = $node->getRelation($this->getName());
67 346
        $tuple->state->setRelation($this->getName(), $related);
68
69 346
        if ($original instanceof ReferenceInterface) {
70 72
            if (!$load && $related === $original && !$original->hasValue()) {
71 40
                $this->finalize($pool, $tuple, $related);
72 40
                return;
73
            }
74 56
            $this->resolve($original, true);
75 56
            $original = $original->getValue();
76 56
            $node->setRelation($this->getName(), $original);
77
        }
78 338
        $original = $this->extract($original);
79
80 338
        if ($related instanceof ReferenceInterface && $this->resolve($related, true) !== null) {
81
            $related = $related->getValue();
82
            $tuple->state->setRelation($this->getName(), $related);
83
        }
84 338
        $related = $this->extractRelated($related, $original);
85
        // $tuple->state->setStorage($this->pivotEntity, $related);
86 338
        $tuple->state->setRelation($this->getName(), $related);
87
88
        // un-link old elements
89 338
        foreach ($original as $item) {
90 192
            if (!$related->has($item)) {
0 ignored issues
show
Bug introduced by
$item of type array is incompatible with the type object expected by parameter $entity of Cycle\ORM\Collection\Pivoted\PivotedStorage::has(). ( Ignorable by Annotation )

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

90
            if (!$related->has(/** @scrutinizer ignore-type */ $item)) {
Loading history...
91 72
                $pivot = $original->get($item);
0 ignored issues
show
Bug introduced by
$item of type array is incompatible with the type object expected by parameter $entity of Cycle\ORM\Collection\Pivoted\PivotedStorage::get(). ( Ignorable by Annotation )

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

91
                $pivot = $original->get(/** @scrutinizer ignore-type */ $item);
Loading history...
92 72
                $this->deleteChild($pool, $pivot, $item);
0 ignored issues
show
Bug introduced by
$item of type array is incompatible with the type object expected by parameter $child of Cycle\ORM\Relation\ManyToMany::deleteChild(). ( Ignorable by Annotation )

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

92
                $this->deleteChild($pool, $pivot, /** @scrutinizer ignore-type */ $item);
Loading history...
Bug introduced by
It seems like $pivot can also be of type array; however, parameter $pivot of Cycle\ORM\Relation\ManyToMany::deleteChild() does only seem to accept null|object, 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

92
                $this->deleteChild($pool, /** @scrutinizer ignore-type */ $pivot, $item);
Loading history...
93 72
                $original->getContext()->offsetUnset($item);
0 ignored issues
show
Bug introduced by
$item of type array is incompatible with the type object expected by parameter $object of SplObjectStorage::offsetUnset(). ( Ignorable by Annotation )

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

93
                $original->getContext()->offsetUnset(/** @scrutinizer ignore-type */ $item);
Loading history...
94
            }
95
        }
96
97 338
        if ($this->inversion === null && \count($related) === 0) {
98
            $tuple->state->setRelationStatus($this->getName(), RelationInterface::STATUS_RESOLVED);
99
            return;
100
        }
101 338
        $tuple->state->setRelationStatus($this->getName(), RelationInterface::STATUS_PROCESS);
102
103
        // link/sync new and existed elements
104 338
        foreach ($related->getElements() as $item) {
105 336
            $this->newLink($pool, $tuple, $related, $item);
106
        }
107
    }
108
109 338
    public function queue(Pool $pool, Tuple $tuple): void
110
    {
111 338
        $related = $tuple->state->getRelation($this->getName());
112
113 338
        if ($related instanceof ReferenceInterface && !$related->hasValue()) {
114
            $tuple->state->setRelationStatus($this->getName(), RelationInterface::STATUS_RESOLVED);
115 338
            return;
116
        }
117
118
        $this->finalize($pool, $tuple, $related);
119
    }
120 338
121 338
    public function init(EntityFactoryInterface $factory, Node $node, array $data): iterable
122 338
    {
123 336
        $elements = [];
124 336
        $pivotData = new SplObjectStorage();
125 336
126 336
        $iterator = Iterator::createWithServices(
127 336
            $this->heap,
128 336
            $this->ormSchema,
129
            $this->entityFactory,
130
            $this->target,
131 338
            $data,
132 162
            true
133 162
        );
134 160
        foreach ($iterator as $pivot => $entity) {
135 96
            if (!\is_array($pivot)) {
136
                // skip partially selected entities (DB level filter)
137 64
                continue;
138 64
            }
139
140 162
            $pivotData[$entity] = $factory->make($this->pivotRole, $pivot, Node::MANAGED);
141
            $elements[] = $entity;
142
        }
143
        $collection = new PivotedStorage($elements, $pivotData);
144 480
        $node->setRelation($this->name, $collection);
145
146 480
        return $this->collect($collection);
147 480
    }
148
149 480
    public function cast(?array $data): array
150 480
    {
151 480
        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...
152 480
            return [];
153 480
        }
154
        $pivotMapper = $this->mapperProvider->getMapper($this->pivotRole);
155
        $targetMapper = $this->mapperProvider->getMapper($this->target);
156
157 480
        foreach ($data as $key => $pivot) {
158 480
            if (isset($pivot['@'])) {
159
                $d = $pivot['@'];
160 24
                // break link
161
                unset($pivot['@']);
162
                $pivot['@'] = $targetMapper->cast($d);
163 480
            }
164 480
            // break link
165
            unset($data[$key]);
166 480
            $data[$key] = $pivotMapper->cast($pivot);
167 480
        }
168
169 480
        return $data;
170
    }
171
172 632
    public function collect(mixed $data): iterable
173
    {
174 632
        return $this->factory->collection(
175 40
            $this->schema[Relation::COLLECTION_TYPE] ?? null
176
        )->collect($data);
177 632
    }
178 632
179
    public function extract(?iterable $data): PivotedStorage
180 632
    {
181 632
        return match (true) {
182 632
            $data instanceof PivotedStorage => $data,
183
            $data instanceof PivotedCollectionInterface => new PivotedStorage(
184 632
                $data->toArray(),
0 ignored issues
show
Bug introduced by
The method toArray() does not exist on null. ( Ignorable by Annotation )

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

184
                $data->/** @scrutinizer ignore-call */ 
185
                       toArray(),

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
185 632
                $data->getPivotContext()
186
            ),
187
            $data instanceof \Doctrine\Common\Collections\Collection => new PivotedStorage($data->toArray()),
188 632
            $data === null => new PivotedStorage(),
189 632
            $data instanceof Traversable => new PivotedStorage(iterator_to_array($data)),
0 ignored issues
show
Bug introduced by
It seems like $data can also be of type null; however, parameter $iterator of iterator_to_array() does only seem to accept Traversable, 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

189
            $data instanceof Traversable => new PivotedStorage(iterator_to_array(/** @scrutinizer ignore-type */ $data)),
Loading history...
190
            default => new PivotedStorage((array)$data),
191
        };
192 632
    }
193
194
    public function extractRelated(?iterable $data, PivotedStorage $original): PivotedStorage
195 648
    {
196
        $related = $this->extract($data);
197 648
        if (\count($original) === 0) {
198 648
            return $related;
199 648
        }
200
        // Merge pivots
201
        foreach ($related as $item) {
202 338
            if ($related->hasContext($item)) {
0 ignored issues
show
Bug introduced by
$item of type array is incompatible with the type object expected by parameter $entity of Cycle\ORM\Collection\Piv...edStorage::hasContext(). ( Ignorable by Annotation )

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

202
            if ($related->hasContext(/** @scrutinizer ignore-type */ $item)) {
Loading history...
203
                continue;
204
            }
205 338
            if ($original->hasContext($item)) {
206 338
                $related->set($item, $original->getContext()->offsetGet($item));
0 ignored issues
show
Bug introduced by
$item of type array is incompatible with the type object expected by parameter $object of SplObjectStorage::offsetGet(). ( Ignorable by Annotation )

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

206
                $related->set($item, $original->getContext()->offsetGet(/** @scrutinizer ignore-type */ $item));
Loading history...
Bug introduced by
$item of type array is incompatible with the type object expected by parameter $entity of Cycle\ORM\Collection\Pivoted\PivotedStorage::set(). ( Ignorable by Annotation )

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

206
                $related->set(/** @scrutinizer ignore-type */ $item, $original->getContext()->offsetGet($item));
Loading history...
207 240
            }
208 240
        }
209
        return $related;
210 258
    }
211 258
212 56
    public function initReference(Node $node): ReferenceInterface
213 338
    {
214
        $scope = [];
215
        $nodeData = $node->getData();
216
        foreach ($this->innerKeys as $key) {
217 338
            if (!isset($nodeData[$key])) {
218
                return new EmptyReference($node->getRole(), new PivotedStorage());
219 338
            }
220 338
            $scope[$key] = $nodeData[$key];
221 322
        }
222
223 32
        return new Reference($this->target, $scope);
224 32
    }
225 32
226
    public function resolve(ReferenceInterface $reference, bool $load): ?iterable
227
    {
228 32
        if ($reference->hasValue()) {
229
            return $reference->getValue();
230
        }
231 308
        if ($load === false) {
232
            return null;
233 308
        }
234 308
        $scope = $reference->getScope();
235 308
        if ($scope === []) {
236 308
            $result = new PivotedStorage();
237 56
            $reference->setValue($result);
238
            return $result;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $result returns the type Cycle\ORM\Collection\Pivoted\PivotedStorage which is incompatible with the type-hinted return iterable|null.
Loading history...
239 268
        }
240
241
        // getting scoped query
242 268
        $query = (new RootLoader(
243
            $this->ormSchema,
244
            $this->sourceProvider,
245 224
            $this->factory,
246
            $this->target
247 224
        ))->buildQuery();
248 80
249
        // responsible for all the scoping
250 192
        $loader = new ManyToManyLoader(
251 24
            $this->ormSchema,
252
            $this->sourceProvider,
253 168
            $this->factory,
254 168
            $this->sourceProvider->getSource($this->target)->getTable(),
255
            $this->target,
256
            $this->schema
257
        );
258
259
        /** @var ManyToManyLoader $loader */
260
        $loader = $loader->withContext($loader, [
261 168
            'scope' => $this->sourceProvider->getSource($this->target)->getScope(),
262 168
            'as' => $this->target,
263 168
            'method' => JoinableLoader::POSTLOAD,
264 168
        ]);
265 168
266 168
        $query = $loader->configureQuery($query, [$scope]);
267
268
        // we are going to add pivot node into virtual root node (only ID) to aggregate the results
269 168
        $root = new RootNode(
270 168
            (array)$this->schema[Relation::INNER_KEY],
271 168
            (array)$this->schema[Relation::INNER_KEY]
272 168
        );
273 168
274 168
        $node = $loader->createNode();
275 168
        $root->linkNode('output', $node);
276
277
        // emulate presence of parent entity
278
        $root->parseRow(0, $scope);
279 168
280 168
        $iterator = $query->getIterator();
281 168
        foreach ($iterator as $row) {
282
            $node->parseRow(0, $row);
283
        }
284
        $iterator->close();
285 168
286
        // load all eager relations, forbid loader to re-fetch data (make it think it was joined)
287
        $loader->withContext($loader, ['method' => JoinableLoader::INLOAD])->loadData($node);
288 168
289 168
        $elements = [];
290 168
        $pivotData = new SplObjectStorage();
291
        $iterator = Iterator::createWithServices(
292
            $this->heap,
293 168
            $this->ormSchema,
294 168
            $this->entityFactory,
295
            $this->target,
296
            $root->getResult()[0]['output'],
297 168
            true,
298
            typecast: true
299 168
        );
300 168
        foreach ($iterator as $pivot => $entity) {
301 168
            $pivotData[$entity] = $this->entityFactory->make(
302
                $this->schema[Relation::THROUGH_ENTITY],
303 168
                $pivot,
304
                Node::MANAGED,
305
                typecast: true
306 168
            );
307
308 168
            $elements[] = $entity;
309 168
        }
310 168
        $result = new PivotedStorage($elements, $pivotData);
311 168
        $reference->setValue($result);
312 168
        return $result;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $result returns the type Cycle\ORM\Collection\Pivoted\PivotedStorage which is incompatible with the type-hinted return iterable|null.
Loading history...
313 168
    }
314 168
315 168
    protected function applyPivotChanges(State $parentState, State $state): void
316
    {
317
        foreach ($this->innerKeys as $i => $innerKey) {
318
            $state->register($this->throughInnerKeys[$i], $parentState->getValue($innerKey));
319 168
        }
320 168
    }
321 168
322
    private function deleteChild(Pool $pool, ?object $pivot, object $child, ?Node $relatedNode = null): void
0 ignored issues
show
Unused Code introduced by
The parameter $relatedNode is not used and could be removed. ( Ignorable by Annotation )

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

322
    private function deleteChild(Pool $pool, ?object $pivot, object $child, /** @scrutinizer ignore-unused */ ?Node $relatedNode = null): void

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
323
    {
324
        // todo: add supporting for nullable pivot entities?
325
        if ($pivot !== null) {
326
            $pool->attachDelete($pivot, $this->isCascade());
327 168
        }
328
        $pool->attachStore($child, true);
329 168
    }
330 168
331 168
    protected function newLink(Pool $pool, Tuple $tuple, PivotedStorage $storage, object $related): void
332
    {
333
        $rTuple = $pool->attachStore($related, $this->isCascade());
334 336
        $this->assertValid($rTuple->node);
335
336 336
        $pivot = $storage->get($related);
337 336
        if (!\is_object($pivot)) {
338
            // first time initialization
339
            $pivot = $this->initPivot($tuple->entity, $storage, $rTuple, $pivot);
340
            $storage->set($related, $pivot);
0 ignored issues
show
Bug introduced by
It seems like $pivot can also be of type null; however, parameter $pivot of Cycle\ORM\Collection\Pivoted\PivotedStorage::set() does only seem to accept array|object, 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

340
            $storage->set($related, /** @scrutinizer ignore-type */ $pivot);
Loading history...
341 72
        }
342
343
        $pTuple = $pool->attachStore($pivot, $this->isCascade());
0 ignored issues
show
Bug introduced by
It seems like $pivot can also be of type null; however, parameter $entity of Cycle\ORM\Transaction\Pool::attachStore() does only seem to accept object, 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

343
        $pTuple = $pool->attachStore(/** @scrutinizer ignore-type */ $pivot, $this->isCascade());
Loading history...
344 72
        // $pRelationName = $tuple->node->getRole() . '.' . $this->getName() . ':' . $this->pivotEntity;
345 72
        // $pNode->setRelationStatus($pRelationName, RelationInterface::STATUS_RESOLVED);
346
347 72
        foreach ($this->throughInnerKeys as $i => $pInnerKey) {
348
            $pTuple->state->register($pInnerKey, $tuple->state->getTransactionData()[$this->innerKeys[$i]] ?? null);
349
350 336
            // $rState->forward($this->outerKeys[$i], $pState, $this->throughOuterKeys[$i]);
351
        }
352 336
353 336
        if ($this->inversion === null) {
354
            // send the Pivot into child's State for the ShadowHasMany relation
355 336
            // $relName = $tuple->node->getRole() . '.' . $this->name . ':' . $this->target;
356 336
            $relName = $this->getTargetRelationName();
357
            $pivots = $rTuple->state->getRelations()[$relName] ?? [];
358 312
            $pivots[] = $pivot;
359 312
            $rTuple->state->setRelation($relName, $pivots);
360
        } else {
361
            $rTuple->state->addToStorage($this->inversion, $pTuple->state);
362 336
        }
363
    }
364
365
    /**
366 336
     * Since many to many relation can overlap from two directions we have to properly resolve the pivot entity upon
367 336
     * it's generation. This is achieved using temporary mapping associated with each of the entity states.
368
     */
369
    protected function initPivot(object $parent, PivotedStorage $storage, Tuple $rTuple, ?array $pivot): ?object
370
    {
371
        if ($this->inversion !== null) {
372 336
            $relatedStorage = $rTuple->state->getRelation($this->inversion);
373
            if ($relatedStorage instanceof PivotedStorage && $relatedStorage->hasContext($parent)) {
374
                return $relatedStorage->get($parent);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $relatedStorage->get($parent) could return the type array which is incompatible with the type-hinted return null|object. Consider adding an additional type-check to rule them out.
Loading history...
375 176
            }
376 176
        }
377 176
378 176
        $entity = $this->entityFactory->make($this->pivotRole, $pivot ?? []);
379
        $storage->set($rTuple->entity, $entity);
380 160
        return $entity;
381
    }
382
383
    private function finalize(Pool $pool, Tuple $tuple, mixed $related): void
384
    {
385
        $tuple->state->setRelationStatus($this->getName(), RelationInterface::STATUS_RESOLVED);
386
387
        $relationName = $tuple->node->getRole() . '.' . $this->name . ':' . $this->pivotRole;
388 312
        $pStates = [];
389
        foreach ($related as $item) {
390 312
            $pivot = $related->get($item);
391 160
            if ($pivot !== null) {
392 160
                $pTuple = $pool->offsetGet($pivot);
393 96
                $this->applyPivotChanges($tuple->state, $pTuple->state);
394
                $pStates[] = $pTuple->state;
395
                $pTuple->state->setRelationStatus($relationName, RelationInterface::STATUS_RESOLVED);
396
            }
397 312
        }
398 312
        if ($this->inversion !== null) {
399 312
            $storage = $tuple->state->getStorage($this->name);
400
            foreach ($storage as $pState) {
401
                if (\in_array($pState, $pStates, true)) {
402
                    continue;
403
                }
404
                $this->applyPivotChanges($tuple->state, $pState);
405
                $pState->setRelationStatus($relationName, RelationInterface::STATUS_RESOLVED);
406
            }
407
            $tuple->state->clearStorage($this->name);
408
        }
409
    }
410
}
411