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

The facade is the recommended default, not a requirement. There are three equivalent entry points, and all three run the same code:

  • The Attributes facade — shortest; recommended for most code.
  • The manager, RoundlyConsulting\Attributes\AttributesManager, injected through the constructor — it’s the facade root, a container singleton with exactly the same API, as an explicit dependency with no static calls.
  • Actions — single-purpose final readonly classes with one execute() method, for composing into your own actions, jobs and commands.

Inject the manager

use RoundlyConsulting\Attributes\AttributesManager;

final class ImportColor
{
    public function __construct(private AttributesManager $attributes) {}

    public function __invoke(Product $product, string $color): void
    {
        $this->attributes->for($product)->set('color', $color);
    }
}

Attributes::fake() swaps the container binding as well as the facade, so an injected AttributesManager records too.

Call an action

use RoundlyConsulting\Attributes\Actions\AttachAttributeAction;
use RoundlyConsulting\Attributes\Actions\SyncAttributesAction;
use RoundlyConsulting\Attributes\DataTransferObjects\AttributeData;

// The raw actions — one execute() each
app(AttachAttributeAction::class)->execute($product, new AttributeData('color', 'red'));

$attributes = app(SyncAttributesAction::class)->execute($product, [
    new AttributeData('color', 'red', collect(['hex' => '#f00'])),
    new AttributeData('size', 'L'),
]);                                                     // Collection<int, Attribute>

Actions take DTOs from RoundlyConsulting\Attributes\DataTransferObjects: AttributeData(name, value = null, ?Collection meta = null) for a value, AttributeDefinitionData for a definition. An action called directly bypasses the manager, so the fake doesn’t record it.

Facade method → action

Facade pathActionexecute()
for($o)->set()AttachAttributeAction(Model $owner, AttributeData $data): Attribute
for($o)->setMany(), for($o)->stage()->save()AttachAttributesAction(Model $owner, AttributeData ...$data): Collection<int, Attribute>
for($o)->sync(), for($o)->stage()->sync()SyncAttributesAction(Model $owner, list<AttributeData> $data, bool $forceDelete = false): Collection<int, Attribute>
for($o)->forget()DetachAttributesAction(Model $owner, list<string> $names, bool $forceDelete = false): int
for($o)->forgetExcept()DetachAttributesExceptAction(Model $owner, list<string> $keep, bool $forceDelete = false): list<string>
for($o)->meta()SyncAttributeMetaAction(Model $owner, string $name, ?Collection $meta): Attribute
prune()PruneAttributesAction(?int $days = null): int

WriteAttributeAction (the raw upsert behind every attach, sync and meta write) and RecordAttributeRevisionAction are @internal — the actions above call them, so don’t call them yourself. The attach, sync and meta actions validate before they write, and every write action runs in one transaction — a savepoint when one is already open. The definition and validation methods have no action; they live on the AttributeRegistry.

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.