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

Category Laravel Package

vanilo/category

Vanilo Category adds hierarchical product categories to Laravel e-commerce apps. It ships a tree-structured category model with nesting, ordering, and slugs, plus the contracts and helpers needed to organize catalog products under parent and child categories.

View on GitHub
Deep Wiki
Context7

Getting Started

vanilo/category is the category-tree module of the Vanilo e-commerce stack. It is a thin model layer over kalnoy/nestedset: it gives you a hierarchical Taxonomy/Taxon (a.k.a. Category) model pair, migrations, configurable model bindings, and a service provider — no controllers, routes, or admin UI. In practice you either install it directly or, more commonly, get it transitively via vanilo/framework.

Minimal setup:

composer require vanilo/category
php artisan vendor:publish   # pick the category config + migration tags
php artisan migrate

The service provider (Vanilo\Category\Providers\CategoryServiceProvider) is auto-discovered on modern Laravel; register it manually only if you have discovery disabled. Confirm the exact publish tag with php artisan vendor:publish --list — module tags are version-specific.

Where to look first, in this order:

  1. src/Contracts/ — Taxonomy and Taxon interfaces. These define the public API surface; everything else is an implementation detail.
  2. src/Models/ — Taxonomy, Taxon, and Category (a convenience subclass/alias of Taxon). This is where the nested-set trait and relations live.
  3. database/migrations/ — note _lft, _rgt, parent_id, taxonomy_id, and the slug column. These columns are the whole game.
  4. config/vanilo.category.php — model class mappings you can override.
  5. src/Providers/CategoryServiceProvider.php — container bindings between contracts and concrete models.

First real use case — a two-level navigation menu:

use Vanilo\Category\Models\Taxonomy;
use Vanilo\Category\Models\Category;

$taxonomy = Taxonomy::create(['name' => 'Catalog']);

$books   = Category::create(['name' => 'Books', 'taxonomy_id' => $taxonomy->id]);
$fiction = Category::create(['name' => 'Fiction', 'taxonomy_id' => $taxonomy->id]);
$fiction->appendToNode($books)->save();   // nested-set move, then persist

$menu = Category::where('taxonomy_id', $taxonomy->id)->get()->toTree();

If you are on a pre-4.x Vanilo, Category is the primary model and Taxonomy/Taxon do not exist — check the version constraint before copy-pasting anything from the docs.

Implementation Patterns

Treat the slug as your public identifier, the id as your internal one. Categories are almost always looked up by slug in storefronts. Add a route-model-binding override or a findBySlug scope rather than matching in controllers. If you need localized or per-taxonomy slugs, enforce uniqueness with a composite index on (taxonomy_id, slug).

Resolve the contract, query the concrete class. Controller/service constructors should type-hint Vanilo\Category\Contracts\Taxon or Taxonomy so that config-based model swaps keep working. For static queries (::create, ::where), pull the configured class name:

$taxonClass = config('vanilo.category.models.taxon');
$node = $taxonClass::create([...]);

Wrapping both in a small CategoryRepository is the cleanest way to keep the swap-friendly design without littering app() calls.

Build the tree once, cache it, invalidate on write. Nested-set reads are cheap but recursive eager loading is not. For a nav menu:

$tree = Cache::rememberForever('catalog.tree', fn () =>
    Category::where('taxonomy_id', $id)->get()->toTree()
);

Then bust the key in an observer on saved/deleted (and on any move — appendToNode, moveTo, etc., all fire saved). Never with('children.children.children'); depth is unbounded.

Seed in tree order. Create parents first, call appendToNode(...)->save() immediately, and wrap the whole seed in a transaction. Seeding leaves without _lft/_rgt values (or with stale ones) produces silent corruption that only surfaces when toTree() returns nonsense.

Attach products via the product module's pivot. The category module does not own the product relation; the pivot table and relation name come from vanilo/product. Define a taxons()/categories() relation on your own Product extension if you need custom scoping, and always eager-load it alongside the tree.

Expose the tree through an API resource recursively. A CategoryResource that maps children through itself is the standard pattern; guard against cycles with a depth counter, since nested-set bugs can produce them.

Admin/CRUD integration is manual. The canonical write operations are appendToNode, prependToNode, insertBeforeNode, insertAfterNode, moveTo, and delete(). Build drag-and-drop around those, not around direct _lft/_rgt writes.

Gotchas and Tips

  • appendToNode() returns the node, but you still must save(). The single most common bug: the in-memory tree looks right, the DB does not.
  • Nested-set writes are multi-row UPDATEs. Moving or deleting a node rewrites a swath of _lft/_rgt. Wrap bulk reorders in DB::transaction() and avoid doing this inside long-running jobs without locking. Concurrent moves can corrupt the tree.
  • When corruption happens, Category::fixTree() is your recovery tool. Keep it in a tinker or an artisan command; it is not run automatically.
  • Deleting a parent cascades or orphans children depending on the nested-set delete strategy. Decide explicitly whether deleting a category should delete descendants, reparent them, or refuse; enforce it in a policy or observer.
  • Mixing Category and Taxon queries is a footgun. If Category subclasses Taxon, then Taxon::where(...) returns categories too. Be consistent, and document which one your app treats as canonical.
  • A default taxonomy is assumed but not guaranteed. If Category::create() relies on a "default" taxonomy, make sure it is seeded before the first write, or always set taxonomy_id explicitly.
  • Slug collisions resolve silently. Sluggable traits append -1, -2; that is fine for SEO and fatal for tests that assert exact slugs. Assert on id or a stable attribute instead.
  • Soft deletes and unique slug indexes conflict. A soft-deleted row still occupies the constraint. Either drop the unique index in favour of app-level checks or include deleted_at in the index in a way your DB supports.
  • Config caching breaks model swaps. Changing config/vanilo.category.php without php artisan config:clear is a classic "why is it still using the old model" moment.
  • Version-lock the whole Vanilo family. vanilo/category majors track vanilo/support/vanilo/framework; mixing a 4.x category module with a 3.x product module will produce contract/interface mismatches at bind time, not at compile time.
  • The package is low-traffic (single-digit stars). Documentation is thin, so budget time to read src/ and the nested-set docs. Do not expect shipped factories, model policies, or an admin UI — those live in vanilo/admin and your own app.
  • Extension points, in order of usefulness: (1) override model classes in config; (2) bind your own implementations to Taxonomy/Taxon contracts in a service provider (after the package provider so yours wins); (3) observe model events for cache invalidation and audit logging. There are no hooks or macros in this module — if you need behaviour, subclass the configured model.
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.
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle