Completed
Branch feature/pre-split (4ff102)
by Anton
03:27
created

RecordEntity::getState()   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\Traits\SaturateTrait;
10
use Spiral\Models\Traits\SolidableTrait;
11
use Spiral\ORM\Commands\DeleteCommand;
12
use Spiral\ORM\Commands\InsertCommand;
13
use Spiral\ORM\Commands\NullCommand;
14
use Spiral\ORM\Commands\UpdateCommand;
15
use Spiral\ORM\Entities\RelationMap;
16
use Spiral\ORM\Events\RecordEvent;
17
use Spiral\ORM\Exceptions\RecordException;
18
use Spiral\ORM\Exceptions\RelationException;
19
20
/**
21
 * Provides ActiveRecord-less abstraction for carried data with ability to automatically apply
22
 * setters, getters, generate update, insert and delete sequences and access nested relations.
23
 *
24
 * Class implementations statically analyzed to define DB schema.
25
 *
26
 * @see RecordEntity::SCHEMA
27
 */
28
abstract class RecordEntity extends AbstractRecord implements RecordInterface
29
{
30
    use SaturateTrait, SolidableTrait;
31
32
    /*
33
     * Begin set of behaviour and description constants.
34
     * ================================================
35
     */
36
37
    /**
38
     * Default ORM relation types, see ORM configuration and documentation for more information.
39
     *
40
     * @see RelationSchemaInterface
41
     * @see RelationSchema
42
     */
43
    const HAS_ONE      = 101;
44
    const HAS_MANY     = 102;
45
    const BELONGS_TO   = 103;
46
    const MANY_TO_MANY = 104;
47
48
    /**
49
     * Morphed relation types are usually created by inversion or equivalent of primary relation
50
     * types.
51
     *
52
     * @see RelationSchemaInterface
53
     * @see RelationSchema
54
     * @see MorphedRelation
55
     */
56
    const BELONGS_TO_MORPHED = 108;
57
    const MANY_TO_MORPHED    = 109;
58
59
    /**
60
     * Constants used to declare relations in record schema, used in normalized relation schema.
61
     *
62
     * @see RelationSchemaInterface
63
     */
64
    const OUTER_KEY         = 901; //Outer key name
65
    const INNER_KEY         = 902; //Inner key name
66
    const MORPH_KEY         = 903; //Morph key name
67
    const PIVOT_TABLE       = 904; //Pivot table name
68
    const PIVOT_COLUMNS     = 905; //Pre-defined pivot table columns
69
    const PIVOT_DEFAULTS    = 906; //Pre-defined pivot table default values
70
    const THOUGHT_INNER_KEY = 907; //Pivot table options
71
    const THOUGHT_OUTER_KEY = 908; //Pivot table options
72
    const WHERE             = 909; //Where conditions
73
    const WHERE_PIVOT       = 910; //Where pivot conditions
74
75
    /**
76
     * Additional constants used to control relation schema behaviour.
77
     *
78
     * @see RecordEntity::SCHEMA
79
     * @see RelationSchemaInterface
80
     */
81
    const INVERSE           = 1001; //Relation should be inverted to parent record
82
    const CREATE_CONSTRAINT = 1002; //Relation should create foreign keys (default)
83
    const CONSTRAINT_ACTION = 1003; //Default relation foreign key delete/update action (CASCADE)
84
    const CREATE_PIVOT      = 1004; //Many-to-Many should create pivot table automatically (default)
85
    const NULLABLE          = 1005; //Relation can be nullable (default)
86
    const CREATE_INDEXES    = 1006; //Indication that relation is allowed to create required indexes
87
    const MORPHED_ALIASES   = 1007; //Aliases for morphed sub-relations
88
89
    /**
90
     * Set of columns to be used in relation (attention, make sure that loaded records are set as
91
     * NON SOLID if you planning to modify their data).
92
     */
93
    const RELATION_COLUMNS = 1009;
94
95
    /**
96
     * Constants used to declare indexes in record schema.
97
     *
98
     * @see Record::INDEXES
99
     */
100
    const INDEX  = 1000;            //Default index type
101
    const UNIQUE = 2000;            //Unique index definition
102
103
    /*
104
     * ================================================
105
     * End set of behaviour and description constants.
106
     */
107
108
    /**
109
     * Model behaviour configurations.
110
     */
111
    const SECURED   = '*';
112
    const HIDDEN    = [];
113
    const FILLABLE  = [];
114
    const SETTERS   = [];
115
    const GETTERS   = [];
116
    const ACCESSORS = [];
117
118
    /**
119
     * Record relations and columns can be described in one place - record schema.
120
     * Attention: while defining table structure make sure that ACTIVE_SCHEMA constant is set to t
121
     * rue.
122
     *
123
     * Example:
124
     * const SCHEMA = [
125
     *      'id'        => 'primary',
126
     *      'name'      => 'string',
127
     *      'biography' => 'text'
128
     * ];
129
     *
130
     * You can pass additional options for some of your columns:
131
     * const SCHEMA = [
132
     *      'pinCode' => 'string(128)',         //String length
133
     *      'status'  => 'enum(active, hidden)', //Enum values
134
     *      'balance' => 'decimal(10, 2)'       //Decimal size and precision
135
     * ];
136
     *
137
     * Every created column will be stated as NOT NULL with forced default value, if you want to
138
     * have nullable columns, specify special data key: protected $schema = [
139
     *      'name'      => 'string, nullable'
140
     * ];
141
     *
142
     * You can easily combine table and relations definition in one schema:
143
     * const SCHEMA = [
144
     *      'id'          => 'bigPrimary',
145
     *      'name'        => 'string',
146
     *      'email'       => 'string',
147
     *      'phoneNumber' => 'string(32)',
148
     *
149
     *      //Relations
150
     *      'profile'     => [
151
     *          self::HAS_ONE => 'Records\Profile',
152
     *          self::INVERSE => 'user'
153
     *      ],
154
     *      'roles'       => [
155
     *          self::MANY_TO_MANY => 'Records\Role',
156
     *          self::INVERSE => 'users'
157
     *      ]
158
     * ];
159
     *
160
     * @var array
161
     */
162
    const SCHEMA = [];
163
164
    /**
165
     * Default field values.
166
     *
167
     * @var array
168
     */
169
    const DEFAULTS = [];
170
171
    /**
172
     * Set of indexes to be created for associated record table, indexes only created when record is
173
     * not abstract and has active schema set to true.
174
     *
175
     * Use constants INDEX and UNIQUE to describe indexes, you can also create compound indexes:
176
     * const INDEXES = [
177
     *      [self::UNIQUE, 'email'],
178
     *      [self::INDEX, 'board_id'],
179
     *      [self::INDEX, 'board_id', 'check_id']
180
     * ];
181
     *
182
     * @var array
183
     */
184
    const INDEXES = [];
185
186
    /**
187
     * Record state.
188
     *
189
     * @var int
190
     */
191
    private $state;
192
193
    /**
194
     * Points to last queued insert command for this entity, required to properly handle multiple
195
     * entity updates inside one transaction.
196
     *
197
     * @var InsertCommand
198
     */
199
    private $firstInsert = null;
200
201
    /**
202
     * Initiate entity inside or outside of ORM scope using given fields and state.
203
     *
204
     * @param array             $data
205
     * @param int               $state
206
     * @param ORMInterface|null $orm
207
     */
208
    public function __construct(
209
        array $data = [],
210
        int $state = ORMInterface::STATE_NEW,
211
        ORMInterface $orm = null
212
    ) {
213
        //We can use global container as fallback if no default values were provided
214
        $orm = $this->saturate($orm, ORMInterface::class);
215
216
        $this->state = $state;
217
218
        //Non loaded records should be in solid state by default
219
        $this->solidState($this->state == ORMInterface::STATE_NEW);
220
221
        parent::__construct($orm, $data, new RelationMap($this, $orm));
222
    }
223
224
    /**
225
     * Check if entity been loaded (non new).
226
     *
227
     * @return bool
228
     */
229
    public function isLoaded(): bool
230
    {
231
        return $this->getState() != ORMInterface::STATE_NEW
232
            && $this->getState() != ORMInterface::STATE_DELETED
233
            && $this->getState() != ORMInterface::STATE_SCHEDULED_DELETE;
234
    }
235
236
    /**
237
     * Current model state.
238
     *
239
     * @return int
240
     */
241
    public function getState(): int
242
    {
243
        return $this->state;
244
    }
245
246
    /**
247
     * {@inheritdoc}
248
     *
249
     * @param bool $queueRelations
250
     *
251
     * @throws RecordException
252
     * @throws RelationException
253
     */
254
    public function queueStore(bool $queueRelations = true): ContextualCommandInterface
255
    {
256
        if (!$this->isLoaded()) {
257
            $command = $this->prepareInsert();
258
        } else {
259
            $command = $this->prepareUpdate();
260
        }
261
262
        //Reset all tracked entity changes
263
        $this->flushChanges();
264
265
        //Relation commands
266
        if ($queueRelations) {
267
            //Queue relations before and after parent command (if needed)
268
            return $this->relations->queueRelations($command);
269
        }
270
271
        return $command;
272
    }
273
274
    /**
275
     * {@inheritdoc}
276
     *
277
     * @throws RecordException
278
     * @throws RelationException
279
     */
280
    public function queueDelete(): CommandInterface
281
    {
282
        if (!$this->isLoaded()) {
283
            //Nothing to do
284
            return new NullCommand();
285
        }
286
287
        return $this->prepareDelete();
288
    }
289
290
    /**
291
     * @return InsertCommand
292
     */
293
    private function prepareInsert(): InsertCommand
294
    {
295
        $data = $this->packValue();
296
        unset($data[$this->primaryColumn()]);
297
298
        $command = new InsertCommand($this->orm->table(static::class), $data);
299
300
        //Entity indicates it's own status
301
        $this->state = ORMInterface::STATE_SCHEDULED_INSERT;
302
        $this->dispatch('insert', new RecordEvent($this, $command));
0 ignored issues
show
Unused Code introduced by
The call to RecordEvent::__construct() has too many arguments starting with $command.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
303
304
        //Executed when transaction successfully completed
305
        $command->onComplete(function (InsertCommand $command) {
306
            $this->handleInsert($command);
307
        });
308
309
        $command->onRollBack(function () {
310
            //Flushing existed insert command to prevent collisions
311
            $this->firstInsert = null;
312
        });
313
314
        //Keep reference to the last insert command
315
        return $this->firstInsert = $command;
316
    }
317
318
    /**
319
     * @return UpdateCommand
320
     */
321
    private function prepareUpdate(): UpdateCommand
322
    {
323
        $command = new UpdateCommand(
324
            $this->orm->table(static::class),
325
            [$this->primaryColumn() => $this->primaryKey()],
326
            $this->packChanges(true)
327
        );
328
329
        if (!empty($this->firstInsert)) {
330
            $this->firstInsert->onExecute(function (InsertCommand $insert) use ($command) {
331
                //Sync primary key values
332
                $command->setWhere([$this->primaryColumn() => $insert->getInsertID()]);
333
            });
334
        }
335
336
        //Entity indicates it's own status
337
        $this->state = ORMInterface::STATE_SCHEDULED_UPDATE;
338
        $this->dispatch('update', new RecordEvent($this));
339
340
        //Executed when transaction successfully completed
341
        $command->onComplete(function (UpdateCommand $command) {
342
            $this->handleUpdate($command);
343
        });
344
345
        return $command;
346
    }
347
348
    /**
349
     * @return DeleteCommand
350
     */
351
    private function prepareDelete(): DeleteCommand
352
    {
353
        $command = new DeleteCommand(
354
            $this->orm->table(static::class),
355
            [$this->primaryColumn() => $this->primaryKey()]
356
        );
357
358
        if (!empty($this->firstInsert)) {
359
            $this->firstInsert->onExecute(function (InsertCommand $insert) use ($command) {
360
                //Sync primary key values
361
                $command->setWhere([$this->primaryColumn() => $insert->getInsertID()]);
362
            });
363
        }
364
365
        //Entity indicates it's own status
366
        $this->state = ORMInterface::STATE_SCHEDULED_DELETE;
367
        $this->dispatch('delete', new RecordEvent($this));
368
369
        //Executed when transaction successfully completed
370
        $command->onComplete(function (DeleteCommand $command) {
371
            $this->handleDelete($command);
372
        });
373
374
        return $command;
375
    }
376
377
    /**
378
     * Handle result of insert command.
379
     *
380
     * @param InsertCommand $command
381
     */
382
    private function handleInsert(InsertCommand $command)
383
    {
384
        //Flushing reference to last insert command
385
        $this->firstInsert = null;
386
387
        //Mounting PK
388
        $this->setField($this->primaryColumn(), $command->getInsertID(), true, false);
389
390
        //Once command executed we will know some information about it's context (for exampled added FKs)
391
        foreach ($command->getContext() as $name => $value) {
392
            $this->setField($name, $value, true, false);
393
        }
394
395
        $this->state = ORMInterface::STATE_LOADED;
396
        $this->dispatch('created', new RecordEvent($this));
397
    }
398
399
    /**
400
     * Handle result of update command.
401
     *
402
     * @param UpdateCommand $command
403
     */
404
    private function handleUpdate(UpdateCommand $command)
405
    {
406
        //Once command executed we will know some information about it's context (for exampled added FKs)
407
        foreach ($command->getContext() as $name => $value) {
408
            $this->setField($name, $value, true, false);
409
        }
410
411
        $this->state = ORMInterface::STATE_LOADED;
412
        $this->dispatch('updated', new RecordEvent($this));
413
    }
414
415
    /**
416
     * Handle result of delete command.
417
     *
418
     * @param DeleteCommand $command
419
     */
420
    private function handleDelete(DeleteCommand $command)
421
    {
422
        $this->state = ORMInterface::STATE_DELETED;
423
        $this->dispatch('deleted', new RecordEvent($this));
424
    }
425
}
426