Description
- Laravel Version: 7.25.0
- PHP Version: 7.4.7
- Database Driver & Version: MySQL 8.0.20
Description:
For Illuminate\Database\Eloquent\Relations\BelongsToMany
relations only, the updateOrCreate()
method is checking the pivot table instead of the related table for Model existence:
Illuminate\Database\Eloquent\Relations\BelongsToMany
public function updateOrCreate(array $attributes, array $values = [], array $joining = [], $touch = true)
{
if (is_null($instance = $this->where($attributes)->first())) {
return $this->create($values, $joining, $touch);
}
$instance->fill($values);
$instance->save(['touch' => false]);
return $instance;
}
It appears this was an oversight because the BelongsToMany#create
method however would then proceed to create an entity in $this->related
.
As a side note, the HasOneOrMany#updateOrCreate
method is vastly different with its tap($this->firstOrNew(...), ...)
call.
Proposed fix (change first line in method body):
public function updateOrCreate(array $attributes, array $values = [], array $joining = [], $touch = true)
{
if (is_null($instance = $this->related->where($attributes)->first())) {
return $this->create($values, $joining, $touch);
}
$instance->fill($values);
$instance->save(['touch' => false]);
return $instance;
}
Steps To Reproduce:
Example Gist with migrations, test and stacktrace (friend User-User relation): https://gist.github.com/Harti/2afb5afd524f3639045b3d57756655fd
Inline Example:
Role::create(['name' => 'existing.role', 'description' => 'foo']);
$user->roles()->updateOrCreate(['name' => 'existing.role'], ['description' => 'bar']);
parent::assertEquals(1, Role::count()); // assertion fails (or is never reached); count equals 2
A second/duplicate entry is inserted into the roles
table (or a Integrity Constraint Violation is thrown, should name
be unique).
Expected behavior: The existing Role is updated, and saved to the $user->roles()
relation (which also only seems to happen if the instance was newly created).
Best regards,
Harti