NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages
Likes for Laravel

Weighted score & trending

Two ranking scopes go beyond raw counts:

Post::orderByLikeScore()->get();    // rank by a weighted sum of reactions (desc)
Post::orderByTrending()->get();     // rank by recency-weighted activity (desc)

Post::orderByLikeScore('asc')->get();
Post::orderByTrending('desc', 'love')->get();   // trending loves

Weighted score

orderByLikeScore() ranks by a weighted sum of reactions from config('likes.weights'). With the default (no weights) the score equals the raw like count. Give some reactions more pull:

// config/likes.php
'weights' => ['like' => 1, 'love' => 4],
'default_weight' => 1,

Once weights is non-empty, any reaction not listed uses default_weight.

Trending

orderByTrending() scores each row as recent weighted activity × recent_multiplier + all-time weighted activity, where recent means created inside trending.window. The window cut-off is computed in PHP and bound as a parameter, and every weight is bound too — so the SQL stays portable across SQLite, MySQL and PostgreSQL, and injection-safe.

// config/likes.php
'trending' => [
    'window' => '3 days',        // what counts as "recent"
    'recent_multiplier' => 5,    // recent likes weigh 5x on top of all-time activity
    'driver_expressions' => [],
],

Reading the score

Each scope selects its score onto the row — like_score or trending_score — so you can show it or debug the order:

$top = Post::orderByLikeScore()->first();
$top->like_score;        // the weighted score the rows were ranked by

$hot = Post::whereNotLikedBy($user)->orderByTrending()->limit(10)->get();
$hot->first()->trending_score;

Both scopes accept a direction (asc or desc — anything else sorts descending) and an optional reaction type, ignore unliked rows, and compose with other scopes.

Per-driver expressions

Hosts that want an exact decay curve can replace the trending score per database driver with a raw SQL aggregate over the likes rows. orderByTrending() uses it verbatim for queries on a connection of that driver — the portable hybrid for any other — and every ? in it is bound to the window cut-off (now minus trending.window):

// config/likes.php
'trending' => [
    'window' => '7 days',
    'recent_multiplier' => 3,
    'driver_expressions' => [
        // Gravity: each like decays with its age in hours.
        'pgsql' => 'SUM(1 / POWER(EXTRACT(EPOCH FROM (NOW() - created_at)) / 3600 + 2, 1.8))',
        // Recent likes count 10x, bound to the window cut-off.
        'sqlite' => 'SUM(CASE WHEN created_at >= ? THEN 10 ELSE 1 END)',
    ],
],

The expression is host configuration, never user input — it is placed into the query as written. TrendingScore::trending() resolves the same expression for your own ranking queries:

use RoundlyConsulting\Likes\Support\TrendingScore;

['expression' => $expression, 'bindings' => $bindings] = TrendingScore::trending();        // the likes model's driver
['expression' => $expression, 'bindings' => $bindings] = TrendingScore::trending('pgsql');
// your driver_expressions entry for that driver (every ? bound to the window cut-off)
// when one is configured, otherwise the portable expression

Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

More ways to support, including crypto

By donating, you agree to our donation terms.

Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.