Skip to content

[12.x] Allow fillAndInsert() usage with HasMany relationships by automatically setting the foreign key #55288

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

Closed
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
49 changes: 49 additions & 0 deletions src/Illuminate/Database/Eloquent/Relations/HasOneOrMany.php
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,55 @@ public function create(array $attributes = [])
});
}

/**
* Insert into the database after merging the model's default attributes, setting timestamps, and casting values.
*
* @param array<int, array<string, mixed>> $values
* @return bool
*/
public function fillAndInsert(array $values)
{
return $this->query->fillAndInsert($this->fillForInsert($values));
}

/**
* Insert (ignoring errors) into the database after merging the model's default attributes, setting timestamps, and casting values.
*
* @param array<int, array<string, mixed>> $values
* @return int
*/
public function fillAndInsertOrIgnore(array $values)
{
return $this->query->fillAndInsertOrIgnore($this->fillForInsert($values));
}

/**
* Insert a record into the database and get its ID after merging the model's default attributes, setting timestamps, and casting values.
*
* @param array<string, mixed> $values
* @return int
*/
public function fillAndInsertGetId(array $values)
{
return $this->query->fillAndInsertGetId($this->fillForInsert($values)[0]);
}

protected function fillForInsert(array $values)
{
if (empty($values)) {
return [];
}

if (! is_array(reset($values))) {
$values = [$values];
}

return $this->related->unguarded(fn () => array_map(
fn ($value) => $this->make($value)->getAttributes(),
$values
));
}

/**
* Create a new instance of the related model without raising any events to the parent model.
*
Expand Down
169 changes: 161 additions & 8 deletions tests/Database/DatabaseEloquentIntegrationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,17 @@ protected function createSchema()
$table->string('role_string');
});

$this->schema()->create('posts_having_uuids', function (Blueprint $table) {
$table->id();
$table->uuid();
$table->integer('user_id');
$table->timestamp('published_at', 6);
$table->string('name');
$table->tinyInteger('status');
$table->string('status_string');
$table->timestamps();
});

foreach (['default', 'second_connection'] as $connection) {
$this->schema($connection)->create('users', function ($table) {
$table->increments('id');
Expand Down Expand Up @@ -2565,7 +2576,7 @@ public function testCanFillAndInsertWithUniqueStringIds()
'22222222-0000-7000-0000-000000000000',
]);

$this->assertTrue(ModelWithUniqueStringIds::fillAndInsert([
$this->assertTrue(UserWithUniqueStringIds::fillAndInsert([
[
'name' => 'Taylor', 'role' => IntBackedRole::Admin, 'role_string' => StringBackedRole::Admin,
],
Expand All @@ -2580,7 +2591,7 @@ public function testCanFillAndInsertWithUniqueStringIds()
],
]));

$models = ModelWithUniqueStringIds::get();
$models = UserWithUniqueStringIds::get();

$taylor = $models->firstWhere('name', 'Taylor');
$nuno = $models->firstWhere('name', 'Nuno');
Expand Down Expand Up @@ -2612,13 +2623,13 @@ public function testFillAndInsertOrIgnore()
'22222222-0000-7000-0000-000000000000',
]);

$this->assertEquals(1, ModelWithUniqueStringIds::fillAndInsertOrIgnore([
$this->assertEquals(1, UserWithUniqueStringIds::fillAndInsertOrIgnore([
[
'id' => 1, 'name' => 'Taylor', 'role' => IntBackedRole::Admin, 'role_string' => StringBackedRole::Admin,
],
]));

$this->assertSame(1, ModelWithUniqueStringIds::fillAndInsertOrIgnore([
$this->assertSame(1, UserWithUniqueStringIds::fillAndInsertOrIgnore([
[
'id' => 1, 'name' => 'Taylor', 'role' => IntBackedRole::Admin, 'role_string' => StringBackedRole::Admin,
],
Expand All @@ -2627,7 +2638,7 @@ public function testFillAndInsertOrIgnore()
],
]));

$models = ModelWithUniqueStringIds::get();
$models = UserWithUniqueStringIds::get();
$this->assertSame('00000000-0000-7000-0000-000000000000', $models->firstWhere('name', 'Taylor')->uuid);
$this->assertSame(
['uuid' => '22222222-0000-7000-0000-000000000000', 'role' => IntBackedRole::User],
Expand All @@ -2643,13 +2654,107 @@ public function testFillAndInsertGetId()

DB::enableQueryLog();

$this->assertIsInt($newId = ModelWithUniqueStringIds::fillAndInsertGetId([
$this->assertIsInt($newId = UserWithUniqueStringIds::fillAndInsertGetId([
'name' => 'Taylor',
'role' => IntBackedRole::Admin,
'role_string' => StringBackedRole::Admin,
]));
$this->assertCount(1, DB::getRawQueryLog());
$this->assertSame($newId, ModelWithUniqueStringIds::sole()->id);
$this->assertSame($newId, UserWithUniqueStringIds::sole()->id);
}

public function testCanFillAndInsertIntoHasManyRelationship()
{
$now = Carbon::now()->startOfSecond();
Carbon::setTestNow($now);

Str::createUuidsUsingSequence([
'00000000-0000-7000-0000-000000000000', // user
'11111111-1111-1111-1111-111111111111', // post id=1
'33333333-3333-3333-3333-333333333333', // post id=3
]);

$user = tap(new UserWithUniqueStringIds(), function ($user) {
$user->forceFill(['name' => 'Taylor Otwell'])->save();
});

DB::enableQueryLog();

$this->assertTrue($user->posts()->fillAndInsert([
[
'id' => 1,
'name' => 'ship or die',
'published_at' => '2025-01-31T22:18:21.000Z',
'status' => 3,
'status_string' => StringBackedStatus::Published,
],
[
'id' => 3,
'name' => 'Welcome to the future of Laravel.',
'published_at' => new Carbon('2025-02-24T15:16:55.000Z'),
// status is default
// status_string is default
],
]));

$this->assertCount(1, DB::getQueryLog());

$this->assertCount(2, $user->posts);

$this->assertSame('ship or die', $user->posts->find(1)->name);
$this->assertSame('11111111-1111-1111-1111-111111111111', $user->posts->find(1)->uuid);
$this->assertEquals($now, $user->posts->find(1)->created_at);
$this->assertEquals($now, $user->posts->find(1)->updated_at);
$this->assertSame(IntBackedStatus::Published, $user->posts->find(1)->status);
$this->assertSame(StringBackedStatus::Published, $user->posts->find(1)->status_string);
$this->assertEquals(Carbon::parse('2025-01-31T22:18:21.000Z'), $user->posts->find(1)->published_at);

$this->assertSame('Welcome to the future of Laravel.', $user->posts->find(3)->name);
$this->assertSame('33333333-3333-3333-3333-333333333333', $user->posts->find(3)->uuid);
$this->assertEquals($now, $user->posts->find(3)->created_at);
$this->assertEquals($now, $user->posts->find(3)->updated_at);
$this->assertSame(IntBackedStatus::Draft, $user->posts->find(3)->status);
$this->assertSame(StringBackedStatus::Draft, $user->posts->find(3)->status_string);
$this->assertEquals(Carbon::parse('2025-02-24T15:16:55.000Z'), $user->posts->find(3)->published_at);
}

public function testFillAndInsertOrIgnoreIntoHasManyRelationship()
{
Str::createUuidsUsingSequence([
'00000000-0000-7000-0000-000000000000', // user
'11111111-1111-1111-1111-111111111111', // post id=1
'22222222-2222-2222-2222-222222222222', // post id=1 ignored
'33333333-3333-3333-3333-333333333333', // post id=3
]);

$user = tap(new UserWithUniqueStringIds(), function ($user) {
$user->forceFill(['name' => 'Taylor Otwell'])->save();
});

$attributes = [
['id' => 1, 'published_at' => now(), 'name' => 'ship of die'],
['id' => 3, 'published_at' => now(), 'name' => 'Welcome to the future of Laravel'],
];

$this->assertSame(1, $user->posts()->fillAndInsertOrIgnore(array_slice($attributes, 0, 1)));
$this->assertSame(1, $user->posts()->fillAndInsertOrIgnore(array_slice($attributes, 0, 2)));

$this->assertSame('11111111-1111-1111-1111-111111111111', $user->posts->find(1)->uuid);
$this->assertSame('33333333-3333-3333-3333-333333333333', $user->posts->find(3)->uuid);
}

public function testfillAndInsertGetIdIntoHasManyRelationship()
{
$user = tap(new UserWithUniqueStringIds(), function ($user) {
$user->forceFill(['name' => 'Taylor Otwell'])->save();
});

$id = $user->posts()->fillAndInsertGetId([
'name' => 'We must ship.',
'published_at' => now(),
]);

$this->assertSame(1, $id);
}

/**
Expand Down Expand Up @@ -2995,7 +3100,7 @@ public function eloquentTestUsers()
}
}

class ModelWithUniqueStringIds extends Eloquent
class UserWithUniqueStringIds extends Eloquent
{
use HasUuids;

Expand All @@ -3020,6 +3125,11 @@ public function uniqueIds()
{
return ['uuid'];
}

public function posts()
{
return $this->hasMany(PostWithUniqueStringIds::class, 'user_id');
}
}

enum IntBackedRole: int
Expand All @@ -3033,3 +3143,46 @@ enum StringBackedRole: string
case User = 'user';
case Admin = 'admin';
}

class PostWithUniqueStringIds extends Eloquent
{
use HasUuids;

protected $table = 'posts_having_uuids';

protected function casts()
{
return [
'published_at' => 'datetime',
'status' => IntBackedStatus::class,
'status_string' => StringBackedStatus::class,
];
}

protected $attributes = [
'status' => IntBackedStatus::Draft,
'status_string' => StringBackedStatus::Draft,
];

public function uniqueIds()
{
return ['uuid'];
}

public function user()
{
return $this->belongsTo(UserWithUniqueStringIds::class, 'user_id');
}
}

enum IntBackedStatus: int
{
case Draft = 1;
case Published = 3;
}

enum StringBackedStatus: string
{
case Draft = 'draft';
case Published = 'published';
}
Loading