NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages
Tags for Laravel

Configuration

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

KeyEnvDefaultPurpose
primary_key_typeTAGS_PRIMARY_KEY_TYPEbigintThe 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_typeTAGS_KEY_TYPEbigintThe 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

KeyEnvDefaultPurpose
tables.tags—tagsThe tag table. Letters, digits and underscores; rename it before migrating when the app already owns a tags table.
tables.taggables—taggablesThe pivot — one row per tag on a model.
tables.locks—tag_locksOne 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\TagThe tag model; a replacement must be a subclass of the packaged one. Every query, relation and the factory resolve the configured class.

Tag types

KeyEnvDefaultPurpose
default_type—tagThe 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

KeyEnvDefaultPurpose
names.max_length—64Characters per locale, 1–255.
descriptions.max_length—500Characters per locale, 1–10000. Descriptions are plain text.
slugs.max_length—80Characters per slug, 1–255.
slugs.separator—-One to three of - . _ ~.
slugs.unicode—falseKeep 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, @ atCharacters spelled out in slugs, so C, C++ and C# stay three tags (c, c-plus-plus, c-sharp).

Hierarchy, suggestions and popularity

KeyEnvDefaultPurpose
hierarchy.max_depth—5The deepest tree, 1–10 levels (a root is level 1). A type asking for more is clamped to it.
suggest.limit—10Suggestions when a call gives no limit, 1 up to suggest.max_limit.
suggest.max_limit—50The most suggestions one call returns, 1–1000.
popular.limit—10Popular tags when a call gives no limit, 1 up to popular.max_limit.
popular.max_limit—100The most popular tags one call returns, 1–1000.

API aliases and resources

KeyEnvDefaultPurpose
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::classThe 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

KeyEnvDefaultPurpose
authorization.abilities.view—nullGate ability asked before every API read, with the type name and the scope. null = no check.
authorization.abilities.manage—tags.manageGate 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.scopeGate 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

KeyEnvDefaultPurpose
routes.enabledTAGS_ROUTESoffServe 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—apiURI 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—120Requests a minute per user (per IP for an anonymous caller) on the tags-api limiter, 1–100000.
routes.suggest_rate_limit—120Requests a minute per user (else per IP) on the suggest endpoint’s own tags-suggest limiter.
routes.max_per_page—100The largest list page, 1–1000; the default page size is 20.
routes.max_tree_nodes—1000The 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

Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

More ways to support, including crypto

By donating, you agree to our donation terms.

Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.