Completed
Branch feature/pre-split (9269d3)
by Anton
05:27
created

RecordEntity::setState()   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 1
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\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
            && !empty($this->getField($this->primaryColumn(), null, false));
238
    }
239
240
    /**
241
     * Current model state.
242
     *
243
     * @return int
244
     */
245
    public function getState(): int
246
    {
247
        return $this->state;
248
    }
249
250
    /**
251
     * {@inheritdoc}
252
     *
253
     * @param bool $queueRelations
254
     *
255
     * @throws RecordException
256
     * @throws RelationException
257
     */
258
    public function queueStore(bool $queueRelations = true): CommandInterface
259
    {
260
        if ($this->state == ORMInterface::STATE_READONLY) {
261
            //Nothing to do on readonly entities
262
            return new NullCommand();
263
        }
264
265
        if ($this->state & ORMInterface::STATE_SCHEDULED) {
266
            throw new RecordException(
267
                "Unable to save already scheduled record, commit previous transaction first"
268
            );
269
        }
270
271
        if (!$this->isLoaded()) {
272
            $command = $this->prepareInsert();
273
        } else {
274
            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...
275
                $command = $this->prepareUpdate();
276
            } else {
277
                $command = new NullCommand();
278
            }
279
        }
280
281
        //Changes are flushed BEFORE entity is saved, this is required to present
282
        //recursive update loops
283
        $this->flushChanges();
284
285
        //Relation commands
286
        if ($queueRelations) {
287
            //Queue relations before and after parent command (if needed)
288
            return $this->relations->queueRelations($command);
289
        }
290
291
        return $command;
292
    }
293
294
    /**
295
     * {@inheritdoc}
296
     *
297
     * @throws RecordException
298
     * @throws RelationException
299
     */
300
    public function queueDelete(): CommandInterface
301
    {
302
        if ($this->state == ORMInterface::STATE_READONLY || !$this->isLoaded()) {
303
            //Nothing to do
304
            return new NullCommand();
305
        }
306
307
        if ($this->state & ORMInterface::STATE_SCHEDULED) {
308
            throw new RecordException(
309
                "Unable to delete scheduled record, commit previous transaction first"
310
            );
311
        }
312
313
        return $this->prepareDelete();
314
    }
315
316
    /*
317
     * Code below used to generate transaction commands.
318
     */
319
320
    /**
321
     * Change object state.
322
     *
323
     * @param int $state
324
     */
325
    private function setState(int $state)
326
    {
327
        $this->state = $state;
328
    }
329
330
    /**
331
     * @return InsertCommand
332
     */
333
    private function prepareInsert(): InsertCommand
334
    {
335
        $command = new InsertCommand($this->packValue(), $this->orm->table(static::class));
0 ignored issues
show
Unused Code introduced by
The call to InsertCommand::__construct() has too many arguments starting with $this->packValue().

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...
336
337
        //Entity indicates it's own status
338
        $this->setState(ORMInterface::STATE_SCHEDULED_INSERT);
339
        $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...
340
341
        //Executed when transaction successfully completed
342
        $command->onComplete($this->syncState());
343
344
        //Keep reference to the last insert command
345
        return $this->insertCommand = $command;
346
    }
347
348
    /**
349
     * @return UpdateCommand
350
     */
351
    private function prepareUpdate(): UpdateCommand
352
    {
353
        $command = new UpdateCommand(
354
            $this->packChanges(true),
0 ignored issues
show
Unused Code introduced by
The call to UpdateCommand::__construct() has too many arguments starting with $this->packChanges(true).

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
            $this->isLoaded() ? $this->primaryKey() : null,
356
            $this->orm->table(static::class)
357
        );
358
359
        if (!empty($this->insertCommand)) {
360
            $this->insertCommand->onExecute(function (InsertCommand $insert) use ($command) {
361
                //Sync primary key values
362
                $command->setPrimary($insert->lastInsertID());
0 ignored issues
show
Bug introduced by
The method lastInsertID() does not seem to exist on object<Spiral\ORM\Commands\InsertCommand>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
Bug introduced by
The method setPrimary() does not seem to exist on object<Spiral\ORM\Commands\UpdateCommand>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
363
            });
364
        }
365
366
        //Entity indicates it's own status
367
        $this->setState(ORMInterface::STATE_SCHEDULED_UPDATE);
368
        $this->dispatch('update', new RecordEvent($this));
369
370
        //Executed when transaction successfully completed
371
        $command->onComplete($this->syncState());
0 ignored issues
show
Unused Code introduced by
The call to UpdateCommand::onComplete() has too many arguments starting with $this->syncState().

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...
372
373
        return $command;
374
    }
375
376
    /**
377
     * @return DeleteCommand
378
     */
379
    private function prepareDelete(): DeleteCommand
380
    {
381
        //Entity indicates it's own status
382
        $this->setState(ORMInterface::STATE_SCHEDULED_DELETE);
383
        $this->dispatch('delete', new RecordEvent($this));
384
385
        $command = new DeleteCommand(
386
            $this->primaryKey(),
0 ignored issues
show
Unused Code introduced by
The call to DeleteCommand::__construct() has too many arguments starting with $this->primaryKey().

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...
387
            $this->orm->define(static::class, ORMInterface::R_DATABASE),
388
            $this->orm->define(static::class, ORMInterface::R_TABLE)
389
        );
390
391
        //Executed when transaction successfully completed
392
        $command->onComplete($this->syncState());
0 ignored issues
show
Unused Code introduced by
The call to DeleteCommand::onComplete() has too many arguments starting with $this->syncState().

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...
393
394
        return $command;
395
    }
396
397
    private function syncState(): \Closure
398
    {
399
        return function ($command) {
400
            dump($command);
401
        };
402
403
        //got command!
404
405
//        //Command context MIGHT include some fields set by parent commands (i.e. runtime values)
406
//        foreach ($command->getContext() as $field => $value) {
407
//            $this->setField($field, $value, true, false);
408
//        }
409
//
410
//        $this->setField(
411
//            $this->recordSchema[self::SH_PRIMARY_KEY],
412
//            $command->primaryKey(),
413
//            true,
414
//            false
415
//        );
416
417
    }
418
}