Completed
Branch feature/pre-split (c70fe0)
by Anton
03:38
created

HasManyRelation::createSelector()   B

Complexity

Conditions 3
Paths 4

Size

Total Lines 24
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

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