Too much info :D Hard to grasp everything without running it.
custom Relation : matching Collection after addEagerConstraints()
I have 3 Models: Post, User, Comment
They are linked to each others like this:
Post.php Model has a trait with this:
public function commentators(): CommentatorsRelation
{
/** @var \App\Contracts\CommentableInterface $this */
return new CommentatorsRelation($this);
}
User.php Model has a trait with this :
public function commentatorComments(): HasMany
{
return $this->hasMany(Comment::class, 'user_id', 'id');
}
Comment.php Model has this:
public function commentable()
{
return $this->morphTo();
}
Basically, there is a hasManyThrough between Post and User (users are commentators, not talking about the post's author here), through the polymorphic comments table.
Actually, that would be more like a BelongsToManyThrough relationship I believe, but such a thing does not exist in Eloquent.
I have a mainpage where I list 12 posts per page. Each of these posts should show an excerpt of the names of the last 5 commentators (User model).
To be more precise, that list of 5 commentators should be ordered like this: on top of the list is the user that commented on the post the very first. The second on the list is the user that commented the very latest. The the 3 others are those who commented the latest as well.
So i had to write my own custom Relation, which so far look like this :
<?php
namespace App\Http\Resources;
use App\Contracts\CommentableInterface;
use App\User;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Database\Eloquent\Collection;
class CommentatorsRelation extends Relation
{
/** @var \App\User|\Illuminate\Database\Eloquent\Builder */
protected $query;
/** @var CommentableInterface $parent */
protected $parent;
public function __construct(CommentableInterface $parent)
{
parent::__construct(User::query(), $parent);
}
/**
* Set the base constraints on the relation query.
*
* @return void
*/
public function addConstraints()
{
$this->query // farParent = \App\User
->join(
'comments', // throughParent
'comments.user_id', // secondLocalKey or qualifiedParentKeyName
'=',
'users.id' // secondKey or farKey or qualifiedForeignKeyName
)
->orderBy('comments.created_at')->orderBy('users.created_at')->orderByDesc('users.id')
->whereNull('comments.deleted_at')
->where('comments.commentable_type', get_class($this->parent))
->select('users.*');
}
/**
* Set the constraints for an eager load of the relation.
*
* @param array $models
*
* @return void
*/
public function addEagerConstraints(array $commentables)
{
$this->query
->whereIn(
'comments.commentable_id', // firstKey or qualifiedFirstKeyName
$this->getKeys($commentables, 'id') // localKey
)
->with('commentatorComments')
->select('users.*');
}
/**
* Initialize the relation on a set of models.
*
* @param array $commentables
* @param string $relation
*
* @return array
*/
public function initRelation(array $commentables, $relation)
{
foreach ($commentables as $commentable) {
$commentable->setRelation(
$relation, // commentators
$this->related->newCollection() // creates an empty User model (see constructor of abstract class ```Relation``` : ```$this->related = $query->getModel();```
);
}
return $commentables;
}
/**
* Match the eagerly loaded results to their parents.
*
* @param array $commentables
* @param \Illuminate\Database\Eloquent\Collection $commentators
* @param string $relation
*
* @return array
*/
public function match(array $commentables, Collection $commentators, $relation)
{
if ($commentators->isEmpty()) {
return $commentables;
}
foreach ($commentables as $commentable) {
// test
/*$users = collect($commentators);
foreach ($users as $user) {
$comments = collect($user->commentatorComments);
foreach ($comments as $key => $comment) {
if ($comment->commentable_id !== $commentable->id) {
$comments->forget($key);
}
}
}
$users->commentatorComments = $comments;*/
// end test
// linking commentators to the correct commentable
$commentable->setRelation(
$relation, // commentators
$commentators->filter(function (User $commentator) use ($commentable) {
//if($commentator->id==='05410865-84eb-415f-a8d2-e6fd3ad3c842') dd($commentator->commentatorComments->pluck('commentable_id'));
return $commentator->commentatorComments->pluck('commentable_id')->contains($commentable->id);
})->unique()
);
}
return $commentables;
}
/**
* Get the results of the relationship.
*
* @return mixed
*/
public function getResults()
{
return $this->query->get();
}
}
This works great and fast as it eagerloads everything they way I expected it. However I'm not totally happy with it yet, and here is why:
Remember I have 12 posts per page.
What follows is a dump&die for the post identified by id fc81e8c8-c47e-445a-bfc5-485c55e1a403 (we will call it "post number 9" for readability)
with dd($post->commentators);
It does successfully return the 4 distinct commentators that left a comment on post number 9 :
https://i.imgur.com/weHq1tH.png
You can see User number 3 did leave a comment on that Post number 9 indeed : https://i.imgur.com/SNuKot6.png
That User never left any comment for the 11 other posts of that page. He left only 1 comment on post number 9 (post id, which is commentable_id as well, is fc81e8c8-c47e-445a-bfc5-485c55e1a403)
The one that annoys me is User number 2. He left only 5 comments on Post number 9, but there are 9 comments shown :
https://i.imgur.com/8dcNNal.png
The reason is because he also left 4 other comments on a different post (that post is also part of the pagination of 12 posts per page, so it somehow is logical and makes sense for those comments to be listed in the tree of that user's comments) :
https://i.imgur.com/W240jbx.png
I do not want these comments to be listed here.
So, first question, the most important: how do I pop them away? It all happens within the match() method of my custom Relation. Please note that removing these unwanted comments should not affect other posts of the pagination. Each posts has it's own commentators and own comments.
Second question (bonus): where/how can I re arrange that list of comment (for each user) so that users are, in the very end, sorted the way I explained above?
Thank you for your help.
Please or to participate in this conversation.