Passed
Push — master ( 61bbc0...3efc91 )
by Anton
02:31
created

ManyToMany::sortRelation()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 2
dl 0
loc 7
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * Cycle DataMapper ORM
5
 *
6
 * @license   MIT
7
 * @author    Anton Titov (Wolfy-J)
8
 */
9
10
declare(strict_types=1);
11
12
namespace Cycle\ORM\Relation;
13
14
use Cycle\ORM\Command\Branch\Nil;
15
use Cycle\ORM\Command\Branch\Sequence;
16
use Cycle\ORM\Command\CommandInterface;
17
use Cycle\ORM\Command\ContextCarrierInterface as CC;
18
use Cycle\ORM\Heap\Node;
19
use Cycle\ORM\Heap\State;
20
use Cycle\ORM\Iterator;
21
use Cycle\ORM\ORMInterface;
22
use Cycle\ORM\Promise\Collection\CollectionPromiseInterface;
23
use Cycle\ORM\Promise\ReferenceInterface;
24
use Cycle\ORM\Relation;
25
use Cycle\ORM\Relation\Pivoted;
26
use Doctrine\Common\Collections\Collection;
27
28
class ManyToMany extends Relation\AbstractRelation
29
{
30
31
    /** @var string */
32
    protected $throughInnerKey;
33
34
    /** @var string */
35
    protected $throughOuterKey;
36
    /** @var string|null */
37
    private $pivotEntity;
38
39
    /**
40
     * @param ORMInterface $orm
41
     * @param string       $name
42
     * @param string       $target
43
     * @param array        $schema
44
     */
45
    public function __construct(ORMInterface $orm, string $name, string $target, array $schema)
46
    {
47
        parent::__construct($orm, $name, $target, $schema);
48
        $this->pivotEntity = $this->schema[Relation::THROUGH_ENTITY] ?? null;
49
        $this->throughInnerKey = $this->schema[Relation::THROUGH_INNER_KEY] ?? null;
50
        $this->throughOuterKey = $this->schema[Relation::THROUGH_OUTER_KEY] ?? null;
51
    }
52
53
    /**
54
     * @inheritdoc
55
     */
56
    public function init(Node $node, array $data): array
57
    {
58
        $elements = [];
59
        $pivotData = new \SplObjectStorage();
60
61
        $iterator = new Iterator($this->orm, $this->target, $data);
62
        foreach ($iterator as $pivot => $entity) {
63
            if (!is_array($pivot)) {
64
                // skip partially selected entities (DB level filter)
65
                continue;
66
            }
67
68
            $pivotData[$entity] = $this->orm->make($this->pivotEntity, $pivot, Node::MANAGED);
0 ignored issues
show
Bug introduced by
It seems like $this->pivotEntity can also be of type null; however, parameter $role of Cycle\ORM\ORMInterface::make() does only seem to accept string, 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

68
            $pivotData[$entity] = $this->orm->make(/** @scrutinizer ignore-type */ $this->pivotEntity, $pivot, Node::MANAGED);
Loading history...
Bug introduced by
The method make() does not exist on Cycle\ORM\Select\SourceProviderInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Cycle\ORM\Select\SourceProviderInterface. ( Ignorable by Annotation )

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

68
            /** @scrutinizer ignore-call */ 
69
            $pivotData[$entity] = $this->orm->make($this->pivotEntity, $pivot, Node::MANAGED);
Loading history...
69
            $elements[] = $entity;
70
        }
71
72
        return [
73
            new Pivoted\PivotedCollection($elements, $pivotData),
74
            new Pivoted\PivotedStorage($elements, $pivotData)
75
        ];
76
    }
77
78
    /**
79
     * @inheritdoc
80
     */
81
    public function extract($data)
82
    {
83
        if ($data instanceof CollectionPromiseInterface && !$data->isInitialized()) {
84
            return $data->getPromise();
85
        }
86
87
        if ($data instanceof Pivoted\PivotedCollectionInterface) {
88
            return new Pivoted\PivotedStorage($data->toArray(), $data->getPivotContext());
89
        }
90
91
        if ($data instanceof Collection) {
92
            return new Pivoted\PivotedStorage($data->toArray());
93
        }
94
95
        return new Pivoted\PivotedStorage();
96
    }
97
98
    /**
99
     * @inheritdoc
100
     */
101
    public function initPromise(Node $node): array
102
    {
103
        $innerKey = $this->fetchKey($node, $this->innerKey);
104
        if ($innerKey === null) {
105
            return [new Pivoted\PivotedCollection(), null];
106
        }
107
108
        // will take care of all the loading and scoping
109
        $p = new Pivoted\PivotedPromise(
110
            $this->orm,
111
            $this->target,
112
            $this->schema,
113
            $innerKey
114
        );
115
116
        return [new Pivoted\PivotedCollectionPromise($p), $p];
117
    }
118
119
    /**
120
     * @inheritdoc
121
     *
122
     * @param Pivoted\PivotedStorage $related
123
     * @param Pivoted\PivotedStorage $original
124
     */
125
    public function queue(CC $store, $entity, Node $node, $related, $original): CommandInterface
126
    {
127
        $original = $original ?? new Pivoted\PivotedStorage();
128
129
        if ($related instanceof ReferenceInterface) {
0 ignored issues
show
introduced by
$related is never a sub-type of Cycle\ORM\Promise\ReferenceInterface.
Loading history...
130
            $related = $this->resolve($related);
131
        }
132
133
        if ($original instanceof ReferenceInterface) {
0 ignored issues
show
introduced by
$original is never a sub-type of Cycle\ORM\Promise\ReferenceInterface.
Loading history...
134
            $original = $this->resolve($original);
135
        }
136
137
        $sequence = new Sequence();
138
139
        // link/sync new and existed elements
140
        foreach ($related->getElements() as $item) {
141
            $sequence->addCommand($this->link($node, $item, $related->get($item), $related));
142
        }
143
144
        // un-link old elements
145
        foreach ($original->getElements() as $item) {
146
            if (!$related->has($item)) {
147
                // todo: add support for nullable pivot entities
148
                $sequence->addCommand($this->orm->queueDelete($original->get($item)));
0 ignored issues
show
Bug introduced by
The method queueDelete() does not exist on Cycle\ORM\Select\SourceProviderInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Cycle\ORM\Select\SourceProviderInterface. ( Ignorable by Annotation )

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

148
                $sequence->addCommand($this->orm->/** @scrutinizer ignore-call */ queueDelete($original->get($item)));
Loading history...
149
            }
150
        }
151
152
        return $sequence;
153
    }
154
155
    /**
156
     * Link two entities together and create/update pivot context.
157
     *
158
     * @param Node                   $node
159
     * @param object                 $related
160
     * @param object                 $pivot
161
     * @param Pivoted\PivotedStorage $storage
162
     * @return CommandInterface
163
     */
164
    protected function link(Node $node, $related, $pivot, Pivoted\PivotedStorage $storage): CommandInterface
165
    {
166
        $rStore = $this->orm->queueStore($related);
0 ignored issues
show
Bug introduced by
The method queueStore() does not exist on Cycle\ORM\Select\SourceProviderInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Cycle\ORM\Select\SourceProviderInterface. ( Ignorable by Annotation )

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

166
        /** @scrutinizer ignore-call */ 
167
        $rStore = $this->orm->queueStore($related);
Loading history...
167
        $rNode = $this->getNode($related, +1);
168
        $this->assertValid($rNode);
0 ignored issues
show
Bug introduced by
It seems like $rNode can also be of type null; however, parameter $related of Cycle\ORM\Relation\AbstractRelation::assertValid() does only seem to accept Cycle\ORM\Heap\Node, 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

168
        $this->assertValid(/** @scrutinizer ignore-type */ $rNode);
Loading history...
169
170
        if (!is_object($pivot)) {
171
            // first time initialization
172
            $pivot = $this->initPivot($node, $related, $pivot);
173
        }
174
175
        $pNode = $this->getNode($pivot);
176
177
        // defer the insert until pivot keys are resolved
178
        $pStore = $this->orm->queueStore($pivot);
179
180
        $this->forwardContext(
181
            $node,
182
            $this->innerKey,
183
            $pStore,
184
            $pNode,
0 ignored issues
show
Bug introduced by
It seems like $pNode can also be of type null; however, parameter $to of Cycle\ORM\Relation\Abstr...ation::forwardContext() does only seem to accept Cycle\ORM\Heap\Node, 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

184
            /** @scrutinizer ignore-type */ $pNode,
Loading history...
185
            $this->throughInnerKey
186
        );
187
188
        $this->forwardContext(
189
            $rNode,
190
            $this->outerKey,
191
            $pStore,
192
            $pNode,
193
            $this->throughOuterKey
194
        );
195
196
        $sequence = new Sequence();
197
        $sequence->addCommand($rStore);
198
        $sequence->addCommand($pStore);
199
200
        // update the link
201
        $storage->set($related, $pivot);
202
203
        return $sequence;
204
    }
205
206
    /**
207
     * Since many to many relation can overlap from two directions we have to properly resolve the pivot entity upon
208
     * it's generation. This is achieved using temporary mapping associated with each of the entity states.
209
     *
210
     * @param Node   $node
211
     * @param object $related
212
     * @param mixed  $pivot
213
     * @return mixed|object|null
214
     */
215
    protected function initPivot(Node $node, $related, $pivot)
216
    {
217
        [$source, $target] = $this->sortRelation($node, $this->getNode($related));
0 ignored issues
show
Bug introduced by
It seems like $this->getNode($related) can also be of type null; however, parameter $related of Cycle\ORM\Relation\ManyToMany::sortRelation() does only seem to accept Cycle\ORM\Heap\Node, 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

217
        [$source, $target] = $this->sortRelation($node, /** @scrutinizer ignore-type */ $this->getNode($related));
Loading history...
218
219
        if ($source->getState()->getStorage($this->pivotEntity)->contains($target)) {
220
            return $source->getState()->getStorage($this->pivotEntity)->offsetGet($target);
221
        }
222
223
        $entity = $this->orm->make($this->pivotEntity, $pivot ?? []);
0 ignored issues
show
Bug introduced by
It seems like $this->pivotEntity can also be of type null; however, parameter $role of Cycle\ORM\ORMInterface::make() does only seem to accept string, 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

223
        $entity = $this->orm->make(/** @scrutinizer ignore-type */ $this->pivotEntity, $pivot ?? []);
Loading history...
224
225
        $source->getState()->getStorage($this->pivotEntity)->offsetSet($target, $entity);
226
227
        return $entity;
228
    }
229
230
    /**
231
     * Keep only one relation branch as primary branch.
232
     *
233
     * @param Node $node
234
     * @param Node $related
235
     * @return array<Node, Node>
236
     */
237
    private function sortRelation(Node $node, Node $related): array
238
    {
239
        // always use single storage
240
        $list = [$node->getRole() => $node, $related->getRole() => $related];
241
        ksort($list);
242
243
        return array_values($list);
244
    }
245
}
246