Laravel’s $casts property is one of those quiet conveniences that shapes how you handle data without drawing much attention. Drop a built-in type like boolean or datetime into the array, and Eloquent automatically converts raw database strings into something friendlier before the value ever reaches your controllers or views. It keeps your code tidy. But the moment you try to call your own helper directly inside that array, the framework pushes back. Writing something like 'username' => 'encrypt_data()' will not work. Laravel expects either a native cast keyword or a class that implements the CastsAttributes interface. That requirement exists because the casting layer runs deep inside Eloquent’s hydration and serialization cycle; it needs a predictable object with a clear contract rather than an arbitrary function string that might not exist at runtime.

Why Helper Functions Fail in the $casts Array

When Eloquent loads a model, it scans the $casts array to decide how to transform each column. The framework recognizes specific primitives—string, integer, array, encrypted, and so on—and it recognizes fully-qualified class names that implement CastsAttributes. It does not evaluate the string as code. So 'encrypt_data()' is treated as an unknown cast type, which triggers an error. Even if Laravel did parse it, there would be no reliable way to pass the model instance, attribute key, current value, and surrounding attributes into your function in the correct order. The interface exists precisely to standardize that handshake between your custom logic and Eloquent’s internals.

You have two solid ways to bridge this gap. One favors long-term reuse. The other favors speed when you only need a quick patch inside a single model.

Method 1: Write a Custom Cast Class

If the same transformation applies to multiple fields or spreads across several models, a dedicated cast class is the cleaner bet. It lives in its own file, can be unit-tested in isolation, and keeps your models free of repetitive boilerplate.

Start by creating app/Casts/CustomEncrypt.php. The namespace should match your autoloading setup, typically App\Casts. The class must implement Illuminate\Contracts\Database\Eloquent\CastsAttributes, which forces you to define two methods: get and set.

<?php

namespace App\Casts;

use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;

class CustomEncrypt implements CastsAttributes
{
    public function get(Model $model, string $key, mixed $value, array $attributes): mixed
    {
        return decrypt_data($value);
    }

    public function set(Model $model, string $key, mixed $value, array $attributes): mixed
    {
        return encrypt_data($value);
    }
}

The signatures matter. Eloquent passes four arguments into each method. $model is the instance being populated or saved, which lets you inspect other fields if your logic depends on them. $key is the column name currently being cast. $value is the raw string or null coming from the database on a get, or the user-supplied value on a set. $attributes is the entire raw array of columns for that row. You do not need to use all four every time, but the interface requires them.

In the get method above, decrypt_data($value) runs after Eloquent fetches the row from the database and before the value lands on your model. In the set method, encrypt_data($value) runs before the INSERT or UPDATE, ensuring the database never sees plaintext.

To wire it up, reference the class inside your model’s $casts array:

protected $casts = [
    'username' => CustomEncrypt::class,
    'password' => CustomEncrypt::class,
];

Because you are using the class constant, the autoloader handles the rest. If your application later needs to swap the encryption scheme, you edit one file, and every mapped field changes behavior instantly. That centralization is hard to beat when you are managing sensitive data across many tables.

Method 2: Reach for an Accessor and Mutator

Sometimes you do not want a new file for a transformation that only matters in one place. Laravel’s Attribute class lets you define get and set logic directly on the model using PHP 8+ closure syntax.

use Illuminate\Database\Eloquent\Casts\Attribute;

protected function username(): Attribute
{
    return Attribute::make(
        get: fn ($value) => decrypt_data($value),
        set: fn ($value) => encrypt_data($value),
    );
}

Here the method name must match the column or attribute you are targeting. When Eloquent reads username from the database, it passes the raw value through the get closure. When you assign a new value to the model’s username property, the set closure encrypts it before Eloquent builds the query.

This approach shines in prototypes or legacy models where a full directory of cast classes feels like overkill. The downside is repetition. If you later decide that email, phone, and backup_code need the same treatment, you will end up copying those closures across multiple methods or models. That noise adds up. It also prevents you from reusing the logic in a unit test without booting the entire model.

انتخاب بین این دو

زمانی از یک کلاس cast سفارشی استفاده کنید که قابلیت استفاده مجدد، تست‌پذیری و سبک نگه داشتن مدل‌ها برایتان اهمیت دارد. این کار به سایر توسعه‌دهندگان سیگنال می‌دهد که این تبدیل، یک مفهوم اصلی در اپلیکیشن شماست، نه یک راهکار موقت و سرهم‌بندی شده.

زمانی از یک accessor استفاده کنید که منطق مورد نظر واقعاً محدود (localized)، آزمایشی است یا احتمال نمی‌رود فراتر از اسپرینت فعلی نیاز باشد. این کار به شما اجازه می‌دهد بدون پراکنده کردن فایل‌های کوچک در کل کد، با سرعت بالا پیش بروید. فقط آماده باشید که به محض اینکه الگوی مشابه برای دومین یا سومین بار ظاهر شد، آن را به یک کلاس cast تبدیل (refactor) کنید.

جزئیات کاربردی که باید در نظر داشت

Castهای سفارشی قدرتمند هستند، اما رفتاری را معرفی می‌کنند که اگر حواستان نباشد، می‌تواند شما را غافلگیر کند. اول، به یاد داشته باشید که متد get هر آنچه را که پایگاه داده بازگردانده است، از جمله null دریافت می‌کند. اگر decrypt_data ورودی null را نمی‌پذیرد، برای آن شرط بگذارید:

public function get(Model $model, string $key, mixed $value, array $attributes): mixed
{
    return is_null($value) ? null : decrypt_data($value);
}

دوم، castها در طول سریال‌سازی آرایه و JSON اجرا می‌شوند. وقتی یک مدل را به عنوان یک API resource برمی‌گردانید یا متد toArray() را فراخوانی می‌کنید، منطق get در cast همچنان اعمال می‌شود. این معمولاً همان چیزی است که می‌خواهید، اما اگر در حال نمایش مقادیر رمزگشایی شده هستید و نیاز دارید لایه‌های کنترل سطح دسترسی اضافی را روی آن‌ها اعمال کنید، بهتر است این نکته را به خاطر داشته باشید.

سوم، و مهم‌تر از همه، عملیات casting پس از بازیابی داده‌ها انجام می‌شود. شما نمی‌توانید روی مقدار تغییریافته کوئری بزنید. کوئری‌ای مانند User::where('username', 'john_doe')->first() رشته john_doe را مستقیماً به پایگاه داده می‌فرستد و هرگز از منطق رمزگشایی شما عبور نمی‌کند. اگر ستون در حالت ذخیره‌سازی رمزنگاری شده باشد، آن کوئری چیزی پیدا نخواهد کرد، مگر اینکه مستقیماً روی خودِ متن رمزنگاری شده (ciphertext) جستجو کنید. الگوهای دسترسی به پایگاه داده خود را بر این اساس برنامه‌ریزی کنید، زیرا castها جایگزینی برای توابع بومی پایگاه داده یا ستون‌های متنیِ ایندکس‌شده (indexed plaintext) نیستند.

کلاس‌های cast سفارشی همچنین مکان‌های بسیار خوبی برای نگهداری بخش‌های کوچکی از تنظیمات هستند. اگر به آرگومان‌های سازنده (constructor) نیاز دارید — مثلاً ارسال یک حالت رمز (cipher mode) یا یک رشته فرمت — لاراول از طریق آرایه $casts و با استفاده از عبارتی مانند 'field' => CustomEncrypt::class . ':arg' از آن‌ها پشتیبانی می‌کند، هرچند این موضوع یک مرحله فراتر از تنظیمات پایه‌ای است که در اینجا توضیح داده شد.

نتیجه‌گیری نهایی

محدودیت در مورد قرار دادن مستقیم توابع کمکی (helper functions) در $casts یک بروکراسی بی‌دلیل نیست. این کار شما را به سمت کدی سوق می‌دهد که صریح، تست‌پذیر و قابل استفاده مجدد باشد. کلاس‌های cast سفارشی، منطق‌های پراکنده در خطوط کد را به اجزای قابل اعتمادی تبدیل می‌کنند که می‌توانید بدون تکرار، آن‌ها را در مدل‌های مختلف به اشتراک بگذارید. accessorها نیز راه را برای اصلاحات سریع و محدود باز می‌گذارند، یعنی زمانی که ایجاد یک فایل جداگانه، کار اضافی و بیهوده به نظر می‌رسد. هر دو را یاد بگیرید، بر اساس محدوده مسئله انتخاب کنید، و لایه Eloquent شما تا مدت‌ها پس از بزرگ شدن اپلیکیشن، خوانا باقی خواهد ماند.