lecturize/laravel-followers
Add follow/unfollow relationships to any Eloquent model in Laravel. Traits let models follow and be followed polymorphically, with helpers like follow(), unfollow(), toggleFollow(), isFollowing(), plus followers() and following() relations and query scopes.
The package is a small polymorphic pivot layer: a followers table plus two traits, one for the model doing the following and one for the model being followed. There is no feed generation, no notification plumbing, and no UI — it is deliberately a persistence primitive, so budget time for the application-level pieces.
Minimal setup
composer require lecturize/laravel-followers
php artisan vendor:publish --provider="Lecturize\Followers\FollowersServiceProvider"
php artisan migrate
If the provider tag differs in your version, run php artisan vendor:publish and pick the lecturize/followers entries; you want both the config file and the migration. Read the published migration before running it — that file is the real contract of the package and tells you the exact column names (follower_id/follower_type/followable_id/followable_type, plus an accepted flag in recent versions), indexes, and whether a unique constraint exists.
Wire up the models. Put both traits on the models that can follow and be followed (typically User); put only Followable on things that are follow targets only (a Team, Category, or Author profile).
use Lecturize\Followers\Traits\Follower;
use Lecturize\Followers\Traits\Followable;
class User extends Authenticatable
{
use Follower;
use Followable;
}
First use case — a "Follow" button and a profile counter:
$author->follow($user); // or $user->follow($author)
$author->isFollowedBy($user); // true
$author->followers()->count();
$user->followings; // collection of followed models
If those method names don't resolve, check the trait source in vendor/lecturize/laravel-followers/src/Traits — it is a few hundred lines and reading it once will save you an afternoon.
Idempotent follow/unfollow endpoints. Wrap the trait calls in a small service rather than calling them straight from controllers, so you can enforce self-follow guards and emit domain events in one place:
class FollowService
{
public function follow(User $actor, Model $target): void
{
abort_if($actor->is($target), 422, 'You cannot follow yourself.');
if (! $actor->isFollowing($target)) {
$actor->follow($target);
FollowCreated::dispatch($actor, $target);
}
}
}
Note that toggleFollow() is convenient for a UI button but is a poor fit for an API — retries and double-clicks flip state. Prefer explicit follow/unfollow endpoints with an idempotent check.
Follow requests for private accounts. The accepted column (when present) lets you model pending follows: create the row unaccepted, expose pendingFollowRequests() on the target, and only count accepted rows in follower counts. Treat this as a state machine in your own code — the package gives you the column, not the workflow or the authorization.
Polymorphic targets. Because both sides are morphs, a user can follow other users, teams, and topics from the same table:
$user->follow($team);
$user->follow($category);
$user->followings()->with('followable')->get()->groupBy('followable_type');
This is the package's strongest feature and its main trap: never assume $user->followings is a list of User models. Group by type before hydrating, or add typed accessors (followedUsers(), followedTeams()) on your model that filter on the morph type.
Feed queries. Build the timeline yourself from the followed IDs, grouped by type, and paginate the underlying query rather than the relation:
$followedIds = $user->followings()
->where('followable_type', (new User)->getMorphClass())
->pluck('followable_id');
$posts = Post::whereIn('user_id', $followedIds)
->latest()
->paginate(20);
Counts and eager loading. Use withCount where possible instead of calling followers()->count() in a loop; a profile index page with 30 users will otherwise issue 60 queries.
User::withCount(['followers', 'followings'])->paginate();
Authorization. Put the "can this user follow that model" decision in a policy and call it from both the controller and any Blade component that renders the button, so the UI and the API cannot drift. The package performs no permission checks.
Notifications. Notifications on follow require a wrapper. In the service above, FollowCreated can be a plain Laravel event; a queued listener sends the mail/database notification. Do not expect the package to fire events.
composer.json, check its constraint against your Laravel version (Laravel 11/12 support is not guaranteed), and accept that a bug fix may mean forking. The upside: the package is small enough to vendor or copy the two traits and migration into app/ if the constraint blocks an upgrade.App\Models\User to App\Domain\User, every existing row silently stops matching. Register Relation::enforceMorphMap([...]) early and never rename casually.follower_id, follower_type, followable_id, followable_type). If it does not, follow() called twice will create two rows and inflate counts — guard in the service and add the index in your own migration.deleting observer that removes rows where the model is the follower or followable, or schedule a pruning command.accepted semantics. Confirm the default in the migration. If it defaults to true, every existing query that "just counts followers" will include pending requests the moment you enable private accounts. Write counts as where('accepted', true) from day one so behaviour does not change later.utf8mb4. If the migration fails, shorten the *_type columns to 191 or index only the IDs.config:clear.RefreshDatabase and assert against the pivot directly: assertDatabaseHas('followers', ['follower_id' => $a->id, 'followable_type' => $a->getMorphClass()]). Testing via counts alone hides duplicate-row bugs.FollowService so that swapping the package out (or forking it) touches one class instead of every controller.How can I help you explore Laravel packages today?