لماذا Generics ليست مجرد موضة عابرة، بل ضرورة؟
\n\nإذا كنت قد كتبت يومًا كود PHP يعمل مع المجموعات، المستودعات أو طبقة الخدمات، فبالتأكيد واجهت موقفًا: الدالة تستقبل array، لكن ما بداخلها لغز. قد يكون هناك مستخدمون، أو ربما قطط. أو الأسوأ من ذلك — الاثنان معًا. هنا تأتي Generics (الجينيريكس) للإنقاذ — أداة قوية للكتابة تجعل كودك قابلًا للتوقع، آمنًا وموثقًا ذاتيًا.
في هذه المقالة، سنشرح كيف تعمل generics في PHP، ولماذا تحتاجها وكيف تستخدمها في المشاريع الحقيقية. ستتعلم كيف تتجنب الأخطاء النمطية وتجعل كودك احترافيًا. هيا بنا!
\n\nما هي Generics بكلمات بسيطة؟
\n\nتخيل أنك مطور متجر إلكتروني. لديك كلاس Cart يخزن المنتجات. بدون generics تكتب:
class Cart {\n private array $items;\n \n public function addItem($item): void {\n $this->items[] = $item;\n }\n \n public function getItems(): array {\n return $this->items;\n }\n}\n\nالمشكلة؟ $item يمكن أن يكون أي شيء — رقمًا، نصًا، كائن Product أو حتى null. IDE لن تخبرك بنوع القيمة المرجعة من getItems(). سيتعين عليك العودة إلى التوثيق أو التصحيح.
Generics تحل هذه المشكلة. تسمح لك بتحديد: "مرحبًا، هذا الكلاس يعمل فقط مع المنتجات!". يبدو الأمر هكذا:
\n\nclass Cart {\n /** @param Product[] $items */\n private array $items;\n \n public function addItem(Product $item): void {\n $this->items[] = $item;\n }\n \n /** @return Product[] */\n public function getItems(): array {\n return $this->items;\n }\n}\n\nلكن هذا مجرد تقليد. ظهرت generics الحقيقية في PHP مع PHP 8.0، والدعم الكامل في PHP 8.1+ باستخدام السمات (attributes) والتحليل الثابت (مثل PHPStan أو Psalm).
\n\nكيف تعمل Generics في PHP فعليًا؟
\n\nعلى عكس Java أو C#، لا يمتلك PHP دعمًا أصليًا لـ generics على مستوى الصياغة (حتى الآن). لكن بفضل تعليقات PHPDoc وأدوات التحليل الثابت يمكننا الحصول على جميع مزايا الكتابة.
\n\nأنواع القوالب (Template Types)
\n\nالنهج الأكثر شيوعًا هو استخدام @template في PHPDoc. لنلق نظرة على مثال مع مجموعة:
/**\n * @template T\n */\nclass Collection {\n /** @var array<int, T> */\n private array $items = [];\n \n /**\n * @param T $item\n */\n public function add(mixed $item): void {\n $this->items[] = $item;\n }\n \n /**\n * @return T\n */\n public function get(int $index): mixed {\n return $this->items[$index] ?? throw new \\OutOfBoundsException();\n }\n}\n\nالآن عند استخدام هذا الكلاس، تحدد النوع المحدد:
\n\n/** @var Collection<User> $userCollection */\n$userCollection = new Collection();\n$userCollection->add(new User('Alice'));\n$user = $userCollection->get(0); // PHPStan يعرف: $user هو User\necho $user->name; // آمن!\n\nالقيود (Constraints) لـ Generics
\n\nأحيانًا تحتاج أن النوع T ليس أي نوع، بل يجب أن ينفذ واجهة معينة. هذا يُسمى bound (تقييد):
\n\n/**\n * @template T of \\JsonSerializable\n */\nclass JsonCollection {\n /** @var array<int, T> */\n private array $items = [];\n \n public function toJson(): string {\n return json_encode($this->items);\n }\n}\n\nالآن لن تتمكن من إضافة كائن لا ينفذ JsonSerializable إلى المجموعة. IDE والمحلل الثابت سيظهران خطأ.