somnambulist/attribute-model
A Laravel package for declarative model attributes: define typed, castable fields on Eloquent models using PHP attributes, with generated accessors, mutators, and validation—so you can add structured metadata without extra tables or boilerplate.
Install and orient yourself
composer require somnambulist/attribute-model
The package is framework-agnostic (no Laravel service provider is required to use it), so the fastest way to understand it is to read, in order:
composer.json — confirm the PHP version constraint and the current namespace. Recent somnambulist majors moved everything under Somnambulist\Components\AttributeModel\..., older tags use Somnambulist\AttributeModel\...; copy the namespace from src/ rather than the README if they disagree.src/AbstractAttributeModel.php — the base class you will extend. Look for getAttribute(), setAttribute(), hasAttribute(), toArray(), and how $casts is declared (property vs. method) in your installed version.src/Casts/ — the built-in casters (string, int, bool, float, array, collection, datetime, uuid, etc.). This directory is the feature list.tests/ — the most reliable usage documentation for a package this small.First use case: typed DTOs from raw arrays
The package pays for itself the moment you stop hand-writing (int), new DateTimeImmutable(...), and json_decode() conversions in every DTO.
use Somnambulist\Components\AttributeModel\AbstractAttributeModel;
final class OrderSummary extends AbstractAttributeModel
{
protected array $casts = [
'id' => 'uuid',
'total' => 'int',
'is_paid' => 'bool',
'lines' => 'array',
'created_at' => 'datetime',
];
}
$order = new OrderSummary([
'id' => '9f0c...-...',
'total' => '1999',
'is_paid' => '1',
'lines' => '[{"sku":"ABC","qty":2}]',
'created_at' => '2024-05-01 10:00:00',
]);
$order->total; // int(1999)
$order->created_at; // DateTimeImmutable
Adapt the constructor/hydration call (new, create(), hydrate(), or assign()) to whichever the installed version exposes — check AbstractAttributeModel for the entry point. Once you can round-trip one array, you have the whole mental model: assign raw, read casted.
Use it as the base for read models and API payloads. This package was extracted from somnambulist/read-models, so the natural home is the read side: hydration from a DB row (via DB::select() / ->getAttributes()), a projection table, a JSON column, or a queue payload. Keep Eloquent out of the read path and hydrate straight into these objects.
Map Eloquent output into attribute models at the boundary:
$summary = new OrderSummary($eloquentOrder->getAttributes());
This decouples your read shape from the schema and gives you casting without dragging a model instance (and its lazy-loading traps) through your application layer.
Register custom casters for your value objects. The cast name in $casts is resolved through the package's caster registry, so you can add your own for Money, Email, enums, or strongly-typed IDs:
// register once, e.g. in a small bootstrap or a Laravel service provider's boot()
// (consult src/ for the exact registry API in your version)
$registry->register('money', MoneyCaster::class);
protected array $casts = [
'unit_price' => 'money',
'status' => 'enum:App\Enums\OrderStatus',
];
Keep casters tiny and side-effect free: cast($value): mixed and optionally a serialize() path for toArray().
Serialization as the output contract. Because the base class implements Arrayable/JsonSerializable (verify in src/), you can return these objects straight from a controller or a Laravel JsonResource and let toArray() produce the array shape. Decide early whether toArray() returns casted values (DateTimeImmutable) or serialized primitives (ISO-8601 strings) — for JSON responses you almost always want the latter.
Test fixtures and factories. These objects are excellent for assertions: build expected DTOs in tests and compare with assertEquals() against the hydrated real one. That gives you value-object equality semantics for free and keeps tests free of raw array comparisons.
Event-sourced / CQRS read side. If you already use somnambulist read-models, this is the same engine underneath; use attribute-model directly in smaller services where the full read-model stack is overkill for one or two DTOs.
Maturity risk is the biggest gotcha. Zero stars, zero dependents, zero score means essentially no community validation. Pin an exact version ("somnambulist/attribute-model": "1.2.3"), vendor the source if your release process allows it, and confirm the MIT licence file is present. The upside: the package is a handful of small classes, so a fork or inlining it is a realistic escape hatch rather than a rewrite.
$casts is not Eloquent's $casts. The names look identical but semantics differ (notably around array/collection, dates, and null handling). Don't assume datetime:Y-m-d-style parameter syntax works unless the caster explicitly parses it. Laravel 11's Eloquent casts() method also collides conceptually — never try to mix the two hierarchies in one class.
Missing keys are usually silent. Assigning an array without a key generally leaves the attribute unset and returns null on read. There is no schema validation. If a missing field is a bug, add an explicit assert/guard in a subclass constructor or a hasAttribute() check at the boundary — do not rely on the caster.
Magic accessors hide your types from static analysis. __get/__set mean PHPStan and Psalm cannot see properties. Add @property-read docblocks on every DTO or install a PHPStan extension. This is the single highest-ROI thing you can do for day-to-day ergonomics.
isset() lies. With magic getters, isset($model->foo) and empty($model->foo) behave according to __isset, which may report false for a legitimately null attribute. Prefer hasAttribute('foo') where it exists, or check for null explicitly.
Watch for double casting. Setting an attribute with an already-cast value (e.g. assigning a DateTimeImmutable to a datetime cast, or an array to an array cast) can throw or silently re-cast depending on the caster. Casters should be tolerant; write a defensive if ($value instanceof X) return $value; guard in every custom caster you author.
Nested structures are shallow. An array cast typically json_decodes one level and stops — nested keys are not recursively cast into objects. If you need nested DTOs, write a caster that maps child arrays into child attribute models explicitly.
Timezones and immutability. Prefer immutable date casts and set the default timezone explicitly (date_default_timezone_set() or a caster argument). Naive datetime casts are the classic source of off-by-one-hour bugs in reporting endpoints.
Performance is fine but not free. Cast resolution happens per attribute access. For hot loops over thousands of rows, resolve casted values once into plain arrays rather than reading magic properties repeatedly.
Extension points to reach for, in order: a custom caster class → overriding getAttribute()/setAttribute() in an intermediate base class your DTOs extend → replacing toArray() for your serialization contract. Avoid overriding the registry globally in library code; do it once at application bootstrap.
Debugging tip: when a cast returns something unexpected, dump the raw assigned value first ($model->getAttributes() or the underlying storage accessor) to confirm whether the problem is assignment or casting — the two failure modes look identical from the outside.
How can I help you explore Laravel packages today?