Search code examples
laraveleloquenthas-many-polymorphs

Eloquent Polymorphic MorphMany with children


I have 3 models (User, Post, and Comment).

User model:

public function posts() // A user can have many posts
{
    return $this->hasMany('App\Post', 'from_id');
}

Post model:

protected $with = [ // I'm using with() here
    'from',
    'for',
    'comments',
];

public function from() // Post owner
{
    return $this->belongsTo(User::class, 'from_id');
}

public function for() // To whom this post is addressed
{
    return $this->belongsTo(User::class, 'for_id');
}

public function comments() // All comments for this post
{
    return $this->morphMany(Comment::class, 'commentable');
}

Post migration:

$table->id();

$table->string('uuid');

$table->foreign('from_id')->references('id')->on('users');
$table->unsignedBigInteger('from_id');
        
$table->foreign('for_id')->references('id')->on('users');
$table->unsignedBigInteger('for_id')->nullable();

$table->text('body');

$table->timestamps();
$table->softDeletes();

Comment model:

protected $with = [ // I'm also using with() here
    'from',
    'children',
];

public function from() // Comment owner
{
    return $this->belongsTo(User::class, 'from_id', 'id');
}

public function commentable()
{
    return $this->morphTo();
}

public function children() // child comments for this comment (?) Not sure how it's working or not
{
    return $this->hasMany(Comment::class, 'parent_id');
}

Comment migration:

$table->id();

$table->string('uuid');

$table->unsignedBigInteger('commentable_id');
$table->string('commentable_type');

$table->foreign('from_id')->references('id')->on('users');
$table->unsignedBigInteger('from_id');

$table->foreign('parent_id')->references('id')->on('comments');
$table->unsignedBigInteger('parent_id')->nullable();

$table->text('body');
        
$table->timestamps();
$table->softDeletes();

Note that I have two dummy users in my Users table.

Then let's say we have two records inside our Posts table:

|------------------------------------------------------|
|  id  |  uuid  |  from_id  |  for_id  |  body  | ...  |
|------------------------------------------------------|
|  1   |  ....  |  1        |  null    |  ...   | ...  |  
|  2   |  ....  |  1        |  null    |  ...   | ...  |

So let's create a comment for id #1:

php artisan tinker
>> $post = App\Post::first();
>> $post->comments()->create(['from_id' => 2, 'body' => 'Test comment']);
>> App\Comment {#3744
       from_id: 2,
       body: "Test comment",
       commentable_id: 1,
       commentable_type: "App\Post",
       uuid: "68bc8dbd-9769-44d7-8139-3e4e14d3df4f",
       updated_at: "2020-09-01 15:00:38",
       created_at: "2020-09-01 15:00:38",
       id: 1,
  }

Now let's see the first posts with comments (I'm using protected $with in my Post model):

php artisan tinker
>> $post = App\Post::first();
>> App\Post {#4030
       id: 1,
       uuid: "e9503551-99ac-495f-902e-b505408ab9ef",
       from_id: 1,
       for_id: null,
       body: "Vero vel officia qui et. Veritatis laudantium itaque nisi sint repellendus laborum. Nihil at aliquam alias in.",
       created_at: "2020-09-01 14:59:11",
       updated_at: "2020-09-01 14:59:11",
       deleted_at: null,
       comments: Illuminate\Database\Eloquent\Collection {#4039
           all: [
               App\Comment {#4049
                   id: 1,
                   uuid: "68bc8dbd-9769-44d7-8139-3e4e14d3df4f",
                   commentable_id: 1,
                   commentable_type: "App\Post",
                   from_id: 2,
                   parent_id: null,
                   body: "Test comment",
                   created_at: "2020-09-01 15:00:38",
                   updated_at: "2020-09-01 15:00:38",
                   deleted_at: null,
                   from: App\User {#4061
                       id: 2,
                       name: "Prof. Runte Jr.
                       email: "abigail@example.test",
                       ...
                   },
                   children: Illuminate\Database\Eloquent\Collection {#4040
                       all: [],
                   },
               },
           ],
       },
   }

We can see the first post now has a comment, alright.

Now I want to create a child comment for the first comment inside the post before:

php artisan tinker

// get the first post, and then get the first comment of it
>> $post = App\Post::first()->comments()->first();

>> App\Comment {#4060
       id: 1,
       uuid: "68bc8dbd-9769-44d7-8139-3e4e14d3df4f",
       commentable_id: 1,
       commentable_type: "App\Post",
       from_id: 2,
       parent_id: null,
       body: "Test comment",
       created_at: "2020-09-01 15:00:38",
       updated_at: "2020-09-01 15:00:38",
       deleted_at: null,
       from: App\User {#4073
           id: 2,
           name: "Prof. Runte Jr.
           email: "abigail@example.test",
           ...
       },
       children: Illuminate\Database\Eloquent\Collection {#4030
           all: [],
       },
   }

// Now we want to create child comment

// Get the first post
>>  $post = App\Post::first()
// Get the first comment from it
    ->comments()->first()
// Get children relationship (IDK if this the right words to put it) and create a child comment
    ->children()->create(['from_id' => 1, 'body' => 'Testing child comment']);

This is what tinker returns:

Illuminate/Database/QueryException with message 'SQLSTATE[HY000]: General error: 1364 Field 'commentable_id' doesn't have a default value (SQL: insert into comments (from_id, body, parent_id, uuid, updated_at, created_at) values (1, Testing child comment, 1, aa3d624f-3984-438c-adb0-26086459de33, 2020-09-01 15:38:36, 2020-09-01 15:38:36))'

So my question is:

How to make a child comment inside polymorphic relationship (morphMany())? I already set the column for parent_id which belongs to comment id.


Solution

  • As comments() return collection of comments. So, You should use this one like:

    $post = App\Post::first();
    foreach($post->comments() as $comment){
      $comment->children()->create([
         'from_id' => 1, 
         'body' => 'Testing child comment'
      ]);
    }
    

    You can also use save() method as:

    $post = App\Post::first();
    
    $comment = new App\Comment([
      'from_id' => 1, 
      'body' => 'Testing child comment'
    ]);
    
    $child = $post->comments()->first()->save($comment);
    
    

    And You have to change in your App\Comment method children() with this :

    public function children()
    {
        return $this->morphMany(Comment::class, 'commentable');
    }