The package works with zero configuration. Publish config/tags.php only to override:
php artisan vendor:publish --tag="tags-config"
Every key is read strictly: a value that is not set — absent, null or blank — takes its default, and a bad one throws an InvalidTagsConfigurationException (or the package toolkit’s InvalidConfigurationException) naming the key, never a silent fallback.
- Booleans read true/false, 1/0, on/off or yes/no; anything else throws.
- Integers accept an int or a plain integer string and must sit in the range each row gives — 5.5 or five throws rather than becoming 0.
- Table names are letters, digits and underscores; type names are lowercase with an optional package prefix; aliases are snake_case.
The config file
use RoundlyConsulting\Tags\Http\Resources\TagScopeResource;
use RoundlyConsulting\Tags\Models\Tag;
return [
'primary_key_type' => env('TAGS_PRIMARY_KEY_TYPE', 'bigint'),
'key_type' => env('TAGS_KEY_TYPE', 'bigint'),
'tables' => [
'tags' => 'tags',
'taggables' => 'taggables',
'locks' => 'tag_locks',
],
'models' => [
'tag' => Tag::class,
],
'default_type' => 'tag',
'types' => [],
'overrides' => [],
'names' => [
'max_length' => 64,
],
'descriptions' => [
'max_length' => 500,
],
'slugs' => [
'max_length' => 80,
'separator' => '-',
'unicode' => false,
'dictionary' => ['+' => 'plus', '#' => 'sharp', '&' => 'and', '@' => 'at'],
],
'hierarchy' => [
'max_depth' => 5,
],
'suggest' => [
'limit' => 10,
'max_limit' => 50,
],
'popular' => [
'limit' => 10,
'max_limit' => 100,
],
'scopes' => [],
'taggables' => [],
'resources' => [
'scope' => TagScopeResource::class,
],
'authorization' => [
'abilities' => [
'view' => null,
'manage' => 'tags.manage',
'scope' => 'tags.scope',
],
],
'routes' => [
'enabled' => env('TAGS_ROUTES') ?: 'off',
'prefix' => 'api',
'middleware' => ['api', 'auth', 'throttle:tags-api'],
'rate_limit' => 120,
'suggest_rate_limit' => 120,
'max_per_page' => 100,
'max_tree_nodes' => 1000,
],
];
Key types
| Key | Env | Default | Purpose |
|---|
primary_key_type | TAGS_PRIMARY_KEY_TYPE | bigint | The package’s own keys — tags.id, tags.parent_id and taggables.tag_id: bigint, uuid or ulid. Read by the migrations, so set it before migrating; frozen afterwards. |
key_type | TAGS_KEY_TYPE | bigint | The keys the package points at: every taggable model (taggable_id) and every scope model (scope_id) must use it. A model keyed otherwise throws InvalidTagsConfigurationException when it is tagged or used as a scope; tags:doctor reports it. |
Tables and the tag model
| Key | Env | Default | Purpose |
|---|
tables.tags | — | tags | The tag table. Letters, digits and underscores; rename it before migrating when the app already owns a tags table. |
tables.taggables | — | taggables | The pivot — one row per tag on a model. |
tables.locks | — | tag_locks | One lock row per tree (type and scope). Keep every table name at most 30 characters: MySQL index names are built from it. |
models.tag | — | Models\Tag | The tag model; a replacement must be a subclass of the packaged one. Every query, relation and the factory resolve the configured class. |
Tag types
| Key | Env | Default | Purpose |
|---|
default_type | — | tag | The type a plain string names when a call gives no type. Unless something defines it, it is flat, free-form and unlimited. Blank or null = no default: every string must then name its type. |
types | — | [] | The app’s own types: name => rules (see Tag types). |
overrides | — | [] | Rules applied over any type — a package’s included — after its definition and every Tags::extend() callback. |
Names, descriptions and slugs
| Key | Env | Default | Purpose |
|---|
names.max_length | — | 64 | Characters per locale, 1–255. |
descriptions.max_length | — | 500 | Characters per locale, 1–10000. Descriptions are plain text. |
slugs.max_length | — | 80 | Characters per slug, 1–255. |
slugs.separator | — | - | One to three of - . _ ~. |
slugs.unicode | — | false | Keep non-ASCII letters (CJK, Cyrillic, …) in slugs. Off, a name that leaves no ASCII letter or digit is refused as unsluggable. |
slugs.dictionary | — | + plus, # sharp, & and, @ at | Characters spelled out in slugs, so C, C++ and C# stay three tags (c, c-plus-plus, c-sharp). |
Hierarchy, suggestions and popularity
| Key | Env | Default | Purpose |
|---|
hierarchy.max_depth | — | 5 | The deepest tree, 1–10 levels (a root is level 1). A type asking for more is clamped to it. |
suggest.limit | — | 10 | Suggestions when a call gives no limit, 1 up to suggest.max_limit. |
suggest.max_limit | — | 50 | The most suggestions one call returns, 1–1000. |
popular.limit | — | 10 | Popular tags when a call gives no limit, 1 up to popular.max_limit. |
popular.max_limit | — | 100 | The most popular tags one call returns, 1–1000. |
API aliases and resources
| Key | Env | Default | Purpose |
|---|
scopes | — | [] | API alias => scope model class, for ?scope=alias:key. Aliases are snake_case (^[a-z][a-z0-9_]{0,63}$); a class name is never accepted. |
taggables | — | [] | API alias => taggable model class, for popular?on=alias. |
resources.scope | — | TagScopeResource::class | The JsonResource a scoped tag’s owner renders through; it receives the scope model. Blank or null = the packaged {id, type}; anything that is not an instantiable JsonResource subclass throws, naming the key. |
Authorization
| Key | Env | Default | Purpose |
|---|
authorization.abilities.view | — | null | Gate ability asked before every API read, with the type name and the scope. null = no check. |
authorization.abilities.manage | — | tags.manage | Gate ability asked before every API write, with the type name and the target scope (null = a global write). It also decides who sees private types and — in each tag’s own scope, so a global tag asks for global management — usage counts, translations and deleted tags. Blank = tags.manage. |
authorization.abilities.scope | — | tags.scope | Gate ability asked with the scope model whenever a request passes ?scope=. A denied scope answers 422 unknown_alias, exactly like a scope that does not exist, so a reference reveals no tenant ids. Blank = tags.scope. |
JSON API
| Key | Env | Default | Purpose |
|---|
routes.enabled | TAGS_ROUTES | off | Serve the JSON API — off unless you switch it on. Written env('TAGS_ROUTES') ?: 'off', and a switch that is not set (missing from a published config whose routes array overrides only some keys, null or blank) is off too. |
routes.prefix | — | api | URI prefix; surrounding slashes are trimmed, null = none. |
routes.middleware | — | ['api', 'auth', 'throttle:tags-api'] | Middleware of every endpoint; with Sanctum use auth:sanctum. tags:doctor warns when no auth middleware is listed. |
routes.rate_limit | — | 120 | Requests a minute per user (per IP for an anonymous caller) on the tags-api limiter, 1–100000. |
routes.suggest_rate_limit | — | 120 | Requests a minute per user (else per IP) on the suggest endpoint’s own tags-suggest limiter. |
routes.max_per_page | — | 100 | The largest list page, 1–1000; the default page size is 20. |
routes.max_tree_nodes | — | 1000 | The largest ?tree=1 answer, 1–100000; a bigger tree answers 422 tree_too_large. |
Changing a slug setting later changes how new strings match, not the slugs already stored — run sluggable:regenerate on the tag model afterwards (see Installation).
Environment
Only the key types and the API switch are env-driven:
TAGS_PRIMARY_KEY_TYPE=bigint
TAGS_KEY_TYPE=bigint
TAGS_ROUTES=on