loops شرح - كيف تعمل PHP 'foreach' بالفعل؟




key value (7)

في المثال 3 ، لا تقوم بتعديل المصفوفة. في جميع الأمثلة الأخرى تقوم بتعديل إما محتويات أو مؤشر الصفيف الداخلي. هذا أمر مهم عندما يتعلق الأمر صفائف PHP بسبب دلالات مشغل الاحالة.

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

هنا مثال:

$a = array(1,2,3);
$b = $a;  // This is lazy cloning of $a. For the time
          // being $a and $b point to the same internal
          // data structure.

$a[] = 3; // Here $a changes, which triggers the actual
          // cloning. From now on, $a and $b are two
          // different data structures. The same would
          // happen if there were a change in $b.

وبالعودة إلى حالات الاختبار الخاصة بك ، يمكنك بسهولة تخيل أن foreach تخلق نوعًا من المكرر مع الإشارة إلى الصفيف. يعمل هذا المرجع تمامًا مثل المتغير $b في المثال الخاص بي. ومع ذلك ، فإن المكرر مع المرجع يعيش فقط خلال الحلقة وبعد ذلك ، يتم تجاهلها. الآن يمكنك أن ترى أنه ، في جميع الحالات ، ولكن 3 ، يتم تعديل المصفوفة أثناء الحلقة ، في حين أن هذا المرجع الزائد على قيد الحياة. هذا يؤدي إلى استنساخ ، وهذا ما يفسر ما يحدث هنا!

هنا مقال ممتاز لأثر جانبي آخر لسلوك النسخ عند الكتابة هذا: مشغل PHP الثلاثي: سريع أم لا؟

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

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

اسمحوا لي أن أعرض ما أعنيه. بالنسبة لحالات الاختبار التالية ، سنعمل مع المصفوفة التالية:

$array = array(1, 2, 3, 4, 5);

حالة الاختبار 1 :

foreach ($array as $item) {
  echo "$item\n";
  $array[] = $item;
}
print_r($array);

/* Output in loop:    1 2 3 4 5
   $array after loop: 1 2 3 4 5 1 2 3 4 5 */

يوضح هذا بوضوح أننا لا نعمل مباشرة مع مصفوفة المصدر - وإلا ستستمر الحلقة إلى الأبد ، حيث أننا نضغط باستمرار على العناصر في الصفيف خلال الحلقة. ولكن فقط للتأكد من هذه الحالة:

حالة الاختبار 2 :

foreach ($array as $key => $item) {
  $array[$key + 1] = $item + 2;
  echo "$item\n";
}

print_r($array);

/* Output in loop:    1 2 3 4 5
   $array after loop: 1 3 4 5 6 7 */

هذا يدعم استنتاجنا الأولي ، ونحن نعمل مع نسخة من مصفوفة المصدر خلال الحلقة ، وإلا فإننا سوف نرى القيم المعدلة خلال الحلقة. لكن...

إذا نظرنا في manual ، فإننا نجد هذا البيان:

عند بدء تنفيذ foreach أولاً ، تتم إعادة تعيين مؤشر الصفيف الداخلي تلقائيًا إلى العنصر الأول من الصفيف.

صحيح ... يبدو أن هذا يشير إلى أن foreach يعتمد على مؤشر الصفيف لمجموعة الصفيف. لكننا أثبتنا أننا لا نعمل مع مصفوفة المصدر ، أليس كذلك؟ حسنًا ، ليس تمامًا.

حالة الاختبار 3 :

// Move the array pointer on one to make sure it doesn't affect the loop
var_dump(each($array));

foreach ($array as $item) {
  echo "$item\n";
}

var_dump(each($array));

/* Output
  array(4) {
    [1]=>
    int(1)
    ["value"]=>
    int(1)
    [0]=>
    int(0)
    ["key"]=>
    int(0)
  }
  1
  2
  3
  4
  5
  bool(false)
*/

لذا ، على الرغم من حقيقة أننا لا نعمل مباشرة مع مصفوفة المصدر ، فنحن نعمل مباشرة مع مؤشر مصفوفة المصدر - حقيقة أن المؤشر في نهاية الصفيف في نهاية الحلقة يُظهر ذلك. باستثناء هذا لا يمكن أن يكون صحيحًا - إذا كان كذلك ، فإن اختبار الحالة 1 سيتكرر إلى الأبد.

يوضح دليل PHP أيضًا:

كما يعتمد foreach على مؤشر الصفيف الداخلي تغييره داخل الحلقة قد يؤدي إلى سلوك غير متوقع.

حسنًا ، لنكتشف ما هو "السلوك غير المتوقع" (من الناحية الفنية ، أي سلوك غير متوقع لأنني لم أعد أعرف ما هو متوقع).

حالة الاختبار 4 :

foreach ($array as $key => $item) {
  echo "$item\n";
  each($array);
}

/* Output: 1 2 3 4 5 */

حالة الاختبار 5 :

foreach ($array as $key => $item) {
  echo "$item\n";
  reset($array);
}

/* Output: 1 2 3 4 5 */

... لا شيء غير متوقع هناك ، في الواقع يبدو أنه يدعم نظرية "نسخة المصدر".

السؤال

ما الذي يجري هنا؟ إن C-fu ليس جيدًا بما يكفي لي كي أستطيع استخلاص استنتاج سليم بمجرد النظر إلى شفرة المصدر PHP ، وسأكون ممتنا لو أن أحدا يستطيع ترجمته إلى الإنجليزية بالنسبة لي.

يبدو لي أن foreach يعمل مع نسخة من الصفيف ، ولكن يحدد مؤشر الصفيف للمصفوفة المصدر إلى نهاية الصفيف بعد الحلقة.

  • هل هذا صحيح وكل القصة؟
  • إذا لم يكن كذلك ، فما الذي تفعله حقًا؟
  • هل هناك أي موقف حيث أن استخدام الدوال التي ضبط مؤشر الصفيف ( each() ، reset() وآخرون) أثناء foreach يمكن أن يؤثر على نتائج الحلقة؟

PHP foreach loop can be used with Indexed arrays , Associative arrays and Object public variables .

In foreach loop, the first thing php does is that it creates a copy of the array which is to be iterated over. PHP then iterates over this new copy of the array rather than the original one. This is demonstrated in the below example:

<?php
$numbers = [1,2,3,4,5,6,7,8,9]; # initial values for our array
echo '<pre>', print_r($numbers, true), '</pre>', '<hr />';
foreach($numbers as $index => $number){
    $numbers[$index] = $number + 1; # this is making changes to the origial array
    echo 'Inside of the array = ', $index, ': ', $number, '<br />'; # showing data from the copied array
}
echo '<hr />', '<pre>', print_r($numbers, true), '</pre>'; # shows the original values (also includes the newly added values).

Besides this, php does allow to use iterated values as a reference to the original array value as well. This is demonstrated below:

<?php
$numbers = [1,2,3,4,5,6,7,8,9];
echo '<pre>', print_r($numbers, true), '</pre>';
foreach($numbers as $index => &$number){
    ++$number; # we are incrementing the original value
    echo 'Inside of the array = ', $index, ': ', $number, '<br />'; # this is showing the original value
}
echo '<hr />';
echo '<pre>', print_r($numbers, true), '</pre>'; # we are again showing the original value

Note: It does not allow original array indexes to be used as references .

Source: http://dwellupper.io/post/47/understanding-php-foreach-loop-with-examples


As per the documentation provided by PHP manual.

On each iteration, the value of the current element is assigned to $v and the internal
array pointer is advanced by one (so on the next iteration, you'll be looking at the next element).

So as per your first example:

$array = ['foo'=>1];
foreach($array as $k=>&$v)
{
   $array['bar']=2;
   echo($v);
}

$array have only single element, so as per the foreach execution, 1 assign to $v and it don't have any other element to move pointer

But in your second example:

$array = ['foo'=>1, 'bar'=>2];
foreach($array as $k=>&$v)
{
   $array['baz']=3;
   echo($v);
}

$array have two element, so now $array evaluate the zero indices and move the pointer by one. For first iteration of loop, added $array['baz']=3; as pass by reference.


بعض النقاط لملاحظة عند العمل مع foreach() :

أ) foreach يعمل على نسخة محتملة من الصفيف الأصلي. يعني أن foreach () سيكون لديك تخزين بيانات مشترك حتى أو ما لم يتم إنشاء prospected copy manual .

ب) ما الذي يطلق نسخة محتملة ؟ يتم إنشاء نسخة مستنيرة استناداً إلى سياسة copy-on-write ، أي ، عندما يتم تغيير مصفوفة تمرير إلى foreach () ، يتم إنشاء نسخة من الصفيف الأصلي.

c) سيكون للمصفوفة الأصلية و foreach () it DISTINCT SENTINEL VARIABLES ، أي واحد للمصفوفة الأصلية والأخرى foreach. انظر رمز الاختبار أدناه. SPL و Iterators و Array Iterator .

question كيفية التأكد من إعادة ضبط القيمة في حلقة "foreach" في PHP؟ يعالج الحالات (3 ، 4 ، 5) من سؤالك.

يوضح المثال التالي أن كل () وإعادة التعيين () لا تؤثر على متغيرات SENTINEL (for example, the current index variable) من مكرر foreach ().

$array = array(1, 2, 3, 4, 5);

list($key2, $val2) = each($array);
echo "each() Original (outside): $key2 => $val2<br/>";

foreach($array as $key => $val){
    echo "foreach: $key => $val<br/>";

    list($key2,$val2) = each($array);
    echo "each() Original(inside): $key2 => $val2<br/>";

    echo "--------Iteration--------<br/>";
    if ($key == 3){
        echo "Resetting original array pointer<br/>";
        reset($array);
    }
}

list($key2, $val2) = each($array);
echo "each() Original (outside): $key2 => $val2<br/>";

انتاج:

each() Original (outside): 0 => 1
foreach: 0 => 1
each() Original(inside): 1 => 2
--------Iteration--------
foreach: 1 => 2
each() Original(inside): 2 => 3
--------Iteration--------
foreach: 2 => 3
each() Original(inside): 3 => 4
--------Iteration--------
foreach: 3 => 4
each() Original(inside): 4 => 5
--------Iteration--------
Resetting original array pointer
foreach: 4 => 5
each() Original(inside): 0=>1
--------Iteration--------
each() Original (outside): 1 => 2

شرح (اقتباس من php.net ):

الحلقات النموذجية الأولى عبر الصفيف المعطى بواسطة array_expression. في كل تكرار ، يتم تعيين قيمة العنصر الحالي إلى قيمة $ ويتم تطوير مؤشر الصفيف الداخلي بواسطة واحد (حتى في التكرار التالي ، سوف تبحث في العنصر التالي).

لذلك ، في المثال الأول لديك عنصر واحد فقط في الصفيف ، وعندما يتم تحريك المؤشر ، لا يكون العنصر التالي موجودًا ، لذلك بعد إضافة عنصر جديد foreach ينتهي لأنه "قرر" بالفعل أنه بمثابة العنصر الأخير.

في المثال الثاني ، تبدأ بعنصرين ، ولا تكون حلقة foreach في العنصر الأخير ، لذا فهي تقوم بتقييم المصفوفة في التكرار التالي ، وبالتالي تحقق من وجود عنصر جديد في المصفوفة.

وأعتقد أن هذا كله نتيجة لكل جزء من التكرار في التفسير في الوثائق ، وهو ما يعني على الأرجح أن foreach يقوم بكل المنطق قبل أن يستدعي الرمز في {} .

حالة اختبار

إذا قمت بتشغيل هذا:

<?
    $array = Array(
        'foo' => 1,
        'bar' => 2
    );
    foreach($array as $k=>&$v) {
        $array['baz']=3;
        echo $v." ";
    }
    print_r($array);
?>

سوف تحصل على هذا الناتج:

1 2 3 Array
(
    [foo] => 1
    [bar] => 2
    [baz] => 3
)

مما يعني أنه قبل التعديل وعبره لأنه تم تعديله "في الوقت المناسب". لكن إذا فعلت هذا:

<?
    $array = Array(
        'foo' => 1,
        'bar' => 2
    );
    foreach($array as $k=>&$v) {
        if ($k=='bar') {
            $array['baz']=3;
        }
        echo $v." ";
    }
    print_r($array);
?>

You will get:

1 2 Array
(
    [foo] => 1
    [bar] => 2
    [baz] => 3
)

Which means that array was modified, but since we modified it when the foreach already was at the last element of the array, it "decided" not to loop anymore, and even though we added new element, we added it "too late" and it was not looped through.

Detailed explanation can be read at How does PHP 'foreach' actually work? which explains the internals behind this behaviour.


foreach يدعم التكرار على ثلاثة أنواع مختلفة من القيم:

  • المصفوفات
  • الأشياء العادية
  • كائنات Traversable

في ما يلي سأحاول أن أشرح بدقة كيفية عمل التكرار في الحالات المختلفة. إلى حد بعيد أبسط الحالات هي كائنات قابلة Traversable ، كما هو الحال بالنسبة لهذه foreach هو في جوهره فقط سكّر نحوي للرمز على طول هذه الخطوط:

foreach ($it as $k => $v) { /* ... */ }

/* translates to: */

if ($it instanceof IteratorAggregate) {
    $it = $it->getIterator();
}
for ($it->rewind(); $it->valid(); $it->next()) {
    $v = $it->current();
    $k = $it->key();
    /* ... */
}

بالنسبة لفئات الدرجات الداخلية ، يتم تجنب استدعاء الأسلوب الفعلي باستخدام واجهة برمجة تطبيقات داخلية تعكس بشكل أساسي واجهة Iterator على مستوى C.

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

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

حتى الان جيدة جدا. لا يمكن أن يكون التكرار فوق القاموس قاسًا جدًا ، أليس كذلك؟ تبدأ المشاكل عندما تدرك أنه يمكن تغيير صفيف / كائن أثناء التكرار. هناك طرق متعددة يمكن أن يحدث هذا:

  • إذا قمت بالتكرار بالرجوع باستخدام foreach ($arr as &$v) فسيتم تحويل $arr إلى مرجع ويمكنك تغييره أثناء التكرار.
  • في PHP 5 ينطبق الشيء نفسه حتى إذا قمت بالتكرار حسب القيمة ، ولكن الصفيف كان مرجعًا مسبقًا: $ref =& $arr; foreach ($ref as $v) $ref =& $arr; foreach ($ref as $v)
  • تحتوي الكائنات على دلالات تمرير ناجمة ، والتي لأغراض عملية يجب أن تعني أنها تتصرف مثل المراجع. لذلك يمكن دائمًا تغيير الكائنات أثناء التكرار.

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

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

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

PHP 5

مؤشر الصفيف الداخلي و HashPointer

تحتوي الصفائف الموجودة في PHP 5 على "مؤشر مصفوفة داخلي" مخصص (IAP) ، والذي يدعم التعديلات بشكل صحيح: كلما تمت إزالة عنصر ، سيكون هناك فحص ما إذا كان IAP يشير إلى هذا العنصر. إذا كان الأمر كذلك ، فهو متقدم إلى العنصر التالي بدلاً من ذلك.

في حين أن foreach يستفيد من IAP ، هناك تعقيد إضافي: يوجد IAP واحد فقط ، ولكن يمكن أن تكون مجموعة واحدة جزءًا من عدة حلقات foreach:

// Using by-ref iteration here to make sure that it's really
// the same array in both loops and not a copy
foreach ($arr as &$v1) {
    foreach ($arr as &$v) {
        // ...
    }
}

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

تكرار صفيف

تعد IAP ميزة مرئية لصفيف (معرّض من خلال مجموعة الوظائف current ) ، حيث إن مثل هذه التغييرات في عدد IAP هي تعديلات تحت دلالات النسخ عند الكتابة. هذا للأسف يعني أن foreach هو في كثير من الحالات اضطر إلى تكرار المصفوفة التي تتكرر. الشروط الدقيقة هي:

  1. المصفوفة ليست مرجعًا (is_ref = 0). إذا كان مرجعًا ، فمن المفترض أن يتم نشر التغييرات عليه ، لذا لا ينبغي أن يتم تكرار ذلك.
  2. يحتوي الصفيف على refcount> 1. إذا كان refcount هو 1 ، فإن الصفيف لا يتم مشاركته ، ونحن حرون في تعديله مباشرة.

إذا لم يكن المصفوفة مكررة (is_ref = 0 ، refcount = 1) ، فسيتم زيادة معامل الإنقاذ فقط (*). بالإضافة إلى ذلك ، إذا تم استخدام foreach حسب المرجع ، فسيتم تحويل المصفوفة (التي من المحتمل أن تكون مكررة) إلى مرجع.

خذ بعين الاعتبار هذا الرمز كمثال حيث يحدث التكرار:

function iterate($arr) {
    foreach ($arr as $v) {}
}

$outerArr = [0, 1, 2, 3, 4];
iterate($arr);

هنا ، سيتم تكرار $arr لمنع تغييرات IAP على $arr من التسرب إلى $outerArr . من حيث الشروط أعلاه ، فإن المصفوفة ليست مرجعًا (is_ref = 0) ويتم استخدامها في مكانين (refcount = 2). يعتبر هذا المطلب أمرًا مؤسفًا وعنصرًا في التنفيذ دون المستوى الأمثل (لا يوجد أي قلق بشأن التعديل أثناء التكرار هنا ، لذلك لا نحتاج حقًا إلى استخدام IAP في المقام الأول).

(*) زيادة refcount هنا يبدو غير ضار ، لكنه ينتهك دلالات النسخ عند الكتابة (COW): هذا يعني أننا سنقوم بتعديل IAP من refcount = 2 مصفوفة ، بينما COW تملي أن التعديلات يمكن تنفيذها فقط على refcount = 1 القيم. وينتج عن هذا الانتهاك تغير في سلوك المستخدم المرئي (بينما يكون COW شفافًا عادةً) ، لأن تغيير IAP على الصفيف المتكرر سيكون قابلًا للملاحظة - ولكن فقط حتى أول تعديل غير قابل للشكل داخل التطبيق على المصفوفة. بدلاً من ذلك ، كانت الخيارات الثلاثة "الصالحة" هي أ) التكرار دائمًا ، ب) عدم زيادة refcount وبالتالي السماح بتعديل الصفيف المتكرر بشكل تعسفي في الحلقة ، أو ج) لا تستخدم IAP على الإطلاق ( حل PHP 7).

موقف تقدم الطلب

توجد تفاصيل تنفيذ واحدة يجب أن تكون على دراية بها لفهم نماذج التعليمة البرمجية أدناه بشكل صحيح. قد تبدو الطريقة "الطبيعية" للتكرار من خلال بعض بنية البيانات شيئًا كهذا في pseudocode:

reset(arr);
while (get_current_data(arr, &data) == SUCCESS) {
    code();
    move_forward(arr);
}

ومع ذلك ، foreach ، كونه ندفة الثلج خاصة بدلا ، يختار أن يفعل الأشياء بشكل مختلف قليلا:

reset(arr);
while (get_current_data(arr, &data) == SUCCESS) {
    move_forward(arr);
    code();
}

أي ، يتم توجيه مؤشر الصفيف للأمام قبل تشغيل نص الحلقة. هذا يعني أنه بينما يعمل جسم الحلقة على العنصر $i ، فإن IAP يكون بالفعل في العنصر $i+1 . هذا هو السبب في أن نماذج التعليمة البرمجية التي تعرض التعديل أثناء التكرار ستؤدي دائمًا إلى إلغاء تعيين العنصر التالي ، بدلاً من العنصر الحالي.

أمثلة: حالات الاختبار الخاصة بك

يجب أن توفر لك الجوانب الثلاثة المذكورة أعلاه انطباعًا غالبًا عن الخصوصيات الخاصة بتطبيق foreach ويمكننا الانتقال إلى مناقشة بعض الأمثلة.

من السهل شرح سلوك حالات الاختبار الخاصة بك في هذه المرحلة:

  • في حالات الاختبار تبدأ $array 1 و 2 $array مع refcount = 1 ، لذلك لن يتم تكرارها من قبل foreach: يتم زيادة refcount فقط. عندما يقوم جسم الحلقة بتعديل المصفوفة (التي تتضمن refcount = 2 عند هذه النقطة) ، ستحدث التكرار في هذه النقطة. ستواصل Foreach العمل على نسخة غير معدلة من $array .

  • في حالة الاختبار 3 ، مرة أخرى لا يتم تكرار الصفيف ، وبالتالي سيتم تعديل foreach IAP لمتغير $array . في نهاية التكرار ، يكون IAP NULL (بمعنى التكرار) ، والذي يشير each بإرجاع false .

  • في كلتا الحالتين 4 و 5 كل من each reset ووظائف reset التوجيه. يحتوي $array على refcount=2 عندما يتم تمريرها إليها ، لذلك يجب أن يتم تكرارها. على هذا النحو سوف تعمل foreach على صفيف منفصل مرة أخرى.

أمثلة: آثار current في foreach

طريقة جيدة لإظهار سلوكيات الازدواجية المختلفة هي مراقبة سلوك الوظيفة current() داخل حلقة foreach. خذ بعين الاعتبار هذا المثال:

foreach ($array as $val) {
    var_dump(current($array));
}
/* Output: 2 2 2 2 2 */

هنا يجب أن تعرف أن current() هي دالة تبعية (بالفعل: prefer-ref) ، على الرغم من أنها لا تقوم بتعديل الصفيف. يجب أن يكون من أجل اللعب بشكل جيد مع جميع الوظائف الأخرى مثل next والتي هي كل من المرجع. تشير الإشارة المرجعية إلى أن المصفوفة يجب أن يتم فصلها وبالتالي سيكون $array المتباين مختلفة. كما تم ذكر سبب حصولك على 2 بدلاً من 1 أعلاه: تقدم foreach مؤشر الصفيف قبل تشغيل رمز المستخدم ، وليس بعده. لذا على الرغم من أن الشفرة موجودة في العنصر الأول ، إلا أن foreach تقدمت بالفعل بالمؤشر إلى الثاني.

يتيح الآن محاولة تعديل صغير:

$ref = &$array;
foreach ($array as $val) {
    var_dump(current($array));
}
/* Output: 2 3 4 5 false */

هنا لدينا is_ref = 1 حالة ، لذلك لا يتم نسخ المصفوفة (مثلما هو مذكور أعلاه). ولكن الآن بعد أن أصبح مرجعًا ، لم يعد من الضروري تكرار المصفوفة عند تمريرها إلى الدالة current() . وبالتالي العمل current() و foreach على نفس المجموعة. أنت لا تزال ترى السلوك خارج عن واحد ، بسبب الطريقة foreach تقدم المؤشر.

تحصل على نفس السلوك عند القيام بالتكرار باستخدام ref:

foreach ($array as &$val) {
    var_dump(current($array));
}
/* Output: 2 3 4 5 false */

هنا الجزء المهم هو أن foreach سوف يجعل $array is_ref = 1 عندما يتم تكراره بالرجوع ، لذلك في الأساس لديك نفس الوضع كما هو مذكور أعلاه.

هناك اختلاف بسيط آخر ، وهذه المرة سنقوم بتعيين الصفيف إلى متغير آخر:

$foo = $array;
foreach ($array as $val) {
    var_dump(current($array));
}
/* Output: 1 1 1 1 1 */

هنا ، تكون قيمة refcount الخاصة $array هي 2 عند بدء تشغيل الحلقة ، لذلك بمجرد أن نضطر فعلًا إلى القيام بالعمليات المسبقة. وبالتالي فإن $array المستخدمة من قبل foreach ستكون منفصلة تمامًا عن البداية. لهذا السبب تحصل على موضع IAP أينما كان قبل الحلقة (في هذه الحالة كان في الموضع الأول).

أمثلة: التعديل أثناء التكرار

محاولة حساب التعديلات أثناء التكرار هي حيث نشأت جميع مشاكل foreach ، لذلك فهو يفكر في بعض الأمثلة لهذه الحالة.

خذ بعين الاعتبار هذه الحلقات المتداخلة على نفس المصفوفة (حيث يتم استخدام التكرار بواسطة ref للتأكد من أنه هو نفسه بالفعل):

foreach ($array as &$v1) {
    foreach ($array as &$v2) {
        if ($v1 == 1 && $v2 == 1) {
            unset($array[1]);
        }
        echo "($v1, $v2)\n";
    }
}

// Output: (1, 1) (1, 3) (1, 4) (1, 5)

الجزء المتوقع هنا هو أن (1, 2) مفقود من الإخراج ، لأنه تمت إزالة العنصر 1 . ما هو غير متوقع على الأرجح هو أن الحلقة الخارجية تتوقف بعد العنصر الأول. لماذا هذا؟

السبب وراء هذا هو الاختراق المتداخل الحلقي الموضح أعلاه: قبل تشغيل هيئة الحلقة ، يتم HashPointer بنسخة احتياطية من موضع IAP الحالي وتجزئة في HashPointer . بعد جسم الحلقة ، سيتم استعادته ، ولكن فقط إذا كان العنصر لا يزال موجودًا ، وإلا فسيتم استخدام موضع IAP الحالي (مهما كان). في المثال أعلاه ، هذه هي الحالة تمامًا: تم إزالة العنصر الحالي من الحلقة الخارجية ، لذلك سيستخدم IAP ، الذي تم وضع علامة عليه بالفعل كأنه حلقة داخلية!

نتيجة أخرى لآلية استعادة + استعادة HashPointer هي أن التغييرات على IAP على الرغم من reset() وما إلى ذلك عادة لا تؤثر على foreach. على سبيل المثال ، يتم تنفيذ التعليمة البرمجية التالية كما لو كانت reset() غير موجودة على الإطلاق:

$array = [1, 2, 3, 4, 5];
foreach ($array as &$value) {
    var_dump($value);
    reset($array);
}
// output: 1, 2, 3, 4, 5

والسبب هو أنه في حين أن reset() تعدل مؤقتًا IAP ، فسيتم استعادتها إلى عنصر foreach الحالي بعد نص الحلقة. لفرض reset() لإحداث تأثير على الحلقة ، يجب عليك أيضًا إزالة العنصر الحالي ، بحيث تفشل آلية النسخ الاحتياطي / الاستعادة:

$array = [1, 2, 3, 4, 5];
$ref =& $array;
foreach ($array as $value) {
    var_dump($value);
    unset($array[1]);
    reset($array);
}
// output: 1, 1, 3, 4, 5

لكن هذه الأمثلة لا تزال عاقلة. يبدأ المتعة الحقيقي إذا كنت تتذكر أن استعادة HashPointer تستخدم مؤشر إلى العنصر وتجزئة لتحديد ما إذا كان لا يزال موجودًا. ولكن: تحتوي Hashes على تصادم ، ويمكن إعادة استخدام المؤشرات! وهذا يعني أنه ، مع اختيار دقيق لمفاتيح الصفيف ، يمكننا أن نعتقد أن foreach يعتقد أن العنصر الذي تم إزالته لا يزال موجودًا ، لذلك سوف يقفز مباشرة إليه. مثال:

$array = ['EzEz' => 1, 'EzFY' => 2, 'FYEz' => 3];
$ref =& $array;
foreach ($array as $value) {
    unset($array['EzFY']);
    $array['FYFY'] = 4;
    reset($array);
    var_dump($value);
}
// output: 1, 4

هنا يجب أن نتوقع عادة الإخراج 1, 1, 3, 4 وفقا للقواعد السابقة. كيف يحدث هو أن 'FYFY' يحتوي على نفس التجزئة مثل العنصر 'EzFY' ، ويحدث المُخصص لإعادة استخدام نفس موقع الذاكرة لتخزين العنصر. لذا ينتهي فورليتش بالقفز مباشرة إلى العنصر المدرج حديثًا ، وبالتالي تقطيع الحلقة.

استبدال الكيان المتكرر خلال الحلقة

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

$arr = [1, 2, 3, 4, 5];
$obj = (object) [6, 7, 8, 9, 10];

$ref =& $arr;
foreach ($ref as $val) {
    echo "$val\n";
    if ($val == 3) {
        $ref = $obj;
    }
}
/* Output: 1 2 3 6 7 8 9 10 */

كما ترون في هذه الحالة سوف تبدأ PHP فقط بتكرار الكيان الآخر من البداية بمجرد حدوث الاستبدال.

PHP 7

التكرارات Hashtable

إذا كنت لا تزال تتذكر ، كانت المشكلة الرئيسية في تكرار الصفيف هي كيفية التعامل مع إزالة العناصر منتصف التكرار. استخدم PHP 5 مؤشر صفيف داخلي واحد (IAP) لهذا الغرض ، والذي كان دون المستوى إلى حد ما ، حيث كان لابد من تمديد مؤشر الصفيف لدعم العديد من حلقات foreach المتزامنة والتفاعل مع reset() وما إلى ذلك.

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

هذا يعني أن foreach لن يستخدم IAP على الإطلاق . لن تكون حلقة foreach تأثيرًا مطلقًا على نتائج current() وما إلى ذلك ، ولن يؤثر سلوكها أبدًا على وظائف مثل reset() وما إلى ذلك.

تكرار صفيف

هناك تغيير مهم آخر بين PHP 5 و PHP 7 يتعلق بتكرار الصفيف. والآن بعد أن لم يعد استخدام IAP ، فإن تكرار الصفات بالقيمة القيمة سيؤدي فقط إلى زيادة refcount (بدلاً من تكرار الصفيف) في جميع الحالات. إذا تم تعديل المصفوفة أثناء حلقة foreach ، فستحدث عند هذه النقطة تكرارًا (وفقًا للنسخ عند الكتابة) وسيستمر foreach في العمل على المصفوفة القديمة.

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

$array = [1, 2, 3, 4, 5];
$ref = &$array;
foreach ($array as $val) {
    var_dump($val);
    $array[2] = 0;
}
/* Old output: 1, 2, 0, 4, 5 */
/* New output: 1, 2, 3, 4, 5 */

كان تكرار المصفوفات المرجعية سابقاً قيمة من قبل حالات خاصة. في هذه الحالة ، لم يحدث أي تكرار ، لذلك ستنعكس جميع التعديلات في الصفيف أثناء التكرار من خلال الحلقة. في PHP 7 تختفي هذه الحالة الخاصة: سيبقى التكرار بالقيمة لصفيف دائمًا يعمل على العناصر الأصلية ، متجاهلاً أي تعديلات أثناء الحلقة.

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

$obj = new stdClass;
$obj->foo = 1;
$obj->bar = 2;
foreach ($obj as $val) {
    var_dump($val);
    $obj->bar = 42;
}
/* Old and new output: 1, 42 */

ويعكس ذلك دلالات الأشياء عن طريق التعامل معها (أي أنها تتصرف على شكل مرجع حتى في سياقات ذات قيمة).

أمثلة

لنأخذ في الاعتبار بعض الأمثلة ، بدءًا بحالات الاختبار الخاصة بك:

  • تحتفظ حالات الاختبار 1 و 2 بنفس المخرجات: يعمل تكرار الصفيف بواسطة القيمة دائمًا على العمل على العناصر الأصلية. (في هذه الحالة ، يكون سلوك التكرار والتكرار هو نفسه بين PHP 5 و PHP 7).

  • تغييرات حالة الاختبار 3: لم يعد Foreach يستخدم IAP ، لذلك لا تتأثر each() بالحلقة. سيكون لها نفس الإخراج قبل وبعد.

  • تظل حالات الاختبار 4 و 5 كما هي: سيعمل each() وإعادة reset() على تكرار الصفيف قبل تغيير IAP ، بينما لا يزال foreach يستخدم الصفيف الأصلي. (ليس من المفترض أن يكون تغيير IAP مهمًا ، حتى لو تمت مشاركة الصفيف.)

المجموعة الثانية من الأمثلة كانت مرتبطة بسلوك current() تحت تكوينات مرجعية / refcounting مختلفة. لم يعد ذلك منطقيًا ، حيث أن current() غير متأثر تمامًا بالحلقة ، لذا تبقى قيمة الإرجاع دائمًا كما هي.

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

$array = [1, 2, 3, 4, 5];
foreach ($array as &$v1) {
    foreach ($array as &$v2) {
        if ($v1 == 1 && $v2 == 1) {
            unset($array[1]);
        }
        echo "($v1, $v2)\n";
    }
}

// Old output: (1, 1) (1, 3) (1, 4) (1, 5)
// New output: (1, 1) (1, 3) (1, 4) (1, 5)
//             (3, 1) (3, 3) (3, 4) (3, 5)
//             (4, 1) (4, 3) (4, 4) (4, 5)
//             (5, 1) (5, 3) (5, 4) (5, 5) 

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

هناك حالة حافة أخرى غريبة يتم إصلاحها الآن ، وهي التأثير الغريب الذي تحصل عليه عند إزالة وإضافة العناصر التي يحدث بها نفس التجزئة:

$array = ['EzEz' => 1, 'EzFY' => 2, 'FYEz' => 3];
foreach ($array as &$value) {
    unset($array['EzFY']);
    $array['FYFY'] = 4;
    var_dump($value);
}
// Old output: 1, 4
// New output: 1, 3, 4

في السابق ، قفزت آلية استعادة HashPointer مباشرة إلى العنصر الجديد ، لأنه "بدا" كما لو كان هو نفسه كعنصر إزالة (بسبب تجزئة المؤشر ومؤشره). نظرًا لأننا لم نعد نعتمد على عنصر التجزئة لأي شيء ، فهذا لم يعد مشكلة.


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

  • استفسارات غير متزامنة وغير متزامنة
  • إجراءات مخزنة إرجاع resultsets متعددة
  • تشفير (SSL)
  • ضغط

إذا كنت في حاجة إليها - فهذه هي أسباب فنية لا شكًا في الابتعاد عن امتداد mysql نحو شيء أكثر أناقة وحديثًا.

ومع ذلك ، هناك أيضًا بعض المشكلات غير الفنية ، والتي قد تجعل تجربتك أكثر صعوبة

  • مزيد من استخدام هذه الوظائف مع إصدارات PHP الحديثة سترفع إشعارات على مستوى موقوف. انهم ببساطة يمكن إيقاف.
  • في المستقبل البعيد ، يمكن إزالتها من بنية PHP الافتراضية. ليست صفقة كبيرة أيضًا ، حيث سيتم نقل امتداد mydsql إلى PECL وسيكون كل مضيف سعيدًا بتجميع PHP معه ، حيث أنهم لا يريدون أن يخسروا عملاء لديهم مواقع تعمل منذ عقود.
  • مقاومة قوية من مجتمع . في كل مرة تذكرون فيها هذه المهام الصادقة ، يقال لك أنهم يخضعون لتحريم صارم.
  • كونك متوسط ​​مستخدم PHP ، فغالبًا ما تكون فكرتك عن استخدام هذه الوظائف عرضة للخطأ والخطأ. فقط بسبب كل هذه البرامج التعليمية والكتيبات العديدة التي تعلمك بطريقة خاطئة. ليس المهام نفسها - يجب أن أؤكد عليها - ولكن الطريقة المستخدمة.

هذه المشكلة الأخيرة هي مشكلة.
لكن في رأيي ، الحل المقترح ليس أفضل.
يبدو لي حلما مثاليا للغاية أن جميع مستخدمي PHP سيتعلمون كيفية التعامل مع الاستفسارات SQL بشكل صحيح في وقت واحد. على الأرجح أنهم سيغيرون mysql_ * إلى mysqli_ * ميكانيكيًا ، تاركًا النهج نفسه . خصوصا لأن mysqli يجعل البيانات المعدة جاهزة لا يصدق مؤلمة ومزعجة.
ناهيك عن أن البيانات المعدة في الأصل لا تكفي للحماية من حقن SQL ، ولا يقدم mysqli ولا PDO حلاً.

لذا ، بدلاً من محاربة هذا الامتداد الصادق ، أفضل أن أقاتل الممارسات الخاطئة وأن أعلم الناس بالطرق الصحيحة.

أيضا ، هناك بعض الأسباب الكاذبة أو غير الهامة ، مثل

  • لا يدعم الإجراءات المخزنة (التي كنا نستخدمها mysql_query("CALL my_proc");للأعمار)
  • لا يدعم المعاملات (كما هو مذكور أعلاه)
  • لا يدعم عبارات متعددة (من يحتاجها؟)
  • ليس تحت التطوير النشط (ماذا يعني ذلك؟ هل يؤثر عليك بأي طريقة عملية؟)
  • يفتقر إلى واجهة OO (لإنشاء واحد هو مسألة عدة ساعات)
  • لا يدعم البيانات الجاهزة أو استعلامات Parametrized

آخر واحد هو نقطة مثيرة للاهتمام. على الرغم من أن امتداد mysql لا يدعم عبارات معدة أصلية ، إلا أنها غير مطلوبة للسلامة. يمكننا بسهولة تزييف البيانات المعدة باستخدام عناصر نائبة تم التعامل معها يدويًا (تمامًا كما تفعل الشركة):

function paraQuery()
{
    $args  = func_get_args();
    $query = array_shift($args);
    $query = str_replace("%s","'%s'",$query); 

    foreach ($args as $key => $val)
    {
        $args[$key] = mysql_real_escape_string($val);
    }

    $query  = vsprintf($query, $args);
    $result = mysql_query($query);
    if (!$result)
    {
        throw new Exception(mysql_error()." [$query]");
    }
    return $result;
}

$query  = "SELECT * FROM table where a=%s AND b LIKE %s LIMIT %d";
$result = paraQuery($query, $a, "%$b%", $limit);

فويلا ، كل شيء معلمات وآمنة.

لكن حسنًا ، إذا لم تعجبك العلبة الحمراء في الدليل ، فستظهر مشكلة الاختيار: mysqli أو PDO؟

حسنا ، سيكون الجواب على النحو التالي:

  • إذا كنت تفهم ضرورة استخدام طبقة تجريد قاعدة بيانات وتبحث عن واجهة برمجة تطبيقات لإنشاء واحدة ، فإن mysqli هو اختيار جيد للغاية ، حيث إنه يدعم بالفعل العديد من الميزات الخاصة بنظام mysql.
  • إذا كنت ، مثل معظم مستخدمي PHP ، تستخدم مكالمات API الأولية مباشرة في رمز التطبيق (وهو في الأساس ممارسة خاطئة) - PDO هو الخيار الوحيد ، لأن هذا الملحق لا يدعي أنه واجهة برمجة تطبيقات فقط بل هو شبه دال ، لا يزال غير مكتمل لكنه يقدم العديد من الميزات المهمة ، مع اثنين منهم يجعل PDO مميزين بشكل دقيق من mysqli:

    • على عكس mysqli ، يمكن لشركة PDO ربط العناصر النائبة حسب القيمة ، مما يجعل الاستعلامات المبنية ديناميكيًا ممكنة بدون العديد من الشاشات ذات التعليمات البرمجية الفوضويّة.
    • على عكس mysqli ، يمكن لـ PDO دائمًا إرجاع نتيجة الاستعلام في مصفوفة معتادة بسيطة ، بينما يمكن لـ mysqli القيام بذلك فقط على تثبيت mysqlnd.

لذا ، إذا كنت من مستخدمي PHP العاديين وترغب في توفير قدر كبير من الصداع عند استخدام البيانات المحلية ، فإن PDO - مرة أخرى - هي الخيار الوحيد.
ومع ذلك ، فإن شركة تنمية نفط عمان ليست رصاصة فضية أيضا ولها مصاعب.
لذلك ، كتبت الحلول لجميع المزالق الشائعة والحالات المعقدة في wiki tag لـ PDO

ومع ذلك ، فإن كل من يتحدث عن الامتدادات يفقد دائمًا حقيقتين مهمتين حول Mysqli و PDO:

  1. البيان الجاهز ليس رصاصة فضية . هناك معرفات ديناميكية لا يمكن ربطها باستخدام البيانات المعدة. هناك استعلامات ديناميكية مع عدد غير معروف من المعلمات مما يجعل الاستعلام ينشئ مهمة صعبة.

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

لذا ، فإن التبديل إلى PDO أو mysqli ليس كافياً. على المرء استخدام ORM ، أو منشئ الاستعلام ، أو أي فئة تجريد قاعدة بيانات بدلاً من استدعاء وظائف API الأولية في التعليمة البرمجية الخاصة بهم.
وعلى العكس - إذا كان لديك طبقة تجريد بين رمز التطبيق الخاص بك وبين API mysql - فإنه لا يهم في الواقع أي محرك يستخدم. يمكنك استخدام امتداد mysql حتى يتم إيقافه ثم إعادة كتابة فئة التجريد بسهولة إلى محرك آخر ، مع الاحتفاظ بكافة تعليمات برمجية التطبيق سليمة.

فيما يلي بعض الأمثلة المستندة إلى فئة safemysql الخاصة بي لإظهار كيف يجب أن تكون فئة التجريد هذه:

$city_ids = array(1,2,3);
$cities   = $db->getCol("SELECT name FROM cities WHERE is IN(?a)", $city_ids);

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

$insert = array('name' => 'John', 'surname' => "O'Hara");
$db->query("INSERT INTO users SET ?u", $insert);

قارنها بإدخالات PDO المعتادة ، عندما يتم تكرار كل اسم حقل واحد من ستة إلى عشرة مرات - في كل هذه العناصر النائبة المتعددة ، والارتباطات ، وتعريفات الاستعلام.

مثال آخر:

$data = $db->getAll("SELECT * FROM goods ORDER BY ?n", $_GET['order']);

يمكنك بالكاد العثور على مثال لشركة تنمية نفط عمان للتعامل مع مثل هذه الحالة العملية.
وسوف تكون غامضة للغاية وعلى الأرجح غير آمنة.

لذا ، مرة أخرى - لا ينبغي أن يكون مجرد سائق الخام الخاص بك القلق ولكن الطبقة التجريدية ، مفيدة ، ليس فقط لأمثلة سخيفة من دليل المبتدئين ولكن لحل أي مشاكل الحياة الحقيقية.





php loops foreach iteration php-internals