Passed
Push — master ( a0406a...5d779f )
by Anton
06:34 queued 04:24
created

Embedded::getTarget()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * Spiral Framework.
5
 *
6
 * @license   MIT
7
 * @author    Anton Titov (Wolfy-J)
8
 */
9
10
declare(strict_types=1);
11
12
namespace Cycle\ORM\Relation;
13
14
use Cycle\ORM\Command\Branch\Nil;
15
use Cycle\ORM\Command\CommandInterface;
16
use Cycle\ORM\Command\ContextCarrierInterface as CC;
17
use Cycle\ORM\Exception\Relation\NullException;
18
use Cycle\ORM\Heap\Node;
19
use Cycle\ORM\Heap\State;
20
use Cycle\ORM\MapperInterface;
21
use Cycle\ORM\ORMInterface;
22
use Cycle\ORM\Promise\PromiseInterface;
23
use Cycle\ORM\Promise\ReferenceInterface;
24
use Cycle\ORM\Relation;
25
use Cycle\ORM\Schema;
26
use Cycle\ORM\Select\SourceProviderInterface;
27
28
/**
29
 * Embeds one object to another.
30
 */
31
final class Embedded implements RelationInterface
32
{
33
    use Relation\Traits\NodeTrait;
34
35
    /** @var ORMInterface|SourceProviderInterface @internal */
36
    protected $orm;
37
38
    /** @var string */
39
    protected $name;
40
41
    /** @var string */
42
    protected $target;
43
44
    /** @var array */
45
    protected $schema;
46
47
    /** @var MapperInterface */
48
    protected $mapper;
49
50
    /** @var string */
51
    protected $primaryKey;
52
53
    /** @var array */
54
    protected $columns = [];
55
56
    /**
57
     * @param ORMInterface $orm
58
     * @param string       $name
59
     * @param string       $target
60
     * @param array        $schema
61
     */
62
    public function __construct(ORMInterface $orm, string $name, string $target, array $schema)
63
    {
64
        $this->orm = $orm;
65
        $this->name = $name;
66
        $this->target = $target;
67
        $this->schema = $schema;
68
        $this->mapper = $this->orm->getMapper($target);
69
70
        // this relation must manage column association manually, bypassing related mapper
71
        $this->primaryKey = $this->orm->getSchema()->define($target, Schema::PRIMARY_KEY);
72
        $this->columns = $this->orm->getSchema()->define($target, Schema::COLUMNS);
73
    }
74
75
    /**
76
     * @inheritdoc
77
     */
78
    public function getName(): string
79
    {
80
        return $this->name;
81
    }
82
83
    /**
84
     * @return string
85
     */
86
    public function getTarget(): string
87
    {
88
        return $this->target;
89
    }
90
91
    /**
92
     * @inheritDoc
93
     */
94
    public function isCascade(): bool
95
    {
96
        // always cascade
97
        return true;
98
    }
99
100
    /**
101
     * @inheritdoc
102
     */
103
    public function init(Node $node, array $data): array
104
    {
105
        // ensure proper object reference
106
        $data[$this->primaryKey] = $node->getData()[$this->primaryKey];
107
108
        $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

108
        /** @scrutinizer ignore-call */ 
109
        $item = $this->orm->make($this->target, $data, Node::MANAGED);
Loading history...
109
110
        return [$item, $item];
111
    }
112
113
    /**
114
     * @inheritdoc
115
     */
116
    public function initPromise(Node $parentNode): array
117
    {
118
        $primaryKey = $this->fetchKey($parentNode, $this->primaryKey);
119
        if (empty($primaryKey)) {
120
            return [null, null];
121
        }
122
123
        /** @var ORMInterface $orm */
124
        $orm = $this->orm;
125
126
        $e = $orm->getHeap()->find($this->target, [$this->primaryKey => $primaryKey]);
127
        if ($e !== null) {
128
            return [$e, $e];
129
        }
130
131
        $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

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

246
        return $this->orm->/** @scrutinizer ignore-call */ get($reference->__role(), $reference->__scope(), true);
Loading history...
247
    }
248
249
    /**
250
     * Fetch key from the state.
251
     *
252
     * @param Node   $state
253
     * @param string $key
254
     * @return mixed|null
255
     */
256
    protected function fetchKey(?Node $state, string $key)
257
    {
258
        if ($state === null) {
259
            return null;
260
        }
261
262
        return $state->getData()[$key] ?? null;
263
    }
264
}
265