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 Laravel Package

tomatophp/tomato-category

Tomato Category is a category management package for TomatoPHP/Laravel apps, offering nested, sortable categories with an admin interface, API endpoints, and easy integration for organizing content across your projects.

View on GitHub
Deep Wiki
Context7

Getting Started

What you're actually installing. tomatophp/tomato-category is a taxonomy layer — categories, tags, and "types" — attached polymorphically to any Eloquent model, plus admin UI wiring for the TomatoPHP/Splade panel. It is not a framework and not a tree library. Frame it as a migration set + one model + a trait + an optional admin resource. That framing sets your expectations correctly for everything below.

Install:

composer require tomatophp/tomato-category
php artisan vendor:publish --tag="tomato-category-config"
php artisan vendor:publish --tag="tomato-category-migrations"
php artisan migrate

Publish before migrating, always. Running migrate first and publishing afterward is the fastest way to end up with an empty schema and a stack of "table not found" errors that look like a broken install.

Read these five places before writing a line of application code. For a package this small it's a 15-minute job and it removes all guesswork from the rest of this document:

  1. src/Models/ — the Category model and how it handles the type discriminator. This tells you whether categories/tags/types are one table or three.
  2. database/migrations/ — the pivot table name, its columns, and whether foreign keys have onDelete('cascade'). Everything downstream depends on this.
  3. src/Traits/ — the exact relationship and helper methods your models inherit. Confirm the trait name (HasCategories vs HasTags vs a single HasTomatoCategory) rather than assuming.
  4. config/tomato-category.php — model bindings, table names, and the UI registration flag.
  5. The admin surface (src/Filament/, src/Http/Controllers/, or Splade resource classes) — only if you're running the Tomato dashboard.

First use case — tag a model and read it back:

use TomatoPHP\TomatoCategory\Traits\HasCategories; // verify name in src/Traits

class Post extends Model
{
    use HasCategories;
}

$post = Post::create([...]);
$post->categories()->sync([$categoryId, $tagId]);
$post->categories; // collection

If the ergonomic helpers you hoped for aren't there, drop straight to the relationship API. The trait is thin sugar over morphToMany; the pivot is the real contract.

Day-one decision to make explicitly: is this a prototype accelerator or infrastructure? Given 0 stars, 0 dependents, and no release since June 2024, the honest answer is the former. Pin the exact version ("1.x.y", not ^1.0) and name an owner.

Implementation Patterns

Type-scoped relationships on your own model. The core idiom is one categories table with a type column. That's convenient for the package and dangerous for you — an unscoped $post->categories will happily return tags, categories, and types in one collection. Define typed relationships on your model early so the where('type', ...) clutter never reaches a controller or a Blade file:

public function categories()
{
    return $this->morphToMany(Category::class, 'categorizable')
        ->where('type', 'category');
}

public function tags()
{
    return $this->morphToMany(Category::class, 'categorizable')
        ->where('type', 'tag');
}

This is the highest-leverage pattern in the package. Adopt it in your first hour.

Sync-style assignment for forms. Create/edit screens should collect IDs and sync once — it handles attach, detach, and no-op in a single call and composes cleanly with Splade/Filament form submissions:

$post->categories()->sync($request->input('categories', []));

For multi-select UIs where you want a stable selection, use syncWithoutDetaching on incremental saves.

Querying by category — invert the relationship. The polymorphic pivot is where listing-page performance dies. Prefer whereHas for correctness and a subquery for scale:

// Simple, readable
Post::whereHas('categories', fn ($q) => $q->where('slug', 'laravel'))->get();

// Faster on large tables — resolve IDs first
$ids = Category::where('slug', 'laravel')->pluck('id');
Post::whereIn('id', DB::table('categorizables')
    ->where('categorizable_type', 'post')
    ->whereIn('category_id', $ids)
    ->pluck('categorizable_id'))->get();

whereMorphedTo is also available on modern Laravel and reads better than a raw pivot join if your version supports it.

Nested categories: build the tree yourself. If the migration carries parent_id, you have an adjacency list — nothing more. Packages this size rarely ship recursive children(), ancestor flattening, or a tree accessor. Add the pair you need on your own model subclass:

public function parent()   { return $this->belongsTo(Category::class, 'parent_id'); }
public function children() { return $this->hasMany(Category::class, 'parent_id'); }

For dropdowns, cache a flattened tree once per request rather than recursing per row. For deeper needs — move operations, depth queries, ordering — reach for kalnoy/nestedset instead of growing this.

Seeding a stable vocabulary. Slugs are what you'll query in production, so never let them be auto-generated inconsistently across environments. Make the seeder idempotent and explicit:

Category::updateOrCreate(
    ['slug' => 'laravel', 'type' => 'tag'],
    ['name' => 'Laravel', 'parent_id' => null]
);

Model rebinding, not vendor edits. The config's model binding key is the intended customization seam. Subclass the package Category, add scopes and casts there, and point the config at your class. This survives composer update, which matters more than usual given the release cadence.

Wrap it behind your own interface. Write a small repository or facade (Tagger::sync($model, $terms)) and have controllers and views call that instead of the package. Given the maintenance profile, swapping to spatie/laravel-tags or a bespoke model later becomes one contained change rather than a grep-and-replace across the app.

Morph map from day one. See Gotchas — this is a pattern, not a footnote, and it must land in a service provider before you attach the first pivot row.

Gotchas and Tips

Enforce a morph map immediately. The pivot stores the full class name (App\Models\Post) by default. Rename or namespace that class later and every existing pivot row is orphaned with no error at write time and silent empty reads afterward. Fix it in AppServiceProvider::boot() before your first attach:

Relation::enforceMorphMap([
    'post'    => \App\Models\Post::class,
    'product' => \App\Models\Product::class,
]);

This is the single most valuable tip here — cheap now, expensive at any later point.

Type values are strings, not enums. where('type', 'Category') versus 'category' fails silently and returns an empty collection. Grep the migrations and seeders for the exact casing the package uses, then define constants or a PHP enum on your own model so the literal appears once.

Check the cascade before production. Deleting a category frequently leaves orphaned pivot rows unless the migration declares onDelete('cascade'). Read the published migration; if the constraint is missing, add it yourself. Alternative: soft-delete categories and scope live relationships to exclude trashed rows — safer, and it keeps historical attribution intact.

N+1 on every listing page. $post->categories inside a loop is the reflexive mistake. Always Post::with('categories', 'tags') on index screens, and withCount when you only render badges.

Cache invalidation is yours. If you cache a nav tree or a facet list, the package will not bust it. Register an observer on Category (or your subclass) to flush the key on save and delete. This bites hardest in staging, where you seed and then stare at a stale menu.

Don't assume helper methods exist. attachCategory($name), getCategoriesByType(), and similar names vary across the Tomato family. Dump the surface before building on it:

php artisan tinker
>>> get_class_methods(App\Models\Post::class)

Then add the ergonomics you want on your own model.

UI registration, in diagnostic order. If the admin resource doesn't appear: (1) the config registration flag, (2) the panel provider's discoverResources path, (3) whether the service provider is registered in bootstrap/providers.php (Laravel 11+) or config/app.php. Auto-discovery usually handles it, but a published config can override it.

Extension points, ranked. Config model bindings > your own subclass > observer hooks > patching the trait. Don't extend the trait itself — traits compose poorly across forks and you'll re-apply your patch on every version bump, assuming there is one.

Write the pivot canary test first. One feature test that attaches, syncs, detaches, and queries through the pivot with a morph map active is worth more than any amount of documentation here. Given the release history, assume you will fork or upgrade; that test is what tells you whether the upgrade was safe.

Translation is conditional. The package's category model may be translatable, but only if spatie/laravel-translatable is genuinely wired up in your app. Per-locale labels are not a translation workflow — verify before anyone promises multilingual taxonomy.

Scope the blast radius. Keep this on internal admin panels, back-office tooling, and MVPs. Not customer-facing, not revenue-critical, not in a path where an abandoned dependency becomes a migration liability at the worst possible moment. Verify Laravel/PHP version coverage against your target before committing — unproven framework coverage surfaces during upgrades, never during install.

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.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings