Skip to content

Commit 865f797

Browse files
[10.x] Model::preventAccessingMissingAttributes() raises exception for enums & primitive castable attributes that were not retrieved (#49480)
* throw an exception if castable attribute was not retrieved * test name * only for primitive types + enums * style * formatting * formatting --------- Co-authored-by: Taylor Otwell <[email protected]>
1 parent 7e9e271 commit 865f797

File tree

3 files changed

+133
-0
lines changed

3 files changed

+133
-0
lines changed

src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2126,6 +2126,13 @@ protected function transformModelValue($key, $value)
21262126
// an appropriate native PHP type dependent upon the associated value
21272127
// given with the key in the pair. Dayle made this comment line up.
21282128
if ($this->hasCast($key)) {
2129+
if (static::preventsAccessingMissingAttributes() &&
2130+
! array_key_exists($key, $this->attributes) &&
2131+
($this->isEnumCastable($key) ||
2132+
in_array($this->getCastType($key), static::$primitiveCastTypes))) {
2133+
$this->throwMissingAttributeExceptionIfApplicable($key);
2134+
}
2135+
21292136
return $this->castAttribute($key, $value);
21302137
}
21312138

tests/Database/DatabaseEloquentModelTest.php

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
use DateTimeInterface;
88
use Exception;
99
use Foo\Bar\EloquentModelNamespacedStub;
10+
use Illuminate\Contracts\Database\Eloquent\Castable;
11+
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
1012
use Illuminate\Contracts\Database\Eloquent\CastsInboundAttributes;
1113
use Illuminate\Contracts\Encryption\Encrypter;
1214
use Illuminate\Contracts\Events\Dispatcher;
@@ -22,6 +24,7 @@
2224
use Illuminate\Database\Eloquent\Casts\AsEnumArrayObject;
2325
use Illuminate\Database\Eloquent\Casts\AsEnumCollection;
2426
use Illuminate\Database\Eloquent\Casts\AsStringable;
27+
use Illuminate\Database\Eloquent\Casts\Attribute;
2528
use Illuminate\Database\Eloquent\Collection;
2629
use Illuminate\Database\Eloquent\Concerns\HasUlids;
2730
use Illuminate\Database\Eloquent\Concerns\HasUuids;
@@ -2576,6 +2579,45 @@ public function testThrowsWhenAccessingMissingAttributes()
25762579
}
25772580
}
25782581

2582+
public function testThrowsWhenAccessingMissingAttributesWhichArePrimitiveCasts()
2583+
{
2584+
$originalMode = Model::preventsAccessingMissingAttributes();
2585+
Model::preventAccessingMissingAttributes();
2586+
2587+
$model = new EloquentModelWithPrimitiveCasts(['id' => 1]);
2588+
$model->exists = true;
2589+
2590+
$exceptionCount = 0;
2591+
$primitiveCasts = EloquentModelWithPrimitiveCasts::makePrimitiveCastsArray();
2592+
try {
2593+
try {
2594+
$this->assertEquals(null, $model->backed_enum);
2595+
} catch (MissingAttributeException) {
2596+
$exceptionCount++;
2597+
}
2598+
2599+
foreach($primitiveCasts as $key => $type) {
2600+
try {
2601+
$v = $model->{$key};
2602+
} catch (MissingAttributeException) {
2603+
$exceptionCount++;
2604+
}
2605+
}
2606+
2607+
$this->assertInstanceOf(Address::class, $model->address);
2608+
2609+
$this->assertEquals(1, $model->id);
2610+
$this->assertEquals('ok', $model->this_is_fine);
2611+
$this->assertEquals('ok', $model->this_is_also_fine);
2612+
2613+
// Primitive castables, enum castable
2614+
$expectedExceptionCount = count($primitiveCasts) + 1;
2615+
$this->assertEquals($expectedExceptionCount, $exceptionCount);
2616+
} finally {
2617+
Model::preventAccessingMissingAttributes($originalMode);
2618+
}
2619+
}
2620+
25792621
public function testUsesOverriddenHandlerWhenAccessingMissingAttributes()
25802622
{
25812623
$originalMode = Model::preventsAccessingMissingAttributes();
@@ -3349,3 +3391,70 @@ class CustomCollection extends BaseCollection
33493391
{
33503392
//
33513393
}
3394+
3395+
class EloquentModelWithPrimitiveCasts extends Model
3396+
{
3397+
public $fillable = ['id'];
3398+
3399+
public $casts = [
3400+
'backed_enum' => CastableBackedEnum::class,
3401+
'address' => Address::class,
3402+
];
3403+
3404+
public static function makePrimitiveCastsArray(): array
3405+
{
3406+
$toReturn = [];
3407+
3408+
foreach(static::$primitiveCastTypes as $index => $primitiveCastType) {
3409+
$toReturn['primitive_cast_' . $index] = $primitiveCastType;
3410+
}
3411+
3412+
return $toReturn;
3413+
}
3414+
3415+
public function __construct(array $attributes = [])
3416+
{
3417+
parent::__construct($attributes);
3418+
3419+
$this->mergeCasts(self::makePrimitiveCastsArray());
3420+
}
3421+
3422+
public function getThisIsFineAttribute($value) {
3423+
return 'ok';
3424+
}
3425+
3426+
public function thisIsAlsoFine(): Attribute
3427+
{
3428+
return Attribute::get(fn() => 'ok');
3429+
}
3430+
}
3431+
3432+
enum CastableBackedEnum: string
3433+
{
3434+
case Value1 = 'value1';
3435+
}
3436+
3437+
class Address implements Castable
3438+
{
3439+
public static function castUsing(array $arguments): CastsAttributes
3440+
{
3441+
return new class implements CastsAttributes
3442+
{
3443+
public function get(Model $model, string $key, mixed $value, array $attributes): Address
3444+
{
3445+
return new Address(
3446+
$attributes['address_line_one'],
3447+
$attributes['address_line_two']
3448+
);
3449+
}
3450+
3451+
public function set(Model $model, string $key, mixed $value, array $attributes): array
3452+
{
3453+
return [
3454+
'address_line_one' => $value->lineOne,
3455+
'address_line_two' => $value->lineTwo,
3456+
];
3457+
}
3458+
};
3459+
}
3460+
}

tests/Database/DatabaseEloquentWithCastsTest.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
namespace Illuminate\Tests\Database;
44

55
use Illuminate\Database\Capsule\Manager as DB;
6+
use Illuminate\Database\Eloquent\MissingAttributeException;
7+
use Illuminate\Database\Eloquent\Model;
68
use Illuminate\Database\Eloquent\Model as Eloquent;
79
use PHPUnit\Framework\TestCase;
810

@@ -76,6 +78,21 @@ public function testWithCreateOrFirst()
7678
$this->assertSame($time1->id, $time2->id);
7779
}
7880

81+
public function testThrowsExceptionIfCastableAttributeWasNotRetrievedAndPreventMissingAttributesIsEnabled()
82+
{
83+
Time::create(['time' => now()]);
84+
$originalMode = Model::preventsAccessingMissingAttributes();
85+
Model::preventAccessingMissingAttributes();
86+
87+
$this->expectException(MissingAttributeException::class);
88+
try {
89+
$time = Time::query()->select('id')->first();
90+
$this->assertNull($time->time);
91+
} finally {
92+
Model::preventAccessingMissingAttributes($originalMode);
93+
}
94+
}
95+
7996
/**
8097
* Get a database connection instance.
8198
*

0 commit comments

Comments
 (0)