Skip to content

Commit 9b98cec

Browse files
[12.x] Allowing merging model attributes before insert via Model::fillAndInsert() (#55038)
* wip * docblock * wip * different approach * unused * add back passthru * fix failing test * tear down * clean up * wip * there we go * welcome hydrateAndInsert * revert unneeded change * change to fill* * formatting --------- Co-authored-by: Taylor Otwell <[email protected]>
1 parent 19fc5b2 commit 9b98cec

File tree

2 files changed

+254
-0
lines changed

2 files changed

+254
-0
lines changed

src/Illuminate/Database/Eloquent/Builder.php

+61
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,67 @@ public function hydrate(array $items)
446446
}, $items));
447447
}
448448

449+
/**
450+
* Insert into the database after merging the model's default attributes, setting timestamps, and casting values.
451+
*
452+
* @param array<int, array<string, mixed>> $values
453+
* @return bool
454+
*/
455+
public function fillAndInsert(array $values)
456+
{
457+
return $this->insert($this->fillForInsert($values));
458+
}
459+
460+
/**
461+
* Insert (ignoring errors) into the database after merging the model's default attributes, setting timestamps, and casting values.
462+
*
463+
* @param array<int, array<string, mixed>> $values
464+
* @return int
465+
*/
466+
public function fillAndInsertOrIgnore(array $values)
467+
{
468+
return $this->insertOrIgnore($this->fillForInsert($values));
469+
}
470+
471+
/**
472+
* Insert a record into the database and get its ID after merging the model's default attributes, setting timestamps, and casting values.
473+
*
474+
* @param array<string, mixed> $values
475+
* @return int
476+
*/
477+
public function fillAndInsertGetId(array $values)
478+
{
479+
return $this->insertGetId($this->fillForInsert([$values])[0]);
480+
}
481+
482+
/**
483+
* Enrich the given values by merging in the model's default attributes, adding timestamps, and casting values.
484+
*
485+
* @param array<int, array<string, mixed>> $values
486+
* @return array<int, array<string, mixed>>
487+
*/
488+
public function fillForInsert(array $values)
489+
{
490+
if (empty($values)) {
491+
return [];
492+
}
493+
494+
if (! is_array(reset($values))) {
495+
$values = [$values];
496+
}
497+
498+
$this->model->unguarded(function () use (&$values) {
499+
foreach ($values as $key => $rowValues) {
500+
$values[$key] = tap(
501+
$this->newModelInstance($rowValues),
502+
fn ($model) => $model->setUniqueIds()
503+
)->getAttributes();
504+
}
505+
});
506+
507+
return $this->addTimestampsToUpsertValues($values);
508+
}
509+
449510
/**
450511
* Create a collection of models from a raw query.
451512
*

tests/Database/DatabaseEloquentIntegrationTest.php

+193
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Illuminate\Database\Capsule\Manager as DB;
88
use Illuminate\Database\Eloquent\Builder;
99
use Illuminate\Database\Eloquent\Collection;
10+
use Illuminate\Database\Eloquent\Concerns\HasUuids;
1011
use Illuminate\Database\Eloquent\Model;
1112
use Illuminate\Database\Eloquent\Model as Eloquent;
1213
use Illuminate\Database\Eloquent\ModelNotFoundException;
@@ -16,13 +17,15 @@
1617
use Illuminate\Database\Eloquent\SoftDeletes;
1718
use Illuminate\Database\Eloquent\SoftDeletingScope;
1819
use Illuminate\Database\QueryException;
20+
use Illuminate\Database\Schema\Blueprint;
1921
use Illuminate\Database\UniqueConstraintViolationException;
2022
use Illuminate\Pagination\AbstractPaginator as Paginator;
2123
use Illuminate\Pagination\Cursor;
2224
use Illuminate\Pagination\CursorPaginator;
2325
use Illuminate\Pagination\LengthAwarePaginator;
2426
use Illuminate\Support\Carbon;
2527
use Illuminate\Support\Facades\Date;
28+
use Illuminate\Support\Str;
2629
use Illuminate\Tests\Integration\Database\Fixtures\Post;
2730
use Illuminate\Tests\Integration\Database\Fixtures\User;
2831
use PHPUnit\Framework\TestCase;
@@ -80,6 +83,14 @@ protected function createSchema()
8083
$table->timestamps();
8184
});
8285

86+
$this->schema()->create('users_having_uuids', function (Blueprint $table) {
87+
$table->id();
88+
$table->uuid();
89+
$table->string('name');
90+
$table->tinyInteger('role');
91+
$table->string('role_string');
92+
});
93+
8394
foreach (['default', 'second_connection'] as $connection) {
8495
$this->schema($connection)->create('users', function ($table) {
8596
$table->increments('id');
@@ -187,6 +198,8 @@ protected function tearDown(): void
187198
Eloquent::unsetConnectionResolver();
188199

189200
Carbon::setTestNow(null);
201+
Str::createUuidsNormally();
202+
DB::flushQueryLog();
190203
}
191204

192205
/**
@@ -2461,6 +2474,147 @@ public function testTouchingBiDirectionalChaperonedModelUpdatesAllRelatedTimesta
24612474
}
24622475
}
24632476

2477+
public function testCanFillAndInsert()
2478+
{
2479+
DB::enableQueryLog();
2480+
Carbon::setTestNow('2025-03-15T07:32:00Z');
2481+
2482+
$this->assertTrue(EloquentTestUser::fillAndInsert([
2483+
['email' => '[email protected]', 'birthday' => null],
2484+
['email' => '[email protected]', 'birthday' => new Carbon('1980-01-01')],
2485+
['email' => '[email protected]', 'birthday' => '1987-11-01', 'created_at' => '2025-01-02T02:00:55', 'updated_at' => Carbon::parse('2025-02-19T11:41:13')],
2486+
]));
2487+
2488+
$this->assertCount(1, DB::getQueryLog());
2489+
2490+
$this->assertCount(3, $users = EloquentTestUser::get());
2491+
2492+
$users->take(2)->each(function (EloquentTestUser $user) {
2493+
$this->assertEquals(Carbon::parse('2025-03-15T07:32:00Z'), $user->created_at);
2494+
$this->assertEquals(Carbon::parse('2025-03-15T07:32:00Z'), $user->updated_at);
2495+
});
2496+
2497+
$tim = $users->firstWhere('email', '[email protected]');
2498+
$this->assertEquals(Carbon::parse('2025-01-02T02:00:55'), $tim->created_at);
2499+
$this->assertEquals(Carbon::parse('2025-02-19T11:41:13'), $tim->updated_at);
2500+
2501+
$this->assertNull($users[0]->birthday);
2502+
$this->assertInstanceOf(\DateTime::class, $users[1]->birthday);
2503+
$this->assertInstanceOf(\DateTime::class, $users[2]->birthday);
2504+
$this->assertEquals('1987-11-01', $users[2]->birthday->format('Y-m-d'));
2505+
2506+
DB::flushQueryLog();
2507+
2508+
$this->assertTrue(EloquentTestWithJSON::fillAndInsert([
2509+
['id' => 1, 'json' => ['album' => 'Keep It Like a Secret', 'release_date' => '1999-02-02']],
2510+
['id' => 2, 'json' => (object) ['album' => 'You In Reverse', 'release_date' => '2006-04-11']],
2511+
]));
2512+
2513+
$this->assertCount(1, DB::getQueryLog());
2514+
2515+
$this->assertCount(2, $testsWithJson = EloquentTestWithJSON::get());
2516+
2517+
$testsWithJson->each(function (EloquentTestWithJSON $testWithJson) {
2518+
$this->assertIsArray($testWithJson->json);
2519+
$this->assertArrayHasKey('album', $testWithJson->json);
2520+
});
2521+
}
2522+
2523+
public function testCanFillAndInsertWithUniqueStringIds()
2524+
{
2525+
Str::createUuidsUsingSequence([
2526+
'00000000-0000-7000-0000-000000000000',
2527+
'11111111-0000-7000-0000-000000000000',
2528+
'22222222-0000-7000-0000-000000000000',
2529+
]);
2530+
2531+
$this->assertTrue(ModelWithUniqueStringIds::fillAndInsert([
2532+
[
2533+
'name' => 'Taylor', 'role' => IntBackedRole::Admin, 'role_string' => StringBackedRole::Admin,
2534+
],
2535+
[
2536+
'name' => 'Nuno', 'role' => 3, 'role_string' => 'admin',
2537+
],
2538+
[
2539+
'name' => 'Dries', 'uuid' => 'bbbb0000-0000-7000-0000-000000000000',
2540+
],
2541+
[
2542+
'name' => 'Chris',
2543+
],
2544+
]));
2545+
2546+
$models = ModelWithUniqueStringIds::get();
2547+
2548+
$taylor = $models->firstWhere('name', 'Taylor');
2549+
$nuno = $models->firstWhere('name', 'Nuno');
2550+
$dries = $models->firstWhere('name', 'Dries');
2551+
$chris = $models->firstWhere('name', 'Chris');
2552+
2553+
$this->assertEquals(IntBackedRole::Admin, $taylor->role);
2554+
$this->assertEquals(StringBackedRole::Admin, $taylor->role_string);
2555+
$this->assertSame('00000000-0000-7000-0000-000000000000', $taylor->uuid);
2556+
2557+
$this->assertEquals(IntBackedRole::Admin, $nuno->role);
2558+
$this->assertEquals(StringBackedRole::Admin, $nuno->role_string);
2559+
$this->assertSame('11111111-0000-7000-0000-000000000000', $nuno->uuid);
2560+
2561+
$this->assertEquals(IntBackedRole::User, $dries->role);
2562+
$this->assertEquals(StringBackedRole::User, $dries->role_string);
2563+
$this->assertSame('bbbb0000-0000-7000-0000-000000000000', $dries->uuid);
2564+
2565+
$this->assertEquals(IntBackedRole::User, $chris->role);
2566+
$this->assertEquals(StringBackedRole::User, $chris->role_string);
2567+
$this->assertSame('22222222-0000-7000-0000-000000000000', $chris->uuid);
2568+
}
2569+
2570+
public function testFillAndInsertOrIgnore()
2571+
{
2572+
Str::createUuidsUsingSequence([
2573+
'00000000-0000-7000-0000-000000000000',
2574+
'11111111-0000-7000-0000-000000000000',
2575+
'22222222-0000-7000-0000-000000000000',
2576+
]);
2577+
2578+
$this->assertEquals(1, ModelWithUniqueStringIds::fillAndInsertOrIgnore([
2579+
[
2580+
'id' => 1, 'name' => 'Taylor', 'role' => IntBackedRole::Admin, 'role_string' => StringBackedRole::Admin,
2581+
],
2582+
]));
2583+
2584+
$this->assertSame(1, ModelWithUniqueStringIds::fillAndInsertOrIgnore([
2585+
[
2586+
'id' => 1, 'name' => 'Taylor', 'role' => IntBackedRole::Admin, 'role_string' => StringBackedRole::Admin,
2587+
],
2588+
[
2589+
'id' => 2, 'name' => 'Nuno',
2590+
],
2591+
]));
2592+
2593+
$models = ModelWithUniqueStringIds::get();
2594+
$this->assertSame('00000000-0000-7000-0000-000000000000', $models->firstWhere('name', 'Taylor')->uuid);
2595+
$this->assertSame(
2596+
['uuid' => '22222222-0000-7000-0000-000000000000', 'role' => IntBackedRole::User],
2597+
$models->firstWhere('name', 'Nuno')->only('uuid', 'role')
2598+
);
2599+
}
2600+
2601+
public function testFillAndInsertGetId()
2602+
{
2603+
Str::createUuidsUsingSequence([
2604+
'00000000-0000-7000-0000-000000000000',
2605+
]);
2606+
2607+
DB::enableQueryLog();
2608+
2609+
$this->assertIsInt($newId = ModelWithUniqueStringIds::fillAndInsertGetId([
2610+
'name' => 'Taylor',
2611+
'role' => IntBackedRole::Admin,
2612+
'role_string' => StringBackedRole::Admin,
2613+
]));
2614+
$this->assertCount(1, DB::getRawQueryLog());
2615+
$this->assertSame($newId, ModelWithUniqueStringIds::sole()->id);
2616+
}
2617+
24642618
/**
24652619
* Helpers...
24662620
*/
@@ -2786,3 +2940,42 @@ public function children()
27862940
return $this->hasMany(EloquentTouchingCategory::class, 'parent_id')->chaperone();
27872941
}
27882942
}
2943+
2944+
class ModelWithUniqueStringIds extends Eloquent
2945+
{
2946+
use HasUuids;
2947+
2948+
public $timestamps = false;
2949+
2950+
protected $table = 'users_having_uuids';
2951+
2952+
protected function casts()
2953+
{
2954+
return [
2955+
'role' => IntBackedRole::class,
2956+
'role_string' => StringBackedRole::class,
2957+
];
2958+
}
2959+
2960+
protected $attributes = [
2961+
'role' => IntBackedRole::User,
2962+
'role_string' => StringBackedRole::User,
2963+
];
2964+
2965+
public function uniqueIds()
2966+
{
2967+
return ['uuid'];
2968+
}
2969+
}
2970+
2971+
enum IntBackedRole: int
2972+
{
2973+
case User = 1;
2974+
case Admin = 3;
2975+
}
2976+
2977+
enum StringBackedRole: string
2978+
{
2979+
case User = 'user';
2980+
case Admin = 'admin';
2981+
}

0 commit comments

Comments
 (0)