Completed
Branch feature/pre-split (825af5)
by Anton
03:36
created

HasManyRelation   A

Complexity

Total Complexity 21

Size/Duplication

Total Lines 197
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 10

Importance

Changes 0
Metric Value
dl 0
loc 197
rs 10
c 0
b 0
f 0
wmc 21
lcom 1
cbo 10

8 Methods

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