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

Reading attributes

The owner handle covers the everyday reads — values with defaults applied, presence, bulk maps and history:

use RoundlyConsulting\Attributes\Facades\Attributes;

$attributes = Attributes::for($product);

$attributes->get('color');    // typed value (or the defined default)
$attributes->has('color');    // bool
$attributes->all();           // Collection<name, value>
$attributes->toKeyValue();    // array<name, value>
$attributes->keys();          // list<string>
$attributes->history('color'); // Collection<AttributeRevision>, newest first

On the model

The trait adds richer reads — the Attribute row, the string form and metadata. Attributes::for($model)->get() and has() call the same methods:

$product->getAttachedAttributes();                      // Collection: ['color' => 'white', 'rating' => 5]
$product->getAttachedAttributeValue('color');           // typed value (mixed)
$product->getAttachedAttributeValueAsString('rating');  // '5' (string view)
$product->getAttachedAttribute('color');                // Attribute model or null
$product->hasAttachedAttribute('color');                // bool
$product->getAttachedAttributeMeta('color');            // Collection|null

The attr() accessor

attr() returns a typed value object — one entry point instead of a method per type. Defaults defined in a definition are applied automatically:

$product->attr('rating')->int();        // ?int (falls back to the defined default)
$product->attr('price')->float();       // ?float
$product->attr('on_sale')->bool();      // ?bool
$product->attr('tags')->array();        // ?array
$product->attr('published_at')->date(); // ?Carbon
$product->attr('color')->string();      // ?string
$product->attr('color')->raw();         // underlying typed value
$product->attr('color')->isNull();      // bool
(string) $product->attr('color');       // '' when unset, else the value

Typed readers

The per-type readers still work. Each returns null when the attribute is unset and has no default:

$product->attributeInt('rating');        // ?int
$product->attributeFloat('price');       // ?float
$product->attributeBool('on_sale');      // ?bool
$product->attributeArray('tags');        // ?array (a scalar is wrapped in an array)
$product->attributeDate('published_at'); // ?Carbon

Eager loading

Every read method is eager-loading aware: load attachedAttributes first and reads run against the loaded relation instead of querying per call:

$products = Product::query()->with('attachedAttributes')->get();

foreach ($products as $product) {
    $product->getAttachedAttributeValue('color'); // read from the loaded relation
    $product->attr('rating')->int();              // no query per call
}

$product->load('attachedAttributes'); // or load on a single model

Bulk reads & exports

Besides the handle’s all(), toKeyValue() and keys(), attribute collections flatten to plain maps:

use RoundlyConsulting\Attributes\Models\Attribute;

$product->attachedAttributes()->get()->toKeyValue(); // array<name, value>
Attribute::collect()->keyByName();                   // every stored attribute, keyed by name

Bulk maps list stored rows only. Defaults are applied by single-value reads — getAttachedAttributeValue(), attr(), the typed readers and Attributes::for()->get() — and only when the attribute is not attached; an attached attribute holding null reads as null. getAttachedAttributes(), all() and toKeyValue() never apply them. Attribute::collect() loads every attribute row in the table, across all owners.

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.