Completed
Branch feature/pre-split (0a985a)
by Anton
05:37
created

AbstractRecord::__toString()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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