diff --git a/src/Illuminate/Database/Eloquent/Relations/HasOneOrMany.php b/src/Illuminate/Database/Eloquent/Relations/HasOneOrMany.php index 7451491cbaf9..5beb9a27c7b4 100755 --- a/src/Illuminate/Database/Eloquent/Relations/HasOneOrMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/HasOneOrMany.php @@ -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> $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> $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 $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. * diff --git a/tests/Database/DatabaseEloquentIntegrationTest.php b/tests/Database/DatabaseEloquentIntegrationTest.php index 1d176afa00f1..9d91bd272ae7 100644 --- a/tests/Database/DatabaseEloquentIntegrationTest.php +++ b/tests/Database/DatabaseEloquentIntegrationTest.php @@ -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'); @@ -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, ], @@ -2580,7 +2591,7 @@ public function testCanFillAndInsertWithUniqueStringIds() ], ])); - $models = ModelWithUniqueStringIds::get(); + $models = UserWithUniqueStringIds::get(); $taylor = $models->firstWhere('name', 'Taylor'); $nuno = $models->firstWhere('name', 'Nuno'); @@ -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, ], @@ -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], @@ -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); } /** @@ -2995,7 +3100,7 @@ public function eloquentTestUsers() } } -class ModelWithUniqueStringIds extends Eloquent +class UserWithUniqueStringIds extends Eloquent { use HasUuids; @@ -3020,6 +3125,11 @@ public function uniqueIds() { return ['uuid']; } + + public function posts() + { + return $this->hasMany(PostWithUniqueStringIds::class, 'user_id'); + } } enum IntBackedRole: int @@ -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'; +}