Repository: laravel/framework (php)

Issue:
ForceCreate through relationship
<!-- DO NOT THROW THIS AWAY -->
<!-- Fill out the FULL versions with patch versions -->

- Laravel Version: 9.11.0
- PHP Version: 8.1.1
- Database Driver & Version: MySQL 8.0

### Description:
When creating a model through a relationship (e.g. `$book->reviews()->create(['text' => '...'])`), you expect that relation foreign attributes will be populated (in this example, `book_id` should be auto-populated when we create the Review instance through the book instance).

However, when using `forceCreate` instead of `create`, this behaviour does not occur as might be expected (at least by me!). This seems to be due to `create` being implemented for relationships to have this behaviour (https://github.com/laravel/framework/blob/3ec1e4ce0c10f6414b7ef3c4f42dd85cd5102d07/src/Illuminate/Database/Eloquent/Relations/HasOneOrMany.php), but `forceCreate` is not and as such it gets forwarded to `Builder` where this setting of foreign attributes doesn't occur.

Either this behaviour is intended but misleading, or it has just been missed?


### Steps To Reproduce:

1. Create two models, one with a HasMany/HasOne relation to the other
2. Attempt to `forceCreate` a model through the HasMany/HasOne relation
3. Watch as either the query fails because the foreign id is not nullable, or the foreign id is set to null

MVP showing this behaviour: https://github.com/atoff/laravel-demo-forcecreate - run `php artisan bug:demo`
![image](https://user-images.githubusercontent.com/17804618/166948223-6e263f8d-274f-4feb-b696-b9fa823dfead.png)


Pull request:
[8.x] Set relation parent key when using `forceCreate` on `HasOne` and `HasMany` relations
<!--
Please only send a pull request to branches that are currently supported: https://laravel.com/docs/releases#support-policy 

If you are unsure which branch your pull request should be sent to, please read: https://laravel.com/docs/contributions#which-branch

Pull requests without a descriptive title, thorough description, or tests will be closed.

In addition, please describe the benefit to end users; the reasons it does not break any existing features; how it makes building web applications easier, etc.
-->

Closes #42273

> **Disclaimer** this is the same as PR #42280 but targeted to Laravel 8.x as suggested by @driesvints .
>
> As I will be out for some hours, I sent this new PR, so you (maintainers) can decide on which branch this PR fits better.
>
> Reason I didn't send to 8.x on the first place is stated on this comment: https://github.com/laravel/framework/pull/42280#issuecomment-1119334122

As explained in issue #42273 when calling several methods that either create or instantiate a related instance from a relation method, the relation parent's key is automatically filled, but not when calling `forceCreate`.

Take this model as an example:

~~~php
class Lesson extends Model
{
    public function questions()
    {
        return $this->hasMany(Question::class);
    }
}
~~~

When calling all these following methods the lesson primary key is set as the new question instance foreign key automatically:

~~~php
$lesson = Lesson::first();

$q1 = $lesson->questions()->create(...);
$q2 = $lesson->questions()->findOrNew(...);
$q3 = $lesson->questions()->firstOrNew(...);
$q4 = $lesson->questions()->make(...);
$q5 = $lesson->questions()->save(new Question(...));
~~~

This behavior is also present in other methods which use the ones listed above such as `updateOrCreate`, `saveMany`, and others.

But when calling `->forceCreate(...)` from a relation, the foreign key is not set, which I agree with issue's #42273 OP, that is not the expected behavior.

Reason it does not work as the other is due to `forceCreate` not being implemented on `HasOneOrMany`, thus it is deferred to the underlying `Builder` which, of course, would not set any additional attributes.

This PR:

- Adds a `forceCreate` method to the `HasOneOrMany` relation class, which adds the relation parent key before actually force creating a related instance
- Adds relevant tests



Patch:
diff --git a/src/Illuminate/Database/Eloquent/Relations/HasOneOrMany.php b/src/Illuminate/Database/Eloquent/Relations/HasOneOrMany.php
index 1c7ff591cd..ff738b9ad2 100755
--- a/src/Illuminate/Database/Eloquent/Relations/HasOneOrMany.php
+++ b/src/Illuminate/Database/Eloquent/Relations/HasOneOrMany.php
@@ -297,6 +297,19 @@ public function create(array $attributes = [])
         });
     }
 
+    /**
+     * Create a new instance of the related model. Allow mass-assignment.
+     *
+     * @param  array  $attributes
+     * @return \Illuminate\Database\Eloquent\Model
+     */
+    public function forceCreate(array $attributes = [])
+    {
+        $attributes[$this->getForeignKeyName()] = $this->getParentKey();
+
+        return $this->related->forceCreate($attributes);
+    }
+
     /**
      * Create a Collection of new instances of the related model.
      *


Test patch:
diff --git a/tests/Database/DatabaseEloquentHasManyTest.php b/tests/Database/DatabaseEloquentHasManyTest.php
index 0d68f8fa7e..24c3025843 100755
--- a/tests/Database/DatabaseEloquentHasManyTest.php
+++ b/tests/Database/DatabaseEloquentHasManyTest.php
@@ -55,6 +55,15 @@ public function testCreateMethodProperlyCreatesNewModel()
         $this->assertEquals($created, $relation->create(['name' => 'taylor']));
     }
 
+    public function testForceCreateMethodProperlyCreatesNewModel()
+    {
+        $relation = $this->getRelation();
+        $created = $this->expectForceCreatedModel($relation, ['name' => 'taylor']);
+
+        $this->assertEquals($created, $relation->forceCreate(['name' => 'taylor']));
+        $this->assertEquals(1, $created->getAttribute('foreign_key'));
+    }
+
     public function testFindOrNewMethodFindsModel()
     {
         $relation = $this->getRelation();
@@ -304,6 +313,18 @@ protected function expectCreatedModel($relation, $attributes)
 
         return $model;
     }
+
+    protected function expectForceCreatedModel($relation, $attributes)
+    {
+        $attributes[$relation->getForeignKeyName()] = $relation->getParentKey();
+
+        $model = m::mock(Model::class);
+        $model->shouldReceive('getAttribute')->with($relation->getForeignKeyName())->andReturn($relation->getParentKey());
+
+        $relation->getRelated()->shouldReceive('forceCreate')->once()->with($attributes)->andReturn($model);
+
+        return $model;
+    }
 }
 
 class EloquentHasManyModelStub extends Model
diff --git a/tests/Database/DatabaseEloquentHasOneTest.php b/tests/Database/DatabaseEloquentHasOneTest.php
index de23a35ca9..20df9afdee 100755
--- a/tests/Database/DatabaseEloquentHasOneTest.php
+++ b/tests/Database/DatabaseEloquentHasOneTest.php
@@ -129,6 +129,20 @@ public function testCreateMethodProperlyCreatesNewModel()
         $this->assertEquals($created, $relation->create(['name' => 'taylor']));
     }
 
+    public function testForceCreateMethodProperlyCreatesNewModel()
+    {
+        $relation = $this->getRelation();
+        $attributes = ['name' => 'taylor', $relation->getForeignKeyName() => $relation->getParentKey()];
+
+        $created = m::mock(Model::class);
+        $created->shouldReceive('getAttribute')->with($relation->getForeignKeyName())->andReturn($relation->getParentKey());
+
+        $relation->getRelated()->shouldReceive('forceCreate')->once()->with($attributes)->andReturn($created);
+
+        $this->assertEquals($created, $relation->forceCreate(['name' => 'taylor']));
+        $this->assertEquals(1, $created->getAttribute('foreign_key'));
+    }
+
     public function testRelationIsProperlyInitialized()
     {
         $relation = $this->getRelation();