Completed
Branch feature/pre-split (d4e072)
by Anton
04:00
created

RecordEntity::__unset()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 1
dl 0
loc 8
rs 9.4285
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\Core\Traits\SaturateTrait;
11
use Spiral\Models\AccessorInterface;
12
use Spiral\Models\SchematicEntity;
13
use Spiral\Models\Traits\SolidableTrait;
14
use Spiral\ORM\Exceptions\FieldException;
15
16
/**
17
 * Provides ActiveRecord-less abstraction for carried data with ability to automatically apply
18
 * setters, getters, generate update, insert and delete sequences and access nested relations.
19
 *
20
 * Class implementations statically analyzed to define DB schema.
21
 *
22
 * @see RecordEntity::SCHEMA
23
 */
24
abstract class RecordEntity extends SchematicEntity
25
{
26
    use SaturateTrait, SolidableTrait;
27
28
    /*
29
     * Begin set of behaviour and description constants.
30
     * ================================================
31
     */
32
33
    /**
34
     * Set of schema sections needed to describe entity behaviour.
35
     */
36
    const SH_PRIMARIES = 0;
37
    const SH_DEFAULTS  = 1;
38
    const SH_RELATIONS = 6;
39
40
    /**
41
     * Default ORM relation types, see ORM configuration and documentation for more information.
42
     *
43
     * @see RelationSchemaInterface
44
     * @see RelationSchema
45
     */
46
    const HAS_ONE      = 101;
47
    const HAS_MANY     = 102;
48
    const BELONGS_TO   = 103;
49
    const MANY_TO_MANY = 104;
50
51
    /**
52
     * Morphed relation types are usually created by inversion or equivalent of primary relation
53
     * types.
54
     *
55
     * @see RelationSchemaInterface
56
     * @see RelationSchema
57
     * @see MorphedRelation
58
     */
59
    const BELONGS_TO_MORPHED = 108;
60
    const MANY_TO_MORPHED    = 109;
61
62
    /**
63
     * Constants used to declare relations in record schema, used in normalized relation schema.
64
     *
65
     * @see RelationSchemaInterface
66
     */
67
    const OUTER_KEY         = 901; //Outer key name
68
    const INNER_KEY         = 902; //Inner key name
69
    const MORPH_KEY         = 903; //Morph key name
70
    const PIVOT_TABLE       = 904; //Pivot table name
71
    const PIVOT_COLUMNS     = 905; //Pre-defined pivot table columns
72
    const PIVOT_DEFAULTS    = 906; //Pre-defined pivot table default values
73
    const THOUGHT_INNER_KEY = 907; //Pivot table options
74
    const THOUGHT_OUTER_KEY = 908; //Pivot table options
75
    const WHERE             = 909; //Where conditions
76
    const WHERE_PIVOT       = 910; //Where pivot conditions
77
78
    /**
79
     * Additional constants used to control relation schema behaviour.
80
     *
81
     * @see RecordEntity::SCHEMA
82
     * @see RelationSchemaInterface
83
     */
84
    const INVERSE           = 1001; //Relation should be inverted to parent record
85
    const CREATE_CONSTRAINT = 1002; //Relation should create foreign keys (default)
86
    const CONSTRAINT_ACTION = 1003; //Default relation foreign key delete/update action (CASCADE)
87
    const CREATE_PIVOT      = 1004; //Many-to-Many should create pivot table automatically (default)
88
    const NULLABLE          = 1005; //Relation can be nullable (default)
89
    const CREATE_INDEXES    = 1006; //Indication that relation is allowed to create required indexes
90
    const MORPHED_ALIASES   = 1007; //Aliases for morphed sub-relations
91
92
    /**
93
     * Set of columns to be used in relation (attention, make sure that loaded records are set as
94
     * NON SOLID if you planning to modify their data).
95
     */
96
    const RELATION_COLUMNS = 1009;
97
98
    /**
99
     * Constants used to declare indexes in record schema.
100
     *
101
     * @see Record::INDEXES
102
     */
103
    const INDEX  = 1000;            //Default index type
104
    const UNIQUE = 2000;            //Unique index definition
105
106
    /*
107
     * ================================================
108
     * End set of behaviour and description constants.
109
     */
110
111
    /**
112
     * Model behaviour configurations.
113
     */
114
    const SECURED   = '*';
115
    const HIDDEN    = [];
116
    const FILLABLE  = [];
117
    const SETTERS   = [];
118
    const GETTERS   = [];
119
    const ACCESSORS = [];
120
121
    /**
122
     * Record relations and columns can be described in one place - record schema.
123
     * Attention: while defining table structure make sure that ACTIVE_SCHEMA constant is set to t
124
     * rue.
125
     *
126
     * Example:
127
     * const SCHEMA = [
128
     *      'id'        => 'primary',
129
     *      'name'      => 'string',
130
     *      'biography' => 'text'
131
     * ];
132
     *
133
     * You can pass additional options for some of your columns:
134
     * const SCHEMA = [
135
     *      'pinCode' => 'string(128)',         //String length
136
     *      'status'  => 'enum(active, hidden)', //Enum values
137
     *      'balance' => 'decimal(10, 2)'       //Decimal size and precision
138
     * ];
139
     *
140
     * Every created column will be stated as NOT NULL with forced default value, if you want to
141
     * have nullable columns, specify special data key: protected $schema = [
142
     *      'name'      => 'string, nullable'
143
     * ];
144
     *
145
     * You can easily combine table and relations definition in one schema:
146
     * const SCHEMA = [
147
     *      'id'          => 'bigPrimary',
148
     *      'name'        => 'string',
149
     *      'email'       => 'string',
150
     *      'phoneNumber' => 'string(32)',
151
     *
152
     *      //Relations
153
     *      'profile'     => [
154
     *          self::HAS_ONE => 'Records\Profile',
155
     *          self::INVERSE => 'user'
156
     *      ],
157
     *      'roles'       => [
158
     *          self::MANY_TO_MANY => 'Records\Role',
159
     *          self::INVERSE => 'users'
160
     *      ]
161
     * ];
162
     *
163
     * @var array
164
     */
165
    const SCHEMA = [];
166
167
    /**
168
     * Default field values.
169
     *
170
     * @var array
171
     */
172
    const DEFAULTS = [];
173
174
    /**
175
     * Set of indexes to be created for associated record table, indexes only created when record is
176
     * not abstract and has active schema set to true.
177
     *
178
     * Use constants INDEX and UNIQUE to describe indexes, you can also create compound indexes:
179
     * const INDEXES = [
180
     *      [self::UNIQUE, 'email'],
181
     *      [self::INDEX, 'board_id'],
182
     *      [self::INDEX, 'board_id', 'check_id']
183
     * ];
184
     *
185
     * @var array
186
     */
187
    const INDEXES = [];
188
189
    /**
190
     * Record behaviour definition.
191
     *
192
     * @var array
193
     */
194
    private $recordSchema = [];
195
196
    /**
197
     * Record state.
198
     *
199
     * @var int
200
     */
201
    private $state;
202
203
    /**
204
     * Record field updates (changed values).
205
     *
206
     * @var array
207
     */
208
    private $changes = [];
209
210
    /**
211
     * Associated relation instances and/or initial loaded data.
212
     *
213
     * @var array
214
     */
215
    private $relations = [];
216
217
    /**
218
     * Parent ORM instance, responsible for relation initialization and lazy loading operations.
219
     *
220
     * @invisible
221
     * @var ORMInterface
222
     */
223
    protected $orm;
224
225
    /**
226
     * Initiate entity inside or outside of ORM scope using given fields and state.
227
     *
228
     * @param array             $fields
229
     * @param int               $state
230
     * @param ORMInterface|null $orm
231
     * @param array|null        $schema
232
     */
233
    public function __construct(
234
        array $fields = [],
235
        int $state = ORMInterface::STATE_NEW,
236
        ORMInterface $orm = null,
237
        array $schema = null
238
    ) {//We can use global container as fallback if no default values were provided
239
        $this->orm = $this->saturate($orm, ORMInterface::class);
240
241
        //Use supplied schema or fetch one from ORM
242
        $this->recordSchema = !empty($schema) ? $schema : $this->orm->define(
243
            static::class,
244
            ORMInterface::R_SCHEMA
245
        );
246
247
        $this->state = $state;
248
        if ($this->state == ORMInterface::STATE_NEW) {
249
            //Non loaded records should be in solid state by default
250
            $this->solidState(true);
251
        }
252
253
        $this->extractRelations($fields);
254
        parent::__construct($fields + $this->recordSchema[self::SH_DEFAULTS], $schema);
255
    }
256
257
    //todo: think about it
258
    public function getState(): int
259
    {
260
        return $this->state;
261
    }
262
263
    //todo: think about it
264
    public function setState(int $state): self
265
    {
266
        $this->state = $state;
267
268
        return $this;
269
    }
270
271
    /**
272
     * {@inheritdoc}
273
     */
274
    public function getField(string $name, $default = null, bool $filter = true)
275
    {
276
        if (!$this->hasField($name) && !isset($this->recordSchema[self::SH_RELATIONS][$name])) {
277
            throw new FieldException(sprintf(
278
                "No such property '%s' in '%s', check schema being relevant",
279
                $name,
280
                get_called_class()
281
            ));
282
        }
283
284
        //todo: get relation
285
286
        return parent::getField($name, $default, $filter);
287
    }
288
289
    /**
290
     * {@inheritdoc}
291
     *
292
     * Tracks field changes.
293
     */
294
    public function setField(string $name, $value, bool $filter = true)
295
    {
296
        //todo: check if relation
297
298
        if (!$this->hasField($name)) {
299
            //We are only allowing to modify existed fields, this is strict schema
300
            throw new FieldException(sprintf(
301
                "No such property '%s' in '%s', check schema being relevant",
302
                $name,
303
                get_called_class()
304
            ));
305
        }
306
307
        $this->registerChange($name);
308
309
        parent::setField($name, $value, $filter);
310
    }
311
312
    /**
313
     * {@inheritdoc}
314
     */
315
    public function __isset($name)
316
    {
317
        //todo: if relation
318
319
        return parent::__isset($name);
320
    }
321
322
    /**
323
     * {@inheritdoc}
324
     *
325
     * @throws FieldException
326
     */
327
    public function __unset($offset)
328
    {
329
        if (!$this->isNullable($offset)) {
330
            throw new FieldException("Unable to unset not nullable field '{$offset}'");
331
        }
332
333
        $this->setField($offset, null, false);
334
    }
335
336
    /**
337
     * {@inheritdoc}
338
     *
339
     * Method does not check updates in nested relation, but only in primary record.
340
     *
341
     * @param string $field Check once specific field changes.
342
     */
343
    public function hasUpdates(string $field = null): bool
344
    {
345
        //Check updates for specific field
346
        if (!empty($field)) {
347
            if (array_key_exists($field, $this->changes)) {
348
                return true;
349
            }
350
351
            //Do not force accessor creation
352
            $value = $this->getField($field, null, false);
353
            if ($value instanceof SQLAccessorInterface && $value->hasUpdates()) {
354
                return true;
355
            }
356
357
            return false;
358
        }
359
360
        if (!empty($this->changes)) {
361
            return true;
362
        }
363
364
        //Do not force accessor creation
365
        foreach ($this->getFields(false) as $value) {
366
            //Checking all fields for changes (handled internally)
367
            if ($value instanceof SQLAccessorInterface && $value->hasUpdates()) {
368
                return true;
369
            }
370
        }
371
372
        return false;
373
    }
374
375
    /**
376
     * @return array
377
     */
378
    public function __debugInfo()
379
    {
380
        return [
381
            'database'  => $this->orm->define(static::class, ORMInterface::R_DATABASE),
382
            'table'     => $this->orm->define(static::class, ORMInterface::R_TABLE),
383
            'fields'    => $this->getFields(),
384
            'relations' => $this->relations
385
        ];
386
    }
387
388
    /**
389
     * {@inheritdoc}
390
     *
391
     * DocumentEntity will pass ODM instance as part of accessor context.
392
     *
393
     * @see CompositionDefinition
394
     */
395
    protected function createAccessor(
396
        $accessor,
397
        string $name,
398
        $value,
399
        array $context = []
400
    ): AccessorInterface {
401
        //Giving ORM as context
402
        return parent::createAccessor($accessor, $name, $value, $context + ['orm' => $this->orm]);
403
    }
404
405
    /**
406
     * {@inheritdoc}
407
     */
408
    protected function iocContainer()
409
    {
410
        if ($this->orm instanceof Component) {
411
            //Forwarding IoC scope to parent ORM instance
412
            return $this->orm->iocContainer();
413
        }
414
415
        return parent::iocContainer();
416
    }
417
418
    /**
419
     * @param string $name
420
     */
421
    private function registerChange(string $name)
422
    {
423
        $original = $this->getField($name, null, false);
424
425
        if (!array_key_exists($name, $this->changes)) {
426
            //Let's keep track of how field looked before first change
427
            $this->changes[$name] = $original instanceof AccessorInterface
428
                ? $original->packValue()
429
                : $original;
430
        }
431
    }
432
433
    /**
434
     * Extract relations data from given entity fields.
435
     *
436
     * @param array $data
437
     */
438
    private function extractRelations(array &$data)
439
    {
440
        //Fetch all relations
441
        $relations = array_intersect_key($data, $this->recordSchema[self::SH_RELATIONS]);
442
443
        foreach ($relations as $name => $relation) {
444
            $this->relations[$name] = $relation;
445
            unset($data[$name]);
446
        }
447
    }
448
}