Search by

laraarabdev / laravel-turbo-tags

salemB88

A high-performance polymorphic tagging system with translation support for Laravel

Package info

github.com/LaraArabDev/laravel-turbo-tags

Homepage

pkg:composer/laraarabdev/laravel-turbo-tags

Fund package maintenance!

LaraArabDev

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 12

dev-main 2026-09-02 02:39 UTC

README

TurboTags Banner

TurboTags

Translatable, hierarchical, polymorphic tags for Laravel
وسوم متعددة اللغات وهرمية ومتعددة الأشكال لـ Laravel

Latest Version Total Downloads License PHP Laravel

Tests codecov Static Analysis Code Style

PHP 8.2+ · Laravel 11 / 12 · 95%+ Test Coverage · PHPStan Level Max

LaraArabDev — We build, develop, empower, and contribute. An Arab open-source community crafting production-grade Laravel packages.
LaraArabDev — نبني، نطوّر، نُمكّن، ونُساهم. مجتمع عربي مفتوح المصدر يصنع حزم Laravel احترافية وجاهزة للإنتاج.

Quick Start · Why TurboTags · Features · Usage · Configuration · Testing · عربي

What is TurboTags?

TurboTags is a complete tagging solution for Laravel. Tag any Eloquent model — posts, products, users, anything — with tags that support multiple languages, parent-child hierarchies, typed categories, ownership scoping, and smart caching.

Unlike other tagging packages, TurboTags ships with built-in translation support (no need for spatie/laravel-translatable or any translation package), hierarchical tag trees with circular reference protection, and a configurable cache layer that auto-invalidates — all in a single, zero-dependency package.

ما هو TurboTags؟

TurboTags هو حل متكامل للوسوم في Laravel. أضف وسومًا لأي موديل Eloquent — مقالات، منتجات، مستخدمين، أي شيء — مع دعم تعدد اللغات، الهيكلة الشجرية (أب-ابن)، التصنيف حسب النوع، ملكية الوسوم، والتخزين المؤقت الذكي. لا يحتاج أي حزمة ترجمة خارجية — الترجمة مدمجة.

🚀 Quick Start

composer require laraarabdev/laravel-turbo-tags

No external dependencies required. Translations, slugs, caching — everything is built in.

لا يحتاج أي حزمة خارجية. الترجمة، الروابط النصية، التخزين المؤقت — كل شيء مدمج.

Publish the config and migrations:

php artisan vendor:publish --tag="laravel-turbo-tags-config"
php artisan vendor:publish --tag="laravel-turbo-tags-migrations"
php artisan migrate

Add the trait to any model:

use LaraArabDev\TurboTags\Concerns\HasTags;

class Post extends Model
{
    use HasTags;
}

Start tagging:

$post->attachTags(['Laravel', 'PHP', 'Open Source']);

That's it. You're ready.

Requirement Version
PHP 8.2+
Laravel 11 or 12
External packages None

💡 Why TurboTags?

Built-in translations — no extra packages

Other tagging packages require spatie/laravel-translatable for multilingual support. TurboTags stores translations natively as JSON with a smart fallback chain: requested locale → primary locale → app locale → fallback locale → first available. Zero configuration for single-language apps, full power for multilingual ones.

$tag = Tag::create([
    'name' => ['en' => 'Technology', 'ar' => 'تقنية', 'fr' => 'Technologie'],
]);

$tag->getTranslatedName('ar'); // "تقنية"
$tag->getTranslatedName();     // Uses your app locale automatically

Hierarchical tags out of the box

Build tag trees like Programming → PHP → Laravel. Load entire subtrees with a single eager-loaded query. Navigate up with ancestors(), down with descendants(). Circular references are automatically prevented at the database and model level.

Type-safe with PHP enums

Categorize tags using native PHP BackedEnum — no magic strings:

enum TagType: string {
    case Language = 'language';
    case Framework = 'framework';
}

$post->attachTag('Laravel', TagType::Framework);

Production-tested quality

  • 95%+ code coverage across 101 tests
  • PHPStan level max — zero errors
  • Pint-clean code style
  • Soft deletes, cache invalidation, circular reference protection

📦 Features

Feature Description
Polymorphic tagging Tag any Eloquent model via morphToMany
Built-in translations Multilingual tag names stored as JSON — no extra packages needed
Hierarchical tags Parent-child tree structure with recursive eager loading
Tag types Categorize tags (e.g., language, framework) with string or BackedEnum
Tag ownership Scope tags globally, per user, or per team
Query scopes withAllTags, withAnyTags, withoutTags, withTagsOfType
Auto slug generation URL-friendly slugs with uniqueness enforcement
Tag suggestions Search-as-you-type with configurable min length and limit
Smart caching Configurable cache layer with automatic invalidation on changes
Sync operations Sync tags globally or per type, preserving other types
Metadata Attach arbitrary JSON data to any tag
Ordering Optional order_column for custom sort order
Circular reference protection Prevents invalid parent-child cycles, even through soft-deleted tags
Soft deletes Tags support soft deleting with nullOnDelete for children
Batch operations findOrCreateMany with deferred cache flushing for performance

📖 Usage

Attaching & detaching tags

// By name — auto-creates tags if they don't exist
$post->attachTag('Laravel');
$post->attachTags(['Laravel', 'PHP', 'Testing']);

// By model instance or ID
$tag = Tag::findOrCreate('Laravel');
$post->attachTag($tag);
$post->attachTag($tag->id);

// With a type
$post->attachTag('PHP', 'language');
$post->attachTag('Laravel', 'framework');

// Detach
$post->detachTag('Laravel');
$post->detachTags(['Laravel', 'PHP']);
$post->removeAllTags();

Syncing tags

// Replace all tags on the model
$post->syncTags(['Laravel', 'PHP']);

// Replace only tags of a specific type (other types are preserved)
$post->syncTagsWithType(['Python', 'Ruby'], 'language');

Checking tags

$post->hasTag('Laravel');                // true / false
$post->hasAllTags(['Laravel', 'PHP']);   // true if ALL are present
$post->hasAnyTags(['Laravel', 'Ruby']); // true if ANY is present

Query scopes

// Models tagged with ALL of these
Post::withAllTags(['Laravel', 'PHP'])->get();

// Models tagged with ANY of these
Post::withAnyTags(['Laravel', 'Python'])->get();

// Models NOT tagged with these
Post::withoutTags(['Deprecated'])->get();

// Models with tags of a specific type
Post::withTagsOfType('language')->get();

// Eager load tags / tag count
Post::withTagsLoaded()->get();
Post::withTagCount()->get();

🌳 Hierarchical Tags

Build tag trees with parent-child relationships. Useful for categories, taxonomies, nested navigation, and more.

// Create a tree: Programming → PHP → Laravel
$programming = Tag::create(['name' => ['en' => 'Programming', 'ar' => 'برمجة']]);
$php = Tag::create(['name' => ['en' => 'PHP'], 'parent_id' => $programming->id]);
$laravel = Tag::create(['name' => ['en' => 'Laravel'], 'parent_id' => $php->id]);

// Navigate the tree
$laravel->parent;               // → PHP tag
$programming->children;         // → [PHP]
$laravel->isLeaf();             // true (no children)
$programming->isRoot();         // true (no parent)

// Load entire subtree in one query
$programming->load('childrenRecursive');

// Get all ancestors (bottom → top)
$laravel->ancestors();          // [PHP, Programming]

// Get all descendants (flattened)
$programming->descendants();    // [PHP, Laravel]

// Query root tags only
Tag::roots()->get();

Circular references are prevented automatically. Setting a tag's parent to itself or to one of its own descendants throws InvalidArgumentException. This protection works even through soft-deleted tags.

🌍 Translations (Built-in — No Extra Packages)

Important: TurboTags does NOT require spatie/laravel-translatable or any translation package. Translations are built into the package using JSON columns with a smart fallback chain.

ملاحظة مهمة: لا يحتاج TurboTags إلى حزمة spatie/laravel-translatable أو أي حزمة ترجمة أخرى. الترجمة مدمجة في الحزمة باستخدام أعمدة JSON مع سلسلة بديلة ذكية.

// Create a tag with multiple translations
$tag = Tag::create([
    'name' => ['en' => 'Technology', 'ar' => 'تقنية', 'fr' => 'Technologie'],
]);

// Get translated name
$tag->getTranslatedName('en');  // "Technology"
$tag->getTranslatedName('ar');  // "تقنية"
$tag->getTranslatedName();      // Uses app locale automatically

// Set or update a translation
$tag->setTranslatedName('Tecnologia', 'es');
$tag->save();

// Check and list
$tag->hasTranslation('ar');     // true
$tag->getTranslations();        // ['en' => 'Technology', 'ar' => 'تقنية', ...]

Fallback chain: requested locale → config locale.primaryapp()->getLocale() → config locale.fallback → first available translation. Your users always see something meaningful.

👤 Tag Ownership

Scope tags to specific users, teams, or any model. Perfect for user-generated tags or multi-tenant apps.

// Create a user-owned tag
$tag = Tag::create([
    'name' => ['en' => 'Favorites'],
    'owner_type' => User::class,
    'owner_id' => $user->id,
]);

// Query scopes
Tag::global()->get();               // Tags with no owner
Tag::ownedBy($user)->get();         // Tags belonging to this user
Tag::availableTo($user)->get();     // Global + this user's tags

🏷️ Tag Types & Enums

Organize tags into categories using strings or type-safe PHP enums:

// Using strings
$post->attachTag('PHP', 'language');
Tag::ofType('language')->get();

// Using BackedEnum (recommended)
enum TagType: string
{
    case Category = 'category';
    case Label = 'label';
    case Language = 'language';
}

$tag = Tag::findOrCreate('PHP', TagType::Language, 'en');
$post->attachTag('Laravel', TagType::Category);
$post->syncTagsWithType(['PHP', 'Go'], TagType::Language);
Tag::ofType(TagType::Language)->get();

🔍 Tag Suggestions

Build autocomplete / search-as-you-type with built-in suggestion support:

$suggestions = Tag::suggestions('Lara');                    // Tags containing "Lara"
$suggestions = Tag::suggestions('PH', 'language', 'en', 5); // With type, locale, limit

Minimum search length is configurable to prevent overly broad queries.

⚡ Caching

Enable the built-in cache layer for high-traffic apps. Cache is automatically invalidated whenever tags are created, updated, or deleted — no manual flushing needed.

// config/laravel-turbo-tags.php
'cache' => [
    'enabled' => true,
    'ttl' => 3600,             // seconds
    'store' => null,            // null = default cache store
    'key_prefix' => 'turbo_tags',
],

Cached methods: findOrCreate, allCached, allOfTypeCached, suggestions.

// Get all tags from cache
$tags = Tag::allCached();
$tags = Tag::allOfTypeCached('language');

// Batch operations use deferred flushing (1 flush instead of N)
$tags = Tag::findOrCreateMany(['PHP', 'Laravel', 'Go']);

// Manual flush if needed
Tag::flushTagCache();

📦 findOrCreate

Efficiently find or create tags — with caching and batch support:

// Single tag
$tag = Tag::findOrCreate('Laravel', 'framework', 'en');

// Batch — 1 query to find existing + creates for missing, 1 cache flush total
$tags = Tag::findOrCreateMany(['PHP', 'Laravel', 'Testing'], 'framework', 'en');

⚙️ Configuration

After publishing (php artisan vendor:publish --tag="laravel-turbo-tags-config"):

return [
    // Custom tag model (extend Tag if you need custom behavior)
    'tag_model' => \LaraArabDev\TurboTags\Models\Tag::class,

    // Table names (customize if they conflict with your app)
    'tables' => [
        'tags' => 'tags',
        'taggables' => 'taggables',
    ],

    // Locale settings — null uses app locale
    'locale' => [
        'primary' => null,       // e.g., 'en' or 'ar'
        'fallback' => null,      // e.g., 'en'
    ],

    // Slug generation from tag names
    'slugger' => [
        'source' => 'name',
        'generate_on_create' => true,   // Auto-generate slug on create
        'generate_unique' => true,      // Append -2, -3, etc. for uniqueness
    ],

    // Performance caching
    'cache' => [
        'enabled' => false,      // Enable in production for speed
        'ttl' => 3600,           // Cache lifetime in seconds
        'store' => null,         // null = default cache driver
        'key_prefix' => 'turbo_tags',
    ],

    // Performance tuning
    'performance' => [
        'chunk_size' => 1000,
    ],

    // Suggestion / autocomplete settings
    'suggestions' => [
        'limit' => 10,           // Max suggestions returned
        'min_length' => 2,       // Min search string length
    ],
];

🧪 Testing

The package has 101 tests with 95%+ code coverage and passes PHPStan at max level.

# Run tests
composer test

# Run tests with coverage report
composer test-coverage

# Static analysis (PHPStan max level)
composer analyse

# Code style check (Laravel Pint)
composer format-test

# Fix code style
composer format

📋 API Reference

HasTags Trait Methods

Method Description
attachTag($tags, $type, $locale) Attach one or more tags
attachTags($tags, $type, $locale) Alias for attachTag
detachTag($tags, $type, $locale) Detach one or more tags
detachTags($tags, $type, $locale) Alias for detachTag
syncTags($tags, $type, $locale) Replace all tags (or per type)
syncTagsWithType($tags, $type, $locale) Sync tags of a specific type
removeAllTags() Remove all tags from the model
hasTag($tag, $type, $locale) Check if model has a tag
hasAllTags($tags, $type, $locale) Check if model has all given tags
hasAnyTags($tags, $type, $locale) Check if model has any of the given tags

Tag Model Methods

Method Description
Tag::findOrCreate($name, $type, $locale) Find or create a single tag
Tag::findOrCreateMany($names, $type, $locale) Batch find or create
Tag::allCached() Get all tags (cached)
Tag::allOfTypeCached($type) Get tags by type (cached)
Tag::suggestions($search, $type, $locale, $limit) Search tag suggestions
Tag::flushTagCache() Flush all cached tags
Tag::roots() Query scope: root tags only
Tag::global() Query scope: unowned tags
Tag::ownedBy($model) Query scope: tags owned by model
Tag::availableTo($model) Query scope: global + owned
Tag::ofType($type) Query scope: filter by type
Tag::containing($search, $locale) Query scope: name contains
Tag::ordered($direction) Query scope: order by column
Tag::withSlug($slug) Query scope: find by slug

Tag Instance Methods

Method Description
$tag->parent Get parent tag
$tag->children Get direct children
$tag->childrenRecursive Get full subtree (eager-loadable)
$tag->ancestors() Get all ancestors (bottom-up)
$tag->descendants() Get all descendants (flattened)
$tag->isRoot() Has no parent?
$tag->isLeaf() Has no children?
$tag->getTranslatedName($locale) Get translated name
$tag->setTranslatedName($value, $locale) Set translation
$tag->getTranslations() Get all translations
$tag->hasTranslation($locale) Check if translation exists
$tag->taggedModels(Model::class) Get models tagged with this tag

🌐 Arabic Documentation

التوثيق بالعربية

TurboTags هو حزمة وسوم شاملة لـ Laravel تتميز بالآتي:

المميزات الرئيسية

  • وسوم متعددة الأشكال — أضف وسومًا لأي موديل Eloquent
  • ترجمة مدمجة — لا تحتاج أي حزمة خارجية مثل Spatie. الأسماء تُخزّن كـ JSON متعدد اللغات
  • وسوم هرمية — علاقات أب-ابن مع حماية من المراجع الدائرية
  • أنواع الوسوم — صنّف الوسوم حسب النوع (مثلاً: لغة، إطار عمل، فئة)
  • ملكية الوسوم — وسوم عامة أو خاصة بمستخدم أو فريق
  • تخزين مؤقت ذكي — مع إبطال تلقائي عند أي تغيير
  • اقتراحات بحث — دعم البحث أثناء الكتابة
  • روابط نصية تلقائية (Slugs) — مع ضمان التفرد

التثبيت

composer require laraarabdev/laravel-turbo-tags
php artisan vendor:publish --tag="laravel-turbo-tags-config"
php artisan vendor:publish --tag="laravel-turbo-tags-migrations"
php artisan migrate

الاستخدام الأساسي

use LaraArabDev\TurboTags\Concerns\HasTags;

class Post extends Model
{
    use HasTags;
}

// إضافة وسوم
$post->attachTags(['Laravel', 'PHP']);

// إنشاء وسم بترجمات متعددة
$tag = Tag::create([
    'name' => ['en' => 'Technology', 'ar' => 'تقنية'],
]);

// الحصول على الترجمة
$tag->getTranslatedName('ar'); // "تقنية"

// إنشاء شجرة وسوم
$programming = Tag::create(['name' => ['ar' => 'برمجة']]);
$php = Tag::create(['name' => ['ar' => 'بي إتش بي'], 'parent_id' => $programming->id]);

// البحث عن وسوم جذرية فقط
Tag::roots()->get();

// الوسوم التابعة
$programming->descendants(); // [بي إتش بي]

إعداد اللغة العربية كلغة أساسية

// config/laravel-turbo-tags.php
'locale' => [
    'primary' => 'ar',
    'fallback' => 'en',
],

بهذا الإعداد، سيُرجع getTranslatedName() الترجمة العربية تلقائيًا بدون تمرير اللغة.

Changelog

Please see CHANGELOG for more information on what has changed recently.

Contributing

We welcome contributions from the community! Please see CONTRIBUTING for details. All contributions must meet our acceptance criteria.

Commit Convention

This project follows Conventional Commits. All commit messages and PR titles must follow this format:

type(scope): description

Allowed types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert, hotfix

Branch naming: type/short-description (e.g., feat/add-caching, fix/slug-generation)

Security

Please review our security policy on how to report security vulnerabilities. Do not open a public issue.

Policies

Document Description
Code of Conduct Community standards and expectations
Contributing Guide How to contribute, conventions, and acceptance criteria
Security Policy How to report vulnerabilities responsibly
Support How to get help
Governance Decision-making process and roles
Code Owners Required reviewers by area

License

The MIT License (MIT). Please see License File for more information.

Built with ❤ by LaraArabDev — An Arab open-source community
صُنع بـ ❤ بواسطة LaraArabDev — مجتمع عربي مفتوح المصدر