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 value | Enum case | Enforces |
|---|---|---|
| none, false, blank or omitted | UniqueScope::None | Nothing — the default. |
| owner or true | UniqueScope::Owner | Unique among owners of the same model type. |
| global | UniqueScope::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 replacedAn 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 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.