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

Unique, required & defaults

Unique values

A unique definition rejects a value that another owner already holds:

use RoundlyConsulting\Attributes\DataTransferObjects\AttributeDefinitionData;
use RoundlyConsulting\Attributes\Enums\AttributeType;
use RoundlyConsulting\Attributes\Enums\UniqueScope;
use RoundlyConsulting\Attributes\Facades\Attributes;

Attributes::define(new AttributeDefinitionData('sku', AttributeType::String_, unique: UniqueScope::Global_));

Attributes::for($a)->set('sku', 'ABC');
Attributes::for($b)->set('sku', 'ABC'); // throws DuplicateAttributeValueException
Attributes::for($a)->set('sku', 'ABC'); // re-saving the owner's own value is fine

Attributes::for($a)->forget('sku');
Attributes::for($b)->set('sku', 'ABC'); // fine — detaching freed the value
Config valueEnum caseEnforces
none, false, blank or omittedUniqueScope::NoneNothing — the default.
owner or trueUniqueScope::OwnerUnique among owners of the same model type.
globalUniqueScope::Global_Unique across every owner, whatever its type.

Re-saving the same owner’s own value is idempotent, detaching an attribute frees its value, and null values are never checked. DuplicateAttributeValueException exposes attributeName and scope.

Uniqueness is a database guarantee, not just a check before the write: each unique value carries a deterministic hash in the unique_hash column, which has a unique index — two concurrent writers of the same value cannot both commit, and the loser gets DuplicateAttributeValueException. It works for encrypted values too: their hash is a keyed blind index (an HMAC under a key derived from APP_KEY), so the database never holds the plaintext. Rotating APP_KEY changes that key — re-save encrypted unique values afterwards.

Required attributes

required is enforced on demand, not on every attach — call validateAttributes() when the model should be complete, for example at the end of an import or a checkout step:

use RoundlyConsulting\Attributes\Exceptions\MissingRequiredAttributeException;

Attributes::define(new AttributeDefinitionData('rating', AttributeType::Integer, required: true));

try {
    $product->validateAttributes();
} catch (MissingRequiredAttributeException $e) {
    $e->attributeName; // 'rating'
}

validateAttributes() walks every definition that applies to the model, global and model-declared. It throws MissingRequiredAttributeException for the first required attribute that is absent, and re-validates every present value against its type and rules.

Default values

A definition’s default is returned by reads when the attribute is not attached. Defaults apply on read only — they are never persisted, and bulk getAttachedAttributes() lists stored rows only:

Attributes::define(new AttributeDefinitionData('retries', AttributeType::Integer, default: 3));

Attributes::for($product)->get('retries'); // 3 when not attached, the stored value when set
$product->attributeInt('retries');         // 3
$product->attr('retries')->int();          // 3

Attributes::for($product)->set('retries', null);
Attributes::for($product)->get('retries'); // null — an explicitly stored null is not replaced

An attribute explicitly stored as null reads as null — the default fills in only for an attribute that isn’t attached.

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.