המאפיין $casts של Laravel הוא אחד מאותן נוחות שקטות שמעצבות את האופן שבו אתם מטפלים בנתונים מבלי למשוך תשומת לב רבה. הכניסו טיפוס מובנה כמו boolean או datetime למערך, ו-Eloquent ימיר אוטומטית מחרוזות גולמיות מהדאטה-בייס למשהו ידידותי יותר עוד לפני שהערך מגיע לבקרים (controllers) או לתצוגות (views) שלכם. זה שומר על הקוד שלכם מסודר. אך ברגע שתנסו לקרוא לפונקציית עזר (helper) משלכם ישירות בתוך המערך הזה, הפריימוורק יתנגד. כתיבת משהו כמו 'username' => 'encrypt_data()' לא תעבוד. Laravel מצפה למילת מפתח native cast או למחלקה שמממשת את ממשק CastsAttributes. הדרישה הזו קיימת מכיוון ששכבת ה-casting פועלת עמוק בתוך מחזור ה-hydration וה-serialization של Eloquent; היא זקוקה לאובייקט צפוי עם חוזה ברור, ולא למחרוזת פונקציה שרירותית שייתכן שלא קיימת בזמן ריצה.

למה פונקציות עזר נכשלות במערך ה-$casts

כאשר Eloquent טוען מודל, הוא סורק את מערך ה-$casts כדי להחליט כיצד להפוך כל עמודה. הפריימוורק מזהה טיפוסים פרימיטיביים ספציפיים—string, integer, array, encrypted וכו'—ומזהה שמות מחלקות מלאים (fully-qualified class names) שמממשים את CastsAttributes. הוא אינו מעריך את המחרוזת כקוד. לכן 'encrypt_data()' נחשב לסוג cast לא ידוע, מה שגורם לשגיאה. גם אם Laravel היה מנתח אותה, לא הייתה דרך אמינה להעביר את מופע המודל (model instance), את מפתח המאפיין (attribute key), את הערך הנוכחי ואת המאפיינים הסובבים לתוך הפונקציה שלכם בסדר הנכון. הממשק קיים בדיוק כדי לתקנן את ה"לחיצת יד" (handshake) הזו בין הלוגיקה המותאמת אישית שלכם לבין הפנימיים של Eloquent.

יש לכם שתי דרכים מוצקות לגשר על הפער הזה. אחת מעדיפה שימוש חוזר לטווח ארוך. השנייה מעדיפה מהירות כשאתם זקוקים רק לתיקון מהיר בתוך מודל בודד.

שיטה 1: כתיבת מחלקת Cast מותאמת אישית

אם אותה טרנספורמציה חלה על מספר שדות או מתפרסת על פני מספר מודלים, מחלקת cast ייעודית היא הבחירה הנקייה יותר. היא חיה בקובץ משלה, ניתנת לבדיקות יחידה (unit-tested) בבידוד, ושומרת על המודלים שלכם נקיים מקוד boilerplate חזרתי.

התחילו ביצירת app/Casts/CustomEncrypt.php. ה-namespace צריך להתאים להגדרת ה-autoloading שלכם, בדרך כלל App\Casts. המחלקה חייבת לממש את Illuminate\Contracts\Database\Eloquent\CastsAttributes, מה שמחייב אתכם להגדיר שתי מתודות: get ו-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);
    }
}

לחתימות (signatures) יש חשיבות. Eloquent מעביר ארבעה ארגומנטים לכל מתודה. $model הוא המופע שממלאים או שומרים, מה שמאפשר לכם לבדוק שדות אחרים אם הלוגיקה שלכם תלויה בהם. $key הוא שם העמודה שעוברת כעת casting. $value הוא המחרוזת הגולמית או null שמגיעים מהדאטה-בייס ב-get, או הערך שסופק על ידי המשתמש ב-set. $attributes הוא המערך הגולמי המלא של העמודות עבור השורה הזו. אינכם חייבים להשתמש בכל הארבעה בכל פעם, אך הממשק מחייב אותם.

במתודת ה-get לעיל, decrypt_data($value) רצה לאחר ש-Eloquent שולף את השורה מהדאטה-בייס ולפני שהערך מגיע למודל שלכם. במתודת ה-set, encrypt_data($value) רצה לפני ה-INSERT או ה-UPDATE, מה שמבטיח שהדאטה-בייס לעולם לא יראה טקסט גלוי (plaintext).

כדי לחבר את זה, התייחסו למחלקה בתוך מערך ה-$casts של המודל שלכם:

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

מכיוון שאתם משתמשים בקבוע של המחלקה (class constant), ה-autoloader מטפל בשאר. אם האפליקציה שלכם תצטרך מאוחר יותר להחליף את שיטת ההצפנה, תעריכו קובץ אחד, וכל שדה שמקושר אליו ישנה את התנהגותו באופן מיידי. הריכוזיות הזו קשה להכריע כשיש לכם נתונים רגישים בטבלאות רבות.

שיטה 2: שימוש ב-Accessor ו-Mutator

לפעמים אתם לא רוצים קובץ חדש עבור טרנספורמציה שחשובה רק במקום אחד. מחלקת ה-Attribute של Laravel מאפשרת לכם להגדיר לוגיקת get ו-set ישירות על המודל באמצעות תחביר closure של PHP 8+.

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),
    );
}

כאן שם המתודה חייב להתאים לעמודה או למאפיין שאתם מכוונים אליהם. כאשר Eloquent קורא את username מהדאטה-בייס, הוא מעביר את הערך הגולמי דרך ה-closure של ה-get. כאשר אתם מקצים ערך חדש למאפיין username של המודל, ה-closure של ה-set מצפין אותו לפני ש-Eloquent בונה את השאילתה.

הגישה הזו מצטיינת באבות-טיפוס (prototypes) או במודלים ישנים (legacy) שבהם ספרייה מלאה של מחלקות cast מרגישה כמו מוגזם. החיסרון הוא חזרתיות. אם תחליטו מאוחר יותר ש-email, phone ו-backup_code זקוקים לאותה טיפול, תמצאו את עצמכם מעתיקים את ה-closures הללו בין מתודות או מודלים שונים. הרעש הזה מצטבר. זה גם מונע מכם להשתמש מחדש בלוגיקה בבדיקת יחידה מבלי להריץ את המודל כולו.

בחירה בין השניים

השתמש במחלקת cast מותאמת אישית (custom cast class) כאשר חשוב לך שימוש חוזר, בדיקות ושמירה על מודלים רזים. זה מסמן למפתחים אחרים שהטרנספורמציה הזו היא מושג ברמה גבוהה (first-class concept) באפליקציה שלך, ולא "hack" חד-פעמי.

השתמש ב-accessor כאשר הלוגיקה היא באמת מקומית, ניסיונית, או כזו שסביר להניח שלא תישאר מעבר לספרינט הנוכחי. זה מאפשר לך לנוע מהר מבלי לפזר קבצים קטנים ברחבי בסיס הקוד. רק היה מוכן לבצע refactor למחלקת cast ברגע שהתבנית אותה תופיע בפעם השנייה או השלישית.

פרטים מעשיים שכדאי לזכור

Custom casts הם עוצמתיים, אך הם מציגים התנהגות שעלולה להפתיע אותך אם אינך שם לב לכך. ראשית, זכור ש-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);
}

שנית, casts רצים במהלך serialization של מערכים ו-JSON. כאשר אתה מחזיר מודל כ-API resource או קורא ל-toArray(), לוגיקת ה-get של ה-cast עדיין חלה. זה בדרך כלל מה שאתה רוצה, אך כדאי לזכור זאת אם אתה חושף ערכים שהוצפנו (decrypted) וזקוק להוספת שכבות נוספות של בקרת נראות מעליהם.

שלישית, וחשוב מכל, ה-casting מתבצע אחרי השליפה. אינך יכול לבצע שאילתה מול הערך המומר. שאילתה כמו User::where('username', 'john_doe')->first() שולחת את המחרוזת john_doe ישירות למסד הנתונים. היא מעולם לא עוברת דרך לוגיקת ה-decrypt שלך. אם העמודה מוצפנת במנוחה (encrypted at rest), השאילתה הזו לא תמצא דבר אלא אם תחפש מול ה-ciphertext המוצפן עצמו. תכנן את דפוסי הגישה למסד הנתונים שלך בהתאם, כיוון ש-casts אינם תחליף לפונקציות מסד נתונים טבעיות או לעמודות plaintext עם אינדקס.

מחלקות custom cast הן גם בית מצוין להגדרות (configuration) קטנות. אם אתה זקוק לארגומנטים לבנאי (constructor arguments) — למשל העברת מצב הצפנה (cipher mode) או מחרוזת פורמט — Laravel תומכת בהם דרך מערך ה-$casts באמצעות ביטוי כמו 'field' => CustomEncrypt::class . ':arg', אם כי זהו שלב מתקדם יותר מההגדרה הבסיסית המתוארת כאן.

השורה התחתונה

המגבלה נגד הוספת פונקציות עזר ישירות לתוך $casts אינה בירוקרטיה שרירותית. היא מדרבנת אותך לכתוב קוד מפורש (explicit), ניתן לבדיקה (testable) וניתן לשימוש חוזר (reusable). מחלקות custom cast הופכות לוגיקה מפוזרת בתוך הקוד לרכיבים אמינים שניתן לשתף בין מודלים שונים ללא כפילות. Accessors משאירים פתח לתיקונים מהירים ומקומיים כאשר יצירת קובץ נפרד מרגישה כמו "טקס" מיותר (ceremony). שלט בשניהם, בחר בהתאם להיקף הבעיה, ושכבת ה-Eloquent שלך תישאר קריאה גם הרבה אחרי שהאפליקציה תגדל.