laravel-user-tags maintained by mujahidabbas
Laravel User Tags
User-scoped tagging for Laravel Eloquent models. Each user has their own private set of tags that are invisible to and independent from other users' tags.
// User A tags an article
$article->attachUserTag('important'); // Creates User A's "important" tag
// User B tags the same article
Auth::login($userB);
$article->attachUserTag('important'); // Creates User B's separate "important" tag
// Query User A's tagged articles (only sees their own tags)
Auth::login($userA);
$articles = Article::withAnyUserTags(['important'])->get();
Why This Package?
If you've used spatie/laravel-tags and wondered how to give each user their own private tag namespace, this package is for you.
The problem: Global tagging packages share tags across all users. User A's "important" tag is the same as User B's "important" tag.
The solution: This package isolates tags per user. Each user has their own private set of tags that are completely invisible to and independent from other users' tags.
Use cases:
- Personal CRM systems where users organize their own contacts
- Bookmark managers with private collections
- Note-taking apps with personal organization
- Any multi-user app where users need private categorization
Requirements
- PHP 8.1, 8.2, or 8.3
- Laravel 10.x, 11.x, or 12.x
Installation
You can install the package via composer:
composer require mujahidabbas/laravel-user-tags
Publish and run the migrations:
php artisan vendor:publish --tag="user-tags-migrations"
php artisan migrate
Optionally, you can publish the config file:
php artisan vendor:publish --tag="user-tags-config"
Configuration
The published config file (config/user-tags.php) contains:
return [
// Your User model class
'user_model' => 'App\\Models\\User',
// Table names (customize if needed)
'tables' => [
'tags' => 'user_tags',
'taggables' => 'user_taggables',
],
];
Usage
Preparing Your Model
Add the HasUserTags trait to any Eloquent model you want to tag:
use Illuminate\Database\Eloquent\Model;
use MujahidAbbas\LaravelUserTags\Traits\HasUserTags;
class Article extends Model
{
use HasUserTags;
}
Attaching Tags
Use attachUserTag() to add tags to a model. Tags are automatically created if they don't exist:
// Attach a single tag (uses authenticated user)
$article->attachUserTag('important');
// Attach multiple tags at once
$article->attachUserTag(['important', 'read-later', 'favorites']);
// Attach with a specific user (useful in queues, commands, etc.)
$article->attachUserTag('important', $user);
Tags are user-scoped, so User A's "important" tag is completely separate from User B's "important" tag.
Detaching Tags
Use detachUserTag() to remove tags from a model. Non-existent tags are silently ignored:
// Detach a single tag
$article->detachUserTag('important');
// Detach multiple tags
$article->detachUserTag(['important', 'read-later']);
// Detach with a specific user
$article->detachUserTag('important', $user);
Note: This only removes the association; the tag itself is not deleted.
Syncing Tags
Use syncUserTags() to replace all of a user's tags on a model with a new set:
// Replace all current user's tags with exactly these
$article->syncUserTags(['priority', 'work']);
// Sync with a specific user
$article->syncUserTags(['priority', 'work'], $user);
Important: syncUserTags() only affects the specified user's tags. Other users' tags on the same model remain untouched.
Accessing Tags
The userTags() relationship returns all tags attached to a model (from all users):
// Get all tags on this model
$tags = $article->userTags;
// Eager load tags
$articles = Article::with('userTags')->get();
// Filter to a specific user's tags
$myTags = $article->userTags->where('user_id', auth()->id());
Querying by Tags
Query models that have specific tags using scope methods:
// Get posts with ANY of these tags (OR logic)
$posts = Post::withAnyUserTags(['important', 'urgent'])->get();
// Get posts with ALL of these tags (AND logic)
$posts = Post::withAllUserTags(['priority', 'review'])->get();
// Get posts WITHOUT specific tags
$posts = Post::withoutUserTags(['archived', 'spam'])->get();
// Chain scopes
$posts = Post::withAnyUserTags(['tech', 'science'])
->withoutUserTags(['archived'])
->get();
// With a specific user (useful in queues, commands)
$posts = Post::withAnyUserTags(['important'], $user)->get();
Note: Query scopes respect user isolation - User A's scope queries only consider User A's tags.
Using a Custom User
All tag methods accept an optional user parameter. This is useful when the authenticated user is not available:
// In a queue job
public function handle()
{
$user = User::find($this->userId);
$article->attachUserTag('processed', $user);
}
// In a console command
$user = User::find($userId);
$article->syncUserTags(['archived'], $user);
If no user is authenticated and none is passed, a NoAuthenticatedUserException is thrown:
use MujahidAbbas\LaravelUserTags\Exceptions\NoAuthenticatedUserException;
try {
$article->attachUserTag('important');
} catch (NoAuthenticatedUserException $e) {
// Handle missing user
}
Important: MorphMap Configuration
This package uses polymorphic relationships to attach tags to any model. By default, Laravel stores the full class name (e.g., App\Models\Article) in the database.
Why MorphMap matters:
- If you rename or move a model class, existing tag associations break
- Full class names are verbose and expose your application structure
- Consistent short names make database queries cleaner
Recommended: Configure MorphMap in your AppServiceProvider:
use Illuminate\Database\Eloquent\Relations\Relation;
public function boot(): void
{
Relation::enforceMorphMap([
'article' => \App\Models\Article::class,
'post' => \App\Models\Post::class,
'comment' => \App\Models\Comment::class,
// Add all models that use HasUserTags
]);
}
This ensures stable, clean identifiers in your user_taggables table instead of full class paths.
Alternatives
If you need global tags (shared across all users), use spatie/laravel-tags instead. It's excellent for content categorization, blog post tags, and similar use cases where tags are public.
This package is designed to complement spatie/laravel-tags, not replace it. You can use both in the same application:
spatie/laravel-tagsfor public/global tags (e.g., blog categories)mujahidabbas/laravel-user-tagsfor private/user-scoped tags (e.g., personal bookmarks)
Testing
composer test
Static Analysis
composer analyse
Code Style
composer format
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.
Roadmap
Future releases may include:
- Tag management utilities (
Tag::forUser(), tag search/autocomplete) - Events (
UserTagAttached,UserTagDetached,UserTagCreated) - Tag metadata (colors, icons via JSON column)
- Tag usage statistics
See the GitHub issues for feature requests and discussions.
Credits
License
The MIT License (MIT). Please see License File for more information.