Skip to content

Commit ac9f10b

Browse files
[12.x] Fillable relation as an attribute
1 parent 8f8235f commit ac9f10b

File tree

4 files changed

+138
-1
lines changed

4 files changed

+138
-1
lines changed

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

+31
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
namespace Illuminate\Database\Eloquent\Concerns;
44

5+
use Illuminate\Database\Eloquent\Relations\BelongsTo;
6+
57
trait GuardsAttributes
68
{
79
/**
@@ -32,6 +34,20 @@ trait GuardsAttributes
3234
*/
3335
protected static $guardableColumns = [];
3436

37+
/**
38+
* Indicates if filling an attribute named as a Belongs-To relation should be fillable.
39+
*
40+
* @var bool
41+
*/
42+
public static $fillBelongsToRelations = false;
43+
44+
/**
45+
* The cache for fillable Belongs To Relations methods.
46+
*
47+
* @var array<class-string<static>,bool>
48+
*/
49+
protected static $fillableBelongsToRelationsCache = [];
50+
3551
/**
3652
* Get the fillable attributes for the model.
3753
*
@@ -257,4 +273,19 @@ protected function fillableFromArray(array $attributes)
257273

258274
return $attributes;
259275
}
276+
277+
/**
278+
* Check if the relation is fillable.
279+
*
280+
* @param string $key
281+
* @return bool
282+
*/
283+
protected function isBelongsToFillable($key)
284+
{
285+
return static::$fillBelongsToRelations
286+
&& (
287+
static::$fillableBelongsToRelationsCache[static::class][$key]
288+
??= $this->isRelation($key) && $this->{$key}() instanceof BelongsTo
289+
);
290+
}
260291
}

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

+16
Original file line numberDiff line numberDiff line change
@@ -1052,6 +1052,22 @@ protected function newRelatedThroughInstance($class)
10521052
return new $class;
10531053
}
10541054

1055+
/**
1056+
* Fills a Belongs To relation like it was an attribute value.
1057+
*
1058+
* @param string $key
1059+
* @param \Illuminate\Database\Eloquent\Model $value
1060+
* @return $this
1061+
*/
1062+
protected function fillBelongsToRelation($key, $value)
1063+
{
1064+
if (!$value->exists) {
1065+
$value->save();
1066+
}
1067+
1068+
return $this->{$key}()->associate($value);
1069+
}
1070+
10551071
/**
10561072
* Get all the loaded relations for the instance.
10571073
*

src/Illuminate/Database/Eloquent/Model.php

+5-1
Original file line numberDiff line numberDiff line change
@@ -584,7 +584,11 @@ public function fill(array $attributes)
584584
// which means only those attributes may be set through mass assignment to
585585
// the model, and all others will just get ignored for security reasons.
586586
if ($this->isFillable($key)) {
587-
$this->setAttribute($key, $value);
587+
if ($value instanceof Model && $this->isBelongsToFillable($key)) {
588+
$this->fillBelongsToRelation($key, $value);
589+
} else {
590+
$this->setAttribute($key, $value);
591+
}
588592
} elseif ($totallyGuarded || static::preventsSilentlyDiscardingAttributes()) {
589593
if (isset(static::$discardedAttributeViolationCallback)) {
590594
call_user_func(static::$discardedAttributeViolationCallback, $this, [$key]);

tests/Database/DatabaseEloquentModelTest.php

+86
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ protected function tearDown(): void
6868
{
6969
parent::tearDown();
7070

71+
Model::$fillBelongsToRelations = false;
72+
EloquentModelStub::$fillBelongsToRelations = false;
73+
7174
m::close();
7275
Carbon::setTestNow(null);
7376

@@ -3330,6 +3333,89 @@ public function testUseFactoryAttribute()
33303333
$this->assertEquals(EloquentModelWithUseFactoryAttribute::class, $factory->modelName());
33313334
$this->assertEquals('test name', $instance->name); // Small smoke test to ensure the factory is working
33323335
}
3336+
3337+
public function testFillableWithBelongsToRelationDoesntWorkIfDisabledByDefault()
3338+
{
3339+
$model = new EloquentModelStub;
3340+
$this->addMockConnection($model);
3341+
3342+
$model->fillable(['name', 'age']);
3343+
$relation = new EloquentModelSaveStub();
3344+
$relation->id = 10;
3345+
3346+
$model->fill(['name' => 'foo', 'age' => 'bar', 'belongsToStub' => $relation, 'morphToStub' => $relation]);
3347+
3348+
$this->assertNull($model->belongs_to_stub_id);
3349+
$this->assertNull($model->morph_to_stub_id);
3350+
3351+
$model->fillable(['name', 'age', 'belongsToStub', 'morphToStub']);
3352+
$model->fill(['name' => 'foo', 'age' => 'bar', 'belongsToStub' => $relation, 'morphToStub' => $relation]);
3353+
3354+
$this->assertSame('foo', $model->name);
3355+
$this->assertSame('bar', $model->age);
3356+
$this->assertNull($model->belongs_to_stub_id);
3357+
$this->assertNull($model->morph_to_stub_id);
3358+
$this->assertNull($model->morph_to_stub_type);
3359+
$this->assertFalse($model->relationLoaded('belongsToStub'));
3360+
$this->assertFalse($model->relationLoaded('morphToStub'));
3361+
}
3362+
3363+
public function testFillableWithBelongsToRelation()
3364+
{
3365+
EloquentModelStub::$fillBelongsToRelations = true;
3366+
3367+
$model = new EloquentModelStub;
3368+
$this->addMockConnection($model);
3369+
3370+
$model->fillable(['name', 'age']);
3371+
$relation = new EloquentModelSaveStub();
3372+
$relation->id = 10;
3373+
3374+
$model->fill(['name' => 'foo', 'age' => 'bar', 'belongsToStub' => $relation, 'morphToStub' => $relation]);
3375+
3376+
$this->assertNull($model->belongs_to_stub_id);
3377+
$this->assertNull($model->morph_to_stub_id);
3378+
3379+
$model->fillable(['name', 'age', 'belongsToStub', 'morphToStub']);
3380+
$model->fill(['name' => 'foo', 'age' => 'bar', 'belongsToStub' => $relation, 'morphToStub' => $relation]);
3381+
3382+
$this->assertSame('foo', $model->name);
3383+
$this->assertSame('bar', $model->age);
3384+
$this->assertSame(10, $model->belongs_to_stub_id);
3385+
$this->assertSame(10, $model->morph_to_stub_id);
3386+
$this->assertSame(EloquentModelSaveStub::class, $model->morph_to_stub_type);
3387+
$this->assertSAme($relation, $model->getRelation('belongsToStub'));
3388+
$this->assertSAme($relation, $model->getRelation('morphToStub'));
3389+
}
3390+
3391+
public function testFillableWithBelongsToRelationNotFillableIfDoesntExist()
3392+
{
3393+
EloquentModelStub::$fillBelongsToRelations = true;
3394+
3395+
$model = new EloquentModelStub;
3396+
$this->addMockConnection($model);
3397+
$model->fillable(['name', 'foo']);
3398+
3399+
$relation = new EloquentModelSaveStub();
3400+
3401+
$model->fill(['foo' => $relation]);
3402+
3403+
$this->assertNull($model->getRelation('foo'));
3404+
$this->assertSame($relation, $model->getAttribute('foo'));
3405+
}
3406+
3407+
public function testFillableWithBelongsToRelationNotModelIsSetAsAttribute()
3408+
{
3409+
EloquentModelStub::$fillBelongsToRelations = true;
3410+
3411+
$model = new EloquentModelStub;
3412+
$model->fillable(['name', 'age', 'belongsToStub']);
3413+
3414+
$model->fill(['belongsToStub' => 'foo']);
3415+
3416+
$this->assertNull($model->getRelation('belongsToStub'));
3417+
$this->assertSame('foo', $model->getAttribute('belongsToStub'));
3418+
}
33333419
}
33343420

33353421
class EloquentTestObserverStub

0 commit comments

Comments
 (0)