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

The published config/attributes.php in full, with the definition examples commented out:

return [
    'model' => RoundlyConsulting\Attributes\Models\Attribute::class,
    'table' => 'attributes',
    'key_type' => env('ATTRIBUTES_KEY_TYPE', 'bigint'),
    'actor_key_type' => env('ATTRIBUTES_ACTOR_KEY_TYPE', 'bigint'),
    'strict' => env('ATTRIBUTES_STRICT', false),
    'prune_after_days' => env('ATTRIBUTES_PRUNE_AFTER_DAYS', 30),
    'delete_with_owner' => env('ATTRIBUTES_DELETE_WITH_OWNER', true),
    'history' => [
        'enabled' => env('ATTRIBUTES_HISTORY', false),
        'table' => 'attribute_revisions',
        'actor' => [
            'enabled' => env('ATTRIBUTES_HISTORY_ACTOR', false),
            'guard' => env('ATTRIBUTES_HISTORY_ACTOR_GUARD'),
        ],
    ],
    'validation' => [
        'whole_set' => env('ATTRIBUTES_VALIDATE_WHOLE_SET', false),
    ],
    'resources' => [
        'revision_actor' => RoundlyConsulting\Attributes\Http\Resources\RevisionActorResource::class,
    ],
    'unique_scope' => null,
    'definitions' => [
        // 'rating'  => ['type' => 'integer', 'rules' => ['min:1', 'max:5'], 'required' => true],
        // 'sku'     => ['type' => 'string', 'unique' => 'global'],
        // 'code'    => ['type' => 'string', 'unique' => 'scope', 'unique_scope' => 'team_id'],
        // 'token'   => ['type' => 'string', 'encrypted' => true],
        // 'retries' => ['type' => 'integer', 'default' => 3],
    ],
];

Every key

KeyDefaultEnvPurpose
modelAttribute::class—Eloquent model that stores attributes. Must be the packaged Attribute model or a subclass of it; anything else throws.
tableattributes—Table used by the migration and the model. A blank value is not set and uses the default; a non-string value throws.
key_typebigintATTRIBUTES_KEY_TYPEOwner key type for the morph columns: bigint, uuid or ulid (case-insensitive; anything else throws InvalidConfigurationException when the migrations run). Read when the migrations run.
actor_key_typebigintATTRIBUTES_ACTOR_KEY_TYPEKey type of the revisions’ actor columns (the update_attribute_revisions_add_actor migration): bigint, uuid or ulid, independent of key_type — your users may use other keys than your owner models. Read when the migrations run.
strictfalseATTRIBUTES_STRICTReject writes — a value or its meta — to names with neither a global nor a model-schema definition (UnknownAttributeException).
prune_after_days30ATTRIBUTES_PRUNE_AFTER_DAYSDefault age in days for attributes:prune. A whole number, 0 or more (0 purges every trashed attribute); blank (ATTRIBUTES_PRUNE_AFTER_DAYS=) is not set and gives 30; anything else throws.
delete_with_ownertrueATTRIBUTES_DELETE_WITH_OWNERDeleting an owner for good — a model without SoftDeletes, or forceDelete() — removes its attribute rows and frees its unique values; a soft delete keeps them. Revisions are kept either way. See Removing & syncing.
history.enabledfalseATTRIBUTES_HISTORYRecord an old→new revision on every attach, update, meta change and detach.
history.tableattribute_revisions—Table for the audit trail. A blank value is not set and uses the default; a non-string value throws.
history.actor.enabledfalseATTRIBUTES_HISTORY_ACTORAlso record who made each change — needs the actor migration. Planned to default on in the next major.
history.actor.guardnullATTRIBUTES_HISTORY_ACTOR_GUARDAuth guard the default actor comes from; null or blank means the default guard; a non-string value throws.
validation.whole_setfalseATTRIBUTES_VALIDATE_WHOLE_SETEvery value write — set(), setMany(), stage()->save(), sync() — and validateAttributes() validate the owner’s whole attribute set, so rules may refer to other attributes. See Cross-attribute validation.
resources.revision_actorRevisionActorResource::class—JSON resource a revision’s actor renders through in AttributeRevisionResource. Null or blank is the default; anything that is not an instantiable JsonResource subclass throws InvalidConfigurationException naming the key.
unique_scopenull—What a 'unique' => 'scope' definition without its own unique_scope is unique within: an attribute or relation name on the owner (team_id, team) or a ResolvesUniqueScope class-string. Null or blank means no default; a non-string value throws InvalidConfigurationException. See Unique, required & defaults.
definitions[]—Registry seed, keyed by attribute name; each entry is an array. Loaded once at boot.

Definition keys

Each entry under definitions is keyed by attribute name and accepts these keys:

Definition keyTypePurpose
typestringstring, integer, float, boolean, array or datetime (or the AttributeType case) — values are validated against it and stored in it ('5' becomes 5 for integer). Unset or blank means string; anything else throws.
rulesarrayExtra Laravel validation rules, applied after the type’s own rule. Must be an array (['min:1', 'max:5']); a pipe string throws.
requiredboolEnforced by $model->validateAttributes(). Read like the boolean env values; anything else throws.
defaultmixedReturned by reads when the attribute is not attached. Never persisted.
uniquestring / boolowner or true (per owner type), scope (per owner type within the owner’s team or tenant), global (across every owner), none or false (unset or blank also means none); anything else throws instead of switching uniqueness off. Backed by a database unique index — encrypted values included.
unique_scopestring / ClosureOnly with 'unique' => 'scope' — an attribute or relation name on the owner (team_id, team), a ResolvesUniqueScope class-string, or a closure in a model’s attributeDefinitions(). Unset falls back to the unique_scope setting. On any other unique value, or blank, it throws.
encryptedboolStores the value as Crypt ciphertext at rest. Read like the boolean env values; anything else throws.
// config/attributes.php
'definitions' => [
    'rating'  => ['type' => 'integer', 'rules' => ['min:1', 'max:5'], 'required' => true],
    'sku'     => ['type' => 'string', 'unique' => 'global'],
    'token'   => ['type' => 'string', 'encrypted' => true],
    'retries' => ['type' => 'integer', 'default' => 3],
],

Config definitions are loaded into the registry once, when the app boots; an invalid entry throws InvalidConfigurationException at boot, naming attributes.definitions.<name>.<key>. You can also register definitions in code or declare them per model — see Definitions & validation and Per-model schemas.

Environment

The deployment-sensitive keys are env-backed:

# bigint | uuid | ulid — set before you run the migrations
ATTRIBUTES_KEY_TYPE=bigint

# reject attribute names that are not registered
ATTRIBUTES_STRICT=false

# default age (days) for attributes:prune
ATTRIBUTES_PRUNE_AFTER_DAYS=30

# remove an owner's attributes when the owner is deleted for good
ATTRIBUTES_DELETE_WITH_OWNER=true

# record an old -> new revision on every change
ATTRIBUTES_HISTORY=false

# also record who made each change (needs the actor migration)
ATTRIBUTES_HISTORY_ACTOR=false

# guard the default actor comes from — empty means the default guard
ATTRIBUTES_HISTORY_ACTOR_GUARD=

# key type of the revisions' actor columns — set before you migrate
ATTRIBUTES_ACTOR_KEY_TYPE=bigint

# validate the owner's whole attribute set on every value write
ATTRIBUTES_VALIDATE_WHOLE_SET=false

The boolean env values accept true/false, 1/0, on/off and yes/no. Strict mode, history, the history actor and whole-set validation are read at call time, so a change takes effect without a restart. Every setting is read strictly: a key that is not set — absent, null or blank (a host’s KEY=) — takes its default, and any other invalid value throws InvalidConfigurationException naming the key — it never falls back silently.

Check the resolved values at any time with Laravel’s about command. Definitions are reported by count only, never by name — an attribute name often names the very secret the encrypted flag protects:

php artisan about --only=attributes

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.