Completed
Branch feature/pre-split (9d6b17)
by Anton
03:28
created

AbstractRecord::hasChanges()   D

Complexity

Conditions 9
Paths 7

Size

Total Lines 31
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
eloc 14
nc 7
nop 1
dl 0
loc 31
rs 4.909
c 0
b 0
f 0
1
<?php
2
/**
3
 * components
4
 *
5
 * @author    Wolfy-J
6
 */
7
namespace Spiral\ORM;
8
9
use Spiral\Models\AccessorInterface;
10
use Spiral\Models\SchematicEntity;
11
use Spiral\Models\Traits\SolidableTrait;
12
use Spiral\ORM\Entities\RelationBucket;
13
14
/**
15
 * Provides data and relation access functionality.
16
 */
17
abstract class AbstractRecord extends SchematicEntity
18
{
19
    use SolidableTrait;
20
21
    /**
22
     * Set of schema sections needed to describe entity behaviour.
23
     */
24
    const SH_PRIMARY_KEY = 0;
25
    const SH_DEFAULTS    = 1;
26
    const SH_RELATIONS   = 6;
27
28
    /**
29
     * Record behaviour definition.
30
     *
31
     * @var array
32
     */
33
    private $recordSchema = [];
34
35
    /**
36
     * Record field updates (changed values). This array contain set of initial property values if
37
     * any of them changed.
38
     *
39
     * @var array
40
     */
41
    private $changes = [];
42
43
    /**
44
     * AssociatedRelation bucket. Manages declared record relations.
45
     *
46
     * @var RelationBucket
47
     */
48
    protected $relations;
49
50
    /**
51
     * Parent ORM instance, responsible for relation initialization and lazy loading operations.
52
     *
53
     * @invisible
54
     * @var ORMInterface
55
     */
56
    protected $orm;
57
58
    /**
59
     * @param ORMInterface   $orm
60
     * @param array          $data
61
     * @param RelationBucket $relations
62
     */
63
    public function __construct(
64
        ORMInterface $orm,
65
        array $data = [],
66
        RelationBucket $relations
67
    ) {
68
        $this->orm = $orm;
69
        $this->recordSchema = $this->orm->define(static::class, ORMInterface::R_SCHEMA);
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->orm->define(stati...ORMInterface::R_SCHEMA) of type * is incompatible with the declared type array of property $recordSchema.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
70
71
        $this->relations = $relations;
72
        $this->relations->extractRelations($data);
73
74
        //Populating default fields
75
        parent::__construct($data + $this->recordSchema[self::SH_DEFAULTS], $this->recordSchema);
76
    }
77
78
    /**
79
     * Get value of primary of model. Make sure to call isLoaded first!
80
     *
81
     * @return int|string|null
82
     */
83
    public function primaryKey()
84
    {
85
        return $this->getField($this->primaryColumn(), null);
86
    }
87
88
    /**
89
     * {@inheritdoc}
90
     *
91
     * @throws RelationException
92
     */
93
    public function getField(string $name, $default = null, bool $filter = true)
94
    {
95
        if ($this->relations->has($name)) {
96
            return $this->relations->getRelated($name);
97
        }
98
99
        $this->assertField($name);
100
101
        return parent::getField($name, $default, $filter);
102
    }
103
104
    /**
105
     * {@inheritdoc}
106
     *
107
     * @param bool $registerChanges Track field changes.
108
     *
109
     * @throws RelationException
110
     */
111
    public function setField(
112
        string $name,
113
        $value,
114
        bool $filter = true,
115
        bool $registerChanges = true
116
    ) {
117
        if ($this->relations->has($name)) {
118
            //Would not work with relations which do not represent singular entities
119
            $this->relations->setRelated($name, $value);
120
121
            return;
122
        }
123
124
        $this->assertField($name);
125
        if ($registerChanges) {
126
            $this->registerChange($name);
127
        }
128
129
        parent::setField($name, $value, $filter);
130
    }
131
132
    /**
133
     * {@inheritdoc}
134
     */
135
    public function hasField(string $name): bool
136
    {
137
        if ($this->relations->has($name)) {
138
            return $this->relations->hasRelated($name);
139
        }
140
141
        return parent::hasField($name);
142
    }
143
144
    /**
145
     * {@inheritdoc}
146
     *
147
     * @throws FieldException
148
     * @throws RelationException
149
     */
150
    public function __unset($offset)
151
    {
152
        if ($this->relations->has($offset)) {
153
            //Flush associated relation value if possible
154
            $this->relations->flushRelated($offset);
155
156
            return;
157
        }
158
159
        if (!$this->isNullable($offset)) {
160
            throw new FieldException("Unable to unset not nullable field '{$offset}'");
161
        }
162
163
        $this->setField($offset, null, false);
164
    }
165
166
    /**
167
     * {@inheritdoc}
168
     *
169
     * Method does not check updates in nested relation, but only in primary record.
170
     *
171
     * @param string $field Check once specific field changes.
172
     */
173
    public function hasChanges(string $field = null): bool
174
    {
175
        //Check updates for specific field
176
        if (!empty($field)) {
177
            if (array_key_exists($field, $this->changes)) {
178
                return true;
179
            }
180
181
            //Do not force accessor creation
182
            $value = $this->getField($field, null, false);
183
            if ($value instanceof RecordAccessorInterface && $value->hasUpdates()) {
184
                return true;
185
            }
186
187
            return false;
188
        }
189
190
        if (!empty($this->changes)) {
191
            return true;
192
        }
193
194
        //Do not force accessor creation
195
        foreach ($this->getFields(false) as $value) {
196
            //Checking all fields for changes (handled internally)
197
            if ($value instanceof RecordAccessorInterface && $value->hasUpdates()) {
198
                return true;
199
            }
200
        }
201
202
        return false;
203
    }
204
205
    /**
206
     * @return array
207
     */
208
    public function __debugInfo()
209
    {
210
        return [
211
            'database'  => $this->orm->define(static::class, ORMInterface::R_DATABASE),
212
            'table'     => $this->orm->define(static::class, ORMInterface::R_TABLE),
213
            'fields'    => $this->getFields(),
214
            'relations' => $this->relations
215
        ];
216
    }
217
218
    /**
219
     * {@inheritdoc}
220
     */
221
    protected function isNullable(string $field): bool
222
    {
223
        if (array_key_exists($field, $this->recordSchema[self::SH_DEFAULTS])) {
224
            //Only fields with default null value can be nullable
225
            return is_null($this->recordSchema[self::SH_DEFAULTS][$field]);
226
        }
227
228
        //Values unknown to schema always nullable
229
        return true;
230
    }
231
232
    /**
233
     * {@inheritdoc}
234
     *
235
     * DocumentEntity will pass ODM instance as part of accessor context.
236
     *
237
     * @see CompositionDefinition
238
     */
239
    protected function createAccessor(
240
        $accessor,
241
        string $name,
242
        $value,
243
        array $context = []
244
    ): AccessorInterface {
245
        //Giving ORM as context
246
        return parent::createAccessor($accessor, $name, $value, $context + ['orm' => $this->orm]);
247
    }
248
249
    /**
250
     * {@inheritdoc}
251
     */
252
    protected function iocContainer()
253
    {
254
        if ($this->orm instanceof Component) {
0 ignored issues
show
Bug introduced by
The class Spiral\ORM\Component does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
255
            //Forwarding IoC scope to parent ORM instance
256
            return $this->orm->iocContainer();
257
        }
258
259
        return parent::iocContainer();
260
    }
261
262
    /**
263
     * Name of column used as primary key.
264
     *
265
     * @return string
266
     */
267
    protected function primaryColumn(): string
268
    {
269
        return $this->recordSchema[self::SH_PRIMARY_KEY];
270
    }
271
272
    /**
273
     * Create set of fields to be sent to UPDATE statement.
274
     *
275
     * @param bool $skipPrimary Skip primary key
276
     *
277
     * @return array
278
     */
279
    protected function packChanges(bool $skipPrimary = false): array
280
    {
281
        if (!$this->hasChanges() && !$this->isSolid()) {
282
            return [];
283
        }
284
285
        if ($this->isSolid()) {
286
            //Solid record always updated as one big solid
287
            $updates = $this->packValue();
288
        } else {
289
            //Updating each field individually
290
            $updates = [];
291
            foreach ($this->getFields(false) as $field => $value) {
292
                //Handled by sub-accessor
293
                if ($value instanceof RecordAccessorInterface) {
294
                    if ($value->hasUpdates()) {
295
                        $updates[$field] = $value->compileUpdates($field);
296
                        continue;
297
                    }
298
299
                    $value = $value->packValue();
300
                }
301
302
                //Field change registered
303
                if (array_key_exists($field, $this->changes)) {
304
                    $updates[$field] = $value;
305
                }
306
            }
307
        }
308
309
        if ($skipPrimary) {
310
            unset($updates[$this->primaryColumn()]);
311
        }
312
313
        return $updates;
314
    }
315
316
    /**
317
     * Indicate that all updates done, reset dirty state.
318
     */
319
    protected function flushChanges()
320
    {
321
        $this->changes = [];
322
323
        foreach ($this->getFields(false) as $field => $value) {
324
            if ($value instanceof RecordAccessorInterface) {
325
                $value->flushUpdates();
326
            }
327
        }
328
    }
329
330
    /**
331
     * @param string $name
332
     */
333
    private function registerChange(string $name)
334
    {
335
        $original = $this->getField($name, null, false);
336
337
        if (!array_key_exists($name, $this->changes)) {
338
            //Let's keep track of how field looked before first change
339
            $this->changes[$name] = $original instanceof AccessorInterface
340
                ? $original->packValue()
341
                : $original;
342
        }
343
    }
344
345
    /**
346
     * @param string $name
347
     *
348
     * @throws FieldException
349
     */
350
    private function assertField(string $name)
351
    {
352
        if (!$this->hasField($name)) {
353
            throw new FieldException(sprintf(
354
                "No such property '%s' in '%s', check schema being relevant",
355
                $name,
356
                get_called_class()
357
            ));
358
        }
359
    }
360
}