worksome/model-attributes
A Laravel package for defining typed, castable attributes on Eloquent models, cutting casting boilerplate and keeping model data consistent.
worksome/model-attributes lets you move the logic that would otherwise live in Eloquent accessors/mutators (getFooAttribute / setFooAttribute) into small, reusable, independently testable attribute classes that are declared on the model. If you have ever copy‑pasted the same "format this column" or "normalise this input" logic across three models, this package is the replacement.
Requirements for v0.2.0
platform config in composer.json before upgrading).illuminate/* ^10.0 added in this release). If you are still on Laravel 9 or PHP 8.1, pin worksome/model-attributes:^0.1.Install
composer require worksome/model-attributes
There is nothing to publish — no config file, no migration, and the service provider is auto-discovered.
Where to look first
get/set halves of the lifecycle.getPhoneAttribute().First use case
Take one accessor + mutator pair on your User model:
// Before
public function getPhoneAttribute($value): ?string
{
return $value ? preg_replace('/\s+/', '', $value) : null;
}
Extract it into one attribute class, register it on the model, and delete both methods. You get a class you can unit test without touching the database, and a second model can reuse it by adding one line.
One attribute class = one concern. Keep classes tiny and pure: normalise on set, format on get, nothing else. No queries, no auth(), no now() — the moment an attribute needs the container it becomes as hard to test as the accessor it replaced.
Reuse is the main win. Value-like attributes (phone numbers, money, slugs, enum-ish strings, JSON blobs) are the sweet spot. Build a small internal library of them (app/Attributes/) and treat them as shared infrastructure rather than per-model helpers.
Compose with casts, don't replace them. Casts are the right tool for type conversion and serialisation of whole columns; model attributes are the right tool for behavioural transformation on read/write. Use both on the same column when a cast handles the type and an attribute handles the formatting.
Test through the model, not just the class. A cheap feature test that creates a model via factory, sets the attribute and asserts both the database value and the hydrated value catches the integration mistakes (wrong registration, wrong column name) that a pure unit test cannot.
Keep models declarative. Put the registration in one obvious block/trait on the model so a reviewer can see at a glance which attributes a model participates in. If you have ten of them, group them behind a small trait per domain concept.
Factories and seeders. Since the setter side runs on assignment, factory data gets normalised for free — which is usually what you want, but make sure the test asserts the normalised form, not the raw input you passed in.
The PHP 8.2 bump is the real breaking change in v0.2.0. Nothing in the public API changed, but the floor moved: a project on 8.1 will silently fail to resolve, and CI images lag behind locally more often than you'd expect. Run composer why-not php 8.2 before upgrading and verify config.platform.php isn't pinned lower. composer update worksome/model-attributes -W is the cleanest way to see the constraint failure early.
Attributes are PHP-level only — they do not exist in SQL. You cannot use them in where(), orderBy() or raw queries. If a value is stored in a transformed shape, query with the stored shape (or scope it), and don't assume the attribute output matches the column.
Watch for read-triggering-read loops. An attribute that internally reads another attribute on the same model is fine, but an attribute that reads itself (or two attributes that read each other) will recurse until the stack blows. Prefer reading $this->getRawOriginal('column') inside the model when a raw value is needed.
Null and empty strings. Decide explicitly whether null passes through untouched or is normalised. Silent coercion of null into '' is the most common cause of "why is this column not nullable anymore" surprises.
Serialisation. toArray() / toJson() reflect whatever Laravel sees after the getter side runs. If an API contract depends on the raw value, assert it in a test rather than assuming.
Performance. Getter logic runs on every attribute access, including inside loops and Blade templates. Keep it allocation-light; if it does real work, cache the result on the instance rather than recomputing per read.
Debugging. If an attribute appears not to run, check registration first (typo in the column name, attribute registered after the value was already hydrated), then whether another cast is applied to the same key — two mechanisms on one column is the usual culprit. $model->getRawOriginal('column') versus $model->column is the fastest way to tell which layer is doing what.
Extension points. Subclass the package's base attribute when you need shared behaviour (trimming, null policy, logging); keep any app-wide conventions in that one subclass so the dozens of concrete attributes stay one-liners.
How can I help you explore Laravel packages today?