عندما يتوقف CRUD العادي عن العمل: التعرف على CQRS و Event Sourcing
\n\nهل سبق لك أن واجهت موقفًا حيث ينمو تطبيق PHP الخاص بك، وينمو... وفي يوم من الأيام يتحول UserController العادي إلى وحش من 2000 سطر؟ مألوف؟ تتكاثر الأخطاء، وينخفض الأداء، وتسبب إضافة ميزة جديدة ارتعاشًا عصبيًا للفريق بأكمله. تهانينا، لقد وصلت إلى اللحظة التي تحتاج فيها إلى تطبيق أنماط معمارية جادة.
سنتحدث اليوم عن أقوى أداتين — CQRS (Command Query Responsibility Segregation) و Event Sourcing. لا تخف من المصطلحات المعقدة. في الواقع، إنها مجرد طرق أنيقة للقول: "دعنا نتوقف عن خلط القراءة والكتابة في كومة واحدة" و "سنخزن ليس الحالة الحالية، بل تاريخ التغييرات".
\n\nهذه المقالة مخصصة لمطوري PHP الذين أتقنوا أساسيات OOP ويريدون الارتقاء بمشاريعهم إلى المستوى التالي. ستتعلم كيفية تطبيق CQRS و Event Sourcing عمليًا، وسترى كودًا حقيقيًا، وستفهم متى تكون هذه الأنماط ضرورية حقًا ومتى يكون من الأفضل التخلي عنها.
\n\nما هو CQRS؟ فصل الأوامر والاستعلامات
\n\nتخيل أن الكود الخاص بك هو مطعم. عادةً، يأخذ النادل (الـ Controller) الطلب (طلب كتابة)، ويركض إلى المطبخ (قاعدة البيانات)، ويجهز الطبق (يعالج البيانات)، ويقدمه فورًا (يعيد الرد). كل شيء رائع عندما يكون في المطعم 10 أشخاص. ولكن عندما يأتي 1000 ضيف، يبدأ المطبخ في الاختناق.
\n\nCQRS (Command Query Responsibility Segregation) يقترح فصل المسؤوليات: بعض الطهاة (الأوامر) يطبخون فقط، والنوادل الآخرون (الاستعلامات) يقدمون فقط. من الناحية الفنية، هذا يعني أنك تستخدم نماذج مختلفة لقراءة البيانات وكتابتها.
\n\nتطبيق أساسي لـ CQRS في PHP
\n\nلنرى كيف يبدو هذا في الكود. سنبدأ بمثال بسيط بدون أطر عمل، لفهم الجوهر.
\n\n// الأمر هو DTO مع بيانات لتغيير الحالة\nclass CreateUserCommand {\n public function __construct(\n public readonly string $email,\n public readonly string $name\n ) {}\n}\n\n// معالج الأمر — هنا يحدث منطق الأعمال\nclass CreateUserHandler {\n public function __construct(\n private UserRepository $repository,\n private EventDispatcher $dispatcher\n ) {}\n\n public function handle(CreateUserCommand $command): void {\n $user = new User($command->email, $command->name);\n $this->repository->save($user);\n $this->dispatcher->dispatch(new UserCreatedEvent($user->getId()));\n }\n}\n\n// الاستعلام هو DTO لجلب البيانات\nclass GetUserQuery {\n public function __construct(\n public readonly int $userId\n ) {}\n}\n\n// معالج الاستعلام — قراءة فقط، لا تغييرات\nclass GetUserHandler {\n public function __construct(\n private UserReadRepository $readRepository\n ) {}\n\n public function handle(GetUserQuery $query): ?UserDTO {\n return $this->readRepository->findById($query->userId);\n }\n}\n\nلاحظ: لدينا مستودعان مختلفان. UserRepository مسؤول عن الكتابة (يمكنه استخدام منطق أعمال معقد، والتحقق من الصحة)، بينما UserReadRepository مسؤول عن القراءة (يمكن أن يكون استعلام SQL بسيطًا أو حتى جدولًا غير معياري).
ناقل الأوامر (Command Bus)
\n\nفي المشاريع الحقيقية، يتم إرسال الأوامر والاستعلامات عبر ناقل — موزع مركزي يجد المعالج المناسب بنفسه.
\n\n// تطبيق بسيط لـ Command Bus\nclass SimpleCommandBus {\n public function __construct(\n private array $handlers = []\n ) {}\n\n public function registerHandler(string $commandClass, object $handler): void {\n $this->handlers[$commandClass] = $handler;\n }\n\n public function dispatch(object $command): void {\n $handler = $this->handlers[$co