Model   F
last analyzed

Complexity

Total Complexity 62

Size/Duplication

Total Lines 728
Duplicated Lines 0 %

Importance

Changes 10
Bugs 2 Features 2
Metric Value
eloc 131
c 10
b 2
f 2
dl 0
loc 728
rs 3.44
wmc 62

46 Methods

Rating   Name   Duplication   Size   Complexity  
A fill() 0 4 2
A delete() 0 6 1
A save() 0 24 3
A count() 0 6 1
A getPrimaryKey() 0 3 1
A insertAndSetId() 0 5 1
A syncOriginalAttribute() 0 5 1
A find() 0 10 2
A first() 0 2 1
A jsonSerialize() 0 2 1
A where() 0 6 1
A offsetGet() 0 3 1
A finishSave() 0 5 1
A performUpdate() 0 21 2
A __get() 0 3 1
A findOrFail() 0 7 1
A newBaseQueryBuilder() 0 7 1
A setAttribute() 0 5 1
A getAttribute() 0 13 3
A __set() 0 3 1
A latest() 0 2 1
A newQuery() 0 4 1
A bootIfNotBooted() 0 6 2
A getForeignKey() 0 3 1
A setUpdatedAt() 0 5 1
A usesTimestamps() 0 3 1
A syncOriginal() 0 5 1
A offsetSet() 0 3 1
A getConnection() 0 3 1
A setPrimaryKey() 0 3 1
A updateTimestamps() 0 10 3
A getCreatedAtColumn() 0 3 1
A freshTimestamp() 0 3 1
A offsetExists() 0 3 1
A offsetUnset() 0 3 1
A setIncrementing() 0 3 1
A getAttributes() 0 3 1
A isIncrementing() 0 3 1
A setCreatedAt() 0 5 1
A boot() 0 2 1
A getUpdatedAtColumn() 0 3 1
A remove() 0 9 2
A getTable() 0 13 3
A all() 0 11 1
A __construct() 0 7 1
A performInsert() 0 35 4

How to fix   Complexity   

Complex Class

Complex classes like Model often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Model, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Framy Framework
4
 *
5
 * @copyright Copyright Framy
6
 * @Author Marco Bier <[email protected]>
7
 */
8
9
namespace app\framework\Component\Database\Model;
10
11
use app\framework\Component\Database\Connection\ConnectionFactory;
12
use app\framework\Component\Database\Connection\ConnectionNotConfiguredException;
13
use app\framework\Component\Database\Model\Concerns\HasRelationships;
14
use app\framework\Component\Database\Query\Builder as QueryBuilder;
15
use app\framework\Component\StdLib\StdObject\ArrayObject\ArrayObject;
16
use app\framework\Component\StdLib\StdObject\StringObject\StringObject;
17
use app\framework\Component\StdLib\StdObject\StringObject\StringObjectException;
18
use ArrayAccess;
19
use JsonSerializable;
20
21
/**
22
 * @package app\framework\Component\Database\Model
23
 */
24
class Model implements ArrayAccess, JsonSerializable
25
{
26
    use HasRelationships;
27
28
    /**
29
     * The connection name for the model.
30
     *
31
     * @var string $connection
32
     */
33
    protected $connection;
34
35
    /**
36
     * The table associated with the model.
37
     *
38
     * @var string
39
     */
40
    protected $table;
41
42
    /**
43
     * Rather or not the model has been booted
44
     *
45
     * @var bool
46
     */
47
    protected static $isBooted = false;
48
49
    /**
50
     * The primary key for the model.
51
     *
52
     * @var string
53
     */
54
    protected $primaryKey = 'id';
55
56
    /**
57
     * The "type" of the auto-incrementing ID.
58
     *
59
     * @var string
60
     */
61
    protected $keyType = 'int';
62
63
    /**
64
     * Indicates if the IDs are auto-incrementing.
65
     *
66
     * @var bool
67
     */
68
    protected $incrementing = true;
69
70
    /**
71
     * @var bool
72
     */
73
    protected $timestamps = true;
74
75
    /**
76
     * The model's attributes.
77
     *
78
     * @var array
79
     */
80
    protected $attributes = [];
81
82
    /**
83
     * The model attribute's original state.
84
     *
85
     * @var array
86
     */
87
    protected $original = [];
88
89
    /**
90
     * The number of models to return for pagination.
91
     *
92
     * @var int
93
     */
94
    protected $perPage = 15;
95
96
    /**
97
     * Indicates if the model exists.
98
     *
99
     * @var bool
100
     */
101
    public $exists = false;
102
103
    /**use app\framework\Component\Database\DB;
104
105
     * The name of the "created at" column.
106
     *
107
     * @var string
108
     */
109
    const CREATED_AT = 'created_at';
110
111
    /**
112
     * The name of the "updated at" column.
113
     *
114
     * @var string
115
     */
116
    const UPDATED_AT = 'updated_at';
117
118
    /**
119
     * Model constructor.
120
     * @param array $attributes
121
     */
122
    public function __construct(array $attributes = [])
123
    {
124
        $this->bootIfNotBooted();
125
126
        $this->syncOriginal();
127
128
        $this->fill($attributes);
129
    }
130
131
    protected static function boot()
132
    {
133
        // todo: something should happen here
134
    }
135
136
    /**
137
     * Check if the model needs to be booted, and if so boot
138
     */
139
    public function bootIfNotBooted()
140
    {
141
        if (! self::$isBooted) {
142
            self::boot();
143
144
            self::$isBooted = true;
145
        }
146
    }
147
148
    /**
149
     * Sync the original attributes with the current.
150
     *
151
     * @return $this
152
     */
153
    public function syncOriginal()
154
    {
155
        $this->original = $this->attributes;
156
157
        return $this;
158
    }
159
160
    /**
161
     * Sync a single original attribute with its current value.
162
     *
163
     * @param  string  $attribute
164
     * @return $this
165
     */
166
    public function syncOriginalAttribute($attribute)
167
    {
168
        $this->original[$attribute] = $this->attributes[$attribute];
169
170
        return $this;
171
    }
172
173
    /**
174
     * primaryKey getter
175
     *
176
     * @return string
177
     */
178
    public function getPrimaryKey(): string
179
    {
180
        return $this->primaryKey;
181
    }
182
183
    /**
184
     * primaryKey setter
185
     *
186
     * @param string $primaryKey
187
     */
188
    public function setPrimaryKey(string $primaryKey): void
189
    {
190
        $this->primaryKey = $primaryKey;
191
    }
192
193
    /**
194
     * Get the default foreign key name for the model.
195
     *
196
     * @return string
197
     */
198
    public function getForeignKey()
199
    {
200
        return str(class_basename($this).'s_'.$this->getPrimaryKey())->snakeCase()->val();
201
    }
202
203
    /**
204
     * @return bool
205
     */
206
    public function isIncrementing(): bool
207
    {
208
        return $this->incrementing;
209
    }
210
211
    /**
212
     * @param bool $incrementing
213
     */
214
    public function setIncrementing(bool $incrementing): void
215
    {
216
        $this->incrementing = $incrementing;
217
    }
218
219
    /**
220
     * Save the model to the database.
221
     *
222
     * @return bool
223
     * @throws ConnectionNotConfiguredException
224
     * @throws StringObjectException
225
     */
226
    public function save()
227
    {
228
        // If the model already exists in the database we can just update our record
229
        // that is already in this database using the current IDs in this "where"
230
        // clause to only update this model. Otherwise, we'll just insert them.
231
        if ($this->exists) {
232
            $saved = $this->performUpdate($this->newQuery());
233
        }
234
235
        // If the model is brand new, we'll insert it into our database and set the
236
        // ID attribute on the model to the value of the newly inserted row's ID
237
        // which is typically an auto-increment value managed by the database.
238
        else {
239
            $saved = $this->performInsert($this->newQuery());
240
        }
241
242
        // If the model is successfully saved, we need to do a few more things once
243
        // that is done. We will call the "saved" method here to run any actions
244
        // we need to happen after a model gets successfully saved right here.
245
        if ($saved) {
246
            $this->finishSave();
247
        }
248
249
        return $saved;
250
    }
251
252
    /**
253
     * Get number of entries in table
254
     */
255
    public function count()
256
    {
257
        $instance       = new static();
258
        $result         = $instance->newQuery()->count();
259
260
        return $result;
261
    }
262
263
    /**
264
     * Fill attributes by giving array.
265
     *
266
     * @param array $attributes
267
     */
268
    public function fill(array $attributes)
269
    {
270
        foreach ($attributes as $key => $values) {
271
            $this->setAttribute($key, $values);
272
        }
273
    }
274
275
    /**
276
     * Deletes the model
277
     *
278
     * @return int Number of effected rows
279
     * @throws StringObjectException
280
     * @throws ConnectionNotConfiguredException
281
     */
282
    public function delete()
283
    {
284
        return $this->newQuery()
285
            ->wherePrimaryKey(
286
                $this->offsetGet($this->getPrimaryKey())
287
            )->delete();
288
    }
289
290
    /**
291
     * Receive all model
292
     *
293
     * @param array $columns
294
     * @return mixed
295
     * @throws ConnectionNotConfiguredException
296
     * @throws StringObjectException
297
     */
298
    public static function all(array $columns = ['*'])
299
    {
300
        $instance = new static();
301
        /** @var ArrayObject $result */
302
        $result   = $instance->newQuery()->get($columns);
303
304
        $result->map(function ($item) {
305
            $item->exists = true;
306
        });
307
308
        return $result;
309
    }
310
311
    /**
312
     * @param array|int $id
313
     * @return ArrayObject|Model|null
314
     * @throws ConnectionNotConfiguredException
315
     * @throws StringObjectException
316
     */
317
    public static function find($id)
318
    {
319
        $instance = new static();
320
        $result   = $instance->newQuery()->find($id);
321
322
        if (! is_null($result)) {
323
            $result->exists = true;
324
        }
325
326
        return $result;
327
    }
328
329
    public static function findOrFail($id)
330
    {
331
        $instance       = new static();
332
        $result         = $instance->newQuery()->findOrFail($id);
333
        $result->exists = true;
334
335
        return $result;
336
    }
337
338
    public static function first()
339
    {
340
        // TODO: implement
341
    }
342
343
    public static function latest()
344
    {
345
        // TODO: implement
346
    }
347
348
    /**
349
     * Remove an selection of Models
350
     *
351
     * @param $id array|int
352
     * @return int Number of effected rows
353
     * @throws ConnectionNotConfiguredException
354
     * @throws StringObjectException
355
     */
356
    public static function remove($id)
357
    {
358
        $instance = new static();
359
360
        if (! is_array($id)) {
361
            $id = [$id];
362
        }
363
364
        return $instance->newQuery()->remove($id);
365
    }
366
367
    /**
368
     * @param               $column
369
     * @param string $operator
370
     * @param               $value
371
     * @param string $boolean
372
     * @return QueryBuilder
373
     * @throws ConnectionNotConfiguredException
374
     * @throws StringObjectException
375
     */
376
    public static function where($column, $operator = "=", $value = null, $boolean = 'and')
377
    {
378
        $instance = new static();
379
        $result   = $instance->newQuery()->where($column, $operator, $value, $boolean);
380
381
        return $result;
382
    }
383
384
    /**
385
     * Set a given attribute on the model.
386
     *
387
     * @param  string  $key
388
     * @param  mixed  $value
389
     * @return $this
390
     */
391
    public function setAttribute($key, $value)
392
    {
393
        $this->attributes[$key] = $value;
394
395
        return $this;
396
    }
397
398
    public function getAttributes()
399
    {
400
        return $this->attributes;
401
    }
402
403
    /**
404
     * Get an attribute from the model.
405
     *
406
     * @param  string  $key
407
     * @return mixed
408
     */
409
    public function getAttribute($key)
410
    {
411
        if (! $key) {
412
            return;
413
        }
414
415
        $attr = $this->getAttributes()[$key];
416
417
        if (isset($attr)) {
418
            return $attr;
419
        }
420
421
        return;
422
    }
423
424
    /**
425
     * Get connection
426
     *
427
     * @return string
428
     */
429
    public function getConnection()
430
    {
431
        return $this->connection;
432
    }
433
434
    /**
435
     * Returns table name. Extracts table name if not yet set.
436
     *
437
     * @return mixed
438
     * @throws StringObjectException
439
     */
440
    public function getTable()
441
    {
442
        /** @var String|StringObject $table */
443
        $table = $this->table;
444
445
        if ($table === null) {
446
            $table = str(str(get_class($this))->explode("\\")->last());
447
            $table->snakeCase();
448
449
            $table->append("s");
450
        }
451
452
        return $this->table = is_string($table) ? $table : $table->val();
453
    }
454
455
    /**
456
     * Set the value of the "created at" attribute.
457
     *
458
     * @param  mixed  $value
459
     * @return $this
460
     */
461
    public function setCreatedAt($value)
462
    {
463
        $this->{static::CREATED_AT} = $value;
464
465
        return $this;
466
    }
467
468
    /**
469
     * Set the value of the "updated at" attribute.
470
     *
471
     * @param  mixed  $value
472
     * @return $this
473
     */
474
    public function setUpdatedAt($value)
475
    {
476
        $this->{static::UPDATED_AT} = $value;
477
478
        return $this;
479
    }
480
481
    /**
482
     * Get the name of the "created at" column.
483
     *
484
     * @return string
485
     */
486
    public function getCreatedAtColumn()
487
    {
488
        return static::CREATED_AT;
489
    }
490
491
    /**
492
     * Get the name of the "updated at" column.
493
     *
494
     * @return string
495
     */
496
    public function getUpdatedAtColumn()
497
    {
498
        return static::UPDATED_AT;
499
    }
500
501
    /**
502
     * Perform a model insert operation.
503
     *
504
     * @param Builder $query
505
     * @return bool
506
     */
507
    protected function performInsert(Builder $query)
508
    {
509
        // First we'll need to create a fresh query instance and touch the creation and
510
        // update timestamps on this model, which are maintained by us for developer
511
        // convenience. After, we will just continue saving these model instances.
512
        if ($this->usesTimestamps()) {
513
            $this->updateTimestamps();
514
        }
515
516
        // If the model has an incrementing key, we can use the "insertGetId" method on
517
        // the query builder, which will give us back the final inserted ID for this
518
        // table from the database. Not all tables have to be incrementing though.
519
        $attributes = $this->getAttributes();
520
521
        if ($this->isIncrementing()) {
522
            $this->insertAndSetId($query, $attributes);
523
        }
524
525
        // If the table isn't incrementing we'll simply insert these attributes as they
526
        // are. These attribute arrays must contain an "id" column previously placed
527
        // there by the developer as the manually determined key for these models.
528
        else {
529
            if (empty($attributes)) {
530
                return true;
531
            }
532
533
            $query->insert($attributes);
534
        }
535
536
        // We will go ahead and set the exists property to true, so that it is set when
537
        // the created event is fired, just in case the developer tries to update it
538
        // during the event. This will allow them to do so and run an update here.
539
        $this->exists = true;
540
541
        return true;
542
    }
543
544
    /**
545
     * Performing an model update operation.
546
     *
547
     * @param  Builder $query
548
     * @return bool
549
     */
550
    protected function performUpdate(Builder $query)
551
    {
552
        // We will need to set the updated at stamp to current time
553
        if ($this->usesTimestamps()) {
554
            $this->setUpdatedAt($this->freshTimestamp());
555
        }
556
557
        // Get the changed attributes which shall then be updated
558
        $attributes = arr($this->getAttributes())->difference($this->original);
559
560
        // casting to bool because if it performed an action
561
        // we will have $result = true and false otherwise
562
        $result = (bool) $query->where(
563
            $this->getPrimaryKey(),
564
            '=',
565
            $this->offsetGet($this->getPrimaryKey())
566
        )->update($attributes);
567
568
        $this->syncOriginal();
569
570
        return $result;
571
    }
572
573
    /**
574
     * Whether a offset exists
575
     * @link https://php.net/manual/en/arrayaccess.offsetexists.php
576
     * @param mixed $offset <p>
577
     * An offset to check for.
578
     * </p>
579
     * @return boolean true on success or false on failure.
580
     * </p>
581
     * <p>
582
     * The return value will be casted to boolean if non-boolean was returned.
583
     * @since 5.0.0
584
     */
585
    public function offsetExists($offset)
586
    {
587
        return isset($this->attributes[$offset]);
588
    }
589
590
    /**
591
     * Offset to retrieve
592
     * @link https://php.net/manual/en/arrayaccess.offsetget.php
593
     * @param mixed $offset <p>
594
     * The offset to retrieve.
595
     * </p>
596
     * @return mixed Can return all value types.
597
     * @since 5.0.0
598
     */
599
    public function offsetGet($offset)
600
    {
601
        return $this->attributes[$offset];
602
    }
603
604
    /**
605
     * Offset to set
606
     * @link https://php.net/manual/en/arrayaccess.offsetset.php
607
     * @param mixed $offset <p>
608
     * The offset to assign the value to.
609
     * </p>
610
     * @param mixed $value <p>
611
     * The value to set.
612
     * </p>
613
     * @return void
614
     * @since 5.0.0
615
     */
616
    public function offsetSet($offset, $value)
617
    {
618
        $this->attributes[$offset] = $value;
619
    }
620
621
    /**
622
     * Offset to unset
623
     * @link https://php.net/manual/en/arrayaccess.offsetunset.php
624
     * @param mixed $offset <p>
625
     * The offset to unset.
626
     * </p>
627
     * @return void
628
     * @since 5.0.0
629
     */
630
    public function offsetUnset($offset)
631
    {
632
        unset($this->attributes[$offset]);
633
    }
634
635
    /**
636
     * Specify data which should be serialized to JSON
637
     * @link https://php.net/manual/en/jsonserializable.jsonserialize.php
638
     * @return mixed data which can be serialized by <b>json_encode</b>,
639
     * which is a value of any type other than a resource.
640
     * @since 5.4.0
641
     */
642
    public function jsonSerialize()
643
    {
644
        // TODO: Implement jsonSerialize() method.
645
    }
646
647
    /**
648
     * @inheritDoc
649
     */
650
    public function __set($name, $value)
651
    {
652
        $this->offsetSet($name, $value);
653
    }
654
655
    /**
656
     * @inheritDoc
657
     */
658
    public function __get($name)
659
    {
660
        return $this->offsetGet($name);
661
    }
662
663
    /**
664
     * Get a new Query Builder
665
     *
666
     * @return Builder
667
     * @throws ConnectionNotConfiguredException
668
     * @throws StringObjectException
669
     */
670
    public function newQuery()
671
    {
672
        return new Builder(
673
            $this
674
        );
675
    }
676
677
    /**
678
     * Get a new query builder instance for the connection.
679
     *
680
     * @return QueryBuilder
681
     * @throws ConnectionNotConfiguredException
682
     */
683
    protected function newBaseQueryBuilder()
684
    {
685
        $conn = ConnectionFactory::getInstance()->get(
686
            $this->getConnection()
687
        );
688
689
        return new QueryBuilder($conn);
690
    }
691
692
    /**
693
     * Update the creation and update timestamps.
694
     *
695
     * @return void
696
     */
697
    protected function updateTimestamps()
698
    {
699
        $time = $this->freshTimestamp();
700
701
        if (! is_null(static::UPDATED_AT) ) {
702
            $this->setUpdatedAt($time);
703
        }
704
705
        if (! is_null(static::CREATED_AT)) {
706
            $this->setCreatedAt($time);
707
        }
708
    }
709
710
    /**
711
     * Insert the given attributes and set the ID on the model.
712
     *
713
     * @param  Builder  $query
714
     * @param  array  $attributes
715
     * @return void
716
     */
717
    protected function insertAndSetId(Builder $query, $attributes)
718
    {
719
        $id = $query->insertGetId($attributes, $keyName = $this->getPrimaryKey());
0 ignored issues
show
Unused Code introduced by
The call to app\framework\Component\...\Builder::insertGetId() has too many arguments starting with $keyName = $this->getPrimaryKey(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

719
        /** @scrutinizer ignore-call */ 
720
        $id = $query->insertGetId($attributes, $keyName = $this->getPrimaryKey());

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. Please note the @ignore annotation hint above.

Loading history...
720
721
        $this->setAttribute($keyName, $id);
722
    }
723
724
    /**
725
     * Determine if the model uses timestamps.
726
     *
727
     * @return bool
728
     */
729
    public function usesTimestamps()
730
    {
731
        return $this->timestamps;
732
    }
733
734
    /**
735
     * Returns current date time
736
     *
737
     * @return string
738
     */
739
    protected function freshTimestamp()
740
    {
741
        return datetime()->val();
742
    }
743
744
    /**
745
     * Perform any actions that are necessary after the model is saved.
746
     */
747
    protected function finishSave()
748
    {
749
        // fire Event?
750
751
        $this->syncOriginal();
752
    }
753
}
754