Passed
Push — 2.x ( 0b5227...cb81b7 )
by butschster
16:17
created

ManyToMany::queue()   B

Complexity

Conditions 8
Paths 13

Size

Total Lines 32
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 21
CRAP Score 8.006

Importance

Changes 0
Metric Value
cc 8
eloc 21
nc 13
nop 2
dl 0
loc 32
ccs 21
cts 22
cp 0.9545
crap 8.006
rs 8.4444
c 0
b 0
f 0
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
                $tuple->state->setRelationStatus($this->getName(), RelationInterface::STATUS_RESOLVED);
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
        $tuple->state->setRelationStatus($this->getName(), RelationInterface::STATUS_RESOLVED);
114
115 338
        if ($related instanceof ReferenceInterface && !$related->hasValue()) {
116
            return;
117
        }
118
119
        // $relationName = $this->getTargetRelationName();
120 338
        $relationName = $tuple->node->getRole() . '.' . $this->name . ':' . $this->pivotRole;
121 338
        $pStates = [];
122 338
        foreach ($related as $item) {
123 336
            $pivot = $related->get($item);
124 336
            if ($pivot !== null) {
125 336
                $pTuple = $pool->offsetGet($pivot);
126 336
                $this->applyPivotChanges($tuple->state, $pTuple->state);
127 336
                $pStates[] = $pTuple->state;
128 336
                $pTuple->state->setRelationStatus($relationName, RelationInterface::STATUS_RESOLVED);
129
            }
130
        }
131 338
        if ($this->inversion !== null) {
132 162
            $storage = $tuple->state->getStorage($this->name);
133 162
            foreach ($storage as $pState) {
134 160
                if (in_array($pState, $pStates, true)) {
135 96
                    continue;
136
                }
137 64
                $this->applyPivotChanges($tuple->state, $pState);
138 64
                $pState->setRelationStatus($relationName, RelationInterface::STATUS_RESOLVED);
139
            }
140 162
            $tuple->state->clearStorage($this->name);
141
        }
142
    }
143
144 480
    public function init(EntityFactoryInterface $factory, Node $node, array $data): iterable
145
    {
146 480
        $elements = [];
147 480
        $pivotData = new SplObjectStorage();
148
149 480
        $iterator = Iterator::createWithServices(
150 480
            $this->heap,
151 480
            $this->ormSchema,
152 480
            $this->entityFactory,
153 480
            $this->target,
154
            $data,
155
            true
156
        );
157 480
        foreach ($iterator as $pivot => $entity) {
158 480
            if (!\is_array($pivot)) {
159
                // skip partially selected entities (DB level filter)
160 24
                continue;
161
            }
162
163 480
            $pivotData[$entity] = $factory->make($this->pivotRole, $pivot, Node::MANAGED);
164 480
            $elements[] = $entity;
165
        }
166 480
        $collection = new PivotedStorage($elements, $pivotData);
167 480
        $node->setRelation($this->name, $collection);
168
169 480
        return $this->collect($collection);
170
    }
171
172 632
    public function cast(?array $data): array
173
    {
174 632
        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...
175 40
            return [];
176
        }
177 632
        $pivotMapper = $this->mapperProvider->getMapper($this->pivotRole);
178 632
        $targetMapper = $this->mapperProvider->getMapper($this->target);
179
180 632
        foreach ($data as $key => $pivot) {
181 632
            if (isset($pivot['@'])) {
182 632
                $d = $pivot['@'];
183
                // break link
184 632
                unset($pivot['@']);
185 632
                $pivot['@'] = $targetMapper->cast($d);
186
            }
187
            // break link
188 632
            unset($data[$key]);
189 632
            $data[$key] = $pivotMapper->cast($pivot);
190
        }
191
192 632
        return $data;
193
    }
194
195 648
    public function collect(mixed $data): iterable
196
    {
197 648
        return $this->factory->collection(
198 648
            $this->schema[Relation::COLLECTION_TYPE] ?? null
199 648
        )->collect($data);
200
    }
201
202 338
    public function extract(?iterable $data): PivotedStorage
203
    {
204
        return match (true) {
205
            $data instanceof PivotedStorage => $data,
206 338
            $data instanceof PivotedCollectionInterface => new PivotedStorage(
207 240
                $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

207
                $data->/** @scrutinizer ignore-call */ 
208
                       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...
208 240
                $data->getPivotContext()
209
            ),
210 258
            $data instanceof \Doctrine\Common\Collections\Collection => new PivotedStorage($data->toArray()),
211 258
            $data === null => new PivotedStorage(),
212 56
            $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

212
            $data instanceof Traversable => new PivotedStorage(iterator_to_array(/** @scrutinizer ignore-type */ $data)),
Loading history...
213 338
            default => new PivotedStorage((array)$data),
214
        };
215
    }
216
217 338
    public function extractRelated(?iterable $data, PivotedStorage $original): PivotedStorage
218
    {
219 338
        $related = $this->extract($data);
220 338
        if ($data instanceof PivotedStorage || $data instanceof PivotedCollectionInterface || \count($original) === 0) {
221 322
            return $related;
222
        }
223 32
        foreach ($related as $item) {
224 32
            if ($original->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

224
            if ($original->hasContext(/** @scrutinizer ignore-type */ $item)) {
Loading history...
225 32
                $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

225
                $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

225
                $related->set(/** @scrutinizer ignore-type */ $item, $original->getContext()->offsetGet($item));
Loading history...
226
            }
227
        }
228 32
        return $related;
229
    }
230
231 308
    public function initReference(Node $node): ReferenceInterface
232
    {
233 308
        $scope = [];
234 308
        $nodeData = $node->getData();
235 308
        foreach ($this->innerKeys as $key) {
236 308
            if (!isset($nodeData[$key])) {
237 56
                return new EmptyReference($node->getRole(), new PivotedStorage());
238
            }
239 268
            $scope[$key] = $nodeData[$key];
240
        }
241
242 268
        return new Reference($this->target, $scope);
243
    }
244
245 224
    public function resolve(ReferenceInterface $reference, bool $load): ?iterable
246
    {
247 224
        if ($reference->hasValue()) {
248 80
            return $reference->getValue();
249
        }
250 192
        if ($load === false) {
251 24
            return null;
252
        }
253 168
        $scope = $reference->getScope();
254 168
        if ($scope === []) {
255
            $result = new PivotedStorage();
256
            $reference->setValue($result);
257
            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...
258
        }
259
260
        // getting scoped query
261 168
        $query = (new RootLoader(
262 168
            $this->ormSchema,
263 168
            $this->sourceProvider,
264 168
            $this->factory,
265 168
            $this->target
266 168
        ))->buildQuery();
267
268
        // responsible for all the scoping
269 168
        $loader = new ManyToManyLoader(
270 168
            $this->ormSchema,
271 168
            $this->sourceProvider,
272 168
            $this->factory,
273 168
            $this->sourceProvider->getSource($this->target)->getTable(),
274 168
            $this->target,
275 168
            $this->schema
276
        );
277
278
        /** @var ManyToManyLoader $loader */
279 168
        $loader = $loader->withContext($loader, [
280 168
            'scope' => $this->sourceProvider->getSource($this->target)->getScope(),
281 168
            'as' => $this->target,
282
            'method' => JoinableLoader::POSTLOAD,
283
        ]);
284
285 168
        $query = $loader->configureQuery($query, [$scope]);
286
287
        // we are going to add pivot node into virtual root node (only ID) to aggregate the results
288 168
        $root = new RootNode(
289 168
            (array)$this->schema[Relation::INNER_KEY],
290 168
            (array)$this->schema[Relation::INNER_KEY]
291
        );
292
293 168
        $node = $loader->createNode();
294 168
        $root->linkNode('output', $node);
295
296
        // emulate presence of parent entity
297 168
        $root->parseRow(0, $scope);
298
299 168
        $iterator = $query->getIterator();
300 168
        foreach ($iterator as $row) {
301 168
            $node->parseRow(0, $row);
302
        }
303 168
        $iterator->close();
304
305
        // load all eager relations, forbid loader to re-fetch data (make it think it was joined)
306 168
        $loader->withContext($loader, ['method' => JoinableLoader::INLOAD])->loadData($node);
307
308 168
        $elements = [];
309 168
        $pivotData = new SplObjectStorage();
310 168
        $iterator = Iterator::createWithServices(
311 168
            $this->heap,
312 168
            $this->ormSchema,
313 168
            $this->entityFactory,
314 168
            $this->target,
315 168
            $root->getResult()[0]['output'],
316
            true,
317
            typecast: true
318
        );
319 168
        foreach ($iterator as $pivot => $entity) {
320 168
            $pivotData[$entity] = $this->entityFactory->make(
321 168
                $this->schema[Relation::THROUGH_ENTITY],
322
                $pivot,
323
                Node::MANAGED,
324
                typecast: true
325
            );
326
327 168
            $elements[] = $entity;
328
        }
329 168
        $result = new PivotedStorage($elements, $pivotData);
330 168
        $reference->setValue($result);
331 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...
332
    }
333
334 336
    protected function applyPivotChanges(State $parentState, State $state): void
335
    {
336 336
        foreach ($this->innerKeys as $i => $innerKey) {
337 336
            $state->register($this->throughInnerKeys[$i], $parentState->getValue($innerKey));
338
        }
339
    }
340
341 72
    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

341
    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...
342
    {
343
        // todo: add supporting for nullable pivot entities?
344 72
        if ($pivot !== null) {
345 72
            $pool->attachDelete($pivot, $this->isCascade());
346
        }
347 72
        $pool->attachStore($child, true);
348
    }
349
350 336
    protected function newLink(Pool $pool, Tuple $tuple, PivotedStorage $storage, object $related): void
351
    {
352 336
        $rTuple = $pool->attachStore($related, $this->isCascade());
353 336
        $this->assertValid($rTuple->node);
354
355 336
        $pivot = $storage->get($related);
356 336
        if (!\is_object($pivot)) {
357
            // first time initialization
358 312
            $pivot = $this->initPivot($tuple->entity, $storage, $rTuple, $pivot);
359 312
            $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

359
            $storage->set($related, /** @scrutinizer ignore-type */ $pivot);
Loading history...
360
        }
361
362 336
        $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

362
        $pTuple = $pool->attachStore(/** @scrutinizer ignore-type */ $pivot, $this->isCascade());
Loading history...
363
        // $pRelationName = $tuple->node->getRole() . '.' . $this->getName() . ':' . $this->pivotEntity;
364
        // $pNode->setRelationStatus($pRelationName, RelationInterface::STATUS_RESOLVED);
365
366 336
        foreach ($this->throughInnerKeys as $i => $pInnerKey) {
367 336
            $pTuple->state->register($pInnerKey, $tuple->state->getTransactionData()[$this->innerKeys[$i]] ?? null);
368
369
            // $rState->forward($this->outerKeys[$i], $pState, $this->throughOuterKeys[$i]);
370
        }
371
372 336
        if ($this->inversion === null) {
373
            // send the Pivot into child's State for the ShadowHasMany relation
374
            // $relName = $tuple->node->getRole() . '.' . $this->name . ':' . $this->target;
375 176
            $relName = $this->getTargetRelationName();
376 176
            $pivots = $rTuple->state->getRelations()[$relName] ?? [];
377 176
            $pivots[] = $pivot;
378 176
            $rTuple->state->setRelation($relName, $pivots);
379
        } else {
380 160
            $rTuple->state->addToStorage($this->inversion, $pTuple->state);
381
        }
382
    }
383
384
    /**
385
     * Since many to many relation can overlap from two directions we have to properly resolve the pivot entity upon
386
     * it's generation. This is achieved using temporary mapping associated with each of the entity states.
387
     */
388 312
    protected function initPivot(object $parent, PivotedStorage $storage, Tuple $rTuple, ?array $pivot): ?object
389
    {
390 312
        if ($this->inversion !== null) {
391 160
            $relatedStorage = $rTuple->state->getRelation($this->inversion);
392 160
            if ($relatedStorage instanceof PivotedStorage && $relatedStorage->hasContext($parent)) {
393 96
                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...
394
            }
395
        }
396
397 312
        $entity = $this->entityFactory->make($this->pivotRole, $pivot ?? []);
398 312
        $storage->set($rTuple->entity, $entity);
399 312
        return $entity;
400
    }
401
}
402