Completed
Branch feature/pre-split (540d96)
by Anton
03:41
created

RecordEntity::syncState()   B

Complexity

Conditions 2
Paths 1

Size

Total Lines 25
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 13
nc 1
nop 0
dl 0
loc 25
rs 8.8571
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\RelationBucket;
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
 * Potentially requires split for StateWatcher.
29
 */
30
abstract class RecordEntity extends AbstractRecord implements RecordInterface
31
{
32
    use SaturateTrait, SolidableTrait;
33
34
    /*
35
     * Begin set of behaviour and description constants.
36
     * ================================================
37
     */
38
39
    /**
40
     * Default ORM relation types, see ORM configuration and documentation for more information.
41
     *
42
     * @see RelationSchemaInterface
43
     * @see RelationSchema
44
     */
45
    const HAS_ONE      = 101;
46
    const HAS_MANY     = 102;
47
    const BELONGS_TO   = 103;
48
    const MANY_TO_MANY = 104;
49
50
    /**
51
     * Morphed relation types are usually created by inversion or equivalent of primary relation
52
     * types.
53
     *
54
     * @see RelationSchemaInterface
55
     * @see RelationSchema
56
     * @see MorphedRelation
57
     */
58
    const BELONGS_TO_MORPHED = 108;
59
    const MANY_TO_MORPHED    = 109;
60
61
    /**
62
     * Constants used to declare relations in record schema, used in normalized relation schema.
63
     *
64
     * @see RelationSchemaInterface
65
     */
66
    const OUTER_KEY         = 901; //Outer key name
67
    const INNER_KEY         = 902; //Inner key name
68
    const MORPH_KEY         = 903; //Morph key name
69
    const PIVOT_TABLE       = 904; //Pivot table name
70
    const PIVOT_COLUMNS     = 905; //Pre-defined pivot table columns
71
    const PIVOT_DEFAULTS    = 906; //Pre-defined pivot table default values
72
    const THOUGHT_INNER_KEY = 907; //Pivot table options
73
    const THOUGHT_OUTER_KEY = 908; //Pivot table options
74
    const WHERE             = 909; //Where conditions
75
    const WHERE_PIVOT       = 910; //Where pivot conditions
76
77
    /**
78
     * Additional constants used to control relation schema behaviour.
79
     *
80
     * @see RecordEntity::SCHEMA
81
     * @see RelationSchemaInterface
82
     */
83
    const INVERSE           = 1001; //Relation should be inverted to parent record
84
    const CREATE_CONSTRAINT = 1002; //Relation should create foreign keys (default)
85
    const CONSTRAINT_ACTION = 1003; //Default relation foreign key delete/update action (CASCADE)
86
    const CREATE_PIVOT      = 1004; //Many-to-Many should create pivot table automatically (default)
87
    const NULLABLE          = 1005; //Relation can be nullable (default)
88
    const CREATE_INDEXES    = 1006; //Indication that relation is allowed to create required indexes
89
    const MORPHED_ALIASES   = 1007; //Aliases for morphed sub-relations
90
91
    /**
92
     * Set of columns to be used in relation (attention, make sure that loaded records are set as
93
     * NON SOLID if you planning to modify their data).
94
     */
95
    const RELATION_COLUMNS = 1009;
96
97
    /**
98
     * Constants used to declare indexes in record schema.
99
     *
100
     * @see Record::INDEXES
101
     */
102
    const INDEX  = 1000;            //Default index type
103
    const UNIQUE = 2000;            //Unique index definition
104
105
    /*
106
     * ================================================
107
     * End set of behaviour and description constants.
108
     */
109
110
    /**
111
     * Model behaviour configurations.
112
     */
113
    const SECURED   = '*';
114
    const HIDDEN    = [];
115
    const FILLABLE  = [];
116
    const SETTERS   = [];
117
    const GETTERS   = [];
118
    const ACCESSORS = [];
119
120
    /**
121
     * Record relations and columns can be described in one place - record schema.
122
     * Attention: while defining table structure make sure that ACTIVE_SCHEMA constant is set to t
123
     * rue.
124
     *
125
     * Example:
126
     * const SCHEMA = [
127
     *      'id'        => 'primary',
128
     *      'name'      => 'string',
129
     *      'biography' => 'text'
130
     * ];
131
     *
132
     * You can pass additional options for some of your columns:
133
     * const SCHEMA = [
134
     *      'pinCode' => 'string(128)',         //String length
135
     *      'status'  => 'enum(active, hidden)', //Enum values
136
     *      'balance' => 'decimal(10, 2)'       //Decimal size and precision
137
     * ];
138
     *
139
     * Every created column will be stated as NOT NULL with forced default value, if you want to
140
     * have nullable columns, specify special data key: protected $schema = [
141
     *      'name'      => 'string, nullable'
142
     * ];
143
     *
144
     * You can easily combine table and relations definition in one schema:
145
     * const SCHEMA = [
146
     *      'id'          => 'bigPrimary',
147
     *      'name'        => 'string',
148
     *      'email'       => 'string',
149
     *      'phoneNumber' => 'string(32)',
150
     *
151
     *      //Relations
152
     *      'profile'     => [
153
     *          self::HAS_ONE => 'Records\Profile',
154
     *          self::INVERSE => 'user'
155
     *      ],
156
     *      'roles'       => [
157
     *          self::MANY_TO_MANY => 'Records\Role',
158
     *          self::INVERSE => 'users'
159
     *      ]
160
     * ];
161
     *
162
     * @var array
163
     */
164
    const SCHEMA = [];
165
166
    /**
167
     * Default field values.
168
     *
169
     * @var array
170
     */
171
    const DEFAULTS = [];
172
173
    /**
174
     * Set of indexes to be created for associated record table, indexes only created when record is
175
     * not abstract and has active schema set to true.
176
     *
177
     * Use constants INDEX and UNIQUE to describe indexes, you can also create compound indexes:
178
     * const INDEXES = [
179
     *      [self::UNIQUE, 'email'],
180
     *      [self::INDEX, 'board_id'],
181
     *      [self::INDEX, 'board_id', 'check_id']
182
     * ];
183
     *
184
     * @var array
185
     */
186
    const INDEXES = [];
187
188
    /**
189
     * Record state.
190
     *
191
     * @var int
192
     */
193
    private $state;
194
195
    /**
196
     * Points to last queued insert command for this entity, required to properly handle multiple
197
     * entity updates inside one transaction.
198
     *
199
     * @var InsertCommand
200
     */
201
    private $insertCommand = null;
202
203
    /**
204
     * Initiate entity inside or outside of ORM scope using given fields and state.
205
     *
206
     * @param array             $data
207
     * @param int               $state
208
     * @param ORMInterface|null $orm
209
     * @param array|null        $recordSchema
210
     */
211
    public function __construct(
212
        array $data = [],
213
        int $state = ORMInterface::STATE_NEW,
214
        ORMInterface $orm = null,
215
        array $recordSchema = null
216
    ) {
217
        //We can use global container as fallback if no default values were provided
218
        $orm = $this->saturate($orm, ORMInterface::class);
219
220
        $this->state = $state;
221
        if ($this->state == ORMInterface::STATE_NEW) {
222
            //Non loaded records should be in solid state by default
223
            $this->solidState(true);
224
        }
225
226
        parent::__construct($orm, $data, new RelationBucket($this, $orm));
227
    }
228
229
    /**
230
     * Check if entity been loaded (non new).
231
     *
232
     * @return bool
233
     */
234
    public function isLoaded(): bool
235
    {
236
        return $this->state != ORMInterface::STATE_NEW;
237
    }
238
239
    /**
240
     * Current model state.
241
     *
242
     * @return int
243
     */
244
    public function getState(): int
245
    {
246
        return $this->state;
247
    }
248
249
    /**
250
     * {@inheritdoc}
251
     *
252
     * @param bool $queueRelations
253
     *
254
     * @throws RecordException
255
     * @throws RelationException
256
     */
257
    public function queueStore(bool $queueRelations = true): CommandInterface
258
    {
259
        if ($this->state == ORMInterface::STATE_READONLY) {
260
            //Nothing to do on readonly entities
261
            return new NullCommand();
262
        }
263
264
        if ($this->state & ORMInterface::STATE_SCHEDULED) {
265
            throw new RecordException(
266
                "Unable to save already scheduled record, commit previous transaction first"
267
            );
268
        }
269
270
        if (!$this->isLoaded()) {
271
            $command = $this->prepareInsert();
272
        } else {
273
            if ($this->hasChanges() || $this->solidState) {
0 ignored issues
show
Documentation introduced by
The property $solidState is declared private in Spiral\Models\Traits\SolidableTrait. Since you implemented __get(), maybe consider adding a @property or @property-read annotation. This makes it easier for IDEs to provide auto-completion.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
274
                $command = $this->prepareUpdate();
275
            } else {
276
                $command = new NullCommand();
277
            }
278
        }
279
280
        //Changes are flushed BEFORE entity is saved, this is required to present
281
        //recursive update loops
282
        $this->flushChanges();
283
284
        //Relation commands
285
        if ($queueRelations) {
286
            //Queue relations before and after parent command (if needed)
287
            return $this->relations->queueRelations($command);
288
        }
289
290
        return $command;
291
    }
292
293
    /**
294
     * {@inheritdoc}
295
     *
296
     * @throws RecordException
297
     * @throws RelationException
298
     */
299
    public function queueDelete(): CommandInterface
300
    {
301
        if ($this->state == ORMInterface::STATE_READONLY || !$this->isLoaded()) {
302
            //Nothing to do
303
            return new NullCommand();
304
        }
305
306
        return $this->prepareDelete();
307
    }
308
309
    /*
310
     * Code below used to generate transaction commands.
311
     */
312
    /**
313
     * Handle result of insert command.
314
     *
315
     * @param InsertCommand $command
316
     */
317
    protected function handleInsert(InsertCommand $command)
318
    {
319
        //Flushing reference to last insert command
320
        $this->insertCommand = null;
321
322
        //We not how our primary value (add support of user supplied PK values (no autoincrement))
323
        $this->setField(
324
            $this->primaryColumn(),
325
            $command->getInsertID(),
326
            true,
327
            false
328
        );
329
330
        $this->state = ORMInterface::STATE_LOADED;
331
        $this->dispatch('created', new RecordEvent($this));
332
    }
333
334
    /**
335
     * Handle result of delete command.
336
     *
337
     * @param DeleteCommand $command
338
     */
339
    protected function handleDelete(DeleteCommand $command)
340
    {
341
        $this->state = ORMInterface::STATE_DELETED;
342
        $this->dispatch('deleted', new RecordEvent($this));
343
    }
344
345
    /**
346
     * @return InsertCommand
347
     */
348
    private function prepareInsert(): InsertCommand
349
    {
350
        $command = new InsertCommand($this->orm->table(static::class), $this->packValue());
351
352
        //Entity indicates it's own status
353
        $this->state = ORMInterface::STATE_SCHEDULED_INSERT;
354
        $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...
355
356
        //Executed when transaction successfully completed
357
        $command->onComplete(function (InsertCommand $command) {
358
            $this->handleInsert($command);
359
        });
360
361
        //Keep reference to the last insert command
362
        return $this->insertCommand = $command;
363
    }
364
365
    /**
366
     * @return UpdateCommand
367
     */
368
    private function prepareUpdate(): UpdateCommand
369
    {
370
        $command = new UpdateCommand(
371
            $this->orm->table(static::class),
372
            $this->getField($this->primaryColumn(), null, false),
0 ignored issues
show
Unused Code introduced by
The call to UpdateCommand::__construct() has too many arguments starting with $this->getField($this->p...yColumn(), null, false).

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...
373
            $this->packChanges(true)
374
        );
375
376
        if (!empty($this->insertCommand)) {
377
            $this->insertCommand->onExecute(function (InsertCommand $insert) use ($command) {
378
                //Sync primary key values
379
                $command->setWhere([$this->primaryColumn() => $insert->getInsertID()]);
380
            });
381
        }
382
383
        //Entity indicates it's own status
384
        $this->state = ORMInterface::STATE_SCHEDULED_UPDATE;
385
        $this->dispatch('update', new RecordEvent($this));
386
387
        //Executed when transaction successfully completed
388
        $command->onComplete(function (UpdateCommand $command) {
389
            $this->handleUpdate($command);
390
        });
391
392
        return $command;
393
    }
394
395
    /**
396
     * @return DeleteCommand
397
     */
398
    private function prepareDelete(): DeleteCommand
399
    {
400
        $command = new DeleteCommand(
401
            $this->orm->table(static::class),
402
            [$this->primaryColumn() => $this->primaryKey()]
403
        );
404
405
        if (!empty($this->insertCommand)) {
406
            $this->insertCommand->onExecute(function (InsertCommand $insert) use ($command) {
407
                //Sync primary key values
408
                $command->setWhere([$this->primaryColumn() => $insert->getInsertID()]);
409
            });
410
        }
411
412
        //Entity indicates it's own status
413
        $this->state = ORMInterface::STATE_SCHEDULED_DELETE;
414
        $this->dispatch('delete', new RecordEvent($this));
415
416
        //Executed when transaction successfully completed
417
        $command->onComplete(function (DeleteCommand $command) {
418
            $this->handleDelete($command);
419
        });
420
421
        return $command;
422
    }
423
}