• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

rotexsoft / leanorm / 25626720283

10 May 2026 10:46AM UTC coverage: 96.372% (-0.04%) from 96.41%
25626720283

push

github

rotexdegba
Pre 7.x release updates

61 of 62 new or added lines in 2 files covered. (98.39%)

5 existing lines in 3 files now uncovered.

1700 of 1764 relevant lines covered (96.37%)

192.52 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

100.0
/src/LeanOrm/Model/CommonRecordCodeTrait.php
1
<?php
2
declare(strict_types=1);
3
namespace LeanOrm\Model;
4

5
/**
6
 * This trait contains code shared between ReadOnlyRecord & Record
7
 * 
8
 * @author Rotimi Adegbamigbe
9
 * @copyright (c) 2026, Rotexsoft
10
 */
11
trait CommonRecordCodeTrait {
12
    
13
    public const RELATED_DATA_NOT_LOADED_OR_EXISTENT_MSG = "Data either not loaded or doesn't exist in the database";
14
    
15
    /**
16
     * Data for this record ([to be saved to the db] or [as read from the db]).
17
     */
18
    protected array $data = [];
19
    
20
    /**
21
     * Data for this record (not to be saved to the db i.e. not from any actual db column and not related data).
22
     */
23
    protected array $non_table_col_and_non_related_data = [];
24
    
25
    /**
26
     * Holds relationship data retrieved based on definitions in the array below.
27
     * \GDAO\Model::$relations
28
     */
29
    protected array $related_data = [];
30

31
    /**
32
     * The model object that saves and reads data to and from the db on behalf 
33
     * of this record.
34
     */
35
    protected \GDAO\Model $model;
36
    
37
    /**
38
     * 
39
     * @param array $data associative array of data to be loaded into this record.
40
     *                    [
41
     *                      'col_name1'=>'value_for_col1', 
42
     *                      .............................,
43
     *                      .............................,
44
     *                      'col_nameN'=>'value_for_colN'
45
     *                    ]
46
     * @param \GDAO\Model $model The model object that transfers data between the db and this record.
47
     */
48
    public function __construct(array $data, \GDAO\Model $model) {
49
        
50
        $this->setModel($model);
656✔
51
        $this->loadData($data);
656✔
52
    }
53
    
54
    public function __destruct() {
55

56
        //print "Destroying Record with Primary key Value: " . $this->getPrimaryVal() . "<br>";
57
        unset($this->data);
656✔
58

59
        if(property_exists($this, 'initial_data')) {
656✔
60

61
            /** @psalm-suppress UndefinedThisPropertyFetch */
62
            unset($this->initial_data);
564✔
63
        }
64

65
        if(property_exists($this, 'is_new')) {
656✔
66

67
            /** @psalm-suppress UndefinedThisPropertyFetch */
68
            unset($this->is_new);
564✔
69
        }
70

71
        unset($this->non_table_col_and_non_related_data);
656✔
72
        unset($this->related_data);
656✔
73
        //Don't unset $this->model, it may still be referenced by other 
74
        //Record and / or Collection objects.
75
    }
76
    
77
    /**
78
     * Get the data for this record.
79
     * Modifying the returned data will not affect the data inside this record.
80
     * 
81
     * @return array a copy of the current data for this record
82
     */
83
    public function getData(): array {
84
        
85
        return $this->data;
224✔
86
    }
87
    
88
    /**
89
     * Get the data contained in an instance of the record class and all
90
     * loaded related data for the record. Related data that was not loaded
91
     * for a record before calling this method will not be loaded even if it
92
     * exists in the database, it will be marked as not loaded or not existent.
93
     */
94
    public function getDataAndRelatedData(): array {
95

96
        $data = $this->data; // take a copy of the db table data for this record
8✔
97

98
        /** @psalm-suppress MixedAssignment */
99
        foreach ($this->getModel()->getRelationNames() as $relation) {
8✔
100

101
            /** @psalm-suppress MixedArrayOffset */
102
            if(isset($this->related_data[$relation])) {
8✔
103

104
                // Make sure the related data was load before a call to this method
105
                if(
106
                    $this->related_data[$relation] instanceof Record
8✔
107
                    || $this->related_data[$relation] instanceof ReadOnlyRecord
8✔
108
                    || $this->related_data[$relation] instanceof Collection
8✔
109
                ) {
110
                    /** @psalm-suppress MixedArrayOffset */
111
                    $data[$relation] = $this->related_data[$relation]->getDataAndRelatedData();
8✔
112

113
                } else {
114

115
                    /** @psalm-suppress MixedArrayOffset */
116
                    $data[$relation] = $this->related_data[$relation];
8✔
117
                }
118

119
            } else {
120

121
                // Related data was not loaded before this method was called
122
                // Either the data exists in the db but was not eager loaded
123
                // or the data does not exist.
124
                /** @psalm-suppress MixedArrayOffset */
125
                $data[$relation] = static::RELATED_DATA_NOT_LOADED_OR_EXISTENT_MSG;
8✔
126

127
            } // if(isset($this->related_data[$relation]))
128
        } // foreach ($this->getModel()->getRelationNames() as $relation)
129

130
        return $data;
8✔
131
    }
132
    
133
    /**
134
     * Get all the related data loaded into this record.
135
     * Modifying the returned data will not affect the related data inside this record.
136
     * 
137
     * @return array a reference to all the related data loaded into this record.
138
     */
139
    public function getRelatedData(): array {
140
        
141
        return $this->related_data;
44✔
142
    }
143
    
144
    /**
145
     * Get data for this record that does not belong to any of it's table columns and is not related data.
146
     * 
147
     * @return array Data for this record (not to be saved to the db i.e. not from any actual db column and not related data).
148
     */
149
    public function getNonTableColAndNonRelatedData(): array {
150
        
151
        return $this->non_table_col_and_non_related_data;
52✔
152
    }
153
    
154
    /**
155
     * Get a reference to the data for this record.
156
     * Modifying the returned data will affect the data inside this record.
157
     * 
158
     * @return array a reference to the current data for this record.
159
     */
160
    public function &getDataByRef(): array {
161
        
162
        return $this->data;
8✔
163
    }
164
    
165
    /**
166
     * Get a reference to all the related data loaded into this record.
167
     * Modifying the returned data will affect the related data inside this record.
168
     * 
169
     * @return array a reference to all the related data loaded into this record.
170
     */
171
    public function &getRelatedDataByRef(): array {
172
        
173
        return $this->related_data;
4✔
174
    }
175
    
176
    /**
177
     * Get data for this record that does not belong to any of it's table columns and is not related data.
178
     * 
179
     * @return array reference to the data for this record (not from any actual db column and not related data).
180
     */
181
    public function &getNonTableColAndNonRelatedDataByRef(): array {
182
        
183
        return $this->non_table_col_and_non_related_data;
4✔
184
    }
185

186
    /**
187
     * Set relation data for this record.
188
     * 
189
     * @param string $key relation name
190
     * @param mixed $value an array or record or collection containing related data
191
     * 
192
     * @throws \GDAO\Model\RecordRelationWithSameNameAsAnExistingDBTableColumnNameException
193
     */
194
    public function setRelatedData(string $key, array|\GDAO\Model\RecordInterface|\GDAO\Model\CollectionInterface $value): static {
195
        
196
        $my_model = $this->getModel();
160✔
197
        $table_cols = $my_model->getTableColNames();
160✔
198
        
199
        if( in_array($key, $table_cols) ) {
160✔
200
            
201
            //Error trying to add a relation whose name collides with an actual
202
            //name of a column in the db table associated with this record's model.
203
            $msg = sprintf("ERROR: You cannont add a relationship with the name '%s' ", $key)
4✔
204
                 . " to the record (".$this::class."). The database table "
4✔
205
                 . sprintf(" '%s' associated with the ", $my_model->getTableName())
4✔
206
                 . " record's model (".$my_model::class.") already contains"
4✔
207
                 . " a column with the same name."
4✔
208
                 . PHP_EOL . $this::class . '::' . __FUNCTION__ . '(...).' 
4✔
209
                 . PHP_EOL;
4✔
210
                 
211
            throw new \GDAO\Model\RecordRelationWithSameNameAsAnExistingDBTableColumnNameException($msg);
4✔
212
        }
213
        
214
        //We're safe, set the related data.
215
        $this->related_data[$key] = $value;
156✔
216
        
217
        return $this;
156✔
218
    }
219
    
220
    /**
221
     * Get the model object that saves and reads data to and from the db on 
222
     * behalf of this record
223
     */
224
    public function getModel(): \GDAO\Model {
225
        
226
        return $this->model;
656✔
227
    }
228
    
229
    /**
230
     * @return string name of the primary-key column of the db table this record belongs to
231
     */
232
    public function getPrimaryCol(): string {
233

234
        return $this->getModel()->getPrimaryCol();
112✔
235
    }
236

237
    /**
238
     * @return mixed the value stored in the primary-key column for this record.
239
     */
240
    public function getPrimaryVal(): mixed {
241

242
        return $this->{$this->getPrimaryCol()};
108✔
243
    }
244
    
245
    /**
246
     * Set the \GDAO\Model object for this record
247
     * 
248
     * @param \GDAO\Model $model A model object that will be used by this record to communicate with the DB
249
     */
250
    public function setModel(\GDAO\Model $model): static {
251
        
252
        $this->model = $model;
656✔
253
        
254
        return $this;
656✔
255
    }
256
    
257
    /**
258
     * Get all the data and property (name & value pairs) for this record.
259
     * 
260
     * @return array of all data & property (name & value pairs) for this record.
261
     */
262
    public function toArray(): array {
263

264
        $array = get_object_vars($this);
40✔
265
        
266
        // remove the model object, it can be fetched via $this->getModel() if needed
267
        unset($array['model']);
40✔
268
        
269
        return $array;
40✔
270
    }
271
    
272
    //Interface Methods
273

274
    /**
275
     * ArrayAccess: does the requested key exist?
276
     * 
277
     * @param string $key The requested key.
278
     */
279
    public function offsetExists($key): bool {
280

281
        return $this->__isset($key);
52✔
282
    }
283

284
    /**
285
     * ArrayAccess: get a key value.
286
     * 
287
     * @param string $key The requested key.
288
     */
289
    public function offsetGet($key): mixed {
290

291
        return $this->__get($key);
128✔
292
    }
293

294
    /**
295
     * ArrayAccess: set a key value.
296
     * 
297
     * @param string $key The requested key.
298
     * 
299
     * @param string $val The value to set it to.
300
     */
301
    public function offsetSet($key, $val): void {
302

303
        $this->__set($key, $val);
16✔
304
    }
305

306
    /**
307
     * ArrayAccess: unset a key.
308
     * 
309
     * @param string $key The requested key.
310
     */
311
    public function offsetUnset($key): void {
312

313
        $this->__unset($key);
44✔
314
    }
315

316
    /**
317
     * Countable: how many keys are there?
318
     */
319
    public function count(): int {
320

321
        return count($this->data);
32✔
322
    }
323

324
    public function getIterator(): \ArrayIterator {
325

326
        return new \ArrayIterator($this->data + $this->related_data + $this->non_table_col_and_non_related_data);
16✔
327
    }
328
    
329
    //Magic Methods
330
    
331
    /**
332
     * Gets a data value.
333
     * 
334
     * @param string $key The requested data key.
335
     * 
336
     * @return mixed The data value.
337
     */
338
    public function __get($key): mixed {
339

340
        if ( array_key_exists($key, $this->data) ) {
256✔
341
            
342
            return $this->data[$key];
244✔
343
            
344
        } elseif ( array_key_exists($key, $this->related_data) ) {
212✔
345

346
            return $this->related_data[$key];
160✔
347
            
348
        } elseif ( array_key_exists($key, $this->non_table_col_and_non_related_data) ) { 
164✔
349
            
350
            return $this->non_table_col_and_non_related_data[$key];
36✔
351
            
352
        } elseif ( 
353
            $this->getModel() instanceof \GDAO\Model 
152✔
354
            && in_array($key, $this->getModel()->getTableColNames())
152✔
355
        ) {
356
            //$key is a valid db column in the db table assoiciated with this 
357
            //model's record. Initialize it to a null value since it has not 
358
            //yet been set.
359
            $this->data[$key] = null;
88✔
360
            
361
            return $this->data[$key];
88✔
362
            
363
        } elseif(
364
            $this->getModel() instanceof \GDAO\Model 
120✔
365
            && in_array($key, $this->getModel()->getRelationNames()) 
120✔
366
        ) {
367
            if($this->data !== []) {
104✔
368
                //$key is a valid relation name in the model for this record but the 
369
                //related data needs to be loaded for this particular record.
370
                /** @psalm-suppress UndefinedMethod */
371
                $this->getModel()->loadRelationshipData($key, $this, true, true);
96✔
372
                
373
            } else {
374
               
375
                // $this->related_data[$key] === [], meaning we can't fetch related data
376
                $this->related_data[$key] = null;
16✔
377
            }
378
            
379
            //return loaded data
380
            return $this->related_data[$key];
104✔
381
            
382
        } else {
383

384
            //$key is not a valid db column name or relation name.
385
            $msg = sprintf("Property '%s' does not exist in ", $key) 
24✔
386
                   . $this::class . PHP_EOL . $this->__toString();
24✔
387
            
388
            throw new \LeanOrm\Exceptions\Model\NoSuchPropertyForRecordException($msg);
24✔
389
        }
390
    }
391
    
392
    /**
393
     * Does a certain key exist in the data?
394
     * 
395
     * Note that this is slightly different from normal PHP isset(); it will
396
     * say the key is set, even if the key value is null or otherwise empty.
397
     * 
398
     * @param string $key The requested data key.
399
     */
400
    public function __isset($key): bool {
401
        
402
        try { $this->$key;  } //access the property first to make sure the data is loaded
56✔
403
        catch ( \Exception ) {  } //do nothing if exception was thrown
16✔
404
        
405
        return array_key_exists($key, $this->data) 
56✔
406
            || array_key_exists($key, $this->related_data)
56✔
407
            || array_key_exists($key, $this->non_table_col_and_non_related_data);
56✔
408
    }
409
    
410
    /**
411
     * Get the string representation of all the data and property 
412
     * (name & value pairs) for this record.
413
     * 
414
     * @return string string representation of all the data & property 
415
     *                (name & value pairs) for this record.
416
     */
417
    public function __toString(): string {
418
        
419
        return var_export($this->toArray(), true);
28✔
420
    }
421
    
422
    protected function injectData(\GDAO\Model\RecordInterface|array $data_2_load, array $cols_2_load = []): static {
423
        
424
        if (
425
            $data_2_load instanceof \GDAO\Model\RecordInterface
656✔
426
            && $data_2_load->getModel()->getTableName() !== $this->getModel()->getTableName()
656✔
427
        ) {
428
            //Cannot load data
429
            //2 records whose models are associated with different db tables.
430
            //Can't load data, schemas don't match.
431
            $msg = "ERROR: Can't load data from an instance of '".$data_2_load::class
8✔
432
                   . "' into an instance of '".$this::class."'. Their models"
8✔
433
                   . "'".$data_2_load->getModel()::class."' and '"
8✔
434
                   . $this->getModel()::class."' are associated with different"
8✔
435
                   . " db tables ('".$data_2_load->getModel()->getTableName() 
8✔
436
                   ."' and '". $this->getModel()->getTableName()."')."
8✔
437
                   . PHP_EOL . "Unloaded Data:"
8✔
438
                   . PHP_EOL . var_export($data_2_load->getData(), true)  . PHP_EOL;
8✔
439
            
440
            throw new \GDAO\Model\LoadingDataFromInvalidSourceIntoRecordException($msg);
8✔
441
        }
442

443
        $table_col_names_4_my_model = $this->getModel()->getTableColNames();
656✔
444
        
445
        if ( $cols_2_load === [] ) {
656✔
446

447
            if ( is_array($data_2_load) && $data_2_load !== [] ) {
656✔
448

449
                /** @psalm-suppress MixedAssignment */
450
                foreach( $data_2_load as $col_name => $value_2_load ) {
512✔
451
                    
452
                    if ( in_array($col_name, $table_col_names_4_my_model) ) {
512✔
453
                        
454
                        $this->data[$col_name] = $value_2_load;
512✔
455
                        
456
                    } else {
457
                        
458
                        $this->non_table_col_and_non_related_data[$col_name] = $value_2_load;
284✔
459
                    }
460
                }
461
                
462
            } elseif ($data_2_load instanceof \GDAO\Model\RecordInterface) {
224✔
463

464
                $this->data = $data_2_load->getData();
8✔
465
                $this->non_table_col_and_non_related_data = $data_2_load->getNonTableColAndNonRelatedData();
8✔
466
            }
467
            
468
        } else {
469

470
            /** @psalm-suppress MixedAssignment */
471
            foreach ( $cols_2_load as $col_name ) {
8✔
472

473
                /**
474
                 * @psalm-suppress MixedArgument
475
                 * @psalm-suppress MixedArrayOffset
476
                 */
477
                if (
UNCOV
478
                    (
479
                        is_array($data_2_load)
8✔
480
                        && $data_2_load !== []
8✔
481
                        && array_key_exists($col_name, $data_2_load)
8✔
482
                    ) 
483
                    || (
484
                        $data_2_load instanceof \GDAO\Model\RecordInterface 
8✔
485
                        && isset($data_2_load[$col_name])
8✔
486
                    )
487
                ) {
488
                    if ( in_array($col_name, $table_col_names_4_my_model) ) {
8✔
489

490
                        $this->data[$col_name] = $data_2_load[$col_name];
8✔
491

492
                    } else {
493

494
                        $this->non_table_col_and_non_related_data[$col_name] = $data_2_load[$col_name];
8✔
495
                    }
496
                }
497
            } // foreach ( $cols_2_load as $col_name )
498
        }// elseif ( is_array($cols_2_load) && $cols_2_load !== [] )
499
        
500
        return $this;
656✔
501
    }
502

503
    ///////////////////////////////
504
    // Metadata retreiving methods.
505
    ///////////////////////////////
506

507
    /**
508
     * @psalm-suppress PossiblyUnusedMethod
509
     */
510
    public function isAutoIncrementingField(string $fieldName): bool {
511

512
        /** @psalm-suppress UndefinedMethod */
513
        return $this->getModel()->isAutoIncrementingField($fieldName);
8✔
514
    }
515

516
    /**
517
     * @psalm-suppress PossiblyUnusedMethod
518
     */
519
    public function isPrimaryKeyField(string $fieldName): bool {
520

521
        /** @psalm-suppress UndefinedMethod */
522
        return $this->getModel()->isPrimaryKeyField($fieldName);
8✔
523
    }
524

525
    /**
526
     * @psalm-suppress PossiblyUnusedMethod
527
     */
528
    public function isRequiredField(string $fieldName): bool {
529

530
        /** @psalm-suppress UndefinedMethod */
531
        return $this->getModel()->isRequiredField($fieldName);
8✔
532
    }
533

534
    /**
535
     * @psalm-suppress PossiblyUnusedMethod
536
     */
537
    public function getFieldLength(string $fieldName): ?int {
538

539
        /** @psalm-suppress UndefinedMethod */
540
        return $this->getModel()->getFieldLength($fieldName);
8✔
541
    }
542

543
    /**
544
     * @psalm-suppress PossiblyUnusedMethod
545
     */
546
    public function getFieldDefaultValue(string $fieldName): mixed {
547

548
        /** @psalm-suppress UndefinedMethod */
549
        return $this->getModel()->getFieldDefaultValue($fieldName);
8✔
550
    }
551

552
    /**
553
     * @psalm-suppress PossiblyUnusedMethod
554
     */
555
    public function getFieldMetadata(string $fieldName): array {
556

557
        /** @psalm-suppress UndefinedMethod */
558
        return $this->getModel()->getFieldMetadata($fieldName);
8✔
559
    }
560
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc