From b54aff8868a4f51098af75623b3ce44afbcaad7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C5=BEuris?= Date: Thu, 30 Jan 2025 19:25:42 +0200 Subject: [PATCH 1/6] Test casts for pending attributes --- .../DatabaseEloquentWithAttributesTest.php | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/tests/Database/DatabaseEloquentWithAttributesTest.php b/tests/Database/DatabaseEloquentWithAttributesTest.php index 85b11d7991f3..dfdb76a5e5a2 100755 --- a/tests/Database/DatabaseEloquentWithAttributesTest.php +++ b/tests/Database/DatabaseEloquentWithAttributesTest.php @@ -3,7 +3,9 @@ namespace Illuminate\Tests\Database; use Illuminate\Database\Capsule\Manager as DB; +use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Schema\Builder; use PHPUnit\Framework\TestCase; class DatabaseEloquentWithAttributesTest extends TestCase @@ -20,6 +22,11 @@ protected function setUp(): void $db->setAsGlobal(); } + protected function tearDown(): void + { + $this->schema()->dropIfExists((new WithAttributesModel)->getTable()); + } + public function testAddsAttributes(): void { $key = 'a key'; @@ -51,9 +58,101 @@ public function testAddsWheres(): void 'boolean' => 'and', ], $wheres); } + + public function testAddsWithCasts(): void + { + $query = WithAttributesModel::query() + ->withAttributes([ + 'is_admin' => 1, + 'first_name' => 'FIRST', + 'last_name' => 'LAST', + 'type' => WithAttributesEnum::internal, + ]); + + $model = $query->make(); + + $this->assertSame(true, $model->is_admin); + $this->assertSame('First', $model->first_name); + $this->assertSame('Last', $model->last_name); + $this->assertSame(WithAttributesEnum::internal, $model->type); + + $this->assertEqualsCanonicalizing([ + 'id_admin' => 1, + 'first_name' => 'first', + 'last_name' => 'last', + 'type' => 'int', + ], $model->getAttributes()); + } + + public function testAddsWithCastsViaDb(): void + { + $this->bootTable(); + + $query = WithAttributesModel::query() + ->withAttributes([ + 'is_admin' => 1, + 'first_name' => 'FIRST', + 'last_name' => 'LAST', + 'type' => WithAttributesEnum::internal, + ]); + + $query->create(); + + $model = WithAttributesModel::first(); + + $this->assertSame(true, $model->is_admin); + $this->assertSame('First', $model->first_name); + $this->assertSame('Last', $model->last_name); + $this->assertSame(WithAttributesEnum::internal, $model->type); + } + + protected function bootTable(): void + { + $this->schema()->create((new WithAttributesModel)->getTable(), function ($table) { + $table->id(); + $table->boolean('is_admin'); + $table->string('first_name'); + $table->string('last_name'); + $table->string('type'); + $table->timestamps(); + }); + } + + protected function schema(): Builder + { + return WithAttributesModel::getConnectionResolver()->connection()->getSchemaBuilder(); + } } class WithAttributesModel extends Model { protected $guarded = []; + + protected $casts = [ + 'is_admin' => 'boolean', + 'type' => WithAttributesEnum::class, + ]; + + public function setFirstNameAttribute(string $value): void + { + $this->attributes['first_name'] = strtolower($value); + } + + public function getFirstNameAttribute(?string $value): string + { + return ucfirst($value); + } + + protected function lastName(): Attribute + { + return Attribute::make( + get: fn (string $value) => ucfirst($value), + set: fn (string $value) => strtolower($value), + ); + } +} + +enum WithAttributesEnum: string +{ + case internal = 'int'; } From 2b849955d834322e7fa1e62b28e11c836d3295b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C5=BEuris?= Date: Thu, 30 Jan 2025 19:37:06 +0200 Subject: [PATCH 2/6] Dix typo --- tests/Database/DatabaseEloquentWithAttributesTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Database/DatabaseEloquentWithAttributesTest.php b/tests/Database/DatabaseEloquentWithAttributesTest.php index dfdb76a5e5a2..bf033678080c 100755 --- a/tests/Database/DatabaseEloquentWithAttributesTest.php +++ b/tests/Database/DatabaseEloquentWithAttributesTest.php @@ -77,7 +77,7 @@ public function testAddsWithCasts(): void $this->assertSame(WithAttributesEnum::internal, $model->type); $this->assertEqualsCanonicalizing([ - 'id_admin' => 1, + 'is_admin' => 1, 'first_name' => 'first', 'last_name' => 'last', 'type' => 'int', From 0dccc345f015628fcfc0be803199aa7b24ea6396 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C5=BEuris?= Date: Fri, 31 Jan 2025 23:47:44 +0200 Subject: [PATCH 3/6] Add a failing test --- ...EloquentHasOneOrManyWithAttributesTest.php | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/Database/DatabaseEloquentHasOneOrManyWithAttributesTest.php b/tests/Database/DatabaseEloquentHasOneOrManyWithAttributesTest.php index 80f1e677eb37..ed686e594121 100755 --- a/tests/Database/DatabaseEloquentHasOneOrManyWithAttributesTest.php +++ b/tests/Database/DatabaseEloquentHasOneOrManyWithAttributesTest.php @@ -267,9 +267,30 @@ public function testOneKeepsAttributesFromMorphMany(): void $this->assertSame($parent::class, $relatedModel->relatable_type); $this->assertSame($value, $relatedModel->$key); } + + public function testHasManyAddsCastedAttributes(): void + { + $parentId = 123; + + $parent = new RelatedWithAttributesModel; + $parent->id = $parentId; + + $relationship = $parent + ->hasMany(RelatedWithAttributesModel::class, 'parent_id') + ->withAttributes(['is_admin' => 1]); + + $relatedModel = $relationship->make(); + + $this->assertSame($parentId, $relatedModel->parent_id); + $this->assertSame(true, $relatedModel->is_admin); + } } class RelatedWithAttributesModel extends Model { protected $guarded = []; + + protected $casts = [ + 'is_admin' => 'boolean', + ]; } From 3e3bb7eeeef98145c53eeb7f318f967a357b8bf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C5=BEuris?= Date: Fri, 31 Jan 2025 23:54:21 +0200 Subject: [PATCH 4/6] Fix withAttributes with casts on related models --- .../Database/Eloquent/Relations/HasOneOrMany.php | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Relations/HasOneOrMany.php b/src/Illuminate/Database/Eloquent/Relations/HasOneOrMany.php index de94098dcfeb..71d1eb6500de 100755 --- a/src/Illuminate/Database/Eloquent/Relations/HasOneOrMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/HasOneOrMany.php @@ -3,7 +3,7 @@ namespace Illuminate\Database\Eloquent\Relations; use Illuminate\Database\Eloquent\Builder; -use Illuminate\Database\Eloquent\Collection as EloquentCollection; +use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\Concerns\InteractsWithDictionary; use Illuminate\Database\Eloquent\Relations\Concerns\SupportsInverseRelations; @@ -119,7 +119,7 @@ public function addEagerConstraints(array $models) * @param string $relation * @return array */ - public function matchOne(array $models, EloquentCollection $results, $relation) + public function matchOne(array $models, Collection $results, $relation) { return $this->matchOneOrMany($models, $results, $relation, 'one'); } @@ -132,7 +132,7 @@ public function matchOne(array $models, EloquentCollection $results, $relation) * @param string $relation * @return array */ - public function matchMany(array $models, EloquentCollection $results, $relation) + public function matchMany(array $models, Collection $results, $relation) { return $this->matchOneOrMany($models, $results, $relation, 'many'); } @@ -146,7 +146,7 @@ public function matchMany(array $models, EloquentCollection $results, $relation) * @param string $type * @return array */ - protected function matchOneOrMany(array $models, EloquentCollection $results, $relation, $type) + protected function matchOneOrMany(array $models, Collection $results, $relation, $type) { $dictionary = $this->buildDictionary($results); @@ -189,7 +189,7 @@ protected function getRelationValue(array $dictionary, $key, $type) * @param \Illuminate\Database\Eloquent\Collection $results * @return array> */ - protected function buildDictionary(EloquentCollection $results) + protected function buildDictionary(Collection $results) { $foreign = $this->getForeignKeyName(); @@ -448,7 +448,9 @@ protected function setForeignAttributesForCreate(Model $model) $model->setAttribute($this->getForeignKeyName(), $this->getParentKey()); foreach ($this->getQuery()->pendingAttributes as $key => $value) { - if (! $model->hasAttribute($key)) { + $attributes ??= $model->getAttributes(); + + if (! array_key_exists($key, $attributes)) { $model->setAttribute($key, $value); } } From cb2033e40b77f7b72bd138c68b6ca4b8a501a1b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C5=BEuris?= Date: Fri, 31 Jan 2025 23:56:55 +0200 Subject: [PATCH 5/6] Fix withAttributes with casts on morphrelated models --- src/Illuminate/Database/Eloquent/Relations/MorphOneOrMany.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Database/Eloquent/Relations/MorphOneOrMany.php b/src/Illuminate/Database/Eloquent/Relations/MorphOneOrMany.php index 44531957d5b7..1e879c1dcef1 100755 --- a/src/Illuminate/Database/Eloquent/Relations/MorphOneOrMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/MorphOneOrMany.php @@ -97,7 +97,9 @@ protected function setForeignAttributesForCreate(Model $model) $model->{$this->getMorphType()} = $this->morphClass; foreach ($this->getQuery()->pendingAttributes as $key => $value) { - if (! $model->hasAttribute($key)) { + $attributes ??= $model->getAttributes(); + + if (! array_key_exists($key, $attributes)) { $model->setAttribute($key, $value); } } From 1999cd634a3a63ee98df14fa0e9fc518783b6b38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C5=BEuris?= Date: Sat, 1 Feb 2025 00:11:17 +0200 Subject: [PATCH 6/6] Clean up --- .../Database/Eloquent/Relations/HasOneOrMany.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Relations/HasOneOrMany.php b/src/Illuminate/Database/Eloquent/Relations/HasOneOrMany.php index 71d1eb6500de..3d8426c2a85d 100755 --- a/src/Illuminate/Database/Eloquent/Relations/HasOneOrMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/HasOneOrMany.php @@ -3,7 +3,7 @@ namespace Illuminate\Database\Eloquent\Relations; use Illuminate\Database\Eloquent\Builder; -use Illuminate\Database\Eloquent\Collection; +use Illuminate\Database\Eloquent\Collection as EloquentCollection; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\Concerns\InteractsWithDictionary; use Illuminate\Database\Eloquent\Relations\Concerns\SupportsInverseRelations; @@ -119,7 +119,7 @@ public function addEagerConstraints(array $models) * @param string $relation * @return array */ - public function matchOne(array $models, Collection $results, $relation) + public function matchOne(array $models, EloquentCollection $results, $relation) { return $this->matchOneOrMany($models, $results, $relation, 'one'); } @@ -132,7 +132,7 @@ public function matchOne(array $models, Collection $results, $relation) * @param string $relation * @return array */ - public function matchMany(array $models, Collection $results, $relation) + public function matchMany(array $models, EloquentCollection $results, $relation) { return $this->matchOneOrMany($models, $results, $relation, 'many'); } @@ -146,7 +146,7 @@ public function matchMany(array $models, Collection $results, $relation) * @param string $type * @return array */ - protected function matchOneOrMany(array $models, Collection $results, $relation, $type) + protected function matchOneOrMany(array $models, EloquentCollection $results, $relation, $type) { $dictionary = $this->buildDictionary($results); @@ -189,7 +189,7 @@ protected function getRelationValue(array $dictionary, $key, $type) * @param \Illuminate\Database\Eloquent\Collection $results * @return array> */ - protected function buildDictionary(Collection $results) + protected function buildDictionary(EloquentCollection $results) { $foreign = $this->getForeignKeyName();