PHPackages                             ritechoice23/laravel-followable - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Database &amp; ORM](/categories/database)
4. /
5. ritechoice23/laravel-followable

ActiveLibrary[Database &amp; ORM](/categories/database)

ritechoice23/laravel-followable
===============================

A Laravel package to add follow/unfollow functionality to Eloquent models.

1.0.2(4mo ago)1334↓100%[1 PRs](https://github.com/ritechoice23/laravel-followable/pulls)MITPHPPHP ^8.2CI passing

Since Nov 6Pushed 6d agoCompare

[ Source](https://github.com/ritechoice23/laravel-followable)[ Packagist](https://packagist.org/packages/ritechoice23/laravel-followable)[ Docs](https://github.com/ritechoice23/laravel-followable)[ GitHub Sponsors](https://github.com/ritechoice23)[ RSS](/packages/ritechoice23-laravel-followable/feed)WikiDiscussions master Synced today

READMEChangelog (3)Dependencies (12)Versions (5)Used By (0)

Laravel Followable
==================

[](#laravel-followable)

[![Latest Version on Packagist](https://camo.githubusercontent.com/c801fdda31ba779178a731c5062b2224651f650c798ce192afe318a683eecc3b/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f7269746563686f69636532332f6c61726176656c2d666f6c6c6f7761626c652e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/ritechoice23/laravel-followable)[![GitHub Tests Action Status](https://camo.githubusercontent.com/38d8d5be9ea0f2113a7c8e02f6616679c9e8cba0594e994b3d9d9fed5a419769/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f7269746563686f69636532332f6c61726176656c2d666f6c6c6f7761626c652f72756e2d74657374732e796d6c3f6272616e63683d6d6173746572266c6162656c3d7465737473267374796c653d666c61742d737175617265)](https://github.com/ritechoice23/laravel-followable/actions?query=workflow%3Arun-tests+branch%3Amaster)[![GitHub Code Style Action Status](https://camo.githubusercontent.com/6df9ffaefdfaf238f7db36a00c88fde5abf1832775791a7d146fa0123c0760cd/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f7269746563686f69636532332f6c61726176656c2d666f6c6c6f7761626c652f6669782d7068702d636f64652d7374796c652d6973737565732e796d6c3f6272616e63683d6d6173746572266c6162656c3d636f64652532307374796c65267374796c653d666c61742d737175617265)](https://github.com/ritechoice23/laravel-followable/actions?query=workflow%3A%22Fix+PHP+code+style+issues%22+branch%3Amaster)[![Total Downloads](https://camo.githubusercontent.com/aaa2d8a2650ecc1b0f4cf25f04a21d98889f77fcc2d527a497a5c0f636e3d063/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f7269746563686f69636532332f6c61726176656c2d666f6c6c6f7761626c652e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/ritechoice23/laravel-followable)

**The only fully polymorphic follow package that handles mixed follower types elegantly with metadata support.**

A modern Laravel package that adds follow/unfollow functionality to Eloquent models with true bidirectional polymorphism. Any model can follow any other model, with intelligent handling of mixed-type relationships and optimized queries.

Why This Package?
-----------------

[](#why-this-package)

While other popular packages like [overtrue/laravel-follow](https://github.com/overtrue/laravel-follow) and [rennokki/laravel-eloquent-interactions](https://github.com/rennokki/laravel-eloquent-interactions) offer basic follow functionality, they often force you to hardcode the User model or require complex workarounds for heterogeneous relationships.

**Laravel Followable** was built to solve real-world challenges in complex applications where:

- Organizations follow other organizations
- Teams follow users and other teams
- Mixed-type followers need to be handled elegantly (Users + Teams + Organization following the same post)
- You need metadata for analytics (tracking follow sources, campaigns, referrers)
- Mutual relationships and bidirectional queries are essential
- Performance matters (optimized queries, no N+1 problems)

### What Makes It Unique

[](#what-makes-it-unique)

✅ **True Bidirectional Polymorphism** - Both follower AND followable can be ANY model type (User→Team, Team→Team, Organization→User, etc.)

✅ **Intelligent Mixed-Type Handling** - Smart `MixedModelsCollection` for when followers are of different types (Users + Teams + Organizations)

✅ **Rich Metadata Support** - Attach JSON metadata to track source, campaign data, referrer, or any custom attributes

✅ **Flexible Query API** - Separate optimized methods for single-type (`followers()`) vs multi-type (`followersGrouped()`) scenarios

✅ **Mutual Relationships** - Built-in support for identifying mutual follows and connections

✅ **Full MorphMap Support** - Works seamlessly with Laravel's `Relation::morphMap()` for cleaner database storage

✅ **Modern Architecture** - Uses latest Laravel features with 100+ comprehensive tests using Pest PHP

Features
--------

[](#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()` and `whereFollowers()`
- **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
------------

[](#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
-------------

[](#configuration)

The published config file (`config/follow.php`) includes:

```
return [
    'table_name' => 'follows',
    'allow_self_follow' => false,
    'metadata_column' => 'metadata',
];
```

Usage
-----

[](#usage)

### Setup Models

[](#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

[](#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

[](#working-with-followers)

#### Get Actual Follower Models

[](#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

[](#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, 'App\Models\Team' => Collection]

// 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

[](#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

[](#access-follow-pivot-records)

When you need the actual Follow records (e.g., for metadata):

```
// Get Follow pivot records
$followRecords = $team->followRecords;  // Collection

// With eager loading
$followRecords = $team->followRecords()->with('follower')->get();

// Access metadata
foreach ($followRecords as $follow) {
    $metadata = $follow->metadata;
    $followerModel = $follow->follower;
}
```

### Follow with Metadata

[](#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

[](#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

[](#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

[](#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

[](#working-with-followings)

The `CanFollow` trait provides powerful methods to query what models a user is following.

#### Get Actual Followable Models

[](#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

[](#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, 'App\Models\Team' => Collection]

// 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

[](#counting-followings)

```
// Total followings
$totalFollowings = $user->followingCount();

// Count by specific type
$userFollowings = $user->followingCount(User::class);
$teamFollowings = $user->followingCount(Team::class);
```

#### Access Following Records

[](#access-following-records)

When you need the actual Follow records (e.g., for metadata):

```
// Get Follow pivot records
$followingRecords = $user->followingRecords;  // Collection

// With eager loading
$followingRecords = $user->followingRecords()->with('followable')->get();

// Access metadata
foreach ($followingRecords as $follow) {
    $metadata = $follow->metadata;
    $followableModel = $follow->followable;
}
```

Important Notes
---------------

[](#important-notes)

### Use Cases for Different Methods

[](#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

[](#performance-considerations)

The package uses optimized database queries with negligible overhead:

- **Single type queries**: Both `followers()` and `followings()` use efficient JOIN queries (1 query instead of N+1)
- **Multiple types**: `followersGrouped()` and `followingsGrouped()` fetch all types efficiently with batch queries
- **Smart Collection Wrapping**: `MixedModelsCollection` adds ~0.006ms overhead per operation (tested with 100 items)
- **Counting**: Direct COUNT queries on indexed columns
- **All queries leverage database indexes** for fast lookups
- **MorphMap Compatible**: Works seamlessly with morphMap for cleaner storage and better performance

**Benchmark Results** (50 followings, mixed types):

- Single type query: ~1.1ms
- Mixed types (2 types): ~16-20ms (includes multiple type queries + sorting)
- Grouped query: ~3.8ms (most efficient for mixed types)

💡 **Pro Tip**: For mixed-type scenarios, `followingsGrouped()` is 4x faster than `followings()->get()`

### Working with Mixed Follower Types

[](#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
--------------

[](#advanced-usage)

### Prevent Self-Following

[](#prevent-self-following)

By default, models cannot follow themselves. Enable it in config if needed:

```
// config/follow.php
'allow_self_follow' => true,
```

### Idempotent Operations

[](#idempotent-operations)

Following an already-followed model returns `false` without creating duplicates:

```
$user->follow($team);  // true
$user->follow($team);  // false (already following)
```

### Database Indexes

[](#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
-------

[](#testing)

Run the test suite:

```
composer test
```

Run tests with coverage:

```
composer test-coverage
```

Changelog
---------

[](#changelog)

Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.

Contributing
------------

[](#contributing)

Please see [CONTRIBUTING](CONTRIBUTING.md) for details.

Security Vulnerabilities
------------------------

[](#security-vulnerabilities)

Please review [our security policy](../../security/policy) on how to report security vulnerabilities.

Credits
-------

[](#credits)

- [Daramola Babatunde Ebenezer](https://github.com/ritechoice23)
- [All Contributors](../../contributors)

License
-------

[](#license)

The MIT License (MIT). Please see [License File](LICENSE.md) for more information.

###  Health Score

43

↑

FairBetter than 91% of packages

Maintenance89

Actively maintained with recent releases

Popularity15

Limited adoption so far

Community9

Small or concentrated contributor base

Maturity50

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 75% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~2 days

Total

3

Last Release

132d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/cc2c3a32449f76aa8dfca355106fce6c5ade0effe7d22fa265493051e890aa40?d=identicon)[ritechoice23](/maintainers/ritechoice23)

---

Top Contributors

[![ritechoice23](https://avatars.githubusercontent.com/u/62488893?v=4)](https://github.com/ritechoice23 "ritechoice23 (9 commits)")[![dependabot[bot]](https://avatars.githubusercontent.com/in/29110?v=4)](https://github.com/dependabot[bot] "dependabot[bot] (2 commits)")[![github-actions[bot]](https://avatars.githubusercontent.com/in/15368?v=4)](https://github.com/github-actions[bot] "github-actions[bot] (1 commits)")

---

Tags

laravellaravel-followlaravel-followablelaravel-packagesearchlaravelmodeleloquenttraitmetadatacmsbloganalyticsarticlespostsFollowtaxonomycontent managementRelationshipsnormalizationcategorizationpolymorphictrendingritechoice23unfollow

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/ritechoice23-laravel-followable/health.svg)

```
[![Health](https://phpackages.com/badges/ritechoice23-laravel-followable/health.svg)](https://phpackages.com/packages/ritechoice23-laravel-followable)
```

###  Alternatives

[rinvex/laravel-categories

Rinvex Categories is a polymorphic Laravel package, for category management. You can categorize any eloquent model with ease, and utilize the power of Nested Sets, and the awesomeness of Sluggable, and Translatable models out of the box.

471160.2k3](/packages/rinvex-laravel-categories)[lacodix/laravel-model-filter

A Laravel package to filter, search and sort models with ease while fetching from database.

17648.2k](/packages/lacodix-laravel-model-filter)[esensi/model

The base model traits of Esensi

20166.5k1](/packages/esensi-model)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
