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

HasManyRelation::getDeleted()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 4
rs 10
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\Commands\TransactionalCommand;
13
use Spiral\ORM\ContextualCommandInterface;
14
use Spiral\ORM\Entities\RecordIterator;
15
use Spiral\ORM\Entities\Relations\Traits\MatchTrait;
16
use Spiral\ORM\Entities\Relations\Traits\PartialTrait;
17
use Spiral\ORM\Exceptions\RelationException;
18
use Spiral\ORM\Exceptions\SelectorException;
19
use Spiral\ORM\Record;
20
use Spiral\ORM\RecordInterface;
21
use Spiral\ORM\RelationInterface;
22
23
/**
24
 * Attention, this relation delete operation works inside loaded scope!
25
 *
26
 * When empty array assigned to relation it will schedule all related instances to be deleted.
27
 *
28
 * If you wish to load with relation WITHOUT loading previous records use [] initialization.
29
 */
30
class HasManyRelation extends AbstractRelation implements \IteratorAggregate, \Countable
31
{
32
    use MatchTrait, PartialTrait;
33
34
    /**
35
     * Loaded list of records. SplObjectStorage?
36
     *
37
     * @var RecordInterface[]
38
     */
39
    private $instances = [];
40
41
    /**
42
     * Records deleted from list. Potentially pre-schedule command?
43
     *
44
     * @var RecordInterface[]
45
     */
46
    private $deletedInstances = [];
47
48
    /**
49
     * {@inheritdoc}
50
     */
51
    public function hasRelated(): bool
52
    {
53
        if (!$this->isLoaded()) {
54
            //Lazy loading our relation data
55
            $this->loadData();
56
        }
57
58
        return !empty($this->instances);
59
    }
60
61
    /**
62
     * {@inheritdoc}
63
     */
64
    public function withContext(
65
        RecordInterface $parent,
66
        bool $loaded = false,
67
        array $data = null
68
    ): RelationInterface {
69
        $hasMany = parent::withContext($parent, $loaded, $data);
70
71
        /**
72
         * @var self $hasMany
73
         */
74
        if ($hasMany->loaded) {
75
            //Init all nested models immediately
76
            $hasMany->initInstances();
77
        }
78
79
        return $hasMany->initInstances();
80
    }
81
82
    /**
83
     * {@inheritdoc}
84
     *
85
     * @throws RelationException
86
     */
87
    public function setRelated($value)
88
    {
89
        $this->loadData(true);
90
91
        if (is_null($value)) {
92
            $value = [];
93
        }
94
95
        if (!is_array($value)) {
96
            throw new RelationException("HasMany relation can only be set with array of entities");
97
        }
98
99
        //Cleaning existed instances
100
        $this->deletedInstances = array_unique(array_merge(
101
            $this->deletedInstances,
102
            $this->instances
103
        ));
104
105
        $this->instances = [];
106
        foreach ($value as $item) {
107
            if (!is_null($item)) {
108
                $this->assertValid($item);
109
                $this->instances[] = $item;
110
            }
111
        }
112
    }
113
114
    /**
115
     * Has many relation represent itself (see getIterator method).
116
     *
117
     * @return $this
118
     */
119
    public function getRelated()
120
    {
121
        return $this;
122
    }
123
124
    /**
125
     * Iterate over instance set.
126
     *
127
     * @return \ArrayIterator
128
     */
129
    public function getIterator()
130
    {
131
        return new \ArrayIterator($this->loadData(true)->instances);
132
    }
133
134
    /**
135
     * @return int
136
     */
137
    public function count()
138
    {
139
        return count($this->loadData(true)->instances);
140
    }
141
142
    /**
143
     * Iterate over deleted instances.
144
     *
145
     * @return \ArrayIterator
146
     */
147
    public function getDeleted()
148
    {
149
        return new \ArrayIterator($this->deletedInstances);
150
    }
151
152
    /**
153
     * Method will autoload data.
154
     *
155
     * @param array|RecordInterface|mixed $query Fields, entity or PK.
156
     *
157
     * @return bool
158
     */
159
    public function has($query): bool
160
    {
161
        return !empty($this->matchOne($query));
162
    }
163
164
    /**
165
     * Add new record into entity set. Attention, usage of this method WILL load relation data
166
     * unless partial.
167
     *
168
     * @param RecordInterface $record
169
     *
170
     * @return self
171
     *
172
     * @throws RelationException
173
     */
174
    public function add(RecordInterface $record): self
175
    {
176
        $this->assertValid($record);
177
        $this->loadData(true)->instances[] = $record;
178
179
        return $this;
180
    }
181
182
    /**
183
     * Delete one record, strict compaction, make sure exactly same instance is given.
184
     *
185
     * @param RecordInterface $record
186
     *
187
     * @return self
188
     *
189
     * @throws RelationException
190
     */
191
    public function delete(RecordInterface $record): self
192
    {
193
        $this->loadData(true);
194
        $this->assertValid($record);
195
196
        foreach ($this->instances as $index => $instance) {
197
            if ($instance === $record) {
198
                //Remove from save
199
                unset($this->instances[$index]);
200
                $this->deletedInstances[] = $instance;
201
                break;
202
            }
203
        }
204
205
        $this->instances = array_values($this->instances);
206
207
        return $this;
208
    }
209
210
    /**
211
     * Fine one entity for a given query or return null. Method will autoload data.
212
     *
213
     * Example: ->matchOne(['value' => 'something', ...]);
214
     *
215
     * @param array|RecordInterface|mixed $query Fields, entity or PK.
216
     *
217
     * @return RecordInterface|null
218
     */
219
    public function matchOne($query)
220
    {
221
        foreach ($this->loadData(true)->instances as $instance) {
222
            if ($this->match($instance, $query)) {
223
                return $instance;
224
            }
225
        }
226
227
        return null;
228
    }
229
230
    /**
231
     * Return only instances matched given query, performed in memory! Only simple conditions are
232
     * allowed. Not "find" due trademark violation. Method will autoload data.
233
     *
234
     * Example: ->matchMultiple(['value' => 'something', ...]);
235
     *
236
     * @param array|RecordInterface|mixed $query Fields, entity or PK.
237
     *
238
     * @return \ArrayIterator
239
     */
240
    public function matchMultiple($query)
241
    {
242
        $result = [];
243
        foreach ($this->loadData()->instances as $instance) {
244
            if ($this->match($instance, $query)) {
245
                $result[] = $instance;
246
            }
247
        }
248
249
        return new \ArrayIterator($result);
250
    }
251
252
    /**
253
     * {@inheritdoc}
254
     */
255
    public function queueCommands(ContextualCommandInterface $command): CommandInterface
256
    {
257
        //No autoloading here
258
259
        if (empty($this->instances) && empty($this->deletedInstances)) {
260
            return new NullCommand();
261
        }
262
263
        $transaction = new TransactionalCommand();
264
265
        //Delete old instances first
266
        foreach ($this->deletedInstances as $deleted) {
267
            //To de-associate use BELONGS_TO relation
268
            $transaction->addCommand($deleted->queueDelete());
269
        }
270
271
        //Store all instances
272
        foreach ($this->instances as $instance) {
273
            $transaction->addCommand($this->queueRelated($command, $instance));
274
        }
275
276
        //Flushing instances
277
        $this->deletedInstances = [];
278
279
        return $transaction;
280
    }
281
282
    /**
283
     * {@inheritdoc}
284
     *
285
     * @return self
286
     *
287
     * @throws SelectorException
288
     * @throws QueryException (needs wrapping)
289
     */
290
    protected function loadData(bool $autoload = true): self
291
    {
292
        if ($this->loaded) {
293
            return $this;
294
        }
295
296
        $this->loaded = true;
297
298
        if (empty($this->data) || !is_array($this->data)) {
299
            if ($this->autoload && $autoload) {
300
                //Only for non partial selections (excluded already selected)
301
                $this->data = $this->loadRelated();
302
            } else {
303
                $this->data = [];
304
            }
305
        }
306
307
        return $this->initInstances();
308
    }
309
310
    /**
311
     * Fetch data from database. Lazy load.
312
     *
313
     * @return array
314
     */
315
    protected function loadRelated(): array
316
    {
317
        $innerKey = $this->key(Record::INNER_KEY);
318
        if (!empty($this->parent->getField($innerKey))) {
319
            return $this->orm
320
                ->selector($this->class)
321
                ->where($this->key(Record::OUTER_KEY), $this->parent->getField($innerKey))
0 ignored issues
show
Unused Code introduced by
The call to RecordSelector::where() has too many arguments starting with $this->parent->getField($innerKey).

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
322
                ->fetchData();
323
        }
324
325
        return [];
326
    }
327
328
    /**
329
     * Init pre-loaded data.
330
     *
331
     * @return HasManyRelation
332
     */
333
    private function initInstances(): self
334
    {
335
        if (is_array($this->data) && !empty($this->data)) {
336
            //Iterates and instantiate records
337
            $iterator = new RecordIterator($this->data, $this->class, $this->orm);
338
339
            foreach ($iterator as $item) {
340
                if ($this->has($item)) {
341
                    //Skip duplicates
342
                    continue;
343
                }
344
345
                $this->instances[] = $item;
346
            }
347
        }
348
349
        //Memory free
350
        $this->data = null;
351
352
        return $this;
353
    }
354
355
    /**
356
     * @param ContextualCommandInterface $command
357
     * @param RecordInterface            $instance
358
     *
359
     * @return CommandInterface
360
     */
361
    private function queueRelated(
362
        ContextualCommandInterface $command,
363
        RecordInterface $instance
364
    ): CommandInterface {
365
        //Related entity store command
366
        $inner = $instance->queueStore(true);
367
368
        if (!$this->isSynced($this->parent, $instance)) {
369
            //Syncing FKs
370
            if ($this->key(Record::INNER_KEY) != $this->primaryColumnOf($this->parent)) {
371
                $command->addContext(
372
                    $this->key(Record::OUTER_KEY),
373
                    $this->parent->getField($this->key(Record::INNER_KEY))
374
                );
375
            } else {
376
                //Syncing FKs
377
                $command->onExecute(function (ContextualCommandInterface $command) use ($inner) {
378
                    $inner->addContext(
379
                        $this->key(Record::OUTER_KEY),
380
                        $command->primaryKey()
381
                    );
382
                });
383
            }
384
        }
385
386
        return $inner;
387
    }
388
}