كيف يعمل foreach في PHP: تشريح الحلقة

مدرب بايثون أونلاين للمبتدئين

تعلم Python بسهولة دون إرهاق نظري. حل مهام عملية مع التحقق التلقائي واكتب الكود مباشرة في المتصفح.

ابدأ الدورة

مقدمة: foreach ليس مجرد سكر نحوي

\n\n

كل مطور PHP، من المبتدئ إلى الخبير، يستخدم foreach بشكل يومي تقريبًا. يبدو بسيطًا وواضحًا: "مرر على كل عنصر في المصفوفة". لكن هل تساءلت يومًا: كيف يقوم PHP بذلك بالضبط؟ لماذا يتصرف نفس foreach بشكل مختلف مع المصفوفات والكائنات؟ لماذا يؤدي تعديل المصفوفة داخل الحلقة أحيانًا إلى نتائج غير متوقعة؟

\n\n

سؤال "كيف يعمل foreach في PHP فعليًا؟" على Stack Overflow حصد عشرات الآلاف من المشاهدات ومئات الأصوات. هذا ليس من قبيل الصدفة: تحت غطاء هذا العامل البسيط ظاهريًا، يختبئ آلية معقدة وأنيقة. اليوم سننظر في قلب PHP ونحلل تشريح حلقة foreach خطوة بخطوة.

\n\n

بنهاية هذه المقالة، لن تفهم فقط كيف يعمل foreach، بل ستتعلم أيضًا تجنب الأخطاء النموذجية التي تقع فيها حتى المبرمجين ذوي الخبرة. هيا بنا!

\n\n

الفصل الأول: foreach ومؤشر المصفوفة الداخلي

\n\n

عندما يواجه PHP البنية foreach ($array as $value)، فإنه لا يمر ببساطة عبر الفهارس. أساس العمل هو مؤشر المصفوفة الداخلي (internal array pointer). هذا "سهم" غير مرئي يشير إلى العنصر الحالي.

\n\n

إليك كيف تبدو هذه العملية خطوة بخطوة:

\n\n
    \n
  1. إعادة تعيين المؤشر. يستدعي PHP الدالة reset($array) — يصبح المؤشر على العنصر الأول.
  2. \n
  3. قراءة المفتاح والقيمة. يتم أخذ العنصر الحالي من المصفوفة.
  4. \n
  5. التعيين. يتم نسخ القيمة إلى المتغير $value (إذا لم يكن هناك &).
  6. \n
  7. تحريك المؤشر. يقوم PHP بتحريك المؤشر إلى العنصر التالي.
  8. \n
  9. التكرار. يتم تنفيذ الخطوات 2-4 حتى يصل المؤشر إلى نهاية المصفوفة.
  10. \n
\n\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 يعمل مع نسخة من المؤشر الداخلي، وليس مع الأصل. تم تصميم هذا عمدًا لمنع تعديل المصفوفة داخل الحلقة من كسر التكرار.

\n\n

الفصل الثاني: foreach بالقيمة مقابل بالمرجع

\n\n

المصدر الأكثر شيوعًا للارتباك هو الفرق بين foreach ($array as $value) و foreach ($array as &$value). دعنا نوضح كل شيء.

\n\n

foreach بالقيمة: نسخ في كل خطوة

\n\n

عندما تكتب foreach ($array as $value)، يقوم PHP في كل تكرار بنقل القيمة الحالية للمصفوفة إلى المتغير $value. هذا يعني أن تغيير $value لا يؤثر على المصفوفة الأصلية.

\n\n
<?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) فقط، وليس الكائنات نفسها.

\n\n

foreach بالمرجع: العمل مع الأصل

\n\n

الصيغة foreach ($array as &$value) تجعل PHP يمرر مرجعًا إلى العنصر، وليس نسخة. الآن $value هو اسم مستعار لعنصر المصفوفة.

\n\n
<?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

المشكلة الشهيرة: المرجع المتبقي

\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) بعد الحلقة الأولى لقطع الاتصال المرجعي.

\n\n

الفصل الثالث: foreach مع الكائنات

\n\n

عند استخدام foreach مع الكائنات، يتغير السلوك بشكل جذري. بدلاً من مؤشر المصفوفة الداخلي، يستخدم PHP واجهة Iterator أو الخصائص العامة للكائن.

\n\n

الكائنات التي تطبق Iterator

\n\n

إذا كان الكائن يطبق واجهة Iterator، فإن foreach يستدعي طرقها:

\n\n
    \n
  • rewind() — إعادة التعيين إلى البداية
  • \n
  • valid() — التحقق من وجود عنصر حالي
  • \n
  • current() — الحصول على القيمة الحالية
  • \n
  • key() — الحصول على المفتاح الحالي
  • \n
  • next() — الانتقال إلى العنصر التالي
  • \n
\n\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 يمر عبر جميع الخصائص العامة للكائن:

\n\n
<?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، إلا أن هناك بعض النقاط التي يجب مراعاتها:

\n\n

1. foreach أسرع من for

\n\n

في معظم الحالات، foreach أسرع من for مع المصفوفات، لأنه لا يحتاج إلى استدعاء count() في كل تكرار:

\n\n
<?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\n

2. تجنب النسخ غير الضروري

\n\n

عند التعامل مع المصفوفات الكبيرة، استخدم المراجع إذا كنت بحاجة إلى تعديل القيم، ولكن تذكر دائمًا استخدام unset() بعد الحلقة.

\n\n

3. استخدم array_map و array_filter عند الإمكان

\n\n

للمهام البسيطة مثل تحويل أو تصفية المصفوفات، استخدم دوال المصفوفات المدمجة بدلاً من foreach:

\n\n
<?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\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
الخاصيةforeach بالقيمةforeach بالمرجع
تعديل المصفوفة الأصليةلانعم
الأداء مع المصفوفات الكبيرةأبطأ (نسخ)أسرع
خطر الأخطاءمنخفضمرتفع (المرجع المتبقي)
الاستخدام الموصى بهالقراءة فقطعند الحاجة للتعديل
\n\n

الآن أنت تعرف كل شيء عن آلية عمل foreach في PHP. استخدم هذه المعرفة لكتابة كود أكثر أمانًا وكفاءة. تذكر: المعرفة الحقيقية تأتي من الفهم العميق، وليس فقط من حفظ الصيغ!

\n\n

هل لديك أسئلة أو تجارب مع foreach؟ شاركها في التعليقات أدناه!

المدونات

توصيات الكتب