Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[10.x] Model::preventAccessingMissingAttributes() raises exception for enums & primitive castable attributes that were not retrieved #49480

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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