DI and actions
The facade is the recommended default, not the only way in. Three entry points run the same code: the Tags facade (shortest), the manager RoundlyConsulting\Tags\TagsManager — the facade’s root, a container singleton — injected through the constructor (same API, explicit dependency, no static calls), and actions (final readonly classes with one execute(), for composing into your own actions, jobs and commands):
use RoundlyConsulting\Tags\Actions\Tagging\AttachTagsAction;
use RoundlyConsulting\Tags\DataTransferObjects\TagSelection;
use RoundlyConsulting\Tags\Facades\Tags;
use RoundlyConsulting\Tags\TagsManager;
Tags::for($concert)->attach(['live'], 'label'); // facade
app(TagsManager::class)->for($concert)->attach(['outdoor'], 'label'); // dependency injection
app(AttachTagsAction::class)->execute($concert, TagSelection::of(['free'], 'label')); // actionInject the manager
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use RoundlyConsulting\Tags\TagsManager;
final class ConcertLabelsController
{
public function __construct(private TagsManager $tags) {}
public function update(Request $request, Concert $concert): Response
{
$this->tags->for($concert)->sync($request->input('labels', []), 'label');
return response()->noContent();
}
}The manager holds no container of its own: it resolves actions and read services from the current one on every call, so it is Octane-safe and a binding made inside a request applies.
Call an action
use RoundlyConsulting\Tags\Actions\Catalog\DeleteTagAction;
use RoundlyConsulting\Tags\Actions\Maintenance\RepairTagsAction;
use RoundlyConsulting\Tags\Actions\Tagging\SyncTagsAction;
use RoundlyConsulting\Tags\DataTransferObjects\RepairOptions;
use RoundlyConsulting\Tags\DataTransferObjects\TagSelection;
use RoundlyConsulting\Tags\Enums\TagChildren;
app(SyncTagsAction::class)->execute($concert, TagSelection::of($jazz, 'genre'));
app(DeleteTagAction::class)->execute($jazz, TagChildren::Reparent);
app(RepairTagsAction::class)->execute(new RepairOptions(counts: true, paths: false, scopeKeys: false, orphans: false));Facade method → action
Namespaces are relative to RoundlyConsulting\Tags\Actions:
| Action | execute(…) | Returns | Facade path |
|---|---|---|---|
Tagging\AttachTagsAction | execute($taggable, TagSelection $selection) | TagChanges | Tags::attach(), for()->attach(), attachTags() |
Tagging\DetachTagsAction | execute($taggable, TagSelection $selection) | TagChanges | Tags::detach(), for()->detach(), detachTags() |
Tagging\SyncTagsAction | execute($taggable, TagSelection $selection) | TagChanges | Tags::sync(), for()->sync(), syncTags() |
Tagging\ClearTagsAction | execute($taggable, $type = null) | TagChanges | Tags::clear(), for()->clear(), clearTags() |
Catalog\CreateTagAction | execute($type, TagData $data, ?Model $scope = null) | Tag | type()->create(), make()->create() |
Catalog\FindOrCreateTagAction | execute($type, string|array|TagData $name, ?Model $scope = null) | Tag | type()->findOrCreate(), make()->findOrCreate() |
Catalog\UpdateTagAction | execute(Tag $tag, UpdateTagData $data) | Tag | type()->update(), rename(), edit()->save() |
Catalog\MoveTagAction | execute(Tag $tag, ?Tag $under = null, ?int $position = null) | Tag | type()->move() |
Catalog\ReorderTagsAction | execute($type, ?Model $scope, ?Tag $parent, array $order) | Collection<int, Tag> | type()->reorder() |
Catalog\DeleteTagAction | execute(Tag $tag, TagChildren $children = Refuse) | Tag | type()->delete() |
Catalog\RestoreTagAction | execute(Tag $tag) | Tag | type()->restore() |
Maintenance\RepairTagsAction | execute(RepairOptions $options) | RepairReport | Tags::repair(), tags:repair |
- Every write — from the facade, an injected manager, a handle, a builder or a model method — funnels through the manager, which resolves the action from the container, so a container binding of an action applies everywhere.
- Tags::fake() swaps the manager in the container too, so an injected TagsManager is faked as well. Calling an action directly is not recorded — only manager calls are.
- The scope checks of a type() handle (a write matches its scope exactly) live in the handle: an action called directly writes whatever tag you hand it.
- The read services BuildTagTree, LoadAncestors, PopularTags and SuggestTags (RoundlyConsulting\Tags\Queries) are resolved from the container too, so you may bind your own.
- Actions\Internal\* (AdjustUsageCountsAction, ApplyTagChangesAction, ClearDeletedTaggableAction, ResolveTagsAction, RewriteSubtreePathsAction) are building blocks — don’t call them.
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 cryptoBy 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.