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

Tomato Category Module Laravel Package

tomatophp/tomato-category-module

Tomato Category Module adds flexible category management to Laravel and Tomato PHP apps. Create nested, sortable categories with CRUD screens, tree views, and seamless admin panel integration to organize products, posts, and content. Built for Tomato PHP's modular ecosystem.

View on GitHub
Deep Wiki
Context7

Getting Started

Assume nothing about the API surface — read three files first. The package ships no real documentation beyond a description, so ground yourself in this order:

  1. vendor/tomatophp/tomato-category-module/src/ — the service provider tells you what is auto-registered (routes, migrations, translations, menu entries).
  2. database/migrations/* — this is the authoritative schema. Confirm the table names (conventionally categories plus a polymorphic pivot such as categorizables), the parent-column name, and whether ordering uses order/_lft/_rgt.
  3. src/Models/Category.php — the traits on it answer the two questions that matter most: which tree strategy (adjacency list vs nested set vs recursive CTE) and which translation strategy (likely spatie/laravel-translatable, so name is stored as JSON).

Minimal install.

composer require tomatophp/tomato-category-module
php artisan vendor:publish --provider="TomatoPHP\Category\CategoryServiceProvider"
php artisan migrate

If the provider doesn't appear in bootstrap/providers.php, check the package's composer.json extra.laravel.providers and whether Tomato's module registry (config/tomato.php or the tomato/modules mechanism) needs an entry. Splade/Tomato modules often register a menu item automatically — if the category screen doesn't show in the admin nav, that registration is usually the culprit, not the CRUD itself.

First use case — prove the round trip end to end before touching domain models:

  1. Log into the Tomato admin, create one parent category and one child under it with en and ar names.
  2. Attach it to a throwaway model:
$post->categories()->attach($category->id);   // or sync([...]) if the relation is BelongsToMany
$post->load('categories');
  1. Render the tree in a Blade/Splade view so you see the nesting and the locale fallback work.

That single loop exposes the migration, the relation name, the translation behaviour, and the tree shape in about twenty minutes. Everything after that is repetition.

Implementation Patterns

Attaching taxonomy to a model. The module's whole value proposition is polymorphic reuse, so keep the relationship defined once and let every categorizable model opt in:

class Post extends Model
{
    public function categories(): MorphToMany
    {
        return $this->morphToMany(Category::class, 'categorizable');
    }
}

Verify the morph name against the migration rather than guessing — a mismatched morph key fails silently as an empty collection, which is a nasty way to lose an afternoon. Always write via sync()/syncWithoutDetaching() inside a transaction when you're replacing a full category set from a form.

Eager-load the tree, always. Trees are the textbook N+1 trap. If the package exposes a children/parent self-relation:

$tree = Category::with('children.children')->whereNull('parent_id')->get();

Cache the fully-hydrated tree (it changes rarely) under a key you bust on any write:

Cache::rememberForever('categories.tree', fn () => Category::tree());
// in an observer on saved/deleted:
Cache::forget('categories.tree');

Register that invalidation in a model observer, not scattered across controllers — you will forget one otherwise.

Filtering entities by category. Prefer whereHas for single-category filters and a join against the pivot for "all posts in this subtree":

Post::whereHas('categories', fn ($q) => $q->where('categories.id', $id))->paginate();

For subtree queries, resolve descendant IDs first (recursive walk or nested-set range, depending on the strategy) and pass them as whereIn — this is far saner than trying to express recursion in the query builder.

Multilingual handling. If names are translatable, write locale-keyed arrays from your admin/import code and read with the app locale set:

app()->setLocale('ar');
$category->name;              // resolves via spatie fallback
$category->getTranslation('name', 'en');

For seeders and importer scripts, wrap each record in an explicit locale or use setTranslations() — mixing $category->name = 'X' (which writes the current locale) with setTranslations() in the same codebase is how you end up with orphaned en rows.

Extending the admin screens. Don't fork the resource to add a field. The Tomato pattern is: publish the view/config, add your column via the resource's form schema if it's config-driven, or subclass the resource and re-register it in the panel provider. Keep customisations in your app namespace so a composer update doesn't overwrite them.

Permissions. Tomato modules typically lean on spatie/laravel-permission. Create the category permissions (categories.view/create/update/delete) in a seeder rather than relying on the module's defaults, so your role matrix is explicit and testable.

Backfill pattern. If you have an existing taxonomy, do it as a one-shot artisan command with dual-read behind a flag: read old enum/table → create categories → attach pivots → verify counts reconcile → flip the flag → leave the old column in place for one release cycle.

Gotchas and Tips

Confirm your admin stack before you commit. The package description says splade/tomato, and this ecosystem is Splade-first — but some Tomato packages are Filament plugins. If your app is Filament v3 and the module is Splade-based, you're paying for screens you can't mount. Check composer.json requires for filament/* vs tomatophp/tomato-admin and decide accordingly. If it doesn't fit, use the migration + model pair standalone and build your own UI — that's a legitimate, low-cost path.

Pin exact versions. The v1.0.1 release notes are an auto-generated changelog link with zero content, so you cannot tell a typo fix from a dependency bump. Use an exact constraint ("tomatophp/tomato-category-module": "1.0.1") and upgrade deliberately after reading the diff:

composer require tomatophp/tomato-category-module:1.0.1 --update-with-dependencies
# later, when reviewing:
git diff v1.0.0 v1.0.1 -- vendor/tomatophp/tomato-category-module/database

Treat any migration change in a patch release as a red flag and dry-run with php artisan migrate --pretend first.

Publishing twice creates duplicates. vendor:publish on the migrations tag will happily copy files that already exist or produce colliding table creates. Publish once, commit the migrations, and never re-publish in a deploy script.

Deletion semantics. Ask — and then check the FK — whether deleting a category cascades the pivot or leaves orphans. If the migration has no onDelete('cascade'), orphaned categorizable rows will accumulate silently and skew your whereHas counts. Enforce it in a migration you control rather than in a model deleting hook.

Concurrent reordering. If you expose drag-to-reorder in the admin, wrap the move in DB::transaction() and consider serialising it. Nested-set subtree moves rewrite many rows; two admins dragging simultaneously produces corrupted tree bounds that are painful to repair and produce weird, intermittent display bugs.

Locale fallback surprises. A category with only an ar name will render blank (or the raw JSON) in an en context if no fallback locale is configured. Set app.fallback_locale and add a test asserting an intentionally incomplete translation resolves.

Slug collisions across locales. Automatically generated slugs from translatable names can collide across languages. Decide up front whether slugs are global-unique or per-locale, and check the unique index in the migration to see which the package assumed.

CI. Include the published migrations in your test database build (RefreshDatabase picks them up if they're in database/migrations) and add one feature test per relation direction — attach, detach, filter-by-category, and tree render. That suite is your insurance against an opaque patch release quietly changing behaviour.

Extension points to prefer. Config file over code edits; observers over controller-side cache busting; the module's own tree methods over hand-rolled recursive queries. Every line you don't write against an under-documented, single-vendor package is a line you don't have to re-verify on the next Filament/Splade bump.

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.
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk