Completed
Branch feature/pre-split (c41c6b)
by Anton
03:19
created

HasManyRelation   A

Complexity

Total Complexity 23

Size/Duplication

Total Lines 207
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 10

Importance

Changes 0
Metric Value
dl 0
loc 207
rs 10
c 0
b 0
f 0
wmc 23
lcom 1
cbo 10

8 Methods

Rating   Name   Duplication   Size   Complexity  
B setRelated() 0 30 6
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
B createSelector() 0 25 3
1
<?php
2
/**
3
 * 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
        }
184
185
        return $innerCommand;
186
    }
187
188
    /**
189
     * Fetch data from database. Lazy load.
190
     *
191
     * @return array
192
     */
193
    protected function loadRelated(): array
194
    {
195
        $innerKey = $this->parent->getField($this->key(Record::INNER_KEY));
196
        if (!empty($innerKey)) {
197
            return $this->createSelector($innerKey)->fetchData();
198
        }
199
200
        return [];
201
    }
202
203
    /**
204
     * Create outer selector for a given inner key value.
205
     *
206
     * @param mixed $innerKey
207
     *
208
     * @return RecordSelector
209
     */
210
    protected function createSelector($innerKey): RecordSelector
211
    {
212
        $selector = $this->orm->selector($this->class)->where(
213
            $this->key(Record::OUTER_KEY),
214
            $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...
215
        );
216
217
        if (!empty($this->schema[Record::WHERE])) {
218
            //Configuring where conditions with alias resolution
219
            $decorator = new WhereDecorator($selector, 'where', $selector->getAlias());
220
            $decorator->where($this->schema[Record::WHERE]);
221
222
            if (!empty($this->key(Record::MORPH_KEY))) {
223
                //Clarifying where statement
224
                $decorator->where(
225
                    $this->key(Record::MORPH_KEY),
226
                    $this->orm->define($this->parent, ORMInterface::R_ROLE_NAME)
0 ignored issues
show
Documentation introduced by
$this->parent is of type object<Spiral\ORM\RecordInterface>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
227
                );
228
            }
229
230
            return $selector;
231
        }
232
233
        return $selector;
234
    }
235
}