Passed
Push — master ( 9676b0...44a032 )
by Anton
02:07
created

src/Relation/ManyToMany.php (3 issues)

Labels
1
<?php
2
/**
3
 * Cycle DataMapper ORM
4
 *
5
 * @license   MIT
6
 * @author    Anton Titov (Wolfy-J)
7
 */
8
declare(strict_types=1);
9
10
namespace Cycle\ORM\Relation;
11
12
use Cycle\ORM\Command\Branch\Sequence;
13
use Cycle\ORM\Command\CommandInterface;
14
use Cycle\ORM\Command\ContextCarrierInterface as CC;
15
use Cycle\ORM\Heap\Node;
16
use Cycle\ORM\Iterator;
17
use Cycle\ORM\ORMInterface;
18
use Cycle\ORM\Promise\Collection\CollectionPromiseInterface;
19
use Cycle\ORM\Promise\ReferenceInterface;
20
use Cycle\ORM\Relation;
21
use Cycle\ORM\Relation\Pivoted;
22
use Doctrine\Common\Collections\Collection;
23
24
class ManyToMany extends Relation\AbstractRelation
25
{
26
    /** @var string|null */
27
    private $pivotEntity;
28
29
    /** @var string */
30
    protected $thoughtInnerKey;
31
32
    /** @var string */
33
    protected $thoughtOuterKey;
34
35
    /**
36
     * @param ORMInterface $orm
37
     * @param string       $name
38
     * @param string       $target
39
     * @param array        $schema
40
     */
41
    public function __construct(ORMInterface $orm, string $name, string $target, array $schema)
42
    {
43
        parent::__construct($orm, $name, $target, $schema);
44
        $this->pivotEntity = $this->schema[Relation::THOUGH_ENTITY] ?? null;
45
        $this->thoughtInnerKey = $this->schema[Relation::THOUGH_INNER_KEY] ?? null;
46
        $this->thoughtOuterKey = $this->schema[Relation::THOUGH_OUTER_KEY] ?? null;
47
    }
48
49
    /**
50
     * @inheritdoc
51
     */
52
    public function init(array $data): array
53
    {
54
        $elements = [];
55
        $pivotData = new \SplObjectStorage();
56
57
        $iterator = new Iterator($this->orm, $this->target, $data);
58
        foreach ($iterator as $pivot => $entity) {
59
            $pivotData[$entity] = $this->orm->make($this->pivotEntity, $pivot, Node::MANAGED);
60
            $elements[] = $entity;
61
        }
62
63
        return [
64
            new Pivoted\PivotedCollection($elements, $pivotData),
65
            new Pivoted\PivotedStorage($elements, $pivotData)
66
        ];
67
    }
68
69
    /**
70
     * @inheritdoc
71
     */
72
    public function extract($data)
73
    {
74
        if ($data instanceof CollectionPromiseInterface && !$data->isInitialized()) {
75
            return $data->getPromise();
76
        }
77
78
        if ($data instanceof Pivoted\PivotedCollectionInterface) {
79
            return new Pivoted\PivotedStorage($data->toArray(), $data->getPivotContext());
80
        }
81
82
        if ($data instanceof Collection) {
83
            return new Pivoted\PivotedStorage($data->toArray());
84
        }
85
86
        return new Pivoted\PivotedStorage();
87
    }
88
89
    /**
90
     * @inheritdoc
91
     */
92
    public function initPromise(Node $parentNode): array
93
    {
94
        if (empty($innerKey = $this->fetchKey($parentNode, $this->innerKey))) {
95
            return [new Pivoted\PivotedCollection(), null];
96
        }
97
98
        // will take care of all the loading and scoping
99
        $p = new Pivoted\PivotedPromise($this->orm, $this->target, $this->schema, $innerKey);
100
101
        return [new Pivoted\PivotedCollectionPromise($p), $p];
102
    }
103
104
    /**
105
     * @inheritdoc
106
     *
107
     * @param Pivoted\PivotedStorage $related
108
     * @param Pivoted\PivotedStorage $original
109
     */
110
    public function queue(CC $parentStore, $parentEntity, Node $parentNode, $related, $original): CommandInterface
111
    {
112
        $original = $original ?? new Pivoted\PivotedStorage();
113
114
        if ($related instanceof ReferenceInterface) {
115
            $related = $this->resolve($related);
116
        }
117
118
        if ($original instanceof ReferenceInterface) {
0 ignored issues
show
$original is never a sub-type of Cycle\ORM\Promise\ReferenceInterface.
Loading history...
119
            $original = $this->resolve($original);
120
        }
121
122
        $sequence = new Sequence();
123
124
        // link/sync new and existed elements
125
        foreach ($related->getElements() as $item) {
126
            $sequence->addCommand($this->link($parentNode, $item, $related->get($item), $related));
127
        }
128
129
        // un-link old elements
130
        foreach ($original->getElements() as $item) {
131
            if (!$related->has($item)) {
132
                // todo: THIS IS MAGIC
133
                $sequence->addCommand($this->orm->queueDelete($original->get($item)));
134
            }
135
        }
136
137
        return $sequence;
138
    }
139
140
    /**
141
     * Link two entities together and create/update pivot context.
142
     *
143
     * @param Node                   $parentNode
144
     * @param object                 $related
145
     * @param object                 $pivot
146
     * @param Pivoted\PivotedStorage $storage
147
     * @return CommandInterface
148
     */
149
    protected function link(Node $parentNode, $related, $pivot, Pivoted\PivotedStorage $storage): CommandInterface
150
    {
151
        $relStore = $this->orm->queueStore($related);
152
        $relNode = $this->getNode($related, +1);
153
        $this->assertValid($relNode);
0 ignored issues
show
It seems like $relNode can also be of type null; however, parameter $relNode 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

153
        $this->assertValid(/** @scrutinizer ignore-type */ $relNode);
Loading history...
154
155
        if (!is_object($pivot)) {
156
            // first time initialization
157
            $pivot = $this->initPivot($parentNode, $related, $pivot);
158
        }
159
160
        // defer the insert until pivot keys are resolved
161
        $pivotStore = $this->orm->queueStore($pivot);
162
        $pivotNode = $this->getNode($pivot);
163
164
        $this->forwardContext(
165
            $parentNode,
166
            $this->innerKey,
167
            $pivotStore,
168
            $pivotNode,
0 ignored issues
show
It seems like $pivotNode 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

168
            /** @scrutinizer ignore-type */ $pivotNode,
Loading history...
169
            $this->thoughtInnerKey
170
        );
171
172
        $this->forwardContext(
173
            $relNode,
174
            $this->outerKey,
175
            $pivotStore,
176
            $pivotNode,
177
            $this->thoughtOuterKey
178
        );
179
180
        $sequence = new Sequence();
181
        $sequence->addCommand($relStore);
182
        $sequence->addCommand($pivotStore);
183
184
        // update the link
185
        $storage->set($related, $pivot);
186
187
        return $sequence;
188
    }
189
190
    /**
191
     * Since many to many relation can overlap from two directions we have to properly resolve the pivot entity upon
192
     * it's generation. This is achieved using temporary mapping associated with each of the entity states.
193
     *
194
     * @param Node   $parentNode
195
     * @param object $related
196
     * @param mixed  $pivot
197
     * @return mixed|object|null
198
     */
199
    protected function initPivot(Node $parentNode, $related, $pivot)
200
    {
201
        $relNode = $this->getNode($related);
202
        if ($parentNode->getState()->getStorage($this->pivotEntity)->contains($relNode)) {
203
            return $parentNode->getState()->getStorage($this->pivotEntity)->offsetGet($relNode);
204
        }
205
206
        $entity = $this->orm->make($this->pivotEntity, $pivot ?? []);
207
208
        $parentNode->getState()->getStorage($this->pivotEntity)->offsetSet($relNode, $entity);
209
        $relNode->getState()->getStorage($this->pivotEntity)->offsetSet($parentNode, $entity);
210
211
        return $entity;
212
    }
213
}