Skip to content

Commit

Permalink
[10.x] Model::preventAccessingMissingAttributes() raises exception …
Browse files Browse the repository at this point in the history
…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 <taylor@laravel.com>
  • Loading branch information
cosmastech and taylorotwell committed Dec 27, 2023
1 parent 7e9e271 commit 865f797
Show file tree
Hide file tree
Showing 3 changed files with 133 additions and 0 deletions.
7 changes: 7 additions & 0 deletions src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php
Expand Up @@ -2126,6 +2126,13 @@ protected function transformModelValue($key, $value)
// an appropriate native PHP type dependent upon the associated value
// given with the key in the pair. Dayle made this comment line up.
if ($this->hasCast($key)) {
if (static::preventsAccessingMissingAttributes() &&
! array_key_exists($key, $this->attributes) &&
($this->isEnumCastable($key) ||
in_array($this->getCastType($key), static::$primitiveCastTypes))) {
$this->throwMissingAttributeExceptionIfApplicable($key);
}

return $this->castAttribute($key, $value);
}

Expand Down
109 changes: 109 additions & 0 deletions tests/Database/DatabaseEloquentModelTest.php
Expand Up @@ -7,6 +7,8 @@
use DateTimeInterface;
use Exception;
use Foo\Bar\EloquentModelNamespacedStub;
use Illuminate\Contracts\Database\Eloquent\Castable;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Contracts\Database\Eloquent\CastsInboundAttributes;
use Illuminate\Contracts\Encryption\Encrypter;
use Illuminate\Contracts\Events\Dispatcher;
Expand All @@ -22,6 +24,7 @@
use Illuminate\Database\Eloquent\Casts\AsEnumArrayObject;
use Illuminate\Database\Eloquent\Casts\AsEnumCollection;
use Illuminate\Database\Eloquent\Casts\AsStringable;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Concerns\HasUlids;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
Expand Down Expand Up @@ -2576,6 +2579,45 @@ public function testThrowsWhenAccessingMissingAttributes()
}
}

public function testThrowsWhenAccessingMissingAttributesWhichArePrimitiveCasts()
{
$originalMode = Model::preventsAccessingMissingAttributes();
Model::preventAccessingMissingAttributes();

$model = new EloquentModelWithPrimitiveCasts(['id' => 1]);
$model->exists = true;

$exceptionCount = 0;
$primitiveCasts = EloquentModelWithPrimitiveCasts::makePrimitiveCastsArray();
try {
try {
$this->assertEquals(null, $model->backed_enum);
} catch (MissingAttributeException) {
$exceptionCount++;
}

foreach($primitiveCasts as $key => $type) {
try {
$v = $model->{$key};
} catch (MissingAttributeException) {
$exceptionCount++;
}
}

$this->assertInstanceOf(Address::class, $model->address);

$this->assertEquals(1, $model->id);
$this->assertEquals('ok', $model->this_is_fine);
$this->assertEquals('ok', $model->this_is_also_fine);

// Primitive castables, enum castable
$expectedExceptionCount = count($primitiveCasts) + 1;
$this->assertEquals($expectedExceptionCount, $exceptionCount);
} finally {
Model::preventAccessingMissingAttributes($originalMode);
}
}

public function testUsesOverriddenHandlerWhenAccessingMissingAttributes()
{
$originalMode = Model::preventsAccessingMissingAttributes();
Expand Down Expand Up @@ -3349,3 +3391,70 @@ class CustomCollection extends BaseCollection
{
//
}

class EloquentModelWithPrimitiveCasts extends Model
{
public $fillable = ['id'];

public $casts = [
'backed_enum' => CastableBackedEnum::class,
'address' => Address::class,
];

public static function makePrimitiveCastsArray(): array
{
$toReturn = [];

foreach(static::$primitiveCastTypes as $index => $primitiveCastType) {
$toReturn['primitive_cast_' . $index] = $primitiveCastType;
}

return $toReturn;
}

public function __construct(array $attributes = [])
{
parent::__construct($attributes);

$this->mergeCasts(self::makePrimitiveCastsArray());
}

public function getThisIsFineAttribute($value) {
return 'ok';
}

public function thisIsAlsoFine(): Attribute
{
return Attribute::get(fn() => 'ok');
}
}

enum CastableBackedEnum: string
{
case Value1 = 'value1';
}

class Address implements Castable
{
public static function castUsing(array $arguments): CastsAttributes
{
return new class implements CastsAttributes
{
public function get(Model $model, string $key, mixed $value, array $attributes): Address
{
return new Address(
$attributes['address_line_one'],
$attributes['address_line_two']
);
}

public function set(Model $model, string $key, mixed $value, array $attributes): array
{
return [
'address_line_one' => $value->lineOne,
'address_line_two' => $value->lineTwo,
];
}
};
}
}
17 changes: 17 additions & 0 deletions tests/Database/DatabaseEloquentWithCastsTest.php
Expand Up @@ -3,6 +3,8 @@
namespace Illuminate\Tests\Database;

use Illuminate\Database\Capsule\Manager as DB;
use Illuminate\Database\Eloquent\MissingAttributeException;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Model as Eloquent;
use PHPUnit\Framework\TestCase;

Expand Down Expand Up @@ -76,6 +78,21 @@ public function testWithCreateOrFirst()
$this->assertSame($time1->id, $time2->id);
}

public function testThrowsExceptionIfCastableAttributeWasNotRetrievedAndPreventMissingAttributesIsEnabled()
{
Time::create(['time' => now()]);
$originalMode = Model::preventsAccessingMissingAttributes();
Model::preventAccessingMissingAttributes();

$this->expectException(MissingAttributeException::class);
try {
$time = Time::query()->select('id')->first();
$this->assertNull($time->time);
} finally {
Model::preventAccessingMissingAttributes($originalMode);
}
}

/**
* Get a database connection instance.
*
Expand Down

0 comments on commit 865f797

Please sign in to comment.