x-laravel/followable
A Laravel package that adds follow and unfollow behavior to your models. Attach polymorphic follower relationships, check whether a model is followed, and query followers or followings with a simple, reusable trait.
This is a new package, so start from scratch rather than adapting existing code.
composer require the package, then publish its config and migrations (php artisan vendor:publish and pick the package's provider/tags — confirm the exact tags in the README, since v1.0.0 ships a configurable connection, table and model). Run php artisan migrate.Contracts namespace (what your models must implement), the Concerns traits (what behaviour you get for free), and the published config (connection/table/model overrides). The contracts are the stable API — type-hint against them, not against concrete models.User), implement Contracts\CanFollow and use Concerns\Follower. On every model that can be followed (User, Team, Post, Project…), implement Contracts\HasFollowers and use Concerns\Followable. One model can use both — a User that follows other users is the normal case.$user->follow($team); // create the follow
$user->isFollowing($team); // true
$team->followers()->count(); // list/count
$user->followings(); // what this user follows
$user->unfollow($team);
That single flow — follow, check state, list/count, unfollow — covers most of what you'll build day to day. Everything else (approval, mutuals, pruning) layers on top.
Two-sided traits, both on User. Because Follower and Followable are separate concerns, keep the naming straight in your head: followings() / sentFollows() are outgoing, followers() / receivedFollows() are incoming. Reach for followings() when rendering "who I follow" and followers() when rendering "who follows me".
Approval-gated follows. If the followable model implements Contracts\RequiresFollowApproval, a follow() call lands as a request rather than an active follow. Build the UI around the three states, not two:
$user->isFollowing($team); // active
$user->hasRequestedToFollow($team); // pending, sent by this user
$team->hasFollowRequestFrom($user); // pending, seen from the owner side
$team->followRequests(); // inbox for the owner
$team->acceptFollowRequest($user);
$team->rejectFollowRequest($user);
Make the approval decision a method on the model (e.g. private team → approve, public team → auto-accept) so the rule lives next to the data rather than in controllers.
Render follow state in lists without N+1. This is the highest-value part of the package for typical UI work. On an index of users/posts, combine withFollowedBy($user) with isFollowedBy():
$posts = Post::query()->withFollowedBy($user)->paginate();
// blade: $post->isFollowedBy($user) — no query per row
Without the scope you'll fire one isFollowing() query per row on every listing page; this is the first thing to fix if a page feels slow.
Mutual follows ("friends", "connections"). isMutuallyFollowing(), mutualFollowers() and the withFollowsBack() scope let you build a connections tab or a feed restricted to people who follow back. Use withFollowsBack() when filtering a query (e.g. "posts from mutuals") and mutualFollowers() when you just need the collection.
Owner-initiated removal. unfollow() is the follower acting; removeFollower() is the followable model removing someone. Different authorisation, different controller — don't collapse them into one endpoint.
Events as the integration seam. Followed, Unfollowed, FollowRequested, FollowRequestAccepted, FollowRequestRejected, FollowRequestCancelled are all dispatched after commit, which is the right default. Put notifications, activity-log writes, feed fan-out and analytics in queued listeners (ShouldQueue) so a follow action stays a single fast write.
Pruning. Deleted models and followers are cleaned up automatically, but orphaned rows can still exist (raw deletes, truncations, restored dumps). The package registers a pruneable model; make sure model:prune is actually scheduled in your app and tune the configured frequency and chunk size for your table size. Don't also hand-roll your own cleanup job for the same table.
Config and multi-connection setups. If follows live on a different connection (separate reporting DB, tenant split), set it in config before migrating — the migration must target the same connection or you'll create the table in the wrong place. Custom pivot model is the extension point when you need extra columns on the follow row; subclass the package model rather than editing the published migration in place if you want upgrade paths.
follow() means "following". With RequiresFollowApproval in play, after a follow() you must branch on hasRequestedToFollow() vs isFollowing(). Code that fires a "You're now following X" flash unconditionally will lie to users on private accounts.isFollowing() before calling, and catch the failure in controllers where a stale form can resubmit.withFollowedBy() is per-viewer. It takes the user whose state you're resolving, so it doesn't compose with response caching of an authenticated listing. Cache per-user or cache the neutral list and resolve state separately.Relation::enforceMorphMap() after you have data, existing rows still hold FQCNs — plan a data migration, and set the morph map before launch.RefreshDatabase/DatabaseTransactions) the job/listener side effects may not appear where you expect. Assert wiring with Event::fake([Followed::class]) plus Event::assertDispatched() — faking works regardless — and test listener bodies directly with $this->travel/handle() rather than relying on end-to-end dispatch inside a rolled-back transaction.followers()/followings() are polymorphic; loading them without constraints on a mixed-type collection will pull unexpected models. Constrain by type or withCount() when you only need numbers — withCount('followers') is far cheaper than loading the relation for a card grid.Unfollowed event or add observers if you denormalise follower counts.CanFollow/HasFollowers in policies and form requests keeps your app decoupled from the package's concrete classes, which makes replacement or a version bump far less painful.How can I help you explore Laravel packages today?