جاري التحميل
الرئيسية عني خدماتي أعمالي المدونة المجتمع اتصل بي دخول إنشاء حساب English وظّفني
العودة للمدونة
Laravel — Eloquent & Architecture

تحويل حقول الـ Model إلى كائنات ذكية باستخدام Custom Casts في Laravel

22 Apr 2026 52 مشاهدة

مقدمة

كلنا بنكتب كود زي ده كتير:

// تحويل المبلغ من قرش لجنيه في كل مكان في الكود $price = $product->price / 100; $formatted = number_format($price, 2) . ' EGP';

ده بيتكرر في عشرين مكان في التطبيق. أي تعديل في طريقة العرض — هتعدّل عشرين مكان. الـ Custom Casts بتحل المشكلة دي من جذرها بإنها تحوّل الحقل لكائن PHP كامل عنده منطقه الخاص.


01. ما هو الـ Custom Cast؟

الـ Custom Cast في Laravel هو كلاس بيخبر الـ Eloquent Model: "لما تجيب الحقل ده من الداتابيز، مش هترجعه خام — هتحوّله لكائن PHP". ولما تحفظه، الكائن ده هيتحوّل تلقائياً للقيمة المناسبة للتخزين.

الميزة دي موجودة من Laravel 8 وكتير من المطورين لحد دلوقتي مش بيستخدموها ومش عارفينها.


02. مثال عملي: Money Value Object

هنبني نظام أسعار كامل. الأسعار محفوظة في الداتابيز كـ integer (قروش) وعايزين نتعامل معاها كـ object.

خطوة 1: إنشاء الـ Value Object

// app/ValueObjects/Money.php namespace App\ValueObjects; final readonly class Money { public function __construct( public readonly int $amount, // مخزّن بالقروش في الداتابيز public readonly string $currency = 'EGP', ) {} /** القيمة بالجنيه (للعرض) */ public function value(): float { return $this->amount / 100; } /** عرض منسّق جاهز للواجهة */ public function formatted(): string { return number_format($this->value(), 2) . ' ' . $this->currency; } /** جمع مبلغين */ public function add(Money $other): static { if ($this->currency !== $other->currency) { throw new \InvalidArgumentException('لا يمكن جمع عملتين مختلفتين.'); } return new static($this->amount + $other->amount, $this->currency); } /** تطبيق خصم بالنسبة المئوية */ public function discountBy(float $percentage): static { $discounted = (int) round($this->amount * (1 - $percentage / 100)); return new static($discounted, $this->currency); } /** مقارنة مبلغين */ public function isGreaterThan(Money $other): bool { return $this->amount > $other->amount; } /** إنشاء من قيمة float مباشرة */ public static function fromFloat(float $value, string $currency = 'EGP'): static { return new static((int) round($value * 100), $currency); } }

خطوة 2: إنشاء الـ Cast Class

php artisan make:cast MoneyCast

// app/Casts/MoneyCast.php namespace App\Casts; use App\ValueObjects\Money; use Illuminate\Contracts\Database\Eloquent\CastsAttributes; use Illuminate\Database\Eloquent\Model; class MoneyCast implements CastsAttributes { public function __construct( private readonly string $currency = 'EGP' ) {} /** * من الداتابيز → كائن Money */ public function get(Model $model, string $key, mixed $value, array $attributes): Money { return new Money( amount: (int) $value, currency: $this->currency, ); } /** * كائن Money → للداتابيز (integer) */ public function set(Model $model, string $key, mixed $value, array $attributes): int { return $value instanceof Money ? $value->amount : (int) $value; } }

خطوة 3: ربطه بالـ Model

// app/Models/Product.php namespace App\Models; use App\Casts\MoneyCast; use Illuminate\Database\Eloquent\Model; class Product extends Model { protected $fillable = ['name', 'price', 'original_price']; protected function casts(): array { return [ // تمرير العملة كـ parameter للـ Cast 'price' => MoneyCast::class . ':EGP', 'original_price' => MoneyCast::class . ':EGP', ]; } }

خطوة 4: استخدامه في الكود

قبل Custom Cast — الكود المكرر:

// ❌ الطريقة القديمة — متكررة وغير آمنة $product = Product::find(1); $price = $product->price / 100; $formatted = number_format($price, 2) . ' EGP'; // في مكان تاني في الكود نفس الكلام ... $discounted = $product->price * 0.9 / 100;

بعد Custom Cast — كود نظيف وذكي:

// ✅ الطريقة الجديدة — نظيفة وآمنة $product = Product::find(1); // $product->price الآن كائن Money وليس integer! $product->price->formatted(); // "1,500.00 EGP" $product->price->value(); // 1500.00 $product->price->discountBy(10)->formatted(); // "1,350.00 EGP" // جمع مبلغين $total = $product->price->add($product->original_price); $total->formatted(); // "3,000.00 EGP" // مقارنة if ($product->price->isGreaterThan(Money::fromFloat(1000))) { // المنتج أغلى من 1000 جنيه } // الحفظ في الداتابيز يشتغل بشكل طبيعي $product->price = Money::fromFloat(2000.00); $product->save(); // يُخزّن 200000 في الداتابيز تلقائياً


03. مثال ثانٍ: Coordinate Cast

بدل ما تخزّن الـ latitude والـ longitude في عمودين منفصلين وتحسب المسافات يدوياً في كل مكان — اعمل كائن واحد بيتكلم عن الإحداثيات.

// app/ValueObjects/Coordinate.php namespace App\ValueObjects; final readonly class Coordinate { public function __construct( public readonly float $latitude, public readonly float $longitude, ) {} /** حساب المسافة بالكيلومتر (Haversine Formula) */ public function distanceTo(Coordinate $other): float { $earthRadius = 6371; $latDiff = deg2rad($other->latitude - $this->latitude); $lonDiff = deg2rad($other->longitude - $this->longitude); $a = sin($latDiff / 2) ** 2 + cos(deg2rad($this->latitude)) * cos(deg2rad($other->latitude)) * sin($lonDiff / 2) ** 2; return $earthRadius * 2 * atan2(sqrt($a), sqrt(1 - $a)); } /** تحويل لـ JSON للتخزين */ public function toJson(): string { return json_encode([ 'lat' => $this->latitude, 'lng' => $this->longitude, ]); } public static function fromJson(string $json): static { $data = json_decode($json, true); return new static($data['lat'], $data['lng']); } }

// app/Casts/CoordinateCast.php namespace App\Casts; use App\ValueObjects\Coordinate; use Illuminate\Contracts\Database\Eloquent\CastsAttributes; use Illuminate\Database\Eloquent\Model; class CoordinateCast implements CastsAttributes { public function get(Model $model, string $key, mixed $value, array $attributes): Coordinate { return Coordinate::fromJson($value); } public function set(Model $model, string $key, mixed $value, array $attributes): string { return $value instanceof Coordinate ? $value->toJson() : $value; } }

// app/Models/Branch.php class Branch extends Model { protected function casts(): array { return [ 'location' => CoordinateCast::class, ]; } } // الاستخدام: $branch = Branch::find(1); $customer = new Coordinate(30.0444, 31.2357); // إحداثيات القاهرة $distance = $branch->location->distanceTo($customer); // $distance = 2.4 كيلومتر — بدون أي كود خارج الكائن


04. Inbound Cast — للتحقق فقط عند الحفظ

أحياناً مش محتاج تحوّل القيمة عند القراءة، بس عايز تتحقق منها وتعالجها عند الحفظ فقط. في الحالة دي تستخدم Inbound Cast.

// app/Casts/Uppercase.php namespace App\Casts; use Illuminate\Contracts\Database\Eloquent\CastsInboundAttributes; use Illuminate\Database\Eloquent\Model; class Uppercase implements CastsInboundAttributes { /** يُشغَّل عند الحفظ فقط — يحوّل النص لـ uppercase */ public function set(Model $model, string $key, mixed $value, array $attributes): string { return strtoupper($value); } } // الاستخدام في الـ Model: protected function casts(): array { return [ 'country_code' => Uppercase::class, ]; } // نتيجة: $user->country_code = 'eg'; $user->save(); // يُخزَّن 'EG' في الداتابيز تلقائياً


05. اختبار الـ Custom Casts

لأن كل Cast هو كلاس مستقل، الاختبار بيبقى سهل وسريع جداً:

namespace Tests\Unit\ValueObjects; use App\ValueObjects\Money; use PHPUnit\Framework\TestCase; class MoneyTest extends TestCase { public function test_formatted_output_is_correct(): void { $money = new Money(150000, 'EGP'); $this->assertEquals('1,500.00 EGP', $money->formatted()); } public function test_discount_is_applied_correctly(): void { $money = new Money(100000); // 1000 جنيه $discounted = $money->discountBy(10); $this->assertEquals(90000, $discounted->amount); $this->assertEquals('900.00 EGP', $discounted->formatted()); } public function test_adding_different_currencies_throws(): void { $this->expectException(\InvalidArgumentException::class); (new Money(1000, 'EGP'))->add(new Money(1000, 'USD')); } public function test_from_float_converts_correctly(): void { $money = Money::fromFloat(99.99); $this->assertEquals(9999, $money->amount); } }


06. هيكل المجلدات الموصى به

app/ ├── Casts/ │ ├── MoneyCast.php │ ├── CoordinateCast.php │ └── Uppercase.php ├── ValueObjects/ │ ├── Money.php │ └── Coordinate.php └── Models/ ├── Product.php ← uses MoneyCast └── Branch.php ← uses CoordinateCast


الخلاصة

الـ Custom Casts مع Value Objects هما من أقوى أدوات تصميم الكود في Laravel. بيخليك تتخلص نهائياً من تكرار منطق التحويل في كل مكان، وكل حقل في الـ Model بيبقى عنده "دماغه" الخاص — يعرف يعرض نفسه، يعرف يتعامل مع نفسه، ويعرف يتحقق من نفسه.

تفاعل مع المقال:

التعليقات

لا توجد تعليقات بعد، كن الأول!

يجب تسجيل الدخول للتعليق

انضم للمجتمع وشارك رأيك في المقالات

مشاركة المقال

WhatsApp Telegram Twitter
كل المقالات
🤖

المساعد الذكي

اسألني عن محمود نصر

مرحباً! أنا مساعد ذكي لمحمود نصر. يمكنني الإجابة عن مهاراته وخبراته وأعماله. 👋
🗑️

تأكيد الحذف