Completed
Branch feature/pre-split (9269d3)
by Anton
05:27
created

AbstractRecord::__debugInfo()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 6
nc 1
nop 0
dl 0
loc 9
rs 9.6666
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
use Spiral\ORM\Exceptions\RecordException;
14
15
/**
16
 * Provides data and relation access functionality.
17
 */
18
abstract class AbstractRecord extends SchematicEntity
19
{
20
    use SolidableTrait;
21
22
    /**
23
     * Set of schema sections needed to describe entity behaviour.
24
     */
25
    const SH_PRIMARY_KEY = 0;
26
    const SH_DEFAULTS    = 1;
27
    const SH_RELATIONS   = 6;
28
29
    /**
30
     * Record behaviour definition.
31
     *
32
     * @var array
33
     */
34
    private $recordSchema = [];
35
36
    /**
37
     * Record field updates (changed values). This array contain set of initial property values if
38
     * any of them changed.
39
     *
40
     * @var array
41
     */
42
    private $changes = [];
43
44
    /**
45
     * AssociatedRelation bucket. Manages declared record relations.
46
     *
47
     * @var RelationBucket
48
     */
49
    protected $relations;
50
51
    /**
52
     * Parent ORM instance, responsible for relation initialization and lazy loading operations.
53
     *
54
     * @invisible
55
     * @var ORMInterface
56
     */
57
    protected $orm;
58
59
    /**
60
     * @param ORMInterface   $orm
61
     * @param array          $data
62
     * @param RelationBucket $relations
63
     */
64
    public function __construct(
65
        ORMInterface $orm,
66
        array $data = [],
67
        RelationBucket $relations
68
    ) {
69
        $this->orm = $orm;
70
        $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...
71
72
        $this->relations = $relations;
73
        $this->relations->extractRelations($data);
74
75
        //Populating default fields
76
        parent::__construct($data + $this->recordSchema[self::SH_DEFAULTS], $this->recordSchema);
77
    }
78
79
    /**
80
     * Get value of primary of model. Make sure to call isLoaded first, getting PK before entity
81
     * have been saved is not possible.
82
     *
83
     * @return int|string|null
84
     *
85
     * @throws RecordException When PK is empty.
86
     */
87
    public function primaryKey()
88
    {
89
        $primaryKey = $this->getField($this->primaryColumn(), null, false);
90
        if (empty($primaryKey)) {
91
            throw new RecordException("Unable to get PK, value is empty (@see 'isLoaded')");
92
        }
93
94
        return $primaryKey;
95
    }
96
97
    /**
98
     * {@inheritdoc}
99
     *
100
     * @throws RelationException
101
     */
102
    public function getField(string $name, $default = null, bool $filter = true)
103
    {
104
        if ($this->relations->has($name)) {
105
            return $this->relations->getRelated($name);
106
        }
107
108
        $this->assertField($name);
109
110
        return parent::getField($name, $default, $filter);
111
    }
112
113
    /**
114
     * {@inheritdoc}
115
     *
116
     * @param bool $registerChanges Track field changes.
117
     *
118
     * @throws RelationException
119
     */
120
    public function setField(
121
        string $name,
122
        $value,
123
        bool $filter = true,
124
        bool $registerChanges = true
125
    ) {
126
        if ($this->relations->has($name)) {
127
            //Would not work with relations which do not represent singular entities
128
            $this->relations->setRelated($name, $value);
129
130
            return;
131
        }
132
133
        $this->assertField($name);
134
        if ($registerChanges) {
135
            $this->registerChange($name);
136
        }
137
138
        parent::setField($name, $value, $filter);
139
    }
140
141
    /**
142
     * {@inheritdoc}
143
     */
144
    public function hasField(string $name): bool
145
    {
146
        if ($this->relations->has($name)) {
147
            return $this->relations->hasRelated($name);
148
        }
149
150
        return parent::hasField($name);
151
    }
152
153
    /**
154
     * {@inheritdoc}
155
     *
156
     * @throws FieldException
157
     * @throws RelationException
158
     */
159
    public function __unset($offset)
160
    {
161
        if ($this->relations->has($offset)) {
162
            //Flush associated relation value if possible
163
            $this->relations->flushRelated($offset);
164
165
            return;
166
        }
167
168
        if (!$this->isNullable($offset)) {
169
            throw new FieldException("Unable to unset not nullable field '{$offset}'");
170
        }
171
172
        $this->setField($offset, null, false);
173
    }
174
175
    /**
176
     * {@inheritdoc}
177
     *
178
     * Method does not check updates in nested relation, but only in primary record.
179
     *
180
     * @param string $field Check once specific field changes.
181
     */
182
    public function hasChanges(string $field = null): bool
183
    {
184
        //Check updates for specific field
185
        if (!empty($field)) {
186
            if (array_key_exists($field, $this->changes)) {
187
                return true;
188
            }
189
190
            //Do not force accessor creation
191
            $value = $this->getField($field, null, false);
192
            if ($value instanceof RecordAccessorInterface && $value->hasUpdates()) {
193
                return true;
194
            }
195
196
            return false;
197
        }
198
199
        if (!empty($this->changes)) {
200
            return true;
201
        }
202
203
        //Do not force accessor creation
204
        foreach ($this->getFields(false) as $value) {
205
            //Checking all fields for changes (handled internally)
206
            if ($value instanceof RecordAccessorInterface && $value->hasUpdates()) {
207
                return true;
208
            }
209
        }
210
211
        return false;
212
    }
213
214
    /**
215
     * @return array
216
     */
217
    public function __debugInfo()
218
    {
219
        return [
220
            'database'  => $this->orm->define(static::class, ORMInterface::R_DATABASE),
221
            'table'     => $this->orm->define(static::class, ORMInterface::R_TABLE),
222
            'fields'    => $this->getFields(),
223
            'relations' => $this->relations
224
        ];
225
    }
226
227
    /**
228
     * {@inheritdoc}
229
     *
230
     * DocumentEntity will pass ODM instance as part of accessor context.
231
     *
232
     * @see CompositionDefinition
233
     */
234
    protected function createAccessor(
235
        $accessor,
236
        string $name,
237
        $value,
238
        array $context = []
239
    ): AccessorInterface {
240
        //Giving ORM as context
241
        return parent::createAccessor($accessor, $name, $value, $context + ['orm' => $this->orm]);
242
    }
243
244
    /**
245
     * {@inheritdoc}
246
     */
247
    protected function iocContainer()
248
    {
249
        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...
250
            //Forwarding IoC scope to parent ORM instance
251
            return $this->orm->iocContainer();
252
        }
253
254
        return parent::iocContainer();
255
    }
256
257
    /**
258
     * Name of column used as primary key.
259
     *
260
     * @return string
261
     */
262
    protected function primaryColumn(): string
263
    {
264
        return $this->recordSchema[self::SH_PRIMARY_KEY];
265
    }
266
267
    /**
268
     * Create set of fields to be sent to UPDATE statement.
269
     *
270
     * @param bool $skipPrimary Skip primary key
271
     *
272
     * @return array
273
     */
274
    protected function packChanges(bool $skipPrimary = false): array
275
    {
276
        if (!$this->hasChanges() && !$this->isSolid()) {
277
            return [];
278
        }
279
280
        if ($this->isSolid()) {
281
            //Solid records always saved as one chunk of data
282
            return $this->packValue();
283
        }
284
285
        $updates = [];
286
        foreach ($this->getFields(false) as $field => $value) {
287
            //Handled by sub-accessor
288
            if ($value instanceof RecordAccessorInterface) {
289
                if ($value->hasUpdates()) {
290
                    $updates[$field] = $value->compileUpdates($field);
291
                    continue;
292
                }
293
294
                $value = $value->packValue();
295
            }
296
297
            //Field change registered
298
            if (array_key_exists($field, $this->changes)) {
299
                $updates[$field] = $value;
300
            }
301
        }
302
303
        if ($skipPrimary) {
304
            unset($updates[$this->recordSchema[self::SH_PRIMARY_KEY]]);
305
        }
306
307
        return $updates;
308
    }
309
310
    /**
311
     * Indicate that all updates done, reset dirty state.
312
     */
313
    protected function flushChanges()
314
    {
315
        $this->changes = [];
316
317
        foreach ($this->getFields(false) as $field => $value) {
318
            if ($value instanceof RecordAccessorInterface) {
319
                $value->flushUpdates();
320
            }
321
        }
322
    }
323
324
    /**
325
     * @param string $name
326
     */
327
    private function registerChange(string $name)
328
    {
329
        $original = $this->getField($name, null, false);
330
331
        if (!array_key_exists($name, $this->changes)) {
332
            //Let's keep track of how field looked before first change
333
            $this->changes[$name] = $original instanceof AccessorInterface
334
                ? $original->packValue()
335
                : $original;
336
        }
337
    }
338
339
    /**
340
     * @param string $name
341
     *
342
     * @throws FieldException
343
     */
344
    private function assertField(string $name)
345
    {
346
        if (!$this->hasField($name)) {
347
            throw new FieldException(sprintf(
348
                "No such property '%s' in '%s', check schema being relevant",
349
                $name,
350
                get_called_class()
351
            ));
352
        }
353
    }
354
}