ritechoice23 / laravel-followable
A Laravel package to add follow/unfollow functionality to Eloquent models.
Fund package maintenance!
ritechoice23
Installs: 49
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/ritechoice23/laravel-followable
Requires
- php: ^8.2
- illuminate/contracts: ^11.0||^12.0
- spatie/laravel-package-tools: ^1.16
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.14
- nunomaduro/collision: ^8.8
- orchestra/testbench: ^10.0.0||^9.0.0
- pestphp/pest: ^4.0
- pestphp/pest-plugin-arch: ^4.0
- pestphp/pest-plugin-laravel: ^4.0
- phpstan/extension-installer: ^1.4
- phpstan/phpstan-deprecation-rules: ^2.0
- phpstan/phpstan-phpunit: ^2.0
This package is auto-updated.
Last update: 2025-11-06 21:00:31 UTC
README
A modern, minimal Laravel package that adds follow/unfollow functionality to Eloquent models. Any model can follow any other model with full polymorphic relationships support.
Features
- Fully Polymorphic: Any model can follow any other model (User → Team, User → User, Team → Team, etc.)
- Simple API: Intuitive methods like
follow(),unfollow(),toggleFollow(),isFollowing() - Expressive Scopes: Chainable query scopes like
whereFollowing()andwhereFollowers() - Metadata Support: Attach custom JSON metadata to follows
- Zero Configuration: Works out of the box with sensible defaults
- Full Test Coverage: Comprehensive Pest PHP test suite included
Installation
Install the package via composer:
composer require ritechoice23/laravel-followable
Publish and run the migrations:
php artisan vendor:publish --tag="followable-migrations"
php artisan migrate
Optionally, publish the config file:
php artisan vendor:publish --tag="followable-config"
Configuration
The published config file (config/follow.php) includes:
return [ 'table_name' => 'follows', 'allow_self_follow' => false, 'metadata_column' => 'metadata', ];
Usage
Setup Models
Add traits to your models:
use Illuminate\Database\Eloquent\Model; use Ritechoice23\Followable\Traits\CanFollow; use Ritechoice23\Followable\Traits\HasFollowers; class User extends Model { use CanFollow; // Can follow other models use HasFollowers; // Can be followed by other models } class Team extends Model { use HasFollowers; // Can be followed }
Basic Operations
// Follow a model $user->follow($team); // Unfollow a model $user->unfollow($team); // Toggle follow status $user->toggleFollow($team); // Check if following if ($user->isFollowing($team)) { // User is following the team } // Check if followed by if ($team->isFollowedBy($user)) { // Team is followed by user } // Get counts $user->followingCount(); // Number of models user is following $team->followersCount(); // Number of followers team has
Working with Followers
Get Actual Follower Models
The followers() method returns actual follower models (User, Team, etc.), not Follow pivot records:
// Get all followers (single type or homogeneous followers) $followers = $team->followers()->get(); // Paginate followers $followers = $team->followers()->paginate(15); // Filter and query like any Eloquent relation $activeFollowers = $team->followers() ->where('is_active', true) ->orderBy('name') ->get(); // Quick pagination helper $followers = $team->followersPaginated(10); // Filter by specific follower type $userFollowers = $team->followersOfType(User::class)->get();
Handling Mixed Follower Types
When a model has followers of different types (e.g., both Users and Teams), use followersGrouped():
// ✅ Best approach for mixed types $grouped = $post->followersGrouped(); // Returns: ['App\Models\User' => Collection<User>, 'App\Models\Team' => Collection<Team>] // Iterate through each type foreach ($grouped as $type => $followers) { echo "{$type}: {$followers->count()} followers\n"; foreach ($followers as $follower) { // $follower is the actual User or Team model echo $follower->name; } } // Or query specific types separately $userFollowers = $post->followers(User::class)->get(); $teamFollowers = $post->followers(Team::class)->get();
Counting Followers
// Total followers $totalFollowers = $team->followersCount(); // Count by specific type $userFollowers = $team->followersCount(User::class); $teamFollowers = $team->followersCount(Team::class);
Access Follow Pivot Records
When you need the actual Follow records (e.g., for metadata):
// Get Follow pivot records $followRecords = $team->followRecords; // Collection<Follow> // With eager loading $followRecords = $team->followRecords()->with('follower')->get(); // Access metadata foreach ($followRecords as $follow) { $metadata = $follow->metadata; $followerModel = $follow->follower; }
Follow with Metadata
Attach custom data to follows:
$user->follow($team, [ 'source' => 'web', 'campaign' => 'summer_2024', 'referrer' => 'homepage' ]); // Access and modify metadata (metadata is cast as array) $follow = Follow::first(); // Set metadata $metadata = $follow->metadata ?? []; $metadata['key'] = 'value'; $follow->metadata = $metadata; $follow->save(); // Get metadata $value = $follow->metadata['key'] ?? null; // Remove metadata key $metadata = $follow->metadata; unset($metadata['key']); $follow->metadata = $metadata; $follow->save();
Query Scopes
Find models based on follow relationships:
// Find all users following a team $users = User::whereFollowing($team)->get(); // Find all teams followed by a user $teams = Team::whereFollowers($user)->get(); // Chain with other queries $activeUsers = User::whereFollowing($team) ->where('status', 'active') ->orderBy('created_at', 'desc') ->get();
Polymorphic Follows
Follow any model type:
$user->follow($organization); // User → Organization $user->follow($anotherUser); // User → User $team->follow($anotherTeam); // Team → Team $user->follow($post); // User → Post
Relationships
Access follow relationships:
// Get all follows made by user (Follow records) $user->followingRecords; // Get actual follower models $actualFollowers = $team->followers()->get(); // Get Follow pivot records $followRecords = $team->followRecords; // Eager load relationships on Follow records $follows = Follow::with(['follower', 'followable'])->get();
Working with Followings
The CanFollow trait provides powerful methods to query what models a user is following.
Get Actual Followable Models
The followings() method returns actual followable models (User, Team, etc.), not Follow pivot records:
// Get all followings (single type or homogeneous followings) $followings = $user->followings()->get(); // Paginate followings $followings = $user->followings()->paginate(15); // Filter and query like any Eloquent relation $activeTeams = $user->followings() ->where('is_active', true) ->orderBy('name') ->get(); // Quick pagination helper $followings = $user->followingsPaginated(10); // Filter by specific followable type $teamFollowings = $user->followingsOfType(Team::class)->get();
Handling Mixed Followable Types
When a user follows different types of models (e.g., both Users and Teams), use followingsGrouped():
// ✅ Best approach for mixed types $grouped = $user->followingsGrouped(); // Returns: ['App\Models\User' => Collection<User>, 'App\Models\Team' => Collection<Team>] // Iterate through each type foreach ($grouped as $type => $followables) { echo "{$type}: {$followables->count()} followings\n"; foreach ($followables as $followable) { // $followable is the actual User or Team model echo $followable->name; } } // Or query specific types separately $userFollowings = $user->followings(User::class)->get(); $teamFollowings = $user->followings(Team::class)->get();
Counting Followings
// Total followings $totalFollowings = $user->followingCount(); // Count by specific type $userFollowings = $user->followingCount(User::class); $teamFollowings = $user->followingCount(Team::class);
Access Following Records
When you need the actual Follow records (e.g., for metadata):
// Get Follow pivot records $followingRecords = $user->followingRecords; // Collection<Follow> // With eager loading $followingRecords = $user->followingRecords()->with('followable')->get(); // Access metadata foreach ($followingRecords as $follow) { $metadata = $follow->metadata; $followableModel = $follow->followable; }
Important Notes
Use Cases for Different Methods
For Followers (HasFollowers trait):
Use followers() when:
- All followers are of the same type (e.g., only Users)
- You're filtering by a specific type
- You need to chain Eloquent query methods
- You're working with pagination
Use followersGrouped() when:
- A model has followers of multiple different types
- You need followers organized by their model type
- You want to iterate through each type separately
Use followRecords when:
- You need access to the Follow pivot records
- You want to work with follow metadata
- You need the follow timestamps or other pivot data
For Followings (CanFollow trait):
Use followings() when:
- Following models of the same type (e.g., only Teams)
- You're filtering by a specific type
- You need to chain Eloquent query methods
- You're working with pagination
Use followingsGrouped() when:
- Following multiple different types of models
- You need followings organized by their model type
- You want to iterate through each type separately
Use followingRecords when:
- You need access to the Follow pivot records
- You want to work with follow metadata
- You need the follow timestamps or other pivot data
Performance Considerations
The package uses optimized database queries:
- Single type queries: Both
followers()andfollowings()use efficient JOIN queries (1 query instead of N+1) - Multiple types:
followersGrouped()andfollowingsGrouped()fetch all types efficiently - Counting: Direct COUNT queries on indexed columns
- All queries leverage database indexes for fast lookups
Working with Mixed Follower Types
If your model can be followed by different types (polymorphic scenario):
// ✅ Recommended: Use followersGrouped() $grouped = $post->followersGrouped(); foreach ($grouped as $type => $followers) { // Each type's followers as proper model instances } // ✅ Alternative: Query specific types $userFollowers = $post->followers(User::class)->get(); $teamFollowers = $post->followers(Team::class)->get(); // ✅ Or: Query and merge manually $users = $post->followers(User::class)->get(); $teams = $post->followers(Team::class)->get(); $allFollowers = $users->merge($teams);
Advanced Usage
Prevent Self-Following
By default, models cannot follow themselves. Enable it in config if needed:
// config/follow.php 'allow_self_follow' => true,
Idempotent Operations
Following an already-followed model returns false without creating duplicates:
$user->follow($team); // true $user->follow($team); // false (already following)
Database Indexes
The migration includes optimized indexes for performance:
- Unique composite index on follower and followable (prevents duplicates)
- Index on followable_type and followable_id (for lookups)
- Index on follower_type and follower_id (for reverse lookups)
- Index on created_at (for trending queries)
Testing
Run the test suite:
composer test
Run tests with coverage:
composer test-coverage
Changelog
Please see CHANGELOG for more information on what has changed recently.
Contributing
Please see CONTRIBUTING for details.
Security Vulnerabilities
Please review our security policy on how to report security vulnerabilities.
Credits
License
The MIT License (MIT). Please see License File for more information.