اذهب إلى المحتوى

الأخطاء العشرة الأكثر شيوعًا في شيفرة PHP


Ola Abbas

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

الخطأ 1: ترك مراجع References المصفوفة معلّقة بعد انتهاء حلقات foreach

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

$arr = array(1, 2, 3, 4);
foreach ($arr as &$value) {
    $value = $value * 2;
}
// $arr is now array(2, 4, 6, 8)

ولكن إن لم تكن حذرًا، فقد يكون لهذه المراجع بعض الآثار الجانبية والعواقب غير المرغوب فيها، فمثلًا سيبقى المتغير ‎$value ضمن النطاق Scope وسيحتفظ بمرجع إلى العنصر الأخير من المصفوفة بعد تنفيذ الشيفرة البرمجية في المثال السابق، وبالتالي قد تؤدي العمليات اللاحقة المُطبَّقة على المتغير ‎$value إلى تعديل العنصر الأخير في المصفوفة عن غير قصد.

تذكّر أن حلقة foreach لا تنشئ نطاقًا، وبالتالي يُعَد المتغير ‎$value في المثال السابق مرجعًا ضمن النطاق العلوي للسكريبت، حيث تضبط حلقة foreach المرجع للإشارة إلى العنصر التالي من المصفوفة ‎$arr في كل تكرار، ويبقى المتغير ‎$value يؤشّر إلى العنصر الأخير من المصفوفة ‎$arr، ويبقى ضمن النطاق بعد اكتمال الحلقة.

إليك فيما يلي مثال عن نوع الأخطاء المربكة التي يمكن أن تؤدي إلى هذه المشكلة:

$array = [1, 2, 3];
echo implode(',', $array), "\n";

foreach ($array as &$value) {}    // من خلال المرجع
echo implode(',', $array), "\n";

foreach ($array as $value) {}     // من خلال القيمة (أي النسخة‫)
echo implode(',', $array), "\n";

وسيكون خرج الشيفرة البرمجية السابقة ما يلي:

1,2,3
1,2,3
1,2,2

هذا ليس خطأ مطبعيًا، فالقيمة الأخيرة في السطر الأخير هي 2 وليست 3، والسبب هو بقاء المصفوفة ‎$array دون تغيير بعد المرور على حلقة foreach الأولى، ولكن يُترَك المتغير ‎$value بوصفه مرجعًا مُعلَّقًا للعنصر الأخير في المصفوفة ‎$array لأن حلقة foreach وصلت إلى المتغير ‎$value من خلال المرجع، لذا ستحدث أشياء غريبة عندما نمر على حلقة foreach الثانية، حيث تنسخ حلقة 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 مرجعًا إلى العنصر ‎$arr[3]‎

الخطأ 2: سوء فهم سلوك الدالة isset()‎

تعيد الدالة isset()‎ القيمة false في حالة عدم وجود عنصر، وتعيد أيضًا false للقيم null، حيث يُسبّب هذا السلوك مشكلات، فليكن لدينا المثال التالي:

$data = fetchRecordFromStorage($storage, $identifier);
if (!isset($data['keyShouldBeSet']) {
    // افعل شيئًا هنا عند عدم ضبط‫ 'keyShouldBeSet'
}

يُفترَض أننا نريد التحقق ممّا إذا كان keyShouldBeSet مضبوطًا في ‎$data، ولكن ستعيد الدالة isset($data['keyShouldBeSet'])‎ أيضًا القيمة false حتى عند ضبط ‎$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']‎ تعيد القيمة true، فسيكون الاعتماد على الدالة isset()‎ لذلك قرارًا برمجيًا سيئًا على أيّة حال، لذا يُفضَّل إعادة التحقق من ‎$_POST['active']‎ بدلًا من ذلك كما يلي:

if ($_POST['active']) {
    $postData = extractSomething($_POST);
}

// ...

if ($_POST['active']) {
    echo 'post not active';
}

يُعَد استخدام التابع ‎array_key_exists()‎ حلًا أكثر قوة بالنسبة للحالات التي يكون فيها من المهم التحقق مما إذا كان المتغير مضبوطًا فعليًا، أو للتمييز بين متغير غير مضبوط ومتغير مضبوط على القيمة null، فمثلًا يمكننا إعادة كتابة المثال الأول من المثالين السابقين كما يلي:

$data = fetchRecordFromStorage($storage, $identifier);
if (! array_key_exists('keyShouldBeSet', $data)) {
    // افعل ذلك عند عدم ضبط‫ 'keyShouldBeSet'
}

إذا جمعنا بين التابعين array_key_exists() و get_defined_vars()‎، فيمكننا التحقق بطريقة موثوقة من ضبط متغير ضمن النطاق الحالي كما يلي:

if (array_key_exists('varShouldBeSet', get_defined_vars())) {
    // ‫المتغير ‎$varShouldBeSet موجود في النطاق الحالي
}

الخطأ 3: الخلط بين الإعادة باستخدام المرجع وإعادتها باستخدام القيمة

ليكن لدينا مقطع الشيفرة البرمجية التالي:

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()‎ نسخة من المصفوفة ‎$values، مما يؤدي إلى إضافة عنصر 'test'
// إلى نسخة من المصفوفة‫ ‎$values، وليس إلى المصفوفة ‎$values نفسها
$config->getValues()['test'] = 'test';

// تعيد الدالة‫ getValues()‎ مرة أخرى نسخة أخرى من المصفوفة ‎$values، ولا تحتوي هذه النسخة
// ‫على عنصر 'test'، ولذلك نحصل على رسالة فهرس غير مُعرَّف "undefined index"
echo $config->getValues()['test'];

أحد الحلول الممكنة لهذه المشكلة هو حفظ النسخة الأولى من المصفوفة ‎$values التي تعيدها الدالة getValues()‎، ثم العمل على تلك النسخة لاحقًا كما في المثال التالي:

$vals = $config->getValues();
$vals['test'] = 'test';
echo $vals['test'];

ستعمل هذه الشيفرة البرمجية بنجاح، أي أنها ستعطي test دون إنشاء أيّ رسالة فهرس غير مُعرَّف "undefined index"، ولكن قد يكون هذا الأسلوب مناسبًا أو غير مناسب اعتمادًا على ما تحاول تحقيقه، إذ لن تعدّل هذه الشيفرة البرمجية المصفوفة ‎$values الأصلية، لذا إذا أردتَ أن تؤثر تعديلاتك (مثل إضافة عنصر test) على المصفوفة الأصلية، فيجب أن تعدّل الدالة getValues()‎ لإعادة مرجع إلى المصفوفة ‎$values نفسها. يمكن تحقيق ذلك من خلال إضافة & قبل اسم الدالة، مما يشير إلى أنها يجب أن تعيد مرجعًا كما يلي:

class Config
{
    private $values = [];

    // إعادة مرجع إلى المصفوفة‫ ‎$values الفعلية
    public function &getValues() {
        return $this->values;
    }
}

$config = new Config();

$config->getValues()['test'] = 'test';
echo $config->getValues()['test'];

وسيكون الخرج test كما هو متوقع.

لنجعل الآن الأمور أكثر إرباكًا كما في مقتطف الشيفرة البرمجية التالي:

class Config
{
    private $values;

    // استخدام كائن مصفوفة‫ ArrayObject بدلًا من المصفوفة
    public function __construct() {
        $this->values = new ArrayObject();
    }

    public function getValues() {
        return $this->values;
    }
}

$config = new Config();

$config->getValues()['test'] = 'test';
echo $config->getValues()['test'];

إذا اعتقدتَ أن هذه الشيفرة البرمجية ستؤدي إلى خطأ "undefined index" نفسه الموجود في مثال المصفوفة array السابق، فأنت مخطئ، إذ ستعمل هذه الشيفرة البرمجية بنجاح، والسبب هو أن لغة PHP تمرّر الكائنات من خلال المرجع دائمًا على عكس المصفوفات، فالكائن ArrayObject هو كائن من مكتبة PHP المعيارية -أو كائن SPL- يحاكي استخدام المصفوفات، ولكنه يعمل بوصفه كائنًا.

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

يجب أيضًا تجنب ممارسة إعادة مرجع إلى مصفوفة أو كائن ArrayObject، لأنه يوفر للمستدعِي القدرة على تعديل البيانات الخاصة للنسخة، وهذا يناقض مفهوم التغليف Encapsulation، لذا يُفضَّل استخدام النمط القديم للجوالب Getters والضوابط Setters كما في المثال التالي:

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 الخاصة نفسها.

الخطأ 4: إجراء الاستعلامات ضمن حلقة

يمكن أن تصادف شيئًا كما يلي إذا كانت شيفرة 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 استعلام منفصل للمورد، وإذا اُستدعِي مثل هذا السكربت ضمن خيوط Threads متعددة، فيُحتمَل أن يؤدي ذلك إلى توقف النظام بأكمله، لذا يجب معرفة متى تُجري شيفرتك البرمجية الاستعلامات وجمع القيم ثم تشغيل استعلام واحد لجلب جميع النتائج كلما أمكن ذلك.

أحد الأمثلة على الأماكن الشائعة إلى حدٍ ما التي ترى فيها إجراء الاستعلام بطريقة غير فعالة (أي ضمن حلقة) هو عند نشر استمارة مع قائمة من القيم (المعرّفات مثلًا)، ثم ستمر الشيفرة البرمجية ضمن حلقة على المصفوفة وتجري استعلام 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: تزييف استخدام الذاكرة وعدم كفاءتها

يكون جلب العديد من السجلات في وقت واحد أكثر كفاءة من تشغيل استعلام واحد لجلب كل صف، ولكن يمكن أن يؤدي هذا النهج إلى حالة "نفاد الذاكرة Out of Memory" في مكتبة libmysqlclient عند استخدام إضافة PHP التي هي mysql. ليكن لدينا مثلًا مربع اختبار مع موارد محدودة (512 ميجابايت من ذاكرة RAM) وقاعدة بيانات MySQL وواجهة سطر الأوامر php-cli، حيث سنهيّئ جدول قاعدة بيانات كما يلي:

// الاتصال بقاعدة بيانات‫ mysql
$connection = new mysqli('localhost', 'username', 'password', 'database');

// إنشاء جدول مؤلّف من 400 عمود
$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);

// كتابة 2 مليون صف
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);
}

لنتحقّق الآن من استخدام الموارد كما يلي:

// الاتصال بقاعدة بيانات‫ 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 بدلًا من ذلك، بالرغم من أنها لن تؤدي إلى تحسين استخدام الذاكرة، حيث تُصرَّف Compiled هذه الوحدة بوصفها إضافةً أصيلة Native للغة PHP وتستخدم مدير ذاكرة PHP، لذلك إذا أجرينا الاختبار السابق باستخدام الوحدة mysqlnd بدلًا من الوحدة mysql، فسنحصل على صورة أكثر واقعية لاستخدام ذاكرتنا.

Before: 232048
Limit 1: 324952
Limit 10000: 32572912

تستخدم الوحدة mysql ضعف عدد الموارد التي تستخدمها الوحدة mysqlnd لتخزين البيانات وفقًا لتوثيق PHP، لذا يستخدم السكربت الأصلي الذي يستخدم وحدة 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");
}

إذا أمعنا النظر في هذا الخطأ والخطأ رقم 4، فإننا سندرك أن هناك توازنًا صحيًا تحتاج شيفرتك البرمجية إلى تحقيقه بطريقة مثالية، حيث يجب إما أن تكون استعلاماتك صغيرة ومتكررة جدًا من ناحية، أو أن يكون كل استعلام فردي كبيرًا جدًا من ناحية أخرى، لذا يجب إيجاد توازن بينهما، إذ يمكن أن يسبّب استخدام أيٍّ منهما إلى الحد الأقصى مشكلات مع شيفرة PHP البرمجية التي لن تعمل بطريقة صحيحة.

الخطأ 6: تجاهل مشكلات الترميز Unicode/UTF-8

تُعَد هذه المشكلة خاصة بلغة PHP بحد ذاتها أكثر من كونها مشكلة قد تظهر أثناء تنقيح أخطاء شيفرة PHP، ولكن هذه المشكلة لم تُعالَج بطريقة كافية، إذ كان من المقرر جعل نواة PHP 6 متوافقةً مع الترميز الموحّد Unicode، وجرى تعليق ذلك عند تعليق تطوير PHP 6 في عام 2010، ولكن ذلك لا يعفي المطور بأيّ حال من الأحوال من تقديم ترميز UTF-8 بطريقة صحيحة وتجنب الافتراض الخاطئ بأن جميع السلاسل النصية ستكون "نصًا عاديًا قديمًا يستخدم معيار ASCII". تشتهر الشيفرة البرمجية التي تفشل في معالجة السلاسل النصية التي ليست ASCII بإدخال أخطاء هايزنبغ Heisenbug المعقدة في شيفرتك البرمجية، إذ قد تسبّب حتى استدعاءات strlen($_POST['name'])‎ البسيطة مشكلات إذا حاول شخص يحمل اسم عائلة مثل "Schrödinger" التسجيل في نظامك.

إليك فيما يلي قائمة صغيرة لتجنب مثل هذه المشكلات في شيفرتك البرمجية:

  • إذا كنت لا تعرف الكثير عن ترميز Unicode و UTF-8، فيجب أن تتعلّم الأساسيات على الأقل.
  • تأكد من استخدام دوال mb_*‎ دائمًا بدلًا من دوال السلاسل النصية القديمة، وتأكد من تضمين إضافة "تعدد البايتات multibyte" في إصدار PHP الخاص بك.
  • تأكد من ضبط قاعدة البيانات والجداول الخاصة بك لاستخدام ترميز Unicode، إذ لا تزال العديد من إصدارات MySQL تستخدم معيار latin1 افتراضيًا.
  • تذكّر أن الدالة json_encode()‎ تحوّل الرموز التي ليست رموز ASCII، حيث تحوّل السلسلة النصية "Schrödinger" إلى "Schr\u00f6dinger" مثلًا، ولكن التابع serialize()‎ لا يفعل ذلك.
  • تأكّد من أن ملفات شيفرة PHP البرمجية الخاصة بك مشفَّرة أيضًا باستخدام ترميز UTF-8 لتجنب التعارض عند ضم السلاسل النصية مع ثوابت السلاسل النصية المكتوبة أو المضبوطة.

اطّلع على مقال معالجة الملفات والبيانات المرمزة بترميز UTF-8 في PHP لمزيدٍ من المعلومات.

الخطأ 7: افتراض أن المصفوفة ‎$_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، لذا لا تحمّل لغة PHP حِمل طلب POST تلقائيًا مع أيّ نوع محتوى آخر حتى الأنواع التي تحظى بشعبية كبيرة اليوم مثل application/json.

تُعَد المصفوفة ‎$_POST ذات نطاق عام عالي Superglobal، لذا إذا عدّلناها مرة واحدة (ويُفضَّل أن يكون ذلك في وقت مبكر من السكربت)، فستكون القيمة المُعدَّلة (بما في ذلك حِمل طلب POST) قابلةً للإشارة إليها من شيفرتنا البرمجية. يُعَد ذلك أمرًا مهمًا، لأن أطر عمل PHP وجميع السكربتات المخصَّصة تقريبًا تستخدم المصفوفة ‎$_POST استخدامًا شائعًا لاستخراج بيانات الطلب وتحويلها.

نحتاج إلى تحليل محتويات الطلب يدويًا (أي فك تشفير بيانات JSON) وتعديل المتغير ‎$_POST عند معالجة حِمل طلب POST مع نوع المحتوى application/json مثلًا كما يلي:

// php
$_POST = json_decode(file_get_contents('php://input'), true);

إذا فرّغنا معلومات محتوى المصفوفة ‎$_POST بعد ذلك، فسنرى أنها تتضمّن حِمل طلب POST الصحيح كما في المثال التالي:

array(2) { ["a"]=> string(1) "a" ["b"]=> string(1) "b" }

الخطأ الشائع 8: الاعتقاد بأن لغة PHP تدعم نوع بيانات المحارف Character

اطّلع على الشيفرة البرمجية التالية وحاول تخمين ما سينتج:

for ($c = 'a'; $c <= 'z'; $c++) {
    echo $c . "\n";
}

إذا أجبت أنها ستطبع المحارف من 'a' إلى 'z'، فقد تتفاجأ عندما تعرف أنك مخطئ، حيث أنها ستطبع المحارف من 'a' إلى 'z'، ولكنها ستطبع أيضًا 'aa' إلى 'yz'، إذ لا يوجد نوع البيانات char في لغة PHP، بينما يوجد نوع بيانات السلاسل النصية 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";
}

الخطأ 9: تجاهل قواعد ومعايير كتابة الشيفرة البرمجية

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

توجد توصية لمعايير PHP أو PHP Standards Recommendation -أو PSR اختصارًا، والتي تتألف من المعايير الخمسة التالية:

  • PSR-0: معيار التحميل التلقائي.
  • PSR-1: معيار كتابة الشيفرة البرمجية الأساسي.
  • PSR-2: دليل تنسيق كتابة الشيفرة البرمجية.
  • PSR-3: واجهة المسجِّل.
  • PSR-4: المحمِّل التلقائي.

لقد أُنشِئت معايير PSR بناءً على مدخلات من المشرفين على المنصات الأشهر في السوق لتوفر إرشادات وممارسات موحدة لتنظيم كتابة الأكواد في PHP. وقد وُضعت هذه المعايير من قبل مجموعة من المطورين والمشرفين على أبرز المنصات وأطر العمل الشهيرة مثل Zend و Drupal و Symfony و Joomla وغيرها في هذه المعايير، وتتبع هذه المنصات تلك المعايير، ويشارك الآن إطار عمل PEAR الذي حاول أن يكون معيارًا لسنوات قبل ذلك في وضع معايير PSR.

لا يهم تقريبًا ما هو معيار كتابة الشيفرة البرمجية الخاص بك طالما أنك توافق على المعيار وتلتزم به، ولكن يُعَد اتباع معايير PSR فكرة جيدة إن لم يكن لديك سبب مقنع في مشروعك يخالفها، إذ أصبحت هناك العديد من الفرق والمشاريع التي تتوافق مع معايير PSR التي يُعِدّها أغلب مطوري PHP بوصفها المعيار الحالي، لذا سيساعد استخدامها في ضمان معرفة المطورين الجدد بمعيار كتابة الشيفرة البرمجية الخاص بك وسيشعرون بالارتياح عند انضمامهم إلى فريقك.

الخطأ 10: استخدام الدالة empty()‎ استخدامًا خاطئًا

يفضِّل بعض مطوري PHP استخدام الدالة empty() لإجراء عمليات فحص منطقية لكل شيء تقريبًا، ولكن توجد حالات يمكن أن يؤدي فيها ذلك إلى الارتباك.

أولًا، إذا عُدنا إلى المصفوفات ونُسخ كائن المصفوفة ArrayObject (الذي يحاكي المصفوفات)، فمن السهل افتراض أن المصفوفات ونسخ الكائن ArrayObject ستتصرف بطريقة مماثلة بسبب تشابههما، ولكنه افتراض خطير كما هو موضّح فيما يلي في الإصدار PHP 5.0:

// ‫PHP 5.0 أو بعد‫:
$array = [];
var_dump(empty($array));        // ‫خرجها bool(true)‎ 
$array = new ArrayObject();
var_dump(empty($array));        // ‫خرجها bool(false)‎
// لماذا لا ينتج كلاهما الخرج نفسه؟

وستكون النتائج مختلفة في الإصدارات التي قبل الإصدار PHP 5.0:

//  الإصدارات التي قبل الإصدار‫ PHP 5.0:
$array = [];
var_dump(empty($array));        // ‫خرجها bool(false)‎ 
$array = new ArrayObject();
var_dump(empty($array));        // ‫خرجها bool(false)‎

تحظى هذه الطريقة بشعبية كبيرة لسوء الحظ، فمثلًا هذه هي الطريقة التي يعيد بها كائن Zend\Db\TableGateway في إطار عمل Zend Framework 2 البيانات عند استدعاء الدالة current()‎ مع نتيجة الدالة TableGateway::select()‎، ويمكن للمطور أن يصبح بسهولة ضحيةً لهذا الخطأ مع مثل هذه البيانات.

يمكن تجنب هذه المشكلات من خلال التحقق من وجود بنى مصفوفات فارغة باستخدام الدالة count()‎ كما يلي:

// ‫لاحظ أن ما يلي يعمل في جميع إصدارات PHP (سواء قبل أو بعد الإصدار 5.0):
$array = [];
var_dump(count($array));        // ‫خرجها int(0)‎
$array = new ArrayObject();
var_dump(count($array));        // ‫خرجها int(0)‎

تحوّل لغة PHP القيمة 0 إلى false، فيمكن أيضًا استخدام الدالة count()‎ ضمن شروط تعليمة if للتحقق من المصفوفات الفارغة. تجدر الإشارة أيضًا إلى أن الدالة ‎count()‎‎ في PHP لها تعقيد ثابت Constant Complexity (أو O(1)‎) على المصفوفات، مما يوضّح أنه الاختيار الصحيح.

يوجد مثال آخر لذلك عندما تمثّل الدالة empty()‎ خطرًا من خلال دمجها مع دالة الأصناف السحرية ‎__get()‎. لنعرّف مثلًا صنفين Class مع وجود الخاصية test في كليهما، حيث نعرّف أولًا الصنف Regular الذي يتضمن الخاصية العادية test كما يلي:

class Regular
{
    public $test = 'value';
}

نعرّف بعد ذلك الصنف Magic الذي يستخدم التابع السحري ‎__get()‎ للوصول إلى الخاصية 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);    // ‫خرجها string(4) "value"‎
$magic = new Magic();
var_dump($magic->test);      // ‫خرجها string(4) "value"‎

يبدو كل شيء على ما يرام حتى الآن، ولكن لنرى الآن ما يحدث عندما نستدعي الدالة empty()‎ مع كل منهما كما يلي:

var_dump(empty($regular->test));    // ‫خرجها bool(false)‎
var_dump(empty($magic->test));      // خرجها‫ bool(true)‎

إذا اعتمدنا على الدالة empty()‎، فيمكن تضليلنا للاعتقاد بأن الخاصية test الخاصة بالصنف ‎$magic فارغة، بينما هي مضبوطة على القيمة 'value'.

لسوء الحظ، إذا استخدم الصنفُ التابع السحري ‎__get()‎ لاسترداد قيمة خاصيةٍ ما، فلا توجد طريقة مضمونة للتحقق مما إذا كانت قيمة الخاصية فارغة أم لا. يمكنك خارج نطاق الصنف التحقق فقط من إعادة القيمة 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 تطورًا ملحوظًا على مدار تاريخها الطويل وشهدت تحسينات ومميزات عديدة، لذا يُعَد التعرف على تفاصيلها الدقيقة أمرًا جديرًا بالاهتمام، لأنها ستساعد على ضمان أن البرنامج الذي تنتجه أكثر قابلية للتوسع والقوة والصيانة.

ترجمة -وبتصرُّف- للمقال Buggy PHP Code: The 10 Most Common Mistakes PHP Developers Make لصاحبه Ilya Sanosian.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...