cybercog / laravel-love

Add Social Reactions to Laravel Eloquent Models. It lets people express how they feel about the content. Fully customizable Weighted Reaction System & Reaction Type System with Like, Dislike and any other custom emotion types. Do you react?

Home Page:https://komarev.com/sources/laravel-love

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Usage with Laravel Nova possible?

vesper8 opened this issue · comments

I see that you have a laravel-ban-nova package so maybe you have an idea how I can use Laravel Love with Laravel Nova?

Basically I have a reaction type called "like" and I want to show the users that a user has liked inside Laravel Nova

And I am unsure how to go about achieving this.

I would like to use Nova's HasMany field but I don't know how to defined the liked relationship on my User model

Could you offer some assistance please?

Hrm.. so it seems like I stumbled upon something that works.. although it seems wrong somehow but it does work

In my User nova resource I added this field:

HasMany::make('LikedUsers'),

Then I created a new resource like this:

<?php

namespace App\Nova\Resources;

class LikedUser extends User
{
}

And finally in my User mode I added this relation

    public function likedUsers()
    {
        return $this->whereReactedBy($this, 'Like');
    }

And now I can get a list of users that any user has "liked"... cool

Thoughts?

Now I would like to be able to get the inverse relation but I don't know how

Meaning, if I click on a User that has been liked by another user, I would like to be able to display a list of "Users liked by"

Do you know how could I achieve this?

Thanks!

Never thought about creating Nova package for this one. If you'll create a public Nova wrapper package - I'll be happy to promote it in this repo.

You could try to get this information from the user reactions and iterate through it, something like:

$user->viaLoveReacter()->getReactions();

But it would be not optimal way if there is a lot of data.

whereReactedBy uses joins to optimize queries. You could try to make similar method, for example whereReactedTo and find all reacterable users which reacted to this reactable user.

whereReactedBy uses joins to optimize queries. You could try to make similar method, for example whereReactedTo and find all reacterable users which reacted to this reactable user.

Yes that's what I was thinking.. the terminology is making my head spin though. Shouldn't it be scopeWhereReacteredBy? Or you think it should be scopeWhereReactedTo?

I feel like this would be a nice addition to your package.. having the ability to return the models that have reacted to you.. so as you put it.. whereReactedTo

Any chance you could take a stab at writing this query since you're so familiar with the inner workings? I'd be happy to test it out to confirm that it works

Pretty busy this days, launching new service: https://yhype.me/ 😄
I'll try to look on it in a spare time.

Thanks @antonkomarev and good luck with your launch!

I wrote this and it seems to work! It was quite simple just had to many two small modifications to the scopeWhereReactedBy

What do you think? Did I miss something?

    public function scopeWhereReactedTo(
        Builder $query,
        ReacterableInterface $reacterable,
        ?string $reactionTypeName = null
    ): Builder {
        return $query->whereHas('loveReacter.reactions', function (Builder $reactionsQuery) use ($reacterable, $reactionTypeName) {
            $reactionsQuery->where('reactant_id', $reacterable->getLoveReacter()->getId());
            if ($reactionTypeName !== null) {
                $reactionsQuery->where('reaction_type_id', ReactionType::fromName($reactionTypeName)->getId());
            }
        });
    }

    public function scopeWhereNotReactedTo(
        Builder $query,
        ReacterableInterface $reacterable,
        ?string $reactionTypeName = null
    ): Builder {
        return $query->whereDoesntHave('loveReacter.reactions', function (Builder $reactionsQuery) use ($reacterable, $reactionTypeName) {
            $reactionsQuery->where('reactant_id', $reacterable->getLoveReacter()->getId());
            if ($reactionTypeName !== null) {
                $reactionsQuery->where('reaction_type_id', ReactionType::fromName($reactionTypeName)->getId());
            }
        });
    }

If you like it and it seems to work then maybe you could add it? Or do you want me to submit a PR?

Very close to the correct one. Instead of Reacterable (who reacts) you should bypass Reactable (on whom react) model as second argument:

    public function scopeWhereReactedTo(
        Builder $query,
        ReactableInterface $reactable,
        ?string $reactionTypeName = null
    ): Builder {
        return $query->whereHas('loveReacter.reactions', function (Builder $reactionsQuery) use ($reactable, $reactionTypeName) {
            $reactionsQuery->where('reactant_id', $reactable->getLoveReactant()->getId());
            if ($reactionTypeName !== null) {
                $reactionsQuery->where('reaction_type_id', ReactionType::fromName($reactionTypeName)->getId());
            }
        });
    }

    public function scopeWhereNotReactedTo(
        Builder $query,
        ReactableInterface $reactable,
        ?string $reactionTypeName = null
    ): Builder {
        return $query->whereDoesntHave('loveReacter.reactions', function (Builder $reactionsQuery) use ($reactable, $reactionTypeName) {
            $reactionsQuery->where('reactant_id', $reactable->getLoveReactant()->getId());
            if ($reactionTypeName !== null) {
                $reactionsQuery->where('reaction_type_id', ReactionType::fromName($reactionTypeName)->getId());
            }
        });
    }

You could create a PR. If it will have tests similar as whereReactedBy has - it will be merged and released faster.

These methods should be in \Cog\Laravel\Love\Reacterable\Models\Traits\Reacterable trait.

Thanks for the tips @antonkomarev

Do you know if it's also possible to easily retrieve the reaction date when using scopeWhereReactedTo or scopeWhereReactedBy ?

Right now it returns the users that have reacted to/by the specfied reaction type.. but I'm not seeing an easy way to also retrieve more details about that reaction, namely the datetime it occurred at

Since it returns Eloquent Builder you could get collection and filter it, but it wouldn't be an optimal way. Another approach is to create additional scope methods with datetime range parameters, but I think it will become too complicated. Maybe some kind of services may solve that.

You may add extra scope:

public function scopeWhereWasReactedBetweenDates(
    Builder $query,
    DateTimeImmutable $fromDateTime,
    DateTimeImmutable $toDateTime
): Builder {
    return $query->whereHas('loveReactant.reactions', function (Builder $reactionsQuery) use ($fromDateTime, $toDateTime) {
        $reactionsQuery->whereBetween('created_at', [$fromDateTime, $toDateTime]);
    });
}

And then use both scopes:

$toDate = new DateTimeImmutable();
$fromDate = $toDate->modify('-1 DAY');

$articlesReactedByUser = Article::query()
            ->whereReactedBy($user)
            ->whereWasReactedBetweenDates($fromDate, $toDate)
            ->get();

I haven't tested it, but in theory this should work.

Better than collections filtering, but not so fast as well designed raw SQL query.

I misunderstood you. You don't need to find reactable models within dates, but want to know when exactly user reacted to each of them. Interesting question, I don't have answer on it yet.

Yes that's right.. although those extra scopes could come in handy as well so thanks for sharing : )

I was just hoping that the date of the reaction could be easily retrieved when fetching models that have been reacted to

@antonkomarev I know this won't be super useful and would lend itself to a n+1 problem.. but this is the workaround I came up with to retrieve the raw reaction from the db table in order to expose the datetime of the reaction itself which is useful in my case

It would be nice if a future version could more easily expose/include the date of each reaction, possibly as a pivot value?

Also I hope the new query scopes you have a draft of can be added to the next release too : )

$rawReaction = (new Love)->getRawReaction($user, User::find(request('viaResourceId')), 'LIKE');
<?php

namespace App\Helpers;

use App\Models\LoveReaction;

class Love
{
    public function getRawReaction($reactable, $reacterable, $reactionType)
    {
        $rawReaction = LoveReaction::where('reactant_id', $reactable->love_reactant_id)
        ->where('reacter_id', $reacterable->love_reacter_id)
        ->where('reaction_type_id', config('love.reaction-types')[$reactionType])
        ->first();

        return $rawReaction;
    }
}
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class LoveReaction extends Model
{
    protected $table = 'love_reactions';
}

Thanks for sharing it @vesper8! I've just fixed terminology in 2nd code block to follow same naming convention with the package.

We need to think more about exposing datetime out of the box. I suppose it will be better to create separate issue for it, because we might lose it here. This one was initially about the Laravel Nova support.

Thanks for the edit! I've edited the code in my project to reflect your changes too. I always struggle a bit whenever I try to wrap my mind around the terms reacterable and reactable.

Regarding Nova.. here's a code snippet that showcases how I'm showing the date on my user resource. It's not that clean but it works. As you can see I've adopted Laravel Love fully.. actually there are a few other reaction types not shown here that I'm using. I found the scopes so useful that I'm using it heavily as a replacement for setting up pivot tables

Text::make('Date', function ($user) {
    $rawReaction = null;

    switch (request('viaRelationship')) {
        case 'LikedUsers':
            $rawReaction = (new Love)->getRawReaction($user, User::find(request('viaResourceId')), 'LIKE');
        break;

        case 'LikedByUsers':
            $rawReaction = (new Love)->getRawReaction(User::find(request('viaResourceId')), $user, 'LIKE');
        break;

        case 'DislikedUsers':
            $rawReaction = (new Love)->getRawReaction($user, User::find(request('viaResourceId')), 'DISLIKE');
        break;

        case 'DislikedByUsers':
            $rawReaction = (new Love)->getRawReaction(User::find(request('viaResourceId')), $user, 'DISLIKE');
        break;

        case 'FavoritedUsers':
            $rawReaction = (new Love)->getRawReaction($user, User::find(request('viaResourceId')), 'FAVORITE');
        break;

        case 'FavoritedByUsers':
            $rawReaction = (new Love)->getRawReaction(User::find(request('viaResourceId')), $user, 'FAVORITE');
        break;

        case 'BlockedUsers':
            $rawReaction = (new Love)->getRawReaction($user, User::find(request('viaResourceId')), 'BLOCK');
        break;

        case 'BlockedByUsers':
            $rawReaction = (new Love)->getRawReaction(User::find(request('viaResourceId')), $user, 'BLOCK');
        break;

        default:
            dd(sprintf('Unhandled viaRelationship %s', request('viaRelationship')));
    }

    if ($rawReaction) {
        return (new Timezone)->convertToLocal($rawReaction->created_at);
    }

    // dd(request()->all(), $this->toArray());
    return 'n/a';
}),

User.php

    public function blockedByUsers()
    {
        return $this->whereReactedTo($this, 'Block');
    }

    public function blockedUsers()
    {
        return $this->whereReactedBy($this, 'Block');
    }

    public function likedByUsers()
    {
        return $this->whereReactedTo($this, 'Like');
    }

    public function likedUsers()
    {
        return $this->whereReactedBy($this, 'Like');
    }

    public function dislikedByUsers()
    {
        return $this->whereReactedTo($this, 'Dislike');
    }

    public function dislikedUsers()
    {
        return $this->whereReactedBy($this, 'Dislike');
    }

    public function favoritedByUsers()
    {
        return $this->whereReactedTo($this, 'Favorite');
    }

    public function favoritedUsers()
    {
        return $this->whereReactedBy($this, 'Favorite');
    }

I'll open another issue for the date exposure