Model   F
last analyzed

Complexity

Total Complexity 62

Size/Duplication

Total Lines 522
Duplicated Lines 0 %

Test Coverage

Coverage 11.11%

Importance

Changes 24
Bugs 0 Features 0
Metric Value
eloc 151
c 24
b 0
f 0
dl 0
loc 522
ccs 20
cts 180
cp 0.1111
rs 3.44
wmc 62

28 Methods

Rating   Name   Duplication   Size   Complexity  
A unprefix() 0 7 3
A qualify() 0 8 2
A finder() 0 3 1
A boot() 0 2 1
A clone() 0 2 1
A __clone() 0 10 3
A prefix() 0 11 4
A primaryKey() 0 7 2
A __construct() 0 6 2
A table() 0 10 2
A set() 0 3 1
A __get() 0 3 1
A __isset() 0 10 3
A __set() 0 3 1
A serialize() 0 3 1
A get() 0 7 2
A persistDelete() 0 17 2
A __unserialize() 0 3 1
A equals() 0 15 5
A persistInsert() 0 17 2
A isLoaded() 0 6 2
A __toString() 0 4 1
B save() 0 34 9
A __serialize() 0 3 1
A unserialize() 0 7 2
A persistUpdate() 0 24 2
A delete() 0 17 4
A getQueryBuilderInstance() 0 7 1

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
namespace Ronanchilvers\Orm;
4
5
use Carbon\Carbon;
6
use DateTime;
7
use Exception;
8
use Ronanchilvers\Orm\Features\HasAttributes;
9
use Ronanchilvers\Orm\Features\HasHooks;
10
use Ronanchilvers\Orm\Features\HasRelationships;
11
use Ronanchilvers\Orm\Features\HasTimestamps;
12
use Ronanchilvers\Orm\Orm;
13
use Ronanchilvers\Utility\Str;
14
use RuntimeException;
15
use Serializable;
16
17
/**
18
 * Base model class for all models
19
 *
20
 * @property int id
21
 * @author Ronan Chilvers <[email protected]>
22
 */
23
abstract class Model implements Serializable
24
{
25
    use HasHooks,
26
        HasAttributes,
27
        HasTimestamps,
28
        HasRelationships;
29
30
    /**
31
     * @var string
32
     */
33
    static protected $finder = '';
34
35
    /**
36
     * @var string
37
     */
38
    static protected $table = '';
39
40
    /**
41
     * @var string
42
     */
43
    static protected $columnPrefix = '';
44
45
    /**
46
     * @var string
47
     */
48
    static protected $primaryKey = '';
49
50
    /**
51
     * Get the finder class for this model
52
     *
53
     * @return string
54
     * @author Ronan Chilvers <[email protected]>
55
     */
56
    static public function finder()
57
    {
58
        return static::$finder;
59
    }
60
61
    /**
62
     * Get the table name for this model
63
     *
64
     * @return string
65
     * @author Ronan Chilvers <[email protected]>
66
     */
67
    static public function table()
68
    {
69
        if ('' === static::$table) {
70
            $reflection = new \ReflectionClass(get_called_class());
71
            $table = Str::plural(Str::snake($reflection->getShortName()), 2);
72
73
            return $table;
74
        }
75
76
        return static::$table;
77
    }
78
79
    /**
80
     * Get the primary key fieldname for this model
81
     *
82
     * @return string
83
     * @author Ronan Chilvers <[email protected]>
84
     */
85
    static public function primaryKey()
86
    {
87
        if ('' === static::$primaryKey) {
88
            return static::prefix('id');
89
        }
90
91
        return static::$primaryKey;
92
    }
93
94
    /**
95
     * Prefix a string with the configured field prefix
96
     *
97
     * @param  string $string
98
     * @return string
99
     * @author Ronan Chilvers <[email protected]>
100
     */
101 1
    static public function prefix($string)
102
    {
103 1
        $prefix = static::$columnPrefix;
104 1
        if (!empty($prefix)) {
105
            $prefix = "{$prefix}_";
106
        }
107 1
        if (!empty($prefix) && 0 === strpos($string, $prefix)) {
108
            return $string;
109
        }
110
111 1
        return "{$prefix}{$string}";
112
    }
113
114
    /**
115
     * Un-prefix a string with the configured field prefix
116
     *
117
     * @param string $string
118
     * @return string
119
     * @author Ronan Chilvers <[email protected]>
120
     */
121 1
    static public function unprefix($string)
122
    {
123 1
        if (!empty(static::$columnPrefix) && 0 === strpos($string, static::$columnPrefix)) {
124
            return substr($string, strlen(static::$columnPrefix) + 1);
125
        }
126
127 1
        return $string;
128
    }
129
130
    /**
131
     * Transform a string into a fully qualified column with table and prefix
132
     *
133
     * @param string $string
134
     * @return string
135
     * @author Ronan Chilvers <[email protected]>
136
     */
137
    public static function qualify($string)
138
    {
139
        if ('*' != $string) {
140
            $string = static::prefix($string);
141
        }
142
        $table = static::table();
143
144
        return "{$table}.{$string}";
145
    }
146
147
    /**
148
     * Class constructor
149
     *
150
     * @author Ronan Chilvers <[email protected]>
151
     */
152 2
    public function __construct()
153
    {
154 2
        if ($this->useTimestamps()) {
155 2
            $this->bootHasTimestamps();
156
        }
157 2
        $this->boot();
158
    }
159
160
    /**
161
     * Magic clone method to ensure that cloned models are new
162
     *
163
     * @author Ronan Chilvers <[email protected]>
164
     */
165
    public function __clone()
166
    {
167
        $primaryKey = static::primaryKey();
168
        if (isset($this->data[$primaryKey])) {
169
            unset($this->data[$primaryKey]);
170
        }
171
        if ($this->useTimestamps()) {
172
            $this->clearTimestamps();
173
        }
174
        $this->clone();
175
    }
176
177
    /**
178
     * Clone function designed to be overriden by subclasses
179
     *
180
     *
181
     * @author Ronan Chilvers <[email protected]>
182
     */
183
    protected function clone()
184
    {}
185
186
    /**
187
     * Boot the model
188
     *
189
     * @author Ronan Chilvers <[email protected]>
190
     */
191 2
    protected function boot()
192 2
    {}
193
194
    /**
195
     * Magic property isset
196
     *
197
     * @param string $attribute
198
     * @return bool
199
     * @author Ronan Chilvers <[email protected]>
200
     */
201
    public function __isset($attribute)
202
    {
203
        if ($this->hasAttribute($attribute)) {
204
            return true;
205
        }
206
        if ($this->hasRelation($attribute)) {
207
            return true;
208
        }
209
210
        return false;
211
    }
212
213
    /**
214
     * General property getter
215
     *
216
     * @param string $attribute
217
     * @return mixed
218
     * @author Ronan Chilvers <[email protected]>
219
     */
220 1
    public function get($attribute)
221
    {
222 1
        $result = $this->getRelation($attribute);
223 1
        if (!is_null($result)) {
224
            return $result;
225
        }
226 1
        return $this->getAttribute($attribute);
227
    }
228
229
    /**
230
     * Magic property getter
231
     *
232
     * @param string $attribute
233
     * @return mixed
234
     * @author Ronan Chilvers <[email protected]>
235
     */
236 1
    public function __get($attribute)
237
    {
238 1
        return $this->get($attribute);
239
    }
240
241
    /**
242
     * General property setter
243
     *
244
     * @param string $attribute
245
     * @param mixed $value
246
     * @author Ronan Chilvers <[email protected]>
247
     */
248
    public function set($attribute, $value)
249
    {
250
        return $this->setAttribute($attribute, $value);
251
    }
252
253
    /**
254
     * Magic property setter
255
     *
256
     * @param string $attribute
257
     * @param mixed $value
258
     * @author Ronan Chilvers <[email protected]>
259
     */
260
    public function __set($attribute, $value)
261
    {
262
        return $this->set($attribute, $value);
263
    }
264
265
    /**
266
     * Serialize the data array
267
     *
268
     * @return string
269
     * @author Ronan Chilvers <[email protected]>
270
     */
271
    public function serialize()
272
    {
273
        return serialize($this->toArray());
274
    }
275
276
    /**
277
     * Magic method to support Serializable in PHP 8+
278
     *
279
     * @author Ronan Chilvers <[email protected]>
280
     */
281
    public function __serialize(): array
282
    {
283
        return $this->serialize();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->serialize() returns the type string which is incompatible with the type-hinted return array.
Loading history...
284
    }
285
286
    /**
287
     * Unserialize the data array
288
     *
289
     * @param string $serialized
290
     * @author Ronan Chilvers <[email protected]>
291
     */
292
    public function unserialize($serialized)
293
    {
294
        $this->fromArray(unserialize($serialized));
295
        if ($this->useTimestamps()) {
296
            $this->bootHasTimestamps();
297
        }
298
        $this->boot();
299
    }
300
301
    /**
302
     * Magic method to support Serializable in PHP 8+
303
     *
304
     * @author Ronan Chilvers <[email protected]>
305
     */
306
    public function __unserialize(array $serialized): void
307
    {
308
        $this->unserialize($serialized);
0 ignored issues
show
Bug introduced by
$serialized of type array is incompatible with the type string expected by parameter $serialized of Ronanchilvers\Orm\Model::unserialize(). ( Ignorable by Annotation )

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

308
        $this->unserialize(/** @scrutinizer ignore-type */ $serialized);
Loading history...
309
    }
310
311
    /**
312
     * Standard toString method returns id
313
     *
314
     * @return string
315
     * @author Ronan Chilvers <[email protected]>
316
     */
317
    public function __toString()
318
    {
319
        return (string) $this->getAttribute(
320
            static::primaryKey()
321
        );
322
    }
323
324
    /* Persistance methods **************/
325
    /************************************/
326
327
    /**
328
     * Is this model loaded?
329
     *
330
     * This simply means, do we have a primary key id?
331
     *
332
     * @return boolean
333
     * @author Ronan Chilvers <[email protected]>
334
     */
335
    public function isLoaded()
336
    {
337
        $key = static::primaryKey();
338
        return (
339
            $this->hasAttribute($key) &&
340
            is_numeric($this->getAttribute($key))
341
        );
342
    }
343
344
    /**
345
     * Save this model
346
     *
347
     * This method either inserts or updates the model row based on the presence
348
     * of an ID. It will return false if the save fails.
349
     *
350
     * @return boolean
351
     * @author Ronan Chilvers <[email protected]>
352
     */
353
    public function save()
354
    {
355
        if (false === $this->beforeSave()) {
356
            return false;
357
        }
358
        if (true === $this->isLoaded()) {
359
            if (false === $this->beforeUpdate()) {
360
                return false;
361
            }
362
            if ($this->useTimestamps()) {
363
                $this->updateTimestamps();
364
            }
365
            if (true !== $this->persistUpdate()) {
366
                return false;
367
            }
368
            $this->afterUpdate();
369
            $this->afterSave();
370
371
            return true;
372
        }
373
374
        if (false === $this->beforeCreate()) {
375
            return false;
376
        }
377
        if ($this->useTimestamps()) {
378
            $this->updateTimestamps();
379
        }
380
        if (true !== $this->persistInsert()) {
381
            return false;
382
        }
383
        $this->afterCreate();
384
        $this->afterSave();
385
386
        return true;
387
    }
388
389
    /**
390
     * Delete this model record
391
     *
392
     * @return boolean
393
     * @author Ronan Chilvers <[email protected]>
394
     */
395
    public function delete()
396
    {
397
        if (!$this->isLoaded()) {
398
            throw new RuntimeException(
399
                sprintf('Unable to delete model without primary key %s', static::primaryKey())
400
            );
401
        }
402
        if (false === $this->beforeDelete()) {
403
            return false;
404
        }
405
        if (false === $this->persistDelete()) {
406
            return false;
407
        }
408
        unset($this->data[static::primaryKey()]);
409
        $this->afterDelete();
410
411
        return true;
412
    }
413
414
    /**
415
     * Insert this model into the database
416
     *
417
     * @return boolean
418
     * @author Ronan Chilvers <[email protected]>
419
     */
420
    protected function persistInsert()
421
    {
422
        $this->beforePersist();
423
        $queryBuilder = $this->getQueryBuilderInstance();
424
        $query        = $queryBuilder->insert();
425
        $data         = $this->getAttributes();
426
        unset($data[static::primaryKey()]);
427
        $query->values(
428
            $data
429
        );
430
        if (true !== $query->execute()) {
431
            return false;
432
        }
433
        $this->data[static::primaryKey()] = $queryBuilder->getConnection()->lastInsertId();
434
        $this->oldData = [];
435
436
        return true;
437
    }
438
439
    /**
440
     * Update this model in the database
441
     *
442
     * @return boolean
443
     * @author Ronan Chilvers <[email protected]>
444
     */
445
    protected function persistUpdate()
446
    {
447
        $this->beforePersist();
448
        $queryBuilder = $this->getQueryBuilderInstance();
449
        $query        = $queryBuilder->update();
450
        $data         = $this->getAttributes();
451
        $id           = $data[static::primaryKey()];
452
        unset($data[static::primaryKey()]);
453
        $query
454
            ->set(
455
                $data
456
            )
457
            ->where(
458
                static::primaryKey(),
459
                '=',
460
                $id
461
            );
462
        $result = $query->execute();
463
        if (false == $result) {
464
            return false;
465
        }
466
        $this->oldData = [];
467
468
        return true;
469
    }
470
471
    /**
472
     * Delete this model from the database
473
     *
474
     * @return boolean
475
     * @author Ronan Chilvers <[email protected]>
476
     */
477
    protected function persistDelete()
478
    {
479
        $queryBuilder = $this->getQueryBuilderInstance();
480
        $query = $queryBuilder
481
            ->delete()
482
            ->where(
483
                static::primaryKey(),
484
                '=',
485
                $this->data[static::primaryKey()]
486
            )
487
            ;
488
        if (false === $query->execute()) {
489
            return false;
490
        }
491
        unset($this->data[static::primaryKey()]);
492
493
        return true;
494
    }
495
496
    /**
497
     * Get a query builder for this model
498
     *
499
     * @return \Ronanchilvers\Orm\QueryBuilder
500
     * @author Ronan Chilvers <[email protected]>
501
     */
502
    protected function getQueryBuilderInstance()
503
    {
504
        $connection = Orm::getConnection();
505
506
        return new QueryBuilder(
507
            $connection,
508
            get_called_class()
509
        );
510
    }
511
512
    /* Persistance methods **************/
513
    /************************************/
514
515
    /************************************/
516
    /** Comparison methods **************/
517
518
    /**
519
     * Is this model equal to another one?
520
     *
521
     * For a model to be equal to another it must:
522
     *  - Be loaded
523
     *  - Be of the same class
524
     *  - Have the same id
525
     *
526
     * @param Ronanchilvers\Orm\Model $model
0 ignored issues
show
Bug introduced by
The type Ronanchilvers\Orm\Ronanchilvers\Orm\Model was not found. Did you mean Ronanchilvers\Orm\Model? If so, make sure to prefix the type with \.
Loading history...
527
     * @return bool
528
     * @author Ronan Chilvers <[email protected]>
529
     */
530
    public function equals(Model $that): bool
531
    {
532
        if (!$this->isLoaded() || !$that->isLoaded()) {
533
            return false;
534
        }
535
        if (get_class($that) !== get_called_class()) {
536
            return false;
537
        }
538
        $thisId = $this->getAttributes()[static::primaryKey()];
539
        $thatId = $that->getAttributes()[static::primaryKey()];
540
        if ($thisId != $thatId) {
541
            return false;
542
        }
543
544
        return true;
545
    }
546
547
    /************************************/
548
}
549