Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Categories Laravel Package

angkor/laravel-categories

A Laravel package for managing hierarchical categories and attaching them to Eloquent models. It offers nested category trees, polymorphic relationships, ordering, and helpers for querying or organizing categorized content, with migrations and configuration included out of the box.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a low-traffic fork (angkor/laravel-categories) of rinvex/laravel-categories — zero dependents and no stars means you should treat the upstream README as the de-facto documentation and spend the first 30 minutes confirming the fork hasn't drifted. Check composer.json for the Laravel version constraint and the pinned rinvex/laravel-support version before committing to it; if the fork lags behind your Laravel version, prefer pinning upstream and patching locally.

Install:

composer require angkor/laravel-categories

If the fork isn't published to Packagist, add a VCS repository entry first:

"repositories": [{ "type": "vcs", "url": "https://github.com/angkor/laravel-categories" }],
"require": { "angkor/laravel-categories": "dev-master" }

Publish and migrate:

php artisan vendor:publish --provider="Rinvex\Categories\Providers\CategoriesServiceProvider"
php artisan migrate

Publishing gives you config/rinvex.categories.php (table names, model bindings, slug/translation settings) plus the categories and categorizables migrations. Publish the migrations rather than relying on loadMigrationsFrom — if you already have a categories table or a legacy taxonomy, you'll need to rename tables in config and adjust the migration before running it.

First use case — tag blog posts with a small taxonomy tree:

  1. Add the trait to the model you want to categorize:
use Rinvex\Categories\Traits\Categorizable;

class Post extends Model
{
    use Categorizable;
}
  1. Seed a two-level tree in a seeder:
use Rinvex\Categories\Models\Category;

$root = Category::create(['name' => 'Engineering', 'slug' => 'engineering']);
$child = Category::create(['name' => 'Laravel', 'slug' => 'laravel']);
$child->appendToNode($root)->save();

$post->categories()->sync([$child->id]);
  1. Read it back:
$post->categories;                                  // collection
Post::whereHas('categories', fn ($q) => $q->where('slug', 'laravel'))->get();
$root->descendants;                                 // nested-set tree

If categories() doesn't resolve, open Rinvex\Categories\Traits\Categorizable — the method name and pivot table are defined there and are the two things most likely to differ in a fork.

Implementation Patterns

Treat the taxonomy as reference data, edited rarely. Nested sets rewrite lft/rgt on every insert, move, and delete. Day-to-day you attach posts to categories — that's a pivot write, cheap. Category CRUD belongs in an admin panel or seeded fixtures, not in a hot request path.

Build a picker, not a tree editor, for the 90% case. For most apps the useful shape is: a flat select of leaf categories (or indented labels from Category::toTree()), and sync() on save.

$categoryTree = Category::toTree();  // or ->getNested() / ->toFlatTree()

Query by slug, not by ID, in routes. Slugs are stable and human-readable; IDs leak your seed order.

// routes/web.php
Route::get('/blog/{categorySlug}', function (string $categorySlug) {
    $category = Category::where('slug', $categorySlug)->firstOrFail();

    return view('blog.index', [
        'posts' => Post::whereHas('categories', fn ($q) => $q->where(
            'categories.id', $category->id
        ))->latest()->paginate(20),
        'category' => $category,
    ]);
});

Include descendants when a category is a parent. A post filed under "Laravel" should surface under "Engineering" too:

$ids = $category->descendantsAndSelf()->pluck('id');
$posts = Post::whereHas('categories', fn ($q) => $q->whereIn('categories.id', $ids))->get();

Watch the alias: the whereHas closure qualifies against the categories table, so alias columns as categories.id — mixing in the pivot's category_id silently returns empty sets.

Translations. name/description are typically JSON-backed via spatie/laravel-translatable. Write per-locale inside a locale switch, and keep slugs locale-neutral:

app()->setLocale('ar');
$category->name = 'الهندسة';
$category->save();

Integrate via a service, not the model, in business logic. Wrap category resolution so you can swap the underlying package later without touching controllers:

class CategoryResolver
{
    public function forSlug(string $slug): ?Category
    {
        return Category::where('slug', $slug)->first();
    }

    public function tree(): Collection
    {
        return Category::toTree();
    }
}

This single seam is your insurance policy against the fork going stale — you can reimplement it on kalnoy/nestedset and a plain Category model in an afternoon.

Seeding / imports: wrap bulk category creation in DB::transaction() and call Category::fixTree() once at the end, rather than trusting appendToNode for thousands of rows.

Gotchas and Tips

Enforce a morph map before your first write. Without it, categorizables.categorizable_type stores full class names (App\Models\Post). Rename or namespace a model and every historical row is orphaned.

// AppServiceProvider::boot()
Relation::enforceMorphMap(['post' => \App\Models\Post::class]);

Do it before seeding; retrofitting requires a data migration.

Nested sets and global scopes don't mix. If you add a where('locale', ...) or tenant scope to the Category model, lft/rgt calculations will corrupt the tree because the library computes bounds across the unscoped table. Keep category tables unscoped; scope the pivot or the parent model instead.

Broken trees are silent. A failed transaction mid-move leaves lft/rgt inconsistent and queries start returning wrong descendants. Add a health check to your deploy or admin panel:

if (Category::isBroken()) {
    Category::fixTree();
}

Slug uniqueness + translations collide. A unique index on slug is fine for a single-language site. If you translate names and let the sluggable package derive slugs from translated names, two locales can produce the same slug. Set slugs explicitly (or slugify from a fixed source field) and drop the unique index if you need per-locale slugs.

Config changes require a cache flush. Rinvex packages register config and often cache model resolution. After renaming tables or swapping the model binding in config/rinvex.categories.php:

php artisan config:clear && php artisan cache:clear

Model swapping is the primary extension point. The config exposes a models.category key; bind your own subclass to add scopes, accessors, or a HasFactory trait. Do this rather than editing vendor files — the fork is unlikely to carry your patches.

Deleting a parent category moves its children, it doesn't cascade. Decide explicitly what happens to posts: reparent to the deleted node's parent, or detach in the same transaction.

DB::transaction(function () use ($category) {
    $category->children()->each(fn ($c) => $c->appendToNode($category->parent)->save());
    $category->delete();
});

Watch the dependency tree. The fork pulls rinvex/laravel-support, spatie/laravel-sluggable, spatie/laravel-translatable, and a nested-set implementation. A Laravel major bump will break the fork before it breaks upstream — pin exact versions in composer.json and add a smoke test that seeds a tree and reads descendants, so a dependency update that corrupts lft/rgt fails CI rather than production.

When to walk away. If you need soft deletes on categories, per-tenant taxonomies, or tags-as-categories with high write volume, this package's nested-set model fights you. A plain categories table + categorizables pivot (or spatie/laravel-tags for flat tagging) is less code than the workarounds.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource