crnkovic's avatar

Bidirectional polymorphic many-to-many

Hey artisans,

I've been scratching my head every time I try to do this. I have 2 types of user, say UserA and UserB. And 2 types of posts: PostA and PostB. I want to create a table, let's say items, that's going to morph both:

$table->morphs('user');
$table->morphs('post');

I've been trying to implement this on both User side and Post side, both it doesn't seem like Eloquent can handle that, so if everyone has the idea how to do it, it would be nice.

And bonus question: if this is not possible, my backup plan is to create a table userA_items and userB_items and use morphs there:

$table->unsignedInteger('user_id');
$table->morphs('post');

Is there a way to grab all user's items regardless of their type. Meaning: $user->itemsA would be morphedByMany(PostA::class), and same for $user->itemsB. I want to get $user->items that would automatically morph the Post type (return an instance of the model). Yeah I could simply do hasMany on the User model but that would not morph the post. :)

Update: if getting all items is not possible with Eloquent relationships, it would be nice if someone knows a way to combine itemsA(), itemsB() queries to get items. I can do collection merge, but I need pagination and opting for less queries :-)

Thanks!

0 likes
6 replies
staudenmeir's avatar

I've been trying to implement this on both User side and Post side, both it doesn't seem like Eloquent can handle that"

What implementation have you tried? What didn't work?

crnkovic's avatar

Everything. I have a tough time wrapping my head around the all sorts of polymorphic relationships - morphOne, morphToMany, morphedByMany and morphMany. I have tried all sorts of relationships in my Eloquent models. Every combination. I have found my temporary success (enough to make it work) in making userA_items and userB_items table whose relationship is hasMany(Item::class), then Item model will contain the post method that morphs to the Post model. it's kinda hacky and I'm doing all the repeating logic (re-building sync methods and similar), but it's enough for me to thoroughly test it and build the API of the feature to my needs. Now, I hopefully find a better solution!

Now my code is really gross and slow and does a shit ton of queries, but hey, it works...


@foreach ($items as $item)
        
    @php($post = $item->post)

    @if ($post instanceof \App\PostA)

        @php($post->load('rel1', 'rel2'))

        <postA-card :data="{{ json_encode(new \App\Http\Resources\PostA($post)) }}"></postA-card>

    @else

        @php($post->load('rel3', 'rel4'))

        <postB-card :data="{{ json_encode(new \App\Http\Resources\PostB($post)) }}"></postB-card>

    @endif 

@endforeach

staudenmeir's avatar

Can an item only have one user (of type A or B). Or multiple?

crnkovic's avatar

Multiple. So it's essentially many-to-many relationship, but each component is a morph.

Default many-to-many table would look like: user_id, post_id, while I would have user_type, user_id, post_type, post_id

staudenmeir's avatar

This sounds to me like a case of two standard MorphToMany/MorphedByMany relationships.

Take a look at the documentation example: Tag corresponds to Item. Then there are two pivot tables, one for the users and one for the posts.

crnkovic's avatar

This is not quite what I want because I want user to grab all items regardless of morphed type.

This is what I come up with that reduces number of queries by a lot:

$paginator = $request->user()
    ->items()
    ->with('post.common_relationship')
    ->paginate(5)
    ->loadMorph('post', [
        PostA::class => ['rel1', 'rel2'],
        PostB::class => 'rel3',
    ]);

$collection = $paginator->getCollection()->map->post;

items() is still a hasMany relationship to the items table, and the Item model has post method that morphs the post type.

Which makes my view cleaner:

@foreach ($collection as $item)

    @if ($item instanceof \App\PostA)

        <postA-card :data="{{ json_encode(new \App\Http\Resources\PostA($item)) }}"></postA-card>

    @else

        <postB-card :data="{{ json_encode(new \App\Http\Resources\PostB($item)) }}"></postB-card>

    @endif

@endforeach 

{{ $paginator->links() }}

Please or to participate in this conversation.