nvl / taxonomy
Hierarchical, attachable and translatable taxonomies for Laravel
Requires
- php: ^8.3
- ext-ctype: *
- laravel/framework: ^13.0
- nvl/core: ^2.0
- nvl/tenancy: ^2.0
- nvl/translatable: ^2.0
- spatie/laravel-data: ^4.23
- spatie/typescript-transformer: ^3.3
Requires (Dev)
- larastan/larastan: ^3.10
- laravel/pint: ^1.27
- orchestra/testbench: ^11.0
- pestphp/pest: ^4.0
- pestphp/pest-plugin-laravel: ^4.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-09-26 07:35:38 UTC
README
For support, open an issue. For vulnerabilities, use private reporting. See Contributing.
See the installation and publishing guide for Composer setup, configuration, migration ownership, and agent skills.
Quick reference
| Item | Value |
|---|---|
| Installed through | composer require nvl/taxonomy:^2.0 |
| Module identifier | nvl/taxonomy |
| PHP namespace | Nvl\Taxonomy |
| Service provider | Nvl\Taxonomy\Providers\TaxonomyServiceProvider |
| Configuration | config/taxonomy.php |
Purpose
nvl/taxonomy provides reusable translated vocabularies and hierarchical terms for Laravel 13 on PHP 8.4+. It supports categories, tags, ordered trees, typed metadata, polymorphic owner attachment, moves, merges, pruning, and deterministic localized display copy. It is not an arbitrary attribute, facets, or search engine.
The package depends on nvl/core, nvl/tenancy, and nvl/translatable inside the NVL family.
Requirements and installation
composer require nvl/taxonomy:^2.0 php artisan migrate
Laravel auto-discovers TaxonomyServiceProvider. Optional publish tags are:
php artisan vendor:publish --tag=taxonomy-config php artisan vendor:publish --tag=taxonomy-migrations php artisan vendor:publish --tag=taxonomy-skills
Clean-install migrations use UUID term identifiers, nullable UUID parent identifiers, dedicated term translations, and string-compatible owner identifiers. Set taxonomy.migrations.enabled=false during controlled adoption of existing tables.
Choose exactly one migration owner. For automatic vendor loading, leave
taxonomy.migrations.enabled=true and do not publish taxonomy-migrations.
For host-owned migrations, publish taxonomy-migrations, set
taxonomy.migrations.enabled=false before the first migration, and maintain
the copied files as application migrations. Never run both sources; Laravel
retimestamps published migrations.
Register vocabularies and owners
Declare stable vocabulary rules in config/taxonomy.php:
'taxonomies' => [ 'topics' => [ 'model' => \Nvl\Taxonomy\Models\Term::class, 'hierarchical' => true, 'exclusive' => false, 'open' => true, 'max_depth' => 5, 'sort' => 'position', 'allowed_owners' => ['articles'], 'metadata_rules' => [ 'color' => ['nullable', 'string', 'max:20'], ], ], ], 'owners' => [ 'articles' => Article::class, ],
TaxonomyRegistry and TaxonomyOwnerRegistry reject invalid and duplicate registrations. Owner aliases are installed in Laravel's morph map and persisted in termable_type, so refactoring a PHP namespace does not corrupt attachment identity. Open vocabularies allow term creation through authorized application flows; closed vocabularies require an existing registered term.
Register every concrete owner class that can receive terms. A base-class alias is not inherited by subclasses because Laravel's morph map identifies concrete classes exactly.
Create a translated term
use Nvl\Taxonomy\Actions\CreateTermAction; use Nvl\Taxonomy\Data\MutateTermPayload; $term = app(CreateTermAction::class)->execute(new MutateTermPayload( taxonomy: 'topics', slug: 'engineering', translations: [ 'en' => ['name' => 'Engineering'], 'bg' => ['name' => 'Инженерство'], ], parentId: null, position: 10, meta: ['color' => 'blue'], ));
Taxonomy, parent, slug, position, metadata, and owner attachments are structural. Name and description exist only in terms_i18n and resolve through nvl/translatable. Slugs are canonical and do not change with locale. UUID-shaped slugs are reserved for unambiguous identifier references.
Updates use UpdateTermAction and require expectedRevision. Stale writes fail instead of silently overwriting newer changes.
Hierarchy
Use MoveTermAction for reparenting. It rejects:
- parents from another vocabulary;
- a term as its own parent;
- ancestor/descendant cycles;
- hierarchy on a flat vocabulary;
- moves beyond the configured maximum depth;
- duplicate sibling slugs.
Ordered tree and subtree reads must eager-load translations and use the package's deterministic ordering. Do not recurse through lazily loaded child relationships in an API transformer.
Use TaxonomyTree::for($taxonomy, $locale) for any registered vocabulary. Category::tree() is a convenience wrapper around the same generic service.
Attach terms
Use AttachTermsAction, DetachTermsAction, or SyncTermAttachmentsAction with a registered owner. Do not write the polymorphic attachment table directly. Vocabulary rules enforce allowed owner aliases and exclusive membership where configured.
Attachment actions serialize each owner/vocabulary set with Laravel atomic locks. Production nodes must use a shared lock-capable cache store.
MergeTermsAction moves attachments and eligible children under a connection-correct transaction and requires the expected revisions of both terms. DeleteTermAction requires an expected revision and a DeleteTermStrategy; it rejects unsafe deletion when attachments or children cannot be handled by that strategy.
Central translation management
Taxonomy registers its term resource with TranslationResourceRegistry for
central gathering, coverage, and reads using the package field whitelist, query
scope, authorization, and version hash. Terms declare
TranslationMutationPolicy::DomainActionOnly: generic central sync and locale
deletion actions reject term writes.
Write localized names and descriptions through CreateTermAction or
UpdateTermAction using the translations field of MutateTermPayload.
For updates, provide the expected term revision. Pass mode: TranslationSyncMode::Replace to
UpdateTermAction::execute() with the retained locale map to remove omitted
locales. This keeps localized changes inside Taxonomy's validation, revision,
and event workflow. Consumer applications must authorize these domain actions
before accepting user input.
Generate TypeScript declarations under Nvl.Taxonomy.*:
php artisan nvl:data:types:generate php artisan nvl:data:types:check
Configuration
Important groups are:
owners: stable aliases to Eloquent model classes;taxonomies: hierarchy, openness, exclusivity, depth, sorting, owner allowlist, and metadata validation;table_namesandstorage.connection;migrations.enabled;limits.metadata_bytes,metadata_depth,description_chars, andbulk_terms;transactions.attemptsfor deadlock retries;locks.secondsandlocks.wait_secondsfor attachment-set serialization;slugs.generatorandslugs.locale.
Metadata is bounded and validated; it is not an arbitrary query language.
Commands
php artisan nvl:taxonomy:doctor --strict --format=json php artisan nvl:taxonomy:rebuild --dry-run php artisan nvl:taxonomy:merge --help php artisan nvl:taxonomy:prune --dry-run php artisan nvl:taxonomy:prune category --include-closed --force
Doctor verifies required columns and unique indexes plus registry, connection, parent, attachment, translation, cycle, and depth invariants. Dry-run modes do not mutate state. Pruning protects closed vocabularies unless --include-closed is supplied explicitly.
Database and adoption
The schema indexes vocabulary/parent/slug, tree order, owner/type, term attachment, and locale lookups. Package migrations honor configured table names and connection where supported.
Tenant-local trees
Installing Taxonomy also installs the inert nvl/tenancy library. With tenancy
enabled, register every taxonomy owner as a canonical tenant resource and use
Nvl\Taxonomy\Models\Term as each configured vocabulary model. Run reviewed
adoption for the taxonomy package before tenant traffic; it copies split trees
and translations, rewrites owner attachments, and activates tenant-leading
parent and attachment constraints. Vocabulary aliases remain global immutable
configuration while every term, translation, and attachment is tenant-local.
Take a pre-cutover backup and treat the reviewed split/destination mapping as immutable. An interrupted run may resume after source or schema repair that is consistent with that mapping. Changed tenant or destination assignments require restore and a new reviewed prepare; dropping tenant columns is not rollback once duplicate slugs and copied trees exist. Cleanup traverses one tenant-owned graph at a time and must not prune another tenant's attachments.
Maintenance is explicit per tenant, for example:
php artisan nvl:taxonomy:rebuild category --tenant=<tenant-uuid> --dry-run php artisan nvl:taxonomy:prune tag --tenant=<tenant-uuid> --dry-run
Attachment Actions and owner-to-term lazy/eager relations support a dedicated taxonomy connection. Inverse Term::entries() joins and owner with*Terms / inCategory scopes require the owner and taxonomy connections to address the same physical database because Eloquent cannot execute a cross-database relationship subquery portably.
For an existing schema, disable automatic migrations and run the doctor. Convert root sentinels to null, backfill dedicated translation rows, and resolve identifier differences in an application-owned reversible bridge. A table-name match is not schema compatibility.
Authorization, caching, and failures
This package ships no management routes. Applications authorize Action calls and any API built over them. Registry aliases and vocabulary rules are allowlists, not authorization by themselves.
TermChanged mutation events implement ShouldDispatchAfterCommit. Unknown vocabularies, invalid metadata, stale revisions, ambiguous slugs, hierarchy violations, and unsafe deletes are distinct failures.
Verification
The package tests cover UUID identifiers, stable morph aliases, translation fallback, slug stability, tree order, cycles, subtree depth, moves, merges, attachments, exclusivity, deletion policies, maintenance safety, and configured-connection behavior. CI runs the package on its supported PHP/Laravel and database matrix.
See UPGRADING.md, SECURITY.md, CONTRIBUTING.md, and CHANGELOG.md.
License
Released under the MIT License.