Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Two Way Attribute Casting Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

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/.

  1. Pin the install exactly. 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.
  2. Confirm it loads on your Laravel version first. The package predates Laravel 7's native 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.
  3. Read the cast class you intend to use. Look for its constructor signature (parameters passed after : in $casts), how it handles null, and whether the class is final (this determines whether you can subclass it for app-specific normalisation).
  4. Apply it to one attribute:
protected $casts = [
    'unit_price' => DecimalCast::class.':2', // scale, per the cast's constructor
];
  1. Verify the round trip before adopting further — you need a pass on create, update, and re-hydrate:
$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.

Implementation Patterns

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.

Gotchas and Tips

  • Archived and pre-1.0. No upstream fixes are coming. If the decimal fix in 0.1.2 is what attracted you, port that logic into a native Laravel custom cast (Laravel 7+ 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.
  • 0.1.2 is a behavioural fix. Values read from a decimal attribute before this release may have been floats or truncated strings. Re-run any assertEquals-based test — a passing test may have been passing for the wrong reason. Diff the types, not just the values.
  • Float leak on assignment. 19.99 as a float is already lossy before the cast runs. Always assign strings; treat float input from a caller as a bug.
  • Truncation vs rounding. Confirm which the cast does. 19.999 → 19.99 vs 20.00 are both defensible; only one matches your invoice expectations.
  • String comparison traps. '10.0' !== '10.00'. Pick a canonical scale and normalise on output so equality checks and cache keys stay stable.
  • Null vs zero. A nullable column and a zero default are different states. Check whether the cast preserves null or coerces to 0.00 — this affects whereNull queries and form prefill.
  • Driver differences. SQLite returns integers where MySQL returns strings for the same 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.
  • Debugging. 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.
  • Extension points. If the cast class is non-final, subclass it for app-specific normalisation rather than forking the repo — you keep the option of an upstream fix path, however theoretical here.
  • Exit criteria. Document now what would make you drop the package (Laravel upgrade, a rounding regression, need for multi-currency). With zero dependents and no maintenance, the package should sit behind an interface from day one.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator