cybercog / laravel-love by antonkomarev

Make Laravel Eloquent models reactable with any type of emotions in a minutes!
231,503
1,168
23
Package Data
Maintainer Username: antonkomarev
Maintainer Contact: anton@komarev.com (Anton Komarev)
Package Create Date: 2018-01-02
Package Last Update: 2024-03-09
Home Page: https://komarev.com/sources/laravel-love
Language: PHP
License: MIT
Last Refreshed: 2024-11-29 15:18:43
Package Statistics
Total Downloads: 231,503
Monthly Downloads: 2,697
Daily Downloads: 94
Total Stars: 1,168
Total Watchers: 23
Total Forks: 71
Total Open Issues: 11

Laravel Love

cog-laravel-love

Introduction

Laravel Love is emotional part of the application. It let people express how they feel about the content. Make any model reactable in a minutes!

There are many different implementations in modern applications:

  • Github Reactions
  • Facebook Reactions
  • YouTube Likes
  • Slack Reactions
  • Medium Claps

This package developed in mind that it should cover all the possible use cases and be viable in enterprise applications.

It is a successor of the very simple abandoned package: Laravel Likeable.

Contents

Features

  • Fully customizable types of reactions.
  • Any model can react to models and receive reactions at the same time.
  • Reactant can has many types of reactions.
  • Reacter can add many reactions to one reactant.
  • Reaction counters with detailed aggregated data for each reactant.
  • Reaction totals with total aggregated data for each reactant.
  • Can work with any database id column types.
  • Sort reactable models by reactions total count.
  • Sort reactable models by reactions total weight.
  • Events for added & removed reactions.
  • Has Artisan command love:recount {model?} {type?} to re-fetch reactions stats.
  • Designed to work with Laravel Eloquent models.
  • Using contracts to keep high customization capabilities.
  • Using traits to get functionality out of the box.
  • Using database foreign keys.
  • Using Null Object design pattern.
  • Strict typed.
  • Following PHP Standard Recommendations:
  • Covered with unit tests.

System Design

190102-cog-laravel-love-uml

Glossary

  • Reaction — the response that reveals Reacter's feelings or attitude.
  • ReactionType — type of the emotional response (Like, Dislike, Love, Hate, etc).
  • Reacterable — User, Person, Organization or any other model which can act as Reacter.
  • Reacter — one who reacts.
  • Reactable — Article, Comment, User or any other model which can act as Reactant.
  • Reactant — subject which could receive Reactions.
  • ReactionCounter — aggregated statistical values of ReactionTypes related to Reactant.
  • ReactionTotal — aggregated statistical values of total reactions count & their weight related to Reactant.

Requirements

Laravel Love has a few requirements you should be aware of before installing:

  • PHP 7.1.3+
  • Composer
  • Laravel Framework 5.6+

Installation

Pull in the package through Composer.

$ composer require cybercog/laravel-love

Run database migrations.

$ php artisan migrate

If you want to make changes in migrations, publish them to your application first.

$ php artisan vendor:publish --tag=love-migrations

Integration

To start using package you need to have:

  1. At least one Reacterable model, which will act as Reacter and will react to the content.
  2. At least one Reactable model, which will act as Reactant and will receive reactions.

Prepare Reacterable Models

Each model which can act as Reacter and will react to content must implement Reacterable contract.

Declare that model implements Cog\Contracts\Love\Reacterable\Models\Reacterable contract and use Cog\Laravel\Love\Reacterable\Models\Traits\Reacterable trait.

use Cog\Contracts\Love\Reacterable\Models\Reacterable as ReacterableContract;
use Cog\Laravel\Love\Reacterable\Models\Traits\Reacterable;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable implements ReacterableContract
{
    use Reacterable;
}

After that create & run migrations which will add unsigned big integer column love_reacter_id to each database table where reacterable models are stored.

public function up(): void
{
    Schema::table('users', function (Blueprint $table) {
        $table->unsignedBigInteger('love_reacter_id');
    });
}

Prepare Reactable Models

Each model which can act as Reactant and will receive reactions must implement Reactable contract.

Declare that model implements Cog\Contracts\Love\Reactable\Models\Reactable contract and use Cog\Laravel\Love\Reactable\Models\Traits\Reactable trait.

use Cog\Contracts\Love\Reactable\Models\Reactable as ReactableContract;
use Cog\Laravel\Love\Reactable\Models\Traits\Reactable;
use Illuminate\Database\Eloquent\Model;

class Article extends Model implements ReactableContract
{
    use Reactable;
}

After that create & run migrations which will add unsigned big integer column love_reactant_id to each database table where reactable models are stored.

public function up(): void
{
    Schema::table('articles', function (Blueprint $table) {
        $table->unsignedBigInteger('love_reactant_id');
    });
}

Usage

Reaction Types

ReactionType model describes how Reacter reacted to Reactant. By default there are 2 types of reactions Like and Dislike. Each Like adds +1 to reactant's total weight while Dislike type subtract -1 from it.

Instantiate reaction type from name

$reactionType = ReactionType::fromName('Like');

Get type name

$typeName = $reactionType->getName(); // 'Like'

Get type weight

$typeWeight = $reactionType->getWeight(); // 1

Determine types equality

$likeType = ReactionType::fromName('Like'); 
$dislikeType = ReactionType::fromName('Dislike'); 

$isEqual = $likeType->isEqualTo($likeType); // true
$isEqual = $likeType->isEqualTo($dislikeType); // false

$isNotEqual = $likeType->isNotEqualTo($likeType); // false
$isNotEqual = $likeType->isNotEqualTo($dislikeType); // true

Reacterables

Register reacterable as reacter

To let User react to the content it need to be registered as Reacter.

By default it will be done automatically on successful Reacterable creation, but if this behavior was changed you still can do it manually.

$user->registerAsLoveReacter();

Creation of the Reacter could be done only once for each Reacterable model.

If you will try to register Reacterable as Reacter one more time then Cog\Contracts\Love\Reacterable\Exceptions\AlreadyRegisteredAsLoveReacter exception will be thrown.

If you want to skip auto-creation of related Reacter model just add boolean method shouldRegisterAsLoveReacterOnCreate to Reacterable model which will return false.

Verify reacter registration

If you want to verify if Reacterable is registered as Reacter or not you can use boolean methods.

$isRegistered = $user->isRegisteredAsLoveReacter(); // true

$isNotRegistered = $user->isNotRegisteredAsLoveReacter(); // false

Get reacter model

Only Reacter model can react to content. Get Reacter model from your Reacterable model.

$reacter = $user->getLoveReacter();

If Reacterable model is not registered as Reacter you will receive NullReacter model instead (NullObject design pattern). All it's methods will be callable, but will throw exceptions or return false.

Reacters

Get reacterable

$reacterable = $reacter->getReacterable();

React to reactant

$reacter->reactTo($reactant, $reactionType);

Remove reaction from reactant

$reacter->unreactTo($reactant, $reactionType);

Check if reacter reacted to reactant

Determine if Reacter reacted to Reactant with any type of reaction.

$isReacted = $reacter->isReactedTo($reactant);

$isNotReacted = $reacter->isNotReactedTo($reactant);

Determine if Reacter reacted to Reactant with exact type of reaction.

$reactionType = ReactionType::fromName('Like');

$isReacted = $reacter
    ->isReactedToWithType($reactant, $reactionType);

$isNotReacted = $reacter
    ->isNotReactedToWithType($reactant, $reactionType);

Get reactions which reacter has made

$reactions = $reacter->getReactions();

TODO: Need to add pagination

Reactables

Register reactable as reactant

To let Article to receive reactions from users it need to be registered as Reactant.

By default it will be done automatically on successful Reactable creation, but if this behavior was changed you still can do it manually.

$user->registerAsLoveReactant();

Creation of the Reactant could be done only once for each Reactable model.

If you will try to register Reactable as Reactant one more time then Cog\Contracts\Love\Reactable\Exceptions\AlreadyRegisteredAsLoveReactant exception will be thrown.

If you want to skip auto-creation of related Reactant model just add boolean method shouldRegisterAsLoveReactantOnCreate to Reactable model which will return false.

Verify reactant registration

If you want to verify if Reactable is registered as Reactant or not you can use boolean methods.

$isRegistered = $user->isRegisteredAsLoveReactant(); // true

$isNotRegistered = $user->isNotRegisteredAsLoveReactant(); // false

Get reactant model

Only Reacter model can react to content. Get Reacter model from your Reactable model.

$reactant = $user->getLoveReactant();

If Reactable model is not registered as Reactant you will receive NullReactant model instead (NullObject design pattern). All it's methods will be callable, but will throw exceptions or return false.

Reactants

Get reactable model

$reactable = $reactant->getReactable();

Check if reactant reacted by reacter

Determine if Reacter reacted to Reactant with any type of reaction.

$isReacted = $reactant->isReactedBy($reacter);

$isNotReacted = $reactant->isNotReactedBy($reacter);

Determine if Reacter reacted to Reactant with exact type of reaction.

$reactionType = ReactionType::fromName('Like');

$isReacted = $reactant
    ->isReactedByWithType($reacter, $reactionType);

$isNotReacted = $reactant
    ->isNotReactedByWithType($reacter, $reactionType);

Get reactions which reactant received

$reactions = $reactant->getReactions();

TODO: Need to add pagination

Reactant Reaction Counters

Each Reactant has many counters (one for each reaction type) with aggregated data.

Get reaction counters of reactant

$reactionCounters = $reactant->getReactionCounters();

Or get only counter of exact type.

$reactionType = ReactionType::fromName('Like');

$reactionCounter = $reactant->getReactionCounterOfType($reactionType);

Get reactions count

When you need to determine count of reactions of this type you can get count.

$totalWeight = $reactionCounter->getCount();

Get reactions weight

When you need to determine weight which all reactions of this type gives you can get weight.

$totalWeight = $reactionCounter->getWeight();

Reactant Reaction Totals

Each Reactant has one total with aggregated data. Total is sum of counters of all reaction types.

Get reaction total of reactant

$reactionTotal = $reactant->getReactionTotal();

Get reactions total count

When you need to determine total reactions count you can get count.

$totalWeight = $reactionTotal->getCount();

Get reactions total weight

When you need to determine total weight of reactions you can get weight.

$totalWeight = $reactionTotal->getWeight();

If each Like has weight +1 and Dislike has weight -1 then 3 likes and 5 dislikes will produce -2 total weight.

Reactable Scopes

Find all reactables reacted by user

$reacter = $user->getLoveReacter();

Article::query()
    ->whereReactedBy($reacter)
    ->get();

Find all reactables reacted by user with exact type of reaction

$reacter = $user->getLoveReacter();
$reactionType = ReactionType::fromName('Like');

$articles = Article::query()
    ->whereReactedByWithType($reacter, $reactionType)
    ->get();

Add reaction counter aggregate of exact reaction type to reactables

$reactionType = ReactionType::fromName('Like'); 

$articles = Article::query()
    ->joinReactionCounterOfType($reactionType)
    ->get();

Each Reactable model will contain reactions_count & reactions_weight virtual attributes.

After adding counter aggregate models could be ordered by this value.

$articles = Article::query()
    ->joinReactionCounterOfType($reactionType)
    ->orderBy('reactions_count', 'desc')
    ->get();

Add reaction total aggregate to reactables

$articles = Article::query()
    ->joinReactionTotal()
    ->get();

Each Reactable model will contain reactions_total_count & reactions_total_weight virtual attributes.

After adding total aggregate models could be ordered by this values.

Order by reactions_total_count:

$articles = Article::query()
    ->joinReactionTotal()
    ->orderBy('reactions_total_count', 'desc')
    ->get();

Order by reactions_total_weight:

$articles = Article::query()
    ->joinReactionTotal()
    ->orderBy('reactions_total_weight', 'desc')
    ->get();

Facades

Laravel Love ships with Love facade and allows to execute actions as Reacterable model instead of acting as Reacter and affect on Reactable models instead of Reactant.

Note: Love facade is experimental feature which will be refactored in next releases. Try to avoid it's usage if possible.

Determine if reaction of type

$isOfType = Love::isReactionOfTypeName($reaction, 'Like');

$isNotOfType = Love::isReactionNotOfTypeName($reaction, 'Like');

Same functionality without facade:

$reactionType = ReactionType::fromName('Like');

$isOfType = $reaction->isOfType($reactionType);

$isNotOfType = $reaction->isNotOfType($reactionType);

Determine if reacterable reacted to reactable

$isReacted = Love::isReacterableReactedTo($user, $article);

$isNotReacted = Love::isReacterableNotReactedTo($user, $article);

Same functionality without facade:

$reactant = $article->getLoveReactant();

$isReacted = $reacterable
    ->getLoveReacter()
    ->isReactedTo($reactant);

$isNotReacted = $reacterable
    ->getLoveReacter()
    ->isNotReactedTo($reactant);

Determine if reacterable reacted to reactable with reaction type name

$isReacted = Love::isReacterableReactedToWithTypeName($user, $article, 'Like');

$isReacted = Love::isReacterableNotReactedToWithTypeName($user, $article, 'Like');

Same functionality without facade:

$reactant = $article->getLoveReactant();
$reactionType = ReactionType::fromName('Like');

$isReacted = $reacterable
    ->getLoveReacter()
    ->isReactedToWithType($reactant, $reactionType);

$isNotReacted = $reacterable
    ->getLoveReacter()
    ->isNotReactedToWithType($reactant, $reactionType);

Determine if reactable reacted by reacterable

$isReacted = Love::isReactableReactedBy($article, $user);

$isReacted = Love::isReactableNotReactedBy($article, $user);

Same functionality without facade:

$reacter = $user->getLoveReacter();

$isReacted = $reactable
    ->getLoveReactant()
    ->isReactedBy($reacter);

$isNotReacted = $reactable
    ->getLoveReactant()
    ->isNotReactedBy($reacter);

Determine if reactable reacted by reacterable with reaction type name

$isReacted = Love::isReactableReactedByWithTypeName($article, $user, 'Like');

$isReacted = Love::isReactableNotReactedByWithTypeName($article, $user, 'Like');

Same functionality without facade:

$reacter = $user->getLoveReacter();
$reactionType = ReactionType::fromName('Like');

$isReacted = $reactable
    ->getLoveReactant()
    ->isReactedByWithType($reacter, $reactionType);

$isNotReacted = $reactable
    ->getLoveReactant()
    ->isNotReactedByWithType($reacter, $reactionType);

Get reactable count of reactions for type name

$likesCount = Love::getReactableReactionsCountForTypeName($article, 'Like');

Same functionality without facade:

$reactionType = ReactionType::fromName('Like');

$likesCount = $reactable
    ->getLoveReactant()
    ->getReactionCounterOfType($reactionType)
    ->getCount();

Get reactable weight of reactions for type name

$likesWeight = Love::getReactableReactionsWeightForTypeName($article, 'Like');

Same functionality without facade:

$reactionType = ReactionType::fromName('Like');

$likesWeight = $reactable
    ->getLoveReactant()
    ->getReactionCounterOfType($reactionType)
    ->getWeight();

Get reactable reactions total count

$reactionsTotalCount = Love::getReactableReactionsTotalCount($article);

Same functionality without facade:

$reactionsTotalCount = $reactable
    ->getLoveReactant()
    ->getReactionTotal()
    ->getCount();

Get reactable reactions total weight

$reactionsTotalWeight = Love::getReactableReactionsTotalWeight($article);

Same functionality without facade:

$reactionsTotalWeight = $reactable
    ->getLoveReactant()
    ->getReactionTotal()
    ->getWeight();

Eager Loading

When accessing Eloquent relationships as properties, the relationship data is "lazy loaded". This means the relationship data is not actually loaded until you first access the property. However, Eloquent can "eager load" relationships at the time you query the parent model. Eager loading alleviates the N + 1 query problem. More details read in official Laravel documentation.

List of the most common eager loaded relations:

  • loveReactant.reactions.type
  • loveReactant.reactions.reacter.reacterable
  • loveReactant.reactionCounters
  • loveReactant.reactionTotal
$articles = Article::query()
    ->with([
        'loveReactant.reactions.reacter.reacterable',
        'loveReactant.reactions.type',
        'loveReactant.reactionCounters',
        'loveReactant.reactionTotal',
    ])
    ->get();

Events

On each added reaction Cog\Laravel\Love\Reaction\Events\ReactionHasBeenAdded event is fired.

On each removed reaction Cog\Laravel\Love\Reaction\Events\ReactionHasBeenRemoved event is fired.

Console Commands

Recount likes and dislikes of all model types

$ love:recount

Recount likes and dislikes of concrete model type (using morph map alias)

$ love:recount --model="article"

Recount likes and dislikes of concrete model type (using fully qualified class name)

$ love:recount --model="App\Models\Article"

Recount only likes of all model types

$ love:recount --type="Like"

Recount only likes of concrete model type (using morph map alias)

$ love:recount --model="article" --type="Like"

Recount only likes of concrete model type (using fully qualified class name)

$ love:recount --model="App\Models\Article" --type="Like"

Recount only dislikes of all model types

$ love:recount --type="Dislike"

Recount only dislikes of concrete model type (using morph map alias)

$ love:recount --model="article" --type="Dislike"

Recount only dislikes of concrete model type (using fully qualified class name)

$ love:recount --model="App\Models\Article" --type="Dislike"

Changelog

Please see CHANGELOG for more information on what has changed recently.

Upgrading

Please see UPGRADING for detailed upgrade instructions.

Contributing

Please see CONTRIBUTING for details.

Testing

Run the tests with:

$ vendor/bin/phpunit

Security

If you discover any security related issues, please email open@cybercog.su instead of using the issue tracker.

Contributors

| @antonkomarevAnton Komarev | @squiggSquigg | @acidjazzKevin Olson | @raniesantosRanie Santos |
| :---: | :---: | :---: | :---: |

Laravel Love contributors list

Alternatives

Feel free to add more alternatives as Pull Request.

License

About CyberCog

CyberCog is a Social Unity of enthusiasts. Research best solutions in product & software development is our passion.