إطار Laravel

Laravel Pennant والأعلام المميزة

15 دقيقة الدرس 39 من 45

Laravel Pennant والأعلام المميزة

يوفر Laravel Pennant نظام أعلام مميزة بسيط وخفيف الوزن يسمح لك بطرح ميزات جديدة تدريجياً، وإجراء اختبارات A/B، وإدارة الوصول إلى الميزات لشرائح مختلفة من المستخدمين. تمكنك الأعلام المميزة من نشر الكود دون كشفه فوراً لجميع المستخدمين، مما يقلل من المخاطر ويمكن من التطوير القائم على البيانات.

فهم الأعلام المميزة

تسمح الأعلام المميزة (تسمى أيضاً مفاتيح الميزات) بتمكين أو تعطيل الميزات في وقت التشغيل دون نشر كود جديد:

حالات استخدام الأعلام المميزة:
  • الإطلاق التدريجي: إطلاق الميزات لنسبة صغيرة من المستخدمين أولاً
  • اختبار A/B: اختبار إصدارات مختلفة من الميزات مع مجموعات مستخدمين مختلفة
  • ميزات بيتا: السماح للمستخدمين المميزين أو مستخدمي بيتا بالوصول إلى الميزات التجريبية
  • مفتاح إيقاف طوارئ: تعطيل الميزات الإشكالية بسرعة دون إعادة النشر
  • إصدارات الكناري: اختبار الميزات في الإنتاج مع المستخدمين الداخليين قبل الإطلاق العام

تثبيت Pennant

قم بتثبيت Laravel Pennant وإعداد تخزين قاعدة البيانات:

// تثبيت Pennant composer require laravel/pennant // نشر التكوين php artisan vendor:publish --provider="Laravel\Pennant\PennantServiceProvider" // إنشاء جدول الميزات php artisan pennant:table php artisan migrate // التكوين (config/pennant.php) return [ 'default' => env('PENNANT_STORE', 'database'), 'stores' => [ 'database' => [ 'driver' => 'database', 'connection' => null, ], 'array' => [ 'driver' => 'array', ], ], 'middleware' => [ 'web' => [ // البرامج الوسيطة للتشغيل لطلبات الويب ], ], ];

تعريف الميزات

عرّف الميزات في AppServiceProvider الخاص بك أو في FeatureServiceProvider مخصص:

// app/Providers/AppServiceProvider.php use Laravel\Pennant\Feature; use Illuminate\Support\Lottery; public function boot(): void { // ميزة منطقية بسيطة Feature::define('dark-mode', fn () => true); // ميزة مبنية على المستخدم Feature::define('new-dashboard', fn (User $user) => $user->isAdmin()); // إطلاق قائم على النسبة المئوية (50% من المستخدمين) Feature::define('beta-search', fn (User $user) => Lottery::odds(1, 2)); // الإطلاق التدريجي بواسطة معرف المستخدم (متسق لنفس المستخدم) Feature::define('new-ui', function (User $user) { // إطلاق 10% بناءً على معرف المستخدم modulo return $user->id % 10 === 0; }); // ميزة المستخدم المميز Feature::define('advanced-analytics', function (User $user) { return $user->subscription?->isPremium() ?? false; }); // ميزة قائمة على الوقت Feature::define('holiday-theme', function () { return now()->between( now()->parse('December 1'), now()->parse('January 1') ); }); // ميزة متعددة الشروط Feature::define('ai-assistant', function (User $user) { // فقط للمستخدمين المحققين والمميزين في دول محددة return $user->hasVerifiedEmail() && $user->isPremium() && in_array($user->country, ['US', 'CA', 'GB']); }); // ميزة مع قيمة افتراضية Feature::define('chat-support', fn (?User $user) => $user?->isActive() ?? false); }

فحص الميزات

تحقق مما إذا كانت الميزة نشطة في أجزاء مختلفة من تطبيقك:

// في المتحكمات use Laravel\Pennant\Feature; public function index() { // التحقق للمستخدم الحالي if (Feature::active('new-dashboard')) { return view('dashboard.new'); } return view('dashboard.classic'); } // التحقق لمستخدم محدد $user = User::find(1); if (Feature::for($user)->active('beta-search')) { // عرض بحث بيتا } // التحقق من غير نشط if (Feature::inactive('new-dashboard')) { // الميزة متوقفة } // الحصول على جميع الميزات النشطة للمستخدم $features = Feature::all(); // ['new-dashboard' => true, 'beta-search' => false, ...] // التحقق من ميزات متعددة $results = Feature::values(['new-dashboard', 'beta-search', 'dark-mode']); // When callback - التنفيذ فقط إذا كانت نشطة Feature::when('new-dashboard', fn () => $this->showNewDashboard(), fn () => $this->showClassicDashboard() ); // في المسارات Route::get('/dashboard', [DashboardController::class, 'index']) ->middleware('feature:new-dashboard'); // في قوالب Blade @feature('new-dashboard') <div class="new-dashboard"> <!-- واجهة لوحة التحكم الجديدة --> </div> @else <div class="classic-dashboard"> <!-- واجهة لوحة التحكم الكلاسيكية --> </div> @endfeature // التحقق للمستخدمين الضيوف @feature('public-api') <a href="/api/docs">توثيق API</a> @endfeature
الأداء: يخزن Pennant قيم الميزات مؤقتاً خلال دورة حياة الطلب. نفس فحص الميزة على نفس النطاق سينفذ الإغلاق مرة واحدة فقط، مما يجعل فحوصات الميزات فعالة للغاية.

الميزات المبنية على الفئات

لمنطق الميزات المعقد، أنشئ فئات ميزات مخصصة:

// إنشاء فئة الميزة php artisan pennant:feature NewDashboard // app/Features/NewDashboard.php namespace App\Features; use Illuminate\Support\Lottery; class NewDashboard { /** * حل القيمة الأولية للميزة. */ public function resolve(User $user): mixed { // منطق ميزة معقد if ($user->isAdmin()) { return true; } if ($user->isBetaTester()) { return true; } // إطلاق 10% للمستخدمين العاديين return Lottery::odds(1, 10)->choose(); } /** * تسلسل قيمة الميزة للتخزين. */ public function serialize(mixed $value): mixed { return $value; } } // تسجيل الميزة المبنية على الفئة // app/Providers/AppServiceProvider.php use App\Features\NewDashboard; use Laravel\Pennant\Feature; public function boot(): void { Feature::define(NewDashboard::class); } // التحقق من الميزة use App\Features\NewDashboard; if (Feature::active(NewDashboard::class)) { // لوحة التحكم الجديدة نشطة } // في Blade @feature(App\Features\NewDashboard::class) <!-- لوحة التحكم الجديدة --> @endfeature

نطاق الميزات

يمكن تحديد نطاق الميزات لكيانات مختلفة، وليس فقط المستخدمين:

// تعريف ميزة محددة النطاق للفريق Feature::define('team-chat', fn (Team $team) => $team->plan === 'premium'); // التحقق لفريق محدد $team = Team::find(1); if (Feature::for($team)->active('team-chat')) { // الفريق لديه ميزة الدردشة } // نطاقات متعددة (مستخدم + فريق) Feature::define('advanced-reports', function (User $user, Team $team) { return $team->isPremium() && $user->hasRole('admin'); }); // التحقق مع نطاقات متعددة if (Feature::for([$user, $team])->active('advanced-reports')) { // كل من المستخدم والفريق يستوفيان الشروط } // نطاق مجهول (لا يتطلب مصادقة) Feature::define('maintenance-mode', fn () => Cache::get('maintenance', false)); if (Feature::active('maintenance-mode')) { // الموقع في وضع الصيانة } // نطاق لأي نموذج Feature::define('premium-content', fn (Subscription $subscription) => $subscription->isActive() && $subscription->tier === 'premium' );

تنشيط الميزات يدوياً

قم بتنشيط أو تعطيل الميزات برمجياً لنطاقات محددة:

use Laravel\Pennant\Feature; // التنشيط للمستخدم الحالي Feature::activate('new-dashboard'); // التعطيل للمستخدم الحالي Feature::deactivate('new-dashboard'); // التنشيط لمستخدم محدد $user = User::find(1); Feature::for($user)->activate('beta-search'); // التنشيط لمستخدمين متعددين $users = User::where('is_beta_tester', true)->get(); Feature::for($users)->activate('beta-search'); // تنشيط ميزات متعددة Feature::activateForEveryone(['dark-mode', 'new-ui']); // التعطيل للجميع Feature::deactivateForEveryone('broken-feature'); // نسيان قيمة الميزة (سيعيد التقييم في الفحص التالي) Feature::forget('new-dashboard'); // تطهير جميع القيم المخزنة لميزة Feature::purge('deprecated-feature'); // في لوحة المسؤول public function toggleFeature(Request $request) { $validated = $request->validate([ 'feature' => 'required|string', 'user_id' => 'required|exists:users,id', 'active' => 'required|boolean', ]); $user = User::find($validated['user_id']); if ($validated['active']) { Feature::for($user)->activate($validated['feature']); } else { Feature::for($user)->deactivate($validated['feature']); } return response()->json(['message' => 'تم تحديث الميزة']); }
التنشيط اليدوي: التنشيط/التعطيل اليدوي للميزات يتجاوز منطق الحل للميزة. يتم تخزين هذه القيم في قاعدة البيانات وستستمر حتى يتم تغييرها أو تطهيرها صراحة.

اختبار A/B مع Pennant

استخدم الأعلام المميزة لتطبيق اختبار A/B:

// تعريف متغيرات اختبار A/B Feature::define('checkout-flow', function (User $user) { // تعيين متسق بناءً على معرف المستخدم $variants = ['variant-a', 'variant-b', 'control']; return $variants[$user->id % 3]; }); // في المتحكم public function checkout() { $variant = Feature::value('checkout-flow'); return match($variant) { 'variant-a' => view('checkout.variant-a'), 'variant-b' => view('checkout.variant-b'), default => view('checkout.control'), }; } // تتبع التحويل public function complete(Request $request) { $variant = Feature::value('checkout-flow'); // تسجيل التحويل مع المتغير Analytics::track('checkout_complete', [ 'variant' => $variant, 'user_id' => auth()->id(), 'revenue' => $request->total, ]); // إكمال الطلب... } // اختبار متعدد المتغيرات (أكثر من متغيرين) Feature::define('pricing-page', function (User $user) { return [ 'layout' => Lottery::odds(1, 3)->winner(fn () => 'grid') ->loser(fn () => 'list') ->choose(), 'button_color' => collect(['green', 'blue', 'red']) ->random(), ]; }); // في العرض @php $config = Feature::value('pricing-page'); @endphp <div class="pricing-{{ $config['layout'] }}"> <button class="btn-{{ $config['button_color'] }}"> اشترك الآن </button> </div>

لوحة تحكم الأعلام المميزة

بناء لوحة تحكم مسؤول لإدارة الميزات:

// app/Http/Controllers/Admin/FeatureFlagController.php namespace App\Http\Controllers\Admin; use Illuminate\Http\Request; use Laravel\Pennant\Feature; class FeatureFlagController extends Controller { public function index() { // الحصول على جميع الميزات المعرفة $features = collect([ 'new-dashboard', 'beta-search', 'advanced-analytics', 'dark-mode', ])->map(function ($feature) { return [ 'name' => $feature, 'active_count' => $this->getActiveCount($feature), 'total_count' => User::count(), ]; }); return view('admin.features.index', compact('features')); } public function users(string $feature) { // الحصول على المستخدمين الذين لديهم هذه الميزة نشطة $users = User::query() ->whereHas('features', function ($query) use ($feature) { $query->where('name', $feature) ->where('value', 'true'); }) ->paginate(50); return view('admin.features.users', compact('feature', 'users')); } public function toggle(Request $request, string $feature) { $validated = $request->validate([ 'user_ids' => 'required|array', 'user_ids.*' => 'exists:users,id', 'active' => 'required|boolean', ]); $users = User::findMany($validated['user_ids']); if ($validated['active']) { Feature::for($users)->activate($feature); } else { Feature::for($users)->deactivate($feature); } return back()->with('success', "تم تحديث الميزة {$feature} لـ " . count($users) . " مستخدم"); } public function rollout(Request $request, string $feature) { $validated = $request->validate([ 'percentage' => 'required|integer|min:0|max:100', ]); // حساب عدد المستخدمين الذين يجب أن يكون لديهم الميزة $totalUsers = User::count(); $targetCount = ($totalUsers * $validated['percentage']) / 100; // الحصول على العدد النشط الحالي $currentCount = $this->getActiveCount($feature); if ($currentCount < $targetCount) { // التنشيط لمزيد من المستخدمين $needed = $targetCount - $currentCount; $users = User::whereDoesntHave('features', function ($query) use ($feature) { $query->where('name', $feature); })->inRandomOrder()->limit($needed)->get(); Feature::for($users)->activate($feature); } else { // التعطيل لبعض المستخدمين $excess = $currentCount - $targetCount; $users = User::whereHas('features', function ($query) use ($feature) { $query->where('name', $feature) ->where('value', 'true'); })->inRandomOrder()->limit($excess)->get(); Feature::for($users)->deactivate($feature); } return back()->with('success', "تم إطلاق الميزة {$feature} إلى {$validated['percentage']}%"); } private function getActiveCount(string $feature): int { return User::whereHas('features', function ($query) use ($feature) { $query->where('name', $feature) ->where('value', 'true'); })->count(); } }

التمرين 1: تنفيذ الإطلاق التدريجي للميزة

أنشئ ميزة جديدة مع إطلاق تدريجي:

  1. عرّف الميزة "new-editor" التي تبدأ عند 0%
  2. أنشئ مسار مسؤول لزيادة نسبة الإطلاق
  3. تأكد من التعيين المتسق (نفس المستخدمين يحصلون عليه دائماً)
  4. تتبع المستخدمين الذين لديهم الميزة في قاعدة البيانات
  5. عرض نسبة الإطلاق في لوحة تحكم المسؤول
  6. أضف القدرة على التمكين القسري لمختبري بيتا محددين

التمرين 2: بناء نظام اختبار A/B

أنشئ اختبار A/B لزر دعوة لاتخاذ إجراء:

  1. عرّف الميزة "cta-test" بـ 3 متغيرات: control, variant-a, variant-b
  2. قسم الحركة بالتساوي (33% لكل منها)
  3. اعرض نص/لون زر مختلف لكل متغير
  4. تتبع النقرات مع معلومات المتغير
  5. أنشئ صفحة تحليلات تعرض معدل التحويل لكل متغير
  6. أضف القدرة على إعلان فائز والإطلاق إلى 100%

التمرين 3: نظام ميزات متعدد النطاق

أنشئ ميزة تعمل مع نطاقات متعددة:

  1. عرّف الميزة "team-collaboration" محددة النطاق لـ User + Team
  2. الميزة نشطة فقط إذا: الفريق مميز والمستخدم عضو في الفريق
  3. أنشئ برنامجاً وسيطاً للتحقق من الميزة قبل الوصول إلى المسارات
  4. اعرض حالة الميزة في إعدادات الفريق
  5. اسمح لمسؤولي الفريق بطلب الوصول إلى الميزة
  6. ابن سير عمل موافقة المسؤول

الخلاصة

في هذا الدرس، أتقنت Laravel Pennant لإدارة الأعلام المميزة. تعلمت كيفية تثبيت وتكوين Pennant، وتعريف الميزات البسيطة والمعقدة، والتحقق من حالة الميزة في جميع أنحاء تطبيقك، وإنشاء ميزات مبنية على الفئات لمنطق معقد، وتحديد نطاق الميزات لكيانات مختلفة، وتنشيط الميزات يدوياً، وتطبيق اختبار A/B، وبناء لوحات تحكم لإدارة الميزات. الأعلام المميزة ضرورية لتطوير التطبيقات الحديثة، مما يمكن من نشر خالٍ من المخاطر، وقرارات قائمة على البيانات، وتجارب مستخدم أفضل.

في الدرس التالي، سنستكشف تقنيات تحسين الأداء وأفضل الممارسات لتطبيقات Laravel في الإنتاج.