← Open Source

Friendship

Laravel

A polymorphic friendship system for Laravel — requests, blocking, mutual friends and events.

267 downloads 18 stars Latest version v2.1.0

Install

composer require ghanem/friendship

Laravel Friendship — friend requests, blocking, mutual friends

Laravel Friendship

tests Latest Version on Packagist Total Downloads License

A polymorphic friendship system for Laravel: friend requests, accept/deny, blocking, mutual friends, counts, and events. Any Eloquent model can befriend any other model type — not just users.

Requires PHP 8.1+ and Laravel 10, 11, 12, or 13.

Laravel PHP
10 8.1 – 8.4
11 8.2 – 8.4
12 8.2 – 8.4
13 8.3 – 8.4

Laravel 10 and 11 are past Laravel's own security-support window. This package is tested against them on every release — with real tagged framework versions, not branch heads — but the framework itself no longer receives security patches, so Composer flags every published 10.x and 11.x release. Prefer 12 or 13 for new projects.

Installation

composer require ghanem/friendship

The service provider is auto-discovered. Publish and run the migration:

php artisan vendor:publish --tag=friendship-migrations
php artisan migrate

Setup

Add the trait to any model:

use Ghanem\Friendship\Contracts\Friendable as FriendableContract;
use Ghanem\Friendship\Traits\Friendable;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable implements FriendableContract
{
    use Friendable;
}

Implementing the contract is optional but recommended — it guarantees your model exposes the full API.

Usage

Friend requests

$me->befriend($you);              // send a request (returns ?Friendship)
$you->acceptFriendRequest($me);   // only the recipient can accept
$you->denyFriendRequest($me);     // deny — $me may send a new request later
$me->unfriend($you);              // remove an accepted friendship

befriend() returns null (no-op) when a pending, accepted, or blocked relationship already exists in either direction.

Blocking

$me->block($you);     // works with or without a prior friendship
$me->unblock($you);   // only the blocker can unblock

Blocking removes any pending, accepted, or denied row between the pair and remembers who did the blocking. Calling block() again on someone you already blocked is a no-op that keeps the original row.

Blocks are per-direction. A → B and B → A are separate rows and can both exist at once. Being blocked does not let you clear the other party's block: if A blocks B and B then blocks A, B's later unblock($a) removes only B's own block — A's block stays in force, and B still cannot befriend A. Only the blocker can lift their own block.

With a mutual block in place there are two rows for the pair, one per direction. getFriendship() returns the caller's own row ($me->getFriendship($you) returns the row $me sent), so it never tells you who blocked whom — use hasBlocked() / isBlockedBy() for that.

Checks

$me->isFriendsWith($you);            // accepted friendship exists
$me->hasSentFriendRequestTo($you);   // outgoing pending request
$me->hasFriendRequestFrom($you);     // incoming pending request
$me->hasBlocked($you);               // $me blocked $you
$me->isBlockedBy($you);              // $you blocked $me

All checks return bool and are safe to call when no relationship exists.

Lists

$me->getFriends();               // Collection of friend MODELS (accepted only)
$me->getFriends(perPage: 15);    // LengthAwarePaginator of friend models

$me->getFriendship($you);        // the Friendship row for the pair, or null
$me->getAllFriendships();        // every Friendship row involving $me
$me->getPendingFriendships();
$me->getAcceptedFriendships();
$me->getDeniedFriendships();
$me->getBlockedFriendships();

$me->getFriendRequests();        // incoming pending Friendship rows

List methods return real Collections with sender/recipient eager-loaded — no N+1 queries.

Mutual friends & counts

$me->getMutualFriends($you);             // Collection of shared friends
$me->getMutualFriends($you, limit: 10);
$me->getMutualFriendsCount($you);
$me->getFriendsCount();
$me->getPendingRequestsCount();          // incoming pending requests

Events

Every action fires an event carrying the Friendship model ($event->friendship):

Event Fired when
Ghanem\Friendship\Events\FriendRequestSent a request is sent
Ghanem\Friendship\Events\FriendRequestAccepted a request is accepted
Ghanem\Friendship\Events\FriendRequestDenied a request is denied
Ghanem\Friendship\Events\Unfriended an accepted friendship is removed
Ghanem\Friendship\Events\UserBlocked a model is blocked
Ghanem\Friendship\Events\UserUnblocked a model is unblocked

Listen to them like any Laravel event, e.g. to send a notification when a request arrives.

Statuses

Ghanem\Friendship\Status is a backed enum: Pending (0), Accepted (1), Denied (2), Blocked (3). The status attribute on Friendship is cast to it automatically.

Upgrading from 1.x

v2 is a clean break. Database rows are compatible (same table, same status values), but the API changed:

  • Requires PHP 8.1+ and Laravel 10+ (was PHP 5.5 / Laravel 5).

  • Model Ghanem\Friendship\Models\Friend is now Friendship.

  • Status class constants are now enum cases (Status::Pending instead of Status::PENDING); stored integer values are unchanged.

  • blockFriendRequest() / unblockFriendRequest() are now block() / unblock(). Unblocking deletes the row instead of resetting it to pending.

  • isFriendsWith() returns bool and matches accepted friendships only (it previously counted rows of any status).

  • Accept/deny now verify direction — only the recipient of a request can accept or deny it.

  • A denied request no longer prevents sending a new one.

  • List methods return Collections of Friendship models (previously arrays) and no longer take $limit/$offset arguments; use getFriends(perPage:) for pagination.

  • The friends() morphMany relation was removed; use getFriends().

  • New: events, getMutualFriends(), counts, hasFriendRequestFrom(), hasSentFriendRequestTo().

  • Existing installs: the migration now declares a unique index on (sender, recipient). Your existing table works without it, but you can add it in a new migration for duplicate protection.

    Warning: v1 had no duplicate protection, so an existing table may already contain duplicate (sender, recipient) rows. Adding the index on such a table fails mid-migration with SQLSTATE[23000]. Check first, and de-duplicate before adding the index.

    Check for duplicates:

    SELECT sender_id, sender_type, recipient_id, recipient_type, COUNT(*) AS n
    FROM friendships
    GROUP BY sender_id, sender_type, recipient_id, recipient_type
    HAVING n > 1;

    De-duplicate, keeping the lowest id of each group (MySQL syntax):

    DELETE f1 FROM friendships f1
    JOIN friendships f2
      ON f1.sender_id = f2.sender_id
     AND f1.sender_type = f2.sender_type
     AND f1.recipient_id = f2.recipient_id
     AND f1.recipient_type = f2.recipient_type
    WHERE f1.id > f2.id;

    Multi-table DELETE ... JOIN is MySQL-only. On PostgreSQL and SQLite use a subquery instead:

    DELETE FROM friendships
    WHERE id NOT IN (
        SELECT MIN(id) FROM friendships
        GROUP BY sender_id, sender_type, recipient_id, recipient_type
    );

    Review what the duplicates are before deleting — keeping the lowest id keeps the oldest row, which may not be the row with the status you want. Then add the index:

    Schema::table('friendships', function (Blueprint $table) {
        $table->unique(
            ['sender_id', 'sender_type', 'recipient_id', 'recipient_type'],
            'friendships_sender_recipient_unique'
        );
    });

Filament admin panel

friendship-filament on Packagist

ghanem/friendship-filament adds a Filament panel plugin for this package:

  • a read-only Friendships resource for browsing and moderating every row, filterable by status and sender type;
  • a relation manager you attach to your own UserResource, listing a model's friendships in both directions with the other party resolved;
  • moderation actions — approve a pending request, unfriend, lift a block — all routed through this package's public API, so your event listeners still fire;
  • stats and chart widgets for the dashboard.
composer require ghanem/friendship-filament

It requires Filament 4 or 5, which means Laravel 11.28+ — this core package still supports Laravel 10, the plugin does not.

Limitations

The unique index is directionalA → B and B → A are distinct rows. Two befriend() calls in opposite directions racing at the same instant can therefore both succeed, leaving two pending requests for the same pair; the database cannot reject the second one. Everything else stays consistent (accepting either one makes them friends), but if you expect that kind of concurrency, wrap the call in your own lock keyed on the pair.

Testing

composer test

Related packages

Credits

This package began life in 2015 as an MIT-licensed package by DraperStudio / PackageBackup, whose original repository is no longer published. It has been maintained by GAIT ever since, across Laravel 5 through 13. The original copyright notice is retained in LICENSE.

License

MIT. See LICENSE.

More in this ecosystem