JSON API
Routy, form requesty, controllery, resources, filtre zoznamu cez query-builder a limity požiadaviek — vypnuté, kým ich nezapnete:
TAGS_ROUTES=on- Vypnuté, kým ho nezapnete: nenastavený prepínač — chýbajúci v publikovanej konfigurácii, ktorej pole routes prepisuje len niektoré kľúče, null alebo prázdny — ho nechá vypnuté.
- Prefix tags.routes.prefix (api), názvy rout tags.*, middleware tags.routes.middleware (['api', 'auth', 'throttle:tags-api']).
- {type} je názov typu; {tag} je kľúč vždy, keď zodpovedá typu kľúča, inak slug alebo názov. suggest, popular a reorder sa registrujú pred {tag}, takže štítok s jedným z týchto slugov sa adresuje kľúčom.
- Ak chcete routu zmeniť, publikujte routes/tags.php (--tag=tags-routes); publikovaná kópia nahradí súbor balíka a stále ju riadi tags.routes.enabled.
- Každý zápis ide cez manažéra, takže Tags::fake() zaznamená aj volania API.
Aliasy, Sanctum a oprávnenia
API nikdy neprijme názov triedy: rozsahy a modely so štítkami sa pomenúvajú aliasmi, ktoré povolíte. Balík nedefinuje žiadny Gate a nedefinované oprávnenie zamieta — definujte tie, ktoré používate:
// config/tags.php
'scopes' => ['team' => App\Models\Team::class], // ?scope=team:12
'taggables' => ['concert' => App\Models\Concert::class], // popular?on=concert
'routes' => [
'middleware' => ['api', 'auth:sanctum', 'throttle:tags-api'],
// …
],
// AppServiceProvider::boot() — the package defines no gate; an undefined ability denies
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Gate;
Gate::define('tags.manage', fn (User $user, string $type, ?Model $scope = null) => $scope === null
? $user->is_admin // global tags: admins only
: $user->team_id === $scope->getKey() && $user->is_team_admin);
Gate::define('tags.scope', fn (User $user, Model $scope) => $user->team_id === $scope->getKey());- manage (tags.manage) sa pýta pred každým zápisom s názvom typu a cieľovým rozsahom — null pri globálnom zápise — takže správcovia tenanta môžu spravovať vlastné štítky a nikdy nie globálne. Rozhoduje aj o tom, kto vidí súkromné typy a — vždy v rozsahu daného štítku, takže globálny štítok si pýta globálnu správu bez ohľadu na rozsah, ktorý požiadavka číta — usage_count, preklady a filter[trashed]; kontrola beží raz na typ a rozsah v rámci požiadavky.
- scope (tags.scope) sa pýta s modelom rozsahu vždy, keď požiadavka pošle ?scope=. Zamietnutý rozsah odpovie 422 unknown_alias presne ako rozsah, ktorý neexistuje, takže odkaz neprezradí id žiadneho tenanta.
- view (predvolene null) sa pýta pred čítaním typu a jeho štítkov s názvom typu a rozsahom; bez neho stačí na čítanie middleware routy.
- is_admin, is_team_admin a team_id vyššie zastupujú vaše vlastné stĺpce či kontroly rolí; funguje aj balík oprávnení, ktorý ich udeľuje cez Gate::before.
Routy
| Metóda a cesta | Názov | Oprávnenie | Poznámky |
|---|---|---|---|
GET tag-types | tags.types.index | — | Typy, ktoré volajúci smie vidieť — súkromné len pre ich správcov. Pri zozname typov sa view nepýta. |
GET tag-types/{type} | tags.types.show | view | Jeden typ. |
GET tags/{type} | tags.index | view | Zoznam — filtre, radenie, stránkovanie — alebo ?tree=1 pre vnorené uzly. |
POST tags/{type} | tags.store | manage | Vytvorí v rozsahu požiadavky; 201. |
GET tags/{type}/suggest | tags.suggest | view | ?q= (1–64 znakov) a limit; navyše throttle:tags-suggest. |
GET tags/{type}/popular | tags.popular | view | ?limit a on=alias z tags.taggables. |
POST tags/{type}/reorder | tags.reorder | manage | {parent, order} |
GET tags/{type}/{tag} | tags.show | view | Jeden štítok s predkami. |
PATCH tags/{type}/{tag} | tags.update | manage | Zmenia sa len poslané polia. |
DELETE tags/{type}/{tag} | tags.destroy | manage | ?children=refuse (predvolené), reparent alebo delete; vráti zmazaný štítok. |
POST tags/{type}/{tag}/move | tags.move | manage | {parent: key|null, position} |
POST tags/{type}/{tag}/restore | tags.restore | manage | Podľa kľúča alebo podľa slugu či názvu zmazaného štítku presne z rozsahu požiadavky. |
GET /api/tag-types
GET /api/tags/genre?tree=1
GET /api/tags/label?scope=team:12&filter[search]=pri&sort=-usage_count&per_page=50
GET /api/tags/label/suggest?q=liv&limit=5
GET /api/tags/label/popular?on=concert&limit=10
GET /api/tags/genre/jazz
POST /api/tags/genre {"name": {"en": "Bebop"}, "parent": "2", "color": "#f24e29"}
PATCH /api/tags/genre/bebop {"name": {"en": "Bebop jazz"}, "description": {"sk": null}, "color": null}
POST /api/tags/genre/bebop/move {"parent": null, "position": 0}
POST /api/tags/genre/reorder {"parent": "1", "order": ["4", "2", "3"]}
DELETE /api/tags/genre/music?children=reparent
POST /api/tags/genre/rock/restoreTelá požiadaviek
| Endpoint | Telo / query |
|---|---|
POST tags/{type} | name (mapa jazykov, aspoň jeden jazyk, každý ≤ names.max_length), description (mapa jazykov), slug (mapa jazykov — len kurátorované typy), color, icon, parent (kľúč toho istého typu presne v rozsahu požiadavky; null žiada koreň), position (0–2147483647, číslo alebo reťazec číslic). Názov, ktorý drží zmazaný štítok, tento štítok vráti s atribútmi z tela — tam, kde bol, pokiaľ telo nemá kľúč parent. |
PATCH tags/{type}/{tag} | name, description (null vymaže všetky jazyky; null alebo prázdna položka vymaže daný jazyk), slug, color (null vymaže), icon (null vymaže) — zmení sa len poslané; mapy sa zlúčia po jazykoch. |
POST …/{tag}/move | parent — povinný, kľúč alebo null pre koreň; position ako pri vytvorení. |
POST tags/{type}/reorder | parent (kľúč alebo null pre korene), order — povinné, pole s najviac 1000 kľúčmi. |
DELETE tags/{type}/{tag} | ?children=refuse|reparent|delete |
| každý endpoint | ?scope=alias:kľúč (alias z tags.scopes — čítanie vidí rozsah a globálne štítky, zápisy idú do rozsahu) a ?scope_only=1 (čítanie vidí len štítky rozsahu). |
Zoznam štítkov
| Parameter | Význam |
|---|---|
filter[search] | Názvy, ktoré obsahujú výraz (escapovaný), v ktoromkoľvek podporovanom jazyku. Vždy jeden výraz — čiarka zostáva jeho súčasťou; prázdny výraz nefiltruje nič. |
filter[parent] | Kľúč rodiča alebo root. |
filter[depth] | Číslo úrovne. |
filter[ids] | Kľúče oddelené čiarkou; chybné sa preskočia. |
filter[slug] | Slugy oddelené čiarkou, v ktoromkoľvek jazyku reťaze. |
filter[trashed] | Len pre správcov (pre ostatných neznámy filter, 400): with alebo only — a len zmazané štítky rozsahov, ktoré volajúci spravuje, takže správca tímu, ktorý nesmie spravovať globálne štítky, zmazané globálne štítky nikdy neuvidí. |
sort | position (predvolené), name (v aktuálnom jazyku), usage_count, created_at; - pre zostupne, oddelené čiarkou. |
per_page, page | Najviac tags.routes.max_per_page; predvolene 20. |
tree=1 | Vnorené uzly hierarchického typu (pri plochom type prázdny zoznam); viac než max_tree_nodes odpovie 422 tree_too_large. |
include=translations | Všetky jazyky name, description a slug — ak volajúci spravuje typ v rozsahu požiadavky, a len pri štítkoch, ktorých rozsah spravuje tiež. |
Každý zoznam — index, suggest, popular aj reorder — načíta vlastníkov rozsahov svojich štítkov jedným dotazom, takže stránka stojí rovnaký počet dotazov bez ohľadu na jej veľkosť.
Resources
| Resource | Každý, kto smie čítať | Správcovia navyše |
|---|---|---|
TagResource | id, type (názov typu), name, slug a description v aktuálnom jazyku, color, icon, parent_id, depth, position, scope (cez tags.resources.scope; pri globálnom štítku null), ancestors (ak sú načítané — endpoint show ich načíta), deleted_at (ak je zmazaný), created_at, updated_at. | usage_count — ktorý pri globálnom štítku sčíta priradenia všetkých tenantov — a translations s ?include=translations, pre správcov rozsahu daného štítku: globálny štítok si pýta globálnu správu bez ohľadu na rozsah, ktorý požiadavka číta. |
TagSummaryResource | id, type, name, slug, color, icon, depth — plus ancestors, ak sú načítané. Bez počtu použití, rozsahu, rodiča a časových pečiatok, takže bez Gate kontroly, vzťahu aj dotazu. | — |
TagNodeResource | Súhrn plus parent_id, position a children. | — |
TagTypeResource | name, label (preložený pri vykreslení), hierarchical, max_depth, max, curated. | — |
TagScopeResource | {id, type} — kľúč a krátky názov triedy vlastníka (Team), nikdy celý názov triedy. | — |
GET /api/tags/genre/jazz pre čitateľa:
{
"data": {
"id": 2,
"type": "genre",
"name": "Jazz",
"slug": "jazz",
"description": null,
"color": null,
"icon": "sax",
"parent_id": 1,
"depth": 2,
"position": 0,
"scope": null,
"ancestors": [
{ "id": 1, "type": "genre", "name": "Music", "slug": "music", "color": "#f24e29", "icon": null, "depth": 1 }
],
"created_at": "2026-10-19T09:30:00+00:00",
"updated_at": "2026-10-19T09:30:00+00:00"
}
}Označenie tímu v zozname pre správcu tohto tímu s ?include=translations — vlastník sa vykreslí cez TagScopeResource ako kľúč a krátky názov triedy, nikdy nie celý názov triedy, a scope_type ani scope_key server nikdy neopustia:
{
"id": 31,
"type": "label",
"name": "Printer",
"slug": "printer",
"description": "Office printers.",
"color": null,
"icon": null,
"parent_id": null,
"depth": 1,
"position": 0,
"usage_count": 14,
"scope": { "id": 12, "type": "Team" },
"translations": {
"name": { "en": "Printer", "sk": "Tlačiareň" },
"description": { "en": "Office printers.", "sk": "Kancelárske tlačiarne." },
"slug": { "en": "printer", "sk": "tlaciaren" }
},
"created_at": "2026-10-19T09:30:00+00:00",
"updated_at": "2026-10-19T09:30:00+00:00"
}GET /api/tag-types:
{
"data": [
{ "name": "genre", "label": "Genre", "hierarchical": true, "max_depth": 5, "max": 1, "curated": true },
{ "name": "label", "label": "Label", "hierarchical": false, "max_depth": null, "max": 5, "curated": false }
]
}GET /api/tags/genre?tree=1:
{
"data": [
{
"id": 1, "type": "genre", "name": "Music", "slug": "music", "color": "#f24e29", "icon": null, "depth": 1,
"parent_id": null, "position": 0,
"children": [
{
"id": 2, "type": "genre", "name": "Jazz", "slug": "jazz", "color": null, "icon": "sax", "depth": 2,
"parent_id": 1, "position": 0,
"children": []
}
]
}
]
}Vlastný tvar vlastníka
Nastavte tags.resources.scope na vlastný JsonResource, ak chcete vykresliť meno alebo čokoľvek iné, čo vaše API potrebuje; dostane model rozsahu:
// config/tags.php
'resources' => [
'scope' => App\Http\Resources\TagOwnerResource::class,
],
// app/Http/Resources/TagOwnerResource.php — receives the scope model
final class TagOwnerResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->resource->getKey(),
'name' => $this->resource->name,
];
}
}Súkromné typy
Typ deklarovaný cez private() je neviditeľný pre každého, kto ho nesmie spravovať: chýba v /tag-types a každý endpoint pod ním odpovie 404 unknown_tag_type, presne ako typ, ktorý neexistuje.
Limity požiadaviek
Definujú sa len pri zapnutých routách: tags-api povolí tags.routes.rate_limit (120) požiadaviek za minútu na prihláseného používateľa, inak na IP; endpoint na našepkávanie pridáva tags-suggest s tags.routes.suggest_rate_limit (120).
Chyby
Chyby balíka sa mapujú v jeho vlastných controlleroch — nikdy vo vašom exception handleri — na {message, code} v jazyku požiadavky. Segment cesty, ktorý neoznačuje nič, čo volajúci smie vidieť, je 404, odkaz v tele 422 a štítok iného rozsahu odpovie presne ako chýbajúci:
{ "message": "Unknown tag type [nope].", "code": "unknown_tag_type" }
{ "message": "A tag with the slug [jazz] in [en] already exists.", "code": "duplicate_tag" }| Stav | code | Kedy |
|---|---|---|
| 404 | unknown_tag_type | Segment {type} neoznačuje žiadny typ, ktorý volajúci smie vidieť — súkromný typ je pre nesprávcov neznámy. |
| 404 | tag_not_found | Segment {tag} neoznačuje žiadny štítok v čitateľných rozsahoch (pri zápisoch presne v rozsahu požiadavky). Štítok iného rozsahu odpovie úplne rovnako. |
| 422 | tag_not_found | Kľúč parent alebo order v tele, ktorý chýba alebo je iného typu či rozsahu. |
| 422 | unknown_alias | Alias ?scope= či on=, ktorý povolené zoznamy nepoznajú, názov triedy, chybný kľúč, kľúč bez záznamu — alebo rozsah, ktorý volajúci nesmie použiť: zamietnuté oprávnenie scope odpovie úplne rovnako. |
| 422 | invalid_tag, invalid_hierarchy, tag_type_mismatch | Odmietnutý atribút (farba, slug, jazyk…); cyklus, príliš hlboký strom či zmazaný rodič; štítok iného typu. |
| 422 | tree_too_large | ?tree=1 nad tags.routes.max_tree_nodes. |
| 409 | duplicate_tag, tag_has_children | Slug, ktorý drží iný štítok typu a rozsahu (aj zmazaný); zmazanie, ktoré by osirelo živé deti. |
| 403 | — | Gate manage alebo view zamietol — nedefinované oprávnenie zamieta. |
| 400 | — | Neznámy filter či radenie pri predvolenom mode.unknown_filter / unknown_sort = reject v query-builder; ignore ho namiesto toho zahodí. |
| 422 / 429 | — | Validačné chyby Laravelu (názvy polí z tags::validation.attributes); limitery požiadaviek. |
Prejavte lásku k open source
Tento balík je zadarmo pod licenciou MIT. Ak vám šetrí čas, jednorazový príspevok alebo členstvo na Patreone nám pomôže ho ďalej udržiavať, testovať a dokumentovať.
Ďalšie spôsoby podpory vrátane kryptomienOdoslaním daru súhlasíte s našimi podmienkami prijímania darov.
Chcete to zabudovať do svojho produktu?
Naše balíky integrujeme do zákazkových Laravel a AI riešení. Napíšte nám, na čom pracujete, a ozveme sa do 48 hodín.