پرش به مطلب اصلی

محافظت فیلد کش

مقدمه

سیستم محافظت فیلد کش ساختاری تمیز و جدا برای محافظت از فیلد cache در مدل‌های پایگاه داده فراهم می‌کند. این سیستم تضمین می‌کند که:

  1. هیچ مدلی نمی‌تواند مستقیماً فیلد cache را به‌روزرسانی کند
  2. دسترسی‌کننده‌های خودکار برای تمام کلیدهای کش ایجاد می‌شوند
  3. از پرس‌وجوی مستقیم روی فیلد cache جلوگیری می‌شود

این لایه محافظت برای حفظ یکپارچگی داده‌ها و جلوگیری از تغییرات تصادفی یا غیرمجاز داده‌های کش شده ضروری است.

اجزای معماری

Trait HasCacheField

Trait HasCacheField تمام عملکردهای مورد نیاز برای محافظت و مدیریت فیلد کش را فراهم می‌کند:

trait HasCacheField
{
// متدهای محافظت فیلد کش از تغییرات مستقیم
// تولید خودکار دسترسی‌کننده
// جلوگیری از پرس‌وجوهای مستقیم
// متدهای کمکی برای مدیریت کش
// آمار و گزارش‌گیری کش
// دسترسی ایمن به داده‌های کش
}

ویژگی‌های کلیدی:

  • از setAttribute مستقیم روی فیلد کش جلوگیری می‌کند
  • با استفاده از متد جادویی __call دسترسی‌کننده‌های خودکار ایجاد می‌کند
  • با scope سراسری از پرس‌وجوهای مستقیم محافظت می‌کند
  • فیلد کش را از toArray/toJson مخفی می‌کند
  • متدهای آمار و گزارش‌گیری فراهم می‌کند
  • به طور کامل با سیستم کش یکپارچه می‌شود

CacheFieldAccessException

استثنای تخصصی برای نقض دسترسی فیلد کش.

مکانیزم دور زدن سیستم کش

سیستم کش شامل مکانیزمی برای دور زدن به‌روزرسانی‌های مشروع است. این مکانیزم فقط به خود سیستم کش اجازه به‌روزرسانی فیلد کش را می‌دهد:

// استفاده داخلی در CacheStorageService
$entity->withBypassedCacheProtection(function () use ($entity, $cacheData) {
$entity->update(['cache' => $cacheData]);
});

ویژگی‌های کلیدی:

  • برای استفاده داخلی توسط سیستم کش طراحی شده
  • Thread-safe و Exception-safe
  • حالت دور زدن به طور خودکار بازیابی می‌شود
  • از سوء استفاده جلوگیری می‌کند

دسترسی‌کننده‌های خودکار

سیستم به طور خودکار برای هر کلید کش تعریف شده یک دسترسی‌کننده ایجاد می‌کند:

// اگر پیکربندی کش شامل کلید 'profile_data' باشد
$user->profile_data; // به طور خودکار به getCacheValue('profile_data') ترجمه می‌شود

// اگر پیکربندی کش شامل کلید 'user_statistics' باشد
$user->user_statistics; // به طور خودکار به getCacheValue('user_statistics') ترجمه می‌شود

نحوه کار:

  1. هنگام فراخوانی $model->some_key
  2. سیستم بررسی می‌کند که آیا some_key در پیکربندی کش تعریف شده است
  3. اگر بله، مقدار از cache['some_key']['data'] برگردانده می‌شود
  4. اگر خیر، با جریان عادی Laravel ادامه می‌دهد

ساختار داده کش

داده‌های کش با ساختار زیر در پایگاه داده ذخیره می‌شوند:

{
"profile_data": {
"data": {
"name": "احمد احمدی",
"avatar_url": "https://example.com/avatar.jpg",
"completion_percentage": 85
},
"updated_at": "2023-12-01T10:30:00Z"
},
"statistics": {
"data": {
"total_posts": 42,
"total_likes": 156,
"engagement_rate": 0.75
},
"updated_at": "2023-12-01T11:15:00Z"
}
}

متدهای کمکی

آمار کش

$user = User::find(1);
$stats = $user->getCacheStatistics();

// نتیجه:
[
'total_keys' => 2,
'cached_keys' => 1,
'empty_keys' => 1,
'keys_with_data' => ['profile_data'],
'keys_without_data' => ['statistics'],
'last_updated' => '2023-12-01T10:30:00Z'
]

خلاصه پیکربندی کش

$user = User::find(1);
$summary = $user->getCacheConfigSummary();

// نتیجه:
[
[
'key' => 'profile_data',
'events' => ['App\\Events\\UserUpdatedEvent'],
'module' => 'User',
'mode' => 'sync',
'has_data' => true,
'updated_at' => '2023-12-01T10:30:00Z'
],
[
'key' => 'statistics',
'events' => ['App\\Events\\UserActivityEvent'],
'module' => 'User',
'mode' => 'async',
'has_data' => false,
'updated_at' => null
]
]

دسترسی به چندین کلید کش

$user = User::find(1);

// دریافت داده برای چندین کلید
$data = $user->getCacheDataForKeys(['profile_data', 'statistics']);

// دریافت تمام داده‌های کش
$allData = $user->getAllCacheData();

سریال‌سازی JSON

هنگام تبدیل مدل به آرایه یا JSON:

$user = User::find(1);
$array = $user->toArray();

// نتیجه شامل:
// - تمام ویژگی‌های عادی مدل
// - ویژگی‌های مشتق شده از کش (profile_data, statistics و غیره)
// - بدون فیلد مستقیم 'cache'

عملیات ایمن در مقابل ناایمن

✅ عملیات ایمن

$user = User::find(1);

// دسترسی به داده‌های کش از طریق دسترسی‌کننده‌های خودکار
$profileData = $user->profile_data;
$statistics = $user->statistics;

// بررسی وجود داده کش
$hasProfile = $user->hasCacheData('profile_data');
$hasStats = $user->hasCacheData('statistics');

// دسترسی با تبدیل نوع برای ایمنی نوع بهتر
$viewCount = (int) $user->view_count; // اطمینان از نوع صحیح
$isActive = (bool) $user->is_active; // اطمینان از نوع بولی
$createdDate = $user->created_date instanceof Carbon
? $user->created_date
: Carbon::now(); // مدیریت مقادیر احتمالی null

// دریافت فراداده کش
$profileUpdatedAt = $user->getCacheUpdatedAt('profile_data');
$statsUpdatedAt = $user->getCacheUpdatedAt('statistics');

// دریافت آمار کش
$cacheStats = $user->getCacheStatistics();
echo "کامل بودن کش: {$cacheStats['cached_keys']}/{$cacheStats['total_keys']}";

// دریافت تمام داده‌های کش
$allCacheData = $user->getAllCacheData();

// بررسی کامل بودن
$isComplete = $user->hasCompleteCacheData();

// عملیات عادی مدل
$user->name = 'نام به‌روزرسانی شده';
$user->save(); // ✅ به خوبی کار می‌کند

// انتساب دسته‌ای (فیلدهای غیرکش)
$user->fill(['name' => 'نام جدید', 'email' => 'new@email.com']);

// پرس‌وجوهای عادی
$activeUsers = User::where('status', 'active')->get();

❌ عملیات ناایمن (استثنا پرتاب می‌کنند)

$user = User::find(1);

// دسترسی مستقیم فیلد کش
$cacheData = $user->cache; // ❌ CacheFieldAccessException

// انتساب مستقیم فیلد کش
$user->cache = ['data' => 'value']; // ❌ CacheFieldAccessException

// انتساب دسته‌ای به فیلد کش
$user->fill(['cache' => ['data' => 'value']]); // ❌ CacheFieldAccessException

// به‌روزرسانی مستقیم فیلد کش
$user->update(['cache' => ['data' => 'value']]); // ❌ CacheFieldAccessException

// پرس‌وجو روی فیلد کش
User::where('cache->profile_data', '!=', null)->get(); // ❌ CacheFieldAccessException

// استفاده از scope whereCache
User::whereCache('profile_data', 'value')->get(); // ❌ CacheFieldAccessException

مدیریت خطا

CacheFieldAccessException

این استثنا در موارد زیر پرتاب می‌شود:

  1. به‌روزرسانی مستقیم: تلاش برای به‌روزرسانی مستقیم فیلد کش
  2. پرس‌وجوی مستقیم: تلاش برای پرس‌وجوی مستقیم روی فیلد کش
  3. انتساب دسته‌ای: تلاش برای انتساب دسته‌ای به فیلد کش
try {
$user->cache = ['data' => 'value'];
} catch (CacheFieldAccessException $e) {
// مدیریت خطا
logger()->warning('نقض دسترسی فیلد کش', [
'message' => $e->getMessage(),
'user_id' => $user->id
]);
}

ملاحظات عملکرد

استفاده از حافظه

  • فیلد کش از toArray() حذف می‌شود تا اندازه JSON کاهش یابد
  • ویژگی‌های مشتق شده از کش فقط زمانی نمایش داده می‌شوند که داده وجود داشته باشد

پرس‌وجوهای پایگاه داده

  • هیچ پرس‌وجوی اضافی برای دسترسی به داده‌های کش انجام نمی‌شود
  • تمام عملیات روی داده‌های بارگذاری شده انجام می‌شود

استراتژی کش‌گذاری

  • پیکربندی‌های کش در حافظه نگهداری می‌شوند
  • هیچ سربار اضافی برای دسترسی‌کننده‌ها وجود ندارد

بهترین شیوه‌ها

1. نام‌گذاری کلید کش

// ✅ خوب: توصیفی و سازگار
'user_profile_data'
'post_statistics'
'product_recommendations'

// ❌ بد: مبهم یا ناسازگار
'data'
'info'
'stuff'

2. پیکربندی کش

// ✅ خوب: رویدادهای مشخص و هدف واضح
new CacheConfigDTO(
key: 'user_engagement_metrics',
relatedEvents: [
UserPostCreatedEvent::class,
UserCommentCreatedEvent::class,
UserLikeGivenEvent::class,
],
sourceModule: 'User',
sourceEntity: User::class,
mode: CacheModeEnum::ASYNC
)

3. مدیریت خطا

// ✅ خوب: مدیریت تدریجی
public function getUserProfile(int $userId): ?array
{
$user = User::find($userId);

if (!$user) {
return null;
}

// دسترسی ایمن کش
$profileData = $user->profile_data;

if (!$profileData) {
// در صورت نیاز تازه‌سازی کش را راه‌اندازی کن
event(new UserProfileRequested($user));
return null;
}

return $profileData;
}

عیب‌یابی

مسائل رایج

1. "دسترسی فیلد کش مجاز نیست"

علت: تلاش برای دسترسی مستقیم به فیلد کش راه‌حل: از دسترسی‌کننده‌های خودکار استفاده کنید

2. "پرس‌وجو روی فیلد کش مجاز نیست"

علت: تلاش برای پرس‌وجوی مستقیم روی فیلد کش راه‌حل: از متدهای مدل برای بررسی کش استفاده کنید

3. "دسترسی‌کننده کار نمی‌کند"

علت: کلید کش در پیکربندی تعریف نشده راه‌حل: کلید را به getCacheConfig() اضافه کنید

4. داده کش در toArray/toJson نمایش داده نمی‌شود

علت: این عمدی است تا از افشای داده‌های داخلی جلوگیری شود راه‌حل: از دسترسی‌کننده‌ها برای داده‌های API استفاده کنید:

// در مدل
protected $appends = ['view_count', 'has_video'];

// دسترسی‌کننده‌ها به طور خودکار توسط HasCacheField ایجاد می‌شوند

5. خاصیت تعریف نشده هنگام دسترسی به کلید کش

علت: trait گم شده یا نام کلید نادرست راه‌حل:

  • تأیید کنید که trait HasCacheField در مدل استفاده شده
  • بررسی کنید که کلید دقیقاً با تعریف در getCacheConfig مطابقت دارد (حساس به حروف)
  • بررسی کنید که داده کش برای این کلید وجود دارد
  • از hasCacheData برای بررسی وجود داده استفاده کنید:
if ($post->hasCacheData('view_count')) {
$count = $post->view_count;
} else {
// کش هنوز ایجاد نشده
}

دستورات دیباگ

// بررسی پیکربندی کش
$user = User::find(1);
dd($user->getCacheConfigSummary());

// بررسی آمار کش
dd($user->getCacheStatistics());

// دیباگ ساختار کش
Logger::debug('ساختار کش', [
'raw_cache' => $model->getRawAttribute('cache'),
'config_keys' => $model->getCacheConfigKeys(),
'has_trait' => method_exists($model, 'hasCacheData')
]);