مقدمة: foreach ليس مجرد سكر نحوي
\n\nكل مطور PHP، من المبتدئ إلى الخبير، يستخدم foreach بشكل يومي تقريبًا. يبدو بسيطًا وواضحًا: "مرر على كل عنصر في المصفوفة". لكن هل تساءلت يومًا: كيف يقوم PHP بذلك بالضبط؟ لماذا يتصرف نفس foreach بشكل مختلف مع المصفوفات والكائنات؟ لماذا يؤدي تعديل المصفوفة داخل الحلقة أحيانًا إلى نتائج غير متوقعة؟
سؤال "كيف يعمل foreach في PHP فعليًا؟" على Stack Overflow حصد عشرات الآلاف من المشاهدات ومئات الأصوات. هذا ليس من قبيل الصدفة: تحت غطاء هذا العامل البسيط ظاهريًا، يختبئ آلية معقدة وأنيقة. اليوم سننظر في قلب PHP ونحلل تشريح حلقة foreach خطوة بخطوة.
\n\nبنهاية هذه المقالة، لن تفهم فقط كيف يعمل foreach، بل ستتعلم أيضًا تجنب الأخطاء النموذجية التي تقع فيها حتى المبرمجين ذوي الخبرة. هيا بنا!
الفصل الأول: foreach ومؤشر المصفوفة الداخلي
\n\nعندما يواجه PHP البنية foreach ($array as $value)، فإنه لا يمر ببساطة عبر الفهارس. أساس العمل هو مؤشر المصفوفة الداخلي (internal array pointer). هذا "سهم" غير مرئي يشير إلى العنصر الحالي.
إليك كيف تبدو هذه العملية خطوة بخطوة:
\n\n- \n
- إعادة تعيين المؤشر. يستدعي PHP الدالة
reset($array)— يصبح المؤشر على العنصر الأول. \n - قراءة المفتاح والقيمة. يتم أخذ العنصر الحالي من المصفوفة. \n
- التعيين. يتم نسخ القيمة إلى المتغير
$value(إذا لم يكن هناك &). \n - تحريك المؤشر. يقوم PHP بتحريك المؤشر إلى العنصر التالي. \n
- التكرار. يتم تنفيذ الخطوات 2-4 حتى يصل المؤشر إلى نهاية المصفوفة. \n
لكن هناك فارق مهم: يقوم PHP بإنشاء نسخة من المصفوفة قبل بدء التكرار! دعنا نفهم لماذا هذا ضروري.
\n\nمثال: إعادة تعيين المؤشر داخل الحلقة
\n\n<?php\n$array = [10, 20, 30];\n\nforeach ($array as $value) {\n echo "$value\\n";\n // نحاول إعادة تعيين المؤشر\n reset($array);\n}\n// الإخراج: 10, 20, 30 — reset لم يؤثر على foreach\n?>\n\nلماذا لم يعمل reset()؟ لأن foreach يعمل مع نسخة من المؤشر الداخلي، وليس مع الأصل. تم تصميم هذا عمدًا لمنع تعديل المصفوفة داخل الحلقة من كسر التكرار.
الفصل الثاني: foreach بالقيمة مقابل بالمرجع
\n\nالمصدر الأكثر شيوعًا للارتباك هو الفرق بين foreach ($array as $value) و foreach ($array as &$value). دعنا نوضح كل شيء.
foreach بالقيمة: نسخ في كل خطوة
\n\nعندما تكتب foreach ($array as $value)، يقوم PHP في كل تكرار بنقل القيمة الحالية للمصفوفة إلى المتغير $value. هذا يعني أن تغيير $value لا يؤثر على المصفوفة الأصلية.
<?php\n$array = [1, 2, 3];\nforeach ($array as $value) {\n $value *= 2; // نغير النسخة\n}\nprint_r($array);\n// الإخراج: [1, 2, 3] — المصفوفة لم تتغير\n?>\n\nالإيجابيات: آمن، البيانات الأصلية لا تتلف.
\nالسلبيات: إذا كانت عناصر المصفوفة كائنات كبيرة، قد يكون النسخ مكلفًا. على الرغم من أن PHP يستخدم النسخ عند الكتابة (copy-on-write)، بالنسبة للمصفوفات التي تحتوي على كائنات، يتم نسخ المعرفات (handles) فقط، وليس الكائنات نفسها.
foreach بالمرجع: العمل مع الأصل
\n\nالصيغة foreach ($array as &$value) تجعل PHP يمرر مرجعًا إلى العنصر، وليس نسخة. الآن $value هو اسم مستعار لعنصر المصفوفة.
<?php\n$array = [1, 2, 3];\nforeach ($array as &$value) {\n $value *= 2; // نغير العنصر الأصلي\n}\nprint_r($array);\n// الإخراج: [2, 4, 6] — المصفوفة تغيرت\n?>\n\nالإيجابيات: أداء أفضل مع المصفوفات الكبيرة، والقدرة على تعديل المصفوفة الأصلية مباشرة.
\nالسلبيات: خطر كبير! بعد انتهاء الحلقة، يظل المتغير $value كمرجع لآخر عنصر. أي تغيير لاحق لـ $value سيعدل المصفوفة الأصلية عن طريق الخطأ.
المشكلة الشهيرة: المرجع المتبقي
\n\n<?php\n$array = [1, 2, 3];\nforeach ($array as &$value) {\n // لا تفعل شيئًا\n}\n// الآن $value هو مرجع للعنصر الأخير (3)\n\nforeach ($array as $value) {\n // هذا يغير المصفوفة الأصلية!\n}\nprint_r($array);\n// الإخراج: [1, 2, 2] — مفاجأة!\n?>\n\nالحل: استخدم unset($value) بعد الحلقة الأولى لقطع الاتصال المرجعي.
الفصل الثالث: foreach مع الكائنات
\n\nعند استخدام foreach مع الكائنات، يتغير السلوك بشكل جذري. بدلاً من مؤشر المصفوفة الداخلي، يستخدم PHP واجهة Iterator أو الخصائص العامة للكائن.
الكائنات التي تطبق Iterator
\n\nإذا كان الكائن يطبق واجهة Iterator، فإن foreach يستدعي طرقها:
- \n
rewind()— إعادة التعيين إلى البداية \n valid()— التحقق من وجود عنصر حالي \n current()— الحصول على القيمة الحالية \n key()— الحصول على المفتاح الحالي \n next()— الانتقال إلى العنصر التالي \n
<?php\nclass MyIterator implements Iterator {\n private $items = [];\n private $position = 0;\n\n public function __construct($items) {\n $this->items = $items;\n }\n\n public function rewind() {\n $this->position = 0;\n }\n\n public function current() {\n return $this->items[$this->position];\n }\n\n public function key() {\n return $this->position;\n }\n\n public function next() {\n ++$this->position;\n }\n\n public function valid() {\n return isset($this->items[$this->position]);\n }\n}\n\n$iterator = new MyIterator(['a', 'b', 'c']);\nforeach ($iterator as $key => $value) {\n echo "$key: $value\\n";\n}\n// الإخراج: 0: a, 1: b, 2: c\n?>\n\nالكائنات العادية (بدون Iterator)
\n\nإذا كان الكائن لا يطبق Iterator، فإن foreach يمر عبر جميع الخصائص العامة للكائن:
<?php\nclass Person {\n public $name = 'أحمد';\n public $age = 30;\n protected $salary = 5000; // لن يظهر\n private $password = 'secret'; // لن يظهر\n}\n\n$person = new Person();\nforeach ($person as $key => $value) {\n echo "$key: $value\\n";\n}\n// الإخراج: name: أحمد, age: 30\n?>\n\nملاحظة: الخصائص المحمية (protected) والخاصة (private) لا تظهر في التكرار.
\n\nالفصل الرابع: أداء foreach ونصائح التحسين
\n\nعلى الرغم من أن foreach هو أسرع طريقة لتكرار المصفوفات في PHP، إلا أن هناك بعض النقاط التي يجب مراعاتها:
1. foreach أسرع من for
\n\nفي معظم الحالات، foreach أسرع من for مع المصفوفات، لأنه لا يحتاج إلى استدعاء count() في كل تكرار:
<?php\n// بطيء\nfor ($i = 0; $i < count($array); $i++) {\n echo $array[$i];\n}\n\n// سريع\n$count = count($array);\nfor ($i = 0; $i < $count; $i++) {\n echo $array[$i];\n}\n\n// الأسرع\nforeach ($array as $value) {\n echo $value;\n}\n?>\n\n2. تجنب النسخ غير الضروري
\n\nعند التعامل مع المصفوفات الكبيرة، استخدم المراجع إذا كنت بحاجة إلى تعديل القيم، ولكن تذكر دائمًا استخدام unset() بعد الحلقة.
3. استخدم array_map و array_filter عند الإمكان
\n\nللمهام البسيطة مثل تحويل أو تصفية المصفوفات، استخدم دوال المصفوفات المدمجة بدلاً من foreach:
<?php\n// foreach\n$result = [];\nforeach ($array as $value) {\n $result[] = $value * 2;\n}\n\n// أفضل\n$result = array_map(fn($v) => $v * 2, $array);\n?>\n\nالخلاصة: ملخص سريع
\n\n| الخاصية | \nforeach بالقيمة | \nforeach بالمرجع | \n
|---|---|---|
| تعديل المصفوفة الأصلية | \nلا | \nنعم | \n
| الأداء مع المصفوفات الكبيرة | \nأبطأ (نسخ) | \nأسرع | \n
| خطر الأخطاء | \nمنخفض | \nمرتفع (المرجع المتبقي) | \n
| الاستخدام الموصى به | \nالقراءة فقط | \nعند الحاجة للتعديل | \n
الآن أنت تعرف كل شيء عن آلية عمل foreach في PHP. استخدم هذه المعرفة لكتابة كود أكثر أمانًا وكفاءة. تذكر: المعرفة الحقيقية تأتي من الفهم العميق، وليس فقط من حفظ الصيغ!
هل لديك أسئلة أو تجارب مع foreach؟ شاركها في التعليقات أدناه!