icon picker
Laravel Tags & Categories Package — Development Guide

🚀 Overview

A headless Laravel package to manage:
🏷️ Tags (with synonyms via groups)
📦 Categories
🔗 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)
Category model logic
Relationship traits logic
Custom model override behavior

Feature Tests

Tagging a model
Categorizing a model
Syncing/updating
Finding models by tag group name

Setup

Use SQLite in-memory
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
Want to print your doc?
This is not the way.
Try clicking the ⋯ next to your doc name or using a keyboard shortcut (
CtrlP
) instead.