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 Custom Casts Laravel Package

vkovic/laravel-custom-casts

vkovic/laravel-custom-casts provides reusable, class-based custom attribute casts for Laravel Eloquent models, keeping get/set and serialization logic organized, testable, and out of your models—ideal for value objects, arrays, and other complex attribute transformations.

View on GitHub
Deep Wiki
Context7

Getting Started

  1. Install the package via Composer:

    composer require vkovic/laravel-custom-casts
    
  2. Look first at two files after install:

    • vendor/vkovic/laravel-custom-casts/src/CustomCast.php — the abstract base class every cast extends.
    • vendor/vkovic/laravel-custom-casts/src/HasCustomCasts.php — the trait that bridges the package into Eloquent's getAttribute/setAttribute.
  3. Add the trait to any model that will use a custom cast:

    use Vkovic\LaravelCustomCasts\HasCustomCasts;
    
    class User extends Model
    {
        use HasCustomCasts;
    
        protected $casts = [
            'email' => EmailCast::class,
        ];
    }
    
  4. Write your first cast in app/Casts/:

    namespace App\Casts;
    
    use Vkovic\LaravelCustomCasts\CustomCast;
    
    class EmailCast extends CustomCast
    {
        public function set($value)
        {
            return $value === null ? null : strtolower(trim($value));
        }
    
        public function get($value)
        {
            return $value;
        }
    }
    
  5. First real use case: any attribute where the DB representation and the PHP representation differ and Laravel's built-in casts (array, json, datetime, encrypted) don't cover it — e.g. normalising emails, storing/retrieving money as integer cents, encrypting a single column with your own key strategy, or casting a JSON column into a typed Value Object.

Implementation Patterns

Scalar normalisation cast. The simplest form — one side is a straight transform, the other returns as-is. Use it for casing, trimming, slug generation, phone number E.164 formatting, etc. Keep both get and set implemented even when one is a no-op; the base class is abstract.

Value-object cast. The most valuable day-to-day pattern. Store primitives in the DB, expose a rich object on the model:

class MoneyCast extends CustomCast
{
    public function set($value)
    {
        if ($value === null) return null;
        if ($value instanceof Money) return $value->cents();
        return (int) round($value * 100);
    }

    public function get($value)
    {
        return $value === null ? null : new Money((int) $value);
    }
}

Pair the value object with Illuminate\Contracts\Support\Arrayable and JsonSerializable so $model->toArray() and API resources don't blow up.

Composed casts. Casts that need to serialize a whole structure (array → JSON) should call json_encode/json_decode inside set/get rather than stacking with Laravel's array cast — the built-in cast and the package trait do not cooperate on the same attribute.

Constructor-free cast classes. The package resolves cast classes without full container injection. If your cast needs a collaborator (a formatter, a hasher), inject it via app() inside the method, or move that logic into a plain service the cast calls statically. Prefer constructor-less casts.

Casting on both read and write boundaries. Remember set() runs on fill(), create(), update(), and direct assignment. get() runs on property access and on toArray() / toJson(). Write both assuming the value can be null, an instance of the target type, or a raw scalar — because it will be called with each of those.

Testing pattern. Unit-test casts in isolation without a database:

$cast = new MoneyCast;
$this->assertSame(1050, $cast->set(new Money(10, 50)));
$this->assertEquals(new Money(10, 50), $cast->get(1050));

Then add one feature test that hits the model to confirm the trait is wired up — that's where 90% of integration bugs actually show.

Migration pattern (recommended). On Laravel 7+, prefer native CastsAttributes for new work and only keep this package for legacy models you can't touch yet. If you do migrate, the shapes are close enough that you can wrap a legacy cast in a native adapter:

class LegacyCastAdapter implements CastsAttributes
{
    public function get($model, $key, $value, $attributes) { return (new MoneyCast)->get($value); }
    public function set($model, $key, $value, $attributes) { return (new MoneyCast)->set($value); }
}

Gotchas and Tips

  • It is essentially superseded. Laravel 7 shipped the native CastsAttributes interface, and this package's last release was December 2020. For any project on Laravel 8+, use native custom casts — they get container resolution, $model/$key/$attributes context, and no trait requirement. Treat this package as a legacy-compat tool and plan the migration.

  • The trait is not optional. If HasCustomCasts isn't on the model, the cast class in $casts is treated as an unknown cast type and Eloquent will silently fall through (or throw a generic cast error). First debugging step for "my cast doesn't fire" is to grep the model for the use HasCustomCasts; line.

  • Version pin in composer.json. The 2020 release predates Laravel 8/9/10 and PHP 8.1. Pin the package (^1.x) and add a constraint on laravel/framework if you're on a modern stack, or Composer will happily install something that breaks on the next composer update.

  • Query builder bypasses casts entirely. Model::where('email', $raw)->update(...), DB::table(...), and bulk insert() never invoke set(). If normalisation matters on writes, do it in a mutator, a model event, or a dedicated action class — don't rely on the cast alone.

  • Null handling must be explicit. A get(null) on a nullable column will otherwise construct e.g. new Money(0) and quietly turn "no value" into "zero value" throughout your app. Guard both directions.

  • Serialization uses get(). toArray(), toJson(), queue payloads, and API resources all go through get(). Returning an object that isn't Arrayable/JsonSerializable produces confusing json_encode failures far from the cast itself.

  • Don't cache state in the cast. Instances are not guaranteed to be shared across calls. Any memoization keyed on $this will leak between attributes/models. Use the model or a service for anything stateful.

  • Debugging inside set()/get() is the fastest way to see what's actually arriving:

    public function set($value) { logger()->debug('set', ['type' => gettype($value), 'value' => $value]); /* ... */ }
    

    A dd() here fires on every assignment and every toArray(), so use logger() if the model is being serialized (e.g. in a response).

  • $casts ordering and $dates interplay. If the attribute is also in $dates, the date cast wins and your custom cast never runs. Keep them mutually exclusive.

  • Extension point for shared logic. If several casts need the same normalisation (say, trimming strings before storage), define a trait or abstract subclass over CustomCast and extend that — the package doesn't provide a pipeline, so your codebase's conventions do the work.

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.
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib