Passed
Push — master ( f60105...b4c3d1 )
by Anton
02:20
created

Embedded   A

Complexity

Total Complexity 26

Size/Duplication

Total Lines 222
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 64
dl 0
loc 222
rs 10
c 0
b 0
f 0
wmc 26

12 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 11 1
A extract() 0 3 1
A initPromise() 0 16 3
A getName() 0 3 1
A isCascade() 0 4 1
A init() 0 8 1
A fetchKey() 0 7 2
A compare() 0 7 3
A resolve() 0 8 2
A getChanges() 0 5 1
A mapColumns() 0 12 3
B queue() 0 38 7
1
<?php
2
/**
3
 * Spiral Framework.
4
 *
5
 * @license   MIT
6
 * @author    Anton Titov (Wolfy-J)
7
 */
8
declare(strict_types=1);
9
10
namespace Cycle\ORM\Relation;
11
12
use Cycle\ORM\Command\Branch\Nil;
13
use Cycle\ORM\Command\CommandInterface;
14
use Cycle\ORM\Command\ContextCarrierInterface as CC;
15
use Cycle\ORM\Exception\Relation\NullException;
16
use Cycle\ORM\Heap\Node;
17
use Cycle\ORM\Heap\State;
18
use Cycle\ORM\MapperInterface;
19
use Cycle\ORM\ORMInterface;
20
use Cycle\ORM\Promise\PromiseInterface;
21
use Cycle\ORM\Promise\ReferenceInterface;
22
use Cycle\ORM\Relation;
23
use Cycle\ORM\Schema;
24
use Cycle\ORM\Select\SourceProviderInterface;
25
26
/**
27
 * Embeds one object to another.
28
 */
29
final class Embedded implements RelationInterface
30
{
31
    use  Relation\Traits\NodeTrait;
32
33
    /** @var ORMInterface|SourceProviderInterface @internal */
34
    protected $orm;
35
36
    /** @var string */
37
    protected $name;
38
39
    /** @var string */
40
    protected $target;
41
42
    /** @var array */
43
    protected $schema;
44
45
    /** @var MapperInterface */
46
    protected $mapper;
47
48
    /** @var string */
49
    protected $primaryKey;
50
51
    /** @var array */
52
    protected $columns = [];
53
54
    /**
55
     * @param ORMInterface $orm
56
     * @param string       $name
57
     * @param string       $target
58
     * @param array        $schema
59
     */
60
    public function __construct(ORMInterface $orm, string $name, string $target, array $schema)
61
    {
62
        $this->orm = $orm;
63
        $this->name = $name;
64
        $this->target = $target;
65
        $this->schema = $schema;
66
        $this->mapper = $this->orm->getMapper($target);
67
68
        // this relation must manage column association manually, bypassing related mapper
69
        $this->primaryKey = $this->orm->getSchema()->define($target, Schema::PRIMARY_KEY);
70
        $this->columns = $this->orm->getSchema()->define($target, Schema::COLUMNS);
71
    }
72
73
    /**
74
     * @inheritdoc
75
     */
76
    public function getName(): string
77
    {
78
        return $this->name;
79
    }
80
81
    /**
82
     * @inheritDoc
83
     */
84
    public function isCascade(): bool
85
    {
86
        // always cascade
87
        return true;
88
    }
89
90
    /**
91
     * @inheritdoc
92
     */
93
    public function init(Node $node, array $data): array
94
    {
95
        // ensure proper object reference
96
        $data[$this->primaryKey] = $node->getData()[$this->primaryKey];
97
98
        $item = $this->orm->make($this->target, $data, Node::MANAGED);
0 ignored issues
show
Bug introduced by
The method make() does not exist on Cycle\ORM\Select\SourceProviderInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Cycle\ORM\Select\SourceProviderInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

98
        /** @scrutinizer ignore-call */ 
99
        $item = $this->orm->make($this->target, $data, Node::MANAGED);
Loading history...
99
100
        return [$item, $item];
101
    }
102
103
    /**
104
     * @inheritdoc
105
     */
106
    public function initPromise(Node $parentNode): array
107
    {
108
        if (empty($primaryKey = $this->fetchKey($parentNode, $this->primaryKey))) {
109
            return [null, null];
110
        }
111
112
        /** @var ORMInterface $orm */
113
        $orm = $this->orm;
114
115
        $e = $orm->getHeap()->find($this->target, $this->primaryKey, $primaryKey);
116
        if ($e !== null) {
117
            return [$e, $e];
118
        }
119
120
        $r = $this->orm->promise($this->target, [$this->primaryKey => $primaryKey]);
0 ignored issues
show
Bug introduced by
The method promise() does not exist on Cycle\ORM\Select\SourceProviderInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Cycle\ORM\Select\SourceProviderInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

120
        /** @scrutinizer ignore-call */ 
121
        $r = $this->orm->promise($this->target, [$this->primaryKey => $primaryKey]);
Loading history...
121
        return [$r, [$this->primaryKey => $primaryKey]];
122
    }
123
124
    /**
125
     * @inheritdoc
126
     */
127
    public function extract($data)
128
    {
129
        return $data;
130
    }
131
132
    /**
133
     * @inheritDoc
134
     */
135
    public function queue(CC $store, $entity, Node $node, $related, $original): CommandInterface
136
    {
137
        if ($related instanceof ReferenceInterface) {
138
            if ($related->__scope() === $original) {
139
                // do not update non resolved and non changed promises
140
                if (!$related instanceof PromiseInterface || !$related->__loaded()) {
141
                    return new Nil();
142
                }
143
                $related = $this->resolve($related);
144
            } else {
145
                // do not affect parent embeddings
146
                $related = clone $this->resolve($related);
147
            }
148
        }
149
150
        if ($related === null) {
151
            throw new NullException("Embedded relation `{$this->name}` can't be null");
152
        }
153
154
        $state = $this->getNode($related)->getState();
155
156
        // calculate embedded node changes
157
        $changes = $this->getChanges($related, $state);
158
159
        // register node changes
160
        $state->setData($changes);
161
162
        // store embedded entity changes via parent command
163
        foreach ($this->mapColumns($changes) as $key => $value) {
164
            $store->register($key, $value, true);
165
        }
166
167
        // currently embeddings does not support chain relations, however it is possible by
168
        // exposing relationMap inside this method. in theory it is possible to use
169
        // parent entity command to carry context for nested relations, however, custom context
170
        // propagation chain must be defined (embedded node => parent command)
171
172
        return new Nil();
173
    }
174
175
    /**
176
     * @param mixed $related
177
     * @param State $state
178
     * @return array
179
     */
180
    protected function getChanges($related, State $state): array
181
    {
182
        $data = $this->mapper->extract($related);
183
184
        return array_udiff_assoc($data, $state->getData(), [static::class, 'compare']);
185
    }
186
187
    /**
188
     * Map internal field names to database specific column names.
189
     *
190
     * @param array $columns
191
     * @return array
192
     */
193
    protected function mapColumns(array $columns): array
194
    {
195
        $result = [];
196
        foreach ($columns as $column => $value) {
197
            if (array_key_exists($column, $this->columns)) {
198
                $result[$this->columns[$column]] = $value;
199
            } else {
200
                $result[$column] = $value;
201
            }
202
        }
203
204
        return $result;
205
    }
206
207
    /**
208
     * @param mixed $a
209
     * @param mixed $b
210
     * @return int
211
     */
212
    protected static function compare($a, $b): int
213
    {
214
        if ($a == $b) {
215
            return 0;
216
        }
217
218
        return ($a > $b) ? 1 : -1;
219
    }
220
221
    /**
222
     * Resolve the reference to the object.
223
     *
224
     * @param ReferenceInterface $reference
225
     * @return mixed|null
226
     */
227
    protected function resolve(ReferenceInterface $reference)
228
    {
229
        if ($reference instanceof PromiseInterface) {
230
            return $reference->__resolve();
231
        }
232
233
        $scope = $reference->__scope();
234
        return $this->orm->get($reference->__role(), key($scope), current($scope), true);
0 ignored issues
show
Bug introduced by
The method get() does not exist on Cycle\ORM\Select\SourceProviderInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Cycle\ORM\Select\SourceProviderInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

234
        return $this->orm->/** @scrutinizer ignore-call */ get($reference->__role(), key($scope), current($scope), true);
Loading history...
235
    }
236
237
    /**
238
     * Fetch key from the state.
239
     *
240
     * @param Node   $state
241
     * @param string $key
242
     * @return mixed|null
243
     */
244
    protected function fetchKey(?Node $state, string $key)
245
    {
246
        if (is_null($state)) {
247
            return null;
248
        }
249
250
        return $state->getData()[$key] ?? null;
251
    }
252
}