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

codeigniter4 / CodeIgniter4 / 29123888990

10 Jul 2026 09:10PM UTC coverage: 88.709%. First build
29123888990

Pull #10402

github

web-flow
Merge 3eceb0c92 into 0c101bd61
Pull Request #10402: fix: convert DateTimeInterface objects to strings in Entity::toRawArray

31 of 43 new or added lines in 2 files covered. (72.09%)

22329 of 25171 relevant lines covered (88.71%)

213.8 hits per line

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

93.75
/system/Entity/Entity.php
1
<?php
2

3
declare(strict_types=1);
4

5
/**
6
 * This file is part of CodeIgniter 4 framework.
7
 *
8
 * (c) CodeIgniter Foundation <admin@codeigniter.com>
9
 *
10
 * For the full copyright and license information, please view
11
 * the LICENSE file that was distributed with this source code.
12
 */
13

14
namespace CodeIgniter\Entity;
15

16
use BackedEnum;
17
use CodeIgniter\DataCaster\DataCaster;
18
use CodeIgniter\Entity\Cast\ArrayCast;
19
use CodeIgniter\Entity\Cast\BooleanCast;
20
use CodeIgniter\Entity\Cast\CSVCast;
21
use CodeIgniter\Entity\Cast\DatetimeCast;
22
use CodeIgniter\Entity\Cast\EnumCast;
23
use CodeIgniter\Entity\Cast\FloatCast;
24
use CodeIgniter\Entity\Cast\IntBoolCast;
25
use CodeIgniter\Entity\Cast\IntegerCast;
26
use CodeIgniter\Entity\Cast\JsonCast;
27
use CodeIgniter\Entity\Cast\ObjectCast;
28
use CodeIgniter\Entity\Cast\StringCast;
29
use CodeIgniter\Entity\Cast\TimestampCast;
30
use CodeIgniter\Entity\Cast\URICast;
31
use CodeIgniter\Entity\Exceptions\CastException;
32
use CodeIgniter\I18n\Time;
33
use DateTimeInterface;
34
use Exception;
35
use JsonSerializable;
36
use Traversable;
37
use UnitEnum;
38

39
/**
40
 * Entity encapsulation, for use with CodeIgniter\Model
41
 *
42
 * @see \CodeIgniter\Entity\EntityTest
43
 */
44
class Entity implements JsonSerializable
45
{
46
    /**
47
     * Maps names used in sets and gets against unique
48
     * names within the class, allowing independence from
49
     * database column names.
50
     *
51
     * Example:
52
     *  $datamap = [
53
     *      'class_property_name' => 'db_column_name'
54
     *  ];
55
     *
56
     * @var array<string, string>
57
     */
58
    protected $datamap = [];
59

60
    /**
61
     * The date fields.
62
     *
63
     * @var list<string>
64
     */
65
    protected $dates = [
66
        'created_at',
67
        'updated_at',
68
        'deleted_at',
69
    ];
70

71
    /**
72
     * Array of field names and the type of value to cast them as when
73
     * they are accessed.
74
     *
75
     * @var array<string, string>
76
     */
77
    protected $casts = [];
78

79
    /**
80
     * Custom convert handlers.
81
     *
82
     * @var array<string, string>
83
     */
84
    protected $castHandlers = [];
85

86
    /**
87
     * Default convert handlers.
88
     *
89
     * @var array<string, string>
90
     */
91
    private array $defaultCastHandlers = [
92
        'array'     => ArrayCast::class,
93
        'bool'      => BooleanCast::class,
94
        'boolean'   => BooleanCast::class,
95
        'csv'       => CSVCast::class,
96
        'datetime'  => DatetimeCast::class,
97
        'double'    => FloatCast::class,
98
        'enum'      => EnumCast::class,
99
        'float'     => FloatCast::class,
100
        'int'       => IntegerCast::class,
101
        'integer'   => IntegerCast::class,
102
        'int-bool'  => IntBoolCast::class,
103
        'json'      => JsonCast::class,
104
        'object'    => ObjectCast::class,
105
        'string'    => StringCast::class,
106
        'timestamp' => TimestampCast::class,
107
        'uri'       => URICast::class,
108
    ];
109

110
    /**
111
     * Holds the current values of all class vars.
112
     *
113
     * @var array<string, mixed>
114
     */
115
    protected $attributes = [];
116

117
    /**
118
     * Holds original copies of all class vars so we can determine
119
     * what's actually been changed and not accidentally write
120
     * nulls where we shouldn't.
121
     *
122
     * @var array<string, mixed>
123
     */
124
    protected $original = [];
125

126
    /**
127
     * The data caster.
128
     */
129
    protected ?DataCaster $dataCaster = null;
130

131
    /**
132
     * Holds info whenever properties have to be casted.
133
     */
134
    private bool $_cast = true;
135

136
    /**
137
     * Indicates whether all attributes are scalars (for optimization).
138
     */
139
    private bool $_onlyScalars = true;
140

141
    /**
142
     * Allows filling in Entity parameters during construction.
143
     *
144
     * @param array<string, mixed> $data
145
     */
146
    public function __construct(?array $data = null)
147
    {
148
        $this->dataCaster = $this->dataCaster();
183✔
149

150
        $this->syncOriginal();
183✔
151

152
        $this->fill($data);
183✔
153
    }
154

155
    /**
156
     * Takes an array of key/value pairs and sets them as class
157
     * properties, using any `setCamelCasedProperty()` methods
158
     * that may or may not exist.
159
     *
160
     * @param array<string, mixed> $data
161
     *
162
     * @return $this
163
     */
164
    public function fill(?array $data = null)
165
    {
166
        if (! is_array($data)) {
183✔
167
            return $this;
176✔
168
        }
169

170
        foreach ($data as $key => $value) {
19✔
171
            $this->__set($key, $value);
19✔
172
        }
173

174
        return $this;
19✔
175
    }
176

177
    /**
178
     * General method that will return all public and protected values
179
     * of this entity as an array. All values are accessed through the
180
     * __get() magic method so will have any casts, etc applied to them.
181
     *
182
     * @param bool $onlyChanged If true, only return values that have changed since object creation.
183
     * @param bool $cast        If true, properties will be cast.
184
     * @param bool $recursive   If true, inner entities will be cast as array as well.
185
     *
186
     * @return array<string, mixed>
187
     */
188
    public function toArray(bool $onlyChanged = false, bool $cast = true, bool $recursive = false): array
189
    {
190
        $originalCast = $this->_cast;
15✔
191
        $this->_cast  = $cast;
15✔
192

193
        $keys = array_filter(array_keys($this->attributes), static fn ($key): bool => ! str_starts_with($key, '_'));
15✔
194

195
        if (is_array($this->datamap)) {
15✔
196
            $keys = array_unique(
15✔
197
                [...array_diff($keys, $this->datamap), ...array_keys($this->datamap)],
15✔
198
            );
15✔
199
        }
200

201
        $return = [];
15✔
202

203
        // Loop over the properties, to allow magic methods to do their thing.
204
        foreach ($keys as $key) {
15✔
205
            if ($onlyChanged && ! $this->hasChanged($key)) {
15✔
206
                continue;
2✔
207
            }
208

209
            $return[$key] = $this->__get($key);
15✔
210

211
            if ($recursive) {
15✔
212
                if ($return[$key] instanceof self) {
1✔
213
                    $return[$key] = $return[$key]->toArray($onlyChanged, $cast, $recursive);
1✔
214
                } elseif (is_callable([$return[$key], 'toArray'])) {
1✔
215
                    $return[$key] = $return[$key]->toArray();
×
216
                }
217
            }
218
        }
219

220
        $this->_cast = $originalCast;
15✔
221

222
        return $return;
15✔
223
    }
224

225
    /**
226
     * Returns the raw values of the current attributes.
227
     *
228
     * @param bool $onlyChanged If true, only return values that have changed since object creation.
229
     * @param bool $recursive   If true, inner entities will be cast as array as well.
230
     *
231
     * @return array<string, mixed>
232
     */
233
    public function toRawArray(bool $onlyChanged = false, bool $recursive = false): array
234
    {
235
        $convert = static function ($value) use (&$convert, $recursive) {
55✔
236
            if (! $recursive) {
55✔
237
                return $value;
25✔
238
            }
239

240
            if ($value instanceof self) {
34✔
241
                // Always output full array for nested entities
242
                return $value->toRawArray(false, true);
4✔
243
            }
244

245
            if (is_array($value)) {
34✔
246
                $result = [];
1✔
247

248
                foreach ($value as $k => $v) {
1✔
249
                    $result[$k] = $convert($v);
1✔
250
                }
251

252
                return $result;
1✔
253
            }
254

255
            if (is_object($value) && is_callable([$value, 'toRawArray'])) {
34✔
256
                return $value->toRawArray();
×
257
            }
258

259
            return $value;
34✔
260
        };
55✔
261

262
        // When returning everything
263
        if (! $onlyChanged) {
55✔
264
            $result = array_map($convert, $this->attributes);
46✔
265
        } else {
266
            // When filtering by changed values only
267
            $result = [];
18✔
268

269
            foreach ($this->attributes as $key => $value) {
18✔
270
                // Special handling for arrays of entities in recursive mode
271
                // Skip hasChanged() and do per-entity comparison directly
272
                if ($recursive && is_array($value) && $this->containsOnlyEntities($value)) {
18✔
273
                    $originalValue = $this->original[$key] ?? null;
4✔
274

275
                    if (! is_string($originalValue)) {
4✔
276
                        // No original or invalid format, export all entities
NEW
277
                        $converted = [];
×
278

NEW
279
                        foreach ($value as $idx => $item) {
×
NEW
280
                            $converted[$idx] = $item->toRawArray(false, true);
×
281
                        }
NEW
282
                        $result[$key] = $converted;
×
283

NEW
284
                        continue;
×
285
                    }
286

287
                    // Decode original array structure for per-entity comparison
288
                    $originalArray = json_decode($originalValue, true);
4✔
289
                    $converted     = [];
4✔
290

291
                    foreach ($value as $idx => $item) {
4✔
292
                        // Compare current entity against its original state
293
                        $currentNormalized  = $this->normalizeValue($item);
4✔
294
                        $originalNormalized = $originalArray[$idx] ?? null;
4✔
295

296
                        // Only include if changed, new, or can't determine
297
                        if ($originalNormalized === null || $currentNormalized !== $originalNormalized) {
4✔
298
                            $converted[$idx] = $item->toRawArray(false, true);
3✔
299
                        }
300
                    }
301

302
                    // Only include this property if at least one entity changed
303
                    if ($converted !== []) {
4✔
304
                        $result[$key] = $converted;
3✔
305
                    }
306

307
                    continue;
4✔
308
                }
309

310
                // For all other cases, use hasChanged()
311
                if (! $this->hasChanged($key)) {
18✔
312
                    continue;
16✔
313
                }
314

315
                if ($recursive) {
14✔
316
                    // Special handling for arrays (mixed or not all entities)
317
                    if (is_array($value)) {
9✔
NEW
318
                        $converted = [];
×
319

NEW
320
                        foreach ($value as $idx => $item) {
×
NEW
321
                            $converted[$idx] = $item instanceof self ? $item->toRawArray(false, true) : $convert($item);
×
322
                        }
NEW
323
                        $result[$key] = $converted;
×
324

NEW
325
                        continue;
×
326
                    }
327

328
                    // default recursive conversion
329
                    $result[$key] = $convert($value);
9✔
330

331
                    continue;
9✔
332
                }
333

334
                // non-recursive changed value
335
                $result[$key] = $convert($value);
6✔
336
            }
337
        }
338

339
        // Convert DateTime objects to string for user-facing calls
340
        if (! $recursive) {
55✔
341
            $result = array_map(static function ($value) {
25✔
342
                if ($value instanceof DateTimeInterface) {
25✔
343
                    return method_exists($value, '__toString') ? (string) $value : $value->format('Y-m-d H:i:s');
5✔
344
                }
345

346
                return $value;
24✔
347
            }, $result);
25✔
348
        }
349

350
        return $result;
55✔
351
    }
352

353
    /**
354
     * Ensures our "original" values match the current values.
355
     *
356
     * Objects and arrays are normalized and JSON-encoded for reliable change detection,
357
     * while scalars are stored as-is for performance.
358
     *
359
     * @return $this
360
     */
361
    public function syncOriginal()
362
    {
363
        $this->original     = [];
183✔
364
        $this->_onlyScalars = true;
183✔
365

366
        foreach ($this->attributes as $key => $value) {
183✔
367
            if (is_object($value) || is_array($value)) {
160✔
368
                $this->original[$key] = json_encode($this->normalizeValue($value), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
43✔
369
                $this->_onlyScalars   = false;
43✔
370
            } else {
371
                $this->original[$key] = $value;
153✔
372
            }
373
        }
374

375
        return $this;
183✔
376
    }
377

378
    /**
379
     * Checks a property to see if it has changed since the entity
380
     * was created. Or, without a parameter, checks if any
381
     * properties have changed.
382
     */
383
    public function hasChanged(?string $key = null): bool
384
    {
385
        // If no parameter was given then check all attributes
386
        if ($key === null) {
60✔
387
            if ($this->_onlyScalars) {
6✔
388
                return $this->original !== $this->attributes;
5✔
389
            }
390

391
            foreach (array_keys($this->attributes) as $attributeKey) {
1✔
392
                if ($this->hasChanged($attributeKey)) {
1✔
393
                    return true;
1✔
394
                }
395
            }
396

397
            return false;
1✔
398
        }
399

400
        $dbColumn = $this->mapProperty($key);
56✔
401

402
        // Key doesn't exist in either
403
        if (! array_key_exists($dbColumn, $this->original) && ! array_key_exists($dbColumn, $this->attributes)) {
56✔
404
            return false;
1✔
405
        }
406

407
        // It's a new element
408
        if (! array_key_exists($dbColumn, $this->original) && array_key_exists($dbColumn, $this->attributes)) {
55✔
409
            return true;
4✔
410
        }
411

412
        // It was removed
413
        if (array_key_exists($dbColumn, $this->original) && ! array_key_exists($dbColumn, $this->attributes)) {
53✔
414
            return true;
1✔
415
        }
416

417
        $originalValue = $this->original[$dbColumn];
52✔
418
        $currentValue  = $this->attributes[$dbColumn];
52✔
419

420
        // If original is a string, it was JSON-encoded (object or array)
421
        if (is_string($originalValue) && (is_object($currentValue) || is_array($currentValue))) {
52✔
422
            return $originalValue !== json_encode($this->normalizeValue($currentValue), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
30✔
423
        }
424

425
        // For scalars, use direct comparison
426
        return $originalValue !== $currentValue;
26✔
427
    }
428

429
    /**
430
     * Checks if an array contains only Entity instances.
431
     * This allows optimization for per-entity change tracking.
432
     *
433
     * @param array<array-key, mixed> $data
434
     */
435
    private function containsOnlyEntities(array $data): bool
436
    {
437
        if ($data === []) {
4✔
438
            return false;
×
439
        }
440

441
        foreach ($data as $item) {
4✔
442
            if (! $item instanceof self) {
4✔
443
                return false;
×
444
            }
445
        }
446

447
        return true;
4✔
448
    }
449

450
    /**
451
     * Recursively normalize a value for comparison.
452
     * Converts objects and arrays to a JSON-encodable format.
453
     */
454
    private function normalizeValue(mixed $data): mixed
455
    {
456
        if (is_array($data)) {
43✔
457
            $normalized = [];
32✔
458

459
            foreach ($data as $key => $value) {
32✔
460
                $normalized[$key] = $this->normalizeValue($value);
30✔
461
            }
462

463
            return $normalized;
32✔
464
        }
465

466
        if (is_object($data)) {
42✔
467
            // Check for Entity instance (use raw values, recursive)
468
            if ($data instanceof self) {
37✔
469
                $objectData = $data->toRawArray(false, true);
6✔
470
            } elseif ($data instanceof UnitEnum) {
31✔
471
                return [
8✔
472
                    '__class' => $data::class,
8✔
473
                    '__enum'  => $data instanceof BackedEnum ? $data->value : $data->name,
8✔
474
                ];
8✔
475
            } elseif ($data instanceof JsonSerializable) {
23✔
476
                $objectData = $data->jsonSerialize();
2✔
477
            } elseif (method_exists($data, 'toArray')) {
21✔
478
                $objectData = $data->toArray();
3✔
479
            } elseif ($data instanceof Traversable) {
18✔
480
                $objectData = iterator_to_array($data);
1✔
481
            } elseif ($data instanceof DateTimeInterface) {
18✔
482
                return [
9✔
483
                    '__class'    => $data::class,
9✔
484
                    '__datetime' => $data->format(DATE_RFC3339_EXTENDED),
9✔
485
                ];
9✔
486
            } else {
487
                $objectData = get_object_vars($data);
9✔
488

489
                // Fallback for value objects with __toString()
490
                // when properties are not accessible
491
                if ($objectData === [] && method_exists($data, '__toString')) {
9✔
492
                    return [
1✔
493
                        '__class'  => $data::class,
1✔
494
                        '__string' => (string) $data,
1✔
495
                    ];
1✔
496
                }
497
            }
498

499
            return [
19✔
500
                '__class' => $data::class,
19✔
501
                '__data'  => $this->normalizeValue($objectData),
19✔
502
            ];
19✔
503
        }
504

505
        // Return scalars and null as-is
506
        return $data;
30✔
507
    }
508

509
    /**
510
     * Set raw data array without any mutations.
511
     *
512
     * @param array<string, mixed> $data
513
     *
514
     * @return $this
515
     */
516
    public function injectRawData(array $data)
517
    {
518
        $this->attributes = $data;
29✔
519

520
        $this->syncOriginal();
29✔
521

522
        return $this;
29✔
523
    }
524

525
    /**
526
     * Checks the datamap to see if this property name is being mapped,
527
     * and returns the DB column name, if any, or the original property name.
528
     *
529
     * @return string Database column name.
530
     */
531
    protected function mapProperty(string $key)
532
    {
533
        if ($this->datamap === []) {
170✔
534
            return $key;
126✔
535
        }
536

537
        if (array_key_exists($key, $this->datamap) && $this->datamap[$key] !== '') {
44✔
538
            return $this->datamap[$key];
19✔
539
        }
540

541
        return $key;
37✔
542
    }
543

544
    /**
545
     * Converts the given string|timestamp|DateTimeInterface instance
546
     * into the "CodeIgniter\I18n\Time" object.
547
     *
548
     * @param DateTimeInterface|float|int|string $value
549
     *
550
     * @return Time
551
     *
552
     * @throws Exception
553
     */
554
    protected function mutateDate($value)
555
    {
556
        return DatetimeCast::get($value);
38✔
557
    }
558

559
    /**
560
     * Provides the ability to cast an item as a specific data type.
561
     * Add ? at the beginning of the type (i.e. ?string) to get `null`
562
     * instead of casting $value when $value is null.
563
     *
564
     * @param bool|float|int|string|null $value     Attribute value
565
     * @param string                     $attribute Attribute name
566
     * @param string                     $method    Allowed to "get" and "set"
567
     *
568
     * @return mixed
569
     *
570
     * @throws CastException
571
     */
572
    protected function castAs($value, string $attribute, string $method = 'get')
573
    {
574
        if ($this->dataCaster() instanceof DataCaster) {
161✔
575
            return $this->dataCaster
53✔
576
                // @TODO if $casts is readonly, we don't need the setTypes() method.
53✔
577
                ->setTypes($this->casts)
53✔
578
                ->castAs($value, $attribute, $method);
53✔
579
        }
580

581
        return $value;
108✔
582
    }
583

584
    /**
585
     * Returns a DataCaster instance when casts are defined.
586
     * If no casts are configured, no DataCaster is created and null is returned.
587
     */
588
    protected function dataCaster(): ?DataCaster
589
    {
590
        if ($this->casts === []) {
183✔
591
            $this->dataCaster = null;
134✔
592

593
            return null;
134✔
594
        }
595

596
        if (! $this->dataCaster instanceof DataCaster) {
53✔
597
            $this->dataCaster = new DataCaster(
53✔
598
                array_merge($this->defaultCastHandlers, $this->castHandlers),
53✔
599
                null,
53✔
600
                null,
53✔
601
                false,
53✔
602
            );
53✔
603
        }
604

605
        return $this->dataCaster;
53✔
606
    }
607

608
    /**
609
     * Support for json_encode().
610
     *
611
     * @return array<string, mixed>
612
     */
613
    public function jsonSerialize(): array
614
    {
615
        return $this->toArray();
1✔
616
    }
617

618
    /**
619
     * Change the value of the private $_cast property.
620
     *
621
     * @return bool|Entity
622
     */
623
    public function cast(?bool $cast = null)
624
    {
625
        if ($cast === null) {
14✔
626
            return $this->_cast;
11✔
627
        }
628

629
        $this->_cast = $cast;
13✔
630

631
        return $this;
13✔
632
    }
633

634
    /**
635
     * Magic method to all protected/private class properties to be
636
     * easily set, either through a direct access or a
637
     * `setCamelCasedProperty()` method.
638
     *
639
     * Examples:
640
     *  $this->my_property = $p;
641
     *  $this->setMyProperty() = $p;
642
     *
643
     * @param mixed $value
644
     *
645
     * @return void
646
     *
647
     * @throws Exception
648
     */
649
    public function __set(string $key, $value = null)
650
    {
651
        $dbColumn = $this->mapProperty($key);
139✔
652

653
        // Check if the field should be mutated into a date
654
        if (in_array($dbColumn, $this->dates, true)) {
139✔
655
            $value = $this->mutateDate($value);
23✔
656
        }
657

658
        $value = $this->castAs($value, $dbColumn, 'set');
139✔
659

660
        // if a setter method exists for this key, use that method to
661
        // insert this value. should be outside $isNullable check,
662
        // so maybe wants to do sth with null value automatically
663
        $method = 'set' . str_replace(' ', '', ucwords(str_replace(['-', '_'], ' ', $dbColumn)));
131✔
664

665
        // If a "`_set` + $key" method exists, it is a setter.
666
        if (method_exists($this, '_' . $method)) {
131✔
667
            $this->{'_' . $method}($value);
1✔
668

669
            return;
1✔
670
        }
671

672
        // If a "`set` + $key" method exists, it is also a setter.
673
        if (method_exists($this, $method)) {
130✔
674
            $this->{$method}($value);
12✔
675

676
            return;
12✔
677
        }
678

679
        // Otherwise, just the value. This allows for creation of new
680
        // class properties that are undefined, though they cannot be
681
        // saved. Useful for grabbing values through joins, assigning
682
        // relationships, etc.
683
        $this->attributes[$dbColumn] = $value;
121✔
684
    }
685

686
    /**
687
     * Magic method to allow retrieval of protected and private class properties
688
     * either by their name, or through a `getCamelCasedProperty()` method.
689
     *
690
     * Examples:
691
     *  $p = $this->my_property
692
     *  $p = $this->getMyProperty()
693
     *
694
     * @return mixed
695
     *
696
     * @throws Exception
697
     */
698
    public function __get(string $key)
699
    {
700
        $dbColumn = $this->mapProperty($key);
95✔
701

702
        $result = null;
95✔
703

704
        // Convert to CamelCase for the method
705
        $method = 'get' . str_replace(' ', '', ucwords(str_replace(['-', '_'], ' ', $dbColumn)));
95✔
706

707
        // if a getter method exists for this key,
708
        // use that method to insert this value.
709
        if (method_exists($this, '_' . $method)) {
95✔
710
            // If a "`_get` + $key" method exists, it is a getter.
711
            $result = $this->{'_' . $method}();
1✔
712
        } elseif (method_exists($this, $method)) {
94✔
713
            // If a "`get` + $key" method exists, it is also a getter.
714
            $result = $this->{$method}();
11✔
715
        }
716

717
        // Otherwise return the protected property
718
        // if it exists.
719
        elseif (array_key_exists($dbColumn, $this->attributes)) {
91✔
720
            $result = $this->attributes[$dbColumn];
91✔
721
        }
722

723
        // Do we need to mutate this into a date?
724
        if (in_array($dbColumn, $this->dates, true)) {
95✔
725
            $result = $this->mutateDate($result);
21✔
726
        }
727
        // Or cast it as something?
728
        elseif ($this->_cast) {
88✔
729
            $result = $this->castAs($result, $dbColumn);
88✔
730
        }
731

732
        return $result;
90✔
733
    }
734

735
    /**
736
     * Returns true if a property exists names $key, or a getter method
737
     * exists named like for __get().
738
     */
739
    public function __isset(string $key): bool
740
    {
741
        if ($this->isMappedDbColumn($key)) {
6✔
742
            return false;
2✔
743
        }
744

745
        $dbColumn = $this->mapProperty($key);
6✔
746

747
        $method = 'get' . str_replace(' ', '', ucwords(str_replace(['-', '_'], ' ', $dbColumn)));
6✔
748

749
        if (method_exists($this, $method)) {
6✔
750
            return true;
1✔
751
        }
752

753
        return isset($this->attributes[$dbColumn]);
6✔
754
    }
755

756
    /**
757
     * Unsets an attribute property.
758
     */
759
    public function __unset(string $key): void
760
    {
761
        if ($this->isMappedDbColumn($key)) {
4✔
762
            return;
1✔
763
        }
764

765
        $dbColumn = $this->mapProperty($key);
4✔
766

767
        unset($this->attributes[$dbColumn]);
4✔
768
    }
769

770
    /**
771
     * Whether this key is mapped db column name?
772
     */
773
    protected function isMappedDbColumn(string $key): bool
774
    {
775
        $dbColumn = $this->mapProperty($key);
8✔
776

777
        // The $key is a property name which has mapped db column name
778
        if ($key !== $dbColumn) {
8✔
779
            return false;
5✔
780
        }
781

782
        return $this->hasMappedProperty($key);
6✔
783
    }
784

785
    /**
786
     * Whether this key has mapped property?
787
     */
788
    protected function hasMappedProperty(string $key): bool
789
    {
790
        $property = array_search($key, $this->datamap, true);
6✔
791

792
        return $property !== false;
6✔
793
    }
794
}
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