Buggy PHP Code: الأخطاء العشرة الأكثر شيوعًا التي يرتكبها مطورو PHP
نشرت: 2022-03-11يجعل PHP من السهل نسبيًا إنشاء نظام مستند إلى الويب ، وهو سبب كبير لشعبيته. ولكن على الرغم من سهولة استخدامها ، فقد تطورت PHP إلى لغة معقدة تمامًا مع العديد من الأطر والفروق الدقيقة والدقة التي يمكن أن تقضم المطورين ، مما يؤدي إلى ساعات من تصحيح أخطاء نتف الشعر. تسلط هذه المقالة الضوء على عشرة من الأخطاء الأكثر شيوعًا التي يحتاج مطورو PHP إلى الحذر منها.
الخطأ الشائع الأول: ترك مراجع المصفوفات المتدلية بعد حلقات foreach
ألست متأكدًا من كيفية استخدام حلقات foreach في PHP؟ يمكن أن يكون استخدام المراجع في حلقات foreach
مفيدة إذا كنت تريد العمل على كل عنصر في المصفوفة التي تقوم بالتكرار عليها. علي سبيل المثال:
$arr = array(1, 2, 3, 4); foreach ($arr as &$value) { $value = $value * 2; } // $arr is now array(2, 4, 6, 8)
المشكلة هي أنه إذا لم تكن حريصًا ، فقد يكون لهذا أيضًا بعض الآثار الجانبية غير المرغوب فيها والعواقب. على وجه التحديد ، في المثال أعلاه ، بعد تنفيذ الكود ، ستبقى $value
في النطاق وستحتفظ بمرجع إلى العنصر الأخير في المصفوفة. يمكن أن تنتهي العمليات اللاحقة التي تتضمن $value
عن غير قصد بتعديل العنصر الأخير في المصفوفة.
الشيء الرئيسي الذي يجب تذكره هو أن foreach
لا ينشئ نطاقًا. وبالتالي ، فإن $value
في المثال أعلاه هي مرجع ضمن النطاق العلوي للبرنامج النصي. في كل تكرار يقوم foreach
بتعيين المرجع للإشارة إلى العنصر التالي في $array
. بعد اكتمال الحلقة ، لا تزال $value
تشير إلى العنصر الأخير في $array
وتبقى في النطاق.
في ما يلي مثال على نوع الأخطاء المراوغة والمربكة التي يمكن أن يؤدي إليها ذلك:
$array = [1, 2, 3]; echo implode(',', $array), "\n"; foreach ($array as &$value) {} // by reference echo implode(',', $array), "\n"; foreach ($array as $value) {} // by value (ie, copy) echo implode(',', $array), "\n";
سيخرج الكود أعلاه ما يلي:
1,2,3 1,2,3 1,2,2
لا ، هذا ليس خطأ مطبعي. القيمة الأخيرة في السطر الأخير هي بالفعل 2 وليس 3.
لماذا ا؟
بعد المرور عبر حلقة foreach
الأولى ، تظل $array
بدون تغيير ولكن ، كما هو موضح أعلاه ، تُترك $value
كمرجع متدلي إلى العنصر الأخير في $array
(نظرًا لأن حلقة foreach
تم الوصول إليها $value
بالمرجع ).
نتيجة لذلك ، عندما نمر عبر حلقة foreach
الثانية ، يبدو أن "أشياء غريبة" تحدث. على وجه التحديد ، نظرًا لأنه يتم الآن الوصول إلى $value
بالقيمة (على سبيل المثال ، عن طريق النسخ ) ، فإن foreach
ينسخ كل عنصر $array
متسلسلة إلى $value
في كل خطوة من الحلقة. نتيجة لذلك ، إليك ما يحدث أثناء كل خطوة من حلقة foreach
الثانية:
- تمرير 1: ينسخ
$array[0]
(على سبيل المثال ، "1") إلى$value
(وهي مرجع إلى$array[2]
) ، لذا فإن$array[2]
الآن يساوي 1. لذا فإن$array
يحتوي الآن على [1، 2 ، 1]. - تمرير 2: نسخ
$array[1]
(أي "2") إلى$value
(وهي مرجع إلى$array[2]
) ، لذا فإن$array[2]
الآن يساوي 2. لذا فإن$array
يحتوي الآن على [1، 2 ، 2]. - التمرير 3: نسخ
$array[2]
(التي تساوي الآن "2") إلى$value
(وهي مرجع إلى$array[2]
) ، لذا فإن$array[2]
لا يزال يساوي 2. لذلك تحتوي$array
الآن على [1 ، 2 ، 2].
للاستمرار في الاستفادة من استخدام المراجع في حلقات foreach
دون المخاطرة بهذه الأنواع من المشاكل ، قم باستدعاء unset()
على المتغير ، مباشرة بعد حلقة foreach
، لإزالة المرجع ؛ على سبيل المثال:
$arr = array(1, 2, 3, 4); foreach ($arr as &$value) { $value = $value * 2; } unset($value); // $value no longer references $arr[3]
الخطأ الشائع الثاني: سوء فهم سلوك isset()
على الرغم من اسمها ، فإن isset()
لا يعرض القيمة false إذا لم يكن العنصر موجودًا فحسب ، بل يُرجع أيضًا القيمة false
للقيم null
.
هذا السلوك أكثر إشكالية مما قد يبدو للوهلة الأولى وهو مصدر مشترك للمشاكل.
ضع في اعتبارك ما يلي:
$data = fetchRecordFromStorage($storage, $identifier); if (!isset($data['keyShouldBeSet']) { // do something here if 'keyShouldBeSet' is not set }
من المفترض أن مؤلف هذا الرمز أراد التحقق مما إذا كان keyShouldBeSet
قد تم تعيينه في $data
. ولكن ، كما تمت مناقشته ، فإن isset($data['keyShouldBeSet'])
سيعود أيضًا خطأ إذا تم تعيين $data['keyShouldBeSet']
، ولكن تم ضبطه على قيمة null
. لذا فإن المنطق أعلاه معيب.
إليك مثال آخر:
if ($_POST['active']) { $postData = extractSomething($_POST); } // ... if (!isset($postData)) { echo 'post not active'; }
يفترض الكود أعلاه أنه في حالة إرجاع $_POST['active']
إلى القيمة " true
" ، فسيتم تعيين postData
بالضرورة ، وبالتالي سترجع مجموعة isset($postData)
إلى القيمة " true
". لذلك ، على العكس من ذلك ، تفترض الكود أعلاه أن الطريقة الوحيدة التي isset($postData)
false
هي إذا كان $_POST['active']
قد أرجع القيمة false
أيضًا.
لا.
كما أوضحنا ، فإن isset($postData)
سيعيد القيمة false
إذا تم ضبط $postData
على قيمة null
. لذلك من الممكن لـ isset($postData)
إرجاع القيمة false
حتى إذا أعاد $_POST['active']
القيمة true
. مرة أخرى ، المنطق أعلاه معيب.
وبالمناسبة ، كنقطة جانبية ، إذا كان القصد من الكود أعلاه هو التحقق مرة أخرى مما إذا كان $_POST['active']
قد عاد بشكل صحيح ، بالاعتماد على isset()
لأن هذا كان قرار ترميز ضعيف في أي حال. بدلاً من ذلك ، كان من الأفضل إعادة فحص $_POST['active']
؛ بمعنى آخر:
if ($_POST['active']) { $postData = extractSomething($_POST); } // ... if ($_POST['active']) { echo 'post not active'; }
بالنسبة للحالات ، على الرغم من ذلك ، حيث يكون من المهم التحقق مما إذا تم تعيين متغير بالفعل (أي للتمييز بين المتغير الذي لم يتم تعيينه والمتغير الذي تم تعيينه على قيمة null
) ، فإن طريقة array_key_exists()
هي أكثر قوة المحلول.
على سبيل المثال ، يمكننا إعادة كتابة أول مثالين أعلاه على النحو التالي:
$data = fetchRecordFromStorage($storage, $identifier); if (! array_key_exists('keyShouldBeSet', $data)) { // do this if 'keyShouldBeSet' isn't set }
علاوة على ذلك ، من خلال الجمع بين array_key_exists()
get_defined_vars()
، يمكننا التحقق بشكل موثوق مما إذا كان قد تم تعيين متغير ضمن النطاق الحالي أم لا:
if (array_key_exists('varShouldBeSet', get_defined_vars())) { // variable $varShouldBeSet exists in current scope }
الخطأ الشائع الثالث: الارتباك حول الرجوع بالمرجع مقابل القيمة
ضع في اعتبارك مقتطف الشفرة هذا:
class Config { private $values = []; public function getValues() { return $this->values; } } $config = new Config(); $config->getValues()['test'] = 'test'; echo $config->getValues()['test'];
إذا قمت بتشغيل الكود أعلاه ، فستحصل على ما يلي:
PHP Notice: Undefined index: test in /path/to/my/script.php on line 21
ماذا دهاك؟
المشكلة هي أن الكود أعلاه يخلط بين المصفوفات المرتجعة بالرجوع إلى المصفوفات المرتجعة حسب القيمة. ما لم تخبر PHP صراحةً بإرجاع مصفوفة بالمرجع (على سبيل المثال ، باستخدام &
) ، فإن PHP ستعيد المصفوفة افتراضيًا "حسب القيمة". هذا يعني أنه سيتم إرجاع نسخة من المصفوفة ، وبالتالي فإن الوظيفة التي تم استدعاؤها والمستدعي لن يصلوا إلى نفس مثيل المصفوفة.
لذا فإن الاستدعاء أعلاه لـ getValues()
يُرجع نسخة من مصفوفة $values
بدلاً من مرجع لها. مع وضع ذلك في الاعتبار ، دعنا نعيد النظر في السطرين الرئيسيين من المثال أعلاه:
// getValues() returns a COPY of the $values array, so this adds a 'test' element // to a COPY of the $values array, but not to the $values array itself. $config->getValues()['test'] = 'test'; // getValues() again returns ANOTHER COPY of the $values array, and THIS copy doesn't // contain a 'test' element (which is why we get the "undefined index" message). echo $config->getValues()['test'];
قد يكون أحد الحلول الممكنة هو حفظ النسخة الأولى من مصفوفة $values
التي يتم إرجاعها بواسطة getValues()
ثم العمل على تلك النسخة لاحقًا ؛ على سبيل المثال:
$vals = $config->getValues(); $vals['test'] = 'test'; echo $vals['test'];
سيعمل هذا الرمز بشكل جيد (على سبيل المثال ، سيخرج test
بدون إنشاء أي رسالة "فهرس غير محدد") ، ولكن اعتمادًا على ما تحاول تحقيقه ، قد يكون هذا الأسلوب مناسبًا وقد لا يكون كذلك. على وجه الخصوص ، لن يقوم الكود أعلاه بتعديل مصفوفة $values
الأصلية. لذلك إذا كنت تريد أن تؤثر تعديلاتك (مثل إضافة عنصر "اختبار") على المصفوفة الأصلية ، فستحتاج بدلاً من ذلك إلى تعديل وظيفة getValues()
لإرجاع مرجع إلى مصفوفة $values
نفسها. يتم ذلك عن طريق إضافة &
قبل اسم الوظيفة ، مما يشير إلى أنه يجب إرجاع مرجع ؛ بمعنى آخر:
class Config { private $values = []; // return a REFERENCE to the actual $values array public function &getValues() { return $this->values; } } $config = new Config(); $config->getValues()['test'] = 'test'; echo $config->getValues()['test'];
سيتم test
ناتج هذا ، كما هو متوقع.
ولكن لجعل الأمور أكثر إرباكًا ، ضع في اعتبارك بدلاً من ذلك مقتطف الشفرة التالي:
class Config { private $values; // using ArrayObject rather than array public function __construct() { $this->values = new ArrayObject(); } public function getValues() { return $this->values; } } $config = new Config(); $config->getValues()['test'] = 'test'; echo $config->getValues()['test'];
إذا كنت تعتقد أن هذا سيؤدي إلى نفس خطأ "الفهرس غير المحدد" مثل مثال array
السابق ، فأنت مخطئ. في الواقع ، سيعمل هذا الرمز بشكل جيد. والسبب هو أنه على عكس المصفوفات ، فإن PHP تمرر الكائنات دائمًا بالمرجع . ( ArrayObject
هو كائن SPL ، والذي يحاكي استخدام المصفوفات بالكامل ، ولكنه يعمل ككائن.)
كما توضح هذه الأمثلة ، ليس من الواضح دائمًا في PHP ما إذا كنت تتعامل مع نسخة أو مرجع. لذلك من الضروري فهم هذه السلوكيات الافتراضية (على سبيل المثال ، يتم تمرير المتغيرات والمصفوفات بالقيمة ؛ يتم تمرير الكائنات بالمرجع) وأيضًا التحقق بعناية من وثائق API للوظيفة التي تستدعيها لمعرفة ما إذا كانت تعيد قيمة أم لا ، نسخة من مصفوفة ، مرجع إلى مصفوفة ، أو مرجع إلى كائن.
بعد كل ما قيل ، من المهم ملاحظة أن ممارسة إرجاع مرجع إلى مصفوفة أو كائن ArrayObject
بشكل عام شيء يجب تجنبه ، لأنه يوفر للمتصل القدرة على تعديل البيانات الخاصة للمثيل. هذا "يطير في الوجه" من التغليف. بدلاً من ذلك ، من الأفضل استخدام "أدوات" و "أدوات ضبط" بالنمط القديم ، على سبيل المثال:
class Config { private $values = []; public function setValue($key, $value) { $this->values[$key] = $value; } public function getValue($key) { return $this->values[$key]; } } $config = new Config(); $config->setValue('testKey', 'testValue'); echo $config->getValue('testKey'); // echos 'testValue'
يمنح هذا الأسلوب المتصل القدرة على تعيين أو الحصول على أي قيمة في المصفوفة دون توفير وصول عام إلى مصفوفة $values
الخاصة بخلاف ذلك.
الخطأ الشائع الرابع: تنفيذ الاستعلامات في حلقة
ليس من غير المألوف أن تصادف شيئًا كهذا إذا كان PHP الخاص بك لا يعمل:
$models = []; foreach ($inputValues as $inputValue) { $models[] = $valueRepository->findByValue($inputValue); }
على الرغم من أنه قد لا يكون هناك أي خطأ على الإطلاق هنا ، ولكن إذا اتبعت المنطق في الكود ، فقد تجد أن الاستدعاء البريء أعلاه إلى $valueRepository->findByValue()
ينتج عنه في النهاية استعلام من نوع ما ، مثل:
$result = $connection->query("SELECT `x`,`y` FROM `values` WHERE `value`=" . $inputValue);
ونتيجة لذلك ، فإن كل تكرار للحلقة أعلاه سيؤدي إلى استعلام منفصل لقاعدة البيانات. لذلك ، إذا قمت ، على سبيل المثال ، بتوفير مصفوفة من 1000 قيمة للحلقة ، فسيؤدي ذلك إلى إنشاء 1000 استعلام منفصل للمورد! إذا تم استدعاء مثل هذا البرنامج النصي في سلاسل رسائل متعددة ، فمن المحتمل أن يؤدي إلى توقف النظام عن الطحن.
لذلك من الأهمية بمكان التعرف على وقت إجراء الاستعلامات بواسطة التعليمات البرمجية الخاصة بك ، وكلما أمكن ، قم بجمع القيم ثم تشغيل استعلام واحد لجلب جميع النتائج.
أحد الأمثلة على مكان شائع إلى حد ما لمواجهة الاستعلام الذي يتم إجراؤه بشكل غير فعال (على سبيل المثال ، في حلقة) هو عندما يتم نشر نموذج بقائمة من القيم (المعرفات ، على سبيل المثال). بعد ذلك ، لاسترداد بيانات السجل الكاملة لكل من المعرفات ، سوف يمر الكود عبر المصفوفة ويقوم باستعلام SQL منفصل لكل معرف. سيبدو هذا غالبًا على النحو التالي:
$data = []; foreach ($ids as $id) { $result = $connection->query("SELECT `x`, `y` FROM `values` WHERE `id` = " . $id); $data[] = $result->fetch_row(); }
ولكن يمكن إنجاز الشيء نفسه بشكل أكثر كفاءة في استعلام SQL واحد على النحو التالي:
$data = []; if (count($ids)) { $result = $connection->query("SELECT `x`, `y` FROM `values` WHERE `id` IN (" . implode(',', $ids)); while ($row = $result->fetch_row()) { $data[] = $row; } }
لذلك من الأهمية بمكان التعرف على وقت إجراء الاستعلامات ، إما بشكل مباشر أو غير مباشر ، من خلال التعليمات البرمجية الخاصة بك. كلما أمكن ، اجمع القيم ثم قم بتشغيل استعلام واحد لجلب جميع النتائج. ومع ذلك ، يجب توخي الحذر هناك أيضًا ، مما يقودنا إلى خطأ PHP الشائع التالي ...
الخطأ الشائع # 5: استخدام الذاكرة الزيف وعدم الكفاءة
في حين أن جلب العديد من السجلات مرة واحدة هو بالتأكيد أكثر كفاءة من تشغيل استعلام واحد لكل صف يتم جلبه ، فمن المحتمل أن يؤدي هذا النهج إلى حالة "نفاد الذاكرة" في libmysqlclient
عند استخدام امتداد mysql
الخاص بـ PHP.
للتوضيح ، دعنا نلقي نظرة على مربع اختبار بموارد محدودة (512 ميجا بايت من ذاكرة الوصول العشوائي) و MySQL و php-cli
.
سنقوم بتشغيل جدول قاعدة بيانات مثل هذا:
// connect to mysql $connection = new mysqli('localhost', 'username', 'password', 'database'); // create table of 400 columns $query = 'CREATE TABLE `test`(`id` INT NOT NULL PRIMARY KEY AUTO_INCREMENT'; for ($col = 0; $col < 400; $col++) { $query .= ", `col$col` CHAR(10) NOT NULL"; } $query .= ');'; $connection->query($query); // write 2 million rows for ($row = 0; $row < 2000000; $row++) { $query = "INSERT INTO `test` VALUES ($row"; for ($col = 0; $col < 400; $col++) { $query .= ', ' . mt_rand(1000000000, 9999999999); } $query .= ')'; $connection->query($query); }
حسنًا ، دعنا الآن نتحقق من استخدام الموارد:
// connect to mysql $connection = new mysqli('localhost', 'username', 'password', 'database'); echo "Before: " . memory_get_peak_usage() . "\n"; $res = $connection->query('SELECT `x`,`y` FROM `test` LIMIT 1'); echo "Limit 1: " . memory_get_peak_usage() . "\n"; $res = $connection->query('SELECT `x`,`y` FROM `test` LIMIT 10000'); echo "Limit 10000: " . memory_get_peak_usage() . "\n";
انتاج:

Before: 224704 Limit 1: 224704 Limit 10000: 224704
بارد. يبدو أن الاستعلام تتم إدارته داخليًا بأمان من حيث الموارد.
وللتأكد فقط ، دعنا نزيد الحد مرة أخرى ونضبطه على 100000. عذرًا. عندما نفعل ذلك ، نحصل على:
PHP Warning: mysqli::query(): (HY000/2013): Lost connection to MySQL server during query in /root/test.php on line 11
ماذا حدث؟
المشكلة هنا هي طريقة عمل وحدة mysql
الخاصة بـ PHP. إنه حقًا مجرد وكيل libmysqlclient
، الذي يقوم بالعمل القذر. عند تحديد جزء من البيانات ، ينتقل مباشرة إلى الذاكرة. نظرًا لأن هذه الذاكرة لا تتم إدارتها من قبل مدير PHP ، فلن تظهر memory_get_peak_usage()
أي زيادة في استخدام الموارد نظرًا لأننا نرفع الحد الأقصى في استعلامنا. يؤدي هذا إلى مشاكل مثل تلك الموضحة أعلاه حيث يتم خداعنا إلى الرضا عن الذات معتقدين أن إدارة ذاكرتنا على ما يرام. لكن في الواقع ، إدارة ذاكرتنا معيبة بشكل خطير ويمكن أن نواجه مشاكل مثل تلك الموضحة أعلاه.
يمكنك على الأقل تجنب التزييف أعلاه (على الرغم من أنه لن يؤدي في حد ذاته إلى تحسين استخدام الذاكرة) باستخدام وحدة mysqlnd
بدلاً من ذلك. يتم تجميع mysqlnd
على أنه امتداد PHP أصلي ويستخدم مدير ذاكرة PHP.
لذلك ، إذا أجرينا الاختبار أعلاه باستخدام mysqlnd
بدلاً من mysql
، نحصل على صورة أكثر واقعية لاستخدام الذاكرة لدينا:
Before: 232048 Limit 1: 324952 Limit 10000: 32572912
بالمناسبة ، إنه أسوأ من ذلك. وفقًا لوثائق PHP ، تستخدم mysql
ضعف عدد الموارد الموجودة في mysqlnd
لتخزين البيانات ، لذا فإن النص الأصلي الذي يستخدم mysql
استخدم بالفعل ذاكرة أكبر مما هو موضح هنا (ضعف ذلك العدد تقريبًا).
لتجنب مثل هذه المشاكل ، ضع في اعتبارك تحديد حجم استعلاماتك واستخدام حلقة مع عدد صغير من التكرارات ؛ على سبيل المثال:
$totalNumberToFetch = 10000; $portionSize = 100; for ($i = 0; $i <= ceil($totalNumberToFetch / $portionSize); $i++) { $limitFrom = $portionSize * $i; $res = $connection->query( "SELECT `x`,`y` FROM `test` LIMIT $limitFrom, $portionSize"); }
عندما نفكر في كل من خطأ PHP هذا والخطأ رقم 4 أعلاه ، فإننا ندرك أن هناك توازنًا صحيًا يجب أن تحققه التعليمات البرمجية بشكل مثالي بين ، من ناحية ، أن تكون استفساراتك دقيقة للغاية ومتكررة ، مقابل الحصول على كل منها الاستعلامات الفردية كبيرة جدًا. كما هو الحال مع معظم الأشياء في الحياة ، هناك حاجة إلى التوازن ؛ أي من الحالات القصوى ليس جيدًا ويمكن أن يسبب مشاكل مع PHP لا تعمل بشكل صحيح.
الخطأ الشائع السادس: تجاهل مشكلات Unicode / UTF-8
بمعنى ما ، هذه مشكلة في PHP نفسها أكثر من كونها مشكلة قد تواجهها أثناء تصحيح أخطاء PHP ، لكن لم يتم التعامل معها بشكل كافٍ. كان من المقرر جعل نواة PHP 6 مدركة لـ Unicode ، ولكن تم تعليق ذلك عندما تم تعليق تطوير PHP 6 مرة أخرى في عام 2010.
لكن هذا لا يعفي المطور بأي حال من الأحوال من تسليم UTF-8 بشكل صحيح وتجنب الافتراض الخاطئ بأن جميع السلاسل ستكون بالضرورة "ASCII قديمًا عاديًا". الكود الذي يفشل في التعامل بشكل صحيح مع السلاسل غير ASCII معروف بإدخاله أخطاء heisenbugs في التعليمات البرمجية الخاصة بك. حتى مكالمات strlen($_POST['name'])
يمكن أن تسبب مشاكل إذا حاول شخص ما باسم العائلة مثل "Schrodinger" التسجيل في نظامك.
فيما يلي قائمة تحقق صغيرة لتجنب مثل هذه المشاكل في التعليمات البرمجية الخاصة بك:
- إذا كنت لا تعرف الكثير عن Unicode و UTF-8 ، فيجب أن تتعلم الأساسيات على الأقل. هناك كتاب تمهيدي رائع هنا.
- تأكد دائمًا من استخدام وظائف
mb_*
بدلاً من وظائف السلسلة القديمة (تأكد من تضمين امتداد "multibyte" في إصدار PHP الخاص بك). - تأكد من ضبط قاعدة البيانات والجداول الخاصة بك على استخدام Unicode (لا تزال العديد من بنيات MySQL تستخدم
latin1
افتراضيًا). - تذكر أن
json_encode()
يحول الرموز غير ASCII (على سبيل المثال ، "Schrodinger" تصبح "Schr \ u00f6dinger") لكنserialize()
لا يفعل ذلك. - تأكد من أن ملفات كود PHP الخاصة بك مشفرة أيضًا UTF-8 لتجنب الاصطدامات عند ربط السلاسل مع ثوابت السلسلة المشفرة أو المكونة.
أحد الموارد القيمة بشكل خاص في هذا الصدد هو UTF-8 Primer لـ PHP و MySQL منشور بواسطة فرانسيسكو كلاريا على هذه المدونة.
الخطأ الشائع السابع: افتراض أن $_POST
يحتوي دائمًا على بيانات POST الخاصة بك
على الرغم من اسمها ، فإن مصفوفة $_POST
لن تحتوي دائمًا على بيانات POST ويمكن العثور عليها بسهولة فارغة. لفهم هذا ، دعنا نلقي نظرة على مثال. افترض أننا نجري طلب خادم باستخدام استدعاء jQuery.ajax()
على النحو التالي:
// js $.ajax({ url: 'http://my.site/some/path', method: 'post', data: JSON.stringify({a: 'a', b: 'b'}), contentType: 'application/json' });
(بالمناسبة ، لاحظ contentType: 'application/json'
هنا. نحن نرسل البيانات بتنسيق JSON ، وهو أمر شائع جدًا لواجهات برمجة التطبيقات. إنه الإعداد الافتراضي ، على سبيل المثال ، للنشر في خدمة AngularJS $http
.)
على جانب الخادم في مثالنا ، نقوم ببساطة بتفريغ المصفوفة $_POST
:
// php var_dump($_POST);
والمثير للدهشة أن النتيجة ستكون:
array(0) { }
لماذا ا؟ ماذا حدث لسلسلة JSON {a: 'a', b: 'b'}
؟
الإجابة هي أن PHP تقوم فقط بتحليل حمولة POST تلقائيًا عندما تحتوي على نوع محتوى application/x-www-form-urlencoded
أو multipart/form-data
. أسباب ذلك تاريخية - كان هذان النوعان من المحتوى هما النوعان الوحيدان اللذان تم استخدامهما منذ سنوات عندما تم تنفيذ $_POST
في PHP. لذلك مع أي نوع محتوى آخر (حتى تلك التي تحظى بشعبية كبيرة اليوم ، مثل application/json
) ، لا تقوم PHP تلقائيًا بتحميل حمولة POST.
نظرًا لأن $_POST
هو عالمي خارق ، إذا تجاوزناه مرة واحدة (ويفضل أن يكون ذلك مبكرًا في البرنامج النصي الخاص بنا) ، فستكون القيمة المعدلة (على سبيل المثال ، بما في ذلك حمولة POST) قابلة للإحالة في جميع أنحاء الكود الخاص بنا. هذا مهم لأن $_POST
يُستخدم بشكل شائع بواسطة أطر PHP وجميع البرامج النصية المخصصة تقريبًا لاستخراج بيانات الطلب وتحويلها.
لذلك ، على سبيل المثال ، عند معالجة حمولة POST بنوع محتوى application/json
، نحتاج إلى تحليل محتويات الطلب يدويًا (على سبيل المثال ، فك تشفير بيانات JSON) وتجاوز المتغير $_POST
، على النحو التالي:
// php $_POST = json_decode(file_get_contents('php://input'), true);
ثم عندما نتخلص من مصفوفة $_POST
، نرى أنها تتضمن حمولة POST بشكل صحيح ؛ على سبيل المثال:
array(2) { ["a"]=> string(1) "a" ["b"]=> string(1) "b" }
الخطأ الشائع # 8: التفكير في أن PHP تدعم نوع بيانات حرف
انظر إلى هذا النموذج من التعليمات البرمجية وحاول تخمين ما ستتم طباعته:
for ($c = 'a'; $c <= 'z'; $c++) { echo $c . "\n"; }
إذا أجبت بـ "a" إلى "z" ، فقد تفاجأ بمعرفة أنك كنت مخطئًا.
نعم ، سيطبع "a" إلى "z" ، ولكنه سيطبع بعد ذلك أيضًا "aa" إلى "yz". دعنا نرى لماذا.
في PHP لا يوجد نوع بيانات char
. string
الوحيدة المتاحة. مع أخذ ذلك في الاعتبار ، تؤدي زيادة string
z
في PHP إلى إنتاج aa
:
php> $c = 'z'; echo ++$c . "\n"; aa
ومع ذلك ، لمزيد من الخلط بين الأمور ، فإن aa
أقل معجمًا من z
:
php> var_export((boolean)('aa' < 'z')) . "\n"; true
هذا هو السبب في أن نموذج الكود المعروض أعلاه يطبع الأحرف من a
إلى z
، ولكنه يطبع أيضًا من aa
إلى yz
. يتوقف عندما يصل إلى za
، وهي أول قيمة تصادفه أنها "أكبر من" z
:
php> var_export((boolean)('za' < 'z')) . "\n"; false
في هذه الحالة ، إليك طريقة واحدة للتكرار الصحيح للقيم من "a" إلى "z" في PHP:
for ($i = ord('a'); $i <= ord('z'); $i++) { echo chr($i) . "\n"; }
أو بدلا من ذلك:
$letters = range('a', 'z'); for ($i = 0; $i < count($letters); $i++) { echo $letters[$i] . "\n"; }
الخطأ الشائع التاسع: تجاهل معايير الترميز
على الرغم من أن تجاهل معايير الترميز لا يؤدي بشكل مباشر إلى الحاجة إلى تصحيح أخطاء كود PHP ، إلا أنه لا يزال على الأرجح أحد أهم الأشياء التي يجب مناقشتها هنا.
يمكن أن يتسبب تجاهل معايير الترميز في حدوث عدد كبير من المشكلات في المشروع. في أحسن الأحوال ، ينتج عنه رمز غير متسق (لأن كل مطور "يفعل ما يريده"). ولكن في أسوأ الأحوال ، ينتج رمز PHP لا يعمل أو قد يكون من الصعب (أحيانًا شبه مستحيل) التنقل فيه ، مما يجعل من الصعب للغاية تصحيح الأخطاء وتحسينها وصيانتها. وهذا يعني انخفاض إنتاجية فريقك ، بما في ذلك الكثير من الجهد الضائع (أو على الأقل غير الضروري).
لحسن حظ مطوري PHP ، هناك توصية معايير PHP (PSR) ، وتتألف من المعايير الخمسة التالية:
- PSR-0: معيار التحميل التلقائي
- PSR-1: معيار التشفير الأساسي
- PSR-2: دليل أسلوب الترميز
- PSR-3: واجهة المسجل
- PSR-4: أداة التحميل التلقائي
تم إنشاء PSR في الأصل بناءً على مدخلات من المشرفين على الأنظمة الأساسية الأكثر شهرة في السوق. ساهم كل من Zend و Drupal و Symfony و Joomla وغيرهم في هذه المعايير ، وهم الآن يتبعونها. حتى PEAR ، التي حاولت أن تكون معيارًا لسنوات قبل ذلك ، تشارك في PSR الآن.
بمعنى ما ، لا يهم تقريبًا ماهية معيار الترميز الخاص بك ، طالما أنك توافق على معيار وتلتزم به ، ولكن اتباع PSR يعد فكرة جيدة بشكل عام ما لم يكن لديك سبب مقنع لمشروعك للقيام بخلاف ذلك . المزيد والمزيد من الفرق والمشاريع تتوافق مع PSR. تم التعرف على Tt بالتأكيد في هذه المرحلة على أنها "المعيار" من قبل غالبية مطوري PHP ، لذا فإن استخدامها سيساعد في ضمان أن المطورين الجدد على دراية بمعيار الترميز الخاص بك وارتياحهم عند انضمامهم إلى فريقك.
الخطأ الشائع رقم 10: إساءة استخدام empty()
يفضل بعض مطوري PHP استخدام empty()
لإجراء فحوصات منطقية لكل شيء تقريبًا. ومع ذلك ، هناك حالة يمكن أن يؤدي فيها هذا إلى الارتباك.
أولاً ، دعنا نعود إلى المصفوفات ArrayObject
(التي تحاكي المصفوفات). نظرًا للتشابه بينهما ، من السهل افتراض أن المصفوفات ArrayObject
ستتصرف بشكل متماثل. لكن هذا يثبت أنه افتراض خطير. على سبيل المثال ، في PHP 5.0:
// PHP 5.0 or later: $array = []; var_dump(empty($array)); // outputs bool(true) $array = new ArrayObject(); var_dump(empty($array)); // outputs bool(false) // why don't these both produce the same output?
ولجعل الأمور أسوأ ، كانت النتائج مختلفة قبل إصدار PHP 5.0:
// Prior to PHP 5.0: $array = []; var_dump(empty($array)); // outputs bool(false) $array = new ArrayObject(); var_dump(empty($array)); // outputs bool(false)
هذا النهج هو للأسف شعبية كبيرة. على سبيل المثال ، هذه هي الطريقة التي تُرجع بها Zend\Db\TableGateway
الخاصة بـ Zend Framework 2 البيانات عند استدعاء Current current()
على TableGateway::select()
نتيجة كما يقترح المستند. يمكن أن يصبح المطور بسهولة ضحية لهذا الخطأ مع مثل هذه البيانات.
لتجنب هذه المشكلات ، فإن أفضل طريقة للتحقق من هياكل الصفيف الفارغة هي استخدام count()
:
// Note that this work in ALL versions of PHP (both pre and post 5.0): $array = []; var_dump(count($array)); // outputs int(0) $array = new ArrayObject(); var_dump(count($array)); // outputs int(0)
وبالمناسبة ، نظرًا لأن PHP تلغي القيمة 0
إلى false
، يمكن أيضًا استخدام count()
ضمن شروط if ()
للتحقق من وجود مصفوفات فارغة. تجدر الإشارة أيضًا إلى أنه في PHP ، count()
تعقيدًا ثابتًا (عملية O(1)
) على المصفوفات ، مما يجعل الأمر أكثر وضوحًا أنه الاختيار الصحيح.
مثال آخر عندما يكون empty()
يمكن أن يكون خطيرًا عند دمجه مع وظيفة الفئة السحرية __get()
. دعنا نحدد فئتين ولديهما خاصية test
في كليهما.
أولاً ، دعنا نحدد فئة Regular
تتضمن test
كخاصية عادية:
class Regular { public $test = 'value'; }
ثم دعونا نحدد فئة Magic
التي تستخدم عامل التشغيل __get()
magic للوصول إلى خاصية test
الخاصة بها:
class Magic { private $values = ['test' => 'value']; public function __get($key) { if (isset($this->values[$key])) { return $this->values[$key]; } } }
حسنًا ، دعنا الآن نرى ما يحدث عندما نحاول الوصول إلى خاصية test
لكل من هذه الفئات:
$regular = new Regular(); var_dump($regular->test); // outputs string(4) "value" $magic = new Magic(); var_dump($magic->test); // outputs string(4) "value"
جيد حتى الآن.
لكن الآن دعنا نرى ما يحدث عندما نسميه empty()
في كل من هذه:
var_dump(empty($regular->test)); // outputs bool(false) var_dump(empty($magic->test)); // outputs bool(true)
قرف. لذلك إذا اعتمدنا على empty()
، فيمكن تضليلنا للاعتقاد بأن خاصية test
$magic
فارغة ، بينما في الواقع يتم تعيينها على 'value'
.
لسوء الحظ ، إذا استخدمت فئة الوظيفة __get()
magic لاسترداد قيمة الخاصية ، فلا توجد طريقة مضمونة للتحقق مما إذا كانت قيمة الخاصية فارغة أم لا. خارج نطاق الفئة ، يمكنك فقط التحقق مما إذا كان سيتم إرجاع قيمة null
، وهذا لا يعني بالضرورة أن المفتاح المقابل لم يتم تعيينه ، لأنه كان من الممكن بالفعل تعيينه على قيمة null
.
في المقابل ، إذا حاولنا الإشارة إلى خاصية غير موجودة لمثيل فئة Regular
، فسنحصل على إشعار مشابه لما يلي:
Notice: Undefined property: Regular::$nonExistantTest in /path/to/test.php on line 10 Call Stack: 0.0012 234704 1. {main}() /path/to/test.php:0
لذا فإن النقطة الرئيسية هنا هي أن الطريقة empty()
يجب أن تُستخدم بحذر لأنها يمكن أن تؤدي إلى نتائج مربكة - أو حتى مضللة - إذا لم يكن المرء حريصًا.
يتم إحتوائه
يمكن لسهولة استخدام PHP أن تهدئ من شعور المطورين بالراحة ، مما يجعلهم عرضة لتصحيح أخطاء PHP المطول بسبب بعض الفروق الدقيقة وخصوصيات اللغة. يمكن أن يؤدي هذا إلى عدم عمل PHP ومشكلات مثل تلك الموضحة هنا.
تطورت لغة PHP بشكل ملحوظ على مدار 20 عامًا من تاريخها. يعد التعرف على التفاصيل الدقيقة أمرًا جديرًا بالاهتمام ، حيث سيساعد في ضمان أن يكون البرنامج الذي تنتجه أكثر قابلية للتطوير وقوة وقابلية للصيانة.