genealabs/laravel-two-way-attribute-casting
A Laravel package that enables two-way attribute casting on Eloquent models, letting you transform values both when reading from and writing to the database with custom cast classes. Useful for encapsulating conversion logic in one place for cleaner, reusable model code.
This package is archived (last release 2018-05-29, 0.1.x, 1 star). Treat the repository source as the only documentation and budget an hour to read it before writing any code. There is no facade or documented service-provider API; the surface you care about is the cast classes under src/.
composer require genealabs/laravel-two-way-attribute-casting:0.1.2. At 0.1.x an unreviewed patch can change casting semantics, and the 0.1.2 release was itself a behavioural fix to decimal handling.CastsAttributes contract, so verify the cast actually runs — register it on a throwaway model and dump get_debug_type($model->attr) after a fresh(). If nothing changes, stop here and use a native custom cast instead.: in $casts), how it handles null, and whether the class is final (this determines whether you can subclass it for app-specific normalisation).protected $casts = [
'unit_price' => DecimalCast::class.':2', // scale, per the cast's constructor
];
$order = Order::factory()->create(['unit_price' => '19.99']);
$this->assertSame('19.99', (string) $order->fresh()->unit_price);
$order->update(['unit_price' => '20.00']);
$this->assertSame('20.00', (string) $order->fresh()->unit_price);
First real use case: a decimal(10,2) money column currently passed through float in PHP. The cast's whole job is to make the read path and the write path symmetric so you stop writing a separate accessor and mutator that drift apart.
One cast, both directions. That is the entire pitch: define the transformation once and it applies on hydrate and on persist. Before adopting, delete any existing accessor/mutator for the same attribute — layering them silently breaks the symmetry you adopted the package for.
Keep the DB representation authoritative. Migrations stay decimal(10,2), seeds stay strings. Never seed with floats; that reintroduces the precision loss before the cast ever sees the value.
Match scale in three places. Migration decimal(10,2), cast parameter :2, and validation rule (decimal:0,2). Mismatch here is the single most common source of silent truncation and won't surface until an assertion compares exact strings.
Normalise input at the boundary. Convert request input to a canonical string in prepareForValidation() rather than relying on the cast to accept floats, ints, and strings interchangeably:
protected function prepareForValidation(): void
{
$this->merge([
'unit_price' => is_numeric($this->unit_price)
? number_format((float) $this->unit_price, 2, '.', '')
: $this->unit_price,
]);
}
Do arithmetic outside the model. A precise read does not make operators safe. Use bcmath on the string form, then assign the string back.
Aggregates bypass casts entirely. ->sum('unit_price') returns whatever the driver hands back. Re-wrap the result explicitly before formatting or asserting.
Serialise deliberately. In API Resources emit (string) $this->unit_price so JSON consumers get "19.99", not 19.99. Do the numeric conversion at the resource layer where the precision contract is obvious — never by dropping the cast.
Isolate the dependency for the exit. Put cast declarations behind a base model trait or a single $casts block per domain, so replacing the package with a native CastsAttributes class is a one-file change. Given the archival status, plan that migration now, not later.
Test the write paths individually. Add one assertion per path — create, update, fill, refresh, first() — because casting bugs in 0.1.x releases tend to appear on one path and not others. Use assertSame on the normalised string, never assertEquals on floats.
CastsAttributes, which is bidirectional by design and gives you the same one-class-for-get-and-set model). That is usually a 20-line class.assertEquals-based test — a passing test may have been passing for the wrong reason. Diff the types, not just the values.19.99 as a float is already lossy before the cast runs. Always assign strings; treat float input from a caller as a bug.19.999 → 19.99 vs 20.00 are both defensible; only one matches your invoice expectations.'10.0' !== '10.00'. Pick a canonical scale and normalise on output so equality checks and cache keys stay stable.null or coerces to 0.00 — this affects whereNull queries and form prefill.decimal column. If CI is SQLite and production is MySQL, run the cast tests on both. This is exactly the class of bug a 0.1.2-style patch addresses.dd(get_debug_type($model->unit_price), $model->getRawOriginal('unit_price')) cleanly separates "the cast is wrong" from "the write never persisted what you thought". Add a temporary DB::listen to see the bound parameters on save.How can I help you explore Laravel packages today?