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) { |
||||
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
Bug
introduced
by
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
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 | } |