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)->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()
// 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
// 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.
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. |
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. |
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). |
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 | Throws InvalidAttributeValueException. |
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.