The Attributes facade
RoundlyConsulting\Attributes\Facades\Attributes is the recommended entry point. Attributes::for($owner) returns the owner’s attribute handle — every read and write in one place:
use RoundlyConsulting\Attributes\Facades\Attributes;
// Writes (validated against definitions, recorded in history, events fired)
Attributes::for($product)->set('color', 'red', meta: ['hex' => '#f00']); // Attribute
Attributes::for($product)->set('color', 'blue'); // new value, meta kept
Attributes::for($product)->setMany(['color' => 'red', 'size' => 'L']); // Collection<Attribute>, keeps others
Attributes::for($product)->setValidated(['min_price' => 5, 'max_price' => 9]); // as setMany(), validated as one set
Attributes::for($product)->sync(['color' => 'red'], forceDelete: false); // exactly this set
Attributes::for($product)->forget(['color'], forceDelete: false); // int removed ('color' works too)
Attributes::for($product)->forgetExcept(['color']); // list<string> removed
Attributes::for($product)->meta('color', ['hex' => '#ff0000']); // replace one attribute's meta (null clears)
Attributes::for($product)->stage()->set('color', 'red')->meta('color', ['hex' => '#f00'])->save(); // or ->sync()
Attributes::for($product)->by($admin)->set('color', 'red'); // record $admin as the revision's actor
// Reads
Attributes::for($product)->all(); // Collection<name, value>
Attributes::for($product)->toKeyValue(); // array<name, value>
Attributes::for($product)->keys(); // list<string>
Attributes::for($product)->get('color'); // value (or the defined default)
Attributes::for($product)->has('color'); // bool
Attributes::for($product)->history('color'); // Collection<AttributeRevision>, newest first
// Validation and history
Attributes::validateSet($product, ['min_price' => 5, 'max_price' => 9]); // a complete set, without writing
Attributes::resolveActorUsing(fn () => auth('admin')->user()); // default revision actor
// Housekeeping
Attributes::prune(30); // force-delete attributes trashed more than 30 days ago (default: config)Writes are validated against definitions, recorded in history when it’s on, and fire events. setMany() and sync() take an optional per-name meta map as their last argument (meta: ['color' => ['hex' => '#f00']]). A value write keeps the attribute’s stored meta unless you pass new meta; meta() replaces it and goes through strict mode, history and the AttributeAttached event like any other write.
Writes are all or nothing: setMany(), sync() and stage()->save() validate every value before the first write and run in one database transaction, so an invalid value or a failing write leaves the owner exactly as it was — nothing detached, nothing half-written. setValidated() validates the written values together with the stored ones, so rules may compare attributes — see Cross-attribute validation.
by($user) returns the same handle with an actor named for its writes, so revisions record who made the change — see History & audit trail. It names the actor for that handle only; Attributes::for($product) without it falls back to the resolver or the signed-in user.
The owner handle
Attributes::for($owner) returns an OwnerAttributes handle. Names are always scoped to that owner:
| Method | Returns | What it does |
|---|---|---|
set($name, $value = null, $meta = null) | Attribute | Attach or update one attribute; meta is an array or a Collection — without it the stored meta is kept. |
setMany(array $attributes, array $meta = []) | Collection<int, Attribute> | Attach or update several, all or nothing; other attributes are kept. |
setValidated(array $attributes, array $meta = []) | Collection<int, Attribute> | As setMany(), validated as one set with the stored values, under a lock on the owner’s row — see Cross-attribute validation. |
sync(array $attributes, bool $forceDelete = false, array $meta = []) | Collection<int, Attribute> | Make the attributes exactly this set, all or nothing. |
forget(array|string $names, bool $forceDelete = false) | int | Remove attributes; returns how many. |
forgetExcept(array $keep, bool $forceDelete = false) | list<string> | Remove every attribute not kept; returns the removed names. |
meta($name, $meta) | Attribute | Replace one attribute’s metadata (null clears it) — through strict mode, history and events. |
stage() | AttributeWriter | The fluent builder; save() → setMany(), sync() → sync(). |
all() / toKeyValue() / keys() | Collection / array / list<string> | Every stored attribute of the owner. |
get($name) / has($name) | mixed / bool | One typed value (default applied) / presence. |
history(?string $name = null) | Collection<int, AttributeRevision> | Recorded revisions, newest first. |
by(?Model $actor) | OwnerAttributes | The same handle; its writes record $actor as the revisions’ actor (null records nobody). An unsaved model throws AttributesException. |
Facade methods
The definition and validation methods delegate to the AttributeRegistry singleton — see Definitions & validation:
| Method | Returns | What it does |
|---|---|---|
for(Model $owner) | OwnerAttributes | The owner handle — every read and write for one model. |
prune(?int $days = null) | int | Force-delete attributes trashed more than $days ago (default: prune_after_days). |
resolveActorUsing(?Closure $resolver) | AttributesManager | Who revisions record when a write names no actor; null restores the auth guard default (see History & audit trail). |
define($definition) / defineMany(...$definitions) | AttributesManager | Register global definitions (chainable). |
has($name) / get($name) / all() | bool / ?AttributeDefinitionData / array | Read the global definitions. |
forget($name) / flush() | AttributesManager | Remove one or all global definitions — not stored values. |
resolveFor(?$owner, $name) / definitionsFor($owner) | definition(s) | Owner-aware definitions — the model’s schema over the global one. |
default($name, ?$owner = null) | mixed | A definition’s declared default. |
requiredNames() | list<string> | Names of the required global definitions. |
validate($name, $value) / validateFor(?$owner, $name, $value) | void | One value alone; throws InvalidAttributeValueException. |
validateSet($owner, array $values) | void | A complete set in one pass, so rules may refer to other attributes; throws InvalidAttributeValueException with a bag keyed by name. |
assertUnique($owner, $name, $value) | void | Throws DuplicateAttributeValueException. |
isStrict() / assertKnown($name, ?$owner = null) | bool / void | Strict mode; assertKnown() throws UnknownAttributeException (the owner’s model schema counts as registered). |
fake() | AttributesFake | Facade only — the recording fake (see Testing). |
Model methods are shorthand
The HasAttributes trait keeps short model methods — attachAttribute(), syncAttributes(), $product->attributes() and the rest — for the same operations. Each delegates to Attributes::for($this), so they behave identically and Attributes::fake() records them. The typed readers (attr(), attributeInt() …) and query scopes live only on the model.
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.