A flexible and robust Laravel package that provides comprehensive flagging functionality for Eloquent models. This package allows any model to flag any other model with different flag types such as like, follow, favorite, bookmark, upvote, downvote, and custom types through a sophisticated multi-table architecture.
- Features
- Requirements
- Installation
- Quick Start
- Database Architecture
- Usage Guide
- API Reference
- Advanced Usage
- Default Flag Types
- Performance Considerations
- Troubleshooting
- Contributing
- Security
- Credits
- License
- Flexible Flagging System: Any model can flag any other model with polymorphic relationships
- Multiple Flag Types: Support for like, follow, favorite, bookmark, upvote, downvote, and unlimited custom types
- Sophisticated Architecture: Four-table design for optimal performance and flexibility
- Easy Integration: Simple traits to add flagging capabilities to your models
- Facade Support: Clean API through Laravel facades
- Database Migrations: Automatic database structure setup with proper indexes and constraints
- Default Seeders: Pre-configured flag types ready to use
- Laravel Auto-Discovery: Automatic service provider and facade registration
- Polymorphic Relationships: Full support for different model types as flaggers and targets
- Unique Constraints: Prevents duplicate flags with database-level constraints
- Performance Optimized: Proper indexing and efficient queries
- PHP 8.0 or higher
- Laravel 9.0, 10.0, 11.0, or 12.0
composer require sowailem/flagableThe package will automatically register its service provider and facade through Laravel's auto-discovery feature.
Run the migrations to create the necessary database tables:
php artisan migrateTo populate the database with default flag types:
php artisan db:seed --class="Sowailem\Flagable\Database\Seeders\FlagTypeSeeder"This will create the following flag types: like, follow, favorite, bookmark, upvote, downvote.
Add the appropriate traits to your models:
<?php
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Database\Eloquent\Model;
use Sowailem\Flagable\Traits\CanFlag;
use Sowailem\Flagable\Traits\Flagable;
// User model (can flag other models)
class User extends Authenticatable
{
use CanFlag;
// Your existing model code...
}
// Post model (can be flagged by other models)
class Post extends Model
{
use Flagable;
// Your existing model code...
}
// Comment model (can both flag and be flagged)
class Comment extends Model
{
use CanFlag, Flagable;
// Your existing model code...
}$user = User::find(1);
$post = Post::find(1);
// Flag a post
$flag = $user->flag($post, 'like');
// Check if user has flagged the post
if ($user->hasFlagged($post, 'like')) {
echo "User likes this post!";
}
// Unflag a post
$user->unflag($post, 'like');
// Get flag count for a post
$likeCount = $post->flagCount('like');
$totalFlags = $post->flagCount(); // All flag types
// Get all users who liked a post
$likers = $post->flaggers('like', User::class);
// Check if post is flagged by a specific user
$isLiked = $post->isFlaggedBy($user, 'like');The package uses a sophisticated four-table architecture for maximum flexibility and performance:
Stores available flag types (like, follow, etc.)
- id (Primary Key)
- name (Unique String) - Flag type nameStores model class names that can be flagged
- id (Primary Key)
- name (String) - Fully qualified class nameLinks flag types to target model types (pivot table)
- id (Primary Key)
- flag_type_id (Foreign Key to flag_types)
- flag_target_id (Foreign Key to flag_targets)
- UNIQUE(flag_type_id, flag_target_id)Stores individual flag records
- id (Primary Key)
- flag_link_id (Foreign Key to flag_links)
- flagger_type (String) - Polymorphic type
- flagger_id (Big Integer) - Polymorphic ID
- created_at, updated_at (Timestamps)
- UNIQUE(flag_link_id, flagger_type, flagger_id)
- INDEX(flagger_type, flagger_id)FlagType ──┐
├── FlagLink ──── Flag ──── Flagger (Polymorphic)
FlagTarget ──┘
Add this trait to models that can flag other models:
use Sowailem\Flagable\Traits\CanFlag;
class User extends Model
{
use CanFlag;
}Available Methods:
flag(Model $target, string $type): Flagunflag(Model $target, string $type): boolhasFlagged(Model $target, ?string $type = null): boolflags(): MorphMany- Get all flags created by this model
Add this trait to models that can be flagged by other models:
use Sowailem\Flagable\Traits\Flagable;
class Post extends Model
{
use Flagable;
}Available Methods:
isFlaggedBy(Model $flagger, ?string $type = null): boolflagCount(?string $type = null): intflaggers(string $type, string $flaggerModel): Collectionflags(): HasManyThrough- Get all flags for this model
use Sowailem\Flagable\Facades\Flag;
// Manage flag types
$flagType = Flag::addFlagType('custom_type');
$removed = Flag::removeFlagType('old_type');
// Direct flagging operations
$flag = Flag::flag($user, $post, 'like');
$unflagged = Flag::unflag($user, $post, 'like');
// Query operations
$isFlagged = Flag::isFlaggedBy($post, $user, 'like');
$count = Flag::getFlagCount($post, 'like');
$flaggers = Flag::getFlaggers($post, 'like', User::class);Creates a flag record for the target model.
Parameters:
$target- The model to be flagged$type- The flag type (e.g., 'like', 'follow')
Returns: Flag model instance
Example:
$flag = $user->flag($post, 'like');Removes a flag record for the target model.
Parameters:
$target- The model to unflag$type- The flag type to remove
Returns: bool - True if flag was removed, false otherwise
Example:
$removed = $user->unflag($post, 'like');Checks if this model has flagged the target model.
Parameters:
$target- The model to check$type- Optional flag type filter
Returns: bool
Example:
$hasLiked = $user->hasFlagged($post, 'like');
$hasAnyFlag = $user->hasFlagged($post); // Any flag typeGets all flags created by this model.
Returns: MorphMany relationship
Example:
$userFlags = $user->flags()->get();
$recentFlags = $user->flags()->where('created_at', '>', now()->subDays(7))->get();Checks if this model is flagged by the specified flagger.
Parameters:
$flagger- The model that might have flagged this model$type- Optional flag type filter
Returns: bool
Example:
$isLiked = $post->isFlaggedBy($user, 'like');
$hasAnyFlag = $post->isFlaggedBy($user);Gets the count of flags for this model.
Parameters:
$type- Optional flag type filter
Returns: int
Example:
$likeCount = $post->flagCount('like');
$totalFlags = $post->flagCount();Gets all models that have flagged this model with the specified type.
Parameters:
$type- The flag type$flaggerModel- The class name of the flagger model
Returns: Collection
Example:
$likers = $post->flaggers('like', User::class);
$followers = $user->flaggers('follow', User::class);Gets all flag records for this model.
Returns: HasManyThrough relationship
Example:
$postFlags = $post->flags()->get();
$recentFlags = $post->flags()->where('created_at', '>', now()->subDays(7))->get();Creates a new flag type.
Parameters:
$name- The flag type name
Returns: FlagType model instance
Removes a flag type.
Parameters:
$name- The flag type name to remove
Returns: bool
Creates a flag record.
Parameters:
$flagger- The model creating the flag$target- The model being flagged$type- The flag type
Returns: Flag model instance
Removes a flag record.
Parameters:
$flagger- The model removing the flag$target- The model being unflagged$type- The flag type
Returns: bool
Checks if target is flagged by flagger.
Gets flag count for target.
Gets all flaggers for target.
You can create custom flag types dynamically:
use Sowailem\Flagable\Facades\Flag;
// Add custom flag types
Flag::addFlagType('report');
Flag::addFlagType('spam');
Flag::addFlagType('inappropriate');
// Use them immediately
$user->flag($post, 'report');// Flag multiple posts
$posts = Post::whereIn('id', [1, 2, 3])->get();
foreach ($posts as $post) {
$user->flag($post, 'like');
}
// Get flag counts for multiple posts
$posts = Post::with(['flags' => function ($query) {
$query->whereHas('link.type', function ($q) {
$q->where('name', 'like');
});
}])->get();
foreach ($posts as $post) {
$likeCount = $post->flagCount('like');
echo "Post {$post->id} has {$likeCount} likes\n";
}// Get posts with more than 10 likes
$popularPosts = Post::whereHas('flags', function ($query) {
$query->whereHas('link.type', function ($q) {
$q->where('name', 'like');
});
}, '>', 10)->get();
// Get users who liked specific posts
$postIds = [1, 2, 3];
$likers = User::whereHas('flags', function ($query) use ($postIds) {
$query->whereHas('link', function ($q) use ($postIds) {
$q->whereHas('target', function ($target) {
$target->where('name', Post::class);
})->whereHas('type', function ($type) {
$type->where('name', 'like');
});
})->whereIn('flagger_id', function ($subQuery) use ($postIds) {
$subQuery->select('id')->from('posts')->whereIn('id', $postIds);
});
})->get();// In your User model
public function likedPosts()
{
return $this->belongsToMany(Post::class, 'flags', 'flagger_id', 'flagger_id')
->whereHas('flags.link.type', function ($query) {
$query->where('name', 'like');
})
->where('flagger_type', User::class);
}
// In your Post model
public function likers()
{
return $this->belongsToMany(User::class, 'flags', 'flagger_id', 'flagger_id')
->whereHas('flags.link.type', function ($query) {
$query->where('name', 'like');
})
->where('flagger_type', User::class);
}The package comes with six predefined flag types that are created when you run the seeder:
like- General approval or appreciationfollow- Subscribe to updates or contentfavorite- Mark as preferred or specialbookmark- Save for later referenceupvote- Positive voting (Reddit-style)downvote- Negative voting (Reddit-style)
// All default types are immediately available
$user->flag($post, 'like');
$user->flag($anotherUser, 'follow');
$user->flag($post, 'bookmark');
$user->flag($comment, 'upvote');The package automatically creates the following indexes for optimal performance:
- Unique constraint on
flag_types.name - Unique constraint on
flag_links(flag_type_id, flag_target_id) - Unique constraint on
flags(flag_link_id, flagger_type, flagger_id) - Index on
flags(flagger_type, flagger_id)
// Efficient: Use specific flag type
$likeCount = $post->flagCount('like');
// Less efficient: Count all flags then filter
$allFlags = $post->flags()->get();
$likeCount = $allFlags->where('link.type.name', 'like')->count();
// Efficient: Eager load relationships
$posts = Post::with(['flags.link.type'])->get();
// Efficient: Use database aggregation
$popularPosts = Post::withCount(['flags' => function ($query) {
$query->whereHas('link.type', function ($q) {
$q->where('name', 'like');
});
}])->having('flags_count', '>', 10)->get();// Cache flag counts
$cacheKey = "post_{$post->id}_like_count";
$likeCount = Cache::remember($cacheKey, 3600, function () use ($post) {
return $post->flagCount('like');
});
// Cache popular posts
$popularPosts = Cache::remember('popular_posts', 1800, function () {
return Post::withCount(['flags' => function ($query) {
$query->whereHas('link.type', function ($q) {
$q->where('name', 'like');
});
}])->orderBy('flags_count', 'desc')->take(10)->get();
});Make sure you've run composer dump-autoload after installation.
Ensure you're running the migrations in the correct order. The package migrations are numbered to run in sequence.
The package prevents duplicate flags at the database level. If you're getting constraint violations, check if the flag already exists before creating it:
if (!$user->hasFlagged($post, 'like')) {
$user->flag($post, 'like');
}Use eager loading and database-level aggregations:
// Instead of this:
foreach ($posts as $post) {
$post->flagCount('like'); // N+1 query problem
}
// Do this:
$posts = Post::withCount(['flags' => function ($query) {
$query->whereHas('link.type', function ($q) {
$q->where('name', 'like');
});
}])->get();
foreach ($posts as $post) {
echo $post->flags_count; // No additional queries
}Use chunking for bulk operations:
Post::chunk(100, function ($posts) {
foreach ($posts as $post) {
// Process each post
$likeCount = $post->flagCount('like');
}
});Enable query logging to debug performance issues:
DB::enableQueryLog();
// Your flagging operations here
$user->flag($post, 'like');
// Check executed queries
$queries = DB::getQueryLog();
dd($queries);Always validate flag types before using them:
use Sowailem\Flagable\Models\FlagType;
$validTypes = FlagType::pluck('name')->toArray();
if (in_array($requestedType, $validTypes)) {
$user->flag($post, $requestedType);
} else {
throw new InvalidArgumentException("Invalid flag type: {$requestedType}");
}Contributions are welcome! Please follow these guidelines:
- Fork the repository and create your feature branch
- Write tests for any new functionality
- Follow PSR-12 coding standards
- Update documentation for any API changes
- Submit a pull request with a clear description
# Clone your fork
git clone https://github.com/yourusername/flagable.git
cd flagable
# Install dependencies
composer install
# Run tests (when available)
composer test
# Check code style
composer cs-checkWhen reporting issues, please include:
- Laravel version
- PHP version
- Package version
- Steps to reproduce
- Expected vs actual behavior
- Any error messages
If you discover any security-related issues, please email [email protected] instead of using the issue tracker.
- Always validate user input before creating flags
- Consider rate limiting flag creation to prevent abuse
- Implement proper authorization checks in your controllers
- Be cautious with mass assignment when using flag data
// Example authorization check
public function flagPost(Request $request, Post $post)
{
$this->authorize('flag', $post);
$request->validate([
'type' => 'required|string|in:like,follow,favorite,bookmark'
]);
auth()->user()->flag($post, $request->type);
}- Abdullah Sowailem - Creator and maintainer
- All contributors who have helped improve this package
The MIT License (MIT). Please see License File for more information.
Flagable provides a flexible and robust way to implement flagging functionality in Laravel applications. The sophisticated four-table architecture allows for multiple flag types (like, follow, favorite, etc.) and supports any model flagging any other model with optimal performance and data integrity.