Completed
Branch feature/pre-split (8b986a)
by Anton
06:31
created

ManyToManyRelation::setRelated()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 15
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 6
nc 4
nop 1
dl 0
loc 15
rs 9.4285
c 0
b 0
f 0
1
<?php
2
/**
3
 * components
4
 *
5
 * @author    Wolfy-J
6
 */
7
namespace Spiral\ORM\Entities\Relations;
8
9
use Spiral\Database\Exceptions\QueryException;
10
use Spiral\ORM\CommandInterface;
11
use Spiral\ORM\Commands\NullCommand;
12
use Spiral\ORM\ContextualCommandInterface;
13
use Spiral\ORM\Entities\RecordIterator;
14
use Spiral\ORM\Entities\Relations\Traits\MatchTrait;
15
use Spiral\ORM\Entities\Relations\Traits\PartialTrait;
16
use Spiral\ORM\Exceptions\RelationException;
17
use Spiral\ORM\Exceptions\SelectorException;
18
use Spiral\ORM\Record;
19
use Spiral\ORM\RecordInterface;
20
use Spiral\ORM\RelationInterface;
21
22
class ManyToManyRelation extends AbstractRelation implements \IteratorAggregate, \Countable
23
{
24
    use MatchTrait, PartialTrait;
25
26
    /**
27
     * @var \SplObjectStorage
28
     */
29
    private $pivotData;
30
31
    /**
32
     * Linked records.
33
     *
34
     * @var RecordInterface[]
35
     */
36
    private $linked = [];
37
38
    /**
39
     * Record which pivot data was updated, record must still present in linked array.
40
     *
41
     * @var array
42
     */
43
    private $updated = [];
44
45
    /**
46
     * Records scheduled to be de-associated.
47
     *
48
     * @var RecordInterface[]
49
     */
50
    private $unlinked = [];
51
52
    /**
53
     * {@inheritdoc}
54
     */
55
    public function hasRelated(): bool
56
    {
57
        return !empty($this->linked);
58
    }
59
60
    /**
61
     * {@inheritdoc}
62
     */
63
    public function withContext(
64
        RecordInterface $parent,
65
        bool $loaded = false,
66
        array $data = null
67
    ): RelationInterface {
68
        /**
69
         * @var self $relation
70
         */
71
        $relation = parent::withContext($parent, $loaded, $data);
72
        $relation->pivotData = new \SplObjectStorage();
73
74
        return $relation;
75
    }
76
77
    /**
78
     * {@inheritdoc}
79
     */
80
    public function setRelated($value)
81
    {
82
        $this->loadData(true);
83
84
        if (is_null($value)) {
85
            $value = [];
86
        }
87
88
        if (!is_array($value)) {
89
            throw new RelationException("HasMany relation can only be set with array of entities");
90
        }
91
92
        //Sync values without forcing it (no autoloading), i.e. clear CURRENT associations
93
        //  $this->sync($value, [], false);
94
    }
95
96
    /**
97
     * @return $this
98
     */
99
    public function getRelated()
100
    {
101
        return $this;
102
    }
103
104
    /**
105
     * Iterate over linked instances, will force pre-loading unless partial.
106
     *
107
     * @return \ArrayIterator
108
     */
109
    public function getIterator()
110
    {
111
        return new \ArrayIterator($this->loadData(true)->linked);
112
    }
113
114
    /**
115
     * @return int
116
     */
117
    public function count()
118
    {
119
        return count($this->loadData(true)->linked);
120
    }
121
122
    /**
123
     * Get all unlinked records.
124
     *
125
     * @return \ArrayIterator
126
     */
127
    public function getUnlinked()
128
    {
129
        return new \ArrayIterator($this->unlinked);
130
    }
131
132
    /**
133
     * Get pivot data associated with specific instance.
134
     *
135
     * @param RecordInterface $record
136
     *
137
     * @return array
138
     *
139
     * @throws RelationException
140
     */
141
    public function getPivot(RecordInterface $record): array
142
    {
143
        if (!$this->pivotData->offsetExists($record)) {
144
            throw new RelationException("Unable to get pivot data for non linked object");
145
        }
146
147
        return $this->pivotData->offsetGet($record);
148
    }
149
150
    /**
151
     * Link record with parent entity. Only record instances is accepted.
152
     *
153
     * @param RecordInterface $record
154
     * @param array           $pivotData
155
     *
156
     * @return self
157
     *
158
     * @throws RelationException
159
     */
160
    public function link(RecordInterface $record, array $pivotData = []): self
161
    {
162
        $this->assertValid($record);
163
164
        if (in_array($record, $this->linked)) {
165
            //Merging pivot data
166
            $this->pivotData->offsetSet($record, $pivotData + $this->getPivot($record));
167
168
            if (in_array($record, $this->updated)) {
169
                //Indicating that record pivot data has been changed
170
                $this->updated[] = $record;
171
            }
172
173
            return $this;
174
        }
175
176
        //New association
177
        $this->linked[] = $record;
178
        $this->pivotData->offsetSet($record, $pivotData);
179
180
        return $this;
181
    }
182
183
    /**
184
     * Unlink specific entity from relation.
185
     *
186
     * @param RecordInterface $record
187
     *
188
     * @return self
189
     *
190
     * @throws RelationException When entity not linked.
191
     */
192
    public function unlink(RecordInterface $record): self
193
    {
194
        foreach ($this->linked as $index => $linked) {
195
            if ($linked === $record) {
196
                //Removing
197
                unset($this->linked[$index]);
198
                $this->unlinked[] = $linked;
199
                break;
200
            }
201
        }
202
203
        $this->linked = array_values($this->linked);
204
205
        return $this;
206
    }
207
208
    /**
209
     * Check if given query points to linked entity.
210
     *
211
     * Example:
212
     * echo $post->tags->has(1);
213
     * echo $post->tags->has(['name'=>'tag a']);
214
     *
215
     * @param array|RecordInterface|mixed $query Fields, entity or PK.
216
     *
217
     * @return bool
218
     */
219
    public function has($query)
220
    {
221
        return !empty($this->matchOne($query));
222
    }
223
224
    /**
225
     * Fine one entity for a given query or return null. Method will autoload data.
226
     *
227
     * Example: ->matchOne(['value' => 'something', ...]);
228
     *
229
     * @param array|RecordInterface|mixed $query Fields, entity or PK.
230
     *
231
     * @return RecordInterface|null
232
     */
233
    public function matchOne($query)
234
    {
235
        foreach ($this->loadData(true)->linked as $instance) {
236
            if ($this->match($instance, $query)) {
237
                return $instance;
238
            }
239
        }
240
241
        return null;
242
    }
243
244
    /**
245
     * Return only instances matched given query, performed in memory! Only simple conditions are
246
     * allowed. Not "find" due trademark violation. Method will autoload data.
247
     *
248
     * Example: ->matchMultiple(['value' => 'something', ...]);
249
     *
250
     * @param array|RecordInterface|mixed $query Fields, entity or PK.
251
     *
252
     * @return \ArrayIterator
253
     */
254
    public function matchMultiple($query)
255
    {
256
        $result = [];
257
        foreach ($this->loadData()->linked as $instance) {
258
            if ($this->match($instance, $query)) {
259
                $result[] = $instance;
260
            }
261
        }
262
263
        return new \ArrayIterator($result);
264
    }
265
266
    /**
267
     * {@inheritdoc}
268
     */
269
    public function queueCommands(ContextualCommandInterface $command): CommandInterface
270
    {
271
        return new NullCommand();
272
    }
273
274
    /**
275
     * Load related records from database.
276
     *
277
     * @param bool $autoload
278
     *
279
     * @return self
280
     *
281
     * @throws SelectorException
282
     * @throws QueryException (needs wrapping)
283
     */
284
    protected function loadData(bool $autoload = true): self
285
    {
286
        if ($this->loaded) {
287
            return $this;
288
        }
289
290
        $this->loaded = true;
291
292
        if (empty($this->data) || !is_array($this->data)) {
293
            if ($this->autoload && $autoload) {
294
                //Only for non partial selections
295
                $this->data = $this->loadRelated();
296
            } else {
297
                $this->data = [];
298
            }
299
        }
300
301
        return $this->initInstances();
302
    }
303
304
    /**
305
     * Fetch data from database. Lazy load.
306
     *
307
     * @return array
308
     */
309
    protected function loadRelated(): array
310
    {
311
        $innerKey = $this->key(Record::INNER_KEY);
0 ignored issues
show
Unused Code introduced by
$innerKey is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
312
313
        //todo: load
314
315
        return [];
316
    }
317
318
    /**
319
     * Init relations and populate pivot map.
320
     *
321
     * @return ManyToManyRelation
322
     */
323
    private function initInstances(): self
324
    {
325
        if (is_array($this->data) && !empty($this->data)) {
326
            //Iterates and instantiate records
327
            $iterator = new RecordIterator($this->data, $this->class, $this->orm);
328
329
            foreach ($iterator as $pivotData => $item) {
330
                if (in_array($item, $this->linked)) {
331
                    //Skip duplicates (if any?)
332
                    continue;
333
                }
334
335
                $this->pivotData->attach($item, $pivotData);
336
                $this->linked[] = $item;
337
            }
338
        }
339
340
        //Memory free
341
        $this->data = [];
342
343
        return $this;
344
    }
345
}