The HasAttributes trait
Add the HasAttributes trait and implement the matching contract on any model you want to attach attributes to:
use Illuminate\Database\Eloquent\Model;
use RoundlyConsulting\Attributes\Contracts\HasAttributes as HasAttributesContract;
use RoundlyConsulting\Attributes\Traits\HasAttributes;
class Product extends Model implements HasAttributesContract
{
use HasAttributes;
}Any Eloquent model works — products, users, orders, tickets. Attributes link through a polymorphic owner relation, so one table serves every model.
The relation
Stored rows are exposed as the attachedAttributes() morph-many relation. Mind the name: attributes() is the fluent writer, not the relation. Queries on the relation return an AttributeCollection with toKeyValue() and keyByName() helpers:
$product->attachedAttributes; // AttributeCollection of Attribute models
$product->attachedAttributes()->where('name', 'color')->first();
Product::query()->with('attachedAttributes')->get(); // eager load for cheap readsReading methods
| Method | Returns | Purpose |
|---|---|---|
attachedAttributes() | MorphMany | The relation to the stored Attribute rows. |
getAttachedAttributes() | Collection | name → typed value for every stored attribute. |
getAttachedAttribute($name) | ?Attribute | One Attribute model, or null. |
getAttachedAttributeValue($name) | mixed | The typed value, or the definition’s default. |
getAttachedAttributeValueAsString($name) | ?string | The value in its storage-string form. |
hasAttachedAttribute($name) | bool | Whether the attribute is present. |
getAttachedAttributeMeta($name) | ?Collection | The attribute’s metadata. |
attr($name) | StoredAttributeValue | Typed accessor with defaults applied. |
attributeInt / Float / Bool / Array / Date($name) | ?int … ?Carbon | Per-type readers. |
history($name = null) | Collection | Recorded revisions, newest first. |
attributeHistory($name = null) | MorphMany | The revisions relation. |
Writing methods
| Method | Same as Attributes::for($model)->… | Purpose |
|---|---|---|
attributes() | stage() | Fluent AttributeWriter for staging several attributes. |
attachAttribute($name, $value = null, $meta = null) | set() | Attach or update one attribute, with optional metadata. |
attachAttributes(array $attributes) | setMany() | Attach or update several from a name → value map. |
syncAttributeMeta($name, $meta = null) | meta() | Replace one attribute’s metadata. |
syncAttributes(array $attributes, $forceDelete = false) | sync() | Make the attributes match the given set exactly. |
detachAttribute($name, $forceDelete = false) | forget() | Remove one attribute. |
detachAttributes(array $names, $forceDelete = false) | forget() | Remove several attributes. |
destroyAttributes(array $names, $forceDelete = false) | forget() | Remove several attributes (same path as detachAttributes). |
destroyAttributesExcept(array $names, $forceDelete = false) | forgetExcept() | Remove every attribute not in the list. |
validateAttributes() | — (registry validation) | Assert required attributes are present and values are valid. |
The write methods are shorthand, not a separate path: each one delegates to Attributes::for($this), so host overrides and Attributes::fake() see them. Every writer returns the model, so calls chain; every forceDelete flag defaults to a soft delete. The contract requires attachedAttributes(), getAttachedAttributes(), getAttachedAttributeValue(), hasAttachedAttribute(), attr() and validateAttributes() — the trait provides them all. The query scopes are covered in Query scopes.
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.