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

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:

MethodReturnsWhat it does
set($name, $value = null, $meta = null)AttributeAttach 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)intRemove attributes; returns how many.
forgetExcept(array $keep, bool $forceDelete = false)list<string>Remove every attribute not kept; returns the removed names.
meta($name, $meta)AttributeReplace one attribute’s metadata (null clears it) — through strict mode, history and events.
stage()AttributeWriterThe fluent builder; save() → setMany(), sync() → sync().
all() / toKeyValue() / keys()Collection / array / list<string>Every stored attribute of the owner.
get($name) / has($name)mixed / boolOne 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:

MethodReturnsWhat it does
for(Model $owner)OwnerAttributesThe owner handle — every read and write for one model.
prune(?int $days = null)intForce-delete attributes trashed more than $days ago (default: prune_after_days).
define($definition) / defineMany(...$definitions)AttributesManagerRegister global definitions (chainable).
has($name) / get($name) / all()bool / ?AttributeDefinitionData / arrayRead the global definitions.
forget($name) / flush()AttributesManagerRemove 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)mixedA definition’s declared default.
requiredNames()list<string>Names of the required global definitions.
validate($name, $value) / validateFor(?$owner, $name, $value)voidThrows InvalidAttributeValueException.
assertUnique($owner, $name, $value)voidThrows DuplicateAttributeValueException.
isStrict() / assertKnown($name, ?$owner = null)bool / voidStrict mode; assertKnown() throws UnknownAttributeException (the owner’s model schema counts as registered).
fake()AttributesFakeFacade 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 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.