Laravel المتقدم
Eloquent المتقدم: التحويلات المخصصة وكائنات القيمة
فهم التحويلات المخصصة
تسمح لك التحويلات المخصصة في Laravel بتحويل قيم قاعدة البيانات إلى كائنات PHP غنية تلقائيًا عند استرجاع النماذج. هذا يمكّنك من العمل مع أنواع البيانات المعقدة مع الحفاظ على كود نظيف وموجه للكائنات.
لماذا التحويلات المخصصة؟ تغلف منطق تحويل البيانات، تضمن اتساق البيانات، تمكّن أمان النوع، وتجعل نماذجك تعمل مع كائنات القيمة بدلاً من الأنواع البدائية.
مراجعة التحويلات المدمجة
يوفر Laravel العديد من التحويلات المدمجة جاهزة للاستخدام:
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
protected $casts = [
// الأنواع القياسية
'age' => 'integer',
'price' => 'float',
'active' => 'boolean',
'bio' => 'string',
// التاريخ/الوقت
'email_verified_at' => 'datetime',
'published_at' => 'date',
'created_at' => 'timestamp',
// المجموعات
'options' => 'array',
'metadata' => 'json',
'tags' => 'collection',
// الكائن
'settings' => 'object',
// مشفر
'secret' => 'encrypted',
'encrypted_array' => 'encrypted:array',
'encrypted_json' => 'encrypted:json',
];
}
إنشاء صفوف تحويل مخصصة
صفوف التحويل المخصصة تنفذ واجهة CastsAttributes مع طرق ()get و ()set.
// توليد صف التحويل
php artisan make:cast MoneyValueObject
// app/Casts/MoneyValueObject.php
namespace App\Casts;
use App\ValueObjects\Money;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;
class MoneyValueObject implements CastsAttributes
{
/**
* تحويل القيمة المعطاة (من قاعدة البيانات إلى النموذج)
*
* @param array<string, mixed> $attributes
*/
public function get(Model $model, string $key, mixed $value, array $attributes): Money
{
// تحويل قيمة قاعدة البيانات إلى كائن قيمة Money
if (is_null($value)) {
return new Money(0, $attributes['currency'] ?? 'USD');
}
return new Money(
(float) $value,
$attributes['currency'] ?? 'USD'
);
}
/**
* تحضير القيمة المعطاة للتخزين (من النموذج إلى قاعدة البيانات)
*
* @param array<string, mixed> $attributes
*/
public function set(Model $model, string $key, mixed $value, array $attributes): array
{
if (!$value instanceof Money) {
throw new \InvalidArgumentException('يجب أن تكون القيمة نسخة من Money');
}
return [
$key => $value->amount(),
'currency' => $value->currency(),
];
}
}
تنفيذ كائن القيمة
// app/ValueObjects/Money.php
namespace App\ValueObjects;
class Money
{
private float $amount;
private string $currency;
public function __construct(float $amount, string $currency = 'USD')
{
if ($amount < 0) {
throw new \InvalidArgumentException('لا يمكن أن يكون المبلغ سالبًا');
}
$this->amount = $amount;
$this->currency = strtoupper($currency);
}
public function amount(): float
{
return $this->amount;
}
public function currency(): string
{
return $this->currency;
}
public function add(Money $other): self
{
$this->ensureSameCurrency($other);
return new self($this->amount + $other->amount, $this->currency);
}
public function subtract(Money $other): self
{
$this->ensureSameCurrency($other);
return new self($this->amount - $other->amount, $this->currency);
}
public function multiply(float $multiplier): self
{
return new self($this->amount * $multiplier, $this->currency);
}
public function divide(float $divisor): self
{
if ($divisor == 0) {
throw new \InvalidArgumentException('لا يمكن القسمة على صفر');
}
return new self($this->amount / $divisor, $this->currency);
}
public function format(): string
{
$symbols = [
'USD' => '$',
'EUR' => '€',
'GBP' => '£',
'JPY' => '¥',
];
$symbol = $symbols[$this->currency] ?? $this->currency;
return $symbol . number_format($this->amount, 2);
}
public function equals(Money $other): bool
{
return $this->amount === $other->amount
&& $this->currency === $other->currency;
}
private function ensureSameCurrency(Money $other): void
{
if ($this->currency !== $other->currency) {
throw new \InvalidArgumentException(
"لا يمكن العمل على عملات مختلفة: {$this->currency} و {$other->currency}"
);
}
}
public function __toString(): string
{
return $this->format();
}
}
استخدام التحويلات المخصصة في النماذج
namespace App\Models;
use App\Casts\MoneyValueObject;
use App\ValueObjects\Money;
use Illuminate\Database\Eloquent\Model;
class Product extends Model
{
protected $fillable = ['name', 'price', 'currency', 'cost', 'sale_price'];
protected $casts = [
'price' => MoneyValueObject::class,
'cost' => MoneyValueObject::class,
'sale_price' => MoneyValueObject::class,
];
// الآن تعمل مع كائنات Money بدلاً من floats
public function profit(): Money
{
return $this->price->subtract($this->cost);
}
public function profitMargin(): float
{
if ($this->price->amount() === 0.0) {
return 0.0;
}
return ($this->profit()->amount() / $this->price->amount()) * 100;
}
public function applyDiscount(float $percentage): Money
{
$discount = 1 - ($percentage / 100);
return $this->price->multiply($discount);
}
}
// الاستخدام
$product = Product::find(1);
// price تلقائيًا كائن Money
echo $product->price->format(); // $99.99
// تنفيذ العمليات
$profit = $product->profit();
echo "الربح: {$profit->format()}"; // الربح: $30.00
// إنشاء منتج جديد مع كائن Money
$newProduct = Product::create([
'name' => 'لابتوب',
'price' => new Money(1299.99, 'USD'),
'cost' => new Money(899.99, 'USD'),
]);
// يتم تحويل كائن Money تلقائيًا إلى قيم قاعدة البيانات
// price = 1299.99, currency = 'USD'
التحويل الداخلي والخارجي
يمكنك إنشاء تحويلات تعمل فقط في اتجاه واحد باستخدام CastsInboundAttributes.
namespace App\Casts;
use Illuminate\Contracts\Database\Eloquent\CastsInboundAttributes;
use Illuminate\Database\Eloquent\Model;
class UppercaseCast implements CastsInboundAttributes
{
/**
* يتعامل فقط مع تعيين القيم (من النموذج إلى قاعدة البيانات)
* لا حاجة لطريقة ()get
*/
public function set(Model $model, string $key, mixed $value, array $attributes): string
{
return strtoupper($value);
}
}
class User extends Model
{
protected $casts = [
'name' => UppercaseCast::class,
];
}
$user = new User();
$user->name = 'john doe';
$user->save(); // يُخزن كـ 'JOHN DOE'
echo $user->name; // يخرج: JOHN DOE (القيمة الخام من قاعدة البيانات)
معاملات التحويل
يمكن للتحويلات المخصصة قبول معاملات لتخصيص سلوكها.
الصيغة: استخدم نقطتين لتمرير المعاملات:
'field' => CastClass::class . ':param1,param2'
namespace App\Casts;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;
class RoundedFloat implements CastsAttributes
{
public function __construct(private int $decimals = 2) {}
public function get(Model $model, string $key, mixed $value, array $attributes): float
{
return round((float) $value, $this->decimals);
}
public function set(Model $model, string $key, mixed $value, array $attributes): float
{
return round((float) $value, $this->decimals);
}
}
class Product extends Model
{
protected $casts = [
'price' => RoundedFloat::class . ':2', // منزلتان عشريتان
'weight' => RoundedFloat::class . ':3', // 3 منازل عشرية
'tax_rate' => RoundedFloat::class . ':4', // 4 منازل عشرية
];
}
$product = new Product();
$product->price = 19.999; // يُخزن ويُسترجع كـ 20.00
$product->weight = 1.23456; // يُخزن ويُسترجع كـ 1.235
كائن قيمة معقد: البريد الإلكتروني
// app/ValueObjects/Email.php
namespace App\ValueObjects;
class Email
{
private string $address;
public function __construct(string $address)
{
if (!filter_var($address, FILTER_VALIDATE_EMAIL)) {
throw new \InvalidArgumentException("عنوان بريد إلكتروني غير صالح: {$address}");
}
$this->address = strtolower($address);
}
public function address(): string
{
return $this->address;
}
public function domain(): string
{
return substr(strrchr($this->address, "@"), 1);
}
public function username(): string
{
return strstr($this->address, '@', true);
}
public function isGmail(): bool
{
return $this->domain() === 'gmail.com';
}
public function obfuscate(): string
{
$username = $this->username();
$obfuscated = substr($username, 0, 2)
. str_repeat('*', strlen($username) - 2)
. '@' . $this->domain();
return $obfuscated;
}
public function equals(Email $other): bool
{
return $this->address === $other->address;
}
public function __toString(): string
{
return $this->address;
}
}
// app/Casts/EmailValueObject.php
namespace App\Casts;
use App\ValueObjects\Email;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;
class EmailValueObject implements CastsAttributes
{
public function get(Model $model, string $key, mixed $value, array $attributes): ?Email
{
return $value ? new Email($value) : null;
}
public function set(Model $model, string $key, mixed $value, array $attributes): ?string
{
if (is_null($value)) {
return null;
}
if (is_string($value)) {
$value = new Email($value);
}
if (!$value instanceof Email) {
throw new \InvalidArgumentException('يجب أن تكون القيمة نسخة Email أو string');
}
return $value->address();
}
}
// استخدام النموذج
class User extends Model
{
protected $casts = [
'email' => EmailValueObject::class,
];
}
$user = User::find(1);
echo $user->email->domain(); // gmail.com
echo $user->email->obfuscate(); // jo***@gmail.com
العمل مع تحويلات JSON
// app/ValueObjects/Address.php
namespace App\ValueObjects;
class Address
{
public function __construct(
public string $street,
public string $city,
public string $state,
public string $zipCode,
public string $country
) {}
public function fullAddress(): string
{
return "{$this->street}, {$this->city}, {$this->state} {$this->zipCode}, {$this->country}";
}
public function toArray(): array
{
return [
'street' => $this->street,
'city' => $this->city,
'state' => $this->state,
'zip_code' => $this->zipCode,
'country' => $this->country,
];
}
public static function fromArray(array $data): self
{
return new self(
$data['street'] ?? '',
$data['city'] ?? '',
$data['state'] ?? '',
$data['zip_code'] ?? '',
$data['country'] ?? ''
);
}
}
// app/Casts/AddressValueObject.php
namespace App\Casts;
use App\ValueObjects\Address;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;
class AddressValueObject implements CastsAttributes
{
public function get(Model $model, string $key, mixed $value, array $attributes): ?Address
{
if (is_null($value)) {
return null;
}
$data = json_decode($value, true);
return Address::fromArray($data);
}
public function set(Model $model, string $key, mixed $value, array $attributes): ?string
{
if (is_null($value)) {
return null;
}
if (is_array($value)) {
$value = Address::fromArray($value);
}
if (!$value instanceof Address) {
throw new \InvalidArgumentException('يجب أن تكون القيمة نسخة من Address');
}
return json_encode($value->toArray());
}
}
// النموذج
class Customer extends Model
{
protected $casts = [
'shipping_address' => AddressValueObject::class,
'billing_address' => AddressValueObject::class,
];
}
// الاستخدام
$customer = new Customer();
$customer->shipping_address = new Address(
'123 Main St',
'نيويورك',
'NY',
'10001',
'الولايات المتحدة'
);
echo $customer->shipping_address->fullAddress();
// 123 Main St, نيويورك, NY 10001, الولايات المتحدة
استخدام Enums في PHP 8.1+
Laravel 9+ لديه دعم مدمج لـ enums في PHP كتحويلات.
// app/Enums/OrderStatus.php
namespace App\Enums;
enum OrderStatus: string
{
case PENDING = 'pending';
case PROCESSING = 'processing';
case SHIPPED = 'shipped';
case DELIVERED = 'delivered';
case CANCELLED = 'cancelled';
public function label(): string
{
return match($this) {
self::PENDING => 'في انتظار الدفع',
self::PROCESSING => 'جاري معالجة الطلب',
self::SHIPPED => 'تم الشحن',
self::DELIVERED => 'تم التسليم',
self::CANCELLED => 'ملغى',
};
}
public function color(): string
{
return match($this) {
self::PENDING => 'yellow',
self::PROCESSING => 'blue',
self::SHIPPED => 'purple',
self::DELIVERED => 'green',
self::CANCELLED => 'red',
};
}
public function isCompleted(): bool
{
return in_array($this, [self::DELIVERED, self::CANCELLED]);
}
}
// النموذج
use App\Enums\OrderStatus;
class Order extends Model
{
protected $casts = [
'status' => OrderStatus::class,
];
}
// الاستخدام
$order = Order::find(1);
// enum آمن النوع
if ($order->status === OrderStatus::PENDING) {
// معالجة الدفع
}
echo $order->status->label(); // في انتظار الدفع
echo $order->status->color(); // yellow
// الاستعلام بـ enum
$pendingOrders = Order::where('status', OrderStatus::PENDING)->get();
// الإنشاء مع enum
$order = Order::create([
'status' => OrderStatus::PENDING,
'total' => 99.99,
]);
ملاحظة الأداء: يتم إنشاء كائنات القيمة عند كل وصول للنموذج. للسيناريوهات عالية الأداء، فكر في التخزين المؤقت أو التحميل الكسول لكائنات القيمة المعقدة.
تمرين 1: أنشئ كائن قيمة Phone مع تحويل:
- التحقق من صحة تنسيق رقم الهاتف (E.164 أو وطني)
- طرق: ()format، ()isInternational، ()countryCode
- دعم تنسيقات متعددة (أمريكي، دولي، إلخ)
- استخدامه في نموذج User لحقل رقم الهاتف
تمرين 2: ابن كائن قيمة Coordinates مع تحويل:
- تخزين خطوط الطول والعرض كـ JSON
- طرق: ()distanceTo، ()isValid، ()toGoogleMapsUrl
- التحقق من نطاقات الإحداثيات (-90 إلى 90 للعرض، -180 إلى 180 للطول)
- استخدام صيغة Haversine لحساب المسافة
تمرين 3: أنشئ كائن قيمة ImageDetails:
- خصائص: url، width، height، نص بديل، نوع mime
- طرق: ()thumbnail، ()resize، ()aspectRatio
- التخزين كـ JSON في قاعدة البيانات
- إنشاء تحويل مخصص مع التحقق
أفضل الممارسات: استخدم كائنات القيمة للبيانات المعقدة ذات السلوك (Money، Email). استخدم enums لمجموعات ثابتة من القيم (Status، Role). استخدم التحويلات البسيطة للتحويلات الأساسية (التواريخ، booleans). تحقق دائمًا من صحة البيانات في مُنشئات كائنات القيمة.