🚀 Overview
A headless Laravel package to manage:
🏷️ Tags (with synonyms via groups) 🔗 Polymorphic assignments ⚙️ Fully extensible models, logic, and querying 🧱 Folder Structure
packages/
└── TagsCategories/
├── src/
│ ├── Models/
│ │ ├── Tag.php
│ │ ├── Category.php
│ │ └── TagGroup.php
│ ├── Traits/
│ │ ├── HasTags.php
│ │ └── HasCategories.php
│ ├── Contracts/
│ │ └── TagContract.php
│ ├── Providers/
│ │ └── TagsCategoriesServiceProvider.php
│ ├── Database/
│ │ ├── migrations/
│ │ └── seeders/
│ ├── Actions/
│ ├── Queries/
│ ├── Config/
│ │ └── tags_categories.php
│ ├── Support/
│ ├── Tests/
│ │ ├── Unit/
│ │ └── Feature/
│ └── TagsCategories.php
├── composer.json
└── README.md
⚙️ Config (tags_categories.php)
return [
'models' => [
'tag' => \Vendor\TagsCategories\Models\Tag::class,
'category' => \Vendor\TagsCategories\Models\Category::class,
'tag_group' => \Vendor\TagsCategories\Models\TagGroup::class,
],
];
🗃️ Database Design
tags
| id | name | tag_group_id (nullable) | timestamps |
categories
| id | name | timestamps |
taggables (polymorphic)
| id | tag_id | taggable_type | taggable_id | timestamps |
categorizables (polymorphic)
| id | category_id | categorizable_type | categorizable_id | timestamps |
tag_groups
| id | name | timestamps |
A tag belongs to one group (via tag_group_id) Group can be optional (nullable) Reflects synonym-style logic naturally and efficiently 🧠 Core Business Logic
HasTags Trait
public function tags()
{
return $this->morphToMany(config('tags_categories.models.tag'), 'taggable');
}
public function syncTags(array $tagIds)
{
$this->tags()->sync($tagIds);
}
public function hasTag($tagName): bool
{
return $this->tags->contains('name', $tagName);
}
HasCategories Trait
Similar to HasTags — use morphToMany and sync support.
TagGroup model
public function tags()
{
return $this->belongsToMany(config('tags_categories.models.tag'));
}
public static function findByName(string $name): ?self
{
return static::where('name', $name)->first();
}
Query Helpers
// Get all models with tag or synonym (via group)
TagGroup::findByName('funny')->tags->pluck('id');
Model::whereHas('tags', fn($q) => $q->whereIn('id', $ids))->get();
🧪 Testing Plan (TDD)
Unit Tests
Tag model logic (synonym groups, names, lookups) Relationship traits logic Custom model override behavior Feature Tests
Finding models by tag group name Setup
Load migrations from package Use factories inside TagsCategories\Tests\Factories 🛠️ Service Provider
public function register()
{
$this->mergeConfigFrom(__DIR__.'/../Config/tags_categories.php', 'tags_categories');
$this->app->bind(TagContract::class, config('tags_categories.models.tag'));
// same for others
}
public function boot()
{
$this->loadMigrationsFrom(__DIR__.'/../Database/migrations');
}
🧪 Test Example
public function test_tag_can_be_assigned_to_model()
{
$model = TestModel::factory()->create();
$tag = Tag::factory()->create();
$model->tags()->attach($tag);
$this->assertTrue($model->tags->contains($tag));
}
✅ Development Tips
Always resolve models via config Bind contracts to allow testing/extending Favor Laravel-native features (morphs, config(), bind()) Provide expressive methods like hasTag, addTag, syncTags, etc. Stick to TDD — write tests before traits/relations