موضع نص الإشعار اتصل بنا تفضل الآن!

كيف تبني نظام Notification مخصص بدل النظام الجاهز في Laravel

George Bahgat

لماذا تبني نظام إشعارات مخصص في Laravel؟

لطالما اعتبرت نظام الإشعارات في لارافيل أحد أقوى الميزات الجاهزة التي يقدمها الإطار. فهو يوفر واجهة موحدة لإرسال التنبيهات عبر قنوات متعددة مثل البريد الإلكتروني، قاعدة البيانات، وSlack بسهولة فائقة. جربت استخدام النظام الجاهز في مشاريع عديدة ووجدته حلاً مثالياً للاحتياجات القياسية. لكن مع تعقيد المتطلبات في التطبيقات الكبيرة، لاحظت أن النظام الجاهز بدأ يعيق التطور بدلاً من دعمه.

في أحد المشاريع، طُلب مني تتبع حالة كل إشعار بدقة ــ ليس فقط فيما إذا تم إرساله، ولكن أيضاً إذا تم توصيله فعلياً إلى المستخدم، مع إمكانية إعادة المحاولة تلقائياً عند الفشل، وتقديم إحصائيات مفصلة لفريق الدعم. النظام الجاهز في لارافيل، رغم كفاءته، لم يكن مصمماً لتلبية هذه المستويات من التعقيد. هنا أدركت الحاجة إلى بناء نظام مخصص يتكيف مع احتياجات التطبيق الفريدة.

متى تحتاج إلى نظام مخصص؟ عندما تحتاج إلى تتبع متقدم لحالة الإشعارات، إدارة معقدة لقنوات الإرسال، أو تكامل مع خدمات خارجية غير مدعومة افتراضياً في إطار العمل لارافيل، فإن الاستثمار في بناء نظام مخصص يصبح ضرورياً.

في هذه المقالة الشاملة، سأشاركك رحلتي في بناء نظام إشعارات مخصص من الصفر في لارافيل. سنبدأ بتصميم هندسة قابلة للتوسع، ثم ننفذها خطوة بخطوة مع أمثلة عملية، ونتناول تقنيات متقدمة لضمان الموثوقية والأداء. ستتعلم كيفية إنشاء قنوات مخصصة، وإدارة قوائم الانتظار بكفاءة، وتقديم واجهة إدارة شاملة. كل ذلك مدعوم بأمثلة كود حقيقية من مشروع فعلي.

التخطيط والتصميم: هندسة نظام الإشعارات المخصص

قبل كتابة أي سطر برمجي، وجدت أن التخطيط الجيد هو أساس أي نظام ناجح. النظام الجاهز في لارافيل يعتمد على نهج بسيط: كل إشعار يمثله صنف واحد يحتوي على طرق متعددة مثل `toMail` و `toDatabase` وما شابه. بينما هذا النهج رائع للبساطة، إلا أنه يصبح معقداً عندما تحتاج إلى إضافة سلوكيات متقدمة مثل التتبع، إعادة المحاولة، والإحصائيات.

المقارنة بين النظام الجاهز والنظام المخصص

بعد تجربة كلا النهجين، لاحظت فروق جوهرية. النظام الجاهز يعمل بشكل ممتاز للإشعارات البسيطة، لكنه يظهر قصوراً في السيناريوهات المتقدمة. على سبيل المثال، إذا أردت تتبع حالة إرسال إشعار معين عبر قناة Slack، أو معرفة بالضبط متى فشلت محاولة الإرسال، ستحتاج إلى إضافة منطق إضافي خارج نطاق النظام الأساسي.

في نظامي المخصص، اعتمدت على فصل المكونات بشكل واضح، حيث أصبح لكل كيان مسؤولية محددة. هذا يجعل الكود أكثر قابلية للقراءة والصيانة، وأسهل في الاختبار أيضاً. الفرق الأكبر كان في المرونة ــ فالنظام المخصص سمح لي بتعديل سلوك أي جزء دون التأثير على المنظومة بأكملها.

المكونات الأساسية للنظام المخصص

بعد دراسة معمقة، صممت النظام المخصص ليتكون من عدة مكونات رئيسية:

  • النماذج (Models): نموذج Notification لتخزين بيانات الإشعار، ونموذج NotificationChannel لإدارة قنوات الإرسال.
  • القنوات (Channels): أصناف منفصلة لكل قناة إرسال (EmailChannel, SlackChannel, SmsChannel) مسؤولة عن منطق الإرسال المحدد.
  • المُرسل (Dispatcher): وسيط مسؤول عن توجيه الإشعارات إلى القنوات المناسبة وإدارة قوائم الانتظار.
  • المُجدد (Renderer): مكون خاص بتوليد محتوى الإشعار بناءً على القالب والقناة المستهدفة.
  • واجهة الإدارة: لوحة تحكم تمكن فريق العمل من مشاهدة الإحصائيات وإدارة القوالب ومتابعة حالة الإشعارات.

التنفيذ العملي: بناء النظام خطوة بخطوة

الآن دعنا ننتقل إلى الجزء العملي، حيث سنبني النظام معاً من الصفر. سأستخدم مشروعاً افتراضياً لنظام إدارة المهام، حيث نحتاج إلى إرسال إشعارات للمستخدمين عند تعيين مهام جديدة لهم، أو عند اقتراب موعد التسليم.

إعداد قاعدة البيانات والنماذج

أولاً، نحتاج إلى تصميم هيكل قاعدة البيانات الذي سيدعم نظامنا المخصص. لنبدأ بإنشاء جداول جديدة عبر نظام الهجرة (Migrations) في لارافيل.

// إنشاء جدول الإشعارات المخصصة
php artisan make:migration create_custom_notifications_table

في ملف الهجرة، سننشئ هيكلاً أكثر تفصيلاً من جدول الإشعارات الافتراضي في لارافيل:

public function up()
{
  Schema::create('custom_notifications', function (Blueprint $table) {
    $table->id();
    $table->string('type');
    $table->morphs('notifiable');
    $table->text('data');
    $table->string('status')->default('pending'); // pending, sent, failed, delivered
    $table->timestamp('sent_at')->nullable();
    $table->timestamp('delivered_at')->nullable();
    $table->integer('retry_count')->default(0);
    $table->text('failure_reason')->nullable();
    $table->string('channel'); // email, sms, slack, etc.
    $table->timestamps();
  });
}

لاحظ أنني أضفت حقولاً إضافية مثل `status` لتتبع حالة الإشعار، و`retry_count` لعد محاولات إعادة الإرسال، و`failure_reason` لتسجيل سبب الفشل عند حدوثه. هذه الحقول هي ما يميز نظامنا المخصص وتمنحنا التحليل الدقيق الذي نحتاجه.

الآن، لننشئ نموذج Notification المخصص:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;

class CustomNotification extends Model
{
  protected $table = 'custom_notifications';
  protected $fillable = [
    'type', 'notifiable_id', 'notifiable_type', 'data',
    'status', 'sent_at', 'delivered_at', 'retry_count',
    'failure_reason', 'channel'
  ];
  protected $casts = [
    'data' => 'array',
    'sent_at' => 'datetime',
    'delivered_at' => 'datetime',
  ];

  public function notifiable(): MorphTo
  {
    return $this->morphTo();
  }

  public function markAsSent(): bool
  {
    return $this->update([
      'status' => 'sent',
      'sent_at' => now(),
    ]);
  }

  public function markAsFailed(string $reason): bool
  {
    return $this->update([
      'status' => 'failed',
      'failure_reason' => $reason,
      'retry_count' => $this->retry_count + 1,
    ]);
  }
}

إنشاء قنوات إرسال مخصصة

الآن نصل إلى قلب نظام الإشعارات المخصص: قنوات الإرسال. في النظام الجاهز، تستخدم لارافيل طرقاً مثل `toMail` و `toDatabase` داخل صنف الإشعار الواحد [citation:1]. لكن في نظامنا، سنفصل كل قناة في صنف مستقل تماماً، مما يمنحنا مرونة غير محدودة تقريباً.

لنبدأ بإنشاء هيكل أساسي للقناة، ثم ننفذ قنوات محددة. أولاً، سننشئ واجهة (Interface) تضمن أن جميع القنوات تتبع نفس الهيكل:

<?php

namespace App\Contracts;

use App\Models\CustomNotification;

interface NotificationChannel
{
  public function send(CustomNotification $notification): bool;
  public function canRetry(): bool;
  public function getRetryDelay(): int; // بالثواني
}

الآن لننفذ قناة البريد الإلكتروني كمثال عملي. لاحظ كيف أصبح منطق الإرسال معزولاً تماماً في هذا الصنف:

<?php

namespace App\Services\NotificationChannels;

use App\Contracts\NotificationChannel;
use App\Models\CustomNotification;
use Illuminate\Support\Facades\Mail;
use App\Mail\CustomNotificationMail;

class EmailChannel implements NotificationChannel
{
  protected $maxRetries = 3;

  public function send(CustomNotification $notification): bool
  {
    try {
      $notifiable = $notification->notifiable;
      $data = $notification->data;
      
      Mail::to($notifiable->email)->send(
        new CustomNotificationMail($data)
      );
      
      return true;
    } catch (\Exception $e) {
      logger()->error('فشل إرسال الإشعار عبر البريد: ' . $e->getMessage());
      return false;
    }
  }

  public function canRetry(): bool
  {
    return $notification->retry_count < $this->maxRetries;
  }

  public function getRetryDelay(): int
  {
    // زيادة التأخير مع كل محاولة فاشلة
    return $notification->retry_count * 60; // 60, 120, 180 ثانية
  }
}

بنفس الطريقة، يمكننا إنشاء قنوات أخرى. على سبيل المثال، قناة لـ Slack:

<?php

namespace App\Services\NotificationChannels;

use App\Contracts\NotificationChannel;
use App\Models\CustomNotification;
use Illuminate\Support\Facades\Http;

class SlackChannel implements NotificationChannel
{
  public function send(CustomNotification $notification): bool
  {
    try {
      $webhookUrl = config('services.slack.webhook_url');
      $data = $notification->data;
      
      $response = Http::post($webhookUrl, [
        'text' => $data['message'],
        'channel' => $data['channel'] ?? '#general',
      ]);
      
      return $response->successful();
    } catch (\Exception $e) {
      logger()->error('فشل إرسال الإشعار عبر Slack: ' . $e->getMessage());
      return false;
    }
  }

  // ... rest of the interface methods
}
نصيحة عملية: لاحظ كيف استخدمنا try/catch في كل قناة. هذا يضمن أن أي خطأ في قناة واحدة لن يؤثر على القنوات الأخرى. كما أننا نسجل الأخطاء بشكل منفصل لكل قناة، مما يسهل عملية تصحيح الأخطاء لاحقاً.

بناء خدمة الإرسال الرئيسية

الآن نحتاج إلى وسيط ذكي يقوم بتلقي طلبات الإشعارات وتوجيهها إلى القنوات المناسبة. هذه الخدمة هي العقل المدبر لنظامنا، حيث تدير دورة حياة الإشعار بالكامل.

<?php

namespace App\Services;

use App\Models\CustomNotification;
use App\Contracts\NotificationChannel;
use Illuminate\Support\Facades\Queue;

class NotificationDispatcher
{
  protected $channels = [];

  public function __construct()
  {
    // تسجيل القنوات المتاحة
    $this->channels = [
      'email' => \App\Services\NotificationChannels\EmailChannel::class,
      'slack' => \App\Services\NotificationChannels\SlackChannel::class,
      'sms' => \App\Services\NotificationChannels\SmsChannel::class,
    ];
  }

  public function send($notifiable, array $data, array $channels = ['email']): void
  {
    foreach ($channels as $channel) {
      if (!isset($this->channels[$channel])) {
        continue;
      }
      
      $notification = CustomNotification::create([
        'type' => $data['type'],
        'notifiable_id' => $notifiable->id,
        'notifiable_type' => get_class($notifiable),
        'data' => $data,
        'channel' => $channel,
      ]);
      
      // إرسال المهمة إلى قائمة الانتظار
      Queue::push(new \App\Jobs\SendNotificationJob($notification));
    }
  }
}

إدارة قوائم الانتظار والمعالجة غير المتزامنة

من أكبر التحديات التي واجهتها في نظام الإشعارات هو كيفية معالجة عدد كبير من الإشعارات دون التأثير على أداء التطبيق. الحل كان في استخدام قوائم الانتظار (Queues) بشكل مكثف [citation:1]. في نظامنا المخصص، سننشئ مهمة مخصصة (Job) لكل إشعار.

<?php

namespace App\Jobs;

use App\Models\CustomNotification;
use App\Contracts\NotificationChannel;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

class SendNotificationJob implements ShouldQueue
{
  use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

  public $tries = 3;
  public $timeout = 30;
  public $notification;

  public function __construct(CustomNotification $notification)
  {
    $this->notification = $notification;
  }

  public function handle(): void
  {
    $channelName = $this->notification->channel;
    $channelClass = $this->getChannelClass($channelName);
    
    if (!$channelClass) {
      $this->notification->markAsFailed("القناة غير مدعومة: {$channelName}");
      return;
    }
    
    $channel = app($channelClass);
    
    if ($channel instanceof NotificationChannel) {
      $success = $channel->send($this->notification);
      
      if ($success) {
        $this->notification->markAsSent();
      } else {
        $this->handleFailure($channel);
      }
    }
  }

  public function failed(\Exception $exception): void
  {
    $this->notification->markAsFailed($exception->getMessage());
  }

  protected function handleFailure(NotificationChannel $channel): void
  {
    if ($channel->canRetry()) {
      $this->release($channel->getRetryDelay());
    } else {
      $this->notification->markAsFailed('تم استنفاد عدد المحاولات');
    }
  }

  protected function getChannelClass(string $channelName): ?string
  {
    $channels = [
      'email' => \App\Services\NotificationChannels\EmailChannel::class,
      'slack' => \App\Services\NotificationChannels\SlackChannel::class,
    ];
    
    return $channels[$channelName] ?? null;
  }
}

تقنيات متقدمة لتحسين النظام

بعد بناء الهيكل الأساسي، حان الوقت لتحويل النظام من مجرد ناقل إشعارات إلى نظام ذكي يمكن الاعتماد عليه في بيئات الإنتاج. هذه التقنيات المتقدمة هي نتاج تجارب عملية مع أنظمة إشعارات على نطاق واسع.

نظام إعادة المحاولة الذكية

واحدة من أكبر المشاكل التي واجهتها في البداية كانت كيفية التعامل مع الفشل المؤقت في إرسال الإشعارات. بعض الخدمات مثل Slack أو واجهات برمجة تطبيقات البريد قد تعود للعمل بعد دقائق، بينما تحتاج خدمات أخرى إلى وقت أطول.

الحل كان في تنفيذ استراتيجية إعادة محاولة ذكية (Exponential Backoff). بدلاً من إعادة المحاولة فوراً أو بعد فترات ثابتة، يزيد النظام الوقت بين المحاولات تدريجياً:

public function getRetryDelay(): int
{
  // إستراتيجية Exponential Backoff
  $delay = pow(2, $this->notification->retry_count) * 60;
  
  // لا تزيد عن 24 ساعة
  return min($delay, 24 * 60 * 60);
}

نظام القوالب الديناميكية

في التطبيقات الكبيرة، تحتاج إلى تخصيص محتوى الإشعارات بناءً على لغة المستخدم، تفضيلاته، ونوع الجهاز. نظام القوالب الديناميكية يحل هذه المشكلة بفعالية.

لننشئ نظام قوالب بسيطاً لكنه قوي:

<?php

namespace App\Services;

class NotificationTemplateEngine
{
  public function render(string $template, array $data, string $locale = 'ar'): string
  {
    // تحميل القالب من قاعدة البيانات أو نظام الملفات
    $templateContent = $this->loadTemplate($template, $locale);
    
    // استبدال العناصر النائبة
    foreach ($data as $key => $value) {
      $placeholder = "{{{$key}}}";
      $templateContent = str_replace($placeholder, $value, $templateContent);
    }
    
    return $templateContent;
  }

  protected function loadTemplate(string $template, string $locale): string
  {
    // يمكن تحميل القوالب من قاعدة البيانات
    // أو من نظام الملفات مع دعم التعددية اللغوية
    $path = resource_path("notifications/templates/{$locale}/{$template}.txt");
    
    if (file_exists($path)) {
      return file_get_contents($path);
    }
    
    // العودة للغة الافتراضية
    $defaultPath = resource_path("notifications/templates/en/{$template}.txt");
    return file_get_contents($defaultPath);
  }
}

المراقبة والتقارير

لا يكتمل نظام الإشعارات بدون أدوات مراقبة قوية. في تجربتي، وجدت أن وجود لوحة تحكم تظهر إحصائيات حية عن أداء النظام كان لا يقدر بثمن لفريق العمليات.

لننشئ وحدة تقارير بسيطة:

<?php

namespace App\Services;

use App\Models\CustomNotification;
use Illuminate\Support\Facades\DB;

class NotificationReporter
{
  public function getDailyStats(string $date = null): array
  {
    $date = $date ?: now()->format('Y-m-d');
    
    return [
      'total' => CustomNotification::whereDate('created_at', $date)->count(),
      'sent' => CustomNotification::whereDate('created_at', $date)->where('status', 'sent')->count(),
      'failed' => CustomNotification::whereDate('created_at', $date)->where('status', 'failed')->count(),
      'pending' => CustomNotification::whereDate('created_at', $date)->where('status', 'pending')->count(),
    ];
  }

  public function getChannelPerformance(): array
  {
    return CustomNotification::select('channel',
      DB::raw('COUNT(*) as total'),
      DB::raw('SUM(CASE WHEN status = "sent" THEN 1 ELSE 0 END) as successful'),
      DB::raw('ROUND(SUM(CASE WHEN status = "sent" THEN 1 ELSE 0 END) * 100.0 / COUNT(*), 2) as success_rate')
    )->groupBy('channel')
    ->get()
    ->toArray();
  }

  public function getRecentFailures(int $limit = 10): array
  {
    return CustomNotification::with('notifiable')
      ->where('status', 'failed')
      ->orderBy('updated_at', 'desc')
      ->limit($limit)
      ->get()
      ->toArray();
  }
}

دمج النظام مع لارافيل

حتى الآن بنينا نظاماً مستقلاً، لكن لكي يكون مفيداً، يجب أن يندمج بسلاسة مع إطار عمل لارافيل. هذا يعني توفير واجهات مألوفة للمطورين وتعظيم الاستفادة من ميزات لارافيل الموجودة.

مزود الخدمة (Service Provider)

لنجعل نظامنا متاحاً عبر حاقن الاعتماد (Dependency Injection) في لارافيل، سننشئ مزود خدمة مخصص:

<?php

namespace App\Providers;

use App\Services\NotificationDispatcher;
use App\Services\NotificationTemplateEngine;
use App\Services\NotificationReporter;
use Illuminate\Support\ServiceProvider;

class NotificationServiceProvider extends ServiceProvider
{
  public function register(): void
  {
    $this->app->singleton(NotificationDispatcher::class, function ($app) {
      return new NotificationDispatcher();
    });
    
    $this->app->bind(NotificationTemplateEngine::class, function ($app) {
      return new NotificationTemplateEngine();
    });
    
    $this->app->bind(NotificationReporter::class, function ($app) {
      return new NotificationReporter();
    });
  }

  public function boot(): void
  {
    // نشر ملفات الهجرة تلقائياً
    $this->publishes([
      __DIR__ . '/../../database/migrations' => database_path('migrations'),
    ], 'custom-notifications-migrations');

    // نشر ملفات التهيئة
    $this->publishes([
      __DIR__ . '/../../config/custom-notifications.php' => config_path('custom-notifications.php'),
    ], 'custom-notifications-config');
  }
}

الأوامر Artisan المخصصة

لجعل النظام أكثر احترافية، دعنا ننشئ بعض أوامر Artisan لإدارة النظام من سطر الأوامر:

<?php

namespace App\Console\Commands;

use App\Services\NotificationReporter;
use Illuminate\Console\Command;

class NotificationStatsCommand extends Command
{
  protected $signature = 'notifications:stats {--channel=} {--date=}';
  protected $description = 'عرض إحصائيات الإشعارات';

  public function handle(NotificationReporter $reporter): int
  {
    $date = $this->option('date') ?: now()->format('Y-m-d');
    $channel = $this->option('channel');
    
    $stats = $reporter->getDailyStats($date);
    $performance = $reporter->getChannelPerformance();
    
    $this->info("إحصائيات الإشعارات لتاريخ {$date}:");
    $this->table(['المجموع', 'تم الإرسال', 'فشل', 'قيد الانتظار'], [[
      $stats['total'], $stats['sent'], $stats['failed'], $stats['pending']
    ]]);
    
    $this->info("أداء القنوات:");
    $this->table(['القناة', 'المجموع', 'الناجحة', 'معدل النجاح'], $performance);
    
    return 0;
  }
}

الخاتمة: من النظام الجاهز إلى النظام المخصص

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

اليوم، بعد تنفيذ عدة أنظمة مخصصة لمشاريع مختلفة، أستطيع أن أقول إن الاستثمار في بناء نظام مخصص يؤتي ثماره على المدى الطويل. ليس فقط لأنه يمنحك المرونة والتحكم الكامل، ولكن لأنه يمكنك من تقديم تجربة مستخدم استثنائية ــ حيث تصل الإشعارات الصحيحة إلى المستخدمين الصحيحين في الوقت المناسب وبالشكل المناسب.

النظام الذي بنيناه معاً في هذه المقالة ليس مجرد بديل للنظام الجاهز، بل هو منصة قابلة للتوسع بشكل غير محدود تقريباً. يمكنك إضافة قنوات جديدة بسهولة، تحسين أداء النظام، وتكييفه مع أي متطلبات مستقبلية. الأهم من ذلك، أنك الآن تفهم فلسفة تصميم نظام الإشعارات من الداخل، مما يجعلك قادراً على تحسينه وتطويره باستمرار.

الخطوة التالية: ابدأ بتطبيق النظام في مشروع تجريبي صغير. قدّم قناة أو قناتين فقط في البداية، ثم وسع النظام تدريجياً. ستجد أن المرونة التي تحصل عليها تستحق الجهد المبذول، خاصة عندما تواجه متطلبات فريدة لا يمكن للنظام الجاهز تلبيتها.

في النهاية، تذكر أن بناء النظام المخصص ليس هدفاً في حد ذاته، بل وسيلة لتحسين تجربة مستخدميك وتمكين تطبيقك من النمو دون قيود. النظام الذي شاركته معك هو نقطة بداية، يمكنك ــ ويجب عليك ــ تكييفه وتطويره ليناسب احتياجاتك الخاصة. فأحياناً، أفضل نظام هو النظام الذي تبينه بيديك.

إرسال تعليق

الموافقة على ملفات تعريف الارتباط
”نحن نقدم ملفات تعريف الارتباط على هذا الموقع لتحليل حركة المرور وتذكر تفضيلاتك وتحسين تجربتك.“
لا يتوفر اتصال بالإنترنت!
”يبدو أن هناك خطأ ما في اتصالك بالإنترنت ، يرجى التحقق من اتصالك بالإنترنت والمحاولة مرة أخرى.“
تم الكشف عن مانع الإعلانات!
”لقد اكتشفنا أنك تستخدم مكونًا إضافيًا لحظر الإعلانات في متصفحك.
تُستخدم العائدات التي نحققها من الإعلانات لإدارة موقع الويب هذا ، ونطلب منك إدراج موقعنا في القائمة البيضاء في المكون الإضافي لحظر الإعلانات.“
Site is Blocked
Sorry! This site is not available in your country.