Completed
Branch feature/pre-split (9d6b17)
by Anton
03:28
created

RecordEntity::handleInsert()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 16
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 9
nc 1
nop 1
dl 0
loc 16
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\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 $firstInsert = 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
     */
210
    public function __construct(
211
        array $data = [],
212
        int $state = ORMInterface::STATE_NEW,
213
        ORMInterface $orm = null
214
    ) {
215
        //We can use global container as fallback if no default values were provided
216
        $orm = $this->saturate($orm, ORMInterface::class);
217
218
        $this->state = $state;
219
220
        //Non loaded records should be in solid state by default
221
        $this->solidState($this->state == ORMInterface::STATE_NEW);
222
223
        parent::__construct($orm, $data, new RelationBucket($this, $orm));
224
    }
225
226
    /**
227
     * Check if entity been loaded (non new).
228
     *
229
     * @return bool
230
     */
231
    public function isLoaded(): bool
232
    {
233
        return $this->state != ORMInterface::STATE_NEW;
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): CommandInterface
255
    {
256
        if (!$this->isLoaded()) {
257
            $command = $this->prepareInsert();
258
        } else {
259
            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...
260
                $command = $this->prepareUpdate();
261
            } else {
262
                $command = new NullCommand();
263
            }
264
        }
265
266
        //Changes are flushed BEFORE entity is saved, this is required to present
267
        //recursive update loops
268
        $this->flushChanges();
269
270
        //Relation commands
271
        if ($queueRelations) {
272
            //Queue relations before and after parent command (if needed)
273
            return $this->relations->queueRelations($command);
274
        }
275
276
        return $command;
277
    }
278
279
    /**
280
     * {@inheritdoc}
281
     *
282
     * @throws RecordException
283
     * @throws RelationException
284
     */
285
    public function queueDelete(): CommandInterface
286
    {
287
        if (!$this->isLoaded()) {
288
            //Nothing to do
289
            return new NullCommand();
290
        }
291
292
        return $this->prepareDelete();
293
    }
294
295
    /**
296
     * @return InsertCommand
297
     */
298
    private function prepareInsert(): InsertCommand
299
    {
300
        $data = $this->packValue();
301
        unset($data[$this->primaryColumn()]);
302
303
        $command = new InsertCommand($this->orm->table(static::class), $data);
304
305
        //Entity indicates it's own status
306
        $this->state = ORMInterface::STATE_SCHEDULED_INSERT;
307
        $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...
308
309
        //Executed when transaction successfully completed
310
        $command->onComplete(function (InsertCommand $command) {
311
            $this->handleInsert($command);
312
        });
313
314
        $command->onRollBack(function () {
315
            //Flushing existed insert command to prevent collisions
316
            $this->firstInsert = null;
317
        });
318
319
        //Keep reference to the last insert command
320
        return $this->firstInsert = $command;
321
    }
322
323
    /**
324
     * @return UpdateCommand
325
     */
326
    private function prepareUpdate(): UpdateCommand
327
    {
328
        $command = new UpdateCommand(
329
            $this->orm->table(static::class),
330
            [$this->primaryColumn() => $this->primaryKey()],
331
            $this->packChanges(true)
332
        );
333
334
        if (!empty($this->firstInsert)) {
335
            $this->firstInsert->onExecute(function (InsertCommand $insert) use ($command) {
336
                //Sync primary key values
337
                $command->setWhere([$this->primaryColumn() => $insert->getInsertID()]);
338
            });
339
        }
340
341
        //Entity indicates it's own status
342
        $this->state = ORMInterface::STATE_SCHEDULED_UPDATE;
343
        $this->dispatch('update', new RecordEvent($this));
344
345
        //Executed when transaction successfully completed
346
        $command->onComplete(function (UpdateCommand $command) {
347
            $this->handleUpdate($command);
348
        });
349
350
        return $command;
351
    }
352
353
    /**
354
     * @return DeleteCommand
355
     */
356
    private function prepareDelete(): DeleteCommand
357
    {
358
        $command = new DeleteCommand(
359
            $this->orm->table(static::class),
360
            [$this->primaryColumn() => $this->primaryKey()]
361
        );
362
363
        if (!empty($this->firstInsert)) {
364
            $this->firstInsert->onExecute(function (InsertCommand $insert) use ($command) {
365
                //Sync primary key values
366
                $command->setWhere([$this->primaryColumn() => $insert->getInsertID()]);
367
            });
368
        }
369
370
        //Entity indicates it's own status
371
        $this->state = ORMInterface::STATE_SCHEDULED_DELETE;
372
        $this->dispatch('delete', new RecordEvent($this));
373
374
        //Executed when transaction successfully completed
375
        $command->onComplete(function (DeleteCommand $command) {
376
            $this->handleDelete($command);
377
        });
378
379
        return $command;
380
    }
381
382
    /**
383
     * Handle result of insert command.
384
     *
385
     * @param InsertCommand $command
386
     */
387
    private function handleInsert(InsertCommand $command)
388
    {
389
        //Flushing reference to last insert command
390
        $this->firstInsert = null;
391
392
        //We not how our primary value (add support of user supplied PK values (no autoincrement))
393
        $this->setField(
394
            $this->primaryColumn(),
395
            $command->getInsertID(),
396
            true,
397
            false
398
        );
399
400
        $this->state = ORMInterface::STATE_LOADED;
401
        $this->dispatch('created', new RecordEvent($this));
402
    }
403
404
    /**
405
     * Handle result of update command.
406
     *
407
     * @param UpdateCommand $command
408
     */
409
    private function handleUpdate(UpdateCommand $command)
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
}