Completed
Push — master ( fb9538...a8cb34 )
by Anton
03:39
created

HasManyRelation::detach()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 7
nc 3
nop 1
dl 0
loc 14
rs 9.4285
c 0
b 0
f 0
1
<?php
2
/**
3
 * Spiral, Core Components
4
 *
5
 * @author Wolfy-J
6
 */
7
8
namespace Spiral\ORM\Entities\Relations;
9
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\RecordSelector;
15
use Spiral\ORM\Entities\Relations\Traits\LookupTrait;
16
use Spiral\ORM\Entities\Relations\Traits\SyncedTrait;
17
use Spiral\ORM\Exceptions\RelationException;
18
use Spiral\ORM\Helpers\WhereDecorator;
19
use Spiral\ORM\ORMInterface;
20
use Spiral\ORM\Record;
21
use Spiral\ORM\RecordInterface;
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 MultipleRelation implements \IteratorAggregate, \Countable
31
{
32
    use LookupTrait, SyncedTrait;
33
34
    /**
35
     * Records deleted from list. Potentially pre-schedule command?
36
     *
37
     * @var RecordInterface[]
38
     */
39
    private $deleteInstances = [];
40
41
    /**
42
     * {@inheritdoc}
43
     *
44
     * @throws RelationException
45
     */
46
    public function setRelated($value)
47
    {
48
        $this->loadData(true);
49
50
        if (is_null($value)) {
51
            $value = [];
52
        }
53
54
        if (!is_array($value)) {
55
            throw new RelationException("HasMany relation can only be set with array of entities");
56
        }
57
58
        //Do not add items twice
59
        $matched = [];
60
        foreach ($value as $index => $item) {
61
            if (is_null($item)) {
62
                unset($value[$index]);
63
                continue;
64
            }
65
66
            $this->assertValid($item);
67
            if (!empty($instance = $this->matchOne($item))) {
68
                $matched[] = $instance;
69
                unset($value[$index]);
70
            }
71
        }
72
73
        $this->deleteInstances = array_diff($this->instances, $matched);
74
        $this->instances = $matched + $value;
75
    }
76
77
    /**
78
     * Iterate over deleted instances.
79
     *
80
     * @return \ArrayIterator
81
     */
82
    public function getDeleted()
83
    {
84
        return new \ArrayIterator($this->deleteInstances);
85
    }
86
87
    /**
88
     * Add new record into entity set. Attention, usage of this method WILL load relation data
89
     * unless partial.
90
     *
91
     * @param RecordInterface $record
92
     *
93
     * @return self
94
     *
95
     * @throws RelationException
96
     */
97
    public function add(RecordInterface $record): self
98
    {
99
        $this->assertValid($record);
100
        $this->loadData(true)->instances[] = $record;
101
102
        return $this;
103
    }
104
105
    /**
106
     * Delete one record, strict compaction, make sure exactly same instance is given.
107
     *
108
     * @param RecordInterface $record
109
     *
110
     * @return self
111
     *
112
     * @throws RelationException
113
     */
114
    public function delete(RecordInterface $record): self
115
    {
116
        $this->loadData(true);
117
        $this->assertValid($record);
118
119
        foreach ($this->instances as $index => $instance) {
120
            if ($this->match($instance, $record)) {
121
                //Remove from save
122
                unset($this->instances[$index]);
123
                $this->deleteInstances[] = $instance;
124
                break;
125
            }
126
        }
127
128
        $this->instances = array_values($this->instances);
129
130
        return $this;
131
    }
132
133
    /**
134
     * Detach given object from set of instances but do not delete it in database, use it to
135
     * transfer object between sets.
136
     *
137
     * @param \Spiral\ORM\RecordInterface $record
138
     *
139
     * @return \Spiral\ORM\RecordInterface
140
     *
141
     * @throws RelationException When object not presented in a set.
142
     */
143
    public function detach(RecordInterface $record): RecordInterface
144
    {
145
        $this->loadData(true);
146
        foreach ($this->instances as $index => $instance) {
147
            if ($this->match($instance, $record)) {
148
                //Remove from save
149
                unset($this->instances[$index]);
150
151
                return $instance;
152
            }
153
        }
154
155
        throw new RelationException("Record {$record} not found in HasMany relation");
156
    }
157
158
    /**
159
     * {@inheritdoc}
160
     */
161
    public function queueCommands(ContextualCommandInterface $parentCommand): CommandInterface
162
    {
163
        //No autoloading here
164
165
        if (empty($this->instances) && empty($this->deleteInstances)) {
166
            return new NullCommand();
167
        }
168
169
        $transaction = new TransactionalCommand();
170
171
        //Delete old instances first
172
        foreach ($this->deleteInstances as $deleted) {
173
            //To de-associate use BELONGS_TO relation
174
            $transaction->addCommand($deleted->queueDelete());
175
        }
176
177
        //Store all instances
178
        foreach ($this->instances as $instance) {
179
            $transaction->addCommand($this->queueRelated($parentCommand, $instance));
180
        }
181
182
        //Flushing instances
183
        $this->deleteInstances = [];
184
185
        return $transaction;
186
    }
187
188
    /**
189
     * @param ContextualCommandInterface $parentCommand
190
     * @param RecordInterface            $instance
191
     *
192
     * @return CommandInterface
193
     */
194
    protected function queueRelated(
195
        ContextualCommandInterface $parentCommand,
196
        RecordInterface $instance
197
    ): CommandInterface {
198
        //Related entity store command
199
        $innerCommand = $instance->queueStore(true);
200
201
        if (!$this->isSynced($this->parent, $instance)) {
202
            //Delayed linking
203
            $parentCommand->onExecute(function ($outerCommand) use ($innerCommand) {
204
                $innerCommand->addContext(
205
                    $this->key(Record::OUTER_KEY),
206
                    $this->lookupKey(Record::INNER_KEY, $this->parent, $outerCommand)
207
                );
208
209
                if (!empty($morphKey = $this->key(Record::MORPH_KEY))) {
210
                    //HasOne relation support additional morph key
211
                    $innerCommand->addContext(
212
                        $this->key(Record::MORPH_KEY),
213
                        $this->orm->define(get_class($this->parent), ORMInterface::R_ROLE_NAME)
214
                    );
215
                }
216
            });
217
        }
218
219
        return $innerCommand;
220
    }
221
222
    /**
223
     * Fetch data from database. Lazy load.
224
     *
225
     * @return array
226
     */
227
    protected function loadRelated(): array
228
    {
229
        $innerKey = $this->parent->getField($this->key(Record::INNER_KEY));
230
        if (!empty($innerKey)) {
231
            return $this->createSelector($innerKey)->fetchData();
232
        }
233
234
        return [];
235
    }
236
237
    /**
238
     * Create outer selector for a given inner key value.
239
     *
240
     * @param mixed $innerKey
241
     *
242
     * @return RecordSelector
243
     */
244
    protected function createSelector($innerKey): RecordSelector
245
    {
246
        $selector = $this->orm->selector($this->class)->where(
247
            $this->key(Record::OUTER_KEY),
248
            $innerKey
0 ignored issues
show
Unused Code introduced by
The call to RecordSelector::where() has too many arguments starting with $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...
249
        );
250
251
        if (!empty($this->schema[Record::WHERE])) {
252
            //Configuring where conditions with alias resolution
253
            $decorator = new WhereDecorator($selector, 'where', $selector->getAlias());
254
            $decorator->where($this->schema[Record::WHERE]);
255
        }
256
257
        if (!empty($this->key(Record::MORPH_KEY))) {
258
            //Morph key
259
            $decorator = new WhereDecorator($selector, 'where', $selector->getAlias());
260
            $decorator->where(
261
                '{@}.' . $this->key(Record::MORPH_KEY),
262
                $this->orm->define(get_class($this->parent), ORMInterface::R_ROLE_NAME)
263
            );
264
        }
265
266
        return $selector;
267
    }
268
}